├── .gitattributes
├── .github
└── stale.yml
├── .gitignore
├── .hhconfig
├── .travis.sh
├── .travis.yml
├── LICENSE
├── README.md
├── SECURITY.md
├── bin
└── format
├── composer.json
├── composer.lock
├── hh_autoload.json
├── hhast-lint.json
├── src
└── Nuxed
│ ├── Asset
│ ├── Context
│ │ ├── Context.hack
│ │ ├── IContext.hack
│ │ └── NullContext.hack
│ ├── Exception
│ │ ├── IException.hack
│ │ ├── InvalidArgumentException.hack
│ │ ├── LogicException.hack
│ │ └── RuntimeException.hack
│ ├── IPackage.hack
│ ├── Package.hack
│ ├── Packages.hack
│ ├── PathPackage.hack
│ ├── UrlPackage.hack
│ └── VersionStrategy
│ │ ├── EmptyVersionStrategy.hack
│ │ ├── IVersionStrategy.hack
│ │ ├── JsonManifestVersionStrategy.hack
│ │ └── StaticVersionStrategy.hack
│ ├── Cache
│ ├── Cache.hack
│ ├── Exception
│ │ ├── CacheException.hack
│ │ ├── IException.hack
│ │ ├── InvalidArgumentException.hack
│ │ └── LogicException.hack
│ ├── ICache.hack
│ ├── Serializer
│ │ ├── ISerializer.hack
│ │ ├── JsonSerializer.hack
│ │ └── NativeSerializer.hack
│ ├── Store
│ │ ├── AbstractStore.hack
│ │ ├── ApcStore.hack
│ │ ├── ArrayStore.hack
│ │ ├── FilesystemStore.hack
│ │ ├── IStore.hack
│ │ ├── MCRouterStore.hack
│ │ ├── NullStore.hack
│ │ └── RedisStore.hack
│ └── private.hack
│ ├── Container
│ ├── ContainerBuilder.hack
│ ├── Exception
│ │ ├── ContainerException.hack
│ │ ├── IException.hack
│ │ └── NotFoundException.hack
│ ├── IFactory.hack
│ ├── IInflector.hack
│ ├── IServiceContainerAware.hack
│ ├── IServiceProvider.hack
│ ├── ReflectionServiceContainer.hack
│ ├── Service
│ │ ├── CallableFactoryDecorator.hack
│ │ ├── CallableInflectorDecorator.hack
│ │ ├── Newable.hack
│ │ └── NewableFactoryDecorator.hack
│ ├── ServiceContainer.hack
│ ├── ServiceContainerAwareTrait.hack
│ ├── ServiceDefinition.hack
│ └── functions.hack
│ ├── Contract
│ └── IReset.hack
│ ├── Environment
│ ├── Exception
│ │ ├── IException.hack
│ │ └── InvalidArgumentException.hack
│ ├── _Private
│ │ ├── Parser.hack
│ │ └── State.hack
│ ├── add.hack
│ ├── contains.hack
│ ├── forget.hack
│ ├── get.hack
│ ├── is_debug.hack
│ ├── is_dev.hack
│ ├── load.hack
│ ├── parse.hack
│ └── put.hack
│ ├── EventDispatcher
│ ├── CallableEventListener.hack
│ ├── ErrorEvent.hack
│ ├── EventDispatcher.hack
│ ├── Exception
│ │ ├── IException.hack
│ │ └── InvalidListenerException.hack
│ ├── IEvent.hack
│ ├── IEventDispatcher.hack
│ ├── IEventListener.hack
│ ├── IStoppableEvent.hack
│ ├── ListenerProvider
│ │ ├── AttachableListenerProvider.hack
│ │ ├── IAttachableListenerProvider.hack
│ │ ├── IListenerProvider.hack
│ │ ├── IPrioritizedListenerProvider.hack
│ │ ├── IRandomizedListenerProvider.hack
│ │ ├── IReifiedListenerProvider.hack
│ │ ├── ListenerProviderAggregate.hack
│ │ ├── PrioritizedListenerProvider.hack
│ │ ├── RandomizedListenerProvider.hack
│ │ └── ReifiedListenerProvider.hack
│ └── functional.hack
│ ├── Filesystem
│ ├── Exception
│ │ ├── ExistingNodeException.hack
│ │ ├── IException.hack
│ │ ├── InvalidArgumentException.hack
│ │ ├── InvalidPathException.hack
│ │ ├── MissingNodeException.hack
│ │ ├── OutOfRangeException.hack
│ │ ├── ReadErrorException.hack
│ │ ├── RuntimeException.hack
│ │ ├── UnreadableNodeException.hack
│ │ ├── UnwritableNodeException.hack
│ │ └── WriteErrorException.hack
│ ├── File.hack
│ ├── Folder.hack
│ ├── Lines.hack
│ ├── Node.hack
│ ├── OperationType.hack
│ └── Path.hack
│ ├── Http
│ ├── Client
│ │ ├── CurlHttpClient.hack
│ │ ├── Exception
│ │ │ ├── IException.hack
│ │ │ ├── InvalidArgumentException.hack
│ │ │ ├── NetworkException.hack
│ │ │ └── RequestException.hack
│ │ ├── HttpClient.hack
│ │ ├── HttpClientOptions.hack
│ │ ├── IHttpClient.hack
│ │ ├── MockHttpClient.hack
│ │ └── _Private
│ │ │ └── Structure.hack
│ ├── Emitter
│ │ ├── Emitter.hack
│ │ ├── Exception
│ │ │ ├── EmitterException.hack
│ │ │ └── IException.hack
│ │ ├── IEmitter.hack
│ │ ├── SapiEmitter.hack
│ │ └── SapiStreamEmitter.hack
│ ├── Error
│ │ ├── ErrorHandler.hack
│ │ ├── IErrorHandler.hack
│ │ └── Middleware
│ │ │ └── ErrorMiddleware.hack
│ ├── Flash
│ │ ├── Exception
│ │ │ ├── IException.hack
│ │ │ └── InvalidHopsValueException.hack
│ │ ├── FlashMessages.hack
│ │ └── FlashMessagesMiddleware.hack
│ ├── Message
│ │ ├── Cookie.hack
│ │ ├── CookieSameSite.hack
│ │ ├── Exception
│ │ │ ├── ConflictingHeadersException.hack
│ │ │ ├── IException.hack
│ │ │ ├── InvalidArgumentException.hack
│ │ │ ├── RuntimeException.hack
│ │ │ ├── SuspiciousOperationException.hack
│ │ │ ├── UnreadableStreamException.hack
│ │ │ ├── UnrecognizedProtocolVersionException.hack
│ │ │ ├── UnseekableStreamException.hack
│ │ │ ├── UntellableStreamException.hack
│ │ │ ├── UnwritableStreamException.hack
│ │ │ ├── UploadedFileAlreadyMovedException.hack
│ │ │ └── UploadedFileErrorException.hack
│ │ ├── IStream.hack
│ │ ├── IpUtils.hack
│ │ ├── MessageTrait.hack
│ │ ├── Request.hack
│ │ ├── Request
│ │ │ └── functions.hack
│ │ ├── RequestMethod.hack
│ │ ├── RequestTrait.hack
│ │ ├── Response.hack
│ │ ├── Response
│ │ │ ├── JsonResponse.hack
│ │ │ └── functions.hack
│ │ ├── ServerRequest.hack
│ │ ├── StatusCode.hack
│ │ ├── Stream.hack
│ │ ├── Stream
│ │ │ └── functions.hack
│ │ ├── StreamSeekWhence.hack
│ │ ├── UploadedFile.hack
│ │ ├── UploadedFileError.hack
│ │ ├── Uri.hack
│ │ ├── _Private
│ │ │ ├── HeadersMarshaler.hack
│ │ │ ├── ProtocolVersionMarshaler.hack
│ │ │ ├── UriMarshaler.hack
│ │ │ ├── create_server_request_from_globals.hack
│ │ │ └── inject_content_type_in_headers.hack
│ │ └── functions.hack
│ ├── Router
│ │ ├── Exception
│ │ │ ├── DuplicateRouteException.hack
│ │ │ ├── IException.hack
│ │ │ ├── InvalidArgumentException.hack
│ │ │ └── RuntimeException.hack
│ │ ├── Generator
│ │ │ ├── IUriGenerator.hack
│ │ │ └── UriGenerator.hack
│ │ ├── IRouteCollector.hack
│ │ ├── IRouter.hack
│ │ ├── Matcher
│ │ │ ├── IRequestMatcher.hack
│ │ │ └── RequestMatcher.hack
│ │ ├── Middleware
│ │ │ ├── DispatchMiddleware.hack
│ │ │ ├── ImplicitHeadMiddleware.hack
│ │ │ ├── ImplicitOptionsMiddleware.hack
│ │ │ ├── MethodNotAllowedMiddleware.hack
│ │ │ └── RouteMiddleware.hack
│ │ ├── Route.hack
│ │ ├── RouteCollector.hack
│ │ ├── RouteCollectorTrait.hack
│ │ ├── RouteResult.hack
│ │ ├── Router.hack
│ │ └── _Private
│ │ │ ├── Ref.hack
│ │ │ └── map.hack
│ ├── Server
│ │ ├── Exception
│ │ │ ├── EmptyStackException.hack
│ │ │ ├── IException.hack
│ │ │ ├── InvalidMiddlewareException.hack
│ │ │ ├── RuntimeException.hack
│ │ │ └── ServerException.hack
│ │ ├── Handler
│ │ │ ├── CallableHandlerDecorator.hack
│ │ │ ├── NextMiddlewareHandler.hack
│ │ │ └── NotFoundHandler.hack
│ │ ├── HandlerMiddlewareTrait.hack
│ │ ├── IHandler.hack
│ │ ├── IMiddleware.hack
│ │ ├── IMiddlewareStack.hack
│ │ ├── Middleware
│ │ │ ├── CallableMiddlewareDecorator.hack
│ │ │ ├── HostMiddlewareDecorator.hack
│ │ │ ├── OriginalMessagesMiddleware.hack
│ │ │ ├── PathMiddlewareDecorator.hack
│ │ │ └── RequestHandlerMiddlewareDecorator.hack
│ │ ├── MiddlewareStack.hack
│ │ ├── helpers.hack
│ │ └── types.hack
│ └── Session
│ │ ├── CacheLimiter.hack
│ │ ├── Exception
│ │ ├── IException.hack
│ │ └── InvalidArgumentException.hack
│ │ ├── Persistence
│ │ ├── AbstractSessionPersistence.hack
│ │ ├── CacheSessionPersistence.hack
│ │ └── ISessionPersistence.hack
│ │ ├── Session.hack
│ │ └── SessionMiddleware.hack
│ ├── Jwt
│ ├── Builder.hack
│ ├── Exception
│ │ ├── IException.hack
│ │ ├── InvalidArgumentException.hack
│ │ └── RuntimeException.hack
│ ├── IBuilder.hack
│ ├── IParser.hack
│ ├── ISigner.hack
│ ├── IToken.hack
│ ├── Parser.hack
│ ├── Signer
│ │ ├── Ecdsa.hack
│ │ ├── Ecdsa
│ │ │ ├── ISignatureConverter.hack
│ │ │ ├── MultibyteStringConverter.hack
│ │ │ ├── Sha256.hack
│ │ │ ├── Sha384.hack
│ │ │ └── Sha512.hack
│ │ ├── Hmac.hack
│ │ ├── Hmac
│ │ │ ├── Sha256.hack
│ │ │ ├── Sha384.hack
│ │ │ └── Sha512.hack
│ │ ├── Key.hack
│ │ ├── None.hack
│ │ ├── OpenSSL.hack
│ │ ├── Rsa.hack
│ │ └── Rsa
│ │ │ ├── Sha256.hack
│ │ │ ├── Sha384.hack
│ │ │ └── Sha512.hack
│ ├── Token.hack
│ └── Token
│ │ ├── Claims.hack
│ │ ├── Headers.hack
│ │ └── Signature.hack
│ ├── Log
│ ├── AbstractLogger.hack
│ ├── BufferingLogger.hack
│ ├── Exception
│ │ ├── IException.hack
│ │ ├── InvalidArgumentException.hack
│ │ ├── LogicException.hack
│ │ └── UnexpectedValueException.hack
│ ├── Formatter
│ │ ├── IFormatter.hack
│ │ └── LineFormatter.hack
│ ├── Handler
│ │ ├── AbstractHandler.hack
│ │ ├── FormattableHandlerTrait.hack
│ │ ├── IFormattableHandler.hack
│ │ ├── IHandler.hack
│ │ ├── RotatingFileHandler.hack
│ │ ├── StreamHandler.hack
│ │ ├── SysLogFacility.hack
│ │ └── SysLogHandler.hack
│ ├── ILogger.hack
│ ├── ILoggerAware.hack
│ ├── LogLevel.hack
│ ├── Logger.hack
│ ├── LoggerAwareTrait.hack
│ ├── LoggerTrait.hack
│ ├── NullLogger.hack
│ ├── Processor
│ │ ├── CallableProcessor.hack
│ │ ├── ContextProcessor.hack
│ │ ├── IProcessor.hack
│ │ └── MessageLengthProcessor.hack
│ └── Record.hack
│ ├── Markdown
│ ├── Environment.hack
│ ├── Extension
│ │ ├── AbstractExtension.hack
│ │ └── IExtension.hack
│ └── XHPElement.hack
│ ├── Mercure
│ ├── Exception
│ │ ├── IException.hack
│ │ └── InvalidArgumentException.hack
│ ├── IJwtProvider.hack
│ ├── Provider
│ │ └── StaticJwtProvider.hack
│ ├── Publisher.hack
│ └── Update.hack
│ ├── Stopwatch
│ ├── Event.hack
│ ├── Exception
│ │ ├── IException.hack
│ │ └── LogicException.hack
│ ├── Period.hack
│ ├── Section.hack
│ └── Stopwatch.hack
│ ├── Translation
│ ├── Catalogue
│ │ ├── AbstractOperation.hack
│ │ ├── IOperation.hack
│ │ ├── MergeOperation.hack
│ │ └── TargetOperation.hack
│ ├── Exception
│ │ ├── IException.hack
│ │ ├── InvalidArgumentException.hack
│ │ ├── InvalidResourceException.hack
│ │ ├── LogicException.hack
│ │ ├── NotFoundResourceException.hack
│ │ └── RuntimeException.hack
│ ├── Format.hack
│ ├── Formatter
│ │ ├── IMessageFormatter.hack
│ │ └── MessageFormatter.hack
│ ├── ILocaleAware.hack
│ ├── ITranslator.hack
│ ├── ITranslatorBag.hack
│ ├── Loader
│ │ ├── FileLoader.hack
│ │ ├── ILoader.hack
│ │ ├── IniFileLoader.hack
│ │ ├── JsonFileLoader.hack
│ │ └── TreeLoader.hack
│ ├── LoggingTranslator.hack
│ ├── MessageCatalogue.hack
│ ├── Reader
│ │ ├── ITranslationReader.hack
│ │ └── TranslationReader.hack
│ ├── Translator.hack
│ └── _Private
│ │ ├── LoaderContainer.hack
│ │ └── Parents.hack
│ └── Util
│ ├── Exception
│ └── IException.hack
│ ├── Inflector.hack
│ ├── Json
│ ├── Errors.hack
│ ├── Exception
│ │ ├── JsonDecodeException.hack
│ │ └── JsonEncodeException.hack
│ ├── decode.hack
│ ├── encode.hack
│ ├── spec.hack
│ └── structure.hack
│ ├── Jsonable.hack
│ ├── Stringable.hack
│ ├── StringableTrait.hack
│ ├── alternatives.hack
│ └── stringify.hack
└── tests
└── Nuxed
├── Asset
├── PackageTest.hack
├── PackagesTest.hack
├── PathPackageTest.hack
└── UrlPackageTest.hack
├── Container
├── ContainerBuilderTest.hack
├── ServiceContainerTest.hack
└── ServiceDefinitionTest.hack
├── EventDispatcher
├── EventDispatcherTest.hack
├── Fixture
│ ├── OrderCanceledEvent.hack
│ ├── OrderCanceledEventListener.hack
│ ├── OrderCreatedEvent.hack
│ └── OrderCreatedEventListener.hack
└── ListenerProvider
│ ├── AttachableListenerProviderTest.hack
│ ├── ListenerProviderAggregateTest.hack
│ ├── PrioritizedListenerProviderTest.hack
│ ├── RandomizedListenerProviderTest.hack
│ └── ReifiedListenerProviderTest.hack
├── Filesystem
├── FileTest.hack
├── FolderTest.hack
├── IoTestTrait.hack
├── LinesTest.hack
├── NodeTest.hack
├── NodeTestTrait.hack
└── PathTest.hack
├── Http
├── Message
│ ├── CookieTest.hack
│ ├── RequestTest.hack
│ ├── ResponseTest.hack
│ ├── ServerRequestTest.hack
│ ├── StreamTest.hack
│ ├── UploadedFileTest.hack
│ └── UriTest.hack
├── Server
│ ├── Handler
│ │ └── CallableHandlerDecorator.hack
│ ├── Middleware
│ │ ├── CallableMiddlewareDecoratorTest.hack
│ │ ├── HostMiddlewareDecoratorTest.hack
│ │ ├── OriginalMessageMiddlewareTest.hack
│ │ ├── PathMiddlewareDecoratorTest.hack
│ │ ├── RequestFactoryTestTrait.hack
│ │ └── RequestHandlerMiddlewareDecoratorTest.hack
│ └── MiddlewareStackTest.hack
└── Session
│ ├── Persistence
│ ├── AbstractSessionPersistenceTest.hack
│ └── CacheSessionPersistenceTest.hack
│ ├── SessionMiddlewareTest.hack
│ └── SessionTest.hack
├── Mercure
├── PublisherTest.hack
└── UpdateTest.hack
├── Translation
├── Catalogue
│ ├── AbstractOperationTest.hack
│ ├── MergeOperationTest.hack
│ └── TargetOperationTest.hack
├── Formatter
│ └── MessageFormatterTest.hack
├── Loader
│ ├── IniFileLoaderTest.hack
│ ├── JsonFileLoaderTest.hack
│ ├── LoaderTest.hack
│ └── TreeLoaderTest.hack
├── LoggingTranslatorTest.hack
├── MessageCatalogueTest.hack
├── Reader
│ └── TranslationReaderTest.hack
├── TranslatorTest.hack
└── fixtures
│ ├── messages.en.ini
│ ├── messages.en.json
│ ├── messages.en.yaml
│ ├── messages.fr.ini
│ ├── messages.fr.json
│ ├── messages.fr.yaml
│ ├── user.en.ini
│ ├── user.en.json
│ └── user.en.yaml
└── Util
├── AlternativesTest.hack
├── InflectorTest.hack
└── JsonTest.hack
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.hack linguist-language=Hack
2 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: Stale
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Local folder
3 | local/
4 |
5 | # Composer vendor folder
6 | vendor/
7 |
8 | # HHAST parser cache files
9 | *.hhast.parser-cache
10 |
11 | # Temporary directory used for tests.
12 | tests/tmp/
13 |
--------------------------------------------------------------------------------
/.hhconfig:
--------------------------------------------------------------------------------
1 | assume_php=false
2 | enable_experimental_tc_features = reified_generics
3 | safe_array = true
4 | safe_vector_array = true
5 | unsafe_rx = false
6 | ignored_paths = [ "vendor/.+/tests/.+", "vendor/.+/bin/.+" ]
--------------------------------------------------------------------------------
/.travis.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -ex
3 | apt update -y
4 | DEBIAN_FRONTEND=noninteractive apt install -y php-cli zip unzip
5 | hhvm --version
6 | php --version
7 |
8 | (
9 | cd $(mktemp -d)
10 | curl https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
11 | )
12 |
13 | composer --version
14 |
15 | composer install
16 |
17 | composer check
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | dist: trusty
3 | language: generic
4 | services: docker
5 | env:
6 | - HHVM_VERSION=latest
7 | install:
8 | - docker pull hhvm/hhvm:$HHVM_VERSION
9 | script:
10 | - docker run --rm -w /var/source -v $(pwd):/var/source hhvm/hhvm:$HHVM_VERSION ./.travis.sh
11 | notifications:
12 | slack:
13 | secure: fZ74Yt9xxelrIZiWBe74X6zCNV+RCbI2aD0EjB8P6d3ovIdyHc+JOe/AFDwUwU8tuKBYt1DpiMnw/rFQQwu2Y6CQnAjgGtG+ScCCVhy5OJvHqFTmMt/XMs9Hrgdylak3IofaI6D/4Du+E9ZMXHgXGVgjQQr0SNMsj1s70sSd97oiW4t4Kn5hxlAbZK7EWCs2BWwyTtVJD96UOEJrBK59lD0wQvfv0wSV948Wwnms70cPgO26Fa+pdBGsv4Ho475Dzu/y4JuO/kqMMzodZtMSm7FNDrppwqgX3qYkfGBI/foQ5IpBn5gGcG5w3RhZLXsNhLDrULcEHtF1Ptfo5PQGUArN8KPRf91Mju2CGICg3wy6GMEm+iXHdPWzUkCaQPhw4ty6ix+fm2ELatXW4BGGXANJzL6UNUGuQPh4Z2oVeX8zEFpUAA+PJRzd6FPYdQDdI3Xj8P445x/KQ+Mg4f2wCR/YKTkjWbkYKzqvjvssgrDGkbQfhXWAOr5/NgKj0/vRovT66Tra14UncjZdM6yHUOqeMq5KfDboEBXoj7+jZG1cQmtSErUoFF2CUyI/Jqva7symsJbjOYSVTKv6BAgoL+CncdLcTPGFLzavLiavkqp4Gd3ErWoeOWqFY0ZYByY4cLcnwLtL0TrY6Y9fdzFeJLmN6xhxTteuw3Ils66K/fw=
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2019 Saif Eddin Gmati
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | # Nuxed
12 |
13 | ## High Performance, Asynchronous, Hack Framework for building web applications with expressive, elegant syntax.
14 |
15 | ---
16 |
17 | ### Security
18 |
19 | For information on reporting security vulnerabilities in Nuxed, see [SECURITY.md](SECURITY.md).
20 |
21 | ---
22 |
23 | ### License
24 |
25 | The Nuxed framework is open-sourced software licensed under the MIT-licensed.
26 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security
2 |
3 | If you discover a security vulnerability within Nuxed, please send an e-mail to Saif Eddin Gmati via azjezz@protonmail.com.
4 |
5 | Please withhold public disclosure until after we have addressed the vulnerability.
6 |
7 | There are no hard and fast rules to determine if a bug is worth reporting as a security issue.
8 |
--------------------------------------------------------------------------------
/bin/format:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env hhvm
2 | >
14 | async function main(): Awaitable {
15 | Facebook\AutoloadMap\initialize();
16 | $stdout = IO\request_output();
17 | $files = await Asio\v(
18 | vec[
19 | new Filesystem\Folder(__DIR__.'/../src/Nuxed'),
20 | new Filesystem\Folder(__DIR__.'/../tests/Nuxed'),
21 | ]
22 | |> Vec\map($$, ($node) ==> $node->files(false, true)),
23 | );
24 |
25 | await (
26 | Vec\concat(...$files)
27 | |> Vec\map($$, ($file) ==> format($file, $stdout, false))
28 | |> Asio\v(
29 | Vec\concat(
30 | $$,
31 | vec[format(Filesystem\Node::load(__FILE__), $stdout, true)],
32 | ),
33 | )
34 | );
35 |
36 | await $stdout->writeAsync("\n");
37 | exit(0);
38 | }
39 |
40 | /**
41 | * exec() blocks, so this is not actually async.
42 | */
43 | async function format(
44 | Filesystem\File $file,
45 | IO\WriteHandle $stdout,
46 | bool $ignoreExtension = false,
47 | ): Awaitable {
48 | if ($ignoreExtension || 'hack' === $file->extension()) {
49 | $command = Str\format(
50 | 'bash -c "hackfmt -i %s >> /dev/null 2>&1 &"',
51 | $file->path()->toString(),
52 | );
53 |
54 | concurrent {
55 | await async {
56 | exec($command);
57 | };
58 |
59 | await $stdout->writeAsync('.');
60 | }
61 | } else {
62 | await $stdout->writeAsync('S');
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/hh_autoload.json:
--------------------------------------------------------------------------------
1 | {
2 | "roots": [
3 | "src"
4 | ],
5 | "devRoots": [
6 | "tests"
7 | ],
8 | "devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler"
9 | }
10 |
--------------------------------------------------------------------------------
/hhast-lint.json:
--------------------------------------------------------------------------------
1 | {
2 | "roots": [ "src/" ],
3 | "builtinLinters": "all",
4 | "disabledLinters": [
5 | "Facebook\\HHAST\\Linters\\LicenseHeaderLinter",
6 | "Facebook\\HHAST\\Linters\\AsyncFunctionAndMethodLinter",
7 | "Facebook\\HHAST\\Linters\\UseStatementWithAsLinter",
8 | "Facebook\\HHAST\\Linters\\CamelCasedMethodsUnderscoredFunctionsLinter"
9 | ],
10 | "disableAllAutoFixes": false,
11 | "overrides": [
12 | {
13 | "patterns": [
14 | "src/Nuxed/Http/Message/Stream.hack",
15 | "src/Nuxed/Http/Message/UploadedFile.hack",
16 | "src/Nuxed/Http/Emitter/SapiStreamEmitter.hack"
17 | ],
18 | "disabledLinters": [
19 | "Facebook\\HHAST\\Linters\\DontAwaitInALoopLinter"
20 | ]
21 | }
22 | ]
23 | }
--------------------------------------------------------------------------------
/src/Nuxed/Asset/Context/Context.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\Context;
2 |
3 | class Context implements IContext {
4 | public function __construct(private string $basePath, private bool $secure) {}
5 |
6 | /**
7 | * {@inheritdoc}
8 | */
9 | public function getBasePath(): string {
10 | return $this->basePath;
11 | }
12 |
13 | /**
14 | * {@inheritdoc}
15 | */
16 | public function isSecure(): bool {
17 | return $this->secure;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/Context/IContext.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\Context;
2 |
3 | interface IContext {
4 | /**
5 | * Gets the base path.
6 | *
7 | * @return string The base path
8 | */
9 | public function getBasePath(): string;
10 |
11 | /**
12 | * Checks whether the request is secure or not.
13 | *
14 | * @return bool true if the request is secure, false otherwise
15 | */
16 | public function isSecure(): bool;
17 | }
18 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/Context/NullContext.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\Context;
2 |
3 | class NullContext implements IContext {
4 | /**
5 | * {@inheritdoc}
6 | */
7 | public function getBasePath(): string {
8 | return '';
9 | }
10 |
11 | /**
12 | * {@inheritdoc}
13 | */
14 | public function isSecure(): bool {
15 | return false;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\Exception;
2 |
3 | class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/Exception/LogicException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\Exception;
2 |
3 | class LogicException extends \LogicException implements IException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/Exception/RuntimeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\Exception;
2 |
3 | class RuntimeException extends \RuntimeException implements IException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/IPackage.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset;
2 |
3 | interface IPackage {
4 | /**
5 | * Returns the asset version for an asset.
6 | *
7 | * @param string $path A path
8 | *
9 | * @return string The version string
10 | */
11 | public function getVersion(string $path): string;
12 |
13 | /**
14 | * Returns an absolute or root-relative public path.
15 | *
16 | * @param string $path A path
17 | *
18 | * @return string The public path
19 | */
20 | public function getUrl(string $path): string;
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/Package.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset;
2 |
3 | use namespace HH\Lib\Str;
4 |
5 | /**
6 | * Basic package that adds a version to asset URLs.
7 | */
8 | class Package implements IPackage {
9 | public function __construct(
10 | private VersionStrategy\IVersionStrategy $versionStrategy,
11 | private Context\IContext $context = new Context\NullContext(),
12 | ) {
13 | }
14 |
15 | /**
16 | * {@inheritdoc}
17 | */
18 | public function getVersion(string $path): string {
19 | return $this->versionStrategy->getVersion($path);
20 | }
21 |
22 | /**
23 | * {@inheritdoc}
24 | */
25 | public function getUrl(string $path): string {
26 | if ($this->isAbsoluteUrl($path)) {
27 | return $path;
28 | }
29 |
30 | return $this->versionStrategy->applyVersion($path);
31 | }
32 |
33 | protected function getContext(): Context\IContext {
34 | return $this->context;
35 | }
36 |
37 | protected function getVersionStrategy(): VersionStrategy\IVersionStrategy {
38 | return $this->versionStrategy;
39 | }
40 |
41 | protected function isAbsoluteUrl(string $url): bool {
42 | return Str\contains($url, '://') || Str\starts_with($url, '//');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/VersionStrategy/EmptyVersionStrategy.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\VersionStrategy;
2 |
3 | /**
4 | * Disable version for all assets.
5 | */
6 | class EmptyVersionStrategy implements IVersionStrategy {
7 | /**
8 | * {@inheritdoc}
9 | */
10 | public function getVersion(string $_path): string {
11 | return '';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | public function applyVersion(string $path): string {
18 | return $path;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/VersionStrategy/IVersionStrategy.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\VersionStrategy;
2 |
3 | interface IVersionStrategy {
4 | /**
5 | * Returns the asset version for an asset.
6 | *
7 | * @param string $path A path
8 | *
9 | * @return string The version string
10 | */
11 | public function getVersion(string $path): string;
12 |
13 | /**
14 | * Applies version to the supplied path.
15 | *
16 | * @param string $path A path
17 | *
18 | * @return string The versionized path
19 | */
20 | public function applyVersion(string $path): string;
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/VersionStrategy/JsonManifestVersionStrategy.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\VersionStrategy;
2 |
3 | use namespace HH\Asio;
4 | use namespace HH\Lib\Str;
5 | use namespace Nuxed\Filesystem;
6 | use namespace Nuxed\Util\Json;
7 | use namespace Nuxed\Asset\Exception;
8 |
9 | /**
10 | * Reads the versioned path of an asset from a JSON manifest file.
11 | *
12 | * For example, the manifest file might look like this:
13 | * {
14 | * "main.js": "main.abc123.js",
15 | * "css/styles.css": "css/styles.555abc.css"
16 | * }
17 | *
18 | * You could then ask for the version of "main.js" or "css/styles.css".
19 | */
20 | class JsonManifestVersionStrategy implements IVersionStrategy {
21 | const type TManifest = KeyedContainer;
22 | private ?KeyedContainer $manifestData;
23 |
24 | public function __construct(private Filesystem\File $manifest) {
25 | }
26 |
27 | /**
28 | * With a manifest, we don't really know or care about what
29 | * the version is. Instead, this returns the path to the
30 | * versioned file.
31 | */
32 | public function getVersion(string $path): string {
33 | return $this->applyVersion($path);
34 | }
35 |
36 | public function applyVersion(string $path): string {
37 | return $this->getManifestPath($path) ?? $path;
38 | }
39 |
40 | private function getManifestPath(string $path): ?string {
41 | if ($this->manifestData is null) {
42 | if (!$this->manifest->exists()) {
43 | throw new Exception\RuntimeException(Str\format(
44 | 'Asset manifest file "%s" does not exist.',
45 | $this->manifest->path()->toString(),
46 | ));
47 | }
48 |
49 | $this->manifestData = Json\structure(
50 | Asio\join($this->manifest->read()),
51 | type_structure($this, 'TManifest'),
52 | );
53 | }
54 |
55 | return idx($this->manifestData, $path, null);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Nuxed/Asset/VersionStrategy/StaticVersionStrategy.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Asset\VersionStrategy;
2 |
3 | use namespace HH\Lib\Str;
4 |
5 | class StaticVersionStrategy implements IVersionStrategy {
6 | private string $format;
7 |
8 | /**
9 | * @param string $version Version number
10 | * @param string $format Url format
11 | */
12 | public function __construct(private string $version, ?string $format = null) {
13 | $this->format = $format is nonnull && $format !== '' ? $format : '%s?%s';
14 | }
15 |
16 |
17 | /**
18 | * {@inheritdoc}
19 | */
20 | public function getVersion(string $_path): string {
21 | return $this->version;
22 | }
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function applyVersion(string $path): string {
28 | $versionized = Str\format(
29 | /* HH_IGNORE_ERROR[4027] */
30 | /* HH_IGNORE_ERROR[4110] */
31 | $this->format,
32 | Str\trim_left($path, '/'),
33 | $this->getVersion($path),
34 | );
35 |
36 | if ('' !== $path && '/' === $path[0]) {
37 | return '/'.$versionized;
38 | }
39 |
40 | return $versionized;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Exception/CacheException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Exception;
2 |
3 | class CacheException extends \Exception implements IException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Exception;
2 |
3 | class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Exception/LogicException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Exception;
2 |
3 | class LogicException extends \LogicException implements IException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Serializer/ISerializer.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Serializer;
2 |
3 | /**
4 | * Serializes/Unserializes Hack values.
5 | *
6 | * Implementations of this interface MUST deal with errors carefully. They MUST
7 | * also deal with forward and backward compatibility at the storage format level.
8 | */
9 | interface ISerializer {
10 | /**
11 | * Serialize a value.
12 | *
13 | * When serialization fails, no exception should be
14 | * thrown. Instead, this method should return null.
15 | */
16 | public function serialize(mixed $value): ?string;
17 |
18 | /**
19 | * Unserializes a single value and throws and exception if anything goes wrong.
20 | */
21 | public function unserialize(string $value): dynamic;
22 | }
23 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Serializer/JsonSerializer.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Serializer;
2 |
3 | use namespace Nuxed\Util\Json;
4 |
5 | class JsonSerializer implements ISerializer {
6 | /**
7 | * Serialize a value.
8 | *
9 | * When serialization fails, no exception should be
10 | * thrown. Instead, this method should return null.
11 | */
12 | public function serialize(mixed $value): ?string {
13 | try {
14 | return Json\encode($value, false);
15 | } catch (Json\Exception\JsonEncodeException $e) {
16 | return null;
17 | }
18 | }
19 |
20 | /**
21 | * Unserializes a single value and throws and exception if anything goes wrong.
22 | */
23 | public function unserialize(string $value): dynamic {
24 | return Json\decode($value);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Serializer/NativeSerializer.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Serializer;
2 |
3 | class NativeSerializer implements ISerializer {
4 | /**
5 | * Serialize a value.
6 | *
7 | * When serialization fails, no exception should be
8 | * thrown. Instead, this method should return null.
9 | */
10 | public function serialize(mixed $value): ?string {
11 | try {
12 | $val = @\serialize($value);
13 | if (false === $val) {
14 | return null;
15 | }
16 |
17 | return $val as string;
18 | } catch (\Throwable $e) {
19 | return null;
20 | }
21 | }
22 |
23 | /**
24 | * Unserializes a single value and throws and exception if anything goes wrong.
25 | */
26 | public function unserialize(string $value): dynamic {
27 | if ('b:0;' === $value) {
28 | return false;
29 | }
30 |
31 | if ('N;' === $value) {
32 | return null;
33 | }
34 |
35 | try {
36 | $unserialized = \unserialize($value);
37 |
38 | if (false !== $unserialized) {
39 | return $unserialized;
40 | }
41 |
42 | $error = \error_get_last();
43 | $message = (false === $error) || ($error['message'] is null)
44 | ? 'Failed to unserialize values'
45 | : $error['message'] as string;
46 | throw new \DomainException($message);
47 | } catch (\Error $e) {
48 | throw new \ErrorException(
49 | $e->getMessage(),
50 | (int)$e->getCode(),
51 | \E_ERROR,
52 | $e->getFile(),
53 | $e->getLine(),
54 | );
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Store/ApcStore.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Store;
2 |
3 | use namespace HH\Lib\Str;
4 | use namespace Nuxed\Cache\Serializer;
5 |
6 | class ApcStore extends AbstractStore {
7 | public function __construct(
8 | string $namespace = '',
9 | int $defaultTtl = 0,
10 | protected Serializer\ISerializer $serializer =
11 | new Serializer\NativeSerializer(),
12 | ) {
13 | parent::__construct($namespace, $defaultTtl);
14 | }
15 |
16 | <<__Override>>
17 | protected async function doStore(
18 | string $id,
19 | mixed $value,
20 | int $ttl = 0,
21 | ): Awaitable {
22 | return \apc_store($id, $this->serializer->serialize($value), $ttl);
23 | }
24 |
25 | <<__Override>>
26 | protected async function doContains(string $id): Awaitable {
27 | return \apc_exists($id);
28 | }
29 |
30 | <<__Override>>
31 | protected async function doDelete(string $id): Awaitable {
32 | return \apc_delete($id);
33 | }
34 |
35 | <<__Override>>
36 | protected async function doGet(string $id): Awaitable {
37 | $exist = await $this->doContains($id);
38 | if (!$exist) {
39 | return null;
40 | }
41 |
42 | return $this->serializer->unserialize((string)\apc_fetch($id));
43 | }
44 |
45 | <<__Override>>
46 | protected function doClear(string $namespace): Awaitable {
47 | if (Str\is_empty($namespace)) {
48 | return \apc_clear_cache();
49 | }
50 |
51 | /* HH_IGNORE_ERROR[2049] */
52 | $iterator = new APCIterator(
53 | Str\format('/^%s/', \preg_quote($namespace, '/')),
54 | /* HH_IGNORE_ERROR[2049] */
55 | /* HH_IGNORE_ERROR[4106] */
56 | APC_ITER_KEY,
57 | );
58 |
59 | return \apc_delete($iterator);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Store/IStore.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Store;
2 |
3 | interface IStore {
4 | /**
5 | * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
6 | */
7 | public function store(
8 | string $id,
9 | T $value,
10 | ?int $ttl = null,
11 | ): Awaitable;
12 |
13 | /**
14 | * Sets a cache item to be persisted later.
15 | */
16 | public function defer(string $id, mixed $value, ?int $ttl = null): bool;
17 |
18 | /**
19 | * Determines whether an item is present in the cache.
20 | */
21 | public function contains(string $id): Awaitable;
22 |
23 | /**
24 | * Delete an item from the cache by its unique key.
25 | */
26 | public function delete(string $id): Awaitable;
27 |
28 | /**
29 | * Fetches a value from the cache.
30 | */
31 | public function get(string $id): Awaitable;
32 |
33 | /**
34 | * Wipes clean the entire cache's keys.
35 | */
36 | public function clear(): Awaitable;
37 |
38 | /**
39 | * Persists any deferred cache items.
40 | */
41 | public function commit(): Awaitable;
42 | }
43 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Store/MCRouterStore.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Store;
2 |
3 | use namespace HH\Lib\Str;
4 | use namespace Nuxed\Cache\Serializer;
5 |
6 | class MCRouterStore extends AbstractStore {
7 | public function __construct(
8 | protected \MCRouter $mc,
9 | string $namespace = '',
10 | int $defaultTtl = 0,
11 | protected Serializer\ISerializer $serializer =
12 | new Serializer\NativeSerializer(),
13 | ) {
14 | parent::__construct($namespace, $defaultTtl);
15 | }
16 |
17 | <<__Override>>
18 | protected async function doGet(string $id): Awaitable {
19 | return $this->serializer->unserialize(await $this->mc->get($id));
20 | }
21 |
22 | <<__Override>>
23 | protected async function doDelete(string $id): Awaitable {
24 | await $this->mc->del($id);
25 | return true;
26 | }
27 |
28 | <<__Override>>
29 | protected async function doContains(string $id): Awaitable {
30 | try {
31 | await $this->mc->get($id);
32 | return true;
33 | } catch (\MCRouterException $e) {
34 | return false;
35 | }
36 | }
37 |
38 | <<__Override>>
39 | protected async function doStore(
40 | string $id,
41 | mixed $value,
42 | int $ttl = 0,
43 | ): Awaitable {
44 | $value = $this->serializer->serialize($value);
45 | if ($value is null) {
46 | return false;
47 | }
48 |
49 | if (0 >= $ttl) {
50 | await $this->mc->set($id, $value);
51 | } else {
52 | await $this->mc->set($id, $value, 0, $ttl);
53 | }
54 |
55 | return true;
56 | }
57 |
58 | <<__Override>>
59 | protected async function doClear(string $namespace): Awaitable {
60 | if (Str\is_empty($namespace)) {
61 | await $this->mc->flushAll();
62 | return true;
63 | }
64 |
65 | return false;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/Store/NullStore.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\Store;
2 |
3 | class NullStore implements IStore {
4 | /**
5 | * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
6 | */
7 | public async function store(
8 | string $_id,
9 | mixed $_value,
10 | ?int $_ttl = null,
11 | ): Awaitable {
12 | return false;
13 | }
14 |
15 | /**
16 | * Sets a cache item to be persisted later.
17 | */
18 | public function defer(string $_id, mixed $_value, ?int $_ttl = null): bool {
19 | return false;
20 | }
21 |
22 | /**
23 | * Determines whether an item is present in the cache.
24 | */
25 | public async function contains(string $_id): Awaitable {
26 | return false;
27 | }
28 |
29 | /**
30 | * Delete an item from the cache by its unique key.
31 | */
32 | public async function delete(string $_id): Awaitable {
33 | return false;
34 | }
35 |
36 | /**
37 | * Fetches a value from the cache.
38 | */
39 | public async function get(string $_id): Awaitable {
40 | return null;
41 | }
42 |
43 | /**
44 | * Wipes clean the entire cache's keys.
45 | */
46 | public async function clear(): Awaitable {
47 | return false;
48 | }
49 |
50 | /**
51 | * Persists any deferred cache items.
52 | */
53 | public async function commit(): Awaitable {
54 | return false;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Nuxed/Cache/private.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Cache\_Private;
2 |
3 | use namespace HH\Lib\Str;
4 | use namespace Nuxed\Cache\Exception;
5 |
6 |
7 | /**
8 | * Validates a cache key.
9 | *
10 | * @throws InvalidArgumentException When $key is not valid
11 | */
12 | function validate_key(string $key): void {
13 | if ('' === $key) {
14 | throw new Exception\InvalidArgumentException(
15 | 'Cache key length must be greater than zero',
16 | );
17 | }
18 |
19 | foreach (vec['{', '}', '(', ')', '/', '\\', '@', ':'] as $c) {
20 | if (Str\contains($key, $c)) {
21 | throw new Exception\InvalidArgumentException(Str\format(
22 | 'Cache key "%s" contains reserved characters {}()/\@:',
23 | $key,
24 | ));
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/ContainerBuilder.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container;
2 |
3 | use namespace HH\Lib\{C, Dict, Str};
4 |
5 | final class ContainerBuilder {
6 | private dict $definitions = dict[];
7 |
8 | public function register(IServiceProvider $provider): this {
9 | $provider->register($this);
10 |
11 | return $this;
12 | }
13 |
14 | public function add(
15 | typename $service,
16 | IFactory $factory,
17 | bool $shared = true,
18 | ): this {
19 | $definition = new ServiceDefinition($service, $factory, $shared);
20 | $this->addDefinition($definition);
21 | return $this;
22 | }
23 |
24 | public function inflect(
25 | typename $service,
26 | IInflector $inflector,
27 | ): this {
28 | $definition = $this->getDefinition($service);
29 | $definition->inflect($inflector);
30 |
31 | return $this;
32 | }
33 |
34 | private function addDefinition(ServiceDefinition $definition): void {
35 | $this->definitions[$definition->getId()] = $definition;
36 | }
37 |
38 | private function getDefinition(
39 | typename $service,
40 | ): ServiceDefinition {
41 | if (C\contains_key($this->definitions, $service)) {
42 | /* HH_FIXME[4110] */
43 | return $this->definitions[$service];
44 | }
45 |
46 | throw new Exception\NotFoundException(Str\format(
47 | 'Container builder doesn\'t contain definition for service (%s).',
48 | $service,
49 | ));
50 | }
51 |
52 | public function build(
53 | Container $delegates = vec[],
54 | ): IServiceContainer {
55 | $definitions = Dict\map(
56 | $this->definitions,
57 | ($definition) ==> {
58 | $definition as ServiceDefinition<_>;
59 | return clone $definition;
60 | },
61 | );
62 |
63 | return new ServiceContainer(
64 | /* HH_IGNORE_ERROR[4110] */
65 | $definitions,
66 | $delegates,
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/Exception/ContainerException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container\Exception;
2 |
3 | final class ContainerException extends \Exception implements IException {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container\Exception;
2 |
3 | use namespace His\Container\Exception;
4 |
5 | interface IException extends Exception\ContainerExceptionInterface {}
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/Exception/NotFoundException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container\Exception;
2 |
3 | use namespace His\Container\Exception;
4 |
5 | final class NotFoundException
6 | extends \Exception
7 | implements IException, Exception\NotFoundExceptionInterface {}
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/IFactory.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container;
2 |
3 | interface IFactory {
4 | public function create(IServiceContainer $container): T;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/IInflector.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container;
2 |
3 | interface IInflector {
4 | public function inflect(T $service, IServiceContainer $container): T;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/IServiceContainerAware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container;
2 |
3 | interface IServiceContainerAware {
4 | public function setServiceContainer(IServiceContainer $container): void;
5 |
6 | public function hasServiceContainer(): bool;
7 |
8 | public function getServiceContainer(): IServiceContainer;
9 | }
10 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/IServiceProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container;
2 |
3 | interface IServiceProvider {
4 | public function register(ContainerBuilder $builder): void;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/Service/CallableFactoryDecorator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container\Service;
2 |
3 | use namespace Nuxed\Container;
4 |
5 | class CallableFactoryDecorator implements Container\IFactory {
6 | public function __construct(
7 | private (function(Container\IServiceContainer): T) $call,
8 | ) {}
9 |
10 | public function create(Container\IServiceContainer $container): T {
11 | $call = $this->call;
12 |
13 | return $call($container);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/Service/CallableInflectorDecorator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container\Service;
2 |
3 | use namespace Nuxed\Container;
4 |
5 | class CallableInflectorDecorator implements Container\IInflector {
6 | public function __construct(
7 | private (function(T, Container\IServiceContainer): T) $call,
8 | ) {}
9 |
10 | public function inflect(
11 | T $service,
12 | Container\IServiceContainer $container,
13 | ): T {
14 | $call = $this->call;
15 |
16 | return $call($service, $container);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/Service/Newable.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container\Service;
2 |
3 | <<__ConsistentConstruct>>
4 | abstract class Newable {
5 | public function __construct() {}
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/Service/NewableFactoryDecorator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container\Service;
2 |
3 | use namespace Nuxed\Container;
4 |
5 | class NewableFactoryDecorator<<<__Newable>> T as Newable>
6 | implements Container\IFactory {
7 |
8 | public function __construct(private classname $service) {}
9 |
10 | public function create(?Container\IServiceContainer $_ = null): T {
11 | $class = $this->service;
12 | return new $class();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/ServiceContainerAwareTrait.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container;
2 |
3 | trait ServiceContainerAwareTrait implements IServiceContainerAware {
4 | public ?IServiceContainer $container = null;
5 |
6 | public function setServiceContainer(IServiceContainer $container): void {
7 | $this->container = $container;
8 | }
9 |
10 | public function hasServiceContainer(): bool {
11 | return $this->container is nonnull;
12 | }
13 |
14 | public function getServiceContainer(): IServiceContainer {
15 | return $this->container as nonnull;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/ServiceDefinition.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container;
2 |
3 |
4 | final class ServiceDefinition {
5 | private vec> $inflectors = vec[];
6 | private ?T $resolved = null;
7 |
8 | public function __construct(
9 | private typename $id,
10 | private IFactory $factory,
11 | private bool $shared = true,
12 | ) {}
13 |
14 | public function resolve(IServiceContainer $container): T {
15 | if ($this->isShared() && $this->resolved is nonnull) {
16 | return $this->resolved;
17 | }
18 |
19 | $object = $this->factory->create($container);
20 | foreach ($this->inflectors as $inflector) {
21 | $object = $inflector->inflect($object, $container);
22 | }
23 |
24 | return $this->resolved = $object;
25 | }
26 |
27 | public function getId(): typename {
28 | return $this->id;
29 | }
30 |
31 | public function getFactory(): IFactory {
32 | return $this->factory;
33 | }
34 |
35 | public function setFactory(IFactory $factory): this {
36 | $this->factory = $factory;
37 | $this->resolved = null;
38 |
39 | return $this;
40 | }
41 |
42 | public function isShared(): bool {
43 | return $this->shared;
44 | }
45 |
46 | public function setShared(bool $shared = true): this {
47 | $this->shared = $shared;
48 |
49 | return $this;
50 | }
51 |
52 | public function getInflectors(): Container> {
53 | return $this->inflectors;
54 | }
55 |
56 | public function inflect(IInflector $inflector): this {
57 | $this->inflectors[] = $inflector;
58 | $this->resolved = null;
59 |
60 | return $this;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Nuxed/Container/functions.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Container;
2 |
3 | function factory((function(IServiceContainer): T) $factory): IFactory {
4 | return new Service\CallableFactoryDecorator($factory);
5 | }
6 |
7 | function newable<<<__Newable>> T as Service\Newable>(
8 | classname $service,
9 | ): Service\NewableFactoryDecorator {
10 | return new Service\NewableFactoryDecorator($service);
11 | }
12 |
13 | function inflector(
14 | (function(T, IServiceContainer): T) $inflector,
15 | ): IInflector {
16 | return new Service\CallableInflectorDecorator($inflector);
17 | }
18 |
19 | function alias(classname $alias): IFactory {
20 | return factory(($container) ==> $container->get($alias));
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Contract/IReset.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Contract;
2 |
3 | /**
4 | * Provides a way to reset an object to its initial state.
5 | *
6 | * When calling the "reset()" method on an object, it should be put back to its
7 | * initial state. This usually means clearing any internal buffers and forwarding
8 | * the call to internal dependencies. All properties of the object should be put
9 | * back to the same state it had when it was first ready to use.
10 | *
11 | * This method could be called, for example, to recycle objects that are used as
12 | * services, so that they can be used to handle several requests in the same
13 | * process loop (note that we advise making your services stateless instead of
14 | * implementing this interface when possible.)
15 | */
16 | interface IReset {
17 | public function reset(): void;
18 | }
19 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment\Exception;
2 |
3 | <<__Sealed(InvalidArgumentException::class)>>
4 | interface IException {}
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment\Exception;
2 |
3 | final class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {}
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/_Private/State.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment\_Private;
2 |
3 | enum State: int {
4 | INITIAL = 0;
5 | UNQUOTED = 1;
6 | QUOTED = 2;
7 | ESCAPE = 3;
8 | WHITESPACE = 4;
9 | COMMENT = 5;
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/add.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | /**
4 | * add a variable to the environment if it doesn't exist.
5 | */
6 | function add(string $name, string $value): void {
7 | if (!contains($name)) {
8 | put($name, $value);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/contains.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | /**
4 | * Determine if a variable exists in the environment.
5 | */
6 | function contains(string $name): bool {
7 | return get($name, null) is nonnull;
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/forget.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | /**
4 | * Remove a variable from the environment.
5 | */
6 | function forget(string $name): void {
7 | \putenv(_Private\Parser::parseName($name));
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/get.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | /**
4 | * Fetches a variable from the environment.
5 | */
6 | function get(string $name, ?string $default = null): ?string {
7 | $value = \getenv(_Private\Parser::parseName($name));
8 | if ($value is bool) {
9 | return $default;
10 | }
11 |
12 | return _Private\Parser::parseValue($value);
13 | }
14 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/is_debug.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | use namespace HH\Lib\Str;
4 |
5 | function is_debug(): bool {
6 | $debug = get('APP_DEBUG');
7 | if ($debug is null) {
8 | return false;
9 | }
10 |
11 | $debug = Str\lowercase($debug);
12 | if ($debug === 'false' || $debug === 'off') {
13 | return false;
14 | }
15 |
16 | return (bool)$debug;
17 | }
18 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/is_dev.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | use namespace HH\Lib\Str;
4 |
5 | function is_dev(): bool {
6 | return Str\starts_with(
7 | Str\lowercase(get('APP_ENV', 'prod') as string),
8 | 'dev',
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/load.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | use namespace Nuxed\Filesystem;
4 | use namespace HH\Asio;
5 | use namespace HH\Lib\Str;
6 |
7 | /**
8 | * Load a .env file into the current environment.
9 | */
10 | async function load(string $file, bool $override = false): Awaitable {
11 | $file = Filesystem\Path::create($file);
12 | $file = new Filesystem\File($file, false);
13 | $lines = await $file->lines();
14 | $variables = vec[];
15 | foreach ($lines as $line) {
16 | $variables[] = async {
17 | $trimmed = Str\trim($line);
18 | // ignore comments and empty lines
19 | if (Str\starts_with($trimmed, '#') || Str\is_empty($trimmed)) {
20 | return;
21 | }
22 |
23 | list($name, $value) = parse($line);
24 | if ($value is nonnull) {
25 | $override ? put($name, $value) : add($name, $value);
26 | }
27 | };
28 | }
29 |
30 | await Asio\v($variables);
31 | }
32 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/parse.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | /**
4 | * Parse the given environment variable entry into a name and value.
5 | */
6 | function parse(string $entry): (string, ?string) {
7 | return _Private\Parser::parse($entry);
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Environment/put.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Environment;
2 |
3 | /**
4 | * Store a variable in the environment.
5 | */
6 | function put(string $name, string $value): void {
7 | list($name, $value) = parse($name.'='.$value);
8 | \putenv($name.'='.$value);
9 | }
10 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/CallableEventListener.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher;
2 |
3 | final class CallableEventListener implements IEventListener {
4 | public function __construct(
5 | private (function(T): Awaitable) $listener,
6 | ) {}
7 |
8 | public function process(T $event): Awaitable {
9 | return ($this->listener)($event);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ErrorEvent.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher;
2 |
3 | use namespace HH\ReifiedGenerics;
4 |
5 | final class ErrorEvent extends \Exception implements IEvent {
6 | public function __construct(
7 | private T $event,
8 | private IEventListener $listener,
9 | private \Exception $e,
10 | ) {
11 | parent::__construct($e->getMessage(), $e->getCode(), $e);
12 | }
13 |
14 | public function getEventType(): classname {
15 | /* HH_FIXME[2049] */
16 | /* HH_FIXME[4107] */
17 | return ReifiedGenerics\getClassname();
18 | }
19 |
20 | public function getEvent(): T {
21 | return $this->event;
22 | }
23 |
24 | public function getListener(): IEventListener {
25 | return $this->listener;
26 | }
27 |
28 | public function getException(): \Exception {
29 | return $this->e;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\Exception;
2 |
3 | <<__Sealed(InvalidListenerException::class)>>
4 | interface IException {
5 | require extends \Exception;
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/Exception/InvalidListenerException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\Exception;
2 |
3 | final class InvalidListenerException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/IEvent.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher;
2 |
3 | /**
4 | * Marker interface indicating an event instance.
5 | *
6 | * Event instances may contain zero methods, or as many methods as they
7 | * want. The interface MUST be implemented, however, to provide type-safety
8 | * to both listeners as well as the dispatcher.
9 | */
10 | interface IEvent {}
11 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/IEventDispatcher.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher;
2 |
3 | /**
4 | * Defines a dispatcher for events.
5 | */
6 | interface IEventDispatcher {
7 | /**
8 | * Provide all relevant listeners with an event to process.
9 | *
10 | * @template T as IEvent
11 | *
12 | * @return T The Event that was passed, now modified by listeners.
13 | */
14 | public function dispatch(T $event): Awaitable;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/IEventListener.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher;
2 |
3 | /**
4 | * Defines a listener for an event.
5 | */
6 | interface IEventListener {
7 | /**
8 | * Process the given event.
9 | */
10 | public function process(T $event): Awaitable;
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/IStoppableEvent.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher;
2 |
3 | /**
4 | * An Event whose processing may be interrupted when the event has been handled.
5 | *
6 | * A Dispatcher implementation MUST check to determine an Event
7 | * is marked as stopped after each listener is called. If it is then it should
8 | * return immediately without calling any further Listeners.
9 | */
10 | interface IStoppableEvent extends IEvent {
11 | /**
12 | * Is propagation stopped?
13 | *
14 | * This will typically only be used by the Dispatcher to determine if the
15 | * previous listener halted propagation.
16 | *
17 | * @return bool
18 | * True if the Event is complete and no further listeners should be called.
19 | * False to continue calling listeners.
20 | */
21 | public function isPropagationStopped(): bool;
22 | }
23 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/AttachableListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace HH\Lib\C;
4 | use namespace Nuxed\EventDispatcher;
5 |
6 | class AttachableListenerProvider implements IAttachableListenerProvider {
7 | private dict<
8 | classname,
9 | vec>,
10 | > $listeners = dict[];
11 |
12 | public function listen(
13 | classname $event,
14 | EventDispatcher\IEventListener $listener,
15 | ): void {
16 | $listeners = $this->listeners[$event] ?? vec[];
17 | if (C\contains($listeners, $listener)) {
18 | // duplicate detected
19 | return;
20 | }
21 |
22 | $listeners[] = $listener;
23 | /* HH_FIXME[4110] */
24 | $this->listeners[$event] = $listeners;
25 | }
26 |
27 | public async function getListeners(
28 | T $event,
29 | ): AsyncIterator> {
30 | foreach ($this->listeners as $type => $listeners) {
31 | if (\is_a($event, $type)) {
32 | foreach ($listeners as $listener) {
33 | /* HH_FIXME[4110] */
34 | yield $listener;
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/IAttachableListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace Nuxed\EventDispatcher;
4 |
5 | interface IAttachableListenerProvider extends IListenerProvider {
6 | public function listen(
7 | classname $event,
8 | EventDispatcher\IEventListener $listener,
9 | ): void;
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/IListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace Nuxed\EventDispatcher;
4 |
5 | /**
6 | * Mapper from an event to the listeners that are applicable to that event.
7 | */
8 | interface IListenerProvider {
9 | /**
10 | * @template T as EventDispatcher\IEvent
11 | *
12 | * @param T $event
13 | * An event for which to return the relevant listeners.
14 | * @return AsyncIterator>
15 | * An async iterator (usually an async generator) of listeners. Each
16 | * listener MUST be type-compatible with $event.
17 | */
18 | public function getListeners(
19 | T $event,
20 | ): AsyncIterator>;
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/IPrioritizedListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace Nuxed\EventDispatcher;
4 |
5 | interface IPrioritizedListenerProvider extends IAttachableListenerProvider {
6 | public function listen(
7 | classname $event,
8 | EventDispatcher\IEventListener $listener,
9 | int $priority = 1,
10 | ): void;
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/IRandomizedListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | interface IRandomizedListenerProvider extends IAttachableListenerProvider {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/IReifiedListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace Nuxed\EventDispatcher;
4 |
5 | interface IReifiedListenerProvider extends IListenerProvider {
6 | /**
7 | * Attach a listener
8 | *
9 | * Note: IReifiedListenerProvider::listen must use reified generics.
10 | *
11 | * use RefiedGenerics\getClassname to determine the event type.
12 | */
13 | public function listen<<<__Enforceable>> T as EventDispatcher\IEvent>(
14 | EventDispatcher\IEventListener $listener,
15 | ): void;
16 | }
17 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/ListenerProviderAggregate.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace Nuxed\EventDispatcher;
4 |
5 | final class ListenerProviderAggregate implements IListenerProvider {
6 | private vec $providers = vec[];
7 |
8 | public async function getListeners(
9 | T $event,
10 | ): AsyncIterator> {
11 | foreach ($this->providers as $provider) {
12 | foreach ($provider->getListeners($event) await as $listener) {
13 | yield $listener;
14 | }
15 | }
16 | }
17 |
18 | public function attach(IListenerProvider $provider): void {
19 | $this->providers[] = $provider;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/PrioritizedListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace HH\Lib\{C, Str, Vec};
4 | use namespace Nuxed\EventDispatcher;
5 |
6 | class PrioritizedListenerProvider implements IPrioritizedListenerProvider {
7 | private dict,
9 | vec>,
10 | >> $listeners = dict[];
11 |
12 | public function listen(
13 | classname $event,
14 | EventDispatcher\IEventListener $listener,
15 | int $priority = 1,
16 | ): void {
17 | $priority = Str\format('%d.0', $priority);
18 | if (
19 | C\contains_key($this->listeners, $priority) &&
20 | C\contains_key($this->listeners[$priority], $event) &&
21 | C\contains($this->listeners[$priority][$event], $listener)
22 | ) {
23 | return;
24 | }
25 |
26 | $priorityListeners = $this->listeners[$priority] ?? dict[];
27 | $eventListeners = $priorityListeners[$event] ?? vec[];
28 | $eventListeners[] = $listener;
29 | $priorityListeners[$event] = $eventListeners;
30 | /* HH_FIXME[4110] */
31 | $this->listeners[$priority] = $priorityListeners;
32 | }
33 |
34 | public async function getListeners(
35 | T $event,
36 | ): AsyncIterator> {
37 | $priorities = Vec\keys($this->listeners)
38 | |> Vec\sort($$, ($a, $b) ==> $a <=> $b);
39 |
40 | foreach ($priorities as $priority) {
41 | foreach ($this->listeners[$priority] as $eventName => $listeners) {
42 | if (\is_a($event, $eventName)) {
43 | foreach ($listeners as $listener) {
44 | /* HH_FIXME[4110] */
45 | yield $listener;
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/RandomizedListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace HH\Lib\{C, Vec};
4 | use namespace Nuxed\EventDispatcher;
5 |
6 | class RandomizedListenerProvider implements IRandomizedListenerProvider {
7 | private dict<
8 | classname,
9 | vec>,
10 | > $listeners = dict[];
11 |
12 | public function listen(
13 | classname $event,
14 | EventDispatcher\IEventListener $listener,
15 | ): void {
16 | $listeners = $this->listeners[$event] ?? vec[];
17 | if (C\contains($listeners, $listener)) {
18 | // duplicate detected
19 | return;
20 | }
21 |
22 | $listeners[] = $listener;
23 | /* HH_FIXME[4110] */
24 | $this->listeners[$event] = $listeners;
25 | }
26 |
27 | public async function getListeners(
28 | T $event,
29 | ): AsyncIterator> {
30 | $listeners = vec[];
31 | foreach ($this->listeners as $type => $eventListeners) {
32 | if (\is_a($event, $type)) {
33 | $listeners = Vec\concat($listeners, $eventListeners);
34 | }
35 | }
36 |
37 | foreach (Vec\shuffle($listeners) as $listener) {
38 | /* HH_FIXME[4110] */
39 | yield $listener;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/ListenerProvider/ReifiedListenerProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher\ListenerProvider;
2 |
3 | use namespace HH\Lib\C;
4 | use namespace HH\ReifiedGenerics;
5 | use namespace Nuxed\EventDispatcher;
6 |
7 | class ReifiedListenerProvider implements IReifiedListenerProvider {
8 | private dict<
9 | classname,
10 | vec>,
11 | > $listeners = dict[];
12 |
13 | public function listen<<<__Enforceable>> reify T as EventDispatcher\IEvent>(
14 | EventDispatcher\IEventListener $listener,
15 | ): void {
16 | /* HH_FIXME[2049] */
17 | /* HH_FIXME[4107] */
18 | $event = ReifiedGenerics\getClassname();
19 |
20 | $listeners = $this->listeners[$event] ?? vec[];
21 | if (C\contains($listeners, $listener)) {
22 | // duplicate detected
23 | return;
24 | }
25 |
26 | $listeners[] = $listener;
27 | /* HH_FIXME[4110] */
28 | $this->listeners[$event] = $listeners;
29 | }
30 |
31 | public async function getListeners(
32 | T $event,
33 | ): AsyncIterator> {
34 | foreach ($this->listeners as $type => $listeners) {
35 | if (\is_a($event, $type)) {
36 | foreach ($listeners as $listener) {
37 | /* HH_FIXME[4110] */
38 | yield $listener;
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Nuxed/EventDispatcher/functional.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\EventDispatcher;
2 |
3 | use namespace His\Container;
4 |
5 | /**
6 | * Helper function to create an event listener,
7 | * from a callable.
8 | */
9 | function f(
10 | (function(T): Awaitable) $listener,
11 | ): IEventListener {
12 | return new CallableEventListener($listener);
13 | }
14 |
15 | /**
16 | * Helper function to create a lazy loaded event listener.
17 | */
18 | function lazy(
19 | Container\ContainerInterface $container,
20 | classname> $service,
21 | ): IEventListener {
22 | return f(($event) ==> {
23 | return $container->get($service)->process($event);
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/ExistingNodeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | /**
4 | * Exception thrown when a target node destination already exists.
5 | */
6 | class ExistingNodeException extends RuntimeException implements IException {
7 | }
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | use type InvalidArgumentException as ParentException;
4 |
5 | class InvalidArgumentException extends ParentException implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/InvalidPathException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | /**
4 | * Exception thrown when an invalid file path is used.
5 | */
6 | class InvalidPathException extends RuntimeException implements IException {
7 | }
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/MissingNodeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | /**
4 | * Exception thrown when a node does not exist.
5 | */
6 | class MissingNodeException extends RuntimeException implements IException {
7 | }
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/OutOfRangeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | use type OutOfRangeException as ParentException;
4 |
5 | class OutOfRangeException extends ParentException implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/ReadErrorException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | /**
4 | * Exception thrown when a reading a files fails.
5 | */
6 | class ReadErrorException extends RuntimeException implements IException {
7 | }
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/RuntimeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | use type RuntimeException as ParentException;
4 |
5 | class RuntimeException extends ParentException implements IException {}
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/UnreadableNodeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | /**
4 | * Exception throw when trying to read or retrieve
5 | * a read handle of an unreadable node.
6 | */
7 | class UnreadableNodeException extends RuntimeException implements IException {}
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/UnwritableNodeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | /**
4 | * Exception throw when trying to write or retrieve
5 | * a write handle of an unwritable node.
6 | */
7 | class UnwritableNodeException extends RuntimeException implements IException {}
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Exception/WriteErrorException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem\Exception;
2 |
3 | /**
4 | * Exception thrown when a writing a files fails.
5 | */
6 | class WriteErrorException extends RuntimeException implements IException {}
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/Lines.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem;
2 |
3 | use namespace HH\Lib\{C, Str, Vec};
4 | use type HH\InvariantException;
5 | use type Nuxed\Util\StringableTrait;
6 | use type Iterator;
7 | use type Countable;
8 | use type IteratorAggregate;
9 |
10 | final class Lines implements Countable, IteratorAggregate {
11 | use StringableTrait;
12 |
13 | public function __construct(private Container $lines) {
14 | }
15 |
16 | public function count(): int {
17 | return C\count($this->lines);
18 | }
19 |
20 | public function first(): string {
21 | try {
22 | return C\firstx($this->lines);
23 | } catch (InvariantException $e) {
24 | throw new Exception\OutOfRangeException(
25 | 'Lines instance is empty.',
26 | $e->getCode(),
27 | $e,
28 | );
29 | }
30 | }
31 |
32 | /**
33 | * @return tuple(string, Lines) a tuple of the first line and the rest of
34 | * the lines as a new Lines instance.
35 | */
36 | public function jump(): (string, Lines) {
37 | return tuple($this->first(), new self(Vec\drop($this->lines, 1)));
38 | }
39 |
40 | public static function blank(string $line): bool {
41 | return Str\trim($line, " \t") === '';
42 | }
43 |
44 | public function getIterator(): Iterator {
45 | return (new Vector($this->lines))->getIterator();
46 | }
47 |
48 | public function toString(): string {
49 | return Str\join($this->lines, "\n");
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Nuxed/Filesystem/OperationType.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Filesystem;
2 |
3 | enum OperationType: int {
4 | /*
5 | * Will overwrite the destination file if one exists (file and folder)
6 | */
7 | OVERWRITE = 0;
8 |
9 | /*
10 | * Will merge folders together if they exist at the same location (folder only)
11 | */
12 | MERGE = 1;
13 |
14 | /*
15 | * Will not overwrite the destination file if it exists (file and folder)
16 | */
17 | SKIP = 2;
18 | }
19 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Client/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Client\Exception;
2 |
3 | /**
4 | * Every HTTP client related exception MUST implement this interface.
5 | */
6 | interface IException {
7 | require extends \Exception;
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Client/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Client\Exception;
2 |
3 | final class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {}
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Client/Exception/NetworkException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Client\Exception;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | /**
6 | * Thrown when the request cannot be completed because of network issues.
7 | *
8 | * There is no response object as this exception is thrown when no response has been received.
9 | *
10 | * Example: the target host name can not be resolved or the connection failed.
11 | */
12 | final class NetworkException extends \RuntimeException implements IException {
13 | public function __construct(
14 | private Message\Request $request,
15 | string $message = '',
16 | int $code = 0,
17 | ?\Exception $previous = null,
18 | ) {
19 | parent::__construct($message, $code, $previous);
20 | }
21 |
22 | /**
23 | * Returns the request.
24 | *
25 | * The request object MAY be a different object from the one passed to IHttpClient::sendRequest()
26 | *
27 | * @return Message\Request
28 | */
29 | public function getRequest(): Message\Request {
30 | return $this->request;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Client/Exception/RequestException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Client\Exception;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | /**
6 | * Exception for when a request failed.
7 | *
8 | * Examples:
9 | * - Request is invalid (e.g. method is missing)
10 | * - Runtime request errors (e.g. the body stream is not seekable)
11 | */
12 | final class RequestException extends \RuntimeException implements IException {
13 | public function __construct(
14 | private Message\Request $request,
15 | string $message = '',
16 | int $code = 0,
17 | ?\Exception $previous = null,
18 | ) {
19 | parent::__construct($message, $code, $previous);
20 | }
21 |
22 | /**
23 | * Returns the request.
24 | *
25 | * The request object MAY be a different object from the one passed to IHttpClient::sendRequest()
26 | *
27 | * @return Message\Request
28 | */
29 | public function getRequest(): Message\Request {
30 | return $this->request;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Client/IHttpClient.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Client;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | interface IHttpClient {
6 | /**
7 | * Sends a request and returns a response.
8 | *
9 | * @throws Exception\IException If an error happens while processing the request.
10 | */
11 | public function send(
12 | Message\Request $request,
13 | HttpClientOptions $options = shape(),
14 | ): Awaitable;
15 |
16 | /**
17 | * Create and send an HTTP request.
18 | *
19 | * Use an absolute path to override the base path of the client, or a
20 | * relative path to append to the base path of the client. The URL can
21 | * contain the query string as well.
22 | *
23 | * @throws Exception\IException If an error happens while processing the request.
24 | */
25 | public function request(
26 | string $method,
27 | string $uri,
28 | HttpClientOptions $options = shape(),
29 | ): Awaitable;
30 | }
31 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Client/MockHttpClient.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Client;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | final class MockHttpClient extends HttpClient {
6 | public function __construct(
7 | private (function(Message\Request): Awaitable) $handler,
8 | HttpClientOptions $options = shape(),
9 | ) {
10 | parent::__construct($options);
11 | }
12 |
13 | /**
14 | * Process the request and returns a response.
15 | *
16 | * @throws Exception\IException If an error happens while processing the request.
17 | */
18 | <<__Override>>
19 | public function process(
20 | Message\Request $request,
21 | ): Awaitable {
22 | return ($this->handler)($request);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Client/_Private/Structure.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Client\_Private;
2 |
3 | use namespace Nuxed\Http\Client;
4 |
5 | final abstract class Structure {
6 | const type HttpClientOptions = Client\HttpClientOptions;
7 |
8 | public static function HttpClientOptions(
9 | ): TypeStructure {
10 | return type_structure(static::class, 'HttpClientOptions');
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Emitter/Emitter.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Emitter;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | final class Emitter implements IEmitter {
6 | private IEmitter $sapi;
7 | private IEmitter $stream;
8 |
9 | public function __construct(MaxBufferLength $length = 8192) {
10 | $this->sapi = new SapiEmitter();
11 | $this->stream = new SapiStreamEmitter($length);
12 | }
13 |
14 | public function emit(Message\Response $response): Awaitable {
15 | if (
16 | !$response->hasHeader('Content-Disposition') &&
17 | !$response->hasHeader('Content-Range')
18 | ) {
19 | return $this->sapi->emit($response);
20 | }
21 |
22 | return $this->stream->emit($response);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Emitter/Exception/EmitterException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Emitter\Exception;
2 |
3 | final class EmitterException extends \RuntimeException implements IException {
4 | public static function forHeadersSent(): this {
5 | return new static('Unable to emit response; headers already sent');
6 | }
7 |
8 | public static function forOutputSent(): this {
9 | return new static(
10 | 'Output has been emitted previously; cannot emit response',
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Emitter/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Emitter\Exception;
2 |
3 | /**
4 | * Marker interface for component exceptions.
5 | */
6 | interface IException {
7 | require extends \Exception;
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Emitter/IEmitter.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Emitter;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | interface IEmitter {
6 | /**
7 | * Emit a response.
8 | *
9 | * Emits a response, including status line, headers, and the message body,
10 | * according to the environment.
11 | *
12 | * Implementations of this method may be written in such a way as to have
13 | * side effects, such as usage of header() or pushing output to the
14 | * output buffer.
15 | *
16 | * Implementations MAY raise exceptions if they are unable to emit the
17 | * response; e.g., if headers have already been sent.
18 | *
19 | * Implementations MUST return a boolean. A boolean `true` indicates that
20 | * the emitter was able to emit the response, while `false` indicates
21 | * it was not.
22 | */
23 | public function emit(Message\Response $response): Awaitable;
24 | }
25 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Error/IErrorHandler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Error;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | interface IErrorHandler {
6 | /**
7 | * Handle the error and return a response instance.
8 | */
9 | public function handle(
10 | \Throwable $error,
11 | Message\ServerRequest $request,
12 | ): Awaitable;
13 | }
14 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Error/Middleware/ErrorMiddleware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Error\Middleware;
2 |
3 | use namespace Nuxed\Http\{Error, Message, Server};
4 |
5 | class ErrorMiddleware implements Server\IMiddleware {
6 | public function __construct(private Error\IErrorHandler $handler) {}
7 |
8 | public async function process(
9 | Message\ServerRequest $request,
10 | Server\IHandler $handler,
11 | ): Awaitable {
12 | try {
13 | return await $handler->handle($request);
14 | } catch (\Throwable $e) {
15 | return await $this->handler->handle($e, $request);
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Flash/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Flash\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Flash/Exception/InvalidHopsValueException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Flash\Exception;
2 |
3 | class InvalidHopsValueException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Flash/FlashMessagesMiddleware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Flash;
2 |
3 | use namespace Nuxed\Http\{Message, Server};
4 |
5 | final class FlashMessagesMiddleware implements Server\IMiddleware {
6 | public function __construct(
7 | private string $key = FlashMessages::FLASH_NEXT,
8 | ) {}
9 |
10 | public async function process(
11 | Message\ServerRequest $request,
12 | Server\IHandler $handler,
13 | ): Awaitable {
14 | $session = $request->getSession();
15 | $flash = FlashMessages::create($session, $this->key);
16 | return await $handler->handle($request->withFlash($flash));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/CookieSameSite.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message;
2 |
3 | /**
4 | * Enum representing the cookie Same-Site values.
5 | *
6 | * @link https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
7 | */
8 | enum CookieSameSite: string {
9 | LAX = 'Lax';
10 | STRICT = 'Strict';
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/ConflictingHeadersException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | /**
4 | * The HTTP request contains headers with conflicting information.
5 | */
6 | class ConflictingHeadersException
7 | extends \UnexpectedValueException
8 | implements IException {
9 | }
10 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | /**
4 | * Marker interface for component-specific exceptions.
5 | */
6 | <<__Sealed(
7 | RuntimeException::class,
8 | InvalidArgumentException::class,
9 | UnrecognizedProtocolVersionException::class,
10 | ConflictingHeadersException::class,
11 | SuspiciousOperationException::class,
12 | )>>
13 | interface IException {
14 | }
15 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | final class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/RuntimeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | <<__Sealed(
4 | UnreadableStreamException::class,
5 | UnwritableStreamException::class,
6 | UntellableStreamException::class,
7 | UploadedFileErrorException::class,
8 | UploadedFileAlreadyMovedException::class,
9 | )>>
10 | class RuntimeException extends \RuntimeException implements IException {
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/SuspiciousOperationException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | /**
4 | * Raised when a user has performed an operation that should be considered
5 | * suspicious from a security perspective.
6 | */
7 | class SuspiciousOperationException
8 | extends \UnexpectedValueException
9 | implements IException {
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/UnreadableStreamException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | final class UnreadableStreamException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/UnrecognizedProtocolVersionException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | final class UnrecognizedProtocolVersionException
4 | extends \UnexpectedValueException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/UnseekableStreamException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | use type RuntimeException;
4 |
5 | final class UnseekableStreamException extends RuntimeException {}
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/UntellableStreamException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | final class UntellableStreamException extends RuntimeException {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/UnwritableStreamException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | final class UnwritableStreamException extends RuntimeException {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/UploadedFileAlreadyMovedException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | final class UploadedFileAlreadyMovedException extends RuntimeException {
4 | public function __construct(
5 | string $message = 'Cannot retrieve stream after it has already moved.',
6 | int $code = 0,
7 | ?\Exception $previous = null,
8 | ) {
9 | parent::__construct($message, $code, $previous);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Exception/UploadedFileErrorException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Exception;
2 |
3 | final class UploadedFileErrorException extends RuntimeException {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/IStream.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message;
2 |
3 | use namespace HH\Lib\Experimental\IO;
4 |
5 | /**
6 | * Describes a data stream.
7 | *
8 | * Typically, an instance will wrap a Hack stream; this interface provides
9 | * a wrapper around the most common operations.
10 | */
11 | interface IStream extends IO\ReadHandle, IO\WriteHandle {
12 | /**
13 | * Seek to a position in the stream.
14 | */
15 | public function seek(
16 | int $offset,
17 | StreamSeekWhence $whence = StreamSeekWhence::SET,
18 | ): void;
19 |
20 | /**
21 | * Seek to the beginning of the stream.
22 | *
23 | * If the stream is not seekable, this method will raise an exception;
24 | * otherwise, it will perform a seek(0).
25 | */
26 | public function rewind(): void;
27 |
28 | /**
29 | * Returns the current position of the file read/write pointer
30 | *
31 | * @return int Position of the file pointer
32 | */
33 | public function tell(): int;
34 |
35 | /**
36 | * Returns whether or not the stream is writable.
37 | */
38 | public function isWritable(): bool;
39 |
40 | /**
41 | * Returns whether or not the stream is readable.
42 | */
43 | public function isReadable(): bool;
44 |
45 | /**
46 | * Returns whether or not the stream is seekable.
47 | */
48 | public function isSeekable(): bool;
49 | }
50 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Request.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message;
2 |
3 | use namespace HH\Lib\{C, Dict};
4 |
5 | class Request {
6 | use RequestTrait;
7 |
8 | public function __construct(
9 | string $method,
10 | Uri $uri,
11 | KeyedContainer> $headers = dict[],
12 | ?IStream $body = null,
13 | string $version = '1.1',
14 | ) {
15 | $this->method = $method;
16 | $this->uri = $uri;
17 | $this->setHeaders($headers);
18 | $this->protocol = $version;
19 |
20 | if (!$this->hasHeader('Host')) {
21 | $this->updateHostFromUri();
22 | }
23 |
24 | if ($body is nonnull) {
25 | $this->stream = $body;
26 | }
27 | }
28 |
29 | <<__Override>>
30 | protected function updateHostFromUri(): void {
31 | $host = $this->uri->getHost();
32 | if ('' === $host) {
33 | return;
34 | }
35 |
36 | $port = $this->uri->getPort();
37 |
38 | if ($port is nonnull) {
39 | $host .= ':'.((string)$port);
40 | }
41 |
42 | if (C\contains_key($this->headerNames, 'host')) {
43 | $header = $this->headerNames['host'];
44 | } else {
45 | $header = 'Host';
46 | $this->headerNames['host'] = 'Host';
47 | }
48 |
49 | if (C\contains_key($this->headers, $header)) {
50 | unset($this->headers[$header]);
51 | }
52 |
53 | $this->headers = Dict\merge(dict[$header => vec[$host]], $this->headers);
54 | }
55 |
56 | public function __clone(): void {
57 | $this->messageClone();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Request/functions.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Request;
2 |
3 | use namespace Nuxed\Util\Json;
4 | use namespace Nuxed\Http\Message;
5 |
6 | function json(
7 | Message\Uri $uri,
8 | mixed $data,
9 | string $method = 'POST',
10 | KeyedContainer> $headers = dict[],
11 | string $version = '1.1',
12 | ): Message\Request {
13 | $flags = \JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_AMP | \JSON_HEX_QUOT;
14 | $stream = Message\stream(Json\encode($data, false, $flags));
15 | $headers = dict($headers);
16 | $headers['content-type'] ??= vec['application/json'];
17 | return Message\request($method, $uri, $headers, $stream, $version);
18 | }
19 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/RequestMethod.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message;
2 |
3 | /**
4 | * Defines constants for common HTTP request methods.
5 | */
6 | final abstract class RequestMethod {
7 | const string METHOD_HEAD = 'HEAD';
8 | const string METHOD_GET = 'GET';
9 | const string METHOD_POST = 'POST';
10 | const string METHOD_PUT = 'PUT';
11 | const string METHOD_PATCH = 'PATCH';
12 | const string METHOD_DELETE = 'DELETE';
13 | const string METHOD_PURGE = 'PURGE';
14 | const string METHOD_OPTIONS = 'OPTIONS';
15 | const string METHOD_TRACE = 'TRACE';
16 | const string METHOD_CONNECT = 'CONNECT';
17 | const string METHOD_REPORT = 'REPORT';
18 | const string METHOD_LOCK = 'LOCK';
19 | const string METHOD_UNLOCK = 'UNLOCK';
20 | const string METHOD_COPY = 'COPY';
21 | const string METHOD_MOVE = 'MOVE';
22 | const string METHOD_MERGE = 'MERGE';
23 | const string METHOD_NOTIFY = 'NOTIFY';
24 | const string METHOD_SUBSCRIBE = 'SUBSCRIBE';
25 | const string METHOD_UNSUBSCRIBE = 'UNSUBSCRIBE';
26 | }
27 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/Stream/functions.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\Stream;
2 |
3 | use namespace Nuxed\Filesystem;
4 | use namespace Nuxed\Http\Message;
5 |
6 | function file(
7 | Filesystem\File $file
8 | ): Message\IStream {
9 | return new Message\Stream(
10 | \fopen($file->path()->toString(), 'wb+', false)
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/StreamSeekWhence.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message;
2 |
3 | /**
4 | * Specifies how the cursor position will be calculated
5 | * based on the seek offset. Valid values are identical to the built-in
6 | * Hack $whence values for `fseek()`.
7 | */
8 | enum StreamSeekWhence: int {
9 | /**
10 | * Set position equal to offset bytes.
11 | */
12 | SET = \SEEK_SET;
13 | /**
14 | * Set position to current location plus offset.
15 | */
16 | CURRENT = \SEEK_CUR;
17 | /**
18 | * Set position to end-of-file plus offset.
19 | */
20 | END = \SEEK_END;
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/UploadedFileError.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message;
2 |
3 | enum UploadedFileError: int {
4 | ERROR_OK = 0;
5 | ERROR_EXCEEDS_MAX_INI_SIZE = 1;
6 | ERROR_EXCEEDS_MAX_FORM_SIZE = 2;
7 | ERROR_INCOMPLETE = 3;
8 | ERROR_NO_FILE = 4;
9 | ERROR_TMP_DIR_NOT_SPECIFIED = 6;
10 | ERROR_TMP_DIR_NOT_WRITEABLE = 7;
11 | ERROR_CANCELED_BY_EXTENSION = 8;
12 | }
13 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/_Private/HeadersMarshaler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\_Private;
2 |
3 | use namespace HH\Lib\{C, Str};
4 |
5 | final class HeadersMarshaler {
6 | public function marshal(
7 | KeyedContainer $server,
8 | ): KeyedContainer> {
9 | $headers = dict[];
10 |
11 | $valid = (mixed $value): bool ==>
12 | $value is Container<_> ? C\count($value) > 0 : ((string)$value) !== '';
13 |
14 | foreach ($server as $key => $value) {
15 | // Apache prefixes environment variables with REDIRECT_
16 | // if they are added by rewrite rules
17 | if (Str\search($key, 'REDIRECT_') === 0) {
18 | $key = Str\slice($key, 9);
19 | // We will not overwrite existing variables with the
20 | // prefixed versions, though
21 | if (C\contains_key($server, $key)) {
22 | continue;
23 | }
24 | }
25 |
26 | if (!$valid($value)) {
27 | continue;
28 | }
29 |
30 | if (Str\search($key, 'HTTP_') === 0) {
31 | $name = \strtr(Str\lowercase(Str\slice($key, 5)), '_', '-');
32 |
33 | if (!$value is Container<_>) {
34 | $value = vec[(string)$value];
35 | }
36 |
37 | $val = vec[];
38 | foreach ($value as $v) {
39 | $val[] = (string)$v;
40 | }
41 |
42 | $headers[$name] = $val;
43 | continue;
44 | }
45 |
46 | if (Str\search($key, 'CONTENT_') === 0) {
47 | $name = 'content-'.Str\lowercase(Str\slice($key, 8));
48 | if (!$value is Container<_>) {
49 | $value = vec[(string)$value];
50 | }
51 | $headers[$name] = vec[];
52 | foreach ($value as $v) {
53 | $headers[$name][] = (string)$v;
54 | }
55 | continue;
56 | }
57 | }
58 |
59 | return $headers;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/_Private/ProtocolVersionMarshaler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\_Private;
2 |
3 | use namespace HH\Lib\{Regex, Str};
4 | use namespace Nuxed\Http\Message\Exception;
5 |
6 | final class ProtocolVersionMarshaler {
7 | public function marshal(KeyedContainer $server): string {
8 | $protocol = (string)$server['SERVER_PROTOCOL'] ?? '1.1';
9 | if (
10 | !Regex\matches($protocol, re"#^(HTTP/)?(?P[1-9]\d*(?:\.\d)?)$#")
11 | ) {
12 | throw new Exception\UnrecognizedProtocolVersionException(
13 | Str\format('Unrecognized protocol version (%s).', $protocol),
14 | );
15 | }
16 |
17 | $matches = Regex\first_match(
18 | $protocol,
19 | re"#^(HTTP/)?(?P[1-9]\d*(?:\.\d)?)$#",
20 | ) as nonnull;
21 |
22 | return $matches['version'];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/_Private/inject_content_type_in_headers.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message\_Private;
2 |
3 | use namespace HH\Lib\{C, Str};
4 |
5 | /**
6 | * Inject the provided Content-Type, if none is already present.
7 | */
8 | function inject_content_type_in_headers(
9 | string $contentType,
10 | KeyedContainer> $headers,
11 | ): KeyedContainer> {
12 | $headers = dict($headers);
13 |
14 | $hasContentType = C\reduce_with_key(
15 | $headers,
16 | ($carry, $key, $item) ==>
17 | $carry ?: (Str\lowercase($key) === 'content-type'),
18 | false,
19 | );
20 |
21 | if (false === $hasContentType) {
22 | $headers['content-type'] = vec[
23 | $contentType,
24 | ];
25 | }
26 |
27 | return $headers;
28 | }
29 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Message/functions.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Message;
2 |
3 | function cookie(
4 | string $value,
5 | ?\DateTimeInterface $expires = null,
6 | ?string $path = null,
7 | ?string $domain = null,
8 | bool $secure = false,
9 | bool $httpOnly = false,
10 | ?CookieSameSite $sameSite = null,
11 | ): Cookie {
12 | return new Cookie(
13 | $value,
14 | $expires,
15 | $path,
16 | $domain,
17 | $secure,
18 | $httpOnly,
19 | $sameSite,
20 | );
21 | }
22 |
23 | function request(
24 | string $method,
25 | Uri $uri,
26 | KeyedContainer> $headers = dict[],
27 | ?IStream $body = null,
28 | string $version = '1.1',
29 | ): Request {
30 | return new Request($method, $uri, $headers, $body, $version);
31 | }
32 |
33 | function response(
34 | int $status = 200,
35 | KeyedContainer> $headers = dict[],
36 | ?IStream $body = null,
37 | string $version = '1.1',
38 | ?string $reason = null,
39 | ): Response {
40 | return new Response($status, $headers, $body, $version, $reason);
41 | }
42 |
43 | function stream(string $content): IStream {
44 | $handle = \fopen('php://memory', 'wb+');
45 | \fwrite($handle, $content);
46 | $stream = new Stream($handle);
47 | $stream->rewind();
48 | return $stream;
49 | }
50 |
51 | function uri(string $uri): Uri {
52 | return new Uri($uri);
53 | }
54 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Exception/DuplicateRouteException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Exception;
2 |
3 | class DuplicateRouteException extends \DomainException implements IException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Exception;
2 |
3 | class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Exception/RuntimeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Exception;
2 |
3 | class RuntimeException extends \RuntimeException implements IException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Generator/IUriGenerator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Generator;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | interface IUriGenerator {
6 | /**
7 | * Generate a URI from the named route.
8 | *
9 | * Takes the named route and any substitutions, and attempts to generate a
10 | * URI from it.
11 | *
12 | * The URI generated MUST NOT be escaped. If you wish to escape any part of
13 | * the URI, this should be performed afterwards;
14 | */
15 | public function generate(
16 | string $route,
17 | KeyedContainer $substitutions = dict[],
18 | ): Message\Uri;
19 | }
20 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/IRouter.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router;
2 |
3 | interface IRouter extends Generator\IUriGenerator, Matcher\IRequestMatcher {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Matcher/IRequestMatcher.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Matcher;
2 |
3 | use namespace Nuxed\Http\{Message, Router};
4 |
5 | interface IRequestMatcher {
6 | /**
7 | * Match a request against the known routes.
8 | *
9 | * Implementations will aggregate required information from the provided
10 | * request instance, and pass them to the underlying router implementation;
11 | * when done, they will then marshal a `Router\RouteResult` instance indicating
12 | * the results of the matching operation and return it to the caller.
13 | */
14 | public function match(Message\Request $request): Router\RouteResult;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Middleware/DispatchMiddleware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Middleware;
2 |
3 | use namespace Nuxed\Http\{Message, Router, Server};
4 |
5 | /**
6 | * Default dispatch middleware.
7 | *
8 | * Checks for a composed route result in the request. If none is provided,
9 | * delegates request processing to the handler.
10 | *
11 | * Otherwise, it delegates processing to the route result.
12 | */
13 | class DispatchMiddleware implements Server\IMiddleware {
14 | public async function process(
15 | Message\ServerRequest $request,
16 | Server\IHandler $handler,
17 | ): Awaitable {
18 | $routeResult = $request->getAttribute(Router\RouteResult::class);
19 |
20 | if ($routeResult is Router\RouteResult) {
21 | $route = $routeResult->getMatchedRoute();
22 | if ($route is nonnull) {
23 | return await $route->getMiddleware()->process($request, $handler);
24 | }
25 | }
26 |
27 | return await $handler->handle($request);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Middleware/MethodNotAllowedMiddleware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Middleware;
2 |
3 | use namespace Nuxed\Http\{Message, Router, Server};
4 |
5 | /**
6 | * Emit a 405 Method Not Allowed response
7 | *
8 | * If the request composes a route result, and the route result represents a
9 | * failure due to request method, this middleware will emit a 405 response,
10 | * along with an Allow header indicating allowed methods, as reported by the
11 | * route result.
12 | *
13 | * If no route result is composed, and/or it's not the result of a method
14 | * failure, it passes handling to the provided handler.
15 | */
16 | class MethodNotAllowedMiddleware implements Server\IMiddleware {
17 | public async function process(
18 | Message\ServerRequest $request,
19 | Server\IHandler $handler,
20 | ): Awaitable {
21 | $routeResult = $request->getAttribute(Router\RouteResult::class);
22 |
23 | if (
24 | !$routeResult is Router\RouteResult || !$routeResult->isMethodFailure()
25 | ) {
26 | return await $handler->handle($request);
27 | }
28 |
29 | return Message\response()
30 | ->withStatus(Message\StatusCode::METHOD_NOT_ALLOWED)
31 | ->withHeader('Allow', $routeResult->getAllowedMethods() as nonnull);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Middleware/RouteMiddleware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\Middleware;
2 |
3 | use namespace Nuxed\Http\{Message, Router, Server};
4 |
5 | /**
6 | * Default routing middleware.
7 | *
8 | * Uses the composed router to match against the incoming request, and
9 | * injects the request passed to the handler with the `RouteResult` instance
10 | * returned (using the `RouteResult` class name as the attribute name).
11 | *
12 | * If routing succeeds, injects the request passed to the handler with any
13 | * matched parameters as well.
14 | */
15 | class RouteMiddleware implements Server\IMiddleware {
16 | public function __construct(
17 | protected Router\Matcher\IRequestMatcher $matcher,
18 | ) {}
19 |
20 | public function process(
21 | Message\ServerRequest $request,
22 | Server\IHandler $handler,
23 | ): Awaitable {
24 | $result = $this->matcher->match($request);
25 |
26 | // Inject the actual route result, as well as individual matched parameters.
27 | $request = $request->withAttribute(Router\RouteResult::class, $result);
28 |
29 | if ($result->isSuccess()) {
30 | foreach ($result->getMatchedParams() as $param => $value) {
31 | $request = $request->withAttribute($param, $value);
32 | }
33 | }
34 |
35 | return $handler->handle($request);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/RouteCollector.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router;
2 |
3 | final class RouteCollector implements IRouteCollector {
4 | use RouteCollectorTrait;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/Router.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | final class Router implements IRouter {
6 | public function __construct(
7 | private Matcher\IRequestMatcher $matcher,
8 | private Generator\IUriGenerator $generator,
9 | ) {}
10 |
11 | /**
12 | * Match a request against the known routes.
13 | */
14 | public function match(Message\Request $request): RouteResult {
15 | return $this->matcher->match($request);
16 | }
17 |
18 | /**
19 | * Generate a URI from the named route.
20 | *
21 | * Takes the named route and any substitutions, and attempts to generate a
22 | * URI from it.
23 | *
24 | * The URI generated MUST NOT be escaped. If you wish to escape any part of
25 | * the URI, this should be performed afterwards;
26 | */
27 | public function generate(
28 | string $route,
29 | KeyedContainer $substitutions = dict[],
30 | ): Message\Uri {
31 | return $this->generator->generate($route, $substitutions);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/_Private/Ref.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\_Private;
2 |
3 | final class Ref {
4 | public function __construct(public T $value) {}
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Router/_Private/map.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Router\_Private;
2 |
3 | use namespace HH\Lib\{Dict, Vec};
4 | use namespace Facebook\HackRouter;
5 | use namespace Nuxed\Http\Router;
6 |
7 | function map(
8 | Container $routes,
9 | ): KeyedContainer<
10 | HackRouter\HttpMethod,
11 | HackRouter\PrefixMatching\PrefixMap,
12 | > {
13 | $result = new Ref(dict[]);
14 | Vec\map($routes, ($route) ==> {
15 | $methods = $route->getAllowedMethods();
16 | if ($methods is null) {
17 | $methods = HackRouter\HttpMethod::getValues();
18 | } else {
19 | $methods = HackRouter\HttpMethod::assertAll($methods);
20 | }
21 |
22 | Vec\map($methods, ($method) ==> {
23 | $result->value[$method] ??= dict[];
24 | $result->value[$method][$route->getPath()] = $route;
25 | });
26 | });
27 |
28 | return Dict\map(
29 | $result->value,
30 | ($map) ==> HackRouter\PrefixMatching\PrefixMap::fromFlatMap($map),
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Exception/EmptyStackException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Exception;
2 |
3 | use namespace HH\Lib\Str;
4 | use namespace Nuxed\Http\Server;
5 |
6 | final class EmptyStackException
7 | extends \OutOfBoundsException
8 | implements IException {
9 | public static function forClass(
10 | classname $class,
11 | ): this {
12 | return new static(Str\format(
13 | '%s cannot handle request; no middleware available to process the request.',
14 | $class,
15 | ));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Exception;
2 |
3 | <<__Sealed(
4 | InvalidMiddlewareException::class,
5 | EmptyStackException::class,
6 | RuntimeException::class,
7 | )>>
8 | interface IException {
9 | require extends \Exception;
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Exception/InvalidMiddlewareException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Exception;
2 |
3 | use namespace HH\Lib\Str;
4 | use namespace Nuxed\Http\Server;
5 |
6 | final class InvalidMiddlewareException
7 | extends \InvalidArgumentException
8 | implements IException {
9 | public static function forMiddleware(mixed $middleware): this {
10 | return new static(Str\format(
11 | 'Middleware "%s" is neither a string service name, a "%s" instance, or a "%s" instance.',
12 | \is_object($middleware) ? \get_class($middleware) : \gettype($middleware),
13 | Server\IMiddleware::class,
14 | Server\IHandler::class,
15 | ));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Exception/RuntimeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Exception;
2 |
3 | <<__Sealed(ServerException::class)>>
4 | class RuntimeException extends \RuntimeException implements IException {
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Exception/ServerException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Exception;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | final class ServerException extends RuntimeException {
6 | public function __construct(
7 | protected int $status = Message\StatusCode::INTERNAL_SERVER_ERROR,
8 | protected KeyedContainer> $headers = dict[],
9 | protected ?Message\IStream $body = null
10 | ) {
11 | parent::__construct(
12 | Message\Response::$phrases[$status] ?? Message\Response::$phrases[500]
13 | );
14 | }
15 |
16 | public function getStatusCode(): int {
17 | return $this->status;
18 | }
19 |
20 | public function getHeaders(
21 | ): KeyedContainer> {
22 | return $this->headers;
23 | }
24 |
25 | public function getBody(): ?Message\IStream {
26 | return $this->body;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Handler/CallableHandlerDecorator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Handler;
2 |
3 | use namespace Nuxed\Http\{Message, Server};
4 |
5 | final class CallableHandlerDecorator implements Server\IHandler {
6 | public function __construct(
7 | private Server\CallableHandler $callback,
8 | ) {}
9 |
10 | public function handle(
11 | Message\ServerRequest $request,
12 | ): Awaitable {
13 | $fun = $this->callback;
14 | return $fun($request);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Handler/NextMiddlewareHandler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Handler;
2 |
3 | use namespace Nuxed\Http\{Message, Server};
4 |
5 | class NextMiddlewareHandler implements Server\IHandler {
6 | private \SplPriorityQueue $queue;
7 |
8 | public function __construct(
9 | \SplPriorityQueue $queue,
10 | private Server\IHandler $handler,
11 | ) {
12 | $this->queue = clone $queue;
13 | }
14 |
15 | public async function handle(
16 | Message\ServerRequest $request,
17 | ): Awaitable {
18 | if (0 === $this->queue->count()) {
19 | return await $this->handler->handle($request);
20 | }
21 |
22 | $middleware = $this->queue->extract();
23 |
24 | return await $middleware->process($request, $this);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Handler/NotFoundHandler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Handler;
2 |
3 | use namespace Nuxed\Http\{Message, Server};
4 |
5 | class NotFoundHandler implements Server\IHandler {
6 | public async function handle(
7 | Message\ServerRequest $_request,
8 | ): Awaitable {
9 | throw new Server\Exception\ServerException(404, dict[]);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/HandlerMiddlewareTrait.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | trait HandlerMiddlewareTrait implements IMiddleware {
6 | require implements IHandler;
7 |
8 | public function process(
9 | Message\ServerRequest $request,
10 | IHandler $_handler,
11 | ): Awaitable {
12 | return $this->handle($request);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/IHandler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | /**
6 | * An HTTP request handler process a HTTP request and produces an HTTP response.
7 | * This interface defines the methods require to use the request handler.
8 | */
9 | interface IHandler {
10 | /**
11 | * Handle the request and return a response.
12 | */
13 | public function handle(
14 | Message\ServerRequest $request,
15 | ): Awaitable;
16 | }
17 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/IMiddleware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | /**
6 | * An HTTP middleware component participates in processing an HTTP message,
7 | * either by acting on the request or the response. This interface defines the
8 | * methods required to use the middleware.
9 | */
10 | interface IMiddleware {
11 | /**
12 | * Process an incoming server request and return a response, optionally delegating
13 | * response creation to a handler.
14 | */
15 | public function process(
16 | Message\ServerRequest $request,
17 | IHandler $handler,
18 | ): Awaitable;
19 | }
20 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/IMiddlewareStack.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server;
2 |
3 | /**
4 | * Stack middleware like unix pipes.
5 | *
6 | * This interface represents a stack of middleware, which can be attached using
7 | * the `stack()` method, and is itself middleware.
8 | *
9 | * It creates an instance of `NextMiddlewareProcessor` internally, invoking it with the provided
10 | * request and response instances, passing the original request and the returned
11 | * response to the `$next` argument when complete.
12 | *
13 | * Inspired by Sencha Connect.
14 | *
15 | * @see https://github.com/senchalabs/connect
16 | */
17 | interface IMiddlewareStack extends IHandler, IMiddleware {
18 | /**
19 | * Attach middleware to the stack.
20 | */
21 | public function stack(IMiddleware $middleware, int $priority = 0): void;
22 | }
23 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Middleware/CallableMiddlewareDecorator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Middleware;
2 |
3 | use namespace Nuxed\Http\{Message, Server};
4 |
5 | final class CallableMiddlewareDecorator implements Server\IMiddleware {
6 | public function __construct(private Server\CallableMiddleware $middleware) {}
7 |
8 | public function process(
9 | Message\ServerRequest $request,
10 | Server\IHandler $handler,
11 | ): Awaitable {
12 | $fun = $this->middleware;
13 | return $fun($request, $handler);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Middleware/HostMiddlewareDecorator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Middleware;
2 |
3 | use namespace HH\Lib\Str;
4 | use namespace Nuxed\Http\{Message, Server};
5 |
6 | class HostMiddlewareDecorator implements Server\IMiddleware {
7 | public function __construct(
8 | private string $host,
9 | private Server\IMiddleware $middleware,
10 | ) {}
11 |
12 | public async function process(
13 | Message\ServerRequest $request,
14 | Server\IHandler $handler,
15 | ): Awaitable {
16 | $host = $request->getUri()->getHost();
17 |
18 | if ($host !== Str\lowercase($this->host)) {
19 | return await $handler->handle($request);
20 | }
21 |
22 | return await $this->middleware->process($request, $handler);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Middleware/OriginalMessagesMiddleware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Middleware;
2 |
3 | use namespace Nuxed\Http\{Message, Server};
4 |
5 | class OriginalMessagesMiddleware implements Server\IMiddleware {
6 | public async function process(
7 | Message\ServerRequest $request,
8 | Server\IHandler $handler,
9 | ): Awaitable {
10 | return await $handler->handle(
11 | $request
12 | ->withAttribute('OriginalUri', $request->getUri())
13 | ->withAttribute('OriginalRequest', $request),
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/Middleware/RequestHandlerMiddlewareDecorator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server\Middleware;
2 |
3 | use namespace Nuxed\Http\{Message, Server};
4 |
5 | /**
6 | * Decorate a request handler as middleware.
7 | *
8 | * When pulling handlers from a container, or creating pipelines, it's
9 | * simplest if everything is of the same type, so we do not need to worry
10 | * about varying execution based on type.
11 | *
12 | * To manage this, this class decorates request handlers as middleware, so that
13 | * they may be piped or routed to. When processed, they delegate handling to the
14 | * decorated handler, which will return a response.
15 | */
16 | final class HandlerMiddlewareDecorator
17 | implements Server\IMiddleware, Server\IHandler {
18 | public function __construct(private Server\IHandler $handler) {}
19 |
20 | /**
21 | * Proxies to decorated handler to handle the request.
22 | */
23 | public function handle(
24 | Message\ServerRequest $request,
25 | ): Awaitable {
26 | return $this->handler->handle($request);
27 | }
28 |
29 | /**
30 | * Proxies to decorated handler to handle the request.
31 | */
32 | public function process(
33 | Message\ServerRequest $request,
34 | Server\IHandler $_,
35 | ): Awaitable {
36 | return $this->handler->handle($request);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Server/types.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Server;
2 |
3 | use namespace Nuxed\Http\Message;
4 |
5 | type CallableMiddleware = (function(
6 | Message\ServerRequest,
7 | IHandler,
8 | ): Awaitable);
9 |
10 | type CallableHandler = (function(
11 | Message\ServerRequest,
12 | ): Awaitable);
13 |
14 | type FunctionalMiddleware = (function(
15 | Message\ServerRequest,
16 | CallableHandler,
17 | ): Awaitable);
18 |
19 | type DoublePassMiddleware = (function(
20 | Message\ServerRequest,
21 | Message\Response,
22 | IHandler,
23 | ): Awaitable);
24 |
25 | type DoublePassFunctionalMiddleware = (function(
26 | Message\ServerRequest,
27 | Message\Response,
28 | CallableHandler,
29 | ): Awaitable);
30 |
31 | type DoublePassHandler = (function(
32 | Message\ServerRequest,
33 | Message\Response,
34 | ): Awaitable);
35 |
36 | type LazyMiddleware = (function(): IMiddleware);
37 |
38 | type LazyHandler = (function(): IHandler);
39 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Session/CacheLimiter.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Session;
2 |
3 | enum CacheLimiter: string {
4 | NOCACHE = 'nocache';
5 | PUBLIC = 'public';
6 | PRIVATE = 'private';
7 | PRIVATE_NO_EXPIRE = 'private_no_expire';
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Session/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Session\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Session/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Session\Exception;
2 |
3 | class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Session/Persistence/ISessionPersistence.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Session\Persistence;
2 |
3 | use namespace Nuxed\Http\{Message, Session};
4 |
5 | interface ISessionPersistence {
6 | /**
7 | * Generate a session data instance based on the request.
8 | */
9 | public function initialize(
10 | Message\ServerRequest $request,
11 | ): Awaitable;
12 |
13 | /**
14 | * Persist the session data instance
15 | *
16 | * Persists the session data, returning a response instance with any
17 | * artifacts required to return to the client.
18 | */
19 | public function persist(
20 | Session\Session $session,
21 | Message\Response $response,
22 | ): Awaitable;
23 | }
24 |
--------------------------------------------------------------------------------
/src/Nuxed/Http/Session/SessionMiddleware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Http\Session;
2 |
3 | use type Nuxed\Http\Server\{IHandler, IMiddleware};
4 | use type Nuxed\Http\Message\{Response, ServerRequest};
5 |
6 | class SessionMiddleware implements IMiddleware {
7 | public function __construct(
8 | private Persistence\ISessionPersistence $persistence,
9 | ) {}
10 |
11 | public async function process(
12 | ServerRequest $request,
13 | IHandler $handler,
14 | ): Awaitable {
15 | $session = await $this->persistence->initialize($request);
16 | $request = $request->withSession($session);
17 | $response = await $handler->handle($request);
18 |
19 | return await $this->persistence->persist($request->getSession(), $response);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Exception;
2 |
3 | final class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Exception/RuntimeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Exception;
2 |
3 | final class RuntimeException extends \RuntimeException implements IException {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/IBuilder.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt;
2 |
3 | interface IBuilder {
4 | /**
5 | * Appends new items to audience
6 | */
7 | public function permittedFor(string ...$audiences): IBuilder;
8 |
9 | /**
10 | * Configures the expiration time
11 | */
12 | public function expiresAt(int $expiration): IBuilder;
13 |
14 | /**
15 | * Configures the token id
16 | */
17 | public function identifiedBy(string $id): IBuilder;
18 |
19 | /**
20 | * Configures the time that the token was issued
21 | */
22 | public function issuedAt(int $issuedAt): IBuilder;
23 |
24 | /**
25 | * Configures the issuer
26 | */
27 | public function issuedBy(string $issuer): IBuilder;
28 |
29 | /**
30 | * Configures the time before which the token cannot be accepted
31 | */
32 | public function canOnlyBeUsedAfter(int $notBefore): IBuilder;
33 |
34 | /**
35 | * Configures the subject
36 | */
37 | public function relatedTo(string $subject): IBuilder;
38 |
39 | /**
40 | * Configures a header item
41 | *
42 | * @param mixed $value
43 | */
44 | public function withHeader(string $name, mixed $value): IBuilder;
45 |
46 | /**
47 | * Configures a claim item
48 | *
49 | * @param mixed $value
50 | *
51 | * @throws InvalidArgumentException When trying to set a registered claim.
52 | */
53 | public function withClaim(string $name, mixed $value): IBuilder;
54 |
55 | /**
56 | * Returns a signed token to be used
57 | */
58 | public function getToken(ISigner $signer, Signer\Key $key): IToken;
59 | }
60 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/IParser.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt;
2 |
3 | interface IParser {
4 | /**
5 | * Parses the JWT and returns a token
6 | *
7 | * @throws Exception\InvalidArgumentException
8 | */
9 | public function parse(string $jwt): IToken;
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/ISigner.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt;
2 |
3 | interface ISigner {
4 | /**
5 | * Returns the algorithm id
6 | */
7 | public function getAlgorithmId(): string;
8 |
9 | /**
10 | * Creates a hash for the given payload
11 | *
12 | * @throws Exception\InvalidArgumentException When given key is invalid.
13 | */
14 | public function sign(string $payload, Signer\Key $key): string;
15 |
16 | /**
17 | * Returns if the expected hash matches with the data and key
18 | *
19 | * @throws Exception\InvalidArgumentException When given key is invalid.
20 | */
21 | public function verify(
22 | string $expected,
23 | string $payload,
24 | Signer\Key $key,
25 | ): bool;
26 | }
27 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/IToken.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt;
2 |
3 | use namespace Nuxed\Util;
4 |
5 | interface IToken extends Util\Stringable {
6 | /**
7 | * Returns the token headers
8 | */
9 | public function getHeaders(): Token\Headers;
10 |
11 | /**
12 | * Returns the token claims
13 | */
14 | public function getClaims(): Token\Claims;
15 |
16 | /**
17 | * Returns the token signature
18 | */
19 | public function getSignature(): Token\Signature;
20 |
21 | /**
22 | * Returns the token payload
23 | */
24 | public function getPayload(): string;
25 |
26 | /**
27 | * Returns if the token is allowed to be used by the audience
28 | */
29 | public function isPermittedFor(string $audience): bool;
30 |
31 | /**
32 | * Returns if the token has the given id
33 | */
34 | public function isIdentifiedBy(string $id): bool;
35 |
36 | /**
37 | * Returns if the token has the given subject
38 | */
39 | public function isRelatedTo(string $subject): bool;
40 |
41 | /**
42 | * Returns if the token was issued by any of given issuers
43 | */
44 | public function hasBeenIssuedBy(string ...$issuers): bool;
45 |
46 | /**
47 | * Returns if the token was issued before of given time
48 | *
49 | * Returns NULL if the token doesn't contain the `iat` claim.
50 | */
51 | public function hasBeenIssuedBefore(int $now): ?bool;
52 |
53 | /**
54 | * Returns if the token minimum time is before than given time
55 | *
56 | * Returns NULL if the token doesn't contain the `nbf` claim.
57 | */
58 | public function isMinimumTimeBefore(int $now): ?bool;
59 |
60 | /**
61 | * Returns if the token is expired
62 | */
63 | public function isExpired(int $now): bool;
64 |
65 | /**
66 | * Returns an encoded representation of the token
67 | */
68 | public function toString(): string;
69 | }
70 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Ecdsa.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer;
2 |
3 | <<
4 | __ConsistentConstruct,
5 | __Sealed(Ecdsa\Sha256::class, Ecdsa\Sha384::class, Ecdsa\Sha512::class)
6 | >>
7 | abstract class Ecdsa extends OpenSSL {
8 | public function __construct(private Ecdsa\ISignatureConverter $converter) {}
9 |
10 | public static function create(): Ecdsa {
11 | return new static(new Ecdsa\MultibyteStringConverter());
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | final public function sign(string $payload, Key $key): string {
19 | return $this->converter->fromAsn1(
20 | $this->createSignature(
21 | $key->getContent(),
22 | $key->getPassphrase(),
23 | $payload,
24 | ),
25 | $this->getKeyLength(),
26 | );
27 | }
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | <<__Override>>
33 | final public function verify(
34 | string $expected,
35 | string $payload,
36 | Key $key,
37 | ): bool {
38 | return $this->verifySignature(
39 | $this->converter->toAsn1($expected, $this->getKeyLength()),
40 | $payload,
41 | $key->getContent(),
42 | );
43 | }
44 |
45 | /**
46 | * {@inheritdoc}
47 | */
48 | <<__Override>>
49 | final public function getKeyType(): int {
50 | return \OPENSSL_KEYTYPE_EC;
51 | }
52 |
53 | /**
54 | * Returns the length of each point in the signature, so that we can calculate and verify R and S points properly
55 | *
56 | * @internal
57 | */
58 | abstract public function getKeyLength(): int;
59 | }
60 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Ecdsa/ISignatureConverter.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Ecdsa;
2 |
3 | /**
4 | * Manipulates the result of a ECDSA signature (points R and S) according to the
5 | * JWA specs.
6 | *
7 | * OpenSSL creates a signature using the ASN.1 format and, according the JWA specs,
8 | * the signature for JWTs must be the concatenated values of points R and S (in
9 | * big-endian octet order).
10 | *
11 | * @internal
12 | *
13 | * @see https://tools.ietf.org/html/rfc7518#page-9
14 | * @see https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
15 | */
16 | interface ISignatureConverter {
17 | /**
18 | * Converts the signature generated by OpenSSL into what JWA defines
19 | */
20 | public function fromAsn1(string $signature, int $length): string;
21 |
22 | /**
23 | * Converts the JWA signature into something OpenSSL understands
24 | */
25 | public function toAsn1(string $points, int $length): string;
26 | }
27 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Ecdsa/Sha256.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Ecdsa;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha256 extends Signer\Ecdsa {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'ES256';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): int {
19 | return \OPENSSL_ALGO_SHA256;
20 | }
21 |
22 | /**
23 | * {@inheritdoc}
24 | */
25 | <<__Override>>
26 | public function getKeyLength(): int {
27 | return 64;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Ecdsa/Sha384.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Ecdsa;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha384 extends Signer\Ecdsa {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'ES384';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): int {
19 | return \OPENSSL_ALGO_SHA384;
20 | }
21 |
22 | /**
23 | * {@inheritdoc}
24 | */
25 | <<__Override>>
26 | public function getKeyLength(): int {
27 | return 96;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Ecdsa/Sha512.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Ecdsa;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha512 extends Signer\Ecdsa {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'ES512';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): int {
19 | return \OPENSSL_ALGO_SHA512;
20 | }
21 |
22 | /**
23 | * {@inheritdoc}
24 | */
25 | <<__Override>>
26 | public function getKeyLength(): int {
27 | return 132;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Hmac.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer;
2 |
3 | use namespace Nuxed\Jwt;
4 |
5 | <<__Sealed(Hmac\Sha256::class, Hmac\Sha384::class, Hmac\Sha512::class)>>
6 | abstract class Hmac implements Jwt\ISigner {
7 | /**
8 | * {@inheritdoc}
9 | */
10 | <<__Override>>
11 | final public function sign(string $payload, Key $key): string {
12 | return \hash_hmac(
13 | $this->getAlgorithm(),
14 | $payload,
15 | $key->getContent(),
16 | true,
17 | );
18 | }
19 |
20 | /**
21 | * {@inheritdoc}
22 | */
23 | final public function verify(
24 | string $expected,
25 | string $payload,
26 | Key $key,
27 | ): bool {
28 | return \hash_equals($expected, $this->sign($payload, $key));
29 | }
30 |
31 | abstract public function getAlgorithm(): string;
32 | }
33 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Hmac/Sha256.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Hmac;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha256 extends Signer\Hmac {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'HS256';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): string {
19 | return 'sha256';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Hmac/Sha384.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Hmac;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha384 extends Signer\Hmac {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'HS384';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): string {
19 | return 'sha384';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Hmac/Sha512.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Hmac;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha512 extends Signer\Hmac {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'HS512';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): string {
19 | return 'sha512';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Key.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer;
2 |
3 | final class Key {
4 | public function __construct(
5 | private string $content,
6 | private string $passphrase = '',
7 | ) {}
8 |
9 | public function getContent(): string {
10 | return $this->content;
11 | }
12 |
13 | public function getPassphrase(): string {
14 | return $this->passphrase;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/None.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer;
2 |
3 | use namespace Nuxed\Jwt;
4 |
5 | final class None implements Jwt\ISigner {
6 | /**
7 | * {@inhertdoc}
8 | */
9 | public function getAlgorithmId(): string {
10 | return 'none';
11 | }
12 |
13 | /**
14 | * {@inhertdoc}
15 | */
16 | public function sign(string $_payload, Key $_key): string {
17 | return '';
18 | }
19 |
20 | /**
21 | * {@inhertdoc}
22 | */
23 | public function verify(string $expected, string $_payload, Key $_key): bool {
24 | return $expected === '';
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Rsa.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer;
2 |
3 | <<__Sealed(Rsa\Sha256::class, Rsa\Sha384::class, Rsa\Sha512::class)>>
4 | abstract class Rsa extends OpenSSL {
5 | /**
6 | * {@inheritdoc}
7 | */
8 | <<__Override>>
9 | final public function sign(string $payload, Key $key): string {
10 | return $this->createSignature(
11 | $key->getContent(),
12 | $key->getPassphrase(),
13 | $payload,
14 | );
15 | }
16 |
17 | /**
18 | * {@inheritdoc}
19 | */
20 | <<__Override>>
21 | final public function verify(
22 | string $expected,
23 | string $payload,
24 | Key $key,
25 | ): bool {
26 | return $this->verifySignature($expected, $payload, $key->getContent());
27 | }
28 |
29 | /**
30 | * {@inheritdoc}
31 | */ <<__Override>>
32 | final public function getKeyType(): int {
33 | return \OPENSSL_KEYTYPE_RSA;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Rsa/Sha256.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Rsa;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha256 extends Signer\Rsa {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'RS256';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): int {
19 | return \OPENSSL_ALGO_SHA256;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Rsa/Sha384.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Rsa;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha384 extends Signer\Rsa {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'RS384';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): int {
19 | return \OPENSSL_ALGO_SHA384;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Signer/Rsa/Sha512.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Signer\Rsa;
2 |
3 | use namespace Nuxed\Jwt\Signer;
4 |
5 | final class Sha512 extends Signer\Rsa {
6 | /**
7 | * {@inheritdoc}
8 | */
9 | <<__Override>>
10 | public function getAlgorithmId(): string {
11 | return 'RS512';
12 | }
13 |
14 | /**
15 | * {@inheritdoc}
16 | */
17 | <<__Override>>
18 | public function getAlgorithm(): int {
19 | return \OPENSSL_ALGO_SHA512;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Token/Headers.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Token;
2 |
3 | use namespace HH\Lib\C;
4 | use namespace Nuxed\Util;
5 |
6 | final class Headers {
7 | use Util\StringableTrait;
8 |
9 | private dict $data = dict[];
10 |
11 | public function __construct(
12 | KeyedContainer $data,
13 | private string $encoded,
14 | ) {
15 | foreach ($data as $key => $value) {
16 | $this->data[$key] = $value as dynamic;
17 | }
18 | }
19 |
20 | public function contains(string $header): bool {
21 | return C\contains_key($this->data, $header);
22 | }
23 |
24 | public function get(string $header, mixed $default = null): dynamic {
25 | return $this->data[$header] ?? $default;
26 | }
27 |
28 | public function toString(): string {
29 | return $this->encoded;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Nuxed/Jwt/Token/Signature.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Jwt\Token;
2 |
3 | use namespace Nuxed\Util;
4 |
5 | final class Signature {
6 | use Util\StringableTrait;
7 |
8 | public function __construct(private string $hash, private string $encoded) {}
9 |
10 | public function getHash(): string {
11 | return $this->hash;
12 | }
13 |
14 | public function toString(): string {
15 | return $this->encoded;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/AbstractLogger.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log;
2 |
3 | /**
4 | * This is a simple Logger implementation that other Loggers can inherit from.
5 | *
6 | * It simply delegates all log-level-specific methods to the `log` method to
7 | * reduce boilerplate code that a simple Logger that does the same thing with
8 | * messages regardless of the error level has to implement.
9 | */
10 | abstract class AbstractLogger implements ILogger {
11 | use LoggerTrait;
12 | }
13 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/BufferingLogger.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log;
2 |
3 | use namespace Nuxed\Log;
4 |
5 | /**
6 | * A buffering logger that stacks logs for later.
7 | */
8 | class BufferingLogger extends Log\AbstractLogger {
9 | private vec Log\LogLevel,
11 | 'message' => string,
12 | 'context' => KeyedContainer,
13 | ...
14 | )> $logs = vec[];
15 |
16 | <<__Override>>
17 | public function log(
18 | Log\LogLevel $level,
19 | string $message,
20 | KeyedContainer $context = dict[],
21 | ): void {
22 | $this->logs[] = shape(
23 | 'level' => $level,
24 | 'message' => $message,
25 | 'context' => $context,
26 | );
27 | }
28 |
29 | public function cleanLogs(
30 | ): vec Log\LogLevel,
32 | 'message' => string,
33 | 'context' => KeyedContainer,
34 | ...
35 | )> {
36 | $logs = $this->logs;
37 | $this->reset();
38 | return $logs;
39 | }
40 |
41 | <<__Override>>
42 | public function reset(): void {
43 | $this->logs = vec[];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Exception;
2 |
3 | class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Exception/LogicException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Exception;
2 |
3 | class LogicException extends \LogicException implements IException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Exception/UnexpectedValueException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Exception;
2 |
3 | class UnexpectedValueException
4 | extends \UnexpectedValueException
5 | implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Formatter/IFormatter.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Formatter;
2 |
3 | use namespace Nuxed\Log;
4 |
5 | interface IFormatter {
6 | public function format(Log\LogRecord $record): Log\LogRecord;
7 | }
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Handler/AbstractHandler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Handler;
2 |
3 | use namespace Nuxed\Log;
4 | use type Nuxed\Contract\IReset;
5 |
6 | abstract class AbstractHandler implements IFormattableHandler, IReset {
7 | use FormattableHandlerTrait;
8 |
9 | const dict LEVELS = dict[
10 | Log\LogLevel::DEBUG => 0,
11 | Log\LogLevel::INFO => 1,
12 | Log\LogLevel::NOTICE => 2,
13 | Log\LogLevel::WARNING => 3,
14 | Log\LogLevel::ERROR => 4,
15 | Log\LogLevel::CRITICAL => 5,
16 | Log\LogLevel::ALERT => 6,
17 | Log\LogLevel::EMERGENCY => 7,
18 | ];
19 |
20 | /**
21 | * @param Log\LogLevel $level The minimum logging level at which this handler will be triggered
22 | * @param bool $bubble Whether the messages that are handled can bubble up the stack or not
23 | */
24 | public function __construct(
25 | public Log\LogLevel $level = Log\LogLevel::DEBUG,
26 | public bool $bubble = true,
27 | ) {}
28 |
29 | public function isHandling(Log\LogRecord $record): bool {
30 | $minimum = static::LEVELS[$this->level];
31 | $level = static::LEVELS[$record['level']];
32 |
33 | return $level >= $minimum;
34 | }
35 |
36 | public function handle(Log\LogRecord $record): bool {
37 | if (!$this->isHandling($record)) {
38 | return false;
39 | }
40 |
41 | $record = $this->getFormatter()->format($record);
42 |
43 | $this->write($record);
44 |
45 | return false === $this->bubble;
46 | }
47 |
48 | /**
49 | * Writes the record down to the log of the implementing handler
50 | */
51 | abstract protected function write(Log\LogRecord $record): void;
52 |
53 | public function close(): void {
54 | }
55 |
56 | public function reset(): void {
57 | if ($this->formatter is IReset) {
58 | $this->formatter->reset();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Handler/FormattableHandlerTrait.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Handler;
2 |
3 | use namespace Nuxed\Log\Formatter;
4 |
5 | trait FormattableHandlerTrait {
6 | require implements IFormattableHandler;
7 |
8 | protected ?Formatter\IFormatter $formatter = null;
9 |
10 | public function setFormatter(Formatter\IFormatter $formatter): this {
11 | $this->formatter = $formatter;
12 |
13 | return $this;
14 | }
15 |
16 | public function getFormatter(): Formatter\IFormatter {
17 | if (!$this->formatter) {
18 | $this->formatter = $this->getDefaultFormatter();
19 | }
20 |
21 | return $this->formatter;
22 | }
23 |
24 | /**
25 | * Gets the default formatter.
26 | *
27 | * Overwrite this if the LineFormatter is not a good default for your handler.
28 | */
29 | protected function getDefaultFormatter(): Formatter\IFormatter {
30 | return new Formatter\LineFormatter();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Handler/IFormattableHandler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Handler;
2 |
3 | use namespace Nuxed\Log\Formatter;
4 |
5 | interface IFormattableHandler extends IHandler {
6 | /**
7 | * Sets the formatter.
8 | */
9 | public function setFormatter(Formatter\IFormatter $formatter): this;
10 |
11 | /**
12 | * Gets the formatter.
13 | */
14 | public function getFormatter(): Formatter\IFormatter;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Handler/IHandler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Handler;
2 |
3 | use type Nuxed\Log\LogRecord;
4 |
5 | interface IHandler {
6 | /**
7 | * Checks whether the given record will be handled by this handler.
8 | *
9 | * This is mostly done for performance reasons, to avoid calling processors for nothing.
10 | *
11 | * Handlers should still check the record levels within handle(), returning false in isHandling()
12 | * is no guarantee that handle() will not be called, and isHandling() might not be called
13 | * for a given record.
14 | *
15 | * @param LogRecord $record Partial log record containing only a level key
16 | *
17 | * @return bool
18 | */
19 | public function isHandling(LogRecord $record): bool;
20 |
21 | /**
22 | * Handles a record.
23 | *
24 | * All records may be passed to this method, and the handler should discard
25 | * those that it does not want to handle.
26 | *
27 | * The return value of this function controls the bubbling process of the handler stack.
28 | * Unless the bubbling is interrupted (by returning true), the Logger class will keep on
29 | * calling further handlers in the stack with a given log record.
30 | *
31 | * @param record $record The record to handle
32 | * @return bool true means that this handler handled the record, and that bubbling is not permitted.
33 | * false means the record was either not processed or that this handler allows bubbling.
34 | */
35 | public function handle(LogRecord $record): bool;
36 |
37 | /**
38 | * Closes the handler.
39 | *
40 | * Ends a log cycle and frees all resources used by the handler.
41 | *
42 | * Closing a Handler means flushing all buffers and freeing any open resources/handles.
43 | *
44 | * Implementations have to be idempotent (i.e. it should be possible to call close several times without breakage)
45 | * and ideally handlers should be able to reopen themselves on handle() after they have been closed.
46 | */
47 | public function close(): void;
48 | }
49 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Handler/SysLogFacility.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Handler;
2 |
3 | enum SysLogFacility: int {
4 | AUTH = \LOG_AUTH;
5 | AUTHPRIV = \LOG_AUTHPRIV;
6 | CRON = \LOG_CRON;
7 | DAEMON = \LOG_DAEMON;
8 | KERN = \LOG_KERN;
9 | LPR = \LOG_LPR;
10 | MAIL = \LOG_MAIL;
11 | NEWS = \LOG_NEWS;
12 | SYSLOG = \LOG_SYSLOG;
13 | USER = \LOG_USER;
14 | UUCP = \LOG_UUCP;
15 | LOCAL0 = \LOG_LOCAL0;
16 | LOCAL1 = \LOG_LOCAL1;
17 | LOCAL2 = \LOG_LOCAL2;
18 | LOCAL3 = \LOG_LOCAL3;
19 | LOCAL4 = \LOG_LOCAL4;
20 | LOCAL5 = \LOG_LOCAL5;
21 | LOCAL6 = \LOG_LOCAL6;
22 | LOCAL7 = \LOG_LOCAL7;
23 | }
24 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Handler/SysLogHandler.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Handler;
2 |
3 | use namespace Nuxed\Log;
4 | use namespace HH\Lib\Str;
5 | use namespace Nuxed\Log\Exception;
6 |
7 | class SysLogHandler extends AbstractHandler {
8 | /**
9 | * Translates Monolog log levels to syslog log priorities.
10 | */
11 | protected dict $logLevels = dict[
12 | Log\LogLevel::DEBUG => \LOG_DEBUG,
13 | Log\LogLevel::INFO => \LOG_INFO,
14 | Log\LogLevel::NOTICE => \LOG_NOTICE,
15 | Log\LogLevel::WARNING => \LOG_WARNING,
16 | Log\LogLevel::ERROR => \LOG_ERR,
17 | Log\LogLevel::CRITICAL => \LOG_CRIT,
18 | Log\LogLevel::ALERT => \LOG_ALERT,
19 | Log\LogLevel::EMERGENCY => \LOG_EMERG,
20 | ];
21 |
22 | /**
23 | * @param Log\LogLevel $level The minimum logging level at which this handler will be triggered
24 | * @param bool $bubble Whether the messages that are handled can bubble up the stack or not
25 | */
26 | public function __construct(
27 | protected string $ident,
28 | protected SysLogFacility $facility = SysLogFacility::USER,
29 | Log\LogLevel $level = Log\LogLevel::DEBUG,
30 | bool $bubble = true,
31 | protected int $options = \LOG_PID,
32 | ) {
33 | parent::__construct($level, $bubble);
34 | }
35 |
36 | <<__Override>>
37 | public function write(Log\LogRecord $record): void {
38 | if (!\openlog($this->ident, $this->options, (int)$this->facility)) {
39 | throw new Exception\LogicException(Str\format(
40 | "Can't open syslog for ident %s and facility %d",
41 | $this->ident,
42 | (int)$this->facility,
43 | ));
44 | }
45 |
46 | \syslog(
47 | $this->logLevels[$record['level']],
48 | $record['formatted'] ?? $record['message'],
49 | );
50 | }
51 |
52 | <<__Override>>
53 | public function close(): void {
54 | \closelog();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/ILoggerAware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log;
2 |
3 | /**
4 | * Describes a logger-aware instance.
5 | */
6 | interface ILoggerAware {
7 | /**
8 | * Sets a logger instance on the object.
9 | */
10 | public function setLogger(ILogger $logger): void;
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/LogLevel.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log;
2 |
3 | /**
4 | * Describes log levels.
5 | */
6 | enum LogLevel: string {
7 | EMERGENCY = 'emergency';
8 | ALERT = 'alert';
9 | CRITICAL = 'critical';
10 | ERROR = 'error';
11 | WARNING = 'warning';
12 | NOTICE = 'notice';
13 | INFO = 'info';
14 | DEBUG = 'debug';
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/LoggerAwareTrait.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log;
2 |
3 | /**
4 | * Basic Implementation of ILoggerAware.
5 | */
6 | trait LoggerAwareTrait implements ILoggerAware {
7 | /**
8 | * The logger instance.
9 | */
10 | protected ?ILogger $logger;
11 |
12 | /**
13 | * Sets a logger.
14 | */
15 | public function setLogger(ILogger $logger): void {
16 | $this->logger = $logger;
17 | }
18 |
19 | /**
20 | * Gets a logger.
21 | */
22 | protected function getLogger(): ILogger {
23 | if ($this->logger is null) {
24 | $this->logger = new NullLogger();
25 | }
26 |
27 | return $this->logger;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/NullLogger.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log;
2 |
3 | /**
4 | * This Logger can be used to avoid conditional log calls.
5 | *
6 | * Logging should always be optional, and if no logger is provided to your
7 | * library creating a NullLogger instance to have something to throw logs at
8 | * is a good way to avoid littering your code with `if ($this->logger) { }`
9 | * blocks.
10 | */
11 | class NullLogger extends AbstractLogger {
12 | /**
13 | * Logs with an arbitrary level.
14 | */
15 | <<__Override>>
16 | public function log(
17 | LogLevel $_level,
18 | string $_message,
19 | KeyedContainer $_context = dict[],
20 | ): void {
21 | // noop
22 | }
23 |
24 | <<__Override>>
25 | public function reset(): void {
26 | // noop
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Processor/CallableProcessor.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Processor;
2 |
3 | use type Nuxed\Log\LogRecord;
4 |
5 | class CallableProcessor implements IProcessor {
6 | public function __construct(
7 | protected (function(LogRecord): LogRecord) $callable,
8 | ) {}
9 |
10 | public function process(LogRecord $record): LogRecord {
11 | $fun = $this->callable;
12 |
13 | return $fun($record);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Processor/ContextProcessor.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Processor;
2 |
3 | use namespace HH\Lib\Str;
4 | use namespace Nuxed\Util;
5 | use type Nuxed\Log\LogRecord;
6 |
7 | class ContextProcessor implements IProcessor {
8 | public function process(LogRecord $record): LogRecord {
9 | if (!Str\contains($record['message'], '}')) {
10 | return $record;
11 | }
12 |
13 | foreach ($record['context'] as $key => $value) {
14 | $placeholder = '{'.$key.'}';
15 |
16 | if (!Str\contains($record['message'], $placeholder)) {
17 | continue;
18 | }
19 |
20 | $record['message'] = Str\replace(
21 | $record['message'],
22 | $placeholder,
23 | Util\stringify($value),
24 | );
25 | }
26 |
27 | return $record;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Processor/IProcessor.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Processor;
2 |
3 | use namespace Nuxed\Log;
4 |
5 | interface IProcessor {
6 | public function process(Log\LogRecord $record): Log\LogRecord;
7 | }
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Processor/MessageLengthProcessor.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log\Processor;
2 |
3 | use namespace HH\Lib\Str;
4 | use type Nuxed\Log\LogRecord;
5 |
6 |
7 | class MessageLengthProcessor implements IProcessor {
8 | public function __construct(protected int $maxLength = 1024) {}
9 |
10 | public function process(LogRecord $record): LogRecord {
11 | if (Str\length($record['message']) <= $this->maxLength) {
12 | return $record;
13 | }
14 |
15 | $record['message'] = Str\slice($record['message'], 0, $this->maxLength).
16 | '[...]';
17 |
18 | return $record;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Nuxed/Log/Record.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Log;
2 |
3 | use type Nuxed\Log\LogLevel;
4 | use type DateTime;
5 |
6 | type LogRecord = shape(
7 | 'level' => LogLevel,
8 | 'message' => string,
9 | 'context' => dict,
10 | 'time' => DateTime,
11 | 'extra' => dict,
12 | ?'formatted' => string,
13 | ...
14 | );
15 |
--------------------------------------------------------------------------------
/src/Nuxed/Markdown/Extension/AbstractExtension.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Markdown\Extension;
2 |
3 | use namespace Facebook\Markdown;
4 | use namespace Facebook\Markdown\{Inlines, UnparsedBlocks};
5 |
6 | class AbstractExtension implements IExtension {
7 | /**
8 | * @see Facebook\Markdown\RenderContext::appendFilters()
9 | */
10 | public function getRenderFilters(): Container {
11 | return vec[];
12 | }
13 |
14 | /**
15 | * @see Facebook\Markdown\Inlines\Context::prependInlineTypes()
16 | */
17 | public function getInlineTypes(): Container> {
18 | return vec[];
19 | }
20 |
21 | /**
22 | * @see Facebook\Markdown\UnparsedBlocks\Context::prependBlockTypes()
23 | */
24 | public function getBlockProducers(
25 | ): Container> {
26 | return vec[];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Nuxed/Markdown/Extension/IExtension.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Markdown\Extension;
2 |
3 | use namespace Facebook\Markdown;
4 | use namespace Facebook\Markdown\{Inlines, UnparsedBlocks};
5 |
6 | interface IExtension {
7 | /**
8 | * @see Facebook\Markdown\RenderContext::appendFilters()
9 | */
10 | public function getRenderFilters(): Container;
11 |
12 | /**
13 | * @see Facebook\Markdown\Inlines\Context::prependInlineTypes()
14 | */
15 | public function getInlineTypes(): Container>;
16 |
17 | /**
18 | * @see Facebook\Markdown\UnparsedBlocks\Context::prependBlockTypes()
19 | */
20 | public function getBlockProducers(
21 | ): Container>;
22 | }
23 |
--------------------------------------------------------------------------------
/src/Nuxed/Markdown/XHPElement.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Markdown;
2 |
3 | use namespace Nuxed\Util;
4 |
5 | // Probably don't need XHPAlwaysValidChild - this is likely to be in a
6 | // or other similarly liberal container
7 | final class XHPElement implements \XHPUnsafeRenderable {
8 | use Util\StringableTrait;
9 |
10 | public function __construct(
11 | private string $markdown,
12 | private Environment $env,
13 | ) {
14 | }
15 |
16 | public function toHTMLString(): string {
17 | return $this->env->convert($this->markdown);
18 | }
19 |
20 | public function toString(): string {
21 | return $this->toHTMLString();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Nuxed/Mercure/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Mercure\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Mercure/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Mercure\Exception;
2 |
3 | final class InvalidArgumentException
4 | extends \InvalidArgumentException
5 | implements IException {}
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Mercure/IJwtProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Mercure;
2 |
3 | interface IJwtProvider {
4 | public function getJwt(): string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Mercure/Provider/StaticJwtProvider.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Mercure\Provider;
2 |
3 | use namespace Nuxed\Mercure;
4 |
5 | final class StaticJwtProvider implements Mercure\IJwtProvider {
6 | public function __construct(private string $jwt) {}
7 |
8 | public function getJwt(): string {
9 | return $this->jwt;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Nuxed/Mercure/Update.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Mercure;
2 |
3 | /**
4 | * Represents an update to send to the hub.
5 | *
6 | * @see https://github.com/dunglas/mercure/blob/master/spec/mercure.md#hub
7 | * @see https://github.com/dunglas/mercure/blob/master/hub/update.go
8 | */
9 | final class Update {
10 | public function __construct(
11 | private Container $topics,
12 | private string $data,
13 | private Container $targets = vec[],
14 | private ?string $id = null,
15 | private ?string $type = null,
16 | private ?int $retry = null,
17 | ) {}
18 |
19 | public function getTopics(): Container {
20 | return $this->topics;
21 | }
22 |
23 | public function getData(): string {
24 | return $this->data;
25 | }
26 |
27 | public function getTargets(): Container {
28 | return $this->targets;
29 | }
30 |
31 | public function getId(): ?string {
32 | return $this->id;
33 | }
34 |
35 | public function getType(): ?string {
36 | return $this->type;
37 | }
38 |
39 | public function getRetry(): ?int {
40 | return $this->retry;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Nuxed/Stopwatch/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Stopwatch\Exception;
2 |
3 | use type Exception;
4 |
5 | interface IException {
6 | require extends Exception;
7 | }
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Stopwatch/Exception/LogicException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Stopwatch\Exception;
2 |
3 | use type LogicException as ParentException;
4 |
5 | class LogicException extends ParentException implements IException {
6 | }
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Stopwatch/Period.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Stopwatch;
2 |
3 |
4 | class Period {
5 | private num $start;
6 | private num $end;
7 | private num $memory;
8 |
9 | /**
10 | * @param num $start The relative time of the start of the period (in milliseconds)
11 | * @param num $end The relative time of the end of the period (in milliseconds)
12 | * @param bool $morePrecision If true, time is stored as float to keep the original microsecond precision
13 | */
14 | public function __construct(
15 | num $start,
16 | num $end,
17 | bool $morePrecision = true,
18 | ) {
19 | $this->start = $morePrecision ? (float)$start : (int)$start;
20 | $this->end = $morePrecision ? (float)$end : (int)$end;
21 | $this->memory = \memory_get_usage(true);
22 | }
23 |
24 | /**
25 | * Gets the relative time of the start of the period.
26 | *
27 | * @return num The time (in milliseconds)
28 | */
29 | public function getStartTime(): num {
30 | return $this->start;
31 | }
32 | /**
33 | * Gets the relative time of the end of the period.
34 | *
35 | * @return num The time (in milliseconds)
36 | */
37 | public function getEndTime(): num {
38 | return $this->end;
39 | }
40 |
41 | /**
42 | * Gets the time spent in this period.
43 | *
44 | * @return num The period duration (in milliseconds)
45 | */
46 | public function getDuration(): num {
47 | return $this->end - $this->start;
48 | }
49 |
50 | /**
51 | * Gets the memory usage.
52 | *
53 | * @return int The memory usage (in bytes)
54 | */
55 | public function getMemory(): num {
56 | return $this->memory;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Catalogue/IOperation.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Catalogue;
2 |
3 | use namespace Nuxed\Translation;
4 |
5 | /**
6 | * Represents an operation on catalogue(s).
7 | *
8 | * An instance of this interface performs an operation on one or more catalogues and
9 | * stores intermediate and final results of the operation.
10 | *
11 | * The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and
12 | * the following results are stored:
13 | *
14 | * Messages: also called 'all', are valid messages for the given domain after the operation is performed.
15 | *
16 | * New Messages: also called 'new' (new = all ∖ source = {x: x ∈ all ∧ x ∉ source}).
17 | *
18 | * Obsolete Messages: also called 'obsolete' (obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ all}).
19 | *
20 | * Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'.
21 | */
22 | interface IOperation {
23 | /**
24 | * Returns domains affected by operation.
25 | */
26 | public function getDomains(): Container;
27 |
28 | /**
29 | * Returns all valid messages ('all') after operation.
30 | */
31 | public function getMessages(string $domain): KeyedContainer;
32 |
33 | /**
34 | * Returns new messages ('new') after operation.
35 | */
36 | public function getNewMessages(
37 | string $domain,
38 | ): KeyedContainer;
39 |
40 | /**
41 | * Returns obsolete messages ('obsolete') after operation.
42 | */
43 | public function getObsoleteMessages(
44 | string $domain,
45 | ): KeyedContainer;
46 |
47 | /**
48 | * Returns resulting catalogue ('result').
49 | */
50 | public function getResult(): Translation\MessageCatalogue;
51 | }
52 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Catalogue/MergeOperation.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Catalogue;
2 |
3 | /**
4 | * Merge operation between two catalogues as follows:
5 | * all = source ∪ target = {x: x ∈ source ∨ x ∈ target}
6 | * new = all ∖ source = {x: x ∈ target ∧ x ∉ source}
7 | * obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ source ∧ x ∉ target} = ∅
8 | * Basically, the result contains messages from both catalogues.
9 | */
10 | final class MergeOperation extends AbstractOperation {
11 | /**
12 | * {@inheritdoc}
13 | */
14 | <<__Override>>
15 | protected function processDomain(string $domain): void {
16 | $this->messages[$domain] = shape(
17 | 'all' => dict[],
18 | 'new' => dict[],
19 | 'obsolete' => dict[],
20 | );
21 | foreach ($this->source->domain($domain) as $id => $message) {
22 | $this->messages[$domain]['all'][$id] = $message;
23 | $this->result->add([$id => $message], $domain);
24 | }
25 |
26 | foreach ($this->target->domain($domain) as $id => $message) {
27 | if (!$this->source->has($id, $domain)) {
28 | $this->messages[$domain]['all'][$id] = $message;
29 | $this->messages[$domain]['new'][$id] = $message;
30 | $this->result->add([$id => $message], $domain);
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Exception;
2 |
3 | interface IException {
4 | require extends \Exception;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Exception/InvalidArgumentException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Exception;
2 |
3 | <<__Sealed(InvalidResourceException::class)>>
4 | class InvalidArgumentException
5 | extends \InvalidArgumentException
6 | implements IException {}
7 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Exception/InvalidResourceException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Exception;
2 |
3 | final class InvalidResourceException extends InvalidArgumentException {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Exception/LogicException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Exception;
2 |
3 | final class LogicException extends \LogicException implements IException {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Exception/NotFoundResourceException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Exception;
2 |
3 | final class NotFoundResourceException
4 | extends \RuntimeException
5 | implements IException {}
6 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Exception/RuntimeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Exception;
2 |
3 | final class RuntimeException extends \RuntimeException implements IException {}
4 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Format.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation;
2 |
3 | final class Format {
4 | const classname>
5 | Json = Loader\JsonFileLoader::class,
6 | Ini = Loader\IniFileLoader::class;
7 |
8 | const classname>> Tree =
9 | Loader\TreeLoader::class;
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Formatter/IMessageFormatter.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Formatter;
2 |
3 | interface IMessageFormatter {
4 | /**
5 | * Formats a localized message pattern with given arguments.
6 | */
7 | public function format(
8 | string $message,
9 | string $locale,
10 | KeyedContainer $parameters = dict[],
11 | ): string;
12 | }
13 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Formatter/MessageFormatter.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Formatter;
2 |
3 | use namespace HH\Lib\{C, Str};
4 | use namespace Nuxed\Translation\Exception;
5 |
6 | final class MessageFormatter implements IMessageFormatter {
7 |
8 |
9 | private dict> $cache = dict[];
10 |
11 | /**
12 | * {@inheritdoc}
13 | */
14 | public function format(
15 | string $message,
16 | string $locale,
17 | KeyedContainer $parameters = dict[],
18 | ): string {
19 | $formatter = $this->cache[$locale][$message] ?? null;
20 | if ($formatter is null) {
21 | try {
22 | $formatter = new \MessageFormatter($locale, $message);
23 | if (!C\contains_key($this->cache, $locale)) {
24 | $this->cache[$locale] = dict[];
25 | }
26 | $this->cache[$locale][$message] = $formatter;
27 | } catch (\Throwable $e) {
28 | throw new Exception\InvalidArgumentException(Str\format(
29 | 'Invalid message format (error #%d): %s.',
30 | \intl_get_error_code(),
31 | \intl_get_error_message(),
32 | ));
33 | }
34 | }
35 |
36 | $params = [];
37 | foreach ($parameters as $key => $value) {
38 | if (C\contains(['%', '{'], $key[0])) {
39 | $params[Str\trim($key, '%{ }')] = $value;
40 | } else {
41 | $params[$key] = $value;
42 | }
43 | }
44 |
45 | $message = $formatter->format($params);
46 | if (!$message is string) {
47 | throw new Exception\InvalidArgumentException(Str\format(
48 | 'Unable to format message (error #%s): %s.',
49 | $formatter->getErrorCode(),
50 | $formatter->getErrorMessage(),
51 | ));
52 | }
53 |
54 | return $message;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/ILocaleAware.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation;
2 |
3 | interface ILocaleAware {
4 | /**
5 | * Sets the current locale.
6 | *
7 | * @throws Exception\InvalidArgumentException If the locale contains invalid characters
8 | */
9 | public function setLocale(string $locale): void;
10 |
11 | /**
12 | * Returns the current locale.
13 | */
14 | public function getLocale(): string;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/ITranslator.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation;
2 |
3 | interface ITranslator {
4 | /**
5 | * Translates the given message.
6 | *
7 | * @param string $id
8 | * The message id (may also be an object that can be cast to string)
9 | * @param KeyedContainer $parameters
10 | * An array of parameters for the message
11 | * @param string|null $domain
12 | * The domain for the message or null to use the default
13 | * @param string|null $locale
14 | * The locale or null to use the default
15 | *
16 | * @return string The translated string
17 | *
18 | * @throws Exception\InvalidArgumentException If the locale contains invalid characters
19 | */
20 | public function trans(
21 | string $id,
22 | KeyedContainer $parameters = dict[],
23 | ?string $domain = null,
24 | ?string $locale = null,
25 | ): string;
26 | }
27 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/ITranslatorBag.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation;
2 |
3 | interface ITranslatorBag {
4 | /**
5 | * Gets the catalogue by locale
6 | *
7 | * @throws Exception\InvalidArgumentException If the locale contains invalid characters
8 | */
9 | public function getCatalogue(?string $locale = null): MessageCatalogue;
10 | }
11 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Loader/FileLoader.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Loader;
2 |
3 | use namespace Nuxed\{Filesystem, Translation};
4 | use namespace HH\Lib\Str;
5 | use namespace Nuxed\Translation\Exception;
6 |
7 | abstract class FileLoader implements ILoader {
8 | public function load(
9 | string $resource,
10 | string $locale,
11 | string $domain = 'messages',
12 | ): Translation\MessageCatalogue {
13 | $resource = Filesystem\Path::create($resource);
14 | if (!$resource->exists()) {
15 | throw new Exception\NotFoundResourceException(
16 | Str\format('File (%s) not found.', $resource->toString()),
17 | );
18 | }
19 |
20 | $resource = $this->loadResource($resource);
21 | return new TreeLoader() |> $$->load($resource, $locale, $domain);
22 | }
23 |
24 | /**
25 | * @return tree
26 | */
27 | abstract protected function loadResource(
28 | Filesystem\Path $resource,
29 | ): KeyedContainer;
30 | }
31 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Loader/ILoader.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Loader;
2 |
3 | use namespace Nuxed\Translation;
4 |
5 | interface ILoader {
6 | /**
7 | * Loads a locale.
8 | *
9 | * @throws Translation\Exception\NotFoundResourceException when the resource cannot be found
10 | * @throws Translation\Exception\InvalidResourceException when the resource cannot be loaded
11 | */
12 | public function load(
13 | T $resource,
14 | string $locale,
15 | string $domain = 'messages',
16 | ): Translation\MessageCatalogue;
17 | }
18 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Loader/IniFileLoader.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Loader;
2 |
3 | use namespace Nuxed\Filesystem;
4 | use namespace HH\Lib\Str;
5 | use namespace Facebook\TypeSpec;
6 | use namespace Nuxed\Translation\Exception;
7 |
8 | final class IniFileLoader extends FileLoader {
9 | <<__Override>>
10 | public function loadResource(
11 | Filesystem\Path $resource,
12 | ): KeyedContainer {
13 | $messages = @\parse_ini_file($resource->toString(), true);
14 | if (false === $messages) {
15 | throw new Exception\InvalidResourceException(
16 | Str\format('Error parsing ini file (%s).', $resource->toString()),
17 | );
18 | }
19 |
20 | return TypeSpec\dict(TypeSpec\string(), TypeSpec\mixed())
21 | ->coerceType($messages ?? dict[]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Loader/JsonFileLoader.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Loader;
2 |
3 | use namespace HH\Asio;
4 | use namespace HH\Lib\Str;
5 | use namespace Nuxed\Filesystem;
6 | use namespace Nuxed\Util\Json;
7 | use namespace Facebook\TypeSpec;
8 | use namespace Nuxed\Translation\Exception;
9 |
10 | final class JsonFileLoader extends FileLoader {
11 | <<__Override>>
12 | public function loadResource(
13 | Filesystem\Path $resoruce,
14 | ): KeyedContainer {
15 | $file = Filesystem\Node::load($resoruce) as Filesystem\File;
16 |
17 | try {
18 | $contents = Asio\join($file->read());
19 | $messages = Json\decode($contents);
20 | return TypeSpec\dict(TypeSpec\string(), TypeSpec\mixed())
21 | ->coerceType($messages ?? dict[]);
22 | } catch (Filesystem\Exception\IException $e) {
23 | throw new Exception\InvalidResourceException(
24 | Str\format('Unable to load file (%s).', $resoruce->toString()),
25 | $e->getCode(),
26 | $e,
27 | );
28 | } catch (Json\Exception\JsonDecodeException $e) {
29 | throw new Exception\InvalidResourceException(
30 | Str\format('Error parsing json file (%s).', $resoruce->toString()),
31 | $e->getCode(),
32 | $e,
33 | );
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Loader/TreeLoader.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Loader;
2 |
3 | use namespace HH\Lib\Str;
4 | use namespace Facebook\TypeSpec;
5 | use namespace Nuxed\Translation;
6 |
7 | final class TreeLoader implements ILoader> {
8 | /**
9 | * @param tree $resource
10 | */
11 | public function load(
12 | KeyedContainer $resource,
13 | string $locale,
14 | string $domain = 'messages',
15 | ): Translation\MessageCatalogue {
16 | $catalogue = new Translation\MessageCatalogue($locale);
17 | $catalogue->add($this->flatten($resource), $domain);
18 | return $catalogue;
19 | }
20 |
21 | final protected function flatten(
22 | KeyedContainer $tree,
23 | ): KeyedContainer {
24 | $result = dict[];
25 | foreach ($tree as $key => $value) {
26 | if ($value is arraykey || $value is num) {
27 | $result[$key] = $value is num
28 | ? Str\format_number($value, 2)
29 | : (string)$value;
30 | } else {
31 | $value = TypeSpec\dict(TypeSpec\string(), TypeSpec\mixed())
32 | ->coerceType($value);
33 | foreach ($this->flatten($value) as $k => $v) {
34 | $result[$key.'.'.$k] = $v;
35 | }
36 | }
37 | }
38 |
39 | return $result;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Reader/ITranslationReader.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Reader;
2 |
3 | use namespace Nuxed\Translation;
4 |
5 | /**
6 | * TranslationReader reads translation messages from translation files.
7 | */
8 | interface ITranslationReader {
9 | /**
10 | * Reads translation messages from a directory to the catalogue.
11 | */
12 | public function read(
13 | string $directory,
14 | Translation\MessageCatalogue $catalogue,
15 | ): void;
16 | }
17 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/Reader/TranslationReader.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\Reader;
2 |
3 | use namespace HH\Asio;
4 | use namespace HH\Lib\Str;
5 | use namespace Nuxed\{Filesystem, Translation};
6 | use namespace Nuxed\Translation\Loader;
7 |
8 | /**
9 | * TranslationReader reads translation messages from translation files.
10 | */
11 | final class TranslationReader implements ITranslationReader {
12 | /**
13 | * Loaders used for import.
14 | */
15 | private dict> $loaders = dict[];
16 |
17 | /**
18 | * Adds a loader to the translation reader.
19 | */
20 | public function addLoader(
21 | string $format,
22 | Loader\ILoader $loader,
23 | ): this {
24 | $this->loaders[$format] = $loader;
25 | return $this;
26 | }
27 |
28 | /**
29 | * Reads translation messages from a directory to the catalogue.
30 | */
31 | public function read(
32 | string $directory,
33 | Translation\MessageCatalogue $catalogue,
34 | ): void {
35 | try {
36 | $folder = Filesystem\Node::load(Filesystem\Path::create($directory)) as Filesystem\Folder;
37 | } catch (\Throwable $e) {
38 | return;
39 | }
40 |
41 | $files = Asio\join(Asio\wrap($folder->files(false, true)));
42 | if ($files->isFailed()) {
43 | return;
44 | } else {
45 | $files = $files->getResult();
46 | }
47 |
48 | foreach ($this->loaders as $format => $loader) {
49 | $extension = Str\format('.%s.%s', $catalogue->getLocale(), $format);
50 | foreach ($files as $file) {
51 | $basename = $file->path()->basename();
52 | if (Str\ends_with($basename, $extension)) {
53 | $domain = Str\strip_suffix($basename, $extension);
54 | $catalogue->addCatalogue(
55 | $loader->load(
56 | $file->path()->toString(),
57 | $catalogue->getLocale(),
58 | $domain,
59 | ),
60 | );
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Nuxed/Translation/_Private/LoaderContainer.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Translation\_Private;
2 |
3 | use namespace HH\Lib\{C, Str};
4 | use namespace Nuxed\Translation\{Exception, Loader};
5 |
6 | final class LoaderContainer {
7 | private dict $loaders = dict[];
8 |
9 | public function getLoader(
10 | classname> $format,
11 | ): Loader\ILoader {
12 | if (!C\contains_key($this->loaders, $format)) {
13 | throw new Exception\RuntimeException(
14 | Str\format('The "%s" translation loader is not registered.', $format),
15 | );
16 | }
17 |
18 | /* HH_IGNORE_ERROR[4110] */
19 | return $this->loaders[$format];
20 | }
21 |
22 | public function addLoader(
23 | classname> $format,
24 | Loader\ILoader $loader,
25 | ): void {
26 | $this->loaders[$format] = $loader;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Exception/IException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util\Exception;
2 |
3 | use type Exception;
4 |
5 | interface IException {
6 | require extends Exception;
7 | }
8 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Json/Errors.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util\Json;
2 |
3 | const dict Errors = dict[
4 | \JSON_ERROR_NONE => 'No error',
5 | \JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
6 | \JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
7 | \JSON_ERROR_CTRL_CHAR =>
8 | 'Control character error, possibly incorrectly encoded',
9 | \JSON_ERROR_SYNTAX => 'Syntax error',
10 | \JSON_ERROR_UTF8 =>
11 | 'Malformed UTF-8 characters, possibly incorrectly encoded',
12 | \JSON_ERROR_INF_OR_NAN => 'Inf and NaN cannot be JSON encoded',
13 | \JSON_ERROR_UNSUPPORTED_TYPE =>
14 | 'A value of a type that cannot be encoded was given',
15 | ];
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Json/Exception/JsonDecodeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util\Json\Exception;
2 |
3 | use namespace Nuxed\Util\Exception;
4 |
5 | class JsonDecodeException
6 | extends \InvalidArgumentException
7 | implements Exception\IException {
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Json/Exception/JsonEncodeException.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util\Json\Exception;
2 |
3 | use namespace Nuxed\Util\Exception;
4 |
5 | class JsonEncodeException
6 | extends \InvalidArgumentException
7 | implements Exception\IException {
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Json/decode.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util\Json;
2 |
3 | function decode(string $json, bool $assoc = true): dynamic {
4 | try {
5 | $value = \json_decode(
6 | $json,
7 | $assoc,
8 | 512,
9 | \JSON_BIGINT_AS_STRING | \JSON_FB_HACK_ARRAYS,
10 | );
11 | $error = \json_last_error();
12 | if (\JSON_ERROR_NONE !== $error) {
13 | throw new Exception\JsonDecodeException(Errors[$error], $error);
14 | }
15 |
16 | return $value;
17 | } catch (\Throwable $e) {
18 | throw new Exception\JsonDecodeException(
19 | $e->getMessage(),
20 | (int)$e->getCode(),
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Json/encode.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util\Json;
2 |
3 | use type Nuxed\Util\Jsonable;
4 |
5 | function encode(mixed $value, bool $pretty = false, int $flags = 0): string {
6 | if ($value is Jsonable) {
7 | return $value->toJson($pretty);
8 | }
9 |
10 | $flags |= \JSON_UNESCAPED_UNICODE |
11 | \JSON_UNESCAPED_SLASHES |
12 | \JSON_PRESERVE_ZERO_FRACTION;
13 | if ($pretty) {
14 | $flags |= \JSON_PRETTY_PRINT;
15 | }
16 |
17 | $json = \json_encode($value, $flags);
18 | $error = \json_last_error();
19 | if (\JSON_ERROR_NONE !== $error) {
20 | throw new Exception\JsonEncodeException(Errors[$error], $error);
21 | }
22 |
23 | return $json;
24 | }
25 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Json/spec.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util\Json;
2 |
3 | use namespace Facebook\{TypeAssert, TypeSpec};
4 |
5 | function spec(
6 | string $json,
7 | TypeSpec\TypeSpec $spec,
8 | bool $assert = false,
9 | ): T {
10 | $value = decode($json);
11 | try {
12 | if ($assert) {
13 | return $spec->assertType($value);
14 | }
15 |
16 | return $spec->coerceType($value);
17 | } catch (TypeAssert\TypeCoercionException $e) {
18 | throw new Exception\JsonDecodeException(
19 | $e->getMessage(),
20 | $e->getCode(),
21 | $e,
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Json/structure.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util\Json;
2 |
3 | use namespace Facebook\TypeAssert;
4 |
5 | function structure(string $json, TypeStructure $structure): T {
6 | try {
7 | return TypeAssert\matches_type_structure($structure, decode($json));
8 | } catch (TypeAssert\IncorrectTypeException $e) {
9 | throw new Exception\JsonDecodeException(
10 | $e->getMessage(),
11 | $e->getCode(),
12 | $e,
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Jsonable.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util;
2 |
3 | interface Jsonable {
4 | /**
5 | * Return a valid json string.
6 | * the implementation MUST not call Nuxed\Util\Json::encode
7 | * on it self, instead the inner data.
8 | *
9 | * e.g :
10 | *
11 | * public function toJson(): string
12 | * {
13 | * return \Nuxed\Util\Json::encode($this->data);
14 | * }
15 | *
16 | */
17 | public function toJson(bool $pretty = false): string;
18 | }
19 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/Stringable.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util;
2 |
3 | interface Stringable {
4 | /**
5 | * Return a string representing the current object.
6 | */
7 | public function toString(): string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/StringableTrait.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util;
2 |
3 | trait StringableTrait implements Stringable, \Stringish {
4 | abstract public function toString(): string;
5 |
6 | public function __toString(): string {
7 | try {
8 | return $this->toString();
9 | } catch (\Throwable $e) {
10 | return '';
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/alternatives.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util;
2 |
3 | use namespace HH\Lib\{Dict, Str, Vec};
4 |
5 | /**
6 | * @param string $name The original name of the item that does not exist
7 | * @param Container $items a container of possible items
8 | */
9 | <<__Memoize>>
10 | function alternatives(
11 | string $name,
12 | Container $items,
13 | ): Container {
14 | $alternatives = dict[];
15 | foreach ($items as $item) {
16 | $lev = \levenshtein($name, $item);
17 | if ($lev <= Str\length($name) / 3 || Str\contains($item, $name)) {
18 | $alternatives[$item] = $lev;
19 | }
20 | }
21 |
22 | return Vec\keys(Dict\sort($alternatives));
23 | }
24 |
--------------------------------------------------------------------------------
/src/Nuxed/Util/stringify.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Util;
2 |
3 | use namespace HH\Lib\Str;
4 |
5 | function stringify(mixed $value): string {
6 | if ($value is bool) {
7 | $value = ($value ? 'true' : 'false');
8 | } else if ($value is string) {
9 | $value = '"'.$value.'"';
10 | } else if ($value is num) {
11 | $value = $value is int ? $value : Str\format_number($value, 1);
12 | } else if ($value is resource) {
13 | $value = 'resource['.\get_resource_type($value).']';
14 | } else if ($value is null) {
15 | $value = 'null';
16 | } else if (\is_object($value) && !$value is Container<_>) {
17 | if ($value is \Throwable) {
18 | $value = \get_class($value).
19 | '['.
20 | 'message='.
21 | stringify($value->getMessage()).
22 | ', code='.
23 | stringify($value->getCode()).
24 | ', file='.
25 | stringify($value->getFile()).
26 | ', line='.
27 | stringify($value->getLine()).
28 | ', trace= '.
29 | stringify($value->getTrace()).
30 | ', previous='.
31 | stringify($value->getPrevious()).
32 | ']';
33 | } else if ($value is \DateTimeInterface) {
34 | $value = \get_class($value).'['.$value->format("Y-m-d\TH:i:s.uP").']';
35 | } else {
36 | $value = 'object['.\get_class($value).']';
37 | }
38 | } else if ($value is Container<_>) {
39 | $value = Json\encode($value, false);
40 | } else {
41 | $value = '!'.\gettype($value).Json\encode($value, false);
42 | }
43 |
44 | return (string)$value;
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Nuxed/EventDispatcher/Fixture/OrderCanceledEvent.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Test\EventDispatcher\Fixture;
2 |
3 | use namespace Nuxed\EventDispatcher;
4 |
5 | final class OrderCanceledEvent implements EventDispatcher\IStoppableEvent {
6 | public bool $handled = false;
7 |
8 | public function __construct(public string $orderId) {}
9 |
10 | public function isPropagationStopped(): bool {
11 | return $this->handled;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/Nuxed/EventDispatcher/Fixture/OrderCanceledEventListener.hack:
--------------------------------------------------------------------------------
1 | namespace Nuxed\Test\EventDispatcher\Fixture;
2 |
3 | use namespace Nuxed\EventDispatcher;
4 |
5 | final class OrderCanceledEventListener
6 | implements EventDispatcher\IEventListener {
7 |
8 | public function __construct(
9 | public string $append,
10 | private bool $handle = false,
11 | ) {}
12 |
13 | public async function process(OrderCanceledEvent $event): Awaitable