From ad326b4957007f32fe8bd30f257aab0349f6fc7d Mon Sep 17 00:00:00 2001 From: shiva Date: Sun, 4 Apr 2021 15:17:24 +0530 Subject: [PATCH 01/19] first commit --- README.txt | 163 + classmap.php | 57 + css/algolia-autocomplete.css | 180 + css/algolia-instantsearch.css | 330 + css/algolia-logo.svg | 9 + css/index.php | 1 + ...lass-typesense-admin-page-autocomplete.php | 287 + ...ass-typesense-admin-page-native-search.php | 244 + .../class-typesense-admin-page-settings.php | 508 + includes/admin/class-typesense-admin.php | 331 + includes/admin/css/index.php | 1 + includes/admin/css/typesense-admin.css | 61 + includes/admin/fonts/algolia.eot | Bin 0 -> 1412 bytes includes/admin/fonts/algolia.svg | 10 + includes/admin/fonts/algolia.ttf | Bin 0 -> 1248 bytes includes/admin/fonts/algolia.woff | Bin 0 -> 1324 bytes includes/admin/fonts/index.php | 1 + includes/admin/img/index.php | 1 + includes/admin/img/logo-algolia-avatar.svg | 1 + includes/admin/index.php | 1 + includes/admin/js/algolia-admin.js | 23 + includes/admin/js/index.php | 1 + includes/admin/js/push-settings-button.js | 50 + includes/admin/js/reindex-button.js | 46 + includes/admin/partials/form-options.php | 22 + .../partials/form-override-search-option.php | 71 + includes/admin/partials/index-records.php | 22 + includes/admin/partials/index.php | 1 + .../partials/page-autocomplete-config.php | 66 + includes/admin/partials/page-autocomplete.php | 22 + includes/admin/partials/page-search.php | 30 + includes/class-typesense-api.php | 247 + .../class-typesense-autocomplete-config.php | 192 + includes/class-typesense-cli.php | 144 + includes/class-typesense-compatibility.php | 123 + includes/class-typesense-plugin.php | 452 + includes/class-typesense-scripts.php | 86 + includes/class-typesense-search.php | 359 + includes/class-typesense-settings.php | 483 + includes/class-typesense-styles.php | 50 + includes/class-typesense-template-loader.php | 242 + includes/class-typesense-utils.php | 269 + .../class-typesense-plugin-factory.php | 45 + includes/index.php | 1 + .../indices/class-typesense-index-replica.php | 111 + includes/indices/class-typesense-index.php | 812 + .../indices/class-typesense-posts-index.php | 504 + ...class-typesense-searchable-posts-index.php | 481 + .../indices/class-typesense-terms-index.php | 252 + .../indices/class-typesense-users-index.php | 235 + includes/indices/index.php | 1 + .../.circleci/config.yml | 151 + .../.circleci/index-cleanup.php | 19 + .../algoliasearch-client-php/.dockerignore | 5 + .../algoliasearch-client-php/DOCKER_README.MD | 81 + .../algoliasearch-client-php/Dockerfile | 18 + .../algoliasearch-client-php/LICENSE | 21 + .../algoliasearch-client-php/SECURITY.md | 3 + .../algoliasearch-client-php/autoload.php | 76 + .../src/AccountClient.php | 40 + .../algoliasearch-client-php/src/Algolia.php | 118 + .../src/AnalyticsClient.php | 139 + .../src/Cache/FileCacheDriver.php | 117 + .../src/Cache/NullCacheDriver.php | 78 + .../src/Config/AbstractConfig.php | 118 + .../src/Config/AnalyticsConfig.php | 29 + .../src/Config/InsightsConfig.php | 29 + .../src/Config/PlacesConfig.php | 16 + .../src/Config/RecommendationConfig.php | 29 + .../src/Config/SearchConfig.php | 82 + .../src/Exceptions/AlgoliaException.php | 7 + .../src/Exceptions/BadRequestException.php | 7 + .../src/Exceptions/CannotWaitException.php | 7 + .../src/Exceptions/MissingObjectId.php | 7 + .../src/Exceptions/NotFoundException.php | 7 + .../Exceptions/ObjectNotFoundException.php | 7 + .../src/Exceptions/RequestException.php | 22 + .../src/Exceptions/RetriableException.php | 7 + .../src/Exceptions/UnreachableException.php | 15 + .../ValidUntilNotFoundException.php | 7 + .../src/Http/Guzzle6HttpClient.php | 53 + .../src/Http/HttpClientInterface.php | 24 + .../src/Http/Php53HttpClient.php | 131 + .../src/Http/Psr7/BufferStream.php | 141 + .../src/Http/Psr7/PumpStream.php | 169 + .../src/Http/Psr7/Request.php | 306 + .../src/Http/Psr7/Response.php | 312 + .../src/Http/Psr7/Stream.php | 278 + .../src/Http/Psr7/Uri.php | 710 + .../src/Http/Psr7/UriResolver.php | 227 + .../src/Http/Psr7/functions.php | 82 + .../src/Insights/UserInsightsClient.php | 137 + .../src/InsightsClient.php | 71 + .../src/Iterators/AbstractAlgoliaIterator.php | 128 + .../src/Iterators/ObjectIterator.php | 47 + .../src/Iterators/RuleIterator.php | 34 + .../src/Iterators/SynonymIterator.php | 34 + .../src/Log/DebugLogger.php | 57 + .../src/PlacesClient.php | 83 + .../src/RecommendationClient.php | 88 + .../src/RequestOptions/RequestOptions.php | 368 + .../RequestOptions/RequestOptionsFactory.php | 132 + .../src/Response/AbstractResponse.php | 53 + .../src/Response/AddApiKeyResponse.php | 54 + .../src/Response/BatchIndexingResponse.php | 73 + .../src/Response/DeleteApiKeyResponse.php | 57 + .../src/Response/IndexingResponse.php | 29 + .../src/Response/MultiResponse.php | 52 + .../MultipleIndexBatchIndexingResponse.php | 34 + .../src/Response/NullResponse.php | 16 + .../src/Response/RestoreApiKeyResponse.php | 67 + .../src/Response/UpdateApiKeyResponse.php | 86 + .../src/RetryStrategy/ApiWrapper.php | 269 + .../src/RetryStrategy/ApiWrapperInterface.php | 12 + .../src/RetryStrategy/ClusterHosts.php | 181 + .../src/RetryStrategy/Host.php | 65 + .../src/RetryStrategy/HostCollection.php | 85 + .../src/SearchClient.php | 453 + .../src/SearchIndex.php | 775 + .../src/Support/Helpers.php | 129 + .../src/Support/UserAgent.php | 59 + .../src/functions.php | 8 + .../vendor/psr/http-message/CHANGELOG.md | 36 + .../vendor/psr/http-message/LICENSE | 19 + .../psr/http-message/src/MessageInterface.php | 187 + .../psr/http-message/src/RequestInterface.php | 129 + .../http-message/src/ResponseInterface.php | 68 + .../src/ServerRequestInterface.php | 261 + .../psr/http-message/src/StreamInterface.php | 158 + .../src/UploadedFileInterface.php | 123 + .../psr/http-message/src/UriInterface.php | 323 + .../vendor/psr/log/LICENSE | 19 + .../vendor/psr/log/Psr/Log/AbstractLogger.php | 128 + .../log/Psr/Log/InvalidArgumentException.php | 7 + .../vendor/psr/log/Psr/Log/LogLevel.php | 18 + .../psr/log/Psr/Log/LoggerAwareInterface.php | 18 + .../psr/log/Psr/Log/LoggerAwareTrait.php | 26 + .../psr/log/Psr/Log/LoggerInterface.php | 123 + .../vendor/psr/log/Psr/Log/LoggerTrait.php | 140 + .../vendor/psr/log/Psr/Log/NullLogger.php | 28 + .../log/Psr/Log/Test/LoggerInterfaceTest.php | 140 + .../vendor/psr/simple-cache/LICENSE.md | 21 + .../psr/simple-cache/src/CacheException.php | 10 + .../psr/simple-cache/src/CacheInterface.php | 114 + .../src/InvalidArgumentException.php | 13 + .../typesensesearch-client-php/composer.json | 6 + .../typesensesearch-client-php/composer.lock | 1289 + .../vendor/autoload.php | 7 + .../clue/stream-filter/.github/FUNDING.yml | 2 + .../vendor/clue/stream-filter/CHANGELOG.md | 75 + .../vendor/clue/stream-filter/LICENSE | 21 + .../vendor/clue/stream-filter/README.md | 323 + .../vendor/clue/stream-filter/composer.json | 26 + .../clue/stream-filter/src/CallbackFilter.php | 114 + .../clue/stream-filter/src/functions.php | 327 + .../stream-filter/src/functions_include.php | 6 + .../vendor/composer/ClassLoader.php | 479 + .../vendor/composer/InstalledVersions.php | 503 + .../vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 15 + .../vendor/composer/autoload_files.php | 14 + .../vendor/composer/autoload_namespaces.php | 9 + .../vendor/composer/autoload_psr4.php | 25 + .../vendor/composer/autoload_real.php | 75 + .../vendor/composer/autoload_static.php | 144 + .../vendor/composer/installed.json | 1333 + .../vendor/composer/installed.php | 244 + .../vendor/composer/platform_check.php | 26 + .../vendor/monolog/monolog/CHANGELOG.md | 522 + .../vendor/monolog/monolog/LICENSE | 19 + .../vendor/monolog/monolog/README.md | 109 + .../vendor/monolog/monolog/UPGRADE.md | 72 + .../vendor/monolog/monolog/composer.json | 72 + .../vendor/monolog/monolog/phpstan.neon.dist | 20 + .../monolog/src/Monolog/DateTimeImmutable.php | 49 + .../monolog/src/Monolog/ErrorHandler.php | 266 + .../Monolog/Formatter/ChromePHPFormatter.php | 81 + .../Monolog/Formatter/ElasticaFormatter.php | 85 + .../Formatter/ElasticsearchFormatter.php | 89 + .../Monolog/Formatter/FlowdockFormatter.php | 107 + .../Monolog/Formatter/FluentdFormatter.php | 88 + .../Monolog/Formatter/FormatterInterface.php | 36 + .../Formatter/GelfMessageFormatter.php | 147 + .../src/Monolog/Formatter/HtmlFormatter.php | 139 + .../src/Monolog/Formatter/JsonFormatter.php | 188 + .../src/Monolog/Formatter/LineFormatter.php | 196 + .../src/Monolog/Formatter/LogglyFormatter.php | 45 + .../Monolog/Formatter/LogmaticFormatter.php | 66 + .../Monolog/Formatter/LogstashFormatter.php | 101 + .../Monolog/Formatter/MongoDBFormatter.php | 141 + .../Monolog/Formatter/NormalizerFormatter.php | 271 + .../src/Monolog/Formatter/ScalarFormatter.php | 48 + .../Monolog/Formatter/WildfireFormatter.php | 129 + .../src/Monolog/Handler/AbstractHandler.php | 96 + .../Handler/AbstractProcessingHandler.php | 58 + .../Monolog/Handler/AbstractSyslogHandler.php | 102 + .../src/Monolog/Handler/AmqpHandler.php | 137 + .../Monolog/Handler/BrowserConsoleHandler.php | 243 + .../src/Monolog/Handler/BufferHandler.php | 161 + .../src/Monolog/Handler/ChromePHPHandler.php | 193 + .../src/Monolog/Handler/CouchDBHandler.php | 73 + .../src/Monolog/Handler/CubeHandler.php | 155 + .../monolog/src/Monolog/Handler/Curl/Util.php | 70 + .../Monolog/Handler/DeduplicationHandler.php | 173 + .../Handler/DoctrineCouchDBHandler.php | 46 + .../src/Monolog/Handler/DynamoDbHandler.php | 103 + .../src/Monolog/Handler/ElasticaHandler.php | 124 + .../Monolog/Handler/ElasticsearchHandler.php | 189 + .../src/Monolog/Handler/ErrorLogHandler.php | 86 + .../Monolog/Handler/FallbackGroupHandler.php | 60 + .../src/Monolog/Handler/FilterHandler.php | 189 + .../ActivationStrategyInterface.php | 25 + .../ChannelLevelActivationStrategy.php | 66 + .../ErrorLevelActivationStrategy.php | 40 + .../Monolog/Handler/FingersCrossedHandler.php | 228 + .../src/Monolog/Handler/FirePHPHandler.php | 166 + .../src/Monolog/Handler/FleepHookHandler.php | 116 + .../src/Monolog/Handler/FlowdockHandler.php | 116 + .../Handler/FormattableHandlerInterface.php | 37 + .../Handler/FormattableHandlerTrait.php | 60 + .../src/Monolog/Handler/GelfHandler.php | 59 + .../src/Monolog/Handler/GroupHandler.php | 127 + .../monolog/src/Monolog/Handler/Handler.php | 53 + .../src/Monolog/Handler/HandlerInterface.php | 76 + .../src/Monolog/Handler/HandlerWrapper.php | 136 + .../src/Monolog/Handler/IFTTTHandler.php | 70 + .../src/Monolog/Handler/InsightOpsHandler.php | 59 + .../src/Monolog/Handler/LogEntriesHandler.php | 53 + .../src/Monolog/Handler/LogglyHandler.php | 160 + .../src/Monolog/Handler/LogmaticHandler.php | 88 + .../src/Monolog/Handler/MailHandler.php | 85 + .../src/Monolog/Handler/MandrillHandler.php | 85 + .../Handler/MissingExtensionException.php | 21 + .../src/Monolog/Handler/MongoDBHandler.php | 85 + .../Monolog/Handler/NativeMailerHandler.php | 176 + .../src/Monolog/Handler/NewRelicHandler.php | 197 + .../src/Monolog/Handler/NoopHandler.php | 40 + .../src/Monolog/Handler/NullHandler.php | 54 + .../src/Monolog/Handler/OverflowHandler.php | 154 + .../src/Monolog/Handler/PHPConsoleHandler.php | 240 + .../src/Monolog/Handler/ProcessHandler.php | 193 + .../Handler/ProcessableHandlerInterface.php | 42 + .../Handler/ProcessableHandlerTrait.php | 70 + .../src/Monolog/Handler/PsrHandler.php | 97 + .../src/Monolog/Handler/PushoverHandler.php | 203 + .../src/Monolog/Handler/RedisHandler.php | 96 + .../Monolog/Handler/RedisPubSubHandler.php | 67 + .../src/Monolog/Handler/RollbarHandler.php | 130 + .../Monolog/Handler/RotatingFileHandler.php | 194 + .../src/Monolog/Handler/SamplingHandler.php | 125 + .../src/Monolog/Handler/SendGridHandler.php | 100 + .../src/Monolog/Handler/Slack/SlackRecord.php | 356 + .../src/Monolog/Handler/SlackHandler.php | 233 + .../Monolog/Handler/SlackWebhookHandler.php | 130 + .../src/Monolog/Handler/SocketHandler.php | 380 + .../src/Monolog/Handler/SqsHandler.php | 64 + .../src/Monolog/Handler/StreamHandler.php | 177 + .../Monolog/Handler/SwiftMailerHandler.php | 106 + .../src/Monolog/Handler/SyslogHandler.php | 67 + .../Monolog/Handler/SyslogUdp/UdpSocket.php | 69 + .../src/Monolog/Handler/SyslogUdpHandler.php | 127 + .../Monolog/Handler/TelegramBotHandler.php | 181 + .../src/Monolog/Handler/TestHandler.php | 193 + .../Handler/WebRequestRecognizerTrait.php | 24 + .../Handler/WhatFailureGroupHandler.php | 63 + .../Monolog/Handler/ZendMonitorHandler.php | 96 + .../monolog/monolog/src/Monolog/Logger.php | 611 + .../src/Monolog/Processor/GitProcessor.php | 63 + .../Monolog/Processor/HostnameProcessor.php | 32 + .../Processor/IntrospectionProcessor.php | 108 + .../Processor/MemoryPeakUsageProcessor.php | 34 + .../src/Monolog/Processor/MemoryProcessor.php | 61 + .../Processor/MemoryUsageProcessor.php | 34 + .../Monolog/Processor/MercurialProcessor.php | 63 + .../Monolog/Processor/ProcessIdProcessor.php | 27 + .../Monolog/Processor/ProcessorInterface.php | 25 + .../Processor/PsrLogMessageProcessor.php | 87 + .../src/Monolog/Processor/TagProcessor.php | 48 + .../src/Monolog/Processor/UidProcessor.php | 55 + .../src/Monolog/Processor/WebProcessor.php | 100 + .../monolog/monolog/src/Monolog/Registry.php | 133 + .../src/Monolog/ResettableInterface.php | 34 + .../monolog/src/Monolog/SignalHandler.php | 99 + .../monolog/src/Monolog/Test/TestCase.php | 63 + .../monolog/monolog/src/Monolog/Utils.php | 196 + .../vendor/nyholm/psr7/CHANGELOG.md | 115 + .../vendor/nyholm/psr7/LICENSE | 21 + .../vendor/nyholm/psr7/README.md | 110 + .../vendor/nyholm/psr7/composer.json | 48 + .../vendor/nyholm/psr7/doc/final.md | 20 + .../vendor/nyholm/psr7/psalm.xml | 15 + .../psr7/src/Factory/HttplugFactory.php | 42 + .../nyholm/psr7/src/Factory/Psr17Factory.php | 75 + .../vendor/nyholm/psr7/src/MessageTrait.php | 207 + .../vendor/nyholm/psr7/src/Request.php | 47 + .../vendor/nyholm/psr7/src/RequestTrait.php | 113 + .../vendor/nyholm/psr7/src/Response.php | 90 + .../vendor/nyholm/psr7/src/ServerRequest.php | 164 + .../vendor/nyholm/psr7/src/Stream.php | 287 + .../vendor/nyholm/psr7/src/UploadedFile.php | 173 + .../vendor/nyholm/psr7/src/Uri.php | 312 + .../php-http/client-common/.php_cs.dist | 24 + .../php-http/client-common/CHANGELOG.md | 247 + .../vendor/php-http/client-common/LICENSE | 19 + .../vendor/php-http/client-common/README.md | 55 + .../php-http/client-common/composer.json | 67 + .../client-common/src/BatchClient.php | 42 + .../src/BatchClientInterface.php | 34 + .../client-common/src/BatchResult.php | 157 + .../php-http/client-common/src/Deferred.php | 152 + .../src/EmulatedHttpAsyncClient.php | 25 + .../client-common/src/EmulatedHttpClient.php | 24 + .../src/Exception/BatchException.php | 37 + .../CircularRedirectionException.php | 16 + .../src/Exception/ClientErrorException.php | 16 + .../Exception/HttpClientNoMatchException.php | 33 + .../Exception/HttpClientNotFoundException.php | 16 + .../src/Exception/LoopException.php | 16 + .../MultipleRedirectionException.php | 16 + .../src/Exception/ServerErrorException.php | 16 + .../client-common/src/FlexibleHttpClient.php | 36 + .../src/HttpAsyncClientDecorator.php | 31 + .../src/HttpAsyncClientEmulator.php | 39 + .../client-common/src/HttpClientDecorator.php | 32 + .../client-common/src/HttpClientEmulator.php | 35 + .../client-common/src/HttpClientPool.php | 24 + .../src/HttpClientPool/HttpClientPool.php | 70 + .../src/HttpClientPool/HttpClientPoolItem.php | 181 + .../HttpClientPool/LeastUsedClientPool.php | 45 + .../src/HttpClientPool/RandomClientPool.php | 31 + .../HttpClientPool/RoundRobinClientPool.php | 42 + .../client-common/src/HttpClientRouter.php | 74 + .../src/HttpClientRouterInterface.php | 27 + .../client-common/src/HttpMethodsClient.php | 150 + .../src/HttpMethodsClientInterface.php | 116 + .../php-http/client-common/src/Plugin.php | 33 + .../src/Plugin/AddHostPlugin.php | 76 + .../src/Plugin/AddPathPlugin.php | 78 + .../src/Plugin/AuthenticationPlugin.php | 38 + .../src/Plugin/BaseUriPlugin.php | 57 + .../src/Plugin/ContentLengthPlugin.php | 39 + .../src/Plugin/ContentTypePlugin.php | 122 + .../client-common/src/Plugin/CookiePlugin.php | 180 + .../src/Plugin/DecoderPlugin.php | 135 + .../client-common/src/Plugin/ErrorPlugin.php | 92 + .../src/Plugin/HeaderAppendPlugin.php | 48 + .../src/Plugin/HeaderDefaultsPlugin.php | 46 + .../src/Plugin/HeaderRemovePlugin.php | 44 + .../src/Plugin/HeaderSetPlugin.php | 44 + .../src/Plugin/HistoryPlugin.php | 49 + .../client-common/src/Plugin/Journal.php | 33 + .../src/Plugin/QueryDefaultsPlugin.php | 50 + .../src/Plugin/RedirectPlugin.php | 258 + .../src/Plugin/RequestMatcherPlugin.php | 56 + .../src/Plugin/RequestSeekableBodyPlugin.php | 29 + .../src/Plugin/ResponseSeekableBodyPlugin.php | 32 + .../client-common/src/Plugin/RetryPlugin.php | 177 + .../src/Plugin/SeekableBodyPlugin.php | 47 + .../src/Plugin/VersionBridgePlugin.php | 24 + .../client-common/src/PluginChain.php | 65 + .../client-common/src/PluginClient.php | 133 + .../client-common/src/PluginClientBuilder.php | 76 + .../client-common/src/PluginClientFactory.php | 68 + .../client-common/src/VersionBridgeClient.php | 24 + .../.github/workflows/Build-Test.yml | 90 + .../vendor/php-http/curl-client/.php_cs | 9 + .../vendor/php-http/curl-client/CHANGELOG.md | 204 + .../vendor/php-http/curl-client/LICENSE | 19 + .../vendor/php-http/curl-client/README.md | 44 + .../vendor/php-http/curl-client/composer.json | 57 + .../vendor/php-http/curl-client/puli.json | 242 + .../php-http/curl-client/src/Client.php | 398 + .../php-http/curl-client/src/CurlPromise.php | 111 + .../php-http/curl-client/src/MultiRunner.php | 131 + .../php-http/curl-client/src/PromiseCore.php | 252 + .../curl-client/src/ResponseBuilder.php | 24 + .../vendor/php-http/discovery/CHANGELOG.md | 307 + .../vendor/php-http/discovery/LICENSE | 19 + .../vendor/php-http/discovery/README.md | 46 + .../vendor/php-http/discovery/composer.json | 51 + .../php-http/discovery/src/ClassDiscovery.php | 246 + .../php-http/discovery/src/Exception.php | 12 + .../ClassInstantiationFailedException.php | 14 + .../Exception/DiscoveryFailedException.php | 51 + .../Exception/NoCandidateFoundException.php | 47 + .../src/Exception/NotFoundException.php | 16 + .../Exception/PuliUnavailableException.php | 12 + .../StrategyUnavailableException.php | 15 + .../src/HttpAsyncClientDiscovery.php | 32 + .../discovery/src/HttpClientDiscovery.php | 32 + .../discovery/src/MessageFactoryDiscovery.php | 34 + .../discovery/src/NotFoundException.php | 14 + .../discovery/src/Psr17FactoryDiscovery.php | 136 + .../discovery/src/Psr18ClientDiscovery.php | 32 + .../src/Strategy/CommonClassesStrategy.php | 181 + .../Strategy/CommonPsr17ClassesStrategy.php | 105 + .../src/Strategy/DiscoveryStrategy.php | 23 + .../src/Strategy/MockClientStrategy.php | 27 + .../src/Strategy/PuliBetaStrategy.php | 92 + .../discovery/src/StreamFactoryDiscovery.php | 34 + .../discovery/src/UriFactoryDiscovery.php | 34 + .../vendor/php-http/httplug/CHANGELOG.md | 129 + .../vendor/php-http/httplug/LICENSE | 20 + .../vendor/php-http/httplug/README.md | 63 + .../vendor/php-http/httplug/composer.json | 45 + .../vendor/php-http/httplug/puli.json | 12 + .../vendor/php-http/httplug/src/Exception.php | 14 + .../httplug/src/Exception/HttpException.php | 65 + .../src/Exception/NetworkException.php | 28 + .../src/Exception/RequestAwareTrait.php | 26 + .../src/Exception/RequestException.php | 29 + .../src/Exception/TransferException.php | 14 + .../php-http/httplug/src/HttpAsyncClient.php | 25 + .../php-http/httplug/src/HttpClient.php | 15 + .../src/Promise/HttpFulfilledPromise.php | 54 + .../src/Promise/HttpRejectedPromise.php | 53 + .../php-http/message-factory/CHANGELOG.md | 65 + .../vendor/php-http/message-factory/LICENSE | 19 + .../vendor/php-http/message-factory/README.md | 36 + .../php-http/message-factory/composer.json | 27 + .../vendor/php-http/message-factory/puli.json | 43 + .../message-factory/src/MessageFactory.php | 12 + .../message-factory/src/RequestFactory.php | 34 + .../message-factory/src/ResponseFactory.php | 35 + .../message-factory/src/StreamFactory.php | 25 + .../message-factory/src/UriFactory.php | 24 + .../vendor/php-http/message/CHANGELOG.md | 237 + .../vendor/php-http/message/LICENSE | 19 + .../vendor/php-http/message/README.md | 51 + .../vendor/php-http/message/apigen.neon | 6 + .../vendor/php-http/message/composer.json | 65 + .../vendor/php-http/message/puli.json | 111 + .../php-http/message/src/Authentication.php | 25 + .../src/Authentication/AutoBasicAuth.php | 48 + .../message/src/Authentication/BasicAuth.php | 44 + .../message/src/Authentication/Bearer.php | 37 + .../message/src/Authentication/Chain.php | 47 + .../message/src/Authentication/Header.php | 33 + .../message/src/Authentication/Matching.php | 69 + .../message/src/Authentication/QueryParam.php | 47 + .../src/Authentication/RequestConditional.php | 43 + .../message/src/Authentication/Wsse.php | 68 + .../message/src/Builder/ResponseBuilder.php | 146 + .../vendor/php-http/message/src/Cookie.php | 524 + .../vendor/php-http/message/src/CookieJar.php | 210 + .../php-http/message/src/CookieUtil.php | 53 + .../src/Decorator/MessageDecorator.php | 133 + .../src/Decorator/RequestDecorator.php | 86 + .../src/Decorator/ResponseDecorator.php | 55 + .../message/src/Decorator/StreamDecorator.php | 138 + .../message/src/Encoding/ChunkStream.php | 39 + .../message/src/Encoding/CompressStream.php | 45 + .../message/src/Encoding/DechunkStream.php | 29 + .../message/src/Encoding/DecompressStream.php | 45 + .../message/src/Encoding/DeflateStream.php | 41 + .../message/src/Encoding/Filter/Chunk.php | 30 + .../message/src/Encoding/FilteredStream.php | 234 + .../message/src/Encoding/GzipDecodeStream.php | 45 + .../message/src/Encoding/GzipEncodeStream.php | 45 + .../message/src/Encoding/InflateStream.php | 45 + .../vendor/php-http/message/src/Exception.php | 10 + .../Exception/UnexpectedValueException.php | 9 + .../vendor/php-http/message/src/Formatter.php | 28 + .../src/Formatter/CurlCommandFormatter.php | 93 + .../Formatter/FullHttpMessageFormatter.php | 100 + .../message/src/Formatter/SimpleFormatter.php | 42 + .../DiactorosMessageFactory.php | 82 + .../MessageFactory/GuzzleMessageFactory.php | 55 + .../src/MessageFactory/SlimMessageFactory.php | 74 + .../php-http/message/src/RequestMatcher.php | 26 + .../RequestMatcher/CallbackRequestMatcher.php | 32 + .../RequestMatcher/RegexRequestMatcher.php | 41 + .../src/RequestMatcher/RequestMatcher.php | 78 + .../message/src/Stream/BufferedStream.php | 270 + .../StreamFactory/DiactorosStreamFactory.php | 48 + .../src/StreamFactory/GuzzleStreamFactory.php | 23 + .../src/StreamFactory/SlimStreamFactory.php | 39 + .../src/UriFactory/DiactorosUriFactory.php | 36 + .../src/UriFactory/GuzzleUriFactory.php | 24 + .../message/src/UriFactory/SlimUriFactory.php | 33 + .../vendor/php-http/message/src/filters.php | 6 + .../vendor/php-http/promise/CHANGELOG.md | 48 + .../vendor/php-http/promise/LICENSE | 19 + .../vendor/php-http/promise/README.md | 48 + .../vendor/php-http/promise/composer.json | 38 + .../php-http/promise/src/FulfilledPromise.php | 58 + .../vendor/php-http/promise/src/Promise.php | 69 + .../php-http/promise/src/RejectedPromise.php | 58 + .../vendor/psr/http-client/CHANGELOG.md | 23 + .../vendor/psr/http-client/LICENSE | 19 + .../vendor/psr/http-client/README.md | 12 + .../vendor/psr/http-client/composer.json | 27 + .../src/ClientExceptionInterface.php | 10 + .../psr/http-client/src/ClientInterface.php | 20 + .../src/NetworkExceptionInterface.php | 24 + .../src/RequestExceptionInterface.php | 24 + .../vendor/psr/http-factory/.gitignore | 2 + .../vendor/psr/http-factory/.pullapprove.yml | 7 + .../vendor/psr/http-factory/LICENSE | 21 + .../vendor/psr/http-factory/README.md | 10 + .../vendor/psr/http-factory/composer.json | 35 + .../src/RequestFactoryInterface.php | 18 + .../src/ResponseFactoryInterface.php | 18 + .../src/ServerRequestFactoryInterface.php | 24 + .../src/StreamFactoryInterface.php | 45 + .../src/UploadedFileFactoryInterface.php | 34 + .../http-factory/src/UriFactoryInterface.php | 17 + .../vendor/psr/http-message/CHANGELOG.md | 36 + .../vendor/psr/http-message/LICENSE | 19 + .../vendor/psr/http-message/README.md | 13 + .../vendor/psr/http-message/composer.json | 26 + .../psr/http-message/src/MessageInterface.php | 187 + .../psr/http-message/src/RequestInterface.php | 129 + .../http-message/src/ResponseInterface.php | 68 + .../src/ServerRequestInterface.php | 261 + .../psr/http-message/src/StreamInterface.php | 158 + .../src/UploadedFileInterface.php | 123 + .../psr/http-message/src/UriInterface.php | 323 + .../vendor/psr/log/LICENSE | 19 + .../vendor/psr/log/Psr/Log/AbstractLogger.php | 128 + .../log/Psr/Log/InvalidArgumentException.php | 7 + .../vendor/psr/log/Psr/Log/LogLevel.php | 18 + .../psr/log/Psr/Log/LoggerAwareInterface.php | 18 + .../psr/log/Psr/Log/LoggerAwareTrait.php | 26 + .../psr/log/Psr/Log/LoggerInterface.php | 125 + .../vendor/psr/log/Psr/Log/LoggerTrait.php | 142 + .../vendor/psr/log/Psr/Log/NullLogger.php | 30 + .../vendor/psr/log/Psr/Log/Test/DummyTest.php | 18 + .../log/Psr/Log/Test/LoggerInterfaceTest.php | 138 + .../psr/log/Psr/Log/Test/TestLogger.php | 147 + .../vendor/psr/log/README.md | 58 + .../vendor/psr/log/composer.json | 26 + .../symfony/deprecation-contracts/.gitignore | 3 + .../deprecation-contracts/CHANGELOG.md | 5 + .../symfony/deprecation-contracts/LICENSE | 19 + .../symfony/deprecation-contracts/README.md | 26 + .../deprecation-contracts/composer.json | 35 + .../deprecation-contracts/function.php | 27 + .../symfony/options-resolver/CHANGELOG.md | 76 + .../Debug/OptionsResolverIntrospector.php | 120 + .../Exception/AccessException.php | 22 + .../Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 21 + .../Exception/InvalidOptionsException.php | 23 + .../Exception/MissingOptionsException.php | 23 + .../Exception/NoConfigurationException.php | 26 + .../Exception/NoSuchOptionException.php | 26 + .../Exception/OptionDefinitionException.php | 21 + .../Exception/UndefinedOptionsException.php | 24 + .../vendor/symfony/options-resolver/LICENSE | 19 + .../options-resolver/OptionConfigurator.php | 143 + .../symfony/options-resolver/Options.php | 22 + .../options-resolver/OptionsResolver.php | 1305 + .../vendor/symfony/options-resolver/README.md | 15 + .../symfony/options-resolver/composer.json | 31 + .../vendor/symfony/polyfill-php73/LICENSE | 19 + .../vendor/symfony/polyfill-php73/Php73.php | 43 + .../vendor/symfony/polyfill-php73/README.md | 18 + .../Resources/stubs/JsonException.php | 14 + .../symfony/polyfill-php73/bootstrap.php | 31 + .../symfony/polyfill-php73/composer.json | 36 + .../vendor/symfony/polyfill-php80/LICENSE | 19 + .../vendor/symfony/polyfill-php80/Php80.php | 105 + .../vendor/symfony/polyfill-php80/README.md | 24 + .../Resources/stubs/Attribute.php | 22 + .../Resources/stubs/Stringable.php | 11 + .../Resources/stubs/UnhandledMatchError.php | 5 + .../Resources/stubs/ValueError.php | 5 + .../symfony/polyfill-php80/bootstrap.php | 42 + .../symfony/polyfill-php80/composer.json | 40 + .../vendor/typesense/typesense-php/.gitignore | 4 + .../vendor/typesense/typesense-php/LICENSE | 201 + .../vendor/typesense/typesense-php/README.md | 55 + .../typesense/typesense-php/composer.json | 63 + .../typesense-php/docker-compose.yml | 13 + .../typesense-php/examples/README.md | 14 + .../examples/alias_operations.php | 142 + .../examples/cluster_operations.php | 29 + .../examples/collection_operations.php | 229 + .../examples/curation_operations.php | 148 + .../examples/info_operations.php | 31 + .../examples/keys_operations.php | 160 + .../examples/synonym_operations.php | 150 + .../vendor/typesense/typesense-php/phpcs.xml | 45 + .../typesense/typesense-php/src/Alias.php | 65 + .../typesense/typesense-php/src/Aliases.php | 124 + .../typesense/typesense-php/src/ApiCall.php | 368 + .../typesense/typesense-php/src/Client.php | 153 + .../typesense-php/src/Collection.php | 107 + .../typesense-php/src/Collections.php | 112 + .../typesense/typesense-php/src/Debug.php | 41 + .../typesense/typesense-php/src/Document.php | 89 + .../typesense/typesense-php/src/Documents.php | 214 + .../src/Exceptions/ConfigError.php | 15 + .../src/Exceptions/HTTPStatus0Error.php | 14 + .../src/Exceptions/ObjectAlreadyExists.php | 15 + .../src/Exceptions/ObjectNotFound.php | 15 + .../src/Exceptions/ObjectUnprocessable.php | 15 + .../src/Exceptions/RequestMalformed.php | 15 + .../src/Exceptions/RequestUnauthorized.php | 15 + .../src/Exceptions/ServerError.php | 15 + .../src/Exceptions/ServiceUnavailable.php | 15 + .../typesense-php/src/Exceptions/Timeout.php | 15 + .../src/Exceptions/TypesenseClientError.php | 22 + .../typesense/typesense-php/src/Health.php | 41 + .../typesense/typesense-php/src/Key.php | 65 + .../typesense/typesense-php/src/Keys.php | 118 + .../typesense-php/src/Lib/Configuration.php | 225 + .../typesense/typesense-php/src/Lib/Node.php | 105 + .../typesense/typesense-php/src/Metrics.php | 41 + .../typesense-php/src/MultiSearch.php | 48 + .../typesense-php/src/Operations.php | 48 + .../typesense/typesense-php/src/Override.php | 78 + .../typesense/typesense-php/src/Overrides.php | 119 + .../typesense/typesense-php/src/Synonym.php | 76 + .../typesense/typesense-php/src/Synonyms.php | 117 + .../class-algolia-term-changes-watcher.php | 133 + .../class-algolia-user-changes-watcher.php | 161 + .../class-typesense-changes-watcher.php | 25 + .../class-typesense-post-changes-watcher.php | 173 + includes/watchers/index.php | 1 + index.php | 1 + js/algoliasearch/CHANGELOG.md | 1228 + js/algoliasearch/LICENSE.txt | 22 + js/algoliasearch/bower.json | 24 + .../dist/algoliasearch.angular.js | 7398 ++ .../dist/algoliasearch.angular.min.js | 4 + js/algoliasearch/dist/algoliasearch.jquery.js | 7344 ++ .../dist/algoliasearch.jquery.min.js | 4 + js/algoliasearch/dist/algoliasearch.js | 7190 ++ js/algoliasearch/dist/algoliasearch.min.js | 4 + js/algoliasearch/dist/algoliasearch.parse.js | 3663 + js/algoliasearch/dist/algoliasearchLite.js | 4453 ++ .../dist/algoliasearchLite.min.js | 3 + js/algoliasearch/index.js | 6 + js/algoliasearch/lite.js | 7 + js/algoliasearch/plugins/angular.js | 6 + js/algoliasearch/plugins/jquery.js | 6 + js/algoliasearch/reactnative.js | 4 + js/algoliasearch/src/AlgoliaSearch.js | 767 + js/algoliasearch/src/AlgoliaSearchCore.js | 968 + js/algoliasearch/src/Index.js | 1334 + js/algoliasearch/src/IndexBrowser.js | 39 + js/algoliasearch/src/IndexCore.js | 389 + .../browser/builds/algoliasearch.angular.js | 205 + .../browser/builds/algoliasearch.jquery.js | 151 + .../src/browser/builds/algoliasearch.js | 6 + .../src/browser/builds/algoliasearchLite.js | 6 + .../src/browser/createAlgoliasearch.js | 236 + .../src/browser/get-document-protocol.js | 14 + .../src/browser/inline-headers.js | 15 + js/algoliasearch/src/browser/jsonp-request.js | 126 + .../migration-layer/is-using-latest.js | 23 + .../src/browser/migration-layer/load-v2.js | 50 + .../browser/migration-layer/old-globals.js | 26 + .../src/browser/migration-layer/script.js | 25 + js/algoliasearch/src/buildSearchMethod.js | 67 + js/algoliasearch/src/clone.js | 3 + js/algoliasearch/src/createAnalyticsClient.js | 85 + js/algoliasearch/src/deprecate.js | 15 + js/algoliasearch/src/deprecatedMessage.js | 7 + js/algoliasearch/src/errors.js | 86 + js/algoliasearch/src/exitPromise.js | 7 + js/algoliasearch/src/map.js | 9 + js/algoliasearch/src/merge.js | 19 + js/algoliasearch/src/omit.js | 14 + js/algoliasearch/src/places.js | 49 + .../builds/algoliasearch.reactnative.js | 178 + .../src/server/builds/AlgoliaSearchServer.js | 78 + .../src/server/builds/get-agent.js | 107 + js/algoliasearch/src/server/builds/node.js | 338 + js/algoliasearch/src/server/builds/parse.js | 136 + js/algoliasearch/src/store.js | 86 + js/algoliasearch/src/version.js | 3 + js/autocomplete-noconflict.js | 1 + js/autocomplete.js/CHANGELOG.md | 420 + js/autocomplete.js/CONTRIBUTING.md | 73 + js/autocomplete.js/LICENSE | 22 + js/autocomplete.js/bower.json | 21 + .../dist/autocomplete.angular.js | 2881 + .../dist/autocomplete.angular.min.js | 7 + .../dist/autocomplete.jquery.js | 2930 + .../dist/autocomplete.jquery.min.js | 7 + js/autocomplete.js/dist/autocomplete.js | 4189 ++ js/autocomplete.js/dist/autocomplete.min.js | 7 + js/autocomplete.js/examples/basic.gif | Bin 0 -> 45080 bytes js/autocomplete.js/examples/basic.html | 67 + .../examples/basic_angular.html | 82 + js/autocomplete.js/examples/basic_jquery.html | 66 + js/autocomplete.js/examples/index.html | 24 + js/autocomplete.js/index.js | 3 + js/autocomplete.js/index_angular.js | 3 + js/autocomplete.js/index_jquery.js | 3 + js/autocomplete.js/karma.conf.js | 55 + js/autocomplete.js/release.sh | 50 + js/autocomplete.js/scripts/netlify-deploy.js | 76 + js/autocomplete.js/scripts/release.sh | 51 + js/autocomplete.js/src/angular/directive.js | 114 + js/autocomplete.js/src/autocomplete/css.js | 97 + .../src/autocomplete/dataset.js | 307 + .../src/autocomplete/dropdown.js | 394 + .../src/autocomplete/event_bus.js | 33 + .../src/autocomplete/event_emitter.js | 102 + js/autocomplete.js/src/autocomplete/html.js | 9 + js/autocomplete.js/src/autocomplete/input.js | 341 + .../src/autocomplete/typeahead.js | 654 + js/autocomplete.js/src/common/dom.js | 5 + .../src/common/parseAlgoliaClientVersion.js | 15 + js/autocomplete.js/src/common/utils.js | 131 + js/autocomplete.js/src/jquery/plugin.js | 163 + js/autocomplete.js/src/sources/hits.js | 24 + js/autocomplete.js/src/sources/index.js | 6 + js/autocomplete.js/src/sources/popularIn.js | 85 + js/autocomplete.js/src/standalone/index.js | 91 + js/autocomplete.js/test/ci.sh | 15 + js/autocomplete.js/test/fixtures.js | 54 + js/autocomplete.js/test/helpers/mocks.js | 47 + js/autocomplete.js/test/helpers/waits_for.js | 24 + js/autocomplete.js/test/integration/test.html | 127 + js/autocomplete.js/test/integration/test.js | 427 + js/autocomplete.js/test/playground.css | 52 + js/autocomplete.js/test/playground.html | 152 + .../test/playground_angular.html | 55 + .../test/playground_jquery.html | 207 + js/autocomplete.js/test/test.bundle.js | 5 + js/autocomplete.js/test/unit/angular_spec.js | 64 + js/autocomplete.js/test/unit/dataset_spec.js | 545 + js/autocomplete.js/test/unit/dropdown_spec.js | 494 + .../test/unit/event_emitter_spec.js | 134 + js/autocomplete.js/test/unit/input_spec.js | 495 + js/autocomplete.js/test/unit/jquery_spec.js | 41 + .../unit/parseAlgoliaClientVersion_spec.js | 27 + .../test/unit/popularIn_spec.js | 133 + .../test/unit/standalone_spec.js | 130 + .../test/unit/typeahead_spec.js | 1039 + js/autocomplete.js/test/unit/utils_spec.js | 84 + js/autocomplete.js/version.js | 1 + js/autocomplete.js/zepto.js | 1319 + js/instantsearch.js/CHANGELOG.md | 1460 + js/instantsearch.js/LICENSE | 21 + js/instantsearch.js/dist-es5-module/index.js | 12 + .../src/components/ClearAll/ClearAll.js | 84 + .../ClearAll/__tests__/ClearAll-test.js | 97 + .../CurrentRefinedValues.js | 160 + .../__tests__/CurrentRefinedValues-test.js | 526 + .../dist-es5-module/src/components/Hits.js | 119 + .../src/components/InfiniteHits.js | 76 + .../src/components/Pagination/Pagination.js | 205 + .../components/Pagination/PaginationLink.js | 93 + .../src/components/Pagination/Paginator.js | 78 + .../Pagination/__tests__/Pagination-test.js | 179 + .../Pagination/__tests__/Paginator-test.js | 169 + .../src/components/PriceRanges/PriceRanges.js | 140 + .../components/PriceRanges/PriceRangesForm.js | 118 + .../PriceRanges/__tests__/PriceRanges-test.js | 259 + .../__tests__/PriceRangesForm-test.js | 140 + .../RefinementList/RefinementList.js | 248 + .../RefinementList/RefinementListItem.js | 80 + .../__tests__/RefinementList-test.js | 357 + .../__tests__/RefinementListItem-test.js | 81 + .../src/components/SearchBox/index.js | 127 + .../src/components/Selector.js | 74 + .../src/components/Slider/Slider.js | 113 + .../Slider/__tests__/Slider-test.js | 94 + .../src/components/Stats/Stats.js | 64 + .../components/Stats/__tests__/Stats-test.js | 77 + .../src/components/Template.js | 187 + .../src/components/__tests__/Hits-test.js | 303 + .../src/components/__tests__/Selector-test.js | 108 + .../src/components/__tests__/Template-test.js | 342 + .../src/decorators/__tests__/TestComponent.js | 41 + .../__tests__/autoHideContainer-test.js | 114 + .../decorators/__tests__/headerFooter-test.js | 235 + .../src/decorators/autoHideContainer.js | 48 + .../src/decorators/headerFooter.js | 140 + .../dist-es5-module/src/lib/InstantSearch.js | 306 + .../src/lib/__tests__/InstantSearch-test.js | 366 + .../src/lib/__tests__/main-test.js | 39 + .../src/lib/__tests__/utils-test.js | 498 + .../src/lib/__tests__/version-test.js | 19 + .../dist-es5-module/src/lib/createHelpers.js | 15 + .../dist-es5-module/src/lib/main.js | 132 + .../lib/show-more/defaultShowMoreTemplates.js | 9 + .../src/lib/show-more/getShowMoreConfig.js | 37 + .../dist-es5-module/src/lib/url-sync.js | 267 + .../dist-es5-module/src/lib/utils.js | 291 + .../dist-es5-module/src/lib/version.js | 6 + .../src/shams/Object.freeze.js | 17 + .../src/shims/Object.getPrototypeOf.js | 20 + .../src/widgets/analytics/analytics.js | 151 + .../clear-all/__tests__/clear-all-test.js | 151 + .../src/widgets/clear-all/clear-all.js | 133 + .../src/widgets/clear-all/defaultTemplates.js | 10 + .../__tests__/current-refined-values-test.js | 1024 + .../current-refined-values.js | 291 + .../defaultTemplates.js | 11 + .../__tests__/hierarchical-menu-test.js | 358 + .../hierarchical-menu/defaultTemplates.js | 11 + .../hierarchical-menu/hierarchical-menu.js | 193 + .../__tests__/hits-per-page-selector-test.js | 148 + .../hits-per-page-selector.js | 121 + .../hits/__tests__/defaultTemplates-test.js | 32 + .../src/widgets/hits/__tests__/hits-test.js | 105 + .../src/widgets/hits/defaultTemplates.js | 11 + .../dist-es5-module/src/widgets/hits/hits.js | 106 + .../__tests__/defaultTemplates-test.js | 32 + .../__tests__/infinite-hits-test.js | 148 + .../widgets/infinite-hits/defaultTemplates.js | 11 + .../widgets/infinite-hits/infinite-hits.js | 129 + .../src/widgets/menu/__tests__/menu-test.js | 25 + .../src/widgets/menu/defaultTemplates.js | 11 + .../dist-es5-module/src/widgets/menu/menu.js | 205 + .../__tests__/numeric-refinement-list-test.js | 231 + .../defaultTemplates.js | 11 + .../numeric-refinement-list.js | 246 + .../__tests__/numeric-selector-test.js | 149 + .../numeric-selector/numeric-selector.js | 120 + .../pagination/__tests__/pagination-test.js | 205 + .../src/widgets/pagination/pagination.js | 161 + .../__tests__/generate-ranges-test.js | 48 + .../__tests__/price-ranges-test.js | 198 + .../widgets/price-ranges/defaultTemplates.js | 10 + .../widgets/price-ranges/generate-ranges.js | 88 + .../src/widgets/price-ranges/price-ranges.js | 234 + .../__tests__/range-slider-test.js | 430 + .../src/widgets/range-slider/range-slider.js | 225 + .../__tests__/refinement-list-test.js | 370 + .../refinement-list/defaultTemplates.js | 10 + .../refinement-list/refinement-list.js | 261 + .../search-box/__tests__/search-box-test.js | 659 + .../widgets/search-box/defaultTemplates.js | 8 + .../src/widgets/search-box/search-box.js | 322 + .../__tests__/sort-by-selector-test.js | 132 + .../sort-by-selector/sort-by-selector.js | 110 + .../star-rating/__tests__/star-rating-test.js | 236 + .../src/widgets/star-rating/defaultLabels.js | 8 + .../widgets/star-rating/defaultTemplates.js | 11 + .../src/widgets/star-rating/star-rating.js | 216 + .../src/widgets/stats/__tests__/stats-test.js | 110 + .../src/widgets/stats/defaultTemplates.js | 10 + .../src/widgets/stats/stats.js | 124 + .../widgets/toggle/__tests__/toggle-test.js | 123 + .../src/widgets/toggle/defaultTemplates.js | 10 + .../__tests__/currentToggle-test.js | 469 + .../toggle/implementations/currentToggle.js | 147 + .../toggle/implementations/legacyToggle.js | 128 + .../src/widgets/toggle/toggle.js | 156 + .../dist/instantsearch-preact.js | 42306 +++++++++++ .../dist/instantsearch-preact.js.map | 1 + .../dist/instantsearch-preact.min.js | 14 + .../dist/instantsearch-preact.min.js.map | 1 + js/instantsearch.js/dist/instantsearch.css | 726 + js/instantsearch.js/dist/instantsearch.js | 61127 ++++++++++++++++ js/instantsearch.js/dist/instantsearch.js.map | 1 + .../dist/instantsearch.min.css | 1 + js/instantsearch.js/dist/instantsearch.min.js | 18 + .../dist/instantsearch.min.js.map | 1 + languages/index.php | 1 + languages/wp-search-with-algolia-it_IT.mo | Bin 0 -> 12569 bytes languages/wp-search-with-algolia-it_IT.po | 481 + languages/wp-search-with-algolia.pot | 406 + templates/autocomplete.php | 191 + templates/instantsearch.php | 202 + typesense.php | 36 + uninstall.php | 14 + 865 files changed, 239794 insertions(+) create mode 100644 README.txt create mode 100644 classmap.php create mode 100644 css/algolia-autocomplete.css create mode 100644 css/algolia-instantsearch.css create mode 100644 css/algolia-logo.svg create mode 100644 css/index.php create mode 100644 includes/admin/class-typesense-admin-page-autocomplete.php create mode 100644 includes/admin/class-typesense-admin-page-native-search.php create mode 100644 includes/admin/class-typesense-admin-page-settings.php create mode 100644 includes/admin/class-typesense-admin.php create mode 100644 includes/admin/css/index.php create mode 100644 includes/admin/css/typesense-admin.css create mode 100644 includes/admin/fonts/algolia.eot create mode 100644 includes/admin/fonts/algolia.svg create mode 100644 includes/admin/fonts/algolia.ttf create mode 100644 includes/admin/fonts/algolia.woff create mode 100644 includes/admin/fonts/index.php create mode 100644 includes/admin/img/index.php create mode 100644 includes/admin/img/logo-algolia-avatar.svg create mode 100644 includes/admin/index.php create mode 100644 includes/admin/js/algolia-admin.js create mode 100644 includes/admin/js/index.php create mode 100644 includes/admin/js/push-settings-button.js create mode 100644 includes/admin/js/reindex-button.js create mode 100644 includes/admin/partials/form-options.php create mode 100644 includes/admin/partials/form-override-search-option.php create mode 100644 includes/admin/partials/index-records.php create mode 100644 includes/admin/partials/index.php create mode 100644 includes/admin/partials/page-autocomplete-config.php create mode 100644 includes/admin/partials/page-autocomplete.php create mode 100644 includes/admin/partials/page-search.php create mode 100644 includes/class-typesense-api.php create mode 100644 includes/class-typesense-autocomplete-config.php create mode 100644 includes/class-typesense-cli.php create mode 100644 includes/class-typesense-compatibility.php create mode 100644 includes/class-typesense-plugin.php create mode 100644 includes/class-typesense-scripts.php create mode 100644 includes/class-typesense-search.php create mode 100644 includes/class-typesense-settings.php create mode 100644 includes/class-typesense-styles.php create mode 100644 includes/class-typesense-template-loader.php create mode 100644 includes/class-typesense-utils.php create mode 100644 includes/factories/class-typesense-plugin-factory.php create mode 100644 includes/index.php create mode 100644 includes/indices/class-typesense-index-replica.php create mode 100644 includes/indices/class-typesense-index.php create mode 100644 includes/indices/class-typesense-posts-index.php create mode 100644 includes/indices/class-typesense-searchable-posts-index.php create mode 100644 includes/indices/class-typesense-terms-index.php create mode 100644 includes/indices/class-typesense-users-index.php create mode 100644 includes/indices/index.php create mode 100644 includes/libraries/algoliasearch-client-php/.circleci/config.yml create mode 100644 includes/libraries/algoliasearch-client-php/.circleci/index-cleanup.php create mode 100644 includes/libraries/algoliasearch-client-php/.dockerignore create mode 100644 includes/libraries/algoliasearch-client-php/DOCKER_README.MD create mode 100644 includes/libraries/algoliasearch-client-php/Dockerfile create mode 100644 includes/libraries/algoliasearch-client-php/LICENSE create mode 100644 includes/libraries/algoliasearch-client-php/SECURITY.md create mode 100644 includes/libraries/algoliasearch-client-php/autoload.php create mode 100644 includes/libraries/algoliasearch-client-php/src/AccountClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Algolia.php create mode 100644 includes/libraries/algoliasearch-client-php/src/AnalyticsClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Cache/FileCacheDriver.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Cache/NullCacheDriver.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Config/AbstractConfig.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Config/AnalyticsConfig.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Config/InsightsConfig.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Config/PlacesConfig.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Config/RecommendationConfig.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Config/SearchConfig.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/AlgoliaException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/BadRequestException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/CannotWaitException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/MissingObjectId.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/NotFoundException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/ObjectNotFoundException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/RequestException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/RetriableException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/UnreachableException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Exceptions/ValidUntilNotFoundException.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Guzzle6HttpClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/HttpClientInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Php53HttpClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Psr7/BufferStream.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Psr7/PumpStream.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Psr7/Request.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Psr7/Response.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Psr7/Stream.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Psr7/Uri.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Psr7/UriResolver.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Http/Psr7/functions.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Insights/UserInsightsClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/InsightsClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Iterators/AbstractAlgoliaIterator.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Iterators/ObjectIterator.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Iterators/RuleIterator.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Iterators/SynonymIterator.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Log/DebugLogger.php create mode 100644 includes/libraries/algoliasearch-client-php/src/PlacesClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/RecommendationClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/RequestOptions/RequestOptions.php create mode 100644 includes/libraries/algoliasearch-client-php/src/RequestOptions/RequestOptionsFactory.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/AbstractResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/AddApiKeyResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/BatchIndexingResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/DeleteApiKeyResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/IndexingResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/MultiResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/MultipleIndexBatchIndexingResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/NullResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/RestoreApiKeyResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Response/UpdateApiKeyResponse.php create mode 100644 includes/libraries/algoliasearch-client-php/src/RetryStrategy/ApiWrapper.php create mode 100644 includes/libraries/algoliasearch-client-php/src/RetryStrategy/ApiWrapperInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/src/RetryStrategy/ClusterHosts.php create mode 100644 includes/libraries/algoliasearch-client-php/src/RetryStrategy/Host.php create mode 100644 includes/libraries/algoliasearch-client-php/src/RetryStrategy/HostCollection.php create mode 100644 includes/libraries/algoliasearch-client-php/src/SearchClient.php create mode 100644 includes/libraries/algoliasearch-client-php/src/SearchIndex.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Support/Helpers.php create mode 100644 includes/libraries/algoliasearch-client-php/src/Support/UserAgent.php create mode 100644 includes/libraries/algoliasearch-client-php/src/functions.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/CHANGELOG.md create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/LICENSE create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/src/MessageInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/src/RequestInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/src/ResponseInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/src/ServerRequestInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/src/StreamInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/src/UploadedFileInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/http-message/src/UriInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/LICENSE create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/AbstractLogger.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/InvalidArgumentException.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/LogLevel.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/LoggerAwareInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/LoggerAwareTrait.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/LoggerInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/LoggerTrait.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/NullLogger.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/simple-cache/LICENSE.md create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/simple-cache/src/CacheException.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/simple-cache/src/CacheInterface.php create mode 100644 includes/libraries/algoliasearch-client-php/vendor/psr/simple-cache/src/InvalidArgumentException.php create mode 100644 includes/libraries/typesensesearch-client-php/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/composer.lock create mode 100644 includes/libraries/typesensesearch-client-php/vendor/autoload.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/clue/stream-filter/.github/FUNDING.yml create mode 100644 includes/libraries/typesensesearch-client-php/vendor/clue/stream-filter/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/clue/stream-filter/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/clue/stream-filter/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/clue/stream-filter/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/clue/stream-filter/src/CallbackFilter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/clue/stream-filter/src/functions.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/clue/stream-filter/src/functions_include.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/ClassLoader.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/InstalledVersions.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/autoload_classmap.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/autoload_files.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/autoload_namespaces.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/autoload_psr4.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/autoload_real.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/autoload_static.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/installed.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/installed.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/composer/platform_check.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/UPGRADE.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/phpstan.neon.dist create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/DateTimeImmutable.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/ErrorHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/ChromePHPFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/ElasticaFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/ElasticsearchFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/FlowdockFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/FluentdFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/HtmlFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/LogglyFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/LogmaticFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/LogstashFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/ScalarFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/AmqpHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ElasticaHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ElasticsearchHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Handler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogmaticHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MailHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NoopHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/OverflowHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RedisPubSubHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SendGridHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SqsHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/WebRequestRecognizerTrait.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Logger.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/HostnameProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Registry.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/ResettableInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/SignalHandler.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Test/TestCase.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Utils.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/doc/final.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/psalm.xml create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Factory/HttplugFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Factory/Psr17Factory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/MessageTrait.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Request.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/RequestTrait.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Response.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/ServerRequest.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Stream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/UploadedFile.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Uri.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/.php_cs.dist create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchClientInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchResult.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Deferred.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/EmulatedHttpAsyncClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/EmulatedHttpClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/BatchException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/CircularRedirectionException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/ClientErrorException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/HttpClientNoMatchException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/HttpClientNotFoundException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/LoopException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/MultipleRedirectionException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/ServerErrorException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/FlexibleHttpClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpAsyncClientDecorator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpAsyncClientEmulator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientDecorator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientEmulator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/HttpClientPool.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/HttpClientPoolItem.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/LeastUsedClientPool.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/RandomClientPool.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/RoundRobinClientPool.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientRouter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientRouterInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpMethodsClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpMethodsClientInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AddHostPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AddPathPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AuthenticationPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/BaseUriPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ContentLengthPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ContentTypePlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/CookiePlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/DecoderPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ErrorPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderAppendPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderDefaultsPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderRemovePlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderSetPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HistoryPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/Journal.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/QueryDefaultsPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RedirectPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RequestMatcherPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RequestSeekableBodyPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ResponseSeekableBodyPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RetryPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/SeekableBodyPlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/VersionBridgePlugin.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginChain.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClientBuilder.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClientFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/VersionBridgeClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/.github/workflows/Build-Test.yml create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/.php_cs create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/puli.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/Client.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/CurlPromise.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/MultiRunner.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/PromiseCore.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/ResponseBuilder.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/ClassDiscovery.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/ClassInstantiationFailedException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/DiscoveryFailedException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/NoCandidateFoundException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/NotFoundException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/PuliUnavailableException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/StrategyUnavailableException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/HttpAsyncClientDiscovery.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/HttpClientDiscovery.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/MessageFactoryDiscovery.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/NotFoundException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Psr17FactoryDiscovery.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Psr18ClientDiscovery.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/CommonClassesStrategy.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/CommonPsr17ClassesStrategy.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/DiscoveryStrategy.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/MockClientStrategy.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/PuliBetaStrategy.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/StreamFactoryDiscovery.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/UriFactoryDiscovery.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/puli.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/HttpException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/NetworkException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/RequestAwareTrait.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/RequestException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/TransferException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/HttpAsyncClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/HttpClient.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Promise/HttpFulfilledPromise.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Promise/HttpRejectedPromise.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/puli.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/MessageFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/RequestFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/ResponseFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/StreamFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/UriFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/apigen.neon create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/puli.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/AutoBasicAuth.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/BasicAuth.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Bearer.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Chain.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Header.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Matching.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/QueryParam.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/RequestConditional.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Wsse.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Builder/ResponseBuilder.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Cookie.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/CookieJar.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/CookieUtil.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/MessageDecorator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/RequestDecorator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/ResponseDecorator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/StreamDecorator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/ChunkStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/CompressStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DechunkStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DecompressStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DeflateStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/Filter/Chunk.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/FilteredStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/GzipDecodeStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/GzipEncodeStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/InflateStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Exception.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Exception/UnexpectedValueException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/CurlCommandFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/FullHttpMessageFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/SimpleFormatter.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/DiactorosMessageFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/GuzzleMessageFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/SlimMessageFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/CallbackRequestMatcher.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/RegexRequestMatcher.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/RequestMatcher.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Stream/BufferedStream.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/DiactorosStreamFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/GuzzleStreamFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/SlimStreamFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/DiactorosUriFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/GuzzleUriFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/SlimUriFactory.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/filters.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/promise/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/promise/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/promise/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/promise/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/FulfilledPromise.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/Promise.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/RejectedPromise.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-client/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-client/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-client/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-client/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-client/src/ClientExceptionInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-client/src/ClientInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-client/src/NetworkExceptionInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-client/src/RequestExceptionInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/.gitignore create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/.pullapprove.yml create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/RequestFactoryInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/ResponseFactoryInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/ServerRequestFactoryInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/StreamFactoryInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/UploadedFileFactoryInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/UriFactoryInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/MessageInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/RequestInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/ResponseInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/ServerRequestInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/StreamInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/UploadedFileInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/UriInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/AbstractLogger.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/InvalidArgumentException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/LogLevel.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/LoggerAwareInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/LoggerAwareTrait.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/LoggerInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/LoggerTrait.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/NullLogger.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/DummyTest.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/TestLogger.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/psr/log/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/.gitignore create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/function.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/CHANGELOG.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Debug/OptionsResolverIntrospector.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/AccessException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/ExceptionInterface.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/InvalidArgumentException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/InvalidOptionsException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/MissingOptionsException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/NoConfigurationException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/NoSuchOptionException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/OptionDefinitionException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/UndefinedOptionsException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/OptionConfigurator.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Options.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/OptionsResolver.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/Php73.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/bootstrap.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Php80.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/bootstrap.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/.gitignore create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/LICENSE create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/composer.json create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/docker-compose.yml create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/README.md create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/alias_operations.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/cluster_operations.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/collection_operations.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/curation_operations.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/info_operations.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/keys_operations.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/synonym_operations.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/phpcs.xml create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Alias.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Aliases.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/ApiCall.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Client.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Collection.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Collections.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Debug.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Document.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Documents.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ConfigError.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/HTTPStatus0Error.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectAlreadyExists.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectNotFound.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectUnprocessable.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/RequestMalformed.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/RequestUnauthorized.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ServerError.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ServiceUnavailable.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/Timeout.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/TypesenseClientError.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Health.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Key.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Keys.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Lib/Configuration.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Lib/Node.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Metrics.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/MultiSearch.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Operations.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Override.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Overrides.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Synonym.php create mode 100644 includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Synonyms.php create mode 100644 includes/watchers/class-algolia-term-changes-watcher.php create mode 100644 includes/watchers/class-algolia-user-changes-watcher.php create mode 100644 includes/watchers/class-typesense-changes-watcher.php create mode 100644 includes/watchers/class-typesense-post-changes-watcher.php create mode 100644 includes/watchers/index.php create mode 100644 index.php create mode 100644 js/algoliasearch/CHANGELOG.md create mode 100644 js/algoliasearch/LICENSE.txt create mode 100644 js/algoliasearch/bower.json create mode 100644 js/algoliasearch/dist/algoliasearch.angular.js create mode 100644 js/algoliasearch/dist/algoliasearch.angular.min.js create mode 100644 js/algoliasearch/dist/algoliasearch.jquery.js create mode 100644 js/algoliasearch/dist/algoliasearch.jquery.min.js create mode 100644 js/algoliasearch/dist/algoliasearch.js create mode 100644 js/algoliasearch/dist/algoliasearch.min.js create mode 100644 js/algoliasearch/dist/algoliasearch.parse.js create mode 100644 js/algoliasearch/dist/algoliasearchLite.js create mode 100644 js/algoliasearch/dist/algoliasearchLite.min.js create mode 100644 js/algoliasearch/index.js create mode 100644 js/algoliasearch/lite.js create mode 100644 js/algoliasearch/plugins/angular.js create mode 100644 js/algoliasearch/plugins/jquery.js create mode 100644 js/algoliasearch/reactnative.js create mode 100644 js/algoliasearch/src/AlgoliaSearch.js create mode 100644 js/algoliasearch/src/AlgoliaSearchCore.js create mode 100644 js/algoliasearch/src/Index.js create mode 100644 js/algoliasearch/src/IndexBrowser.js create mode 100644 js/algoliasearch/src/IndexCore.js create mode 100644 js/algoliasearch/src/browser/builds/algoliasearch.angular.js create mode 100644 js/algoliasearch/src/browser/builds/algoliasearch.jquery.js create mode 100644 js/algoliasearch/src/browser/builds/algoliasearch.js create mode 100644 js/algoliasearch/src/browser/builds/algoliasearchLite.js create mode 100644 js/algoliasearch/src/browser/createAlgoliasearch.js create mode 100644 js/algoliasearch/src/browser/get-document-protocol.js create mode 100644 js/algoliasearch/src/browser/inline-headers.js create mode 100644 js/algoliasearch/src/browser/jsonp-request.js create mode 100644 js/algoliasearch/src/browser/migration-layer/is-using-latest.js create mode 100644 js/algoliasearch/src/browser/migration-layer/load-v2.js create mode 100644 js/algoliasearch/src/browser/migration-layer/old-globals.js create mode 100644 js/algoliasearch/src/browser/migration-layer/script.js create mode 100644 js/algoliasearch/src/buildSearchMethod.js create mode 100644 js/algoliasearch/src/clone.js create mode 100644 js/algoliasearch/src/createAnalyticsClient.js create mode 100644 js/algoliasearch/src/deprecate.js create mode 100644 js/algoliasearch/src/deprecatedMessage.js create mode 100644 js/algoliasearch/src/errors.js create mode 100644 js/algoliasearch/src/exitPromise.js create mode 100644 js/algoliasearch/src/map.js create mode 100644 js/algoliasearch/src/merge.js create mode 100644 js/algoliasearch/src/omit.js create mode 100644 js/algoliasearch/src/places.js create mode 100644 js/algoliasearch/src/reactnative/builds/algoliasearch.reactnative.js create mode 100644 js/algoliasearch/src/server/builds/AlgoliaSearchServer.js create mode 100644 js/algoliasearch/src/server/builds/get-agent.js create mode 100644 js/algoliasearch/src/server/builds/node.js create mode 100644 js/algoliasearch/src/server/builds/parse.js create mode 100644 js/algoliasearch/src/store.js create mode 100644 js/algoliasearch/src/version.js create mode 100644 js/autocomplete-noconflict.js create mode 100644 js/autocomplete.js/CHANGELOG.md create mode 100644 js/autocomplete.js/CONTRIBUTING.md create mode 100644 js/autocomplete.js/LICENSE create mode 100644 js/autocomplete.js/bower.json create mode 100644 js/autocomplete.js/dist/autocomplete.angular.js create mode 100644 js/autocomplete.js/dist/autocomplete.angular.min.js create mode 100644 js/autocomplete.js/dist/autocomplete.jquery.js create mode 100644 js/autocomplete.js/dist/autocomplete.jquery.min.js create mode 100644 js/autocomplete.js/dist/autocomplete.js create mode 100644 js/autocomplete.js/dist/autocomplete.min.js create mode 100644 js/autocomplete.js/examples/basic.gif create mode 100644 js/autocomplete.js/examples/basic.html create mode 100644 js/autocomplete.js/examples/basic_angular.html create mode 100644 js/autocomplete.js/examples/basic_jquery.html create mode 100644 js/autocomplete.js/examples/index.html create mode 100644 js/autocomplete.js/index.js create mode 100644 js/autocomplete.js/index_angular.js create mode 100644 js/autocomplete.js/index_jquery.js create mode 100644 js/autocomplete.js/karma.conf.js create mode 100644 js/autocomplete.js/release.sh create mode 100644 js/autocomplete.js/scripts/netlify-deploy.js create mode 100644 js/autocomplete.js/scripts/release.sh create mode 100644 js/autocomplete.js/src/angular/directive.js create mode 100644 js/autocomplete.js/src/autocomplete/css.js create mode 100644 js/autocomplete.js/src/autocomplete/dataset.js create mode 100644 js/autocomplete.js/src/autocomplete/dropdown.js create mode 100644 js/autocomplete.js/src/autocomplete/event_bus.js create mode 100644 js/autocomplete.js/src/autocomplete/event_emitter.js create mode 100644 js/autocomplete.js/src/autocomplete/html.js create mode 100644 js/autocomplete.js/src/autocomplete/input.js create mode 100644 js/autocomplete.js/src/autocomplete/typeahead.js create mode 100644 js/autocomplete.js/src/common/dom.js create mode 100644 js/autocomplete.js/src/common/parseAlgoliaClientVersion.js create mode 100644 js/autocomplete.js/src/common/utils.js create mode 100644 js/autocomplete.js/src/jquery/plugin.js create mode 100644 js/autocomplete.js/src/sources/hits.js create mode 100644 js/autocomplete.js/src/sources/index.js create mode 100644 js/autocomplete.js/src/sources/popularIn.js create mode 100644 js/autocomplete.js/src/standalone/index.js create mode 100644 js/autocomplete.js/test/ci.sh create mode 100644 js/autocomplete.js/test/fixtures.js create mode 100644 js/autocomplete.js/test/helpers/mocks.js create mode 100644 js/autocomplete.js/test/helpers/waits_for.js create mode 100644 js/autocomplete.js/test/integration/test.html create mode 100644 js/autocomplete.js/test/integration/test.js create mode 100644 js/autocomplete.js/test/playground.css create mode 100644 js/autocomplete.js/test/playground.html create mode 100644 js/autocomplete.js/test/playground_angular.html create mode 100644 js/autocomplete.js/test/playground_jquery.html create mode 100644 js/autocomplete.js/test/test.bundle.js create mode 100644 js/autocomplete.js/test/unit/angular_spec.js create mode 100644 js/autocomplete.js/test/unit/dataset_spec.js create mode 100644 js/autocomplete.js/test/unit/dropdown_spec.js create mode 100644 js/autocomplete.js/test/unit/event_emitter_spec.js create mode 100644 js/autocomplete.js/test/unit/input_spec.js create mode 100644 js/autocomplete.js/test/unit/jquery_spec.js create mode 100644 js/autocomplete.js/test/unit/parseAlgoliaClientVersion_spec.js create mode 100644 js/autocomplete.js/test/unit/popularIn_spec.js create mode 100644 js/autocomplete.js/test/unit/standalone_spec.js create mode 100644 js/autocomplete.js/test/unit/typeahead_spec.js create mode 100644 js/autocomplete.js/test/unit/utils_spec.js create mode 100644 js/autocomplete.js/version.js create mode 100644 js/autocomplete.js/zepto.js create mode 100644 js/instantsearch.js/CHANGELOG.md create mode 100644 js/instantsearch.js/LICENSE create mode 100644 js/instantsearch.js/dist-es5-module/index.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/ClearAll/ClearAll.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/ClearAll/__tests__/ClearAll-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/CurrentRefinedValues/CurrentRefinedValues.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/CurrentRefinedValues/__tests__/CurrentRefinedValues-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Hits.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/InfiniteHits.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Pagination/Pagination.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Pagination/PaginationLink.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Pagination/Paginator.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Pagination/__tests__/Pagination-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Pagination/__tests__/Paginator-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/PriceRanges/PriceRanges.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/PriceRanges/PriceRangesForm.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/PriceRanges/__tests__/PriceRanges-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/PriceRanges/__tests__/PriceRangesForm-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/RefinementList/RefinementList.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/RefinementList/RefinementListItem.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/RefinementList/__tests__/RefinementList-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/RefinementList/__tests__/RefinementListItem-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/SearchBox/index.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Selector.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Slider/Slider.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Slider/__tests__/Slider-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Stats/Stats.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Stats/__tests__/Stats-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/Template.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/__tests__/Hits-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/__tests__/Selector-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/components/__tests__/Template-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/decorators/__tests__/TestComponent.js create mode 100644 js/instantsearch.js/dist-es5-module/src/decorators/__tests__/autoHideContainer-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/decorators/__tests__/headerFooter-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/decorators/autoHideContainer.js create mode 100644 js/instantsearch.js/dist-es5-module/src/decorators/headerFooter.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/InstantSearch.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/__tests__/InstantSearch-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/__tests__/main-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/__tests__/utils-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/__tests__/version-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/createHelpers.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/main.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/show-more/defaultShowMoreTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/show-more/getShowMoreConfig.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/url-sync.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/utils.js create mode 100644 js/instantsearch.js/dist-es5-module/src/lib/version.js create mode 100644 js/instantsearch.js/dist-es5-module/src/shams/Object.freeze.js create mode 100644 js/instantsearch.js/dist-es5-module/src/shims/Object.getPrototypeOf.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/analytics/analytics.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/clear-all/__tests__/clear-all-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/clear-all/clear-all.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/clear-all/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/current-refined-values/__tests__/current-refined-values-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/current-refined-values/current-refined-values.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/current-refined-values/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hierarchical-menu/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hierarchical-menu/hierarchical-menu.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hits-per-page-selector/__tests__/hits-per-page-selector-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hits-per-page-selector/hits-per-page-selector.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hits/__tests__/defaultTemplates-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hits/__tests__/hits-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hits/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/hits/hits.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/infinite-hits/__tests__/defaultTemplates-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/infinite-hits/__tests__/infinite-hits-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/infinite-hits/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/infinite-hits/infinite-hits.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/menu/__tests__/menu-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/menu/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/menu/menu.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/numeric-refinement-list/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/numeric-refinement-list/numeric-refinement-list.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/numeric-selector/__tests__/numeric-selector-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/numeric-selector/numeric-selector.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/pagination/__tests__/pagination-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/pagination/pagination.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/price-ranges/__tests__/generate-ranges-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/price-ranges/__tests__/price-ranges-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/price-ranges/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/price-ranges/generate-ranges.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/price-ranges/price-ranges.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/range-slider/__tests__/range-slider-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/range-slider/range-slider.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/refinement-list/__tests__/refinement-list-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/refinement-list/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/refinement-list/refinement-list.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/search-box/__tests__/search-box-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/search-box/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/search-box/search-box.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/sort-by-selector/__tests__/sort-by-selector-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/sort-by-selector/sort-by-selector.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/star-rating/__tests__/star-rating-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/star-rating/defaultLabels.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/star-rating/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/star-rating/star-rating.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/stats/__tests__/stats-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/stats/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/stats/stats.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/toggle/__tests__/toggle-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/toggle/defaultTemplates.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/toggle/implementations/__tests__/currentToggle-test.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/toggle/implementations/currentToggle.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/toggle/implementations/legacyToggle.js create mode 100644 js/instantsearch.js/dist-es5-module/src/widgets/toggle/toggle.js create mode 100644 js/instantsearch.js/dist/instantsearch-preact.js create mode 100644 js/instantsearch.js/dist/instantsearch-preact.js.map create mode 100644 js/instantsearch.js/dist/instantsearch-preact.min.js create mode 100644 js/instantsearch.js/dist/instantsearch-preact.min.js.map create mode 100644 js/instantsearch.js/dist/instantsearch.css create mode 100644 js/instantsearch.js/dist/instantsearch.js create mode 100644 js/instantsearch.js/dist/instantsearch.js.map create mode 100644 js/instantsearch.js/dist/instantsearch.min.css create mode 100644 js/instantsearch.js/dist/instantsearch.min.js create mode 100644 js/instantsearch.js/dist/instantsearch.min.js.map create mode 100644 languages/index.php create mode 100644 languages/wp-search-with-algolia-it_IT.mo create mode 100644 languages/wp-search-with-algolia-it_IT.po create mode 100644 languages/wp-search-with-algolia.pot create mode 100644 templates/autocomplete.php create mode 100644 templates/instantsearch.php create mode 100644 typesense.php create mode 100644 uninstall.php diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..06cd1bd --- /dev/null +++ b/README.txt @@ -0,0 +1,163 @@ + +=== WP Search with Algolia === +Contributors: WebDevStudios, williamsba1, gregrickaby, tw2113, richaber, mrasharirfan +Tags: Search, Algolia, Autocomplete, instant-search, relevant search, search highlight, faceted search, find-as-you-type search, suggest, search by category, ajax search, better search, custom search +Requires at least: 5.0 +Tested up to: 5.5 +Requires PHP: 7.2 +Stable tag: 1.7.0 +License: GNU General Public License v2.0, MIT License + +Improve search on your site. Autocomplete is included, along with full control over look, feel and relevance. + +== Description == + +Improve search on your site. Autocomplete is included, along with full control over look, feel and relevance. + += Features = +* Relevant search results in milliseconds +* Native typo-tolerance +* Is language-agnostic +* Offers hooks and filters for easy customization +* Built by developers, for developers + +This plugin requires API keys from [Algolia](https://www.algolia.com/). API keys are free for small personal projects and non-commercial use. Learn more about [commercial use pricing](https://www.algolia.com/pricing/). + += Links = +* [WebDevStudios](https://webdevstudios.com) +* [Algolia](https://algolia.com) +* [Documentation](https://community.algolia.com/wordpress/configuration.html) +* [Support](https://github.com/WebDevStudios/wp-search-with-algolia/issues) + +*This plugin is a derivative work of the code from the [Search by Algolia – Instant & Relevant results](https://wordpress.org/plugins/search-by-algolia-instant-relevant-results/) plugin for WordPress, which is licensed under the GPLv2.* + +== Installation == + +**If you have the *Search by Algolia – Instant & Relevant results* plugin installed, please deactivate it first.** + +From your WordPress dashboard: + +1. **Visit** Plugins > Add New +2. **Search** for "WP Search with Algolia" +3. **Activate** WP Search with Algolia from your Plugins page +4. **Click** on the new menu item "Algolia Search" and enter your API keys +5. **Read** the step by step [configuration guide](https://community.algolia.com/wordpress/configuration.html) + +== Frequently Asked Questions == + += Is this plugin a fork? = + +Yes. The Algolia Team **[no longer supports their original plugin](https://community.algolia.com/wordpress/)**. The engineering team at WebDevStudios has forked the original plugin, and is now maintaining it. + += Should I switch to this plugin? = + +Yes. Because Algolia no longer supports their plugin, you will no longer receive updates. WebDevStudios uses Algolia on many of it's projects, and is committed to maintaining this plugin for years to come. + += How do I switch from the "Search by Algolia – Instant & Relevant results" plugin? = + +1. **Deactivate** the *Search by Algolia – Instant & Relevant results* plugin +2. **Follow** the [installation instructions](https://wordpress.org/plugins/wp-search-with-algolia/#installation) +3. **Activate** *WP Search with Algolia* +4. **Check** for your API Keys. They should already be there, if not, enter them and then save settings +5. **Delete** the *Search by Algolia – Instant & Relevant results* plugin + += What are the minimum requirements? = + +* Requires WordPress 5.0+ +* PHP version 7.2 or greater (PHP 7.3 is recommended) +* MySQL version 5.0 or greater (MySQL 5.6 or greater is recommended) +* cURL PHP extension +* mbstring PHP extension +* OpenSSL greater than 1.0.1 +* Some payment gateways require fsockopen support (for IPN access) + +Visit the [Search by Algolia server requirements documentation](https://community.algolia.com/wordpress/installation.html) for a detailed list of server requirements. + += Where can I find Algolia documentation and user guides? = + +- For help setting up and configuring Search by Algolia please refer to the [user guide](https://community.algolia.com/wordpress/installation.html). +- For extending or theming the Autocomplete dropdown, see the [Autocomplete Customization guide](https://community.algolia.com/wordpress/customize-autocomplete.html). +- For extending or theming the Instant Search results page, see the [Search Page Customization guide](https://community.algolia.com/wordpress/customize-search-page.html). + += Will it work with my theme? = + +Yes. This plugin will work with any theme, but the Instant Search results page may require some styling to make it match nicely. See the [Search Page Customization](https://community.algolia.com/wordpress/customize-search-page.html). + += Where can I report bugs, request features, or contribute to the project? = + +All development is handled on [GitHub](https://github.com/WebDevStudios/wp-search-with-algolia/issues). + += About Algolia = + +Algolia offers its Search as a Service provider on a incremental payment program, including a free Community Plan which includes 10,000 records & 50,000 operations per month. Beyond that, [plans](https://www.algolia.com/pricing/) start at $29/month. + +*Note: there isn't a direct correlation between the number of posts in WordPress and the number of records in Algolia. Also note that we only offer support starting from the PRO plan.On average, you can expect to have about 10 times more records than you have posts, though this is not a golden rule and you could end up with more records.* + += About WebDevStudios = + +WebDevStudios provides end-to-end WordPress opportunities from strategy and planning to website design and development, as well as full data migration, extensive API integrations, scalability, performance and long-term guidance and maintenance. We have service options and solutions for start-ups, small to mid-size businesses, enterprise organizations and marketing agencies. + +== Screenshots == + +1. Algolia Settings +2. Search Page Settings +3. Autocomplete Settings +4. InstantSearch Dropdown +5. Search Results + +== Changelog == + +Follow along with the changelog on [Github](https://github.com/WebDevStudios/wp-search-with-algolia/releases). + += 1.7.0 = +* Remove 'screen' media attribute from enqueued CSS +* Update Algolia PHP Search Client to version 2.7.3. +* Add "exclude" methods and filters +* Deprecate "blacklist" methods and filters +* Fix replica RequestOptions error +* Fix PHP 8 usort deprecation warning +* Fix JQMIGRATE event shorthand is deprecated warnings in instantsearch.php and autocomplete.php templates +* Add "@version" to template file headers + += 1.6.0 = +* Fix deletion of post records created before indexing was enabled +* Update Algolia PHP Search Client to version 2.7.1. +* Add Algolia_Plugin_Factory to create and return a shared Algolia_Plugin instance +* Add Algolia_Search_Client_Factory to return a new Algolia\AlgoliaSearch\SearchClient instance +* Add Algolia_Http_Client_Interface_Factory to create and return a shared Php53HttpClient instance +* Add algolia_php_53_http_client_options filter to supply cURL options to Php53HttpClient instance +* Deprecate Algolia_Plugin:get_instance() which will be removed in an upcoming release + += 1.5.0 = +* Fix an issue where Pinterest follows a link to the Algolia domain to source text and/or images + * Move Algolia scripts to footer by default + * Changes algolia_load_scripts_in_footer filter default argument to "true" + * Move autocomplete.php template output to footer by default + += 1.4.0 = +* Update Algolia PHP Search Client version 2.7.0. +* Update Algolia JS libraries to most recent compatible (non-breaking) versions + * Updates autocomplete.js to 0.37.1 (current release as of 2020-01-27) + * Updates algoliasearch to 3.35.1 (last of the 3.x series) + * Updates instantsearch.js to 1.12.1 (last of the 1.x series) + += 1.3.0 = +* Fix an issue where, under some circumstances, when a post with a featured image was deleted, the post might be accidentally re-indexed +* Fix bug that prevented reindex display notices +* Add algolia_load_scripts_in_footer filter to allow enqueueing the scripts in the footer instead of in the head +* Add new filters for multisite developers + += 1.2.0 = +* Use filtered value of 'hitsPerPage' as 'posts_per_page' query param +* Fix broken SVG +* Add highlighting to backend search results - props @philipnewcomer + += 1.1.0 = +* Minimum PHP version requirement is now PHP 7.2 +* Minimum WordPress version requirement is now WP 5.0 +* Internationalization/localization improvements, textdomain matches plugin slug +* Addressed a potential WSOD if minimum PHP and WP version requirements were not met +* Tested on WP 5.3 + += 1.0.0 = +* Initial release. diff --git a/classmap.php b/classmap.php new file mode 100644 index 0000000..23e7292 --- /dev/null +++ b/classmap.php @@ -0,0 +1,57 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +if ( ! defined( 'TYPESENSE_PATH' ) ) { + exit(); +} + + +require_once TYPESENSE_PATH . 'includes/libraries/typesensesearch-client-php/vendor/autoload.php'; + + +require_once TYPESENSE_PATH . 'includes/factories/class-typesense-plugin-factory.php'; + +require_once TYPESENSE_PATH . 'includes/indices/class-typesense-index.php'; +require_once TYPESENSE_PATH . 'includes/indices/class-typesense-posts-index.php'; + +require_once TYPESENSE_PATH . 'includes/watchers/class-typesense-changes-watcher.php'; +require_once TYPESENSE_PATH . 'includes/watchers/class-typesense-post-changes-watcher.php'; + +require_once TYPESENSE_PATH . 'includes/class-typesense-api.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-autocomplete-config.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-compatibility.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-plugin.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-search.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-settings.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-template-loader.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-utils.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-styles.php'; +require_once TYPESENSE_PATH . 'includes/class-typesense-scripts.php'; +/* +require_once TYPESENSE_PATH . 'includes/indices/class-typesense-index.php'; +require_once TYPESENSE_PATH . 'includes/indices/class-typesense-index-replica.php'; +require_once TYPESENSE_PATH . 'includes/indices/class-typesense-searchable-posts-index.php'; +require_once TYPESENSE_PATH . 'includes/indices/class-typesense-posts-index.php'; +require_once TYPESENSE_PATH . 'includes/indices/class-typesense-terms-index.php'; +require_once TYPESENSE_PATH . 'includes/indices/class-typesense-users-index.php'; +*/ + +/* +require_once TYPESENSE_PATH . 'includes/watchers/class-typesense-changes-watcher.php'; +require_once TYPESENSE_PATH . 'includes/watchers/class-typesense-post-changes-watcher.php'; +require_once TYPESENSE_PATH . 'includes/watchers/class-typesense-term-changes-watcher.php'; +require_once TYPESENSE_PATH . 'includes/watchers/class-typesense-user-changes-watcher.php'; +*/ +if ( is_admin() ) { + require_once TYPESENSE_PATH . 'includes/admin/class-typesense-admin.php'; + require_once TYPESENSE_PATH . 'includes/admin/class-typesense-admin-page-autocomplete.php'; + require_once TYPESENSE_PATH . 'includes/admin/class-typesense-admin-page-native-search.php'; + require_once TYPESENSE_PATH . 'includes/admin/class-typesense-admin-page-settings.php'; +} diff --git a/css/algolia-autocomplete.css b/css/algolia-autocomplete.css new file mode 100644 index 0000000..fb7d178 --- /dev/null +++ b/css/algolia-autocomplete.css @@ -0,0 +1,180 @@ +.algolia-autocomplete { + z-index: 999999 !important; +} +.aa-dropdown-menu { + /* we set the width in JS */ + font-family: sans-serif; + background-color: #fff; + border-top: none; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + box-shadow: 0 1px 2px rgba(0,0,0,.25),0 0 1px rgba(0,0,0,.35); +} + +.aa-dropdown-menu:after { + content: " "; + display: block; + clear: both; + +} + +.aa-dropdown-menu .aa-input, .aa-dropdown-menu .aa-hint { + width: 100%; +} +.aa-dropdown-menu .aa-hint { + color: #999; +} + +/* Font */ +.aa-dropdown-menu { + color: #1a1a1a; + font-size: 12px; +} + +.aa-dropdown-menu a { + font-size: 12px; + color: #1a1a1a; + font-weight: normal; + text-decoration: none; +} +.aa-dropdown-menu a:hover { + text-decoration: none; +} + +/* Header */ +.aa-dropdown-menu .autocomplete-header { + margin: 0 14px; + line-height: 3em; + border-bottom: 1px solid rgba(0,0,0,.05); +} +.aa-dropdown-menu .autocomplete-header-title, +.aa-dropdown-menu .autocomplete-header-more +{ + letter-spacing: 1px; + text-transform: uppercase; + font-weight: bold; +} + +.aa-dropdown-menu .autocomplete-header-title { + float: left; +} + +.aa-dropdown-menu .autocomplete-header-more { + float: right; +} + +.aa-dropdown-menu .autocomplete-header-more a { + color: rgba(0,0,0,.3); + font-weight: bold; +} + +.aa-dropdown-menu .autocomplete-header-more a:hover { + color: rgba(0,0,0,.4); +} + +/* Suggestion */ + +.aa-dropdown-menu .aa-suggestion { + padding: 5px 0; + +} + +.aa-dropdown-menu .aa-suggestion:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; +} + +.aa-dropdown-menu .aa-suggestion em { + color: #174d8c; + background: rgba(143,187,237,.1); + font-style: normal; +} + +.aa-dropdown-menu .aa-suggestion .suggestion-post-title { + font-weight: bold; + display: block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.aa-dropdown-menu .aa-suggestion .suggestion-post-content { + color: #63676d; + display: block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.aa-dropdown-menu .aa-suggestion .suggestion-post-content em { + padding: 0 0 1px; + background: inherit; + box-shadow: inset 0 -2px 0 0 rgba(69,142,225,.8); + color: inherit; +} + +.aa-dropdown-menu .aa-suggestion.aa-cursor { + background-color: #f9f9f9; + +} + +.aa-dropdown-menu a.suggestion-link { + display: block; + padding: 0 14px; +} + +.aa-dropdown-menu a.suggestion-link.user-suggestion-link { + line-height: 32px; +} + + +.aa-dropdown-menu a.suggestion-link svg { + vertical-align: middle; + fill: rgba(0,0,0,.3); + float: left; +} + +.aa-dropdown-menu .suggestion-post-thumbnail { + float: left; + margin-right: 5px; + margin-bottom: 5px; + border-radius: 3px; + width: 32px; + height: 32px; +} + +.aa-dropdown-menu .suggestion-user-thumbnail { + float: left; + margin-right: 5px; + margin-bottom: 5px; + border-radius: 16px; + width: 32px; + height: 32px; +} + +/* Footer */ +.aa-dropdown-menu .autocomplete-footer-branding { + padding: 15px 14px 0px; + float: right; + color: rgba(0,0,0,.3); + margin-bottom: 7px; +} + +/* Clearfix */ +.aa-dropdown-menu .clear { + clear: both; +} + +/* Empty */ +.autocomplete-empty { + clear: both; + padding: 15px; +} + +.autocomplete-empty .empty-query { + font-weight: bold; +} diff --git a/css/algolia-instantsearch.css b/css/algolia-instantsearch.css new file mode 100644 index 0000000..99dec19 --- /dev/null +++ b/css/algolia-instantsearch.css @@ -0,0 +1,330 @@ +#ais-wrapper { + display: flex; +} + +#ais-main { + padding: 1rem; + width: 100%; +} + +#ais-facets { + width: 40%; + padding: 1rem; +} + +.ais-facets { + margin-bottom: 2rem; +} + +.ais-clearfix { + clear: both; +} + +#algolia-search-box { + position: relative; + margin-bottom: 3rem; +} + +#algolia-search-box input { + border: none; + border-bottom: 2px solid #21A4D7; + background: transparent; + width: 100%; + line-height: 30px; + font-size: 22px; + padding: 10px 0 10px 30px; + font-weight: 200; + box-sizing: border-box; + outline: none; + box-shadow: none; + appearance:none; + -webkit-appearance:none; + -moz-appearance:none; + -ms-appearance:none; +} + +#algolia-search-box .search-icon { + position: absolute; + left: 0px; + top: 14px; + fill: #21A4D7; +} + +.ais-search-box--powered-by { + position: absolute; + top: 60px; + right: 0; + font-size: 14px; + text-align: right; +} + +.ais-search-box--powered-by-link { + display: inline-block; + width: 64px; + height: 21px; + text-indent: 101%; + overflow: hidden; + white-space: nowrap; + background-image: url('algolia-logo.svg'); + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; +} + +.ais-stats { + position: absolute; + top: 60px; + font-size: 14px; +} + +.ais-hits--item { + /* hit item */ + margin-bottom: 2rem; +} + +.ais-hits--item h2 { + margin: 0; +} + +.ais-hits--item em, .ais-hits--item a em { + font-style: normal; + background: #FFFBCC; + border-radius: 2px; +} + +.ais-hits--thumbnail { + float: left; + margin-right: 2rem; +} + +.ais-hits--content { + overflow: hidden; +} + +.ais-hits--thumbnail img { + border-radius: 3px; +} + +.ais-pagination { + margin: 0; +} + +.ais-pagination--item { + /* pagination item */ + display: inline-block; + padding: 3px; +} + +.ais-pagination--item__disabled { + /* disabled pagination item */ + display: none; +} + +.ais-pagination--item__active { + font-weight: bold; +} + +.ais-menu--item__active { + /* active list item */ + font-weight: bold; +} + +.ais-hierarchical-menu--list__lvl1 { + /* item list level 1 */ + margin-left: 10px; +} + +.ais-hierarchical-menu--list__lvl2 { + /* item list level 0 */ + margin-left: 10px; +} + +.ais-range-slider--target { + position: relative; + direction: ltr; + background: #F3F4F7; + height: 6px; + margin-top: 2em; + margin-bottom: 2em; +} + +.ais-range-slider--base { + height: 100%; + position: relative; + z-index: 1; + border-top: 1px solid #DDD; + border-bottom: 1px solid #DDD; + border-left: 2px solid #DDD; + border-right: 2px solid #DDD; +} + +.ais-range-slider--origin { + position: absolute; + right: 0; + top: 0; + left: 0; + bottom: 0; +} + +.ais-range-slider--connect { + background: #46AEDA; +} + +.ais-range-slider--background { + background: #F3F4F7; +} + +.ais-range-slider--handle { + width: 20px; + height: 20px; + position: relative; + z-index: 1; + background: #FFFFFF; + border: 1px solid #46AEDA; + border-radius: 50%; + cursor: pointer; +} + +.ais-range-slider--handle-lower { + left: -10px; + bottom: 7px; +} + +.ais-range-slider--handle-upper { + right: 10px; + bottom: 7px; +} + +.ais-range-slider--tooltip { + position: absolute; + background: #FFFFFF; + top: -22px; + font-size: .8em; +} + +.ais-range-slider--pips { + box-sizing: border-box; + position: absolute; + height: 3em; + top: 100%; + left: 0; + width: 100%; +} + +.ais-range-slider--value { + width: 40px; + position: absolute; + text-align: center; + margin-left: -20px; + padding-top: 15px; + font-size: .8em; +} + +.ais-range-slider--value-sub { + font-size: .8em; + padding-top: 15px; +} + +.ais-range-slider--marker { + position: absolute; + background: #DDD; + margin-left: -1px; + width: 1px; + height: 5px; +} + +.ais-range-slider--marker-sub { + background: #DDD; + width: 2px; + margin-left: -2px; + height: 13px; +} + +.ais-range-slider--marker-large { + background: #DDD; + width: 2px; + margin-left: -2px; + height: 12px; +} + +.ais-range-slider--marker-large:first-child { + margin-left: 0; +} + +.ais-star-rating--item { + /* list item */ + vertical-align: middle; +} + +.ais-star-rating--item__active { + /* active list item */ + font-weight: bold; +} + +.ais-star-rating--star { + /* item star */ + display: inline-block; + width: 1em; + height: 1em; +} + +.ais-star-rating--star:before { + content: '\2605'; + color: #FBAE00; +} + +.ais-star-rating--star__empty { + /* empty star */ + display: inline-block; + width: 1em; + height: 1em; +} + +.ais-star-rating--star__empty:before { + content: '\2606'; + color: #FBAE00; +} + +.ais-star-rating--link__disabled .ais-star-rating--star:before { + color: #C9C9C9; +} + +.ais-star-rating--link__disabled .ais-star-rating--star__empty:before { + color: #C9C9C9; +} + +.ais-root__collapsible .ais-header { + cursor: pointer; +} + +.ais-root__collapsed .ais-body, .ais-root__collapsed .ais-footer { + display: none; +} + +/* Hierarchical Menu: Categories */ +.ais-hierarchical-menu--item__active > div > a { + font-weight: bold; +} + +/* Responsive */ +@media only screen and (max-width: 1000px) { + #ais-facets { + display: none; + } + + .ais-hits--thumbnail img { + width: 100% !important; + } + .ais-hits--item { + border-bottom: 1px solid gainsboro; + padding-bottom: 23px; + } +} + +@media only screen and (max-width: 500px) { + .ais-hits--thumbnail { + margin-right: 0 !important; + margin-bottom: 10px; + float: none !important; + } +} diff --git a/css/algolia-logo.svg b/css/algolia-logo.svg new file mode 100644 index 0000000..d75cef2 --- /dev/null +++ b/css/algolia-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/css/index.php b/css/index.php new file mode 100644 index 0000000..8142269 --- /dev/null +++ b/css/index.php @@ -0,0 +1 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Typesense_Admin_Page_Autocomplete + * + * @since 1.0.0 + */ +class Typesense_Admin_Page_Autocomplete { + + /** + * Admin page slug. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $slug = 'typesense'; + + /** + * Admin page capabilities. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $capability = 'manage_options'; + + /** + * Admin page section. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $section = 'typesense_section_autocomplete'; + + /** + * Admin page option group. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $option_group = 'typesense_autocomplete'; + + /** + * The Typesense_Settings object. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Settings + */ + private $settings; + + /** + * The Typesense_Autocomplete_Config object. + * + * @since 1.0.0 + * + * @var Typesense_Autocomplete_Config + */ + private $autocomplete_config; + + /** + * Typesense_Admin_Page_Autocomplete constructor. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Typesense_Settings $settings The Typesense_Settings object. + * @param Typesense_Autocomplete_Config $autocomplete_config The Typesense_Autocomplete_Config object. + */ + public function __construct(Typesense_Settings $settings) { + $this->settings = $settings; + //$this->autocomplete_config = $autocomplete_config; + + add_action( 'admin_menu', array( $this, 'add_page' ) ); + //add_action( 'admin_init', array( $this, 'add_settings' ) ); + //add_action( 'admin_notices', array( $this, 'display_errors' ) ); + + // @todo: Listen for de-index to remove from autocomplete. + } + + /** + * Add menu pages. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function add_page() { + add_menu_page( + 'Typesense Search', + esc_html__( 'Typesense Search', 'wp-search-with-typesense' ), + 'manage_options', + 'typesense', + array( $this, 'display_page' ), + '' + ); + add_submenu_page( + 'typesense', + esc_html__( 'Autocomplete', 'wp-search-with-typesense' ), + esc_html__( 'Autocomplete', 'wp-search-with-typesense' ), + $this->capability, + $this->slug, + array( $this, 'display_page' ) + ); + } + + /** + * Add and register settings. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function add_settings() { + add_settings_section( + $this->section, + null, + array( $this, 'print_section_settings' ), + $this->slug + ); + /* add_settings_field( + 'typesense_autocomplete_enabled', + esc_html__( 'Enable autocomplete', 'wp-search-with-typesense' ), + array( $this, 'autocomplete_enabled_callback' ), + $this->slug, + $this->section + ); +*/ + add_settings_field( + 'typesense_autocomplete_config', + esc_html__( 'Configuration', 'wp-search-with-typesense' ), + array( $this, 'autocomplete_config_callback' ), + $this->slug, + $this->section + ); + +// register_setting( $this->option_group, 'typesense_autocomplete_enabled', array( $this, 'sanitize_autocomplete_enabled' ) ); + register_setting( $this->option_group, 'typesense_autocomplete_config', array( $this, 'sanitize_autocomplete_config' ) ); + /* + add_settings_field( + 'typesense_autocomplete_config', + esc_html__( 'Configuration', 'wp-search-with-typesense' ), + array( $this, 'autocomplete_config_callback' ), + $this->slug, + $this->section + ); + + register_setting( $this->option_group, 'typesense_autocomplete_config', array( $this, 'sanitize_autocomplete_config' ) ); + */ + } + + /** + * Index Recorrds Callback. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function index_records_callback() { + require_once dirname( __FILE__ ) . '/partials/index-records.php'; + } + + /** + * Callback to print the autocomplete enabled checkbox. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function autocomplete_enabled_callback() { + $value = $this->settings->get_autocomplete_enabled(); + $indices = $this->autocomplete_config->get_form_data(); + $checked = 'yes' === $value ? 'checked ' : ''; + $disabled = empty( $indices ) ? 'disabled ' : ''; +?> + /> + + * @since 1.0.0 + * + * @param string $value The original value. + * + * @return string + */ + public function sanitize_autocomplete_enabled( $value ) { + + add_settings_error( + $this->option_group, + 'autocomplete_enabled', + esc_html__( 'Autocomplete configuration has been saved. Make sure to hit the "re-index" buttons of the different indices that are not indexed yet.', 'wp-search-with-typesense' ), + 'updated' + ); + + return 'yes' === $value ? 'yes' : 'no'; + } + + /** + * Autocomplete Config Callback. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function autocomplete_config_callback() { + $indices = $this->autocomplete_config->get_form_data(); + + require_once dirname( __FILE__ ) . '/partials/page-autocomplete-config.php'; + } + + /** + * Sanitize Autocomplete Config. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param array $values Array of autocomplete config values. + * + * @return array|mixed + */ + public function sanitize_autocomplete_config( $values ) { + return $this->autocomplete_config->sanitize_form_data( $values ); + } + + /** + * Display the page. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function display_page() { + require_once dirname( __FILE__ ) . '/partials/page-autocomplete.php'; + } + + /** + * Display the errors. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return void + */ + public function display_errors() { + settings_errors( $this->option_group ); + + if ( defined( 'TYPESENSE_HIDE_HELP_NOTICES' ) && TYPESENSE_HIDE_HELP_NOTICES ) { + return; + } + + $is_enabled = 'yes' === $this->settings->get_autocomplete_enabled(); + $indices = $this->autocomplete_config->get_config(); + + if ( true === $is_enabled && empty( $indices ) ) { + /* translators: placeholder contains the URL to the autocomplete configuration page. */ + $message = sprintf( __( 'Please select one or multiple indices on the Typesense: Autocomplete configuration page.', 'wp-search-with-typesense' ), esc_url( admin_url( 'admin.php?page=' . $this->slug ) ) ); + echo '
+

' . esc_html__( 'You have enabled the Typesense Autocomplete feature but did not choose any index to search in.', 'wp-search-with-typesense' ) . '

+

' . wp_kses_post( $message ) . '

+
'; + } + } + + /** + * Prints the section text. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function print_section_settings() { + echo '

' . esc_html__( 'The autocomplete feature adds a find-as-you-type dropdown menu to your search bar(s).', 'wp-search-with-typesense' ) . '

'; + } +} diff --git a/includes/admin/class-typesense-admin-page-native-search.php b/includes/admin/class-typesense-admin-page-native-search.php new file mode 100644 index 0000000..80f83a6 --- /dev/null +++ b/includes/admin/class-typesense-admin-page-native-search.php @@ -0,0 +1,244 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Typesense_Admin_Page_Native_Search + * + * @since 1.0.0 + */ +class Typesense_Admin_Page_Native_Search { + + /** + * Admin page slug. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $slug = 'typesense-search-page'; + + /** + * Admin page capabilities. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $capability = 'manage_options'; + + /** + * Admin page section. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $section = 'typesense_section_native_search'; + + /** + * Admin page option group. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $option_group = 'typesense_native_search'; + + /** + * The Typesense_Plugin instance. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Plugin + */ + private $plugin; + + /** + * Typesense_Admin_Page_Native_Search constructor. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Typesense_Plugin $plugin The Typesense_Plugin instance. + */ + public function __construct( Typesense_Plugin $plugin ) { + $this->plugin = $plugin; + + add_action( 'admin_menu', array( $this, 'add_page' ) ); + add_action( 'admin_init', array( $this, 'add_settings' ) ); + //add_action( 'admin_notices', array( $this, 'display_errors' ) ); + } + + /** + * Add submenu page. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function add_page() { + add_submenu_page( + 'typesense', + esc_html__( 'Search Page', 'wp-search-with-typesense' ), + esc_html__( 'Search Page', 'wp-search-with-typesense' ), + $this->capability, + $this->slug, + array( $this, 'display_page' ) + ); + + } + + /** + * Add settings. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function add_settings() { + add_settings_section( + $this->section, + null, + array( $this, 'print_section_settings' ), + $this->slug + ); + + add_settings_field( + 'typesense_override_native_search', + esc_html__( 'Search results', 'wp-search-with-typesense' ), + array( $this, 'override_native_search_callback' ), + $this->slug, + $this->section + ); + + register_setting( $this->option_group, 'typesense_override_native_search', array( $this, 'sanitize_override_native_search' ) ); + } + + /** + * Override native search callback. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function override_native_search_callback() { + $value = $this->plugin->get_settings()->get_override_native_search(); + + require_once dirname( __FILE__ ) . '/partials/form-override-search-option.php'; + } + + /** + * Sanitize override native search. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $value The value to sanitize. + * + * @return array + */ + public function sanitize_override_native_search( $value ) { + + if ( 'backend' === $value ) { + add_settings_error( + $this->option_group, + 'native_search_enabled', + esc_html__( 'WordPress search is now based on Typesense!', 'wp-search-with-typesense' ), + 'updated' + ); + } elseif ( 'instantsearch' === $value ) { + add_settings_error( + $this->option_group, + 'native_search_enabled', + esc_html__( 'WordPress search is now based on Typesense instantsearch.js!', 'wp-search-with-typesense' ), + 'updated' + ); + } else { + $value = 'native'; + add_settings_error( + $this->option_group, + 'native_search_disabled', + esc_html__( 'You chose to keep the WordPress native search instead of Typesense. If you are using the autocomplete feature of the plugin we highly recommend you turn Typesense search on instead of the WordPress native search.', 'wp-search-with-typesense' ), + 'updated' + ); + } + + return $value; + } + + /** + * Display the page. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function display_page() { + require_once dirname( __FILE__ ) . '/partials/page-search.php'; + } + + /** + * Display the errors. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return void + */ + public function display_errors() { + settings_errors( $this->option_group ); + + if ( defined( 'TYPESENSE_HIDE_HELP_NOTICES' ) && TYPESENSE_HIDE_HELP_NOTICES ) { + return; + } + + $settings = $this->plugin->get_settings(); + + if ( ! $settings->should_override_search_in_backend() && ! $settings->should_override_search_with_instantsearch() ) { + return; + } + + $maybe_get_page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_STRING ); + + $searchable_posts_index = $this->plugin->get_index( 'searchable_posts' ); + if ( false === $searchable_posts_index->is_enabled() && ( ! empty( $maybe_get_page ) ) && $maybe_get_page === $this->slug ) { + /* translators: placeholder contains the link to the indexing page. */ + $message = sprintf( __( 'Searchable posts index needs to be checked on the Typesense: Indexing page for the search results to be powered by Typesense.', 'wp-search-with-typesense' ), esc_url( admin_url( 'admin.php?page=typesense-indexing' ) ) ); + echo '
+

' . wp_kses_post( $message ) . '

+
'; + } + } + + /** + * Prints the section text. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function print_section_settings() { + echo '

' . esc_html__( 'By enabling this plugin to override the native WordPress search, your search results will be powered by Typesense\'s typo-tolerant & relevant search algorithms.', 'wp-search-with-typesense' ) . '

'; + + // @Todo: replace this with a check on the searchable_posts_index. + $indices = $this->plugin->get_indices( + array( + 'enabled' => true, + 'contains' => 'posts', + ) + ); + + if ( empty( $indices ) ) { + echo '
' . + esc_html( __( 'You have no index containing only posts yet. Please index some content on the `Indexing` page.', 'wp-search-with-typesense' ) ) . + '
'; + } + } +} diff --git a/includes/admin/class-typesense-admin-page-settings.php b/includes/admin/class-typesense-admin-page-settings.php new file mode 100644 index 0000000..c24f45c --- /dev/null +++ b/includes/admin/class-typesense-admin-page-settings.php @@ -0,0 +1,508 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Typesense_Admin_Page_Settings + * + * @since 1.0.0 + */ +class Typesense_Admin_Page_Settings { + + /** + * Admin page slug. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $slug = 'typesense-account-settings'; + + /** + * Admin page capabilities. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $capability = 'manage_options'; + + /** + * Admin page section. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $section = 'typesense_section_settings'; + + /** + * Admin page option group. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string + */ + private $option_group = 'typesense_settings'; + + /** + * The Typesense_Plugin instance. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Plugin + */ + private $plugin; + + /** + * Typesense_Admin_Page_Settings constructor. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Typesense_Plugin $plugin The Typesense_Plugin instance. + */ + public function __construct( Typesense_Plugin $plugin ) { + $this->plugin = $plugin; + + add_action( 'admin_menu', array( $this, 'add_page' ) ); + add_action( 'admin_init', array( $this, 'add_settings' ) ); + add_action( 'admin_notices', array( $this, 'display_errors' ) ); + + // Display a link to this page from the plugins page. + add_filter( 'plugin_action_links_' . TYPESENSE_PLUGIN_BASENAME, array( $this, 'add_action_links' ) ); + } + + /** + * Add action links. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param array $links Array of action links. + * + * @return array + */ + public function add_action_links( array $links ) { + return array_merge( + $links, array( + '' . esc_html__( 'Settings', 'wp-search-with-typesense' ) . '', + ) + ); + } + + /** + * Add admin menu page. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string|void The resulting page's hook_suffix. + */ + public function add_page() { + $api = $this->plugin->get_api(); + /* + if ( ! $api->is_reachable() ) { + // Means this is the only reachable admin page, so make it the default one! + return add_menu_page( + 'WP Search with Typesense', + esc_html__( 'Typesense Search', 'wp-search-with-typesense' ), + 'manage_options', + $this->slug, + array( $this, 'display_page' ), + '' + ); + } +*/ + add_submenu_page( + 'typesense', + esc_html__( 'Settings', 'wp-search-with-typesense' ), + esc_html__( 'Settings', 'wp-search-with-typesense' ), + $this->capability, + $this->slug, + array( $this, 'display_page' ) + ); + } + + /** + * Add settings. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function add_settings() { + add_settings_section( + $this->section, + null, + array( $this, 'print_section_settings' ), + $this->slug + ); + /* + add_settings_field( + 'typesense_application_id', + esc_html__( 'Application ID', 'wp-search-with-typesense' ), + array( $this, 'application_id_callback' ), + $this->slug, + $this->section + ); +*/ + add_settings_field( + 'typesense_search_api_key', + esc_html__( 'Search-only API key', 'wp-search-with-typesense' ), + array( $this, 'search_api_key_callback' ), + $this->slug, + $this->section + ); +/* + add_settings_field( + 'typesense_api_key', + esc_html__( 'Admin API key', 'wp-search-with-typesense' ), + array( $this, 'api_key_callback' ), + $this->slug, + $this->section + ); +*/ + add_settings_field( + 'typesense_index_name_prefix', + esc_html__( 'Index name prefix', 'wp-search-with-typesense' ), + array( $this, 'index_name_prefix_callback' ), + $this->slug, + $this->section + ); + + add_settings_field( + 'typesense_powered_by_enabled', + esc_html__( 'Remove Typesense powered by logo', 'wp-search-with-typesense' ), + array( $this, 'powered_by_enabled_callback' ), + $this->slug, + $this->section + ); + + //register_setting( $this->option_group, 'typesense_application_id', array( $this, 'sanitize_application_id' ) ); + register_setting( $this->option_group, 'typesense_search_api_key', array( $this, 'sanitize_search_api_key' ) ); + //register_setting( $this->option_group, 'typesense_api_key', array( $this, 'sanitize_api_key' ) ); + //register_setting( $this->option_group, 'typesense_index_name_prefix', array( $this, 'sanitize_index_name_prefix' ) ); + //register_setting( $this->option_group, 'typesense_powered_by_enabled', array( $this, 'sanitize_powered_by_enabled' ) ); + } + + /** + * Application ID callback. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function application_id_callback() { + + $settings = $this->plugin->get_settings(); + $setting = $settings->get_application_id(); + $disabled_html = $settings->is_application_id_in_config() ? ' disabled' : ''; +?> + /> +

+ + * @since 1.0.0 + */ + public function search_api_key_callback() { + $settings = $this->plugin->get_settings(); + $setting = $settings->get_search_api_key(); + $disabled_html = $settings->is_search_api_key_in_config() ? ' disabled' : ''; + +?> + /> +

+ + * @since 1.0.0 + */ + public function api_key_callback() { + $settings = $this->plugin->get_settings(); + $setting = $settings->get_api_key(); + $disabled_html = $settings->is_api_key_in_config() ? ' disabled' : ''; +?> + /> +

+ + * @since 1.0.0 + */ + public function index_name_prefix_callback() { + $settings = $this->plugin->get_settings(); + $index_name_prefix = $settings->get_index_name_prefix(); + $disabled_html = $settings->is_index_name_prefix_in_config() ? ' disabled' : ''; +?> + /> +

+ + * @since 2020-07-24 + */ + public function powered_by_enabled_callback() { + $powered_by_enabled = $this->plugin->get_settings()->is_powered_by_enabled(); + $checked = ''; + if ( ! $powered_by_enabled ) { + $checked = ' checked'; + } + echo "' . + '

' . esc_html( __( 'This will remove the Typesense logo from the autocomplete and the search page. We require that you keep the Typesense logo if you are using a free plan.', 'wp-search-with-typesense' ) ) . '

'; + } + + /** + * Sanitize application ID. + * + * @author Richard Aber + * @since 2020-07-24 + * + * @param string $value The value to sanitize. + * + * @return string + */ + public function sanitize_application_id( $value ) { + if ( $this->plugin->get_settings()->is_application_id_in_config() ) { + $value = $this->plugin->get_settings()->get_application_id(); + } + $value = sanitize_text_field( $value ); + + if ( empty( $value ) ) { + add_settings_error( + $this->option_group, + 'empty', + esc_html__( 'Application ID should not be empty.', 'wp-search-with-typesense' ) + ); + + } + + return $value; + } + + /** + * Sanitize search API key. + * + * @author Richard Aber + * @since 2020-07-24 + * + * @param string $value The value to sanitize. + * + * @return string + */ + public function sanitize_search_api_key( $value ) { + if ( $this->plugin->get_settings()->is_search_api_key_in_config() ) { + $value = $this->plugin->get_settings()->get_search_api_key(); + } + $value = sanitize_text_field( $value ); + + if ( empty( $value ) ) { + add_settings_error( + $this->option_group, + 'empty', + esc_html__( 'Search-only API key should not be empty.', 'wp-search-with-typesense' ) + ); + } + + return $value; + } + + /** + * Sanitize Admin API key. + * + * @author Richard Aber + * @since 2020-07-24 + * + * @param string $value The value to sanitize. + * + * @return string + */ + public function sanitize_api_key( $value ) { + if ( $this->plugin->get_settings()->is_api_key_in_config() ) { + $value = $this->plugin->get_settings()->get_api_key(); + } + $value = sanitize_text_field( $value ); + + if ( empty( $value ) ) { + add_settings_error( + $this->option_group, + 'empty', + esc_html__( 'API key should not be empty', 'wp-search-with-typesense' ) + ); + } + + $errors = get_settings_errors( $this->option_group ); + + // @todo Not 100% clear why this is returning here. + if ( ! empty( $errors ) ) { + return $value; + } + + $settings = $this->plugin->get_settings(); + + $valid_credentials = true; + try { + Typesense_API::assert_valid_credentials( $settings->get_application_id(), $value ); + } catch ( Exception $exception ) { + $valid_credentials = false; + add_settings_error( + $this->option_group, + 'login_exception', + $exception->getMessage() + ); + } + + if ( ! $valid_credentials ) { + add_settings_error( + $this->option_group, + 'no_connection', + esc_html__( + 'We were unable to authenticate you against the Typesense servers with the provided information. Please ensure that you used a valid Application ID and Admin API key.', + 'wp-search-with-typesense' + ) + ); + $settings->set_api_is_reachable( false ); + } else { + if ( ! Typesense_API::is_valid_search_api_key( $settings->get_application_id(), $settings->get_search_api_key() ) ) { + add_settings_error( + $this->option_group, + 'wrong_search_API_key', + esc_html__( + 'It looks like your search API key is wrong. Ensure that the key you entered has only the search capability and nothing else. Also ensure that the key has no limited time validity.', + 'wp-search-with-typesense' + ) + ); + $settings->set_api_is_reachable( false ); + } else { + add_settings_error( + $this->option_group, + 'connection_success', + esc_html__( 'We succesfully managed to connect to the Typesense servers with the provided information. Your search API key has also been checked and is OK.', 'wp-search-with-typesense' ), + 'updated' + ); + $settings->set_api_is_reachable( true ); + } + } + + return $value; + } + + /** + * Determine if the index name prefix is valid. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $index_name_prefix The index name prefix. + * + * @return bool + */ + public function is_valid_index_name_prefix( $index_name_prefix ) { + $to_validate = str_replace( '_', '', $index_name_prefix ); + + return ctype_alnum( $to_validate ); + } + + /** + * Sanitize the index name prefix. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $value The value to sanitize. + * + * @return bool|mixed|string|void + */ + public function sanitize_index_name_prefix( $value ) { + if ( $this->plugin->get_settings()->is_index_name_prefix_in_config() ) { + $value = $this->plugin->get_settings()->get_index_name_prefix(); + } + + if ( $this->is_valid_index_name_prefix( $value ) ) { + return $value; + } + + add_settings_error( + $this->option_group, + 'wrong_prefix', + esc_html__( 'Indices prefix can only contain alphanumeric characters and underscores.', 'wp-search-with-typesense' ) + ); + + $value = get_option( 'typesense_index_name_prefix' ); + + return $this->is_valid_index_name_prefix( $value ) ? $value : 'wp_'; + } + + /** + * Sanitize the powered by enabled setting. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $value The value to sanitize. + * + * @return string + */ + public function sanitize_powered_by_enabled( $value ) { + return 'no' === $value ? 'no' : 'yes'; + } + + /** + * Display the page. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function display_page() { + require_once dirname( __FILE__ ) . '/partials/form-options.php'; + } + + /** + * Display errors. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function display_errors() { + settings_errors( $this->option_group ); + } + + /** + * Print the settings section. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function print_section_settings() { + echo '

' . esc_html__( 'Configure your Typesense account credentials. You can find them in the "API Keys" section of your Typesense dashboard.', 'wp-search-with-typesense' ) . '

'; + echo '

' . esc_html__( 'Once you provide your Typesense Application ID and API key, this plugin will be able to securely communicate with Typesense servers.', 'wp-search-with-typesense' ) . ' ' . esc_html__( 'We ensure your information is correct by testing them against the Typesense servers upon save.', 'wp-search-with-typesense' ) . '

'; + /* translators: the placeholder contains the URL to Typesense's website. */ + echo '

' . wp_kses_post( sprintf( __( 'No Typesense account yet? Follow this link to create one for free in a couple of minutes!', 'wp-search-with-typesense' ), 'https://www.typesense.com/users/sign_up' ) ) . '

'; + } +} diff --git a/includes/admin/class-typesense-admin.php b/includes/admin/class-typesense-admin.php new file mode 100644 index 0000000..3d5059b --- /dev/null +++ b/includes/admin/class-typesense-admin.php @@ -0,0 +1,331 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Typesense_Admin + * + * @since 1.0.0 + */ + +use Typesense\Client; +class Typesense_Admin { + + /** + * The Typesense Plugin. + * + * @since 1.0.0 + * + * @var Typesense_Plugin + */ + private $plugin; + + /** + * Typesense_Admin constructor. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Typesense_Plugin $plugin The Typesense Plugin. + */ + public function __construct( Typesense_Plugin $plugin ) { + $this->plugin = $plugin; + + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); + + $api = $plugin->get_api(); + //if ( $api->is_reachable() ) { + new Typesense_Admin_Page_Autocomplete( $plugin->get_settings()); + new Typesense_Admin_Page_Native_Search( $plugin ); + + add_action( 'wp_ajax_typesense_re_index', array( $this, 're_index' ) ); + add_action( 'wp_ajax_typesense_push_settings', array( $this, 'push_settings' ) ); + + //$maybe_get_page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_STRING ); + //if ( ! empty( $maybe_get_page ) && 'typesense' === substr( $maybe_get_page, 0, 7 ) ) { + // add_action( 'admin_notices', array( $this, 'display_reindexing_notices' ) ); + //} + //} + + new Typesense_Admin_Page_Settings( $plugin ); + + add_action( 'admin_notices', array( $this, 'display_unmet_requirements_notices' ) ); + } + + /** + * Enqueue styles. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function enqueue_styles() { + wp_enqueue_style( 'typesense-admin', plugin_dir_url( __FILE__ ) . 'css/typesense-admin.css', array(), TYPESENSE_VERSION ); + } + + /** + * Enqueue scripts. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function enqueue_scripts() { + /* + wp_enqueue_script( + 'typesense-admin', + plugin_dir_url( __FILE__ ) . 'js/typesense-admin.js', + array( 'jquery', 'jquery-ui-sortable' ), + TYPESENSE_VERSION, + false + ); + wp_enqueue_script( + 'typesense-admin-reindex-button', + plugin_dir_url( __FILE__ ) . 'js/reindex-button.js', + array( 'jquery' ), + TYPESENSE_VERSION, + false + ); + /* + wp_enqueue_script( + 'typesense-admin-push-settings-button', + plugin_dir_url( __FILE__ ) . 'js/push-settings-button.js', + array( 'jquery' ), + TYPESENSE_VERSION, + false + ); + */ + wp_enqueue_script( + 'typesense-admin-reindex-button', + plugin_dir_url( __FILE__ ) . 'js/reindex-button.js', + array( 'jquery' ), + ALGOLIA_VERSION, + false + ); + wp_localize_script( 'ajax-script', 'ajax_object', + array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'we_value' => 1234 ) ); + } + + /** + * Displays an error notice for every unmet requirement. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return void + */ + public function display_unmet_requirements_notices() { + if ( ! extension_loaded( 'mbstring' ) ) { + echo '
+

' . esc_html__( 'Typesense Search requires the "mbstring" PHP extension to be enabled. Please contact your hosting provider.', 'wp-search-with-typesense' ) . '

+
'; + } elseif ( ! function_exists( 'mb_ereg_replace' ) ) { + echo '
+

' . esc_html__( 'Typesense needs "mbregex" NOT to be disabled. Please contact your hosting provider.', 'wp-search-with-typesense' ) . '

+
'; + } + + if ( ! extension_loaded( 'curl' ) ) { + echo '
+

' . esc_html__( 'Typesense Search requires the "cURL" PHP extension to be enabled. Please contact your hosting provider.', 'wp-search-with-typesense' ) . '

+
'; + + return; + } + + $this->w3tc_notice(); + } + + /** + * Display notice to help users adding 'typesense_' as an ignored query string to the db caching configuration. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return void + */ + public function w3tc_notice() { + if ( ! function_exists( 'w3tc_pgcache_flush' ) || ! function_exists( 'w3_instance' ) ) { + return; + } + + $config = w3_instance( 'W3_Config' ); + $enabled = $config->get_integer( 'dbcache.enabled' ); + $settings = array_map( 'trim', $config->get_array( 'dbcache.reject.sql' ) ); + + if ( $enabled && ! in_array( 'typesense_', $settings, true ) ) { + /* translators: placeholder contains the URL to the caching plugin's config page. */ + $message = sprintf( __( 'In order for database caching to work with Typesense you must add typesense_ to the "Ignored Query Stems" option in W3 Total Cache settings here.', 'wp-search-with-typesense' ), esc_url( admin_url( 'admin.php?page=w3tc_dbcache' ) ) ); + ?> +
+

+
+ + * @since 1.0.0 + */ + public function display_reindexing_notices() { + $indices = $this->plugin->get_indices( + array( + 'enabled' => true, + ) + ); + + $allowed_html = array( + 'strong' => array(), + ); + + foreach ( $indices as $index ) { + if ( $index->exists() ) { + continue; + } + ?> +
+

+ %1$s', 'wp-search-with-typesense' ), + esc_html( $index->get_admin_name() ) + ), + $allowed_html + ); + ?> +

+

+ +

+
+ + * @since 1.0.0 + * + * @throws RuntimeException If index ID or page are not provided, or index name dies not exist. + * @throws Exception If index ID or page are not provided, or index name dies not exist. + */ + public function re_index() { + /* + $index_id = filter_input( INPUT_POST, 'index_id', FILTER_SANITIZE_STRING ); + $page = filter_input( INPUT_POST, 'p', FILTER_SANITIZE_STRING ); + + try { + if ( empty( $index_id ) ) { + throw new RuntimeException( 'Index ID should be provided.' ); + } + + if ( ! ctype_digit( $page ) ) { + throw new RuntimeException( 'Page should be provided.' ); + } + $page = (int) $page; + + $index = $this->plugin->get_index( $index_id ); + if ( null === $index ) { + throw new RuntimeException( sprintf( 'Index named %s does not exist.', $index_id ) ); + } + + $total_pages = $index->get_re_index_max_num_pages(); + + ob_start(); + if ( $page <= $total_pages || 0 === $total_pages ) { + $index->re_index( $page ); + } + ob_end_clean(); + + $response = array( + 'totalPagesCount' => $total_pages, + 'finished' => $page >= $total_pages, + ); + + wp_send_json( $response ); + } catch ( Exception $exception ) { + echo esc_html( $exception->getMessage() ); + throw $exception; + } + */ + //echo 'gfg'; + $document = [ + 'post_id' => '1', + 'id' => '500', + 'post_content' => 'New world', + 'post_title' => 'Dummy text 2', + 'post_excerpt' => 'dcd', + 'is_sticky' => 1, + + 'post_modified' => 'fwecfwe', + 'post_date' => 'cwe', + + 'comment_count' => 2, + ]; + $indices = $this->plugin->get_indices(); + //$post_index = $indices[0]; + try{ + $indices[0]->sync($document); + //$client = $this->plugin->get_api()->get_client(); + //$client->collections['posts']->documents->create($document); + //echo gettype($this->plugin->get_api()->get_client()); + //$client = $this->plugin->get_api()->get_client(); + //$client->collections['posts']->documents->create($document); + } + catch(Exception $e){ + echo $e->getMessage(); + } + + echo 'Victory'; + } + + /** + * Push settings. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @throws RuntimeException If index_id is not provided or if the corresponding index is null. + * @throws Exception If index_id is not provided or if the corresponding index is null. + */ + public function push_settings() { + + $index_id = filter_input( INPUT_POST, 'index_id', FILTER_SANITIZE_STRING ); + + try { + if ( empty( $index_id ) ) { + throw new RuntimeException( 'index_id should be provided.' ); + } + + $index = $this->plugin->get_index( $index_id ); + if ( null === $index ) { + throw new RuntimeException( sprintf( 'Index named %s does not exist.', $index_id ) ); + } + + $index->push_settings(); + + $response = array( + 'success' => true, + ); + wp_send_json( $response ); + } catch ( Exception $exception ) { + echo esc_html( $exception->getMessage() ); + throw $exception; + } + + } +} diff --git a/includes/admin/css/index.php b/includes/admin/css/index.php new file mode 100644 index 0000000..8142269 --- /dev/null +++ b/includes/admin/css/index.php @@ -0,0 +1 @@ +OK1~O6g_VyX_{ImQqqi{8i%IwLnWq7ZB=Sg8WH>iHPxD(j%l10lbJS2p#>KT zqTohRT-dr47cSfgg6N_vH@Z@BhOK0@{;3xv7%3_<8N;;sf+xyq1UGAH;~)D)hN$j zeA+j7ihPcvXR@SE1fOj=14s`TADpBRNXf6r-;?)DR_b%X8K~qx$iro;sADG{0dhNe zdqtm{W-WR0jpUlCS4#Jf27i+8&eQD4`I`O3F2-ChJ6T=ufiR_`kym=#*3@o^08cT)c5{a^D#Z7j(%tlEzb#Bl&8SbSh8A$G*KcZWnv zc$-Lzp&jvIaZn6w?-kuGTPdXaBWj>ODs_exDb}5k_70?_ln6(qRDYTzK~a%9rH(@% zH|3JcSJhEfQPt4K;aID9y?fg=ZFe*q+m-9Nm$t@|$uTRPaeZd_RJOiQC@j{p+1g^E zuu#uR`EPT*8RbzVvObcDC_N)v0%wFQ;?dsTXgnBfZ-0@@o0*K6PpR@d+`OSD2%vRA~rlM6Cd=jkNC2O{rux=9&VwY_i!5q@NlYVRV>SF z?jGZ%YHi9gwZ24h?Uu3Tsx`%Y$(Swc)#hHbX69kbtZPQeELHV-X+pa&uN^5`Io6iI pVW(Zy`6~=IFw<#bI_mTj)GlD2^AV^0<-GQn;Hf+PVD+^$ + + + + + + + + + diff --git a/includes/admin/fonts/algolia.ttf b/includes/admin/fonts/algolia.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0527a2a7505ac34bb427846efdf06b0adfba5b4c GIT binary patch literal 1248 zcmaJ3^v&qnwEtz- zBp^Lze0YX}FD1Vv|3uz5Q>icd=b@1QAP<+#qK18V3drr`?GD%_bG&S4b;ASpgaBG0@e@$j(fHM9*sGdYG}aUk*KMvVAea@}q)EWY z5j*8i-v2c(l*Xz&&#t}bKnzE5hRyp%;$m;Czb7PG!h1wojO>k#ieqA^e?auK?52=P zMigH%Ds_f~Qg2UOIy97)QX(9cQpq$)oMKSwlsb-o*_lgh+)&1qprV9!jP|yQcL(;| zR1ZY6z58>057XvEA~9j6Gmg(}oXggi3x$ME)|zh`f{VR;<()2Wp?=!M0qnvZ>%DmVRDz5+ zSRyXDSSCK^Vh`~(7kd$b>f#pac^3yTgeS8_vtpVCW>JKR3Wo_}O0U*tO+y`wColya zRn(Zk0P74Qj>ML$wiNSvX`!rDxv9=Z7f^Xw-&Orx%rbVILdb$wdByriBinmP6s n#|eAyI(ILzjA3`Ap-w+d?J|})pR|7<=llSByn)9F9%&NPJ!`>kRSQs&6`9u|qlZXZ`h}M7m|DSV`Qr(ePFtw8!DMS}(0)<@z zVb&GuZ1b@ie=Cc#fv(UX8)AK(-K?$V6o^uKFkdxFXKGpUZpadRpXnKI)E%O>_GljW^U zrcz2KOO;G!tDKb5-`Bbm-t9o(SRfJbb__SWrkTt_!S3#0$nCDLe-%j^iG-1k9N7I& zvz1~pS**-vwn`%-r7iwS?DJ?(Jhw~WB1i5zl1%Hl)x1_jQyqh@ zQl2y_9+^328f7)78+uVI>kI1qx;maUQ0{R!!CfEr**93;sa9F O2Vh2A+~F(v7ykxv+QUx( literal 0 HcmV?d00001 diff --git a/includes/admin/fonts/index.php b/includes/admin/fonts/index.php new file mode 100644 index 0000000..8142269 --- /dev/null +++ b/includes/admin/fonts/index.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/includes/admin/index.php b/includes/admin/index.php new file mode 100644 index 0000000..8142269 --- /dev/null +++ b/includes/admin/index.php @@ -0,0 +1 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +?> + +
+

+
+ option_group ); + do_settings_sections( $this->slug ); + submit_button(); + ?> +
+
diff --git a/includes/admin/partials/form-override-search-option.php b/includes/admin/partials/form-override-search-option.php new file mode 100644 index 0000000..70c6517 --- /dev/null +++ b/includes/admin/partials/form-override-search-option.php @@ -0,0 +1,71 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +?> + +
+ +
+ This is only a valid option if you wish to search on your content from another website.', + 'wp-search-with-typesense' + ), + [ + 'br' => [], + ] + ); + ?> +
+ + +
+ This will allow your search results to be typo tolerant.
This option does not support filtering and displaying instant search results but has the advantage to play nicely with any theme.', + 'wp-search-with-typesense' + ), + [ + 'br' => [], + 'b' => [], + ] + ); + ?> +
+ + +
+ By default you will be able to filter by post type, categories, tags and authors.
Please note that the plugin is shipped with some sensible default styling rules
but it could require some CSS adjustments to provide an optimal search experience.', + 'wp-search-with-typesense' + ), + [ + 'br' => [], + ] + ); + ?> +
+
diff --git a/includes/admin/partials/index-records.php b/includes/admin/partials/index-records.php new file mode 100644 index 0000000..7221501 --- /dev/null +++ b/includes/admin/partials/index-records.php @@ -0,0 +1,22 @@ + + + + + + + + + + + + +
+ +
+

+ +
+ +
+ +

diff --git a/includes/admin/partials/index.php b/includes/admin/partials/index.php new file mode 100644 index 0000000..8142269 --- /dev/null +++ b/includes/admin/partials/index.php @@ -0,0 +1 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +?> + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + /> + +
+ + + +
+ + + + + + +
+

+ +
+ +
+ +

diff --git a/includes/admin/partials/page-autocomplete.php b/includes/admin/partials/page-autocomplete.php new file mode 100644 index 0000000..4f18afa --- /dev/null +++ b/includes/admin/partials/page-autocomplete.php @@ -0,0 +1,22 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +?> + +
+

+
+ option_group ); + do_settings_sections( $this->slug ); + ?> + +
+
diff --git a/includes/admin/partials/page-search.php b/includes/admin/partials/page-search.php new file mode 100644 index 0000000..1a5f2f1 --- /dev/null +++ b/includes/admin/partials/page-search.php @@ -0,0 +1,30 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +?> + +
+

+ + + +

+
+ option_group ); + do_settings_sections( $this->slug ); + submit_button(); + ?> +
+
diff --git a/includes/class-typesense-api.php b/includes/class-typesense-api.php new file mode 100644 index 0000000..a7d1f4e --- /dev/null +++ b/includes/class-typesense-api.php @@ -0,0 +1,247 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +use Typesense\Client; + +/** + * Class Typesense_API + * + * @since 1.0.0 + */ +class Typesense_API { + + /** + * The Client instance. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Client + */ + private $client; + + /** + * The Typesense_Settings instance. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Settings + */ + private $settings; + public $dummy; + /** + * Typesense_API constructor. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Typesense_Settings $settings The Typesense_Settings instance. + */ + public function __construct( Typesense_Settings $settings ) { + $this->settings = $settings; + } + + /** + * Check if the Aloglia API is reachable. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + public function is_reachable() { + if ( ! $this->settings->get_api_is_reachable() ) { + return false; + } + + try { + // Here we check that all requirements for the PHP API Client are met. + // If they are not, instantiating the client will throw exceptions. + $client = $this->get_client(); + } catch ( Exception $e ) { + return false; + } + + return null !== $client; + } + + /** + * Get the Client. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return Client|null + */ + public function get_client(){ + + //$application_id = $this->settings->get_application_id(); + //$api_key = $this->settings->get_api_key(); + /* + if ( + empty( $application_id ) || + empty( $api_key ) + ) { + return null; + } + */ + $this->client = new Client( + [ + 'api_key' => '123', + 'nodes' => [ + [ + 'host' => 'localhost', + 'port' => '8108', + 'protocol' => 'http', + ], + ], + 'connection_timeout_seconds' => 2, + ] + ); + + return $this->client; + } + + /** + * Assert that the credentials are valid. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $application_id The Typesense Application ID. + * @param string $api_key The Typesense Admin API Key. + * + * @return void + * + * @throws Exception If the Typesense Admin API Key does not have correct ACLs. + */ + /* + public static function assert_valid_credentials( $application_id, $api_key ) { + + $client = Typesense_Search_Client_Factory::create( + (string) $application_id, + (string) $api_key + ); + + // This checks if the API Key is an Admin API key. + // Admin API keys have no scopes so we need a separate check here. + try { + $client->listApiKeys(); + + return; + } catch ( Exception $exception ) { // phpcs:ignore --- intentionally empty catch. + } + + // If this call does not succeed, then the application_ID or API_key is/are wrong. + // This will raise an exception. + $key = $client->getApiKey( (string) $api_key ); + + $required_acls = array( + 'addObject', + 'deleteObject', + 'listIndexes', + 'deleteIndex', + 'settings', + 'editSettings', + ); + + $missing_acls = array(); + foreach ( $required_acls as $required_acl ) { + if ( ! in_array( $required_acl, $key['acl'], true ) ) { + $missing_acls[] = $required_acl; + } + } + + if ( ! empty( $missing_acls ) ) { + throw new Exception( + 'Your admin API key is missing the following ACLs: ' . implode( ', ', $missing_acls ) + ); + } + } + + /** + * Check if the credentials are valid. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $application_id The Typesense Application ID. + * @param string $api_key The Typesense Admin API Key. + * + * @return bool + public static function is_valid_credentials( $application_id, $api_key ) { + try { + self::assert_valid_credentials( $application_id, $api_key ); + } catch ( Exception $e ) { + return false; + } + + return true; + } + + /** + * Check if the Search API Key is valid. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $application_id The Typesense Application ID. + * @param string $search_api_key The Typesense Search API Key. + * + * @return bool + public static function is_valid_search_api_key( $application_id, $search_api_key ) { + + $client = Typesense_Search_Client_Factory::create( + (string) $application_id, + (string) $search_api_key + ); + + // If this call does not succeed, the application_ID and/or API_key are wrong. + try { + $acl = $client->getApiKey( $search_api_key ); + } catch ( TypesenseException $e ) { + return false; + } + + // We expect a search only key for security reasons. Will be used in front. + $scopes = array_flip( $acl['acl'] ); + if ( ! isset( $scopes['search'] ) ) { + return false; + } + unset( $scopes['search'] ); + + if ( isset( $scopes['settings'] ) ) { + unset( $scopes['settings'] ); + } + + if ( isset( $scopes['listIndexes'] ) ) { + unset( $scopes['listIndexes'] ); + } + + // Short circuit ACL checks for local development. + if ( defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV ) { + return true; + } + + if ( ! empty( $scopes ) ) { + // The API key has more permissions than allowed. + return false; + } + + // We do expect a search key without unlimited TTL. + if ( 0 !== $acl['validity'] ) { + return false; + } + + return true; + } + */ +} \ No newline at end of file diff --git a/includes/class-typesense-autocomplete-config.php b/includes/class-typesense-autocomplete-config.php new file mode 100644 index 0000000..bdb57fd --- /dev/null +++ b/includes/class-typesense-autocomplete-config.php @@ -0,0 +1,192 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Typesense_Autocomplete_Config + * + * @since 1.0.0 + */ +class Typesense_Autocomplete_Config { + + /** + * The Typesense_Plugin instance. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Plugin + */ + private $plugin; + + /** + * Typesense_Autocomplete_Config constructor. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Typesense_Plugin $plugin The Typesense_Plugin instance. + */ + public function __construct( Typesense_Plugin $plugin ) { + $this->plugin = $plugin; + } + + /** + * Get form data. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return array + */ + public function get_form_data() { + $indices = $this->plugin->get_indices(); + $config = array(); + + $existing_config = $this->get_config(); + + /** + * Loop over the indices. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Index $index + */ + foreach ( $indices as $index ) { + $index_config = $this->extract_index_config( $existing_config, $index->get_id() ); + if ( $index_config ) { + // If there is an existing configuration, add it. + $config[] = $index_config; + continue; + } + + $default_config = $index->get_default_autocomplete_config(); + $default_config['enabled'] = false; + + $config[] = $default_config; + } + + usort( + $config, function( $a, $b ):int { + return $a['position'] <=> $b['position']; + } + ); + + return $config; + } + + /** + * Sanitize form data. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param array $data The data to sanitize. + * + * @return mixed + */ + public function sanitize_form_data( $data ) { + + if ( ! is_array( $data ) ) { + return array(); + } + + $sanitized = array(); + + foreach ( $data as $index_id => $config ) { + $index = $this->plugin->get_index( $index_id ); + + // Remove disabled indices. + if ( ! isset( $config['enabled'] ) ) { + continue; + } + + $merged_config = array_merge( + $index->get_default_autocomplete_config(), + array( + 'position' => (int) $config['position'], + 'max_suggestions' => (int) $config['max_suggestions'], + ) + ); + + if ( isset( $config['label'] ) && ! empty( $config['label'] ) ) { + $merged_config['label'] = $config['label']; + } + + $sanitized[] = $merged_config; + } + + return $sanitized; + } + + /** + * Extract index config. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param array $config The config. + * @param string $index_id The index id. + * + * @return mixed|void + */ + private function extract_index_config( array $config, $index_id ) { + foreach ( $config as $entry ) { + if ( $index_id === $entry['index_id'] ) { + return $entry; + } + } + } + + /** + * Get config. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return array + */ + public function get_config() { + $settings = $this->plugin->get_settings(); + $config = $settings->get_autocomplete_config(); + foreach ( $config as $key => &$entry ) { + if ( ! isset( $entry['index_id'] ) ) { + unset( $config[ $key ] ); + continue; + } + + $index = $this->plugin->get_index( $entry['index_id'] ); + if ( null === $index ) { + unset( $config[ $key ] ); + continue; + } + $entry['index_name'] = $index->get_name(); + $entry['enabled'] = true; + } + + $config = (array) apply_filters( '_autocomplete_config', $config ); + + // Remove manually disabled indices. + $config = array_filter( + $config, function( $item ) { + return (bool) $item['enabled']; + } + ); + + // Sort the indices. + usort( + $config, function( $a, $b ):int { + return $a['position'] <=> $b['position']; + } + ); + + return $config; + } +} diff --git a/includes/class-typesense-cli.php b/includes/class-typesense-cli.php new file mode 100644 index 0000000..b0c835a --- /dev/null +++ b/includes/class-typesense-cli.php @@ -0,0 +1,144 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Algolia_CLI + * + * Push and re-index records into Algolia indices. + * + * @since 1.0.0 + */ +class Algolia_CLI extends \WP_CLI_Command { + + /** + * The Algolia_Plugin instance. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Algolia_Plugin + */ + private $plugin; + + /** + * Algolia_CLI constructor. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function __construct() { + $this->plugin = Algolia_Plugin_Factory::create(); + } + + /** + * Push all records to Algolia for a given index. + * + * ## OPTIONS + * + * [] + * : The id of the index without the prefix. + * + * [--clear] + * : Clear all existing records prior to pushing the records. + * + * [--all] + * : Re-indexes all the enabled indices. + * + * ## EXAMPLES + * + * wp algolia re-index + * + * @alias re-index + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. + */ + public function reindex( $args, $assoc_args ) { + if ( ! $this->plugin->get_api()->is_reachable() ) { + WP_CLI::error( 'The configuration for this website does not allow to contact the Algolia API.' ); + } + + $index_id = isset( $args[0] ) ? $args[0] : null; + $clear = WP_CLI\Utils\get_flag_value( $assoc_args, 'clear' ); + $all = WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ); + + if ( ! $index_id && ! $all ) { + WP_CLI::error( 'You need to either provide an index name or specify the --all argument to re-index all enabled indices.' ); + } + + if ( $index_id && $all ) { + WP_CLI::error( 'You can not give both an index name and the --all parameter.' ); + } + + if ( $all ) { + $indices = $this->plugin->get_indices( + array( + 'enabled' => true, + ) + ); + } else { + $index = $this->plugin->get_index( $index_id ); + if ( ! $index ) { + WP_CLI::error( sprintf( 'Index with id "%s" does not exist. Make sure you don\'t include the prefix.', $index_id ) ); + } + $indices = array( $index ); + } + + foreach ( $indices as $index ) { + $this->do_reindex( $index, $clear ); + } + } + + /** + * Do reindex. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Algolia_Index $index Algolia_Index instance. + * @param bool $clear Clear all existing records prior to pushing the records. + * + * @return void + */ + private function do_reindex( Algolia_Index $index, $clear ) { + + if ( $clear ) { + /* translators: the placeholder will contain the name of the index. */ + WP_CLI::log( sprintf( __( 'About to clear index %s...', 'wp-search-with-algolia' ), $index->get_name() ) ); + $index->clear(); + /* translators: the placeholder will contain the name of the index. */ + WP_CLI::success( sprintf( __( 'Correctly cleared index "%s".', 'wp-search-with-algolia' ), $index->get_name() ) ); + } + + $total_pages = $index->get_re_index_max_num_pages(); + + if ( 0 === $total_pages ) { + $index->re_index( 1 ); + WP_CLI::success( sprintf( 'Index %s was created but no entries were sent.', $index->get_name() ) ); + + return; + } + + $progress = WP_CLI\Utils\make_progress_bar( sprintf( 'Processing %s pages of results.', $total_pages ), $total_pages ); + + $page = 1; + do { + $index->re_index( $page++ ); + $progress->tick(); + } while ( $page <= $total_pages ); + + $progress->finish(); + + WP_CLI::success( sprintf( 'Indexed "%s" pages of results inside index "%s"', $total_pages, $index->get_name() ) ); + } +} diff --git a/includes/class-typesense-compatibility.php b/includes/class-typesense-compatibility.php new file mode 100644 index 0000000..46ddb25 --- /dev/null +++ b/includes/class-typesense-compatibility.php @@ -0,0 +1,123 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Algolia_Compatibility + * + * @since 1.0.0 + */ +class Algolia_Compatibility { + + /** + * The "current language" from WPML, if available, else null. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var string|null + */ + private $current_language; + + /** + * Algolia_Compatibility constructor. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function __construct() { + add_action( 'algolia_before_get_records', array( $this, 'register_vc_shortcodes' ) ); + add_action( 'algolia_before_get_records', array( $this, 'enable_yoast_frontend' ) ); + add_action( 'algolia_before_get_records', array( $this, 'wpml_switch_language' ) ); + add_action( 'algolia_after_get_records', array( $this, 'wpml_switch_back_language' ) ); + } + + /** + * Enable Yoast frontend. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function enable_yoast_frontend() { + if ( class_exists( 'WPSEO_Frontend' ) && method_exists( 'WPSEO_Frontend', 'get_instance' ) ) { + WPSEO_Frontend::get_instance(); + } + } + + /** + * Register VC shortcodes. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function register_vc_shortcodes() { + if ( class_exists( 'WPBMap' ) && method_exists( 'WPBMap', 'addAllMappedShortcodes' ) ) { + WPBMap::addAllMappedShortcodes(); + } + } + + /** + * WPML switch language. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @global \SitePress $sitepress The WPML global SitePress instance. + * + * @param mixed $post Maybe post object. + * + * @return void + */ + public function wpml_switch_language( $post ) { + if ( ! $post instanceof WP_Post || ! $this->is_wpml_enabled() ) { + return; + } + + global $sitepress; + $lang_info = wpml_get_language_information( null, $post->ID ); + $this->current_language = $sitepress->get_current_language(); + $sitepress->switch_lang( $lang_info['language_code'] ); + } + + /** + * WPML switch back language. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @global \SitePress $sitepress The WPML global SitePress instance. + * + * @param mixed $post Maybe post object. + * + * @return void + */ + public function wpml_switch_back_language( $post ) { + if ( ! $post instanceof WP_Post || ! $this->is_wpml_enabled() ) { + return; + } + + global $sitepress; + + $sitepress->switch_lang( $this->current_language ); + } + + /** + * Check if WPML is enabled. + * + * @link https://github.com/algolia/algoliasearch-wordpress/issues/567 + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + private function is_wpml_enabled() { + return function_exists( 'icl_object_id' ) && ! class_exists( 'Polylang' ); + } +} diff --git a/includes/class-typesense-plugin.php b/includes/class-typesense-plugin.php new file mode 100644 index 0000000..95f65e9 --- /dev/null +++ b/includes/class-typesense-plugin.php @@ -0,0 +1,452 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Typesense_Plugin + * + * @since 1.0.0 + */ +class Typesense_Plugin { + + const NAME = 'typesense'; + + /** + * Instance of Typesense_API. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_API + */ + protected $api; + + /** + * Instance of Typesense_Settings. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Settings + */ + private $settings; + + /** + * Instance of Typesense_Autocomplete_Config. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Autocomplete_Config + */ + private $autocomplete_config; + + /** + * Array of indices. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var array + */ + private $indices; + + /** + * Array of watchers. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var array + */ + private $changes_watchers; + + /** + * Instance of Typesense_Styles. + * + * @author WebDevStudios + * @since 1.5.0 + * + * @var Typesense_Styles + */ + private $styles; + + /** + * Instance of Typesense_Scripts. + * + * @author WebDevStudios + * @since 1.5.0 + * + * @var Typesense_Scripts + */ + private $scripts; + + /** + * Instance of Typesense_Template_Loader. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Template_Loader + */ + private $template_loader; + + /** + * Instance of Typesense_Compatibility. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Typesense_Compatibility + */ + private $compatibility; + + /** + * Get the singleton instance of Typesense_Plugin. + * + * @author WebDevStudios + * @since 1.0.0 + * @deprecated 1.6.0 Use Typesense_Plugin_Factory::create() + * @see Typesense_Plugin_Factory::create() + * + * @return Typesense_Plugin + */ + public static function get_instance() { + //_deprecated_function( __METHOD__, '1.6.0', 'Typesense_Plugin_Factory::create();' ); + return Typesense_Plugin_Factory::create(); + } + + /** + * Typesense_Plugin constructor. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function __construct() { + $this->settings = new Typesense_Settings(); + $this->api = new Typesense_API( $this->settings ); + //$this->compatibility = new Typesense_Compatibility(); + //$this->styles = new Typesense_Styles(); + //$this->scripts = new Typesense_Scripts(); + add_action( 'init', array( $this, 'load' ), 20 ); + } + + /** + * Load. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function load() { + if ( $this->api->is_reachable() ) { + //$this->load_indices(); + //$this->override_wordpress_search(); + //$this->autocomplete_config = new Typesense_Autocomplete_Config( $this ); + //$this->template_loader = new Typesense_Template_Loader( $this ); + } + //$this->autocomplete_config = new Typesense_Autocomplete_Config( $this ); + $this->load_indices(); + // Load admin or public part of the plugin. + if ( is_admin() ) { + new Typesense_Admin( $this ); + } + } + + /** + * Get the plugin name. + * + * The name of the plugin used to uniquely identify it within the context of + * WordPress and to define internationalization functionality. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string The name of the plugin. + */ + public function get_name() { + return self::NAME; + } + + /** + * Retrieve the version number of the plugin. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string The version number of the plugin. + */ + public function get_version() { + return TYPESENSE_VERSION; + } + + /** + * Get the Aloglia_API. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return Typesense_API + */ + public function get_api() { + return $this->api; + } + + /** + * Get the Typesense_Settings. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return Typesense_Settings + */ + public function get_settings() { + return $this->settings; + } + + /** + * Override WordPress native search. + * + * Replaces native WordPress search results by Typesense ranked results. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return void + */ + private function override_wordpress_search() { + // Do not override native search if the feature is not enabled. + if ( ! $this->settings->should_override_search_in_backend() ) { + return; + } + + $index_id = $this->settings->get_native_search_index_id(); + $index = $this->get_index( $index_id ); + + if ( null === $index ) { + return; + } + + new Typesense_Search( $index ); + } + + /** + * Get the Typesense_Autocomplete_Config. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return Typesense_Autocomplete_Config + */ + public function get_autocomplete_config() { + return $this->autocomplete_config; + } + + /** + * Load indices. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function load_indices() { + //$synced_indices_ids = $this->settings->get_synced_indices_ids(); + + $client = $this->get_api()->get_client(); + $index_name_prefix = $this->settings->get_index_name_prefix(); + $this->indices=array(); + $this->indices[] = new Typesense_Posts_Index('post'); + + // Add a searchable posts index. + /* + $searchable_post_types = get_post_types( + array( + 'exclude_from_search' => false, + ), 'names' + ); + $searchable_post_types = (array) apply_filters( 'typesense_searchable_post_types', $searchable_post_types ); + $this->indices[] = new Typesense_Searchable_Posts_Index( $searchable_post_types ); + + // Add one posts index per post type. + $post_types = get_post_types(); + + $excluded_post_types = $this->settings->get_excluded_post_types(); + foreach ( $post_types as $post_type ) { + // Skip excluded post types. + if ( in_array( $post_type, $excluded_post_types, true ) ) { + continue; + } + + $this->indices[] = new Typesense_Posts_Index( $post_type ); + } + + // Add one terms index per taxonomy. + $taxonomies = get_taxonomies(); + $excluded_taxonomies = $this->settings->get_excluded_taxonomies(); + foreach ( $taxonomies as $taxonomy ) { + // Skip excluded taxonomies. + if ( in_array( $taxonomy, $excluded_taxonomies, true ) ) { + continue; + } + + $this->indices[] = new Typesense_Terms_Index( $taxonomy ); + } + + // Add the users index. + $this->indices[] = new Typesense_Users_Index(); + + // Allow developers to filter the indices. + $this->indices = (array) apply_filters( 'typesense_indices', $this->indices ); + */ + //foreach ( $this->indices as $index ) { + //$this->indices[0]->set_name_prefix( $index_name_prefix ); + $this->indices[0]->set_client( $client); + $this->changes_watchers=array(); + $this->changes_watchers[] = new Typesense_Post_Changes_Watcher( $this->indices[0] ); +/*' + if ( in_array( $index->get_id(), $synced_indices_ids, true ) ) { + $index->set_enabled( true ); + + if ( $index->contains_only( 'posts' ) ) { + $this->changes_watchers[] = new Typesense_Post_Changes_Watcher( $index ); + } elseif ( $index->contains_only( 'terms' ) ) { + $this->changes_watchers[] = new Typesense_Term_Changes_Watcher( $index ); + } elseif ( $index->contains_only( 'users' ) ) { + $this->changes_watchers[] = new Typesense_User_Changes_Watcher( $index ); + } + } +*/ + //} +/* + $this->changes_watchers = (array) apply_filters( 'typesense_changes_watchers', $this->changes_watchers ); + + foreach ( $this->changes_watchers as $watcher ) { + $watcher->watch(); + } +*/ + $this->changes_watchers[0]->watch(); + } + + + /** + * Get indices. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param array $args Array of arguments. + * + * @return array + */ + public function get_indices() { + /* + if ( empty( $args ) ) { + return $this->indices; + } + + $indices = $this->indices; + + if ( isset( $args['enabled'] ) && true === $args['enabled'] ) { + $indices = array_filter( + $indices, function( $index ) { + return $index->is_enabled(); + } + ); + } + + if ( isset( $args['contains'] ) ) { + $contains = (string) $args['contains']; + $indices = array_filter( + $indices, function( $index ) use ( $contains ) { + return $index->contains_only( $contains ); + } + ); + } +*/ + return $this->indices; + } + + /** + * Get index. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $index_id The ID of the index to get. + * + * @return Typesense_Index|null + */ + public function get_index( $index_id ) { + foreach ( $this->indices as $index ) { + if ( $index_id === $index->get_id() ) { + return $index; + } + } + + return null; + } + + /** + * Get the plugin path. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string + */ + public function get_path() { + return untrailingslashit( TYPESENSE_PATH ); + } + + /** + * Get the templates path. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string + */ + public function get_templates_path() { + return (string) apply_filters( 'typesense_templates_path', 'typesense/' ); + } + + /** + * Get the Typesense_Template_Loader. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return Typesense_Template_Loader + */ + public function get_template_loader() { + return $this->template_loader; + } + + /** + * Get the Typesense_Styles. + * + * @author WebDevStudios + * @since 1.5.0 + * + * @return Typesense_Styles + */ + public function get_styles() { + return $this->styles; + } + + /** + * Get the Typesense_Scripts. + * + * @author WebDevStudios + * @since 1.5.0 + * + * @return Typesense_Scripts + */ + public function get_scripts() { + return $this->scripts; + } +} diff --git a/includes/class-typesense-scripts.php b/includes/class-typesense-scripts.php new file mode 100644 index 0000000..32d5f92 --- /dev/null +++ b/includes/class-typesense-scripts.php @@ -0,0 +1,86 @@ + + * @since 1.5.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Algolia_Scripts + * + * @since 1.5.0 + */ +class Algolia_Scripts { + + /** + * Algolia_Scripts constructor. + * + * @author WebDevStudios + * @since 1.5.0 + */ + public function __construct() { + add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); + } + + /** + * Register scripts. + * + * @author WebDevStudios + * @since 1.5.0 + */ + public function register_scripts() { + + $in_footer = Algolia_Utils::get_scripts_in_footer_argument(); + + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + + wp_register_script( + 'algolia-search', + ALGOLIA_PLUGIN_URL . 'js/algoliasearch/dist/algoliasearch.jquery' . $suffix . '.js', + [ + 'jquery', + 'underscore', + 'wp-util', + ], + ALGOLIA_VERSION, + $in_footer + ); + + wp_register_script( + 'algolia-autocomplete', + ALGOLIA_PLUGIN_URL . 'js/autocomplete.js/dist/autocomplete' . $suffix . '.js', + [ + 'jquery', + 'underscore', + 'wp-util', + ], + ALGOLIA_VERSION, + $in_footer + ); + + wp_register_script( + 'algolia-autocomplete-noconflict', + ALGOLIA_PLUGIN_URL . 'js/autocomplete-noconflict.js', + [ + 'algolia-autocomplete', + ], + ALGOLIA_VERSION, + $in_footer + ); + + wp_register_script( + 'algolia-instantsearch', + ALGOLIA_PLUGIN_URL . 'js/instantsearch.js/dist/instantsearch-preact' . $suffix . '.js', + [ + 'jquery', + 'underscore', + 'wp-util', + ], + ALGOLIA_VERSION, + $in_footer + ); + } +} diff --git a/includes/class-typesense-search.php b/includes/class-typesense-search.php new file mode 100644 index 0000000..eca3229 --- /dev/null +++ b/includes/class-typesense-search.php @@ -0,0 +1,359 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +use Algolia\AlgoliaSearch\Exceptions\AlgoliaException; + +/** + * Class Algolia_Search + * + * @since 1.0.0 + */ +class Algolia_Search { + + /** + * Current page hits. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var array + */ + private $current_page_hits = []; + + /** + * Total hits. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var int + */ + private $total_hits; + + /** + * Instance of Algolia_Index. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @var Algolia_Index + */ + private $index; + + /** + * Algolia_Search constructor. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Algolia_Index $index Instance of Algolia_Index. + */ + public function __construct( Algolia_Index $index ) { + $this->index = $index; + + add_action( 'loop_start', [ $this, 'begin_highlighting' ] ); + add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); + add_action( 'wp_head', [ $this, 'output_highlighting_bundled_styles' ] ); + } + + /** + * Determines if we should filter the query passed as argument. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param WP_Query $query The WP_Query to check. + * + * @return bool + */ + private function should_filter_query( WP_Query $query ) { + $should_filter = ! $query->is_admin && $query->is_search() && $query->is_main_query(); + + /** + * Allow developers to override the return value of `should_filter_query()`. + * + * @since 1.3.0 + * + * @param bool $should_filter Whether Algolia should filter the search query. + * @param WP_Query $query The WP_Query that was tested for Algolia Search filtering. + */ + return (bool) apply_filters( + 'algolia_should_filter_query', + $should_filter, + $query + ); + } + + /** + * We force the WP_Query to only return records according to Algolia's ranking. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param WP_Query $query The WP_Query being filtered. + * + * @return void + */ + public function pre_get_posts( WP_Query $query ) { + if ( ! $this->should_filter_query( $query ) ) { + return; + } + + $current_page = 1; + if ( get_query_var( 'paged' ) ) { + $current_page = get_query_var( 'paged' ); + } elseif ( get_query_var( 'page' ) ) { + $current_page = get_query_var( 'page' ); + } + + /** + * Filters the array of parameters used in the Algolia Index search. + * + * @author WebDevStudios + * @since 1.0.0 + * @since 1.2.0 Introduced 'highlightPreTag' and 'highlightPostTag` parameters. + * + * @param array $params { + * Search parameters for the Algolia Index search. + * + * @type string $attributesToRetrieve Which attributes to retrieve. + * @type int $hitsPerPage Pagination parameter. The number of hits per page to retrieve. + * @type int $page Pagination parameter. The page of results to retrieve. + * @type string $highlightPreTag HTML string to insert before highlights in result snippets. + * @type string $highlightPostTag HTML string to insert after highlights in result snippets. + * } + */ + $params = apply_filters( + 'algolia_search_params', + array( + 'attributesToRetrieve' => 'post_id', + 'hitsPerPage' => (int) get_option( 'posts_per_page' ), + 'page' => $current_page - 1, // Algolia pages are zero indexed. + 'highlightPreTag' => '', + 'highlightPostTag' => '', + ) + ); + + $order_by = apply_filters( 'algolia_search_order_by', null ); + $order = apply_filters( 'algolia_search_order', 'desc' ); + + try { + $results = $this->index->search( $query->query['s'], $params, $order_by, $order ); + } catch ( AlgoliaException $exception ) { + error_log( $exception->getMessage() ); // phpcs:ignore -- Legacy. + + return; + } + + add_filter( 'found_posts', array( $this, 'found_posts' ), 10, 2 ); + add_filter( 'posts_search', array( $this, 'posts_search' ), 10, 2 ); + + // Store the current page hits, so that we can use them for highlighting later on. + foreach ( $results['hits'] as $hit ) { + $this->current_page_hits[ $hit['post_id'] ] = $hit; + } + + // Store the total number of its, so that we can hook into the `found_posts`. + // This is useful for pagination. + $this->total_hits = $results['nbHits']; + + $post_ids = array(); + foreach ( $results['hits'] as $result ) { + $post_ids[] = $result['post_id']; + } + + // Make sure there are not results by tricking WordPress in trying to find + // a non existing post ID. + // Otherwise, the query returns all the results. + if ( empty( $post_ids ) ) { + $post_ids = array( 0 ); + } + + $query->set( 'posts_per_page', $params['hitsPerPage'] ); + $query->set( 'offset', 0 ); + + $post_types = 'any'; + + $maybe_post_type = filter_input( INPUT_GET, 'post_type', FILTER_SANITIZE_STRING ); + + if ( ! empty( $maybe_post_type ) ) { + $post_type = get_post_type_object( $maybe_post_type ); + if ( null !== $post_type ) { + $post_types = $post_type->name; + } + } + + $query->set( 'post_type', $post_types ); + $query->set( 'post__in', $post_ids ); + $query->set( 'orderby', 'post__in' ); + + // @todo: This actually still excludes trash and auto-drafts. + $query->set( 'post_status', 'any' ); + } + + /** + * This hook returns the actual real number of results available in Algolia. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param int $found_posts The number of posts found. + * @param WP_Query $query The WP_Query instance (passed by reference). + * + * @return int + */ + public function found_posts( $found_posts, WP_Query $query ) { + return $this->should_filter_query( $query ) ? $this->total_hits : $found_posts; + } + + /** + * Filter the search SQL that is used in the WHERE clause of WP_Query. + * Removes the where Like part of the queries as we consider Algolia as being the source of truth. + * We don't want to filter by anything but the actual list of post_ids resulting + * from the Algolia search. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $search Search SQL for WHERE clause. + * @param WP_Query $query The current WP_Query object. + * + * @return string + */ + public function posts_search( $search, WP_Query $query ) { + return $this->should_filter_query( $query ) ? '' : $search; + } + + /** + * Output the bundled styles for highlighting search result matches, if enabled. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return void + */ + public function output_highlighting_bundled_styles() { + if ( ! $this->highlighting_enabled() ) { + return; + } + + if ( ! apply_filters( 'algolia_search_highlighting_enable_bundled_styles', true ) ) { + return; + } + + ?> + + + * @since 1.0.0 + * + * @param WP_Query $query The WP_Query. + * + * @return void + */ + public function begin_highlighting( $query ) { + if ( ! $this->should_filter_query( $query ) ) { + return; + } + + if ( ! $this->highlighting_enabled() ) { + return; + } + + add_filter( 'the_title', [ $this, 'highlight_the_title' ], 10, 2 ); + add_filter( 'get_the_excerpt', [ $this, 'highlight_get_the_excerpt' ], 10, 2 ); + + add_action( 'loop_end', [ $this, 'end_highlighting' ] ); + } + + /** + * Stop highlighting search result matches. + * + * This method is called on the loop_end action, where we want to stop highlighting search result matches. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param WP_Query $query The WP_Query. + */ + public function end_highlighting( $query ) { + remove_filter( 'the_title', [ $this, 'highlight_the_title' ], 10 ); + remove_filter( 'get_the_excerpt', [ $this, 'highlight_get_the_excerpt' ], 10 ); + + remove_action( 'loop_end', [ $this, 'end_highlighting' ] ); + } + + /** + * Filter the_title, replacing it with the highlighted title from the Algolia index. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $title The title string. + * @param int $post_id The post ID. + * + * @return string + */ + public function highlight_the_title( $title, $post_id ) { + $highlighted_title = $this->current_page_hits[ $post_id ]['_highlightResult']['post_title']['value'] ?? null; + + if ( ! empty( $highlighted_title ) ) { + $title = $highlighted_title; + } + + return $title; + } + + /** + * Filter get_the_excerpt, replacing it with the highlighted excerpt from the Algolia index. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $excerpt The excerpt string. + * @param WP_Post $post The post object. + * + * @return string + */ + public function highlight_get_the_excerpt( $excerpt, $post ) { + $highlighted_excerpt = $this->current_page_hits[ $post->ID ]['_snippetResult']['content']['value'] ?? null; + + if ( ! empty( $highlighted_excerpt ) ) { + $excerpt = $highlighted_excerpt; + } + + return $excerpt; + } + + /** + * Determine whether highlighting is enabled. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + private function highlighting_enabled() : bool { + return apply_filters( 'algolia_search_highlighting_enabled', true ); + } +} diff --git a/includes/class-typesense-settings.php b/includes/class-typesense-settings.php new file mode 100644 index 0000000..a7d158b --- /dev/null +++ b/includes/class-typesense-settings.php @@ -0,0 +1,483 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Typesense_Settings + * + * @since 1.0.0 + */ +class Typesense_Settings { + + /** + * Typesense_Settings constructor. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function __construct() { + add_option( 'typesense_application_id', '' ); + add_option( 'typesense_search_api_key', '' ); + add_option( 'typesense_api_key', '' ); + add_option( 'typesense_synced_indices_ids', array() ); + add_option( 'typesense_autocomplete_enabled', 'no' ); + add_option( 'typesense_autocomplete_config', array() ); + add_option( 'typesense_override_native_search', 'native' ); + add_option( 'typesense_index_name_prefix', 'wp_' ); + add_option( 'typesense_api_is_reachable', 'no' ); + add_option( 'typesense_powered_by_enabled', 'yes' ); + } + + /** + * Get the Typesense Application ID. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string + */ + public function get_application_id() { + if ( ! $this->is_application_id_in_config() ) { + + return (string) get_option( 'typesense_application_id', '' ); + } + + $this->assert_constant_is_non_empty_string( TYPESENSE_APPLICATION_ID, 'TYPESENSE_APPLICATION_ID' ); + + return TYPESENSE_APPLICATION_ID; + } + + /** + * Get the Typesense Search-Only API Key. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string + */ + public function get_search_api_key() { + if ( ! $this->is_search_api_key_in_config() ) { + + return (string) get_option( 'typesense_search_api_key', '' ); + } + + $this->assert_constant_is_non_empty_string( TYPESENSE_SEARCH_API_KEY, 'TYPESENSE_SEARCH_API_KEY' ); + + return TYPESENSE_SEARCH_API_KEY; + } + + /** + * Get the Typesense Admin API Key + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string + */ + public function get_api_key() { + if ( ! $this->is_api_key_in_config() ) { + + return (string) get_option( 'typesense_api_key', '' ); + } + + $this->assert_constant_is_non_empty_string( TYPESENSE_API_KEY, 'TYPESENSE_API_KEY' ); + + return TYPESENSE_API_KEY; + } + + /** + * Get the excluded post types. + * + * @author WebDevStudios + * @since 1.0.0 + * @deprecated 1.7.0 Use Typesense_Settings::get_excluded_post_types() + * @see Typesense_Settings::get_excluded_post_types() + * + * @return array + */ + public function get_post_types_blacklist() { + _deprecated_function( + __METHOD__, + '1.7.0', + 'Typesense_Settings::get_excluded_post_types();' + ); + + return $this->get_excluded_post_types(); + } + + /** + * Get the excluded post types. + * + * @author WebDevStudios + * @since 1.7.0 + * + * @return array + */ + public function get_excluded_post_types() { + + // Default array of excluded post types. + $excluded = [ 'nav_menu_item' ]; + + /** + * Filters excluded post types. + * + * @since 1.0.0 + * @deprecated 1.7.0 Use {@see 'typesense_excluded_post_types'} instead. + * + * @param array $excluded The excluded post types. + */ + $excluded = (array) apply_filters_deprecated( + 'typesense_post_types_blacklist', + [ $excluded ], + '1.7.0', + 'typesense_excluded_post_types' + ); + + /** + * Filters excluded post types. + * + * @since 1.7.0 + * + * @param array $excluded The excluded post types. + */ + $excluded = (array) apply_filters( 'typesense_excluded_post_types', $excluded ); + + // Native WordPress. + $excluded[] = 'revision'; + + // Native to Typesense Search plugin. + $excluded[] = 'typesense_task'; + $excluded[] = 'typesense_log'; + + // Native to WordPress VIP platform. + $excluded[] = 'kr_request_token'; + $excluded[] = 'kr_access_token'; + $excluded[] = 'deprecated_log'; + $excluded[] = 'async-scan-result'; + $excluded[] = 'scanresult'; + + return array_unique( $excluded ); + } + + /** + * Get synced indices IDs. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return array + */ + public function get_synced_indices_ids() { + $ids = array(); + + // Gather indices used in autocomplete experience. + $config = $this->get_autocomplete_config(); + foreach ( $config as $index ) { + if ( isset( $index['index_id'] ) ) { + $ids[] = $index['index_id']; + } + } + + // Push index used in instantsearch experience. + // Todo: we should allow users to index without using the shipped search UI or backend implementation. + if ( $this->should_override_search_in_backend() || $this->should_override_search_with_instantsearch() ) { + $ids[] = $this->get_native_search_index_id(); + } + + return (array) apply_filters( 'typesense_get_synced_indices_ids', $ids ); + } + + /** + * Get excluded taxonomies. + * + * @author WebDevStudios + * @since 1.0.0 + * @deprecated 1.7.0 Use Typesense_Settings::get_excluded_taxonomies() + * @see Typesense_Settings::get_excluded_taxonomies() + * + * @return array + */ + public function get_taxonomies_blacklist() { + _deprecated_function( + __METHOD__, + '1.7.0', + 'Typesense_Settings::get_excluded_taxonomies();' + ); + + return $this->get_excluded_taxonomies(); + } + + /** + * Get excluded taxonomies. + * + * @author WebDevStudios + * @since 1.7.0 + * + * @return array + */ + public function get_excluded_taxonomies() { + + // Default array of excluded taxonomies. + $excluded = [ + 'nav_menu', + 'link_category', + 'post_format', + ]; + + /** + * Filters excluded taxonomies. + * + * @since 1.0.0 + * @deprecated 1.7.0 Use {@see 'typesense_excluded_taxonomies'} instead. + * + * @param array $excluded The excluded taxonomies. + */ + $excluded = (array) apply_filters_deprecated( + 'typesense_taxonomies_blacklist', + [ $excluded ], + '1.7.0', + 'typesense_excluded_taxonomies' + ); + + $excluded = (array) apply_filters( 'typesense_excluded_taxonomies', $excluded ); + + return $excluded; + } + + /** + * Get the autocomplete enabled option setting. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string Can be 'yes' or 'no'. + */ + public function get_autocomplete_enabled() { + return get_option( 'typesense_autocomplete_enabled', 'no' ); + } + + /** + * Get the autocomplete config. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return array + */ + public function get_autocomplete_config() { + return (array) get_option( 'typesense_autocomplete_config', array() ); + } + + /** + * Get the override native search option setting. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string Can be 'native' 'backend' or 'instantsearch'. + */ + public function get_override_native_search() { + $search_type = get_option( 'typesense_override_native_search', 'native' ); + + // BC compatibility. + if ( 'yes' === $search_type ) { + $search_type = 'backend'; + } + + return $search_type; + } + + /** + * Determines whether we should override search in backend. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + public function should_override_search_in_backend() { + return $this->get_override_native_search() === 'backend' || $this->should_override_search_with_instantsearch(); + } + + /** + * Determines whether we should override search with instantsearch.js. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + public function should_override_search_with_instantsearch() { + $value = $this->get_override_native_search() === 'instantsearch'; + + return (bool) apply_filters( 'typesense_should_override_search_with_instantsearch', $value ); + } + + /** + * Get native search index ID. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string + */ + public function get_native_search_index_id() { + return (string) apply_filters( 'typesense_native_search_index_id', 'searchable_posts' ); + } + + /** + * Get the index name prefix. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string + */ + public function get_index_name_prefix() { + if ( ! $this->is_index_name_prefix_in_config() ) { + + return (string) get_option( 'typesense_index_name_prefix', 'wp_' ); + } + + $this->assert_constant_is_non_empty_string( TYPESENSE_INDEX_NAME_PREFIX, 'TYPESENSE_INDEX_NAME_PREFIX' ); + + return TYPESENSE_INDEX_NAME_PREFIX; + } + + /** + * Makes sure that constants are non empty strings. + * + * This makes sure that we fail early if the environment configuration is wrong. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param mixed $value The constant value to check. + * @param string $constant_name The constant name to check. + * + * @throws RuntimeException If the constant is not a string or is empty. + */ + protected function assert_constant_is_non_empty_string( $value, $constant_name ) { + if ( ! is_string( $value ) ) { + throw new RuntimeException( sprintf( 'Constant %s in wp-config.php should be a string, %s given.', $constant_name, gettype( $value ) ) ); + } + + if ( 0 === mb_strlen( $value ) ) { + throw new RuntimeException( sprintf( 'Constant %s in wp-config.php cannot be empty.', $constant_name ) ); + } + } + + /** + * Determines if the TYPESENSE_APPLICATION_ID is defined. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + public function is_application_id_in_config() { + return defined( 'TYPESENSE_APPLICATION_ID' ); + } + + /** + * Determines if the TYPESENSE_SEARCH_API_KEY is defined. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + public function is_search_api_key_in_config() { + return defined( 'TYPESENSE_SEARCH_API_KEY' ); + } + + /** + * Determines if the TYPESENSE_API_KEY is defined. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + public function is_api_key_in_config() { + return defined( 'TYPESENSE_API_KEY' ); + } + + /** + * Determines if the TYPESENSE_INDEX_NAME_PREFIX is defined. + * + * @author WebDevStudios + * @since 1.0.0 + * @return bool + */ + public function is_index_name_prefix_in_config() { + return defined( 'TYPESENSE_INDEX_NAME_PREFIX' ); + } + + /** + * Get the API is reachable option setting. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + public function get_api_is_reachable() { + $enabled = get_option( 'typesense_api_is_reachable', 'no' ); + + return 'yes' === $enabled; + } + + /** + * Set the API is reachable option setting. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param bool $flag If the API is reachable or not, 'yes' or 'no'. + */ + public function set_api_is_reachable( $flag ) { + $value = (bool) true === $flag ? 'yes' : 'no'; + update_option( 'typesense_api_is_reachable', $value ); + } + + /** + * Determine if powered by is enabled. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + public function is_powered_by_enabled() { + $enabled = get_option( 'typesense_powered_by_enabled', 'yes' ); + + return 'yes' === $enabled; + } + + /** + * Enable the powered by option setting. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function enable_powered_by() { + update_option( 'typesense_powered_by_enabled', 'yes' ); + } + + /** + * Disable the powered by option setting. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function disable_powered_by() { + update_option( 'typesense_powered_by_enabled', 'no' ); + } +} diff --git a/includes/class-typesense-styles.php b/includes/class-typesense-styles.php new file mode 100644 index 0000000..a91876f --- /dev/null +++ b/includes/class-typesense-styles.php @@ -0,0 +1,50 @@ + + * @since 1.5.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Algolia_Styles + * + * @since 1.5.0 + */ +class Algolia_Styles { + + /** + * Algolia_Styles constructor. + * + * @author WebDevStudios + * @since 1.5.0 + */ + public function __construct() { + add_action( 'wp_enqueue_scripts', [ $this, 'register_styles' ] ); + } + + /** + * Register styles. + * + * @author WebDevStudios + * @since 1.5.0 + */ + public function register_styles() { + + wp_register_style( + 'algolia-autocomplete', + ALGOLIA_PLUGIN_URL . 'css/algolia-autocomplete.css', + [], + ALGOLIA_VERSION + ); + + wp_register_style( + 'algolia-instantsearch', + ALGOLIA_PLUGIN_URL . 'css/algolia-instantsearch.css', + [], + ALGOLIA_VERSION + ); + } +} diff --git a/includes/class-typesense-template-loader.php b/includes/class-typesense-template-loader.php new file mode 100644 index 0000000..dba2a54 --- /dev/null +++ b/includes/class-typesense-template-loader.php @@ -0,0 +1,242 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Algolia_Template_Loader + * + * @since 1.0.0 + */ +class Algolia_Template_Loader { + + /** + * The Algolia Plugin. + * + * @since 1.0.0 + * + * @var Algolia_Plugin + */ + private $plugin; + + /** + * Algolia_Template_Loader constructor. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param Algolia_Plugin $plugin The Algolia Plugin. + */ + public function __construct( Algolia_Plugin $plugin ) { + $this->plugin = $plugin; + + $in_footer = Algolia_Utils::get_scripts_in_footer_argument(); + + // Inject Algolia configuration in a JavaScript variable. + if ( true === $in_footer ) { + add_filter( + 'wp_footer', + [ $this, 'load_algolia_config' ] + ); + } else { + add_filter( + 'wp_head', + [ $this, 'load_algolia_config' ] + ); + } + + // Listen for native templates to override. + add_filter( 'template_include', array( $this, 'template_loader' ) ); + + // Load autocomplete.js search experience if its enabled. + if ( $this->should_load_autocomplete() ) { + add_filter( 'wp_enqueue_scripts', array( $this, 'enqueue_autocomplete_scripts' ) ); + + if ( true === $in_footer ) { + add_filter( 'wp_footer', array( $this, 'load_autocomplete_template' ) ); + } else { + add_filter( 'wp_head', array( $this, 'load_autocomplete_template' ) ); + } + } + } + + /** + * Load config. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function load_algolia_config() { + $settings = $this->plugin->get_settings(); + $autocomplete_config = $this->plugin->get_autocomplete_config(); + + $config = array( + 'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'application_id' => $settings->get_application_id(), + 'search_api_key' => $settings->get_search_api_key(), + 'powered_by_enabled' => $settings->is_powered_by_enabled(), + 'query' => get_search_query(), + 'autocomplete' => array( + 'sources' => $autocomplete_config->get_config(), + 'input_selector' => (string) apply_filters( 'algolia_autocomplete_input_selector', "input[name='s']:not('.no-autocomplete')" ), + ), + 'indices' => array(), + ); + + // Inject all the indices into the config to ease instantsearch.js integrations. + $indices = $this->plugin->get_indices( + array( + 'enabled' => true, + ) + ); + foreach ( $indices as $index ) { + $config['indices'][ $index->get_id() ] = $index->to_array(); + } + + // Give developers a last chance to alter the configuration. + $config = (array) apply_filters( 'algolia_config', $config ); + + echo ''; + } + + /** + * Determines whether we should load autocomplete. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return bool + */ + private function should_load_autocomplete() { + $settings = $this->plugin->get_settings(); + $autocomplete = $this->plugin->get_autocomplete_config(); + + if ( null === $autocomplete ) { + // The user has not provided his credentials yet. + return false; + } + + $config = $autocomplete->get_config(); + if ( 'yes' !== $settings->get_autocomplete_enabled() ) { + return false; + } + + return ! empty( $config ); + } + + /** + * Enqueue Algolia autocomplete.js scripts. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function enqueue_autocomplete_scripts() { + + // Enqueue the autocomplete.js default styles. + wp_enqueue_style( 'algolia-autocomplete' ); + + // Javascript. + wp_enqueue_script( 'algolia-search' ); + + // Enqueue the autocomplete.js library. + wp_enqueue_script( 'algolia-autocomplete' ); + wp_enqueue_script( 'algolia-autocomplete-noconflict' ); + + // Allow users to easily enqueue custom styles and scripts. + do_action( 'algolia_autocomplete_scripts' ); + } + + /** + * Load a template. + * + * Handles template usage so that we can use our own templates instead of the themes. + * + * Templates are in the 'templates' folder. algolia looks for theme. + * overrides in /your-theme/algolia/ by default. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param mixed $template The template to load. + * + * @return string + */ + public function template_loader( $template ) { + $settings = $this->plugin->get_settings(); + if ( is_search() && $settings->should_override_search_with_instantsearch() ) { + + return $this->load_instantsearch_template(); + } + + return $template; + } + + /** + * Load the instantsearch template. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @return string + */ + public function load_instantsearch_template() { + add_action( + 'wp_enqueue_scripts', function () { + // Enqueue the instantsearch.js default styles. + wp_enqueue_style( 'algolia-instantsearch' ); + + // Enqueue the instantsearch.js library. + wp_enqueue_script( 'algolia-instantsearch' ); + + // Allow users to easily enqueue custom styles and scripts. + do_action( 'algolia_instantsearch_scripts' ); + } + ); + + return $this->locate_template( 'instantsearch.php' ); + } + + /** + * Load the autocomplete template. + * + * @author WebDevStudios + * @since 1.0.0 + */ + public function load_autocomplete_template() { + require $this->locate_template( 'autocomplete.php' ); + } + + /** + * Locate a template. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $file The template file. + * + * @return string + */ + private function locate_template( $file ) { + $locations[] = $file; + + $templates_path = $this->plugin->get_templates_path(); + if ( 'algolia/' !== $templates_path ) { + $locations[] = 'algolia/' . $file; + } + + $locations[] = $templates_path . $file; + + $locations = (array) apply_filters( 'algolia_template_locations', $locations, $file ); + + $template = locate_template( array_unique( $locations ) ); + + $default_template = (string) apply_filters( 'algolia_default_template', $this->plugin->get_path() . '/templates/' . $file, $file ); + + return $template ? $template : $default_template; + } +} diff --git a/includes/class-typesense-utils.php b/includes/class-typesense-utils.php new file mode 100644 index 0000000..6628c9e --- /dev/null +++ b/includes/class-typesense-utils.php @@ -0,0 +1,269 @@ + + * @since 1.0.0 + * + * @package WebDevStudios\WPSWA + */ + +/** + * Class Typesense_Utils + * + * @since 1.0.0 + */ +class Typesense_Utils { + + /** + * Retrieve term parents with separator. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param int $id Term ID. + * @param string $taxonomy The taxonomy. + * @param string $separator Optional, default is '/'. How to separate terms. + * @param bool $nicename Optional, default is false. Whether to use nice name for display. + * @param array $visited Optional. Already linked to terms to prevent duplicates. + * + * @return string|WP_Error A list of terms parents on success, WP_Error on failure. + */ + public static function get_term_parents( $id, $taxonomy, $separator = '/', $nicename = false, $visited = array() ) { + $chain = ''; + $parent = get_term( $id, $taxonomy ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + if ( $nicename ) { + $name = $parent->slug; + } else { + $name = $parent->name; + } + + if ( $parent->parent && ( $parent->parent !== $parent->term_id ) && ! in_array( $parent->parent, $visited, true ) ) { + $visited[] = $parent->parent; + $chain .= self::get_term_parents( $parent->parent, $taxonomy, $separator, $nicename, $visited ); + } + + $chain .= $name . $separator; + + return $chain; + } + + /** + * Get taxonomy tree. + * + * This is useful when building hierarchical menus. + * + * Returns an array like: + * array( + * 'lvl0' => ['Sales', 'Marketing'], + * 'lvl1' => ['Sales > Strategies', 'Marketing > Tips & Tricks'] + * ... + * );. + * + * @link https://community.algolia.com/instantsearch.js/documentation/#hierarchicalmenu + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param array $terms The terms. + * @param string $taxonomy The taxonomy. + * @param string $separator The separator. + * + * @return array + */ + public static function get_taxonomy_tree( array $terms, $taxonomy, $separator = ' > ' ) { + $term_ids = wp_list_pluck( $terms, 'term_id' ); + + $parents = array(); + foreach ( $term_ids as $term_id ) { + $path = self::get_term_parents( $term_id, $taxonomy, $separator ); + $parents[] = rtrim( $path, $separator ); + } + + $terms = array(); + foreach ( $parents as $parent ) { + $levels = explode( $separator, $parent ); + + $previous_lvl = ''; + foreach ( $levels as $index => $level ) { + $terms[ 'lvl' . $index ][] = $previous_lvl . $level; + $previous_lvl .= $level . $separator; + + // Make sure we have not duplicate. + // The call to `array_values` ensures that we do not end up with an object in JSON. + $terms[ 'lvl' . $index ] = array_values( array_unique( $terms[ 'lvl' . $index ] ) ); + } + } + + return $terms; + } + + /** + * Get post images. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param int $post_id The post ID. + * + * @return array + */ + public static function get_post_images( $post_id ) { + $images = array(); + + if ( get_post_type( $post_id ) === 'attachment' ) { + $post_thumbnail_id = (int) $post_id; + } else { + $post_thumbnail_id = get_post_thumbnail_id( (int) $post_id ); + } + + if ( $post_thumbnail_id ) { + $sizes = (array) apply_filters( 'algolia_post_images_sizes', array( 'thumbnail' ) ); + foreach ( $sizes as $size ) { + $info = wp_get_attachment_image_src( $post_thumbnail_id, $size ); + if ( ! $info ) { + continue; + } + + $images[ $size ] = array( + 'url' => $info[0], + 'width' => $info[1], + 'height' => $info[2], + ); + } + } + + return (array) apply_filters( 'algolia_get_post_images', $images ); + } + + /** + * Prepare content. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $content The content to prepare. + * + * @return string + */ + public static function prepare_content( $content ) { + $content = self::remove_content_noise( $content ); + + return wp_strip_all_tags( $content ); + } + + /** + * Remove noise from content. + * + * @author WebDevStudios + * @since 1.0.0 + * + * @param string $content The content to remove noise from. + * + * @return string + */ + public static function remove_content_noise( $content ) { + $noise_patterns = array( + // strip out comments. + "''is", + // strip out cdata. + "''is", + // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037 + // Script tags removal now preceeds style tag removal. + // strip out '); + } elseif ($format === 'js') { + static::writeOutput(static::generateScript()); + } + static::resetStatic(); + } + } + + public function close(): void + { + self::resetStatic(); + } + + public function reset() + { + parent::reset(); + + self::resetStatic(); + } + + /** + * Forget all logged records + */ + public static function resetStatic(): void + { + static::$records = []; + } + + /** + * Wrapper for register_shutdown_function to allow overriding + */ + protected function registerShutdownFunction(): void + { + if (PHP_SAPI !== 'cli') { + register_shutdown_function(['Monolog\Handler\BrowserConsoleHandler', 'send']); + } + } + + /** + * Wrapper for echo to allow overriding + */ + protected static function writeOutput(string $str): void + { + echo $str; + } + + /** + * Checks the format of the response + * + * If Content-Type is set to application/javascript or text/javascript -> js + * If Content-Type is set to text/html, or is unset -> html + * If Content-Type is anything else -> unknown + * + * @return string One of 'js', 'html' or 'unknown' + */ + protected static function getResponseFormat(): string + { + // Check content type + foreach (headers_list() as $header) { + if (stripos($header, 'content-type:') === 0) { + // This handler only works with HTML and javascript outputs + // text/javascript is obsolete in favour of application/javascript, but still used + if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) { + return 'js'; + } + if (stripos($header, 'text/html') === false) { + return 'unknown'; + } + break; + } + } + + return 'html'; + } + + private static function generateScript(): string + { + $script = []; + foreach (static::$records as $record) { + $context = static::dump('Context', $record['context']); + $extra = static::dump('Extra', $record['extra']); + + if (empty($context) && empty($extra)) { + $script[] = static::call_array('log', static::handleStyles($record['formatted'])); + } else { + $script = array_merge( + $script, + [static::call_array('groupCollapsed', static::handleStyles($record['formatted']))], + $context, + $extra, + [static::call('groupEnd')] + ); + } + } + + return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);"; + } + + private static function handleStyles(string $formatted): array + { + $args = []; + $format = '%c' . $formatted; + preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); + + foreach (array_reverse($matches) as $match) { + $args[] = '"font-weight: normal"'; + $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0])); + + $pos = $match[0][1]; + $format = Utils::substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . Utils::substr($format, $pos + strlen($match[0][0])); + } + + $args[] = static::quote('font-weight: normal'); + $args[] = static::quote($format); + + return array_reverse($args); + } + + private static function handleCustomStyles(string $style, string $string): string + { + static $colors = ['blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey']; + static $labels = []; + + return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function (array $m) use ($string, &$colors, &$labels) { + if (trim($m[1]) === 'autolabel') { + // Format the string as a label with consistent auto assigned background color + if (!isset($labels[$string])) { + $labels[$string] = $colors[count($labels) % count($colors)]; + } + $color = $labels[$string]; + + return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px"; + } + + return $m[1]; + }, $style); + } + + private static function dump(string $title, array $dict): array + { + $script = []; + $dict = array_filter($dict); + if (empty($dict)) { + return $script; + } + $script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title)); + foreach ($dict as $key => $value) { + $value = json_encode($value); + if (empty($value)) { + $value = static::quote(''); + } + $script[] = static::call('log', static::quote('%s: %o'), static::quote((string) $key), $value); + } + + return $script; + } + + private static function quote(string $arg): string + { + return '"' . addcslashes($arg, "\"\n\\") . '"'; + } + + private static function call(...$args): string + { + $method = array_shift($args); + + return static::call_array($method, $args); + } + + private static function call_array(string $method, array $args): string + { + return 'c.' . $method . '(' . implode(', ', $args) . ');'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php new file mode 100644 index 0000000..fbf5eff --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\ResettableInterface; +use Monolog\Formatter\FormatterInterface; + +/** + * Buffers all records until closing the handler and then pass them as batch. + * + * This is useful for a MailHandler to send only one mail per request instead of + * sending one per log message. + * + * @author Christophe Coevoet + */ +class BufferHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + + /** @var HandlerInterface */ + protected $handler; + protected $bufferSize = 0; + protected $bufferLimit; + protected $flushOnOverflow; + protected $buffer = []; + protected $initialized = false; + + /** + * @param HandlerInterface $handler Handler. + * @param int $bufferLimit How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded + */ + public function __construct(HandlerInterface $handler, int $bufferLimit = 0, $level = Logger::DEBUG, bool $bubble = true, bool $flushOnOverflow = false) + { + parent::__construct($level, $bubble); + $this->handler = $handler; + $this->bufferLimit = $bufferLimit; + $this->flushOnOverflow = $flushOnOverflow; + } + + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + if ($record['level'] < $this->level) { + return false; + } + + if (!$this->initialized) { + // __destructor() doesn't get called on Fatal errors + register_shutdown_function([$this, 'close']); + $this->initialized = true; + } + + if ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) { + if ($this->flushOnOverflow) { + $this->flush(); + } else { + array_shift($this->buffer); + $this->bufferSize--; + } + } + + if ($this->processors) { + $record = $this->processRecord($record); + } + + $this->buffer[] = $record; + $this->bufferSize++; + + return false === $this->bubble; + } + + public function flush(): void + { + if ($this->bufferSize === 0) { + return; + } + + $this->handler->handleBatch($this->buffer); + $this->clear(); + } + + public function __destruct() + { + // suppress the parent behavior since we already have register_shutdown_function() + // to call close(), and the reference contained there will prevent this from being + // GC'd until the end of the request + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + $this->flush(); + + $this->handler->close(); + } + + /** + * Clears the buffer without flushing any messages down to the wrapped handler. + */ + public function clear(): void + { + $this->bufferSize = 0; + $this->buffer = []; + } + + public function reset() + { + $this->flush(); + + parent::reset(); + + $this->resetProcessors(); + + if ($this->handler instanceof ResettableInterface) { + $this->handler->reset(); + } + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + $this->handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); + } + + /** + * {@inheritdoc} + */ + public function getFormatter(): FormatterInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + return $this->handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php new file mode 100644 index 0000000..44dbf0d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\ChromePHPFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; + +/** + * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/) + * + * This also works out of the box with Firefox 43+ + * + * @author Christophe Coevoet + */ +class ChromePHPHandler extends AbstractProcessingHandler +{ + use WebRequestRecognizerTrait; + + /** + * Version of the extension + */ + protected const VERSION = '4.0'; + + /** + * Header name + */ + protected const HEADER_NAME = 'X-ChromeLogger-Data'; + + /** + * Regular expression to detect supported browsers (matches any Chrome, or Firefox 43+) + */ + protected const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|HeadlessChrome|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}'; + + protected static $initialized = false; + + /** + * Tracks whether we sent too much data + * + * Chrome limits the headers to 4KB, so when we sent 3KB we stop sending + * + * @var bool + */ + protected static $overflowed = false; + + protected static $json = [ + 'version' => self::VERSION, + 'columns' => ['label', 'log', 'backtrace', 'type'], + 'rows' => [], + ]; + + protected static $sendHeaders = true; + + /** + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct($level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + if (!function_exists('json_encode')) { + throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s ChromePHPHandler'); + } + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + if (!$this->isWebRequest()) { + return; + } + + $messages = []; + + foreach ($records as $record) { + if ($record['level'] < $this->level) { + continue; + } + $messages[] = $this->processRecord($record); + } + + if (!empty($messages)) { + $messages = $this->getFormatter()->formatBatch($messages); + self::$json['rows'] = array_merge(self::$json['rows'], $messages); + $this->send(); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ChromePHPFormatter(); + } + + /** + * Creates & sends header for a record + * + * @see sendHeader() + * @see send() + */ + protected function write(array $record): void + { + if (!$this->isWebRequest()) { + return; + } + + self::$json['rows'][] = $record['formatted']; + + $this->send(); + } + + /** + * Sends the log header + * + * @see sendHeader() + */ + protected function send(): void + { + if (self::$overflowed || !self::$sendHeaders) { + return; + } + + if (!self::$initialized) { + self::$initialized = true; + + self::$sendHeaders = $this->headersAccepted(); + if (!self::$sendHeaders) { + return; + } + + self::$json['request_uri'] = $_SERVER['REQUEST_URI'] ?? ''; + } + + $json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true); + $data = base64_encode(utf8_encode($json)); + if (strlen($data) > 3 * 1024) { + self::$overflowed = true; + + $record = [ + 'message' => 'Incomplete logs, chrome header size limit reached', + 'context' => [], + 'level' => Logger::WARNING, + 'level_name' => Logger::getLevelName(Logger::WARNING), + 'channel' => 'monolog', + 'datetime' => new \DateTimeImmutable(), + 'extra' => [], + ]; + self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record); + $json = Utils::jsonEncode(self::$json, null, true); + $data = base64_encode(utf8_encode($json)); + } + + if (trim($data) !== '') { + $this->sendHeader(static::HEADER_NAME, $data); + } + } + + /** + * Send header string to the client + */ + protected function sendHeader(string $header, string $content): void + { + if (!headers_sent() && self::$sendHeaders) { + header(sprintf('%s: %s', $header, $content)); + } + } + + /** + * Verifies if the headers are accepted by the current user agent + */ + protected function headersAccepted(): bool + { + if (empty($_SERVER['HTTP_USER_AGENT'])) { + return false; + } + + return preg_match(static::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']) === 1; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php new file mode 100644 index 0000000..b2d1e18 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\JsonFormatter; +use Monolog\Logger; + +/** + * CouchDB handler + * + * @author Markus Bachmann + */ +class CouchDBHandler extends AbstractProcessingHandler +{ + private $options; + + public function __construct(array $options = [], $level = Logger::DEBUG, bool $bubble = true) + { + $this->options = array_merge([ + 'host' => 'localhost', + 'port' => 5984, + 'dbname' => 'logger', + 'username' => null, + 'password' => null, + ], $options); + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $basicAuth = null; + if ($this->options['username']) { + $basicAuth = sprintf('%s:%s@', $this->options['username'], $this->options['password']); + } + + $url = 'http://'.$basicAuth.$this->options['host'].':'.$this->options['port'].'/'.$this->options['dbname']; + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'content' => $record['formatted'], + 'ignore_errors' => true, + 'max_redirects' => 0, + 'header' => 'Content-type: application/json', + ], + ]); + + if (false === @file_get_contents($url, false, $context)) { + throw new \RuntimeException(sprintf('Could not connect to %s', $url)); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php new file mode 100644 index 0000000..00d38e9 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; + +/** + * Logs to Cube. + * + * @link http://square.github.com/cube/ + * @author Wan Chen + */ +class CubeHandler extends AbstractProcessingHandler +{ + private $udpConnection; + private $httpConnection; + private $scheme; + private $host; + private $port; + private $acceptedSchemes = ['http', 'udp']; + + /** + * Create a Cube handler + * + * @throws \UnexpectedValueException when given url is not a valid url. + * A valid url must consist of three parts : protocol://host:port + * Only valid protocols used by Cube are http and udp + */ + public function __construct(string $url, $level = Logger::DEBUG, bool $bubble = true) + { + $urlInfo = parse_url($url); + + if (!isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) { + throw new \UnexpectedValueException('URL "'.$url.'" is not valid'); + } + + if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) { + throw new \UnexpectedValueException( + 'Invalid protocol (' . $urlInfo['scheme'] . ').' + . ' Valid options are ' . implode(', ', $this->acceptedSchemes) + ); + } + + $this->scheme = $urlInfo['scheme']; + $this->host = $urlInfo['host']; + $this->port = $urlInfo['port']; + + parent::__construct($level, $bubble); + } + + /** + * Establish a connection to an UDP socket + * + * @throws \LogicException when unable to connect to the socket + * @throws MissingExtensionException when there is no socket extension + */ + protected function connectUdp(): void + { + if (!extension_loaded('sockets')) { + throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler'); + } + + $this->udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0); + if (!$this->udpConnection) { + throw new \LogicException('Unable to create a socket'); + } + + if (!socket_connect($this->udpConnection, $this->host, $this->port)) { + throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port); + } + } + + /** + * Establish a connection to an http server + * + * @throws \LogicException when unable to connect to the socket + * @throws MissingExtensionException when no curl extension + */ + protected function connectHttp(): void + { + if (!extension_loaded('curl')) { + throw new MissingExtensionException('The curl extension is required to use http URLs with the CubeHandler'); + } + + $this->httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put'); + + if (!$this->httpConnection) { + throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port); + } + + curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $date = $record['datetime']; + + $data = ['time' => $date->format('Y-m-d\TH:i:s.uO')]; + unset($record['datetime']); + + if (isset($record['context']['type'])) { + $data['type'] = $record['context']['type']; + unset($record['context']['type']); + } else { + $data['type'] = $record['channel']; + } + + $data['data'] = $record['context']; + $data['data']['level'] = $record['level']; + + if ($this->scheme === 'http') { + $this->writeHttp(Utils::jsonEncode($data)); + } else { + $this->writeUdp(Utils::jsonEncode($data)); + } + } + + private function writeUdp(string $data): void + { + if (!$this->udpConnection) { + $this->connectUdp(); + } + + socket_send($this->udpConnection, $data, strlen($data), 0); + } + + private function writeHttp(string $data): void + { + if (!$this->httpConnection) { + $this->connectHttp(); + } + + curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']'); + curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen('['.$data.']'), + ]); + + Curl\Util::execute($this->httpConnection, 5, false); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php new file mode 100644 index 0000000..cc0b6c3 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\Curl; + +use CurlHandle; + +/** + * This class is marked as internal and it is not under the BC promise of the package. + * + * @internal + */ +final class Util +{ + private static $retriableErrorCodes = [ + CURLE_COULDNT_RESOLVE_HOST, + CURLE_COULDNT_CONNECT, + CURLE_HTTP_NOT_FOUND, + CURLE_READ_ERROR, + CURLE_OPERATION_TIMEOUTED, + CURLE_HTTP_POST_ERROR, + CURLE_SSL_CONNECT_ERROR, + ]; + + /** + * Executes a CURL request with optional retries and exception on failure + * + * @param resource|CurlHandle $ch curl handler + * @param int $retries + * @param bool $closeAfterDone + * @return bool|string @see curl_exec + */ + public static function execute($ch, int $retries = 5, bool $closeAfterDone = true) + { + while ($retries--) { + $curlResponse = curl_exec($ch); + if ($curlResponse === false) { + $curlErrno = curl_errno($ch); + + if (false === in_array($curlErrno, self::$retriableErrorCodes, true) || !$retries) { + $curlError = curl_error($ch); + + if ($closeAfterDone) { + curl_close($ch); + } + + throw new \RuntimeException(sprintf('Curl error (code %d): %s', $curlErrno, $curlError)); + } + + continue; + } + + if ($closeAfterDone) { + curl_close($ch); + } + + return $curlResponse; + } + + return false; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php new file mode 100644 index 0000000..864c29a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Simple handler wrapper that deduplicates log records across multiple requests + * + * It also includes the BufferHandler functionality and will buffer + * all messages until the end of the request or flush() is called. + * + * This works by storing all log records' messages above $deduplicationLevel + * to the file specified by $deduplicationStore. When further logs come in at the end of the + * request (or when flush() is called), all those above $deduplicationLevel are checked + * against the existing stored logs. If they match and the timestamps in the stored log is + * not older than $time seconds, the new log record is discarded. If no log record is new, the + * whole data set is discarded. + * + * This is mainly useful in combination with Mail handlers or things like Slack or HipChat handlers + * that send messages to people, to avoid spamming with the same message over and over in case of + * a major component failure like a database server being down which makes all requests fail in the + * same way. + * + * @author Jordi Boggiano + */ +class DeduplicationHandler extends BufferHandler +{ + /** + * @var string + */ + protected $deduplicationStore; + + /** + * @var int + */ + protected $deduplicationLevel; + + /** + * @var int + */ + protected $time; + + /** + * @var bool + */ + private $gc = false; + + /** + * @param HandlerInterface $handler Handler. + * @param string $deduplicationStore The file/path where the deduplication log should be kept + * @param string|int $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes + * @param int $time The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(HandlerInterface $handler, ?string $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, int $time = 60, bool $bubble = true) + { + parent::__construct($handler, 0, Logger::DEBUG, $bubble, false); + + $this->deduplicationStore = $deduplicationStore === null ? sys_get_temp_dir() . '/monolog-dedup-' . substr(md5(__FILE__), 0, 20) .'.log' : $deduplicationStore; + $this->deduplicationLevel = Logger::toMonologLevel($deduplicationLevel); + $this->time = $time; + } + + public function flush(): void + { + if ($this->bufferSize === 0) { + return; + } + + $passthru = null; + + foreach ($this->buffer as $record) { + if ($record['level'] >= $this->deduplicationLevel) { + $passthru = $passthru || !$this->isDuplicate($record); + if ($passthru) { + $this->appendRecord($record); + } + } + } + + // default of null is valid as well as if no record matches duplicationLevel we just pass through + if ($passthru === true || $passthru === null) { + $this->handler->handleBatch($this->buffer); + } + + $this->clear(); + + if ($this->gc) { + $this->collectLogs(); + } + } + + private function isDuplicate(array $record): bool + { + if (!file_exists($this->deduplicationStore)) { + return false; + } + + $store = file($this->deduplicationStore, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (!is_array($store)) { + return false; + } + + $yesterday = time() - 86400; + $timestampValidity = $record['datetime']->getTimestamp() - $this->time; + $expectedMessage = preg_replace('{[\r\n].*}', '', $record['message']); + + for ($i = count($store) - 1; $i >= 0; $i--) { + list($timestamp, $level, $message) = explode(':', $store[$i], 3); + + if ($level === $record['level_name'] && $message === $expectedMessage && $timestamp > $timestampValidity) { + return true; + } + + if ($timestamp < $yesterday) { + $this->gc = true; + } + } + + return false; + } + + private function collectLogs(): void + { + if (!file_exists($this->deduplicationStore)) { + return; + } + + $handle = fopen($this->deduplicationStore, 'rw+'); + + if (!$handle) { + throw new \RuntimeException('Failed to open file for reading and writing: ' . $this->deduplicationStore); + } + + flock($handle, LOCK_EX); + $validLogs = []; + + $timestampValidity = time() - $this->time; + + while (!feof($handle)) { + $log = fgets($handle); + if ($log && substr($log, 0, 10) >= $timestampValidity) { + $validLogs[] = $log; + } + } + + ftruncate($handle, 0); + rewind($handle); + foreach ($validLogs as $log) { + fwrite($handle, $log); + } + + flock($handle, LOCK_UN); + fclose($handle); + + $this->gc = false; + } + + private function appendRecord(array $record): void + { + file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php new file mode 100644 index 0000000..b80490d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Formatter\FormatterInterface; +use Doctrine\CouchDB\CouchDBClient; + +/** + * CouchDB handler for Doctrine CouchDB ODM + * + * @author Markus Bachmann + */ +class DoctrineCouchDBHandler extends AbstractProcessingHandler +{ + private $client; + + public function __construct(CouchDBClient $client, $level = Logger::DEBUG, bool $bubble = true) + { + $this->client = $client; + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->client->postDocument($record['formatted']); + } + + protected function getDefaultFormatter(): FormatterInterface + { + return new NormalizerFormatter; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php new file mode 100644 index 0000000..29f340a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Aws\Sdk; +use Aws\DynamoDb\DynamoDbClient; +use Monolog\Formatter\FormatterInterface; +use Aws\DynamoDb\Marshaler; +use Monolog\Formatter\ScalarFormatter; +use Monolog\Logger; + +/** + * Amazon DynamoDB handler (http://aws.amazon.com/dynamodb/) + * + * @link https://github.com/aws/aws-sdk-php/ + * @author Andrew Lawson + */ +class DynamoDbHandler extends AbstractProcessingHandler +{ + public const DATE_FORMAT = 'Y-m-d\TH:i:s.uO'; + + /** + * @var DynamoDbClient + */ + protected $client; + + /** + * @var string + */ + protected $table; + + /** + * @var int + */ + protected $version; + + /** + * @var Marshaler + */ + protected $marshaler; + + /** + * @param int|string $level + */ + public function __construct(DynamoDbClient $client, string $table, $level = Logger::DEBUG, bool $bubble = true) + { + /** @phpstan-ignore-next-line */ + if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) { + $this->version = 3; + $this->marshaler = new Marshaler; + } else { + $this->version = 2; + } + + $this->client = $client; + $this->table = $table; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $filtered = $this->filterEmptyFields($record['formatted']); + if ($this->version === 3) { + $formatted = $this->marshaler->marshalItem($filtered); + } else { + /** @phpstan-ignore-next-line */ + $formatted = $this->client->formatAttributes($filtered); + } + + $this->client->putItem([ + 'TableName' => $this->table, + 'Item' => $formatted, + ]); + } + + protected function filterEmptyFields(array $record): array + { + return array_filter($record, function ($value) { + return !empty($value) || false === $value || 0 === $value; + }); + } + + /** + * {@inheritdoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ScalarFormatter(self::DATE_FORMAT); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ElasticaHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ElasticaHandler.php new file mode 100644 index 0000000..7af68fe --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ElasticaHandler.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\ElasticaFormatter; +use Monolog\Logger; +use Elastica\Client; +use Elastica\Exception\ExceptionInterface; + +/** + * Elastic Search handler + * + * Usage example: + * + * $client = new \Elastica\Client(); + * $options = array( + * 'index' => 'elastic_index_name', + * 'type' => 'elastic_doc_type', Types have been removed in Elastica 7 + * ); + * $handler = new ElasticaHandler($client, $options); + * $log = new Logger('application'); + * $log->pushHandler($handler); + * + * @author Jelle Vink + */ +class ElasticaHandler extends AbstractProcessingHandler +{ + /** + * @var Client + */ + protected $client; + + /** + * @var array Handler config options + */ + protected $options = []; + + /** + * @param Client $client Elastica Client object + * @param array $options Handler configuration + * @param int|string $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + $this->client = $client; + $this->options = array_merge( + [ + 'index' => 'monolog', // Elastic index name + 'type' => 'record', // Elastic document type + 'ignore_error' => false, // Suppress Elastica exceptions + ], + $options + ); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->bulkSend([$record['formatted']]); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($formatter instanceof ElasticaFormatter) { + return parent::setFormatter($formatter); + } + + throw new \InvalidArgumentException('ElasticaHandler is only compatible with ElasticaFormatter'); + } + + public function getOptions(): array + { + return $this->options; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ElasticaFormatter($this->options['index'], $this->options['type']); + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $documents = $this->getFormatter()->formatBatch($records); + $this->bulkSend($documents); + } + + /** + * Use Elasticsearch bulk API to send list of documents + * @throws \RuntimeException + */ + protected function bulkSend(array $documents): void + { + try { + $this->client->addDocuments($documents); + } catch (ExceptionInterface $e) { + if (!$this->options['ignore_error']) { + throw new \RuntimeException("Error sending messages to Elasticsearch", 0, $e); + } + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ElasticsearchHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ElasticsearchHandler.php new file mode 100644 index 0000000..7a7ef05 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ElasticsearchHandler.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Throwable; +use RuntimeException; +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\ElasticsearchFormatter; +use InvalidArgumentException; +use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException; +use Elasticsearch\Client; + +/** + * Elasticsearch handler + * + * @link https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html + * + * Simple usage example: + * + * $client = \Elasticsearch\ClientBuilder::create() + * ->setHosts($hosts) + * ->build(); + * + * $options = array( + * 'index' => 'elastic_index_name', + * 'type' => 'elastic_doc_type', + * ); + * $handler = new ElasticsearchHandler($client, $options); + * $log = new Logger('application'); + * $log->pushHandler($handler); + * + * @author Avtandil Kikabidze + */ +class ElasticsearchHandler extends AbstractProcessingHandler +{ + /** + * @var Client + */ + protected $client; + + /** + * @var array Handler config options + */ + protected $options = []; + + /** + * @param Client $client Elasticsearch Client object + * @param array $options Handler configuration + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + $this->client = $client; + $this->options = array_merge( + [ + 'index' => 'monolog', // Elastic index name + 'type' => '_doc', // Elastic document type + 'ignore_error' => false, // Suppress Elasticsearch exceptions + ], + $options + ); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->bulkSend([$record['formatted']]); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($formatter instanceof ElasticsearchFormatter) { + return parent::setFormatter($formatter); + } + + throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter'); + } + + /** + * Getter options + * + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ElasticsearchFormatter($this->options['index'], $this->options['type']); + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $documents = $this->getFormatter()->formatBatch($records); + $this->bulkSend($documents); + } + + /** + * Use Elasticsearch bulk API to send list of documents + * + * @param array $records + * @throws \RuntimeException + */ + protected function bulkSend(array $records): void + { + try { + $params = [ + 'body' => [], + ]; + + foreach ($records as $record) { + $params['body'][] = [ + 'index' => [ + '_index' => $record['_index'], + '_type' => $record['_type'], + ], + ]; + unset($record['_index'], $record['_type']); + + $params['body'][] = $record; + } + + $responses = $this->client->bulk($params); + + if ($responses['errors'] === true) { + throw $this->createExceptionFromResponses($responses); + } + } catch (Throwable $e) { + if (! $this->options['ignore_error']) { + throw new RuntimeException('Error sending messages to Elasticsearch', 0, $e); + } + } + } + + /** + * Creates elasticsearch exception from responses array + * + * Only the first error is converted into an exception. + * + * @param array $responses returned by $this->client->bulk() + */ + protected function createExceptionFromResponses(array $responses): ElasticsearchRuntimeException + { + foreach ($responses['items'] ?? [] as $item) { + if (isset($item['index']['error'])) { + return $this->createExceptionFromError($item['index']['error']); + } + } + + return new ElasticsearchRuntimeException('Elasticsearch failed to index one or more records.'); + } + + /** + * Creates elasticsearch exception from error array + * + * @param array $error + */ + protected function createExceptionFromError(array $error): ElasticsearchRuntimeException + { + $previous = isset($error['caused_by']) ? $this->createExceptionFromError($error['caused_by']) : null; + + return new ElasticsearchRuntimeException($error['type'] . ': ' . $error['reason'], 0, $previous); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php new file mode 100644 index 0000000..737c070 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; + +/** + * Stores to PHP error_log() handler. + * + * @author Elan Ruusamäe + */ +class ErrorLogHandler extends AbstractProcessingHandler +{ + public const OPERATING_SYSTEM = 0; + public const SAPI = 4; + + protected $messageType; + protected $expandNewlines; + + /** + * @param int $messageType Says where the error should go. + * @param int|string $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries + */ + public function __construct(int $messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, bool $bubble = true, bool $expandNewlines = false) + { + parent::__construct($level, $bubble); + + if (false === in_array($messageType, self::getAvailableTypes(), true)) { + $message = sprintf('The given message type "%s" is not supported', print_r($messageType, true)); + + throw new \InvalidArgumentException($message); + } + + $this->messageType = $messageType; + $this->expandNewlines = $expandNewlines; + } + + /** + * @return array With all available types + */ + public static function getAvailableTypes(): array + { + return [ + self::OPERATING_SYSTEM, + self::SAPI, + ]; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter('[%datetime%] %channel%.%level_name%: %message% %context% %extra%'); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + if (!$this->expandNewlines) { + error_log((string) $record['formatted'], $this->messageType); + + return; + } + + $lines = preg_split('{[\r\n]+}', (string) $record['formatted']); + foreach ($lines as $line) { + error_log($line, $this->messageType); + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.php new file mode 100644 index 0000000..9c92fa2 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Throwable; + +class FallbackGroupHandler extends GroupHandler +{ + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + if ($this->processors) { + $record = $this->processRecord($record); + } + foreach ($this->handlers as $handler) { + try { + $handler->handle($record); + break; + } catch (Throwable $e) { + // What throwable? + } + } + + return false === $this->bubble; + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + if ($this->processors) { + $processed = []; + foreach ($records as $record) { + $processed[] = $this->processRecord($record); + } + $records = $processed; + } + + foreach ($this->handlers as $handler) { + try { + $handler->handleBatch($records); + break; + } catch (Throwable $e) { + // What throwable? + } + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php new file mode 100644 index 0000000..2d7a648 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\ResettableInterface; +use Monolog\Formatter\FormatterInterface; + +/** + * Simple handler wrapper that filters records based on a list of levels + * + * It can be configured with an exact list of levels to allow, or a min/max level. + * + * @author Hennadiy Verkh + * @author Jordi Boggiano + */ +class FilterHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + + /** + * Handler or factory callable($record, $this) + * + * @var callable|\Monolog\Handler\HandlerInterface + */ + protected $handler; + + /** + * Minimum level for logs that are passed to handler + * + * @var int[] + */ + protected $acceptedLevels; + + /** + * Whether the messages that are handled can bubble up the stack or not + * + * @var bool + */ + protected $bubble; + + /** + * @psalm-param HandlerInterface|callable(?array, HandlerInterface): HandlerInterface $handler + * + * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $filterHandler). + * @param int|array $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided + * @param int|string $maxLevel Maximum level to accept, only used if $minLevelOrList is not an array + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, bool $bubble = true) + { + $this->handler = $handler; + $this->bubble = $bubble; + $this->setAcceptedLevels($minLevelOrList, $maxLevel); + + if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { + throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); + } + } + + public function getAcceptedLevels(): array + { + return array_flip($this->acceptedLevels); + } + + /** + * @param int|string|array $minLevelOrList A list of levels to accept or a minimum level or level name if maxLevel is provided + * @param int|string $maxLevel Maximum level or level name to accept, only used if $minLevelOrList is not an array + */ + public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY): self + { + if (is_array($minLevelOrList)) { + $acceptedLevels = array_map('Monolog\Logger::toMonologLevel', $minLevelOrList); + } else { + $minLevelOrList = Logger::toMonologLevel($minLevelOrList); + $maxLevel = Logger::toMonologLevel($maxLevel); + $acceptedLevels = array_values(array_filter(Logger::getLevels(), function ($level) use ($minLevelOrList, $maxLevel) { + return $level >= $minLevelOrList && $level <= $maxLevel; + })); + } + $this->acceptedLevels = array_flip($acceptedLevels); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isHandling(array $record): bool + { + return isset($this->acceptedLevels[$record['level']]); + } + + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + if ($this->processors) { + $record = $this->processRecord($record); + } + + $this->getHandler($record)->handle($record); + + return false === $this->bubble; + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $filtered = []; + foreach ($records as $record) { + if ($this->isHandling($record)) { + $filtered[] = $record; + } + } + + if (count($filtered) > 0) { + $this->getHandler($filtered[count($filtered) - 1])->handleBatch($filtered); + } + } + + /** + * Return the nested handler + * + * If the handler was provided as a factory callable, this will trigger the handler's instantiation. + * + * @return HandlerInterface + */ + public function getHandler(array $record = null) + { + if (!$this->handler instanceof HandlerInterface) { + $this->handler = ($this->handler)($record, $this); + if (!$this->handler instanceof HandlerInterface) { + throw new \RuntimeException("The factory callable should return a HandlerInterface"); + } + } + + return $this->handler; + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } + + /** + * {@inheritdoc} + */ + public function getFormatter(): FormatterInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } + + public function reset() + { + $this->resetProcessors(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php new file mode 100644 index 0000000..1ba99c7 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\FingersCrossed; + +/** + * Interface for activation strategies for the FingersCrossedHandler. + * + * @author Johannes M. Schmitt + */ +interface ActivationStrategyInterface +{ + /** + * Returns whether the given record activates the handler. + */ + public function isHandlerActivated(array $record): bool; +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php new file mode 100644 index 0000000..f98ecfa --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\FingersCrossed; + +use Monolog\Logger; + +/** + * Channel and Error level based monolog activation strategy. Allows to trigger activation + * based on level per channel. e.g. trigger activation on level 'ERROR' by default, except + * for records of the 'sql' channel; those should trigger activation on level 'WARN'. + * + * Example: + * + * + * $activationStrategy = new ChannelLevelActivationStrategy( + * Logger::CRITICAL, + * array( + * 'request' => Logger::ALERT, + * 'sensitive' => Logger::ERROR, + * ) + * ); + * $handler = new FingersCrossedHandler(new StreamHandler('php://stderr'), $activationStrategy); + * + * + * @author Mike Meessen + */ +class ChannelLevelActivationStrategy implements ActivationStrategyInterface +{ + /** + * @var int + */ + private $defaultActionLevel; + + /** + * @var array + */ + private $channelToActionLevel; + + /** + * @param int|string $defaultActionLevel The default action level to be used if the record's category doesn't match any + * @param array $channelToActionLevel An array that maps channel names to action levels. + */ + public function __construct($defaultActionLevel, array $channelToActionLevel = []) + { + $this->defaultActionLevel = Logger::toMonologLevel($defaultActionLevel); + $this->channelToActionLevel = array_map('Monolog\Logger::toMonologLevel', $channelToActionLevel); + } + + public function isHandlerActivated(array $record): bool + { + if (isset($this->channelToActionLevel[$record['channel']])) { + return $record['level'] >= $this->channelToActionLevel[$record['channel']]; + } + + return $record['level'] >= $this->defaultActionLevel; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php new file mode 100644 index 0000000..71601e4 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\FingersCrossed; + +use Monolog\Logger; + +/** + * Error level based activation strategy. + * + * @author Johannes M. Schmitt + */ +class ErrorLevelActivationStrategy implements ActivationStrategyInterface +{ + /** + * @var int + */ + private $actionLevel; + + /** + * @param int|string $actionLevel Level or name or value + */ + public function __construct($actionLevel) + { + $this->actionLevel = Logger::toMonologLevel($actionLevel); + } + + public function isHandlerActivated(array $record): bool + { + return $record['level'] >= $this->actionLevel; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php new file mode 100644 index 0000000..2253e71 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php @@ -0,0 +1,228 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; +use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; +use Monolog\Logger; +use Monolog\ResettableInterface; +use Monolog\Formatter\FormatterInterface; + +/** + * Buffers all records until a certain level is reached + * + * The advantage of this approach is that you don't get any clutter in your log files. + * Only requests which actually trigger an error (or whatever your actionLevel is) will be + * in the logs, but they will contain all records, not only those above the level threshold. + * + * You can then have a passthruLevel as well which means that at the end of the request, + * even if it did not get activated, it will still send through log records of e.g. at least a + * warning level. + * + * You can find the various activation strategies in the + * Monolog\Handler\FingersCrossed\ namespace. + * + * @author Jordi Boggiano + */ +class FingersCrossedHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + + /** @var HandlerInterface */ + protected $handler; + protected $activationStrategy; + protected $buffering = true; + protected $bufferSize; + protected $buffer = []; + protected $stopBuffering; + protected $passthruLevel; + protected $bubble; + + /** + * @psalm-param HandlerInterface|callable(?array, FingersCrossedHandler): HandlerInterface $handler + * + * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $fingersCrossedHandler). + * @param int|string|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action, or a level name/value at which the handler is activated + * @param int $bufferSize How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $stopBuffering Whether the handler should stop buffering after being triggered (default true) + * @param int|string $passthruLevel Minimum level to always flush to handler on close, even if strategy not triggered + */ + public function __construct($handler, $activationStrategy = null, int $bufferSize = 0, bool $bubble = true, bool $stopBuffering = true, $passthruLevel = null) + { + if (null === $activationStrategy) { + $activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING); + } + + // convert simple int activationStrategy to an object + if (!$activationStrategy instanceof ActivationStrategyInterface) { + $activationStrategy = new ErrorLevelActivationStrategy($activationStrategy); + } + + $this->handler = $handler; + $this->activationStrategy = $activationStrategy; + $this->bufferSize = $bufferSize; + $this->bubble = $bubble; + $this->stopBuffering = $stopBuffering; + + if ($passthruLevel !== null) { + $this->passthruLevel = Logger::toMonologLevel($passthruLevel); + } + + if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { + throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); + } + } + + /** + * {@inheritdoc} + */ + public function isHandling(array $record): bool + { + return true; + } + + /** + * Manually activate this logger regardless of the activation strategy + */ + public function activate(): void + { + if ($this->stopBuffering) { + $this->buffering = false; + } + + $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer); + $this->buffer = []; + } + + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + if ($this->processors) { + $record = $this->processRecord($record); + } + + if ($this->buffering) { + $this->buffer[] = $record; + if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) { + array_shift($this->buffer); + } + if ($this->activationStrategy->isHandlerActivated($record)) { + $this->activate(); + } + } else { + $this->getHandler($record)->handle($record); + } + + return false === $this->bubble; + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + $this->flushBuffer(); + + $this->handler->close(); + } + + public function reset() + { + $this->flushBuffer(); + + $this->resetProcessors(); + + if ($this->getHandler() instanceof ResettableInterface) { + $this->getHandler()->reset(); + } + } + + /** + * Clears the buffer without flushing any messages down to the wrapped handler. + * + * It also resets the handler to its initial buffering state. + */ + public function clear(): void + { + $this->buffer = []; + $this->reset(); + } + + /** + * Resets the state of the handler. Stops forwarding records to the wrapped handler. + */ + private function flushBuffer(): void + { + if (null !== $this->passthruLevel) { + $level = $this->passthruLevel; + $this->buffer = array_filter($this->buffer, function ($record) use ($level) { + return $record['level'] >= $level; + }); + if (count($this->buffer) > 0) { + $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer); + } + } + + $this->buffer = []; + $this->buffering = true; + } + + /** + * Return the nested handler + * + * If the handler was provided as a factory callable, this will trigger the handler's instantiation. + * + * @return HandlerInterface + */ + public function getHandler(array $record = null) + { + if (!$this->handler instanceof HandlerInterface) { + $this->handler = ($this->handler)($record, $this); + if (!$this->handler instanceof HandlerInterface) { + throw new \RuntimeException("The factory callable should return a HandlerInterface"); + } + } + + return $this->handler; + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } + + /** + * {@inheritdoc} + */ + public function getFormatter(): FormatterInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php new file mode 100644 index 0000000..c963863 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\WildfireFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol. + * + * @author Eric Clemmons (@ericclemmons) + */ +class FirePHPHandler extends AbstractProcessingHandler +{ + use WebRequestRecognizerTrait; + + /** + * WildFire JSON header message format + */ + protected const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2'; + + /** + * FirePHP structure for parsing messages & their presentation + */ + protected const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'; + + /** + * Must reference a "known" plugin, otherwise headers won't display in FirePHP + */ + protected const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3'; + + /** + * Header prefix for Wildfire to recognize & parse headers + */ + protected const HEADER_PREFIX = 'X-Wf'; + + /** + * Whether or not Wildfire vendor-specific headers have been generated & sent yet + */ + protected static $initialized = false; + + /** + * Shared static message index between potentially multiple handlers + * @var int + */ + protected static $messageIndex = 1; + + protected static $sendHeaders = true; + + /** + * Base header creation function used by init headers & record headers + * + * @param array $meta Wildfire Plugin, Protocol & Structure Indexes + * @param string $message Log message + * @return array Complete header string ready for the client as key and message as value + */ + protected function createHeader(array $meta, string $message): array + { + $header = sprintf('%s-%s', static::HEADER_PREFIX, join('-', $meta)); + + return [$header => $message]; + } + + /** + * Creates message header from record + * + * @see createHeader() + */ + protected function createRecordHeader(array $record): array + { + // Wildfire is extensible to support multiple protocols & plugins in a single request, + // but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake. + return $this->createHeader( + [1, 1, 1, self::$messageIndex++], + $record['formatted'] + ); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new WildfireFormatter(); + } + + /** + * Wildfire initialization headers to enable message parsing + * + * @see createHeader() + * @see sendHeader() + */ + protected function getInitHeaders(): array + { + // Initial payload consists of required headers for Wildfire + return array_merge( + $this->createHeader(['Protocol', 1], static::PROTOCOL_URI), + $this->createHeader([1, 'Structure', 1], static::STRUCTURE_URI), + $this->createHeader([1, 'Plugin', 1], static::PLUGIN_URI) + ); + } + + /** + * Send header string to the client + */ + protected function sendHeader(string $header, string $content): void + { + if (!headers_sent() && self::$sendHeaders) { + header(sprintf('%s: %s', $header, $content)); + } + } + + /** + * Creates & sends header for a record, ensuring init headers have been sent prior + * + * @see sendHeader() + * @see sendInitHeaders() + * @param array $record + */ + protected function write(array $record): void + { + if (!self::$sendHeaders || !$this->isWebRequest()) { + return; + } + + // WildFire-specific headers must be sent prior to any messages + if (!self::$initialized) { + self::$initialized = true; + + self::$sendHeaders = $this->headersAccepted(); + if (!self::$sendHeaders) { + return; + } + + foreach ($this->getInitHeaders() as $header => $content) { + $this->sendHeader($header, $content); + } + } + + $header = $this->createRecordHeader($record); + if (trim(current($header)) !== '') { + $this->sendHeader(key($header), current($header)); + } + } + + /** + * Verifies if the headers are accepted by the current user agent + */ + protected function headersAccepted(): bool + { + if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) { + return true; + } + + return isset($_SERVER['HTTP_X_FIREPHP_VERSION']); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php new file mode 100644 index 0000000..5087009 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Monolog\Logger; + +/** + * Sends logs to Fleep.io using Webhook integrations + * + * You'll need a Fleep.io account to use this handler. + * + * @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation + * @author Ando Roots + */ +class FleepHookHandler extends SocketHandler +{ + protected const FLEEP_HOST = 'fleep.io'; + + protected const FLEEP_HOOK_URI = '/hook/'; + + /** + * @var string Webhook token (specifies the conversation where logs are sent) + */ + protected $token; + + /** + * Construct a new Fleep.io Handler. + * + * For instructions on how to create a new web hook in your conversations + * see https://fleep.io/integrations/webhooks/ + * + * @param string $token Webhook token + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @throws MissingExtensionException + */ + public function __construct(string $token, $level = Logger::DEBUG, bool $bubble = true) + { + if (!extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FleepHookHandler'); + } + + $this->token = $token; + + $connectionString = 'ssl://' . static::FLEEP_HOST . ':443'; + parent::__construct($connectionString, $level, $bubble); + } + + /** + * Returns the default formatter to use with this handler + * + * Overloaded to remove empty context and extra arrays from the end of the log message. + * + * @return LineFormatter + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter(null, null, true, true); + } + + /** + * Handles a log record + */ + public function write(array $record): void + { + parent::write($record); + $this->closeSocket(); + } + + /** + * {@inheritdoc} + */ + protected function generateDataStream(array $record): string + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + /** + * Builds the header of the API Call + */ + private function buildHeader(string $content): string + { + $header = "POST " . static::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n"; + $header .= "Host: " . static::FLEEP_HOST . "\r\n"; + $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } + + /** + * Builds the body of API call + */ + private function buildContent(array $record): string + { + $dataArray = [ + 'message' => $record['formatted'], + ]; + + return http_build_query($dataArray); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php new file mode 100644 index 0000000..062af41 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\FlowdockFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Sends notifications through the Flowdock push API + * + * This must be configured with a FlowdockFormatter instance via setFormatter() + * + * Notes: + * API token - Flowdock API token + * + * @author Dominik Liebler + * @see https://www.flowdock.com/api/push + */ +class FlowdockHandler extends SocketHandler +{ + /** + * @var string + */ + protected $apiToken; + + /** + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * + * @throws MissingExtensionException if OpenSSL is missing + */ + public function __construct(string $apiToken, $level = Logger::DEBUG, bool $bubble = true) + { + if (!extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FlowdockHandler'); + } + + parent::__construct('ssl://api.flowdock.com:443', $level, $bubble); + $this->apiToken = $apiToken; + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if (!$formatter instanceof FlowdockFormatter) { + throw new \InvalidArgumentException('The FlowdockHandler requires an instance of Monolog\Formatter\FlowdockFormatter to function correctly'); + } + + return parent::setFormatter($formatter); + } + + /** + * Gets the default formatter. + */ + protected function getDefaultFormatter(): FormatterInterface + { + throw new \InvalidArgumentException('The FlowdockHandler must be configured (via setFormatter) with an instance of Monolog\Formatter\FlowdockFormatter to function correctly'); + } + + /** + * {@inheritdoc} + * + * @param array $record + */ + protected function write(array $record): void + { + parent::write($record); + + $this->closeSocket(); + } + + /** + * {@inheritdoc} + */ + protected function generateDataStream(array $record): string + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + /** + * Builds the body of API call + */ + private function buildContent(array $record): string + { + return Utils::jsonEncode($record['formatted']['flowdock']); + } + + /** + * Builds the header of the API Call + */ + private function buildHeader(string $content): string + { + $header = "POST /v1/messages/team_inbox/" . $this->apiToken . " HTTP/1.1\r\n"; + $header .= "Host: api.flowdock.com\r\n"; + $header .= "Content-Type: application/json\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php new file mode 100644 index 0000000..fc1693c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; + +/** + * Interface to describe loggers that have a formatter + * + * @author Jordi Boggiano + */ +interface FormattableHandlerInterface +{ + /** + * Sets the formatter. + * + * @param FormatterInterface $formatter + * @return HandlerInterface self + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface; + + /** + * Gets the formatter. + * + * @return FormatterInterface + */ + public function getFormatter(): FormatterInterface; +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php new file mode 100644 index 0000000..3dbf0f6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; + +/** + * Helper trait for implementing FormattableInterface + * + * @author Jordi Boggiano + */ +trait FormattableHandlerTrait +{ + /** + * @var ?FormatterInterface + */ + protected $formatter; + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $this->formatter = $formatter; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFormatter(): FormatterInterface + { + if (!$this->formatter) { + $this->formatter = $this->getDefaultFormatter(); + } + + return $this->formatter; + } + + /** + * Gets the default formatter. + * + * Overwrite this if the LineFormatter is not a good default for your handler. + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php new file mode 100644 index 0000000..d6965fa --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Gelf\PublisherInterface; +use Monolog\Logger; +use Monolog\Formatter\GelfMessageFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Handler to send messages to a Graylog2 (http://www.graylog2.org) server + * + * @author Matt Lehner + * @author Benjamin Zikarsky + */ +class GelfHandler extends AbstractProcessingHandler +{ + /** + * @var PublisherInterface|null the publisher object that sends the message to the server + */ + protected $publisher; + + /** + * @param PublisherInterface $publisher a publisher object + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(PublisherInterface $publisher, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->publisher = $publisher; + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $this->publisher->publish($record['formatted']); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new GelfMessageFormatter(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php new file mode 100644 index 0000000..a7d8a31 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\ResettableInterface; + +/** + * Forwards records to multiple handlers + * + * @author Lenar Lõhmus + */ +class GroupHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface +{ + use ProcessableHandlerTrait; + + /** @var HandlerInterface[] */ + protected $handlers; + protected $bubble; + + /** + * @param HandlerInterface[] $handlers Array of Handlers. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(array $handlers, bool $bubble = true) + { + foreach ($handlers as $handler) { + if (!$handler instanceof HandlerInterface) { + throw new \InvalidArgumentException('The first argument of the GroupHandler must be an array of HandlerInterface instances.'); + } + } + + $this->handlers = $handlers; + $this->bubble = $bubble; + } + + /** + * {@inheritdoc} + */ + public function isHandling(array $record): bool + { + foreach ($this->handlers as $handler) { + if ($handler->isHandling($record)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + if ($this->processors) { + $record = $this->processRecord($record); + } + + foreach ($this->handlers as $handler) { + $handler->handle($record); + } + + return false === $this->bubble; + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + if ($this->processors) { + $processed = []; + foreach ($records as $record) { + $processed[] = $this->processRecord($record); + } + $records = $processed; + } + + foreach ($this->handlers as $handler) { + $handler->handleBatch($records); + } + } + + public function reset() + { + $this->resetProcessors(); + + foreach ($this->handlers as $handler) { + if ($handler instanceof ResettableInterface) { + $handler->reset(); + } + } + } + + public function close(): void + { + parent::close(); + + foreach ($this->handlers as $handler) { + $handler->close(); + } + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + foreach ($this->handlers as $handler) { + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); + } + } + + return $this; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Handler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Handler.php new file mode 100644 index 0000000..9f43fe1 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Handler.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Base Handler class providing basic close() support as well as handleBatch + * + * @author Jordi Boggiano + */ +abstract class Handler implements HandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + foreach ($records as $record) { + $this->handle($record); + } + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + } + + public function __destruct() + { + try { + $this->close(); + } catch (\Throwable $e) { + // do nothing + } + } + + public function __sleep() + { + $this->close(); + + return array_keys(get_object_vars($this)); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php new file mode 100644 index 0000000..68aed18 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Interface that all Monolog Handlers must implement + * + * @author Jordi Boggiano + */ +interface HandlerInterface +{ + /** + * Checks whether the given record will be handled by this handler. + * + * This is mostly done for performance reasons, to avoid calling processors for nothing. + * + * Handlers should still check the record levels within handle(), returning false in isHandling() + * is no guarantee that handle() will not be called, and isHandling() might not be called + * for a given record. + * + * @param array $record Partial log record containing only a level key + * + * @return bool + */ + public function isHandling(array $record): bool; + + /** + * Handles a record. + * + * All records may be passed to this method, and the handler should discard + * those that it does not want to handle. + * + * The return value of this function controls the bubbling process of the handler stack. + * Unless the bubbling is interrupted (by returning true), the Logger class will keep on + * calling further handlers in the stack with a given log record. + * + * @param array $record The record to handle + * @return bool true means that this handler handled the record, and that bubbling is not permitted. + * false means the record was either not processed or that this handler allows bubbling. + */ + public function handle(array $record): bool; + + /** + * Handles a set of records at once. + * + * @param array $records The records to handle (an array of record arrays) + */ + public function handleBatch(array $records): void; + + /** + * Closes the handler. + * + * Ends a log cycle and frees all resources used by the handler. + * + * Closing a Handler means flushing all buffers and freeing any open resources/handles. + * + * Implementations have to be idempotent (i.e. it should be possible to call close several times without breakage) + * and ideally handlers should be able to reopen themselves on handle() after they have been closed. + * + * This is useful at the end of a request and will be called automatically when the object + * is destroyed if you extend Monolog\Handler\Handler. + * + * If you are thinking of calling this method yourself, most likely you should be + * calling ResettableInterface::reset instead. Have a look. + */ + public function close(): void; +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.php new file mode 100644 index 0000000..842c941 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\ResettableInterface; +use Monolog\Formatter\FormatterInterface; + +/** + * This simple wrapper class can be used to extend handlers functionality. + * + * Example: A custom filtering that can be applied to any handler. + * + * Inherit from this class and override handle() like this: + * + * public function handle(array $record) + * { + * if ($record meets certain conditions) { + * return false; + * } + * return $this->handler->handle($record); + * } + * + * @author Alexey Karapetov + */ +class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface +{ + /** + * @var HandlerInterface + */ + protected $handler; + + public function __construct(HandlerInterface $handler) + { + $this->handler = $handler; + } + + /** + * {@inheritdoc} + */ + public function isHandling(array $record): bool + { + return $this->handler->isHandling($record); + } + + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + return $this->handler->handle($record); + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $this->handler->handleBatch($records); + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + $this->handler->close(); + } + + /** + * {@inheritdoc} + */ + public function pushProcessor(callable $callback): HandlerInterface + { + if ($this->handler instanceof ProcessableHandlerInterface) { + $this->handler->pushProcessor($callback); + + return $this; + } + + throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class); + } + + /** + * {@inheritdoc} + */ + public function popProcessor(): callable + { + if ($this->handler instanceof ProcessableHandlerInterface) { + return $this->handler->popProcessor(); + } + + throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + $this->handler->setFormatter($formatter); + + return $this; + } + + throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class); + } + + /** + * {@inheritdoc} + */ + public function getFormatter(): FormatterInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + return $this->handler->getFormatter(); + } + + throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class); + } + + public function reset() + { + if ($this->handler instanceof ResettableInterface) { + $this->handler->reset(); + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php new file mode 100644 index 0000000..921c9f2 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; + +/** + * IFTTTHandler uses cURL to trigger IFTTT Maker actions + * + * Register a secret key and trigger/event name at https://ifttt.com/maker + * + * value1 will be the channel from monolog's Logger constructor, + * value2 will be the level name (ERROR, WARNING, ..) + * value3 will be the log record's message + * + * @author Nehal Patel + */ +class IFTTTHandler extends AbstractProcessingHandler +{ + private $eventName; + private $secretKey; + + /** + * @param string $eventName The name of the IFTTT Maker event that should be triggered + * @param string $secretKey A valid IFTTT secret key + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(string $eventName, string $secretKey, $level = Logger::ERROR, bool $bubble = true) + { + $this->eventName = $eventName; + $this->secretKey = $secretKey; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritdoc} + */ + public function write(array $record): void + { + $postData = [ + "value1" => $record["channel"], + "value2" => $record["level_name"], + "value3" => $record["message"], + ]; + $postString = Utils::jsonEncode($postData); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "https://maker.ifttt.com/trigger/" . $this->eventName . "/with/key/" . $this->secretKey); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postString); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Content-Type: application/json", + ]); + + Curl\Util::execute($ch); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.php new file mode 100644 index 0000000..5ddc66b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Inspired on LogEntriesHandler. + * + * @author Robert Kaufmann III + * @author Gabriel Machado + */ +class InsightOpsHandler extends SocketHandler +{ + /** + * @var string + */ + protected $logToken; + + /** + * @param string $token Log token supplied by InsightOps + * @param string $region Region where InsightOps account is hosted. Could be 'us' or 'eu'. + * @param bool $useSSL Whether or not SSL encryption should be used + * @param string|int $level The minimum logging level to trigger this handler + * @param bool $bubble Whether or not messages that are handled should bubble up the stack. + * + * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing + */ + public function __construct(string $token, string $region = 'us', bool $useSSL = true, $level = Logger::DEBUG, bool $bubble = true) + { + if ($useSSL && !extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for InsightOpsHandler'); + } + + $endpoint = $useSSL + ? 'ssl://' . $region . '.data.logs.insight.rapid7.com:443' + : $region . '.data.logs.insight.rapid7.com:80'; + + parent::__construct($endpoint, $level, $bubble); + $this->logToken = $token; + } + + /** + * {@inheritdoc} + */ + protected function generateDataStream(array $record): string + { + return $this->logToken . ' ' . $record['formatted']; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.php new file mode 100644 index 0000000..66de5f8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * @author Robert Kaufmann III + */ +class LogEntriesHandler extends SocketHandler +{ + /** + * @var string + */ + protected $logToken; + + /** + * @param string $token Log token supplied by LogEntries + * @param bool $useSSL Whether or not SSL encryption should be used. + * @param string|int $level The minimum logging level to trigger this handler + * @param bool $bubble Whether or not messages that are handled should bubble up the stack. + * @param string $host Custom hostname to send the data to if needed + * + * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing + */ + public function __construct(string $token, bool $useSSL = true, $level = Logger::DEBUG, bool $bubble = true, string $host = 'data.logentries.com') + { + if ($useSSL && !extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogEntriesHandler'); + } + + $endpoint = $useSSL ? 'ssl://' . $host . ':443' : $host . ':80'; + parent::__construct($endpoint, $level, $bubble); + $this->logToken = $token; + } + + /** + * {@inheritdoc} + */ + protected function generateDataStream(array $record): string + { + return $this->logToken . ' ' . $record['formatted']; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php new file mode 100644 index 0000000..6fc7066 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LogglyFormatter; +use function array_key_exists; +use CurlHandle; + +/** + * Sends errors to Loggly. + * + * @author Przemek Sobstel + * @author Adam Pancutt + * @author Gregory Barchard + */ +class LogglyHandler extends AbstractProcessingHandler +{ + protected const HOST = 'logs-01.loggly.com'; + protected const ENDPOINT_SINGLE = 'inputs'; + protected const ENDPOINT_BATCH = 'bulk'; + + /** + * Caches the curl handlers for every given endpoint. + * + * @var resource[]|CurlHandle[] + */ + protected $curlHandlers = []; + + protected $token; + + protected $tag = []; + + /** + * @param string $token API token supplied by Loggly + * @param string|int $level The minimum logging level to trigger this handler + * @param bool $bubble Whether or not messages that are handled should bubble up the stack. + * + * @throws MissingExtensionException If the curl extension is missing + */ + public function __construct(string $token, $level = Logger::DEBUG, bool $bubble = true) + { + if (!extension_loaded('curl')) { + throw new MissingExtensionException('The curl extension is needed to use the LogglyHandler'); + } + + $this->token = $token; + + parent::__construct($level, $bubble); + } + + /** + * Loads and returns the shared curl handler for the given endpoint. + * + * @param string $endpoint + * + * @return resource|CurlHandle + */ + protected function getCurlHandler(string $endpoint) + { + if (!array_key_exists($endpoint, $this->curlHandlers)) { + $this->curlHandlers[$endpoint] = $this->loadCurlHandle($endpoint); + } + + return $this->curlHandlers[$endpoint]; + } + + /** + * Starts a fresh curl session for the given endpoint and returns its handler. + * + * @param string $endpoint + * + * @return resource|CurlHandle + */ + private function loadCurlHandle(string $endpoint) + { + $url = sprintf("https://%s/%s/%s/", static::HOST, $endpoint, $this->token); + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + return $ch; + } + + /** + * @param string[]|string $tag + */ + public function setTag($tag): self + { + $tag = !empty($tag) ? $tag : []; + $this->tag = is_array($tag) ? $tag : [$tag]; + + return $this; + } + + /** + * @param string[]|string $tag + */ + public function addTag($tag): self + { + if (!empty($tag)) { + $tag = is_array($tag) ? $tag : [$tag]; + $this->tag = array_unique(array_merge($this->tag, $tag)); + } + + return $this; + } + + protected function write(array $record): void + { + $this->send($record["formatted"], static::ENDPOINT_SINGLE); + } + + public function handleBatch(array $records): void + { + $level = $this->level; + + $records = array_filter($records, function ($record) use ($level) { + return ($record['level'] >= $level); + }); + + if ($records) { + $this->send($this->getFormatter()->formatBatch($records), static::ENDPOINT_BATCH); + } + } + + protected function send(string $data, string $endpoint): void + { + $ch = $this->getCurlHandler($endpoint); + + $headers = ['Content-Type: application/json']; + + if (!empty($this->tag)) { + $headers[] = 'X-LOGGLY-TAG: '.implode(',', $this->tag); + } + + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + Curl\Util::execute($ch, 5, false); + } + + protected function getDefaultFormatter(): FormatterInterface + { + return new LogglyFormatter(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogmaticHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogmaticHandler.php new file mode 100644 index 0000000..209af16 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/LogmaticHandler.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LogmaticFormatter; + +/** + * @author Julien Breux + */ +class LogmaticHandler extends SocketHandler +{ + /** + * @var string + */ + private $logToken; + + /** + * @var string + */ + private $hostname; + + /** + * @var string + */ + private $appname; + + /** + * @param string $token Log token supplied by Logmatic. + * @param string $hostname Host name supplied by Logmatic. + * @param string $appname Application name supplied by Logmatic. + * @param bool $useSSL Whether or not SSL encryption should be used. + * @param int|string $level The minimum logging level to trigger this handler. + * @param bool $bubble Whether or not messages that are handled should bubble up the stack. + * + * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing + */ + public function __construct(string $token, string $hostname = '', string $appname = '', bool $useSSL = true, $level = Logger::DEBUG, bool $bubble = true) + { + if ($useSSL && !extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use SSL encrypted connection for LogmaticHandler'); + } + + $endpoint = $useSSL ? 'ssl://api.logmatic.io:10515' : 'api.logmatic.io:10514'; + $endpoint .= '/v1/'; + + parent::__construct($endpoint, $level, $bubble); + + $this->logToken = $token; + $this->hostname = $hostname; + $this->appname = $appname; + } + + /** + * {@inheritdoc} + */ + protected function generateDataStream(array $record): string + { + return $this->logToken . ' ' . $record['formatted']; + } + + /** + * {@inheritdoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + $formatter = new LogmaticFormatter(); + + if (!empty($this->hostname)) { + $formatter->setHostname($this->hostname); + } + if (!empty($this->appname)) { + $formatter->setAppname($this->appname); + } + + return $formatter; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MailHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MailHandler.php new file mode 100644 index 0000000..3bbfd56 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MailHandler.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\HtmlFormatter; + +/** + * Base class for all mail handlers + * + * @author Gyula Sallai + */ +abstract class MailHandler extends AbstractProcessingHandler +{ + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $messages = []; + + foreach ($records as $record) { + if ($record['level'] < $this->level) { + continue; + } + $messages[] = $this->processRecord($record); + } + + if (!empty($messages)) { + $this->send((string) $this->getFormatter()->formatBatch($messages), $messages); + } + } + + /** + * Send a mail with the given content + * + * @param string $content formatted email body to be sent + * @param array $records the array of log records that formed this content + */ + abstract protected function send(string $content, array $records): void; + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $this->send((string) $record['formatted'], [$record]); + } + + protected function getHighestRecord(array $records): array + { + $highestRecord = null; + foreach ($records as $record) { + if ($highestRecord === null || $highestRecord['level'] < $record['level']) { + $highestRecord = $record; + } + } + + return $highestRecord; + } + + protected function isHtmlBody(string $body): bool + { + return substr($body, 0, 1) === '<'; + } + + /** + * Gets the default formatter. + * + * @return FormatterInterface + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new HtmlFormatter(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.php new file mode 100644 index 0000000..95c612a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Swift; +use Swift_Message; + +/** + * MandrillHandler uses cURL to send the emails to the Mandrill API + * + * @author Adam Nicholson + */ +class MandrillHandler extends MailHandler +{ + /** @var Swift_Message */ + protected $message; + /** @var string */ + protected $apiKey; + + /** + * @psalm-param Swift_Message|callable(): Swift_Message $message + * + * @param string $apiKey A valid Mandrill API key + * @param callable|Swift_Message $message An example message for real messages, only the body will be replaced + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(string $apiKey, $message, $level = Logger::ERROR, bool $bubble = true) + { + parent::__construct($level, $bubble); + + if (!$message instanceof Swift_Message && is_callable($message)) { + $message = $message(); + } + if (!$message instanceof Swift_Message) { + throw new \InvalidArgumentException('You must provide either a Swift_Message instance or a callable returning it'); + } + $this->message = $message; + $this->apiKey = $apiKey; + } + + /** + * {@inheritdoc} + */ + protected function send(string $content, array $records): void + { + $mime = 'text/plain'; + if ($this->isHtmlBody($content)) { + $mime = 'text/html'; + } + + $message = clone $this->message; + $message->setBody($content, $mime); + /** @phpstan-ignore-next-line */ + if (version_compare(Swift::VERSION, '6.0.0', '>=')) { + $message->setDate(new \DateTimeImmutable()); + } else { + /** @phpstan-ignore-next-line */ + $message->setDate(time()); + } + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, 'https://mandrillapp.com/api/1.0/messages/send-raw.json'); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ + 'key' => $this->apiKey, + 'raw_message' => (string) $message, + 'async' => false, + ])); + + Curl\Util::execute($ch); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php new file mode 100644 index 0000000..3965aee --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Exception can be thrown if an extension for a handler is missing + * + * @author Christian Bergau + */ +class MissingExtensionException extends \Exception +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php new file mode 100644 index 0000000..e753917 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Manager; +use MongoDB\Client; +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\MongoDBFormatter; + +/** + * Logs to a MongoDB database. + * + * Usage example: + * + * $log = new \Monolog\Logger('application'); + * $client = new \MongoDB\Client('mongodb://localhost:27017'); + * $mongodb = new \Monolog\Handler\MongoDBHandler($client, 'logs', 'prod'); + * $log->pushHandler($mongodb); + * + * The above examples uses the MongoDB PHP library's client class; however, the + * MongoDB\Driver\Manager class from ext-mongodb is also supported. + */ +class MongoDBHandler extends AbstractProcessingHandler +{ + private $collection; + private $manager; + private $namespace; + + /** + * Constructor. + * + * @param Client|Manager $mongodb MongoDB library or driver client + * @param string $database Database name + * @param string $collection Collection name + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct($mongodb, string $database, string $collection, $level = Logger::DEBUG, bool $bubble = true) + { + if (!($mongodb instanceof Client || $mongodb instanceof Manager)) { + throw new \InvalidArgumentException('MongoDB\Client or MongoDB\Driver\Manager instance required'); + } + + if ($mongodb instanceof Client) { + $this->collection = $mongodb->selectCollection($database, $collection); + } else { + $this->manager = $mongodb; + $this->namespace = $database . '.' . $collection; + } + + parent::__construct($level, $bubble); + } + + protected function write(array $record): void + { + if (isset($this->collection)) { + $this->collection->insertOne($record['formatted']); + } + + if (isset($this->manager, $this->namespace)) { + $bulk = new BulkWrite; + $bulk->insert($record["formatted"]); + $this->manager->executeBulkWrite($this->namespace, $bulk); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new MongoDBFormatter; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.php new file mode 100644 index 0000000..cd20561 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\LineFormatter; + +/** + * NativeMailerHandler uses the mail() function to send the emails + * + * @author Christophe Coevoet + * @author Mark Garrett + */ +class NativeMailerHandler extends MailHandler +{ + /** + * The email addresses to which the message will be sent + * @var array + */ + protected $to; + + /** + * The subject of the email + * @var string + */ + protected $subject; + + /** + * Optional headers for the message + * @var array + */ + protected $headers = []; + + /** + * Optional parameters for the message + * @var array + */ + protected $parameters = []; + + /** + * The wordwrap length for the message + * @var int + */ + protected $maxColumnWidth; + + /** + * The Content-type for the message + * @var string|null + */ + protected $contentType; + + /** + * The encoding for the message + * @var string + */ + protected $encoding = 'utf-8'; + + /** + * @param string|array $to The receiver of the mail + * @param string $subject The subject of the mail + * @param string $from The sender of the mail + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param int $maxColumnWidth The maximum column width that the message lines will have + */ + public function __construct($to, string $subject, string $from, $level = Logger::ERROR, bool $bubble = true, int $maxColumnWidth = 70) + { + parent::__construct($level, $bubble); + $this->to = (array) $to; + $this->subject = $subject; + $this->addHeader(sprintf('From: %s', $from)); + $this->maxColumnWidth = $maxColumnWidth; + } + + /** + * Add headers to the message + * + * @param string|array $headers Custom added headers + */ + public function addHeader($headers): self + { + foreach ((array) $headers as $header) { + if (strpos($header, "\n") !== false || strpos($header, "\r") !== false) { + throw new \InvalidArgumentException('Headers can not contain newline characters for security reasons'); + } + $this->headers[] = $header; + } + + return $this; + } + + /** + * Add parameters to the message + * + * @param string|array $parameters Custom added parameters + */ + public function addParameter($parameters): self + { + $this->parameters = array_merge($this->parameters, (array) $parameters); + + return $this; + } + + /** + * {@inheritdoc} + */ + protected function send(string $content, array $records): void + { + $contentType = $this->getContentType() ?: ($this->isHtmlBody($content) ? 'text/html' : 'text/plain'); + + if ($contentType !== 'text/html') { + $content = wordwrap($content, $this->maxColumnWidth); + } + + $headers = ltrim(implode("\r\n", $this->headers) . "\r\n", "\r\n"); + $headers .= 'Content-type: ' . $contentType . '; charset=' . $this->getEncoding() . "\r\n"; + if ($contentType === 'text/html' && false === strpos($headers, 'MIME-Version:')) { + $headers .= 'MIME-Version: 1.0' . "\r\n"; + } + + $subject = $this->subject; + if ($records) { + $subjectFormatter = new LineFormatter($this->subject); + $subject = $subjectFormatter->format($this->getHighestRecord($records)); + } + + $parameters = implode(' ', $this->parameters); + foreach ($this->to as $to) { + mail($to, $subject, $content, $headers, $parameters); + } + } + + public function getContentType(): ?string + { + return $this->contentType; + } + + public function getEncoding(): string + { + return $this->encoding; + } + + /** + * @param string $contentType The content type of the email - Defaults to text/plain. Use text/html for HTML messages. + */ + public function setContentType(string $contentType): self + { + if (strpos($contentType, "\n") !== false || strpos($contentType, "\r") !== false) { + throw new \InvalidArgumentException('The content type can not contain newline characters to prevent email header injection'); + } + + $this->contentType = $contentType; + + return $this; + } + + public function setEncoding(string $encoding): self + { + if (strpos($encoding, "\n") !== false || strpos($encoding, "\r") !== false) { + throw new \InvalidArgumentException('The encoding can not contain newline characters to prevent email header injection'); + } + + $this->encoding = $encoding; + + return $this; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.php new file mode 100644 index 0000000..177ad21 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Class to record a log on a NewRelic application. + * Enabling New Relic High Security mode may prevent capture of useful information. + * + * This handler requires a NormalizerFormatter to function and expects an array in $record['formatted'] + * + * @see https://docs.newrelic.com/docs/agents/php-agent + * @see https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security + */ +class NewRelicHandler extends AbstractProcessingHandler +{ + /** + * Name of the New Relic application that will receive logs from this handler. + * + * @var string|null + */ + protected $appName; + + /** + * Name of the current transaction + * + * @var string|null + */ + protected $transactionName; + + /** + * Some context and extra data is passed into the handler as arrays of values. Do we send them as is + * (useful if we are using the API), or explode them for display on the NewRelic RPM website? + * + * @var bool + */ + protected $explodeArrays; + + /** + * {@inheritDoc} + * + * @param string|int $level The minimum logging level at which this handler will be triggered. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not. + * @param string|null $appName + * @param bool $explodeArrays + * @param string|null $transactionName + */ + public function __construct( + $level = Logger::ERROR, + bool $bubble = true, + ?string $appName = null, + bool $explodeArrays = false, + ?string $transactionName = null + ) { + parent::__construct($level, $bubble); + + $this->appName = $appName; + $this->explodeArrays = $explodeArrays; + $this->transactionName = $transactionName; + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if (!$this->isNewRelicEnabled()) { + throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler'); + } + + if ($appName = $this->getAppName($record['context'])) { + $this->setNewRelicAppName($appName); + } + + if ($transactionName = $this->getTransactionName($record['context'])) { + $this->setNewRelicTransactionName($transactionName); + unset($record['formatted']['context']['transaction_name']); + } + + if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { + newrelic_notice_error($record['message'], $record['context']['exception']); + unset($record['formatted']['context']['exception']); + } else { + newrelic_notice_error($record['message']); + } + + if (isset($record['formatted']['context']) && is_array($record['formatted']['context'])) { + foreach ($record['formatted']['context'] as $key => $parameter) { + if (is_array($parameter) && $this->explodeArrays) { + foreach ($parameter as $paramKey => $paramValue) { + $this->setNewRelicParameter('context_' . $key . '_' . $paramKey, $paramValue); + } + } else { + $this->setNewRelicParameter('context_' . $key, $parameter); + } + } + } + + if (isset($record['formatted']['extra']) && is_array($record['formatted']['extra'])) { + foreach ($record['formatted']['extra'] as $key => $parameter) { + if (is_array($parameter) && $this->explodeArrays) { + foreach ($parameter as $paramKey => $paramValue) { + $this->setNewRelicParameter('extra_' . $key . '_' . $paramKey, $paramValue); + } + } else { + $this->setNewRelicParameter('extra_' . $key, $parameter); + } + } + } + } + + /** + * Checks whether the NewRelic extension is enabled in the system. + * + * @return bool + */ + protected function isNewRelicEnabled(): bool + { + return extension_loaded('newrelic'); + } + + /** + * Returns the appname where this log should be sent. Each log can override the default appname, set in this + * handler's constructor, by providing the appname in it's context. + */ + protected function getAppName(array $context): ?string + { + if (isset($context['appname'])) { + return $context['appname']; + } + + return $this->appName; + } + + /** + * Returns the name of the current transaction. Each log can override the default transaction name, set in this + * handler's constructor, by providing the transaction_name in it's context + */ + protected function getTransactionName(array $context): ?string + { + if (isset($context['transaction_name'])) { + return $context['transaction_name']; + } + + return $this->transactionName; + } + + /** + * Sets the NewRelic application that should receive this log. + */ + protected function setNewRelicAppName(string $appName): void + { + newrelic_set_appname($appName); + } + + /** + * Overwrites the name of the current transaction + */ + protected function setNewRelicTransactionName(string $transactionName): void + { + newrelic_name_transaction($transactionName); + } + + /** + * @param string $key + * @param mixed $value + */ + protected function setNewRelicParameter(string $key, $value): void + { + if (null === $value || is_scalar($value)) { + newrelic_add_custom_parameter($key, $value); + } else { + newrelic_add_custom_parameter($key, Utils::jsonEncode($value, null, true)); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new NormalizerFormatter(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NoopHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NoopHandler.php new file mode 100644 index 0000000..8ee2b4c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NoopHandler.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * No-op + * + * This handler handles anything, but does nothing, and does not stop bubbling to the rest of the stack. + * This can be used for testing, or to disable a handler when overriding a configuration without + * influencing the rest of the stack. + * + * @author Roel Harbers + */ +class NoopHandler extends Handler +{ + /** + * {@inheritdoc} + */ + public function isHandling(array $record): bool + { + return true; + } + + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + return false; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php new file mode 100644 index 0000000..1f0078a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Blackhole + * + * Any record it can handle will be thrown away. This can be used + * to put on top of an existing stack to override it temporarily. + * + * @author Jordi Boggiano + */ +class NullHandler extends Handler +{ + /** + * @var int + */ + private $level; + + /** + * @param string|int $level The minimum logging level at which this handler will be triggered + */ + public function __construct($level = Logger::DEBUG) + { + $this->level = Logger::toMonologLevel($level); + } + + /** + * {@inheritdoc} + */ + public function isHandling(array $record): bool + { + return $record['level'] >= $this->level; + } + + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + return $record['level'] >= $this->level; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/OverflowHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/OverflowHandler.php new file mode 100644 index 0000000..4c30986 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/OverflowHandler.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; + +/** + * Handler to only pass log messages when a certain threshold of number of messages is reached. + * + * This can be useful in cases of processing a batch of data, but you're for example only interested + * in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right? + * + * Usage example: + * + * ``` + * $log = new Logger('application'); + * $handler = new SomeHandler(...) + * + * // Pass all warnings to the handler when more than 10 & all error messages when more then 5 + * $overflow = new OverflowHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]); + * + * $log->pushHandler($overflow); + *``` + * + * @author Kris Buist + */ +class OverflowHandler extends AbstractHandler implements FormattableHandlerInterface +{ + /** @var HandlerInterface */ + private $handler; + + /** @var int[] */ + private $thresholdMap = [ + Logger::DEBUG => 0, + Logger::INFO => 0, + Logger::NOTICE => 0, + Logger::WARNING => 0, + Logger::ERROR => 0, + Logger::CRITICAL => 0, + Logger::ALERT => 0, + Logger::EMERGENCY => 0, + ]; + + /** + * Buffer of all messages passed to the handler before the threshold was reached + * + * @var mixed[][] + */ + private $buffer = []; + + /** + * @param HandlerInterface $handler + * @param int[] $thresholdMap Dictionary of logger level => threshold + * @param int|string $level The minimum logging level at which this handler will be triggered + * @param bool $bubble + */ + public function __construct( + HandlerInterface $handler, + array $thresholdMap = [], + $level = Logger::DEBUG, + bool $bubble = true + ) { + $this->handler = $handler; + foreach ($thresholdMap as $thresholdLevel => $threshold) { + $this->thresholdMap[$thresholdLevel] = $threshold; + } + parent::__construct($level, $bubble); + } + + /** + * Handles a record. + * + * All records may be passed to this method, and the handler should discard + * those that it does not want to handle. + * + * The return value of this function controls the bubbling process of the handler stack. + * Unless the bubbling is interrupted (by returning true), the Logger class will keep on + * calling further handlers in the stack with a given log record. + * + * @param array $record The record to handle + * + * @return Boolean true means that this handler handled the record, and that bubbling is not permitted. + * false means the record was either not processed or that this handler allows bubbling. + */ + public function handle(array $record): bool + { + if ($record['level'] < $this->level) { + return false; + } + + $level = $record['level']; + + if (!isset($this->thresholdMap[$level])) { + $this->thresholdMap[$level] = 0; + } + + if ($this->thresholdMap[$level] > 0) { + // The overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1 + $this->thresholdMap[$level]--; + $this->buffer[$level][] = $record; + + return false === $this->bubble; + } + + if ($this->thresholdMap[$level] == 0) { + // This current message is breaking the threshold. Flush the buffer and continue handling the current record + foreach ($this->buffer[$level] ?? [] as $buffered) { + $this->handler->handle($buffered); + } + $this->thresholdMap[$level]--; + unset($this->buffer[$level]); + } + + $this->handler->handle($record); + + return false === $this->bubble; + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + $this->handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); + } + + /** + * {@inheritdoc} + */ + public function getFormatter(): FormatterInterface + { + if ($this->handler instanceof FormattableHandlerInterface) { + return $this->handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php new file mode 100644 index 0000000..94802d0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php @@ -0,0 +1,240 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; +use PhpConsole\Connector; +use PhpConsole\Handler as VendorPhpConsoleHandler; +use PhpConsole\Helper; + +/** + * Monolog handler for Google Chrome extension "PHP Console" + * + * Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely + * + * Usage: + * 1. Install Google Chrome extension https://chrome.google.com/webstore/detail/php-console/nfhmhhlpfleoednkpnnnkolmclajemef + * 2. See overview https://github.com/barbushin/php-console#overview + * 3. Install PHP Console library https://github.com/barbushin/php-console#installation + * 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png) + * + * $logger = new \Monolog\Logger('all', array(new \Monolog\Handler\PHPConsoleHandler())); + * \Monolog\ErrorHandler::register($logger); + * echo $undefinedVar; + * $logger->debug('SELECT * FROM users', array('db', 'time' => 0.012)); + * PC::debug($_SERVER); // PHP Console debugger for any type of vars + * + * @author Sergey Barbushin https://www.linkedin.com/in/barbushin + */ +class PHPConsoleHandler extends AbstractProcessingHandler +{ + private $options = [ + 'enabled' => true, // bool Is PHP Console server enabled + 'classesPartialsTraceIgnore' => ['Monolog\\'], // array Hide calls of classes started with... + 'debugTagsKeysInContext' => [0, 'tag'], // bool Is PHP Console server enabled + 'useOwnErrorsHandler' => false, // bool Enable errors handling + 'useOwnExceptionsHandler' => false, // bool Enable exceptions handling + 'sourcesBasePath' => null, // string Base path of all project sources to strip in errors source paths + 'registerHelper' => true, // bool Register PhpConsole\Helper that allows short debug calls like PC::debug($var, 'ta.g.s') + 'serverEncoding' => null, // string|null Server internal encoding + 'headersLimit' => null, // int|null Set headers size limit for your web-server + 'password' => null, // string|null Protect PHP Console connection by password + 'enableSslOnlyMode' => false, // bool Force connection by SSL for clients with PHP Console installed + 'ipMasks' => [], // array Set IP masks of clients that will be allowed to connect to PHP Console: array('192.168.*.*', '127.0.0.1') + 'enableEvalListener' => false, // bool Enable eval request to be handled by eval dispatcher(if enabled, 'password' option is also required) + 'dumperDetectCallbacks' => false, // bool Convert callback items in dumper vars to (callback SomeClass::someMethod) strings + 'dumperLevelLimit' => 5, // int Maximum dumped vars array or object nested dump level + 'dumperItemsCountLimit' => 100, // int Maximum dumped var same level array items or object properties number + 'dumperItemSizeLimit' => 5000, // int Maximum length of any string or dumped array item + 'dumperDumpSizeLimit' => 500000, // int Maximum approximate size of dumped vars result formatted in JSON + 'detectDumpTraceAndSource' => false, // bool Autodetect and append trace data to debug + 'dataStorage' => null, // \PhpConsole\Storage|null Fixes problem with custom $_SESSION handler(see http://goo.gl/Ne8juJ) + ]; + + /** @var Connector */ + private $connector; + + /** + * @param array $options See \Monolog\Handler\PHPConsoleHandler::$options for more details + * @param Connector|null $connector Instance of \PhpConsole\Connector class (optional) + * @param string|int $level The minimum logging level at which this handler will be triggered. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not. + * @throws \RuntimeException + */ + public function __construct(array $options = [], ?Connector $connector = null, $level = Logger::DEBUG, bool $bubble = true) + { + if (!class_exists('PhpConsole\Connector')) { + throw new \RuntimeException('PHP Console library not found. See https://github.com/barbushin/php-console#installation'); + } + parent::__construct($level, $bubble); + $this->options = $this->initOptions($options); + $this->connector = $this->initConnector($connector); + } + + private function initOptions(array $options): array + { + $wrongOptions = array_diff(array_keys($options), array_keys($this->options)); + if ($wrongOptions) { + throw new \RuntimeException('Unknown options: ' . implode(', ', $wrongOptions)); + } + + return array_replace($this->options, $options); + } + + private function initConnector(?Connector $connector = null): Connector + { + if (!$connector) { + if ($this->options['dataStorage']) { + Connector::setPostponeStorage($this->options['dataStorage']); + } + $connector = Connector::getInstance(); + } + + if ($this->options['registerHelper'] && !Helper::isRegistered()) { + Helper::register(); + } + + if ($this->options['enabled'] && $connector->isActiveClient()) { + if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) { + $handler = VendorPhpConsoleHandler::getInstance(); + $handler->setHandleErrors($this->options['useOwnErrorsHandler']); + $handler->setHandleExceptions($this->options['useOwnExceptionsHandler']); + $handler->start(); + } + if ($this->options['sourcesBasePath']) { + $connector->setSourcesBasePath($this->options['sourcesBasePath']); + } + if ($this->options['serverEncoding']) { + $connector->setServerEncoding($this->options['serverEncoding']); + } + if ($this->options['password']) { + $connector->setPassword($this->options['password']); + } + if ($this->options['enableSslOnlyMode']) { + $connector->enableSslOnlyMode(); + } + if ($this->options['ipMasks']) { + $connector->setAllowedIpMasks($this->options['ipMasks']); + } + if ($this->options['headersLimit']) { + $connector->setHeadersLimit($this->options['headersLimit']); + } + if ($this->options['detectDumpTraceAndSource']) { + $connector->getDebugDispatcher()->detectTraceAndSource = true; + } + $dumper = $connector->getDumper(); + $dumper->levelLimit = $this->options['dumperLevelLimit']; + $dumper->itemsCountLimit = $this->options['dumperItemsCountLimit']; + $dumper->itemSizeLimit = $this->options['dumperItemSizeLimit']; + $dumper->dumpSizeLimit = $this->options['dumperDumpSizeLimit']; + $dumper->detectCallbacks = $this->options['dumperDetectCallbacks']; + if ($this->options['enableEvalListener']) { + $connector->startEvalRequestsListener(); + } + } + + return $connector; + } + + public function getConnector(): Connector + { + return $this->connector; + } + + public function getOptions(): array + { + return $this->options; + } + + public function handle(array $record): bool + { + if ($this->options['enabled'] && $this->connector->isActiveClient()) { + return parent::handle($record); + } + + return !$this->bubble; + } + + /** + * Writes the record down to the log of the implementing handler + */ + protected function write(array $record): void + { + if ($record['level'] < Logger::NOTICE) { + $this->handleDebugRecord($record); + } elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { + $this->handleExceptionRecord($record); + } else { + $this->handleErrorRecord($record); + } + } + + private function handleDebugRecord(array $record): void + { + $tags = $this->getRecordTags($record); + $message = $record['message']; + if ($record['context']) { + $message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($record['context'])), null, true); + } + $this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']); + } + + private function handleExceptionRecord(array $record): void + { + $this->connector->getErrorsDispatcher()->dispatchException($record['context']['exception']); + } + + private function handleErrorRecord(array $record): void + { + $context = $record['context']; + + $this->connector->getErrorsDispatcher()->dispatchError( + $context['code'] ?? null, + $context['message'] ?? $record['message'], + $context['file'] ?? null, + $context['line'] ?? null, + $this->options['classesPartialsTraceIgnore'] + ); + } + + private function getRecordTags(array &$record) + { + $tags = null; + if (!empty($record['context'])) { + $context = & $record['context']; + foreach ($this->options['debugTagsKeysInContext'] as $key) { + if (!empty($context[$key])) { + $tags = $context[$key]; + if ($key === 0) { + array_shift($context); + } else { + unset($context[$key]); + } + break; + } + } + } + + return $tags ?: strtolower($record['level_name']); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter('%message%'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php new file mode 100644 index 0000000..36e30b8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Stores to STDIN of any process, specified by a command. + * + * Usage example: + *
+ * $log = new Logger('myLogger');
+ * $log->pushHandler(new ProcessHandler('/usr/bin/php /var/www/monolog/someScript.php'));
+ * 
+ * + * @author Kolja Zuelsdorf + */ +class ProcessHandler extends AbstractProcessingHandler +{ + /** + * Holds the process to receive data on its STDIN. + * + * @var resource|bool|null + */ + private $process; + + /** + * @var string + */ + private $command; + + /** + * @var string|null + */ + private $cwd; + + /** + * @var array + */ + private $pipes = []; + + /** + * @var array + */ + protected const DESCRIPTOR_SPEC = [ + 0 => ['pipe', 'r'], // STDIN is a pipe that the child will read from + 1 => ['pipe', 'w'], // STDOUT is a pipe that the child will write to + 2 => ['pipe', 'w'], // STDERR is a pipe to catch the any errors + ]; + + /** + * @param string $command Command for the process to start. Absolute paths are recommended, + * especially if you do not use the $cwd parameter. + * @param string|int $level The minimum logging level at which this handler will be triggered. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not. + * @param string|null $cwd "Current working directory" (CWD) for the process to be executed in. + * @throws \InvalidArgumentException + */ + public function __construct(string $command, $level = Logger::DEBUG, bool $bubble = true, ?string $cwd = null) + { + if ($command === '') { + throw new \InvalidArgumentException('The command argument must be a non-empty string.'); + } + if ($cwd === '') { + throw new \InvalidArgumentException('The optional CWD argument must be a non-empty string or null.'); + } + + parent::__construct($level, $bubble); + + $this->command = $command; + $this->cwd = $cwd; + } + + /** + * Writes the record down to the log of the implementing handler + * + * @throws \UnexpectedValueException + */ + protected function write(array $record): void + { + $this->ensureProcessIsStarted(); + + $this->writeProcessInput($record['formatted']); + + $errors = $this->readProcessErrors(); + if (empty($errors) === false) { + throw new \UnexpectedValueException(sprintf('Errors while writing to process: %s', $errors)); + } + } + + /** + * Makes sure that the process is actually started, and if not, starts it, + * assigns the stream pipes, and handles startup errors, if any. + */ + private function ensureProcessIsStarted(): void + { + if (is_resource($this->process) === false) { + $this->startProcess(); + + $this->handleStartupErrors(); + } + } + + /** + * Starts the actual process and sets all streams to non-blocking. + */ + private function startProcess(): void + { + $this->process = proc_open($this->command, static::DESCRIPTOR_SPEC, $this->pipes, $this->cwd); + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, false); + } + } + + /** + * Selects the STDERR stream, handles upcoming startup errors, and throws an exception, if any. + * + * @throws \UnexpectedValueException + */ + private function handleStartupErrors(): void + { + $selected = $this->selectErrorStream(); + if (false === $selected) { + throw new \UnexpectedValueException('Something went wrong while selecting a stream.'); + } + + $errors = $this->readProcessErrors(); + + if (is_resource($this->process) === false || empty($errors) === false) { + throw new \UnexpectedValueException( + sprintf('The process "%s" could not be opened: ' . $errors, $this->command) + ); + } + } + + /** + * Selects the STDERR stream. + * + * @return int|bool + */ + protected function selectErrorStream() + { + $empty = []; + $errorPipes = [$this->pipes[2]]; + + return stream_select($errorPipes, $empty, $empty, 1); + } + + /** + * Reads the errors of the process, if there are any. + * + * @codeCoverageIgnore + * @return string Empty string if there are no errors. + */ + protected function readProcessErrors(): string + { + return stream_get_contents($this->pipes[2]); + } + + /** + * Writes to the input stream of the opened process. + * + * @codeCoverageIgnore + */ + protected function writeProcessInput(string $string): void + { + fwrite($this->pipes[0], $string); + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + if (is_resource($this->process)) { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + proc_close($this->process); + $this->process = null; + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.php new file mode 100644 index 0000000..41b52ce --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Processor\ProcessorInterface; + +/** + * Interface to describe loggers that have processors + * + * @author Jordi Boggiano + */ +interface ProcessableHandlerInterface +{ + /** + * Adds a processor in the stack. + * + * @psalm-param ProcessorInterface|callable(array): array $callback + * + * @param ProcessorInterface|callable $callback + * @return HandlerInterface self + */ + public function pushProcessor(callable $callback): HandlerInterface; + + /** + * Removes the processor on top of the stack and returns it. + * + * @psalm-return callable(array): array + * + * @throws \LogicException In case the processor stack is empty + * @return callable + */ + public function popProcessor(): callable; +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.php new file mode 100644 index 0000000..71d767b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\ResettableInterface; + +/** + * Helper trait for implementing ProcessableInterface + * + * @author Jordi Boggiano + */ +trait ProcessableHandlerTrait +{ + /** + * @var callable[] + */ + protected $processors = []; + + /** + * {@inheritdoc} + */ + public function pushProcessor(callable $callback): HandlerInterface + { + array_unshift($this->processors, $callback); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function popProcessor(): callable + { + if (!$this->processors) { + throw new \LogicException('You tried to pop from an empty processor stack.'); + } + + return array_shift($this->processors); + } + + /** + * Processes a record. + */ + protected function processRecord(array $record): array + { + foreach ($this->processors as $processor) { + $record = $processor($record); + } + + return $record; + } + + protected function resetProcessors(): void + { + foreach ($this->processors as $processor) { + if ($processor instanceof ResettableInterface) { + $processor->reset(); + } + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php new file mode 100644 index 0000000..cba96a5 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Psr\Log\LoggerInterface; +use Monolog\Formatter\FormatterInterface; + +/** + * Proxies log messages to an existing PSR-3 compliant logger. + * + * If a formatter is configured, the formatter's output MUST be a string and the + * formatted message will be fed to the wrapped PSR logger instead of the original + * log record's message. + * + * @author Michael Moussa + */ +class PsrHandler extends AbstractHandler implements FormattableHandlerInterface +{ + /** + * PSR-3 compliant logger + * + * @var LoggerInterface + */ + protected $logger; + + /** + * @var FormatterInterface|null + */ + protected $formatter; + + /** + * @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->logger = $logger; + } + + /** + * {@inheritDoc} + */ + public function handle(array $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + if ($this->formatter) { + $formatted = $this->formatter->format($record); + $this->logger->log(strtolower($record['level_name']), (string) $formatted, $record['context']); + } else { + $this->logger->log(strtolower($record['level_name']), $record['message'], $record['context']); + } + + return false === $this->bubble; + } + + /** + * Sets the formatter. + * + * @param FormatterInterface $formatter + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $this->formatter = $formatter; + + return $this; + } + + /** + * Gets the formatter. + * + * @return FormatterInterface + */ + public function getFormatter(): FormatterInterface + { + if (!$this->formatter) { + throw new \LogicException('No formatter has been set and this handler does not have a default formatter'); + } + + return $this->formatter; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php new file mode 100644 index 0000000..3bb99c7 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php @@ -0,0 +1,203 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; + +/** + * Sends notifications through the pushover api to mobile phones + * + * @author Sebastian Göttschkes + * @see https://www.pushover.net/api + */ +class PushoverHandler extends SocketHandler +{ + private $token; + private $users; + private $title; + private $user; + private $retry; + private $expire; + + private $highPriorityLevel; + private $emergencyLevel; + private $useFormattedMessage = false; + + /** + * All parameters that can be sent to Pushover + * @see https://pushover.net/api + * @var array + */ + private $parameterNames = [ + 'token' => true, + 'user' => true, + 'message' => true, + 'device' => true, + 'title' => true, + 'url' => true, + 'url_title' => true, + 'priority' => true, + 'timestamp' => true, + 'sound' => true, + 'retry' => true, + 'expire' => true, + 'callback' => true, + ]; + + /** + * Sounds the api supports by default + * @see https://pushover.net/api#sounds + * @var array + */ + private $sounds = [ + 'pushover', 'bike', 'bugle', 'cashregister', 'classical', 'cosmic', 'falling', 'gamelan', 'incoming', + 'intermission', 'magic', 'mechanical', 'pianobar', 'siren', 'spacealarm', 'tugboat', 'alien', 'climb', + 'persistent', 'echo', 'updown', 'none', + ]; + + /** + * @param string $token Pushover api token + * @param string|array $users Pushover user id or array of ids the message will be sent to + * @param string|null $title Title sent to the Pushover API + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $useSSL Whether to connect via SSL. Required when pushing messages to users that are not + * the pushover.net app owner. OpenSSL is required for this option. + * @param string|int $highPriorityLevel The minimum logging level at which this handler will start + * sending "high priority" requests to the Pushover API + * @param string|int $emergencyLevel The minimum logging level at which this handler will start + * sending "emergency" requests to the Pushover API + * @param int $retry The retry parameter specifies how often (in seconds) the Pushover servers will + * send the same notification to the user. + * @param int $expire The expire parameter specifies how many seconds your notification will continue + * to be retried for (every retry seconds). + */ + public function __construct( + string $token, + $users, + ?string $title = null, + $level = Logger::CRITICAL, + bool $bubble = true, + bool $useSSL = true, + $highPriorityLevel = Logger::CRITICAL, + $emergencyLevel = Logger::EMERGENCY, + int $retry = 30, + int $expire = 25200 + ) { + $connectionString = $useSSL ? 'ssl://api.pushover.net:443' : 'api.pushover.net:80'; + parent::__construct($connectionString, $level, $bubble); + + $this->token = $token; + $this->users = (array) $users; + $this->title = $title ?: gethostname(); + $this->highPriorityLevel = Logger::toMonologLevel($highPriorityLevel); + $this->emergencyLevel = Logger::toMonologLevel($emergencyLevel); + $this->retry = $retry; + $this->expire = $expire; + } + + protected function generateDataStream(array $record): string + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + private function buildContent(array $record): string + { + // Pushover has a limit of 512 characters on title and message combined. + $maxMessageLength = 512 - strlen($this->title); + + $message = ($this->useFormattedMessage) ? $record['formatted'] : $record['message']; + $message = Utils::substr($message, 0, $maxMessageLength); + + $timestamp = $record['datetime']->getTimestamp(); + + $dataArray = [ + 'token' => $this->token, + 'user' => $this->user, + 'message' => $message, + 'title' => $this->title, + 'timestamp' => $timestamp, + ]; + + if (isset($record['level']) && $record['level'] >= $this->emergencyLevel) { + $dataArray['priority'] = 2; + $dataArray['retry'] = $this->retry; + $dataArray['expire'] = $this->expire; + } elseif (isset($record['level']) && $record['level'] >= $this->highPriorityLevel) { + $dataArray['priority'] = 1; + } + + // First determine the available parameters + $context = array_intersect_key($record['context'], $this->parameterNames); + $extra = array_intersect_key($record['extra'], $this->parameterNames); + + // Least important info should be merged with subsequent info + $dataArray = array_merge($extra, $context, $dataArray); + + // Only pass sounds that are supported by the API + if (isset($dataArray['sound']) && !in_array($dataArray['sound'], $this->sounds)) { + unset($dataArray['sound']); + } + + return http_build_query($dataArray); + } + + private function buildHeader(string $content): string + { + $header = "POST /1/messages.json HTTP/1.1\r\n"; + $header .= "Host: api.pushover.net\r\n"; + $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } + + protected function write(array $record): void + { + foreach ($this->users as $user) { + $this->user = $user; + + parent::write($record); + $this->closeSocket(); + } + + $this->user = null; + } + + public function setHighPriorityLevel($value): self + { + $this->highPriorityLevel = Logger::toMonologLevel($value); + + return $this; + } + + public function setEmergencyLevel($value): self + { + $this->emergencyLevel = Logger::toMonologLevel($value); + + return $this; + } + + /** + * Use the formatted message? + */ + public function useFormattedMessage(bool $value): self + { + $this->useFormattedMessage = $value; + + return $this; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php new file mode 100644 index 0000000..46482b6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; + +/** + * Logs to a Redis key using rpush + * + * usage example: + * + * $log = new Logger('application'); + * $redis = new RedisHandler(new Predis\Client("tcp://localhost:6379"), "logs", "prod"); + * $log->pushHandler($redis); + * + * @author Thomas Tourlourat + */ +class RedisHandler extends AbstractProcessingHandler +{ + private $redisClient; + private $redisKey; + protected $capSize; + + /** + * @param \Predis\Client|\Redis $redis The redis instance + * @param string $key The key name to push records to + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param int $capSize Number of entries to limit list size to, 0 = unlimited + */ + public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true, int $capSize = 0) + { + if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) { + throw new \InvalidArgumentException('Predis\Client or Redis instance required'); + } + + $this->redisClient = $redis; + $this->redisKey = $key; + $this->capSize = $capSize; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + if ($this->capSize) { + $this->writeCapped($record); + } else { + $this->redisClient->rpush($this->redisKey, $record["formatted"]); + } + } + + /** + * Write and cap the collection + * Writes the record to the redis list and caps its + */ + protected function writeCapped(array $record): void + { + if ($this->redisClient instanceof \Redis) { + $mode = defined('\Redis::MULTI') ? \Redis::MULTI : 1; + $this->redisClient->multi($mode) + ->rpush($this->redisKey, $record["formatted"]) + ->ltrim($this->redisKey, -$this->capSize, -1) + ->exec(); + } else { + $redisKey = $this->redisKey; + $capSize = $this->capSize; + $this->redisClient->transaction(function ($tx) use ($record, $redisKey, $capSize) { + $tx->rpush($redisKey, $record["formatted"]); + $tx->ltrim($redisKey, -$capSize, -1); + }); + } + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RedisPubSubHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RedisPubSubHandler.php new file mode 100644 index 0000000..c9b1319 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RedisPubSubHandler.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\LineFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; + +/** + * Sends the message to a Redis Pub/Sub channel using PUBLISH + * + * usage example: + * + * $log = new Logger('application'); + * $redis = new RedisPubSubHandler(new Predis\Client("tcp://localhost:6379"), "logs", Logger::WARNING); + * $log->pushHandler($redis); + * + * @author Gaëtan Faugère + */ +class RedisPubSubHandler extends AbstractProcessingHandler +{ + private $redisClient; + private $channelKey; + + /** + * @param \Predis\Client|\Redis $redis The redis instance + * @param string $key The channel key to publish records to + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true) + { + if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) { + throw new \InvalidArgumentException('Predis\Client or Redis instance required'); + } + + $this->redisClient = $redis; + $this->channelKey = $key; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->redisClient->publish($this->channelKey, $record["formatted"]); + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new LineFormatter(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.php new file mode 100644 index 0000000..979d651 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Rollbar\RollbarLogger; +use Throwable; +use Monolog\Logger; + +/** + * Sends errors to Rollbar + * + * If the context data contains a `payload` key, that is used as an array + * of payload options to RollbarLogger's log method. + * + * Rollbar's context info will contain the context + extra keys from the log record + * merged, and then on top of that a few keys: + * + * - level (rollbar level name) + * - monolog_level (monolog level name, raw level, as rollbar only has 5 but monolog 8) + * - channel + * - datetime (unix timestamp) + * + * @author Paul Statezny + */ +class RollbarHandler extends AbstractProcessingHandler +{ + /** + * @var RollbarLogger + */ + protected $rollbarLogger; + + protected $levelMap = [ + Logger::DEBUG => 'debug', + Logger::INFO => 'info', + Logger::NOTICE => 'info', + Logger::WARNING => 'warning', + Logger::ERROR => 'error', + Logger::CRITICAL => 'critical', + Logger::ALERT => 'critical', + Logger::EMERGENCY => 'critical', + ]; + + /** + * Records whether any log records have been added since the last flush of the rollbar notifier + * + * @var bool + */ + private $hasRecords = false; + + protected $initialized = false; + + /** + * @param RollbarLogger $rollbarLogger RollbarLogger object constructed with valid token + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(RollbarLogger $rollbarLogger, $level = Logger::ERROR, bool $bubble = true) + { + $this->rollbarLogger = $rollbarLogger; + + parent::__construct($level, $bubble); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + if (!$this->initialized) { + // __destructor() doesn't get called on Fatal errors + register_shutdown_function(array($this, 'close')); + $this->initialized = true; + } + + $context = $record['context']; + $context = array_merge($context, $record['extra'], [ + 'level' => $this->levelMap[$record['level']], + 'monolog_level' => $record['level_name'], + 'channel' => $record['channel'], + 'datetime' => $record['datetime']->format('U'), + ]); + + if (isset($context['exception']) && $context['exception'] instanceof Throwable) { + $exception = $context['exception']; + unset($context['exception']); + $toLog = $exception; + } else { + $toLog = $record['message']; + } + + $this->rollbarLogger->log($context['level'], $toLog, $context); + + $this->hasRecords = true; + } + + public function flush(): void + { + if ($this->hasRecords) { + $this->rollbarLogger->flush(); + $this->hasRecords = false; + } + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + $this->flush(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->flush(); + + parent::reset(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php new file mode 100644 index 0000000..f61e5eb --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use InvalidArgumentException; +use Monolog\Logger; +use Monolog\Utils; + +/** + * Stores logs to files that are rotated every day and a limited number of files are kept. + * + * This rotation is only intended to be used as a workaround. Using logrotate to + * handle the rotation is strongly encouraged when you can use it. + * + * @author Christophe Coevoet + * @author Jordi Boggiano + */ +class RotatingFileHandler extends StreamHandler +{ + public const FILE_PER_DAY = 'Y-m-d'; + public const FILE_PER_MONTH = 'Y-m'; + public const FILE_PER_YEAR = 'Y'; + + protected $filename; + protected $maxFiles; + protected $mustRotate; + protected $nextRotation; + protected $filenameFormat; + protected $dateFormat; + + /** + * @param string $filename + * @param int $maxFiles The maximal amount of files to keep (0 means unlimited) + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) + * @param bool $useLocking Try to lock log file before doing any writes + */ + public function __construct(string $filename, int $maxFiles = 0, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false) + { + $this->filename = Utils::canonicalizePath($filename); + $this->maxFiles = $maxFiles; + $this->nextRotation = new \DateTimeImmutable('tomorrow'); + $this->filenameFormat = '{filename}-{date}'; + $this->dateFormat = static::FILE_PER_DAY; + + parent::__construct($this->getTimedFilename(), $level, $bubble, $filePermission, $useLocking); + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + parent::close(); + + if (true === $this->mustRotate) { + $this->rotate(); + } + } + + /** + * {@inheritdoc} + */ + public function reset() + { + parent::reset(); + + if (true === $this->mustRotate) { + $this->rotate(); + } + } + + public function setFilenameFormat(string $filenameFormat, string $dateFormat): self + { + if (!preg_match('{^[Yy](([/_.-]?m)([/_.-]?d)?)?$}', $dateFormat)) { + throw new InvalidArgumentException( + 'Invalid date format - format must be one of '. + 'RotatingFileHandler::FILE_PER_DAY ("Y-m-d"), RotatingFileHandler::FILE_PER_MONTH ("Y-m") '. + 'or RotatingFileHandler::FILE_PER_YEAR ("Y"), or you can set one of the '. + 'date formats using slashes, underscores and/or dots instead of dashes.' + ); + } + if (substr_count($filenameFormat, '{date}') === 0) { + throw new InvalidArgumentException( + 'Invalid filename format - format must contain at least `{date}`, because otherwise rotating is impossible.' + ); + } + $this->filenameFormat = $filenameFormat; + $this->dateFormat = $dateFormat; + $this->url = $this->getTimedFilename(); + $this->close(); + + return $this; + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + // on the first record written, if the log is new, we should rotate (once per day) + if (null === $this->mustRotate) { + $this->mustRotate = !file_exists($this->url); + } + + if ($this->nextRotation <= $record['datetime']) { + $this->mustRotate = true; + $this->close(); + } + + parent::write($record); + } + + /** + * Rotates the files. + */ + protected function rotate(): void + { + // update filename + $this->url = $this->getTimedFilename(); + $this->nextRotation = new \DateTimeImmutable('tomorrow'); + + // skip GC of old logs if files are unlimited + if (0 === $this->maxFiles) { + return; + } + + $logFiles = glob($this->getGlobPattern()); + if ($this->maxFiles >= count($logFiles)) { + // no files to remove + return; + } + + // Sorting the files by name to remove the older ones + usort($logFiles, function ($a, $b) { + return strcmp($b, $a); + }); + + foreach (array_slice($logFiles, $this->maxFiles) as $file) { + if (is_writable($file)) { + // suppress errors here as unlink() might fail if two processes + // are cleaning up/rotating at the same time + set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool { + return false; + }); + unlink($file); + restore_error_handler(); + } + } + + $this->mustRotate = false; + } + + protected function getTimedFilename(): string + { + $fileInfo = pathinfo($this->filename); + $timedFilename = str_replace( + ['{filename}', '{date}'], + [$fileInfo['filename'], date($this->dateFormat)], + $fileInfo['dirname'] . '/' . $this->filenameFormat + ); + + if (!empty($fileInfo['extension'])) { + $timedFilename .= '.'.$fileInfo['extension']; + } + + return $timedFilename; + } + + protected function getGlobPattern(): string + { + $fileInfo = pathinfo($this->filename); + $glob = str_replace( + ['{filename}', '{date}'], + [$fileInfo['filename'], '[0-9][0-9][0-9][0-9]*'], + $fileInfo['dirname'] . '/' . $this->filenameFormat + ); + if (!empty($fileInfo['extension'])) { + $glob .= '.'.$fileInfo['extension']; + } + + return $glob; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php new file mode 100644 index 0000000..4c33d4a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; + +/** + * Sampling handler + * + * A sampled event stream can be useful for logging high frequency events in + * a production environment where you only need an idea of what is happening + * and are not concerned with capturing every occurrence. Since the decision to + * handle or not handle a particular event is determined randomly, the + * resulting sampled log is not guaranteed to contain 1/N of the events that + * occurred in the application, but based on the Law of large numbers, it will + * tend to be close to this ratio with a large number of attempts. + * + * @author Bryan Davis + * @author Kunal Mehta + */ +class SamplingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface +{ + use ProcessableHandlerTrait; + + /** + * @var callable|HandlerInterface $handler + */ + protected $handler; + + /** + * @var int $factor + */ + protected $factor; + + /** + * @psalm-param HandlerInterface|callable(array, HandlerInterface): HandlerInterface $handler + * + * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $samplingHandler). + * @param int $factor Sample factor (e.g. 10 means every ~10th record is sampled) + */ + public function __construct($handler, int $factor) + { + parent::__construct(); + $this->handler = $handler; + $this->factor = $factor; + + if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { + throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); + } + } + + public function isHandling(array $record): bool + { + return $this->getHandler($record)->isHandling($record); + } + + public function handle(array $record): bool + { + if ($this->isHandling($record) && mt_rand(1, $this->factor) === 1) { + if ($this->processors) { + $record = $this->processRecord($record); + } + + $this->getHandler($record)->handle($record); + } + + return false === $this->bubble; + } + + /** + * Return the nested handler + * + * If the handler was provided as a factory callable, this will trigger the handler's instantiation. + * + * @return HandlerInterface + */ + public function getHandler(array $record = null) + { + if (!$this->handler instanceof HandlerInterface) { + $this->handler = ($this->handler)($record, $this); + if (!$this->handler instanceof HandlerInterface) { + throw new \RuntimeException("The factory callable should return a HandlerInterface"); + } + } + + return $this->handler; + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); + + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } + + /** + * {@inheritdoc} + */ + public function getFormatter(): FormatterInterface + { + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SendGridHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SendGridHandler.php new file mode 100644 index 0000000..e774d30 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SendGridHandler.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * SendGridrHandler uses the SendGrid API v2 function to send Log emails, more information in https://sendgrid.com/docs/API_Reference/Web_API/mail.html + * + * @author Ricardo Fontanelli + */ +class SendGridHandler extends MailHandler +{ + /** + * The SendGrid API User + * @var string + */ + protected $apiUser; + + /** + * The SendGrid API Key + * @var string + */ + protected $apiKey; + + /** + * The email addresses to which the message will be sent + * @var string + */ + protected $from; + + /** + * The email addresses to which the message will be sent + * @var array + */ + protected $to; + + /** + * The subject of the email + * @var string + */ + protected $subject; + + /** + * @param string $apiUser The SendGrid API User + * @param string $apiKey The SendGrid API Key + * @param string $from The sender of the email + * @param string|array $to The recipients of the email + * @param string $subject The subject of the mail + * @param int|string $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(string $apiUser, string $apiKey, string $from, $to, string $subject, $level = Logger::ERROR, bool $bubble = true) + { + parent::__construct($level, $bubble); + $this->apiUser = $apiUser; + $this->apiKey = $apiKey; + $this->from = $from; + $this->to = (array) $to; + $this->subject = $subject; + } + + /** + * {@inheritdoc} + */ + protected function send(string $content, array $records): void + { + $message = []; + $message['api_user'] = $this->apiUser; + $message['api_key'] = $this->apiKey; + $message['from'] = $this->from; + foreach ($this->to as $recipient) { + $message['to[]'] = $recipient; + } + $message['subject'] = $this->subject; + $message['date'] = date('r'); + + if ($this->isHtmlBody($content)) { + $message['html'] = $content; + } else { + $message['text'] = $content; + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, 'https://api.sendgrid.com/api/mail.send.json'); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($message)); + Curl\Util::execute($ch, 2); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php new file mode 100644 index 0000000..6658d0d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php @@ -0,0 +1,356 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\Slack; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Formatter\FormatterInterface; + +/** + * Slack record utility helping to log to Slack webhooks or API. + * + * @author Greg Kedzierski + * @author Haralan Dobrev + * @see https://api.slack.com/incoming-webhooks + * @see https://api.slack.com/docs/message-attachments + */ +class SlackRecord +{ + public const COLOR_DANGER = 'danger'; + + public const COLOR_WARNING = 'warning'; + + public const COLOR_GOOD = 'good'; + + public const COLOR_DEFAULT = '#e3e4e6'; + + /** + * Slack channel (encoded ID or name) + * @var string|null + */ + private $channel; + + /** + * Name of a bot + * @var string|null + */ + private $username; + + /** + * User icon e.g. 'ghost', 'http://example.com/user.png' + * @var string|null + */ + private $userIcon; + + /** + * Whether the message should be added to Slack as attachment (plain text otherwise) + * @var bool + */ + private $useAttachment; + + /** + * Whether the the context/extra messages added to Slack as attachments are in a short style + * @var bool + */ + private $useShortAttachment; + + /** + * Whether the attachment should include context and extra data + * @var bool + */ + private $includeContextAndExtra; + + /** + * Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] + * @var array + */ + private $excludeFields; + + /** + * @var ?FormatterInterface + */ + private $formatter; + + /** + * @var NormalizerFormatter + */ + private $normalizerFormatter; + + public function __construct( + ?string $channel = null, + ?string $username = null, + bool $useAttachment = true, + ?string $userIcon = null, + bool $useShortAttachment = false, + bool $includeContextAndExtra = false, + array $excludeFields = array(), + FormatterInterface $formatter = null + ) { + $this + ->setChannel($channel) + ->setUsername($username) + ->useAttachment($useAttachment) + ->setUserIcon($userIcon) + ->useShortAttachment($useShortAttachment) + ->includeContextAndExtra($includeContextAndExtra) + ->excludeFields($excludeFields) + ->setFormatter($formatter); + + if ($this->includeContextAndExtra) { + $this->normalizerFormatter = new NormalizerFormatter(); + } + } + + /** + * Returns required data in format that Slack + * is expecting. + */ + public function getSlackData(array $record): array + { + $dataArray = array(); + $record = $this->removeExcludedFields($record); + + if ($this->username) { + $dataArray['username'] = $this->username; + } + + if ($this->channel) { + $dataArray['channel'] = $this->channel; + } + + if ($this->formatter && !$this->useAttachment) { + $message = $this->formatter->format($record); + } else { + $message = $record['message']; + } + + if ($this->useAttachment) { + $attachment = array( + 'fallback' => $message, + 'text' => $message, + 'color' => $this->getAttachmentColor($record['level']), + 'fields' => array(), + 'mrkdwn_in' => array('fields'), + 'ts' => $record['datetime']->getTimestamp(), + ); + + if ($this->useShortAttachment) { + $attachment['title'] = $record['level_name']; + } else { + $attachment['title'] = 'Message'; + $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name']); + } + + if ($this->includeContextAndExtra) { + foreach (array('extra', 'context') as $key) { + if (empty($record[$key])) { + continue; + } + + if ($this->useShortAttachment) { + $attachment['fields'][] = $this->generateAttachmentField( + (string) $key, + $record[$key] + ); + } else { + // Add all extra fields as individual fields in attachment + $attachment['fields'] = array_merge( + $attachment['fields'], + $this->generateAttachmentFields($record[$key]) + ); + } + } + } + + $dataArray['attachments'] = array($attachment); + } else { + $dataArray['text'] = $message; + } + + if ($this->userIcon) { + if (filter_var($this->userIcon, FILTER_VALIDATE_URL)) { + $dataArray['icon_url'] = $this->userIcon; + } else { + $dataArray['icon_emoji'] = ":{$this->userIcon}:"; + } + } + + return $dataArray; + } + + /** + * Returns a Slack message attachment color associated with + * provided level. + */ + public function getAttachmentColor(int $level): string + { + switch (true) { + case $level >= Logger::ERROR: + return static::COLOR_DANGER; + case $level >= Logger::WARNING: + return static::COLOR_WARNING; + case $level >= Logger::INFO: + return static::COLOR_GOOD; + default: + return static::COLOR_DEFAULT; + } + } + + /** + * Stringifies an array of key/value pairs to be used in attachment fields + */ + public function stringify(array $fields): string + { + $normalized = $this->normalizerFormatter->format($fields); + + $hasSecondDimension = count(array_filter($normalized, 'is_array')); + $hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric')); + + return $hasSecondDimension || $hasNonNumericKeys + ? Utils::jsonEncode($normalized, JSON_PRETTY_PRINT|Utils::DEFAULT_JSON_FLAGS) + : Utils::jsonEncode($normalized, Utils::DEFAULT_JSON_FLAGS); + } + + /** + * Channel used by the bot when posting + * + * @param ?string $channel + * + * @return static + */ + public function setChannel(?string $channel = null): self + { + $this->channel = $channel; + + return $this; + } + + /** + * Username used by the bot when posting + * + * @param ?string $username + * + * @return static + */ + public function setUsername(?string $username = null): self + { + $this->username = $username; + + return $this; + } + + public function useAttachment(bool $useAttachment = true): self + { + $this->useAttachment = $useAttachment; + + return $this; + } + + public function setUserIcon(?string $userIcon = null): self + { + $this->userIcon = $userIcon; + + if (\is_string($userIcon)) { + $this->userIcon = trim($userIcon, ':'); + } + + return $this; + } + + public function useShortAttachment(bool $useShortAttachment = false): self + { + $this->useShortAttachment = $useShortAttachment; + + return $this; + } + + public function includeContextAndExtra(bool $includeContextAndExtra = false): self + { + $this->includeContextAndExtra = $includeContextAndExtra; + + if ($this->includeContextAndExtra) { + $this->normalizerFormatter = new NormalizerFormatter(); + } + + return $this; + } + + public function excludeFields(array $excludeFields = []): self + { + $this->excludeFields = $excludeFields; + + return $this; + } + + public function setFormatter(?FormatterInterface $formatter = null): self + { + $this->formatter = $formatter; + + return $this; + } + + /** + * Generates attachment field + * + * @param string|array $value + */ + private function generateAttachmentField(string $title, $value): array + { + $value = is_array($value) + ? sprintf('```%s```', substr($this->stringify($value), 0, 1990)) + : $value; + + return array( + 'title' => ucfirst($title), + 'value' => $value, + 'short' => false, + ); + } + + /** + * Generates a collection of attachment fields from array + */ + private function generateAttachmentFields(array $data): array + { + $fields = array(); + foreach ($this->normalizerFormatter->format($data) as $key => $value) { + $fields[] = $this->generateAttachmentField((string) $key, $value); + } + + return $fields; + } + + /** + * Get a copy of record with fields excluded according to $this->excludeFields + */ + private function removeExcludedFields(array $record): array + { + foreach ($this->excludeFields as $field) { + $keys = explode('.', $field); + $node = &$record; + $lastKey = end($keys); + foreach ($keys as $key) { + if (!isset($node[$key])) { + break; + } + if ($lastKey === $key) { + unset($node[$key]); + break; + } + $node = &$node[$key]; + } + } + + return $record; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php new file mode 100644 index 0000000..1535a18 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php @@ -0,0 +1,233 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Handler\Slack\SlackRecord; + +/** + * Sends notifications through Slack API + * + * @author Greg Kedzierski + * @see https://api.slack.com/ + */ +class SlackHandler extends SocketHandler +{ + /** + * Slack API token + * @var string + */ + private $token; + + /** + * Instance of the SlackRecord util class preparing data for Slack API. + * @var SlackRecord + */ + private $slackRecord; + + /** + * @param string $token Slack API token + * @param string $channel Slack channel (encoded ID or name) + * @param string|null $username Name of a bot + * @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) + * @param string|null $iconEmoji The emoji name to use (or null) + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $useShortAttachment Whether the context/extra messages added to Slack as attachments are in a short style + * @param bool $includeContextAndExtra Whether the attachment should include context and extra data + * @param array $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] + * @throws MissingExtensionException If no OpenSSL PHP extension configured + */ + public function __construct( + string $token, + string $channel, + ?string $username = null, + bool $useAttachment = true, + ?string $iconEmoji = null, + $level = Logger::CRITICAL, + bool $bubble = true, + bool $useShortAttachment = false, + bool $includeContextAndExtra = false, + array $excludeFields = array() + ) { + if (!extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); + } + + parent::__construct('ssl://slack.com:443', $level, $bubble); + + $this->slackRecord = new SlackRecord( + $channel, + $username, + $useAttachment, + $iconEmoji, + $useShortAttachment, + $includeContextAndExtra, + $excludeFields + ); + + $this->token = $token; + } + + public function getSlackRecord(): SlackRecord + { + return $this->slackRecord; + } + + public function getToken(): string + { + return $this->token; + } + + /** + * {@inheritdoc} + */ + protected function generateDataStream(array $record): string + { + $content = $this->buildContent($record); + + return $this->buildHeader($content) . $content; + } + + /** + * Builds the body of API call + */ + private function buildContent(array $record): string + { + $dataArray = $this->prepareContentData($record); + + return http_build_query($dataArray); + } + + protected function prepareContentData(array $record): array + { + $dataArray = $this->slackRecord->getSlackData($record); + $dataArray['token'] = $this->token; + + if (!empty($dataArray['attachments'])) { + $dataArray['attachments'] = Utils::jsonEncode($dataArray['attachments']); + } + + return $dataArray; + } + + /** + * Builds the header of the API Call + */ + private function buildHeader(string $content): string + { + $header = "POST /api/chat.postMessage HTTP/1.1\r\n"; + $header .= "Host: slack.com\r\n"; + $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $header .= "Content-Length: " . strlen($content) . "\r\n"; + $header .= "\r\n"; + + return $header; + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + parent::write($record); + $this->finalizeWrite(); + } + + /** + * Finalizes the request by reading some bytes and then closing the socket + * + * If we do not read some but close the socket too early, slack sometimes + * drops the request entirely. + */ + protected function finalizeWrite(): void + { + $res = $this->getResource(); + if (is_resource($res)) { + @fread($res, 2048); + } + $this->closeSocket(); + } + + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + parent::setFormatter($formatter); + $this->slackRecord->setFormatter($formatter); + + return $this; + } + + public function getFormatter(): FormatterInterface + { + $formatter = parent::getFormatter(); + $this->slackRecord->setFormatter($formatter); + + return $formatter; + } + + /** + * Channel used by the bot when posting + */ + public function setChannel(string $channel): self + { + $this->slackRecord->setChannel($channel); + + return $this; + } + + /** + * Username used by the bot when posting + */ + public function setUsername(string $username): self + { + $this->slackRecord->setUsername($username); + + return $this; + } + + public function useAttachment(bool $useAttachment): self + { + $this->slackRecord->useAttachment($useAttachment); + + return $this; + } + + public function setIconEmoji(string $iconEmoji): self + { + $this->slackRecord->setUserIcon($iconEmoji); + + return $this; + } + + public function useShortAttachment(bool $useShortAttachment): self + { + $this->slackRecord->useShortAttachment($useShortAttachment); + + return $this; + } + + public function includeContextAndExtra(bool $includeContextAndExtra): self + { + $this->slackRecord->includeContextAndExtra($includeContextAndExtra); + + return $this; + } + + public function excludeFields(array $excludeFields): self + { + $this->slackRecord->excludeFields($excludeFields); + + return $this; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php new file mode 100644 index 0000000..17d12b0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Handler\Slack\SlackRecord; + +/** + * Sends notifications through Slack Webhooks + * + * @author Haralan Dobrev + * @see https://api.slack.com/incoming-webhooks + */ +class SlackWebhookHandler extends AbstractProcessingHandler +{ + /** + * Slack Webhook token + * @var string + */ + private $webhookUrl; + + /** + * Instance of the SlackRecord util class preparing data for Slack API. + * @var SlackRecord + */ + private $slackRecord; + + /** + * @param string $webhookUrl Slack Webhook URL + * @param string|null $channel Slack channel (encoded ID or name) + * @param string|null $username Name of a bot + * @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) + * @param string|null $iconEmoji The emoji name to use (or null) + * @param bool $useShortAttachment Whether the the context/extra messages added to Slack as attachments are in a short style + * @param bool $includeContextAndExtra Whether the attachment should include context and extra data + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param array $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] + */ + public function __construct( + string $webhookUrl, + ?string $channel = null, + ?string $username = null, + bool $useAttachment = true, + ?string $iconEmoji = null, + bool $useShortAttachment = false, + bool $includeContextAndExtra = false, + $level = Logger::CRITICAL, + bool $bubble = true, + array $excludeFields = array() + ) { + parent::__construct($level, $bubble); + + $this->webhookUrl = $webhookUrl; + + $this->slackRecord = new SlackRecord( + $channel, + $username, + $useAttachment, + $iconEmoji, + $useShortAttachment, + $includeContextAndExtra, + $excludeFields + ); + } + + public function getSlackRecord(): SlackRecord + { + return $this->slackRecord; + } + + public function getWebhookUrl(): string + { + return $this->webhookUrl; + } + + /** + * {@inheritdoc} + * + * @param array $record + */ + protected function write(array $record): void + { + $postData = $this->slackRecord->getSlackData($record); + $postString = Utils::jsonEncode($postData); + + $ch = curl_init(); + $options = array( + CURLOPT_URL => $this->webhookUrl, + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => array('Content-type: application/json'), + CURLOPT_POSTFIELDS => $postString, + ); + if (defined('CURLOPT_SAFE_UPLOAD')) { + $options[CURLOPT_SAFE_UPLOAD] = true; + } + + curl_setopt_array($ch, $options); + + Curl\Util::execute($ch); + } + + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + parent::setFormatter($formatter); + $this->slackRecord->setFormatter($formatter); + + return $this; + } + + public function getFormatter(): FormatterInterface + { + $formatter = parent::getFormatter(); + $this->slackRecord->setFormatter($formatter); + + return $formatter; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php new file mode 100644 index 0000000..7e8e018 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php @@ -0,0 +1,380 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Stores to any socket - uses fsockopen() or pfsockopen(). + * + * @author Pablo de Leon Belloc + * @see http://php.net/manual/en/function.fsockopen.php + */ +class SocketHandler extends AbstractProcessingHandler +{ + private $connectionString; + private $connectionTimeout; + /** @var resource|null */ + private $resource; + /** @var float */ + private $timeout = 0.0; + /** @var float */ + private $writingTimeout = 10.0; + private $lastSentBytes = null; + /** @var int */ + private $chunkSize = null; + private $persistent = false; + private $errno; + private $errstr; + /** @var ?float */ + private $lastWritingAt; + + /** + * @param string $connectionString Socket connection string + * @param int|string $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(string $connectionString, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + $this->connectionString = $connectionString; + $this->connectionTimeout = (float) ini_get('default_socket_timeout'); + } + + /** + * Connect (if necessary) and write to the socket + * + * @param array $record + * + * @throws \UnexpectedValueException + * @throws \RuntimeException + */ + protected function write(array $record): void + { + $this->connectIfNotConnected(); + $data = $this->generateDataStream($record); + $this->writeToSocket($data); + } + + /** + * We will not close a PersistentSocket instance so it can be reused in other requests. + */ + public function close(): void + { + if (!$this->isPersistent()) { + $this->closeSocket(); + } + } + + /** + * Close socket, if open + */ + public function closeSocket(): void + { + if (is_resource($this->resource)) { + fclose($this->resource); + $this->resource = null; + } + } + + /** + * Set socket connection to be persistent. It only has effect before the connection is initiated. + */ + public function setPersistent(bool $persistent): self + { + $this->persistent = $persistent; + + return $this; + } + + /** + * Set connection timeout. Only has effect before we connect. + * + * @see http://php.net/manual/en/function.fsockopen.php + */ + public function setConnectionTimeout(float $seconds): self + { + $this->validateTimeout($seconds); + $this->connectionTimeout = $seconds; + + return $this; + } + + /** + * Set write timeout. Only has effect before we connect. + * + * @see http://php.net/manual/en/function.stream-set-timeout.php + */ + public function setTimeout(float $seconds): self + { + $this->validateTimeout($seconds); + $this->timeout = $seconds; + + return $this; + } + + /** + * Set writing timeout. Only has effect during connection in the writing cycle. + * + * @param float $seconds 0 for no timeout + */ + public function setWritingTimeout(float $seconds): self + { + $this->validateTimeout($seconds); + $this->writingTimeout = $seconds; + + return $this; + } + + /** + * Set chunk size. Only has effect during connection in the writing cycle. + */ + public function setChunkSize(int $bytes): self + { + $this->chunkSize = $bytes; + + return $this; + } + + /** + * Get current connection string + */ + public function getConnectionString(): string + { + return $this->connectionString; + } + + /** + * Get persistent setting + */ + public function isPersistent(): bool + { + return $this->persistent; + } + + /** + * Get current connection timeout setting + */ + public function getConnectionTimeout(): float + { + return $this->connectionTimeout; + } + + /** + * Get current in-transfer timeout + */ + public function getTimeout(): float + { + return $this->timeout; + } + + /** + * Get current local writing timeout + * + * @return float + */ + public function getWritingTimeout(): float + { + return $this->writingTimeout; + } + + /** + * Get current chunk size + */ + public function getChunkSize(): int + { + return $this->chunkSize; + } + + /** + * Check to see if the socket is currently available. + * + * UDP might appear to be connected but might fail when writing. See http://php.net/fsockopen for details. + */ + public function isConnected(): bool + { + return is_resource($this->resource) + && !feof($this->resource); // on TCP - other party can close connection. + } + + /** + * Wrapper to allow mocking + */ + protected function pfsockopen() + { + return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); + } + + /** + * Wrapper to allow mocking + */ + protected function fsockopen() + { + return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); + } + + /** + * Wrapper to allow mocking + * + * @see http://php.net/manual/en/function.stream-set-timeout.php + */ + protected function streamSetTimeout() + { + $seconds = floor($this->timeout); + $microseconds = round(($this->timeout - $seconds) * 1e6); + + return stream_set_timeout($this->resource, (int) $seconds, (int) $microseconds); + } + + /** + * Wrapper to allow mocking + * + * @see http://php.net/manual/en/function.stream-set-chunk-size.php + */ + protected function streamSetChunkSize() + { + return stream_set_chunk_size($this->resource, $this->chunkSize); + } + + /** + * Wrapper to allow mocking + */ + protected function fwrite($data) + { + return @fwrite($this->resource, $data); + } + + /** + * Wrapper to allow mocking + */ + protected function streamGetMetadata() + { + return stream_get_meta_data($this->resource); + } + + private function validateTimeout($value) + { + $ok = filter_var($value, FILTER_VALIDATE_FLOAT); + if ($ok === false || $value < 0) { + throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)"); + } + } + + private function connectIfNotConnected() + { + if ($this->isConnected()) { + return; + } + $this->connect(); + } + + protected function generateDataStream(array $record): string + { + return (string) $record['formatted']; + } + + /** + * @return resource|null + */ + protected function getResource() + { + return $this->resource; + } + + private function connect(): void + { + $this->createSocketResource(); + $this->setSocketTimeout(); + $this->setStreamChunkSize(); + } + + private function createSocketResource(): void + { + if ($this->isPersistent()) { + $resource = $this->pfsockopen(); + } else { + $resource = $this->fsockopen(); + } + if (!$resource) { + throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)"); + } + $this->resource = $resource; + } + + private function setSocketTimeout(): void + { + if (!$this->streamSetTimeout()) { + throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()"); + } + } + + private function setStreamChunkSize(): void + { + if ($this->chunkSize && !$this->streamSetChunkSize()) { + throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()"); + } + } + + private function writeToSocket(string $data): void + { + $length = strlen($data); + $sent = 0; + $this->lastSentBytes = $sent; + while ($this->isConnected() && $sent < $length) { + if (0 == $sent) { + $chunk = $this->fwrite($data); + } else { + $chunk = $this->fwrite(substr($data, $sent)); + } + if ($chunk === false) { + throw new \RuntimeException("Could not write to socket"); + } + $sent += $chunk; + $socketInfo = $this->streamGetMetadata(); + if ($socketInfo['timed_out']) { + throw new \RuntimeException("Write timed-out"); + } + + if ($this->writingIsTimedOut($sent)) { + throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)"); + } + } + if (!$this->isConnected() && $sent < $length) { + throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)"); + } + } + + private function writingIsTimedOut(int $sent): bool + { + // convert to ms + if (0.0 == $this->writingTimeout) { + return false; + } + + if ($sent !== $this->lastSentBytes) { + $this->lastWritingAt = microtime(true); + $this->lastSentBytes = $sent; + + return false; + } else { + usleep(100); + } + + if ((microtime(true) - $this->lastWritingAt) >= $this->writingTimeout) { + $this->closeSocket(); + + return true; + } + + return false; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SqsHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SqsHandler.php new file mode 100644 index 0000000..a98c87b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SqsHandler.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Aws\Sqs\SqsClient; +use Monolog\Logger; +use Monolog\Utils; + +/** + * Writes to any sqs queue. + * + * @author Martijn van Calker + */ +class SqsHandler extends AbstractProcessingHandler +{ + /** 256 KB in bytes - maximum message size in SQS */ + protected const MAX_MESSAGE_SIZE = 262144; + /** 100 KB in bytes - head message size for new error log */ + protected const HEAD_MESSAGE_SIZE = 102400; + + /** @var SqsClient */ + private $client; + /** @var string */ + private $queueUrl; + + public function __construct(SqsClient $sqsClient, string $queueUrl, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->client = $sqsClient; + $this->queueUrl = $queueUrl; + } + + /** + * Writes the record down to the log of the implementing handler. + * + * @param array $record + */ + protected function write(array $record): void + { + if (!isset($record['formatted']) || 'string' !== gettype($record['formatted'])) { + throw new \InvalidArgumentException('SqsHandler accepts only formatted records as a string'); + } + + $messageBody = $record['formatted']; + if (strlen($messageBody) >= static::MAX_MESSAGE_SIZE) { + $messageBody = Utils::substr($messageBody, 0, static::HEAD_MESSAGE_SIZE); + } + + $this->client->sendMessage([ + 'QueueUrl' => $this->queueUrl, + 'MessageBody' => $messageBody, + ]); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php new file mode 100644 index 0000000..344bd30 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; + +/** + * Stores to any stream resource + * + * Can be used to store into php://stderr, remote and local files, etc. + * + * @author Jordi Boggiano + */ +class StreamHandler extends AbstractProcessingHandler +{ + /** @var resource|null */ + protected $stream; + protected $url; + /** @var string|null */ + private $errorMessage; + protected $filePermission; + protected $useLocking; + private $dirCreated; + + /** + * @param resource|string $stream If a missing path can't be created, an UnexpectedValueException will be thrown on first write + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) + * @param bool $useLocking Try to lock log file before doing any writes + * + * @throws \InvalidArgumentException If stream is not a resource or string + */ + public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false) + { + parent::__construct($level, $bubble); + if (is_resource($stream)) { + $this->stream = $stream; + } elseif (is_string($stream)) { + $this->url = Utils::canonicalizePath($stream); + } else { + throw new \InvalidArgumentException('A stream must either be a resource or a string.'); + } + + $this->filePermission = $filePermission; + $this->useLocking = $useLocking; + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + if ($this->url && is_resource($this->stream)) { + fclose($this->stream); + } + $this->stream = null; + $this->dirCreated = null; + } + + /** + * Return the currently active stream if it is open + * + * @return resource|null + */ + public function getStream() + { + return $this->stream; + } + + /** + * Return the stream URL if it was configured with a URL and not an active resource + * + * @return string|null + */ + public function getUrl(): ?string + { + return $this->url; + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + if (!is_resource($this->stream)) { + if (null === $this->url || '' === $this->url) { + throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); + } + $this->createDir(); + $this->errorMessage = null; + set_error_handler([$this, 'customErrorHandler']); + $this->stream = fopen($this->url, 'a'); + if ($this->filePermission !== null) { + @chmod($this->url, $this->filePermission); + } + restore_error_handler(); + if (!is_resource($this->stream)) { + $this->stream = null; + + throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $this->url)); + } + } + + if ($this->useLocking) { + // ignoring errors here, there's not much we can do about them + flock($this->stream, LOCK_EX); + } + + $this->streamWrite($this->stream, $record); + + if ($this->useLocking) { + flock($this->stream, LOCK_UN); + } + } + + /** + * Write to stream + * @param resource $stream + * @param array $record + */ + protected function streamWrite($stream, array $record): void + { + fwrite($stream, (string) $record['formatted']); + } + + private function customErrorHandler($code, $msg): bool + { + $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); + + return true; + } + + private function getDirFromStream(string $stream): ?string + { + $pos = strpos($stream, '://'); + if ($pos === false) { + return dirname($stream); + } + + if ('file://' === substr($stream, 0, 7)) { + return dirname(substr($stream, 7)); + } + + return null; + } + + private function createDir(): void + { + // Do not try to create dir if it has already been tried. + if ($this->dirCreated) { + return; + } + + $dir = $this->getDirFromStream($this->url); + if (null !== $dir && !is_dir($dir)) { + $this->errorMessage = null; + set_error_handler([$this, 'customErrorHandler']); + $status = mkdir($dir, 0777, true); + restore_error_handler(); + if (false === $status && !is_dir($dir)) { + throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage, $dir)); + } + } + $this->dirCreated = true; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.php new file mode 100644 index 0000000..2c5c5da --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Swift_Message; +use Swift; + +/** + * SwiftMailerHandler uses Swift_Mailer to send the emails + * + * @author Gyula Sallai + */ +class SwiftMailerHandler extends MailHandler +{ + protected $mailer; + private $messageTemplate; + + /** + * @psalm-param Swift_Message|callable(string, array): Swift_Message $message + * + * @param \Swift_Mailer $mailer The mailer to use + * @param callable|Swift_Message $message An example message for real messages, only the body will be replaced + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(\Swift_Mailer $mailer, $message, $level = Logger::ERROR, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->mailer = $mailer; + $this->messageTemplate = $message; + } + + /** + * {@inheritdoc} + */ + protected function send(string $content, array $records): void + { + $this->mailer->send($this->buildMessage($content, $records)); + } + + /** + * Gets the formatter for the Swift_Message subject. + * + * @param string|null $format The format of the subject + */ + protected function getSubjectFormatter(?string $format): FormatterInterface + { + return new LineFormatter($format); + } + + /** + * Creates instance of Swift_Message to be sent + * + * @param string $content formatted email body to be sent + * @param array $records Log records that formed the content + * @return Swift_Message + */ + protected function buildMessage(string $content, array $records): Swift_Message + { + $message = null; + if ($this->messageTemplate instanceof Swift_Message) { + $message = clone $this->messageTemplate; + $message->generateId(); + } elseif (is_callable($this->messageTemplate)) { + $message = ($this->messageTemplate)($content, $records); + } + + if (!$message instanceof Swift_Message) { + throw new \InvalidArgumentException('Could not resolve message as instance of Swift_Message or a callable returning it'); + } + + if ($records) { + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); + $message->setSubject($subjectFormatter->format($this->getHighestRecord($records))); + } + + $mime = 'text/plain'; + if ($this->isHtmlBody($content)) { + $mime = 'text/html'; + } + + $message->setBody($content, $mime); + /** @phpstan-ignore-next-line */ + if (version_compare(Swift::VERSION, '6.0.0', '>=')) { + $message->setDate(new \DateTimeImmutable()); + } else { + /** @phpstan-ignore-next-line */ + $message->setDate(time()); + } + + return $message; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php new file mode 100644 index 0000000..20594ce --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Logs to syslog service. + * + * usage example: + * + * $log = new Logger('application'); + * $syslog = new SyslogHandler('myfacility', 'local6'); + * $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%"); + * $syslog->setFormatter($formatter); + * $log->pushHandler($syslog); + * + * @author Sven Paulus + */ +class SyslogHandler extends AbstractSyslogHandler +{ + protected $ident; + protected $logopts; + + /** + * @param string $ident + * @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param int $logopts Option flags for the openlog() call, defaults to LOG_PID + */ + public function __construct(string $ident, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, int $logopts = LOG_PID) + { + parent::__construct($facility, $level, $bubble); + + $this->ident = $ident; + $this->logopts = $logopts; + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + closelog(); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + if (!openlog($this->ident, $this->logopts, $this->facility)) { + throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"'); + } + syslog($this->logLevels[$record['level']], (string) $record['formatted']); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.php new file mode 100644 index 0000000..228a705 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler\SyslogUdp; + +use Monolog\Utils; +use Socket; + +class UdpSocket +{ + protected const DATAGRAM_MAX_LENGTH = 65023; + + /** @var string */ + protected $ip; + /** @var int */ + protected $port; + /** @var resource|Socket|null */ + protected $socket; + + public function __construct(string $ip, int $port = 514) + { + $this->ip = $ip; + $this->port = $port; + $domain = AF_INET; + $protocol = SOL_UDP; + // Check if we are using unix sockets. + if ($port === 0) { + $domain = AF_UNIX; + $protocol = IPPROTO_IP; + } + $this->socket = socket_create($domain, SOCK_DGRAM, $protocol) ?: null; + } + + public function write($line, $header = "") + { + $this->send($this->assembleMessage($line, $header)); + } + + public function close(): void + { + if (is_resource($this->socket) || $this->socket instanceof Socket) { + socket_close($this->socket); + $this->socket = null; + } + } + + protected function send(string $chunk): void + { + if (!is_resource($this->socket) && !$this->socket instanceof Socket) { + throw new \RuntimeException('The UdpSocket to '.$this->ip.':'.$this->port.' has been closed and can not be written to anymore'); + } + socket_sendto($this->socket, $chunk, strlen($chunk), $flags = 0, $this->ip, $this->port); + } + + protected function assembleMessage(string $line, string $header): string + { + $chunkSize = static::DATAGRAM_MAX_LENGTH - strlen($header); + + return $header . Utils::substr($line, 0, $chunkSize); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php new file mode 100644 index 0000000..a31ae9a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use DateTimeInterface; +use Monolog\Logger; +use Monolog\Handler\SyslogUdp\UdpSocket; + +/** + * A Handler for logging to a remote syslogd server. + * + * @author Jesper Skovgaard Nielsen + * @author Dominik Kukacka + */ +class SyslogUdpHandler extends AbstractSyslogHandler +{ + const RFC3164 = 0; + const RFC5424 = 1; + const RFC5424e = 2; + + private $dateFormats = array( + self::RFC3164 => 'M d H:i:s', + self::RFC5424 => \DateTime::RFC3339, + self::RFC5424e => \DateTime::RFC3339_EXTENDED, + ); + + protected $socket; + protected $ident; + protected $rfc; + + /** + * @param string $host Either IP/hostname or a path to a unix socket (port must be 0 then) + * @param int $port Port number, or 0 if $host is a unix socket + * @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param string $ident Program name or tag for each log message. + * @param int $rfc RFC to format the message for. + */ + public function __construct(string $host, int $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, string $ident = 'php', int $rfc = self::RFC5424) + { + parent::__construct($facility, $level, $bubble); + + $this->ident = $ident; + $this->rfc = $rfc; + + $this->socket = new UdpSocket($host, $port); + } + + protected function write(array $record): void + { + $lines = $this->splitMessageIntoLines($record['formatted']); + + $header = $this->makeCommonSyslogHeader($this->logLevels[$record['level']], $record['datetime']); + + foreach ($lines as $line) { + $this->socket->write($line, $header); + } + } + + public function close(): void + { + $this->socket->close(); + } + + private function splitMessageIntoLines($message): array + { + if (is_array($message)) { + $message = implode("\n", $message); + } + + return preg_split('/$\R?^/m', (string) $message, -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * Make common syslog header (see rfc5424 or rfc3164) + */ + protected function makeCommonSyslogHeader(int $severity, DateTimeInterface $datetime): string + { + $priority = $severity + $this->facility; + + if (!$pid = getmypid()) { + $pid = '-'; + } + + if (!$hostname = gethostname()) { + $hostname = '-'; + } + + if ($this->rfc === self::RFC3164 && ($datetime instanceof \DateTimeImmutable || $datetime instanceof \DateTime)) { + $datetime->setTimezone(new \DateTimeZone('UTC')); + } + $date = $datetime->format($this->dateFormats[$this->rfc]); + + if ($this->rfc === self::RFC3164) { + return "<$priority>" . + $date . " " . + $hostname . " " . + $this->ident . "[" . $pid . "]: "; + } else { + return "<$priority>1 " . + $date . " " . + $hostname . " " . + $this->ident . " " . + $pid . " - - "; + } + } + + /** + * Inject your own socket, mainly used for testing + */ + public function setSocket(UdpSocket $socket): self + { + $this->socket = $socket; + + return $this; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php new file mode 100644 index 0000000..025c89e --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use RuntimeException; +use Monolog\Logger; + +/** + * Handler send logs to Telegram using Telegram Bot API. + * + * How to use: + * 1) Create telegram bot with https://telegram.me/BotFather + * 2) Create a telegram channel where logs will be recorded. + * 3) Add created bot from step 1 to the created channel from step 2. + * + * Use telegram bot API key from step 1 and channel name with '@' prefix from step 2 to create instance of TelegramBotHandler + * + * @link https://core.telegram.org/bots/api + * + * @author Mazur Alexandr + */ +class TelegramBotHandler extends AbstractProcessingHandler +{ + private const BOT_API = 'https://api.telegram.org/bot'; + + /** + * @var array AVAILABLE_PARSE_MODES The available values of parseMode according to the Telegram api documentation + */ + private const AVAILABLE_PARSE_MODES = [ + 'HTML', + 'MarkdownV2', + 'Markdown' // legacy mode without underline and strikethrough, use MarkdownV2 instead + ]; + + /** + * Telegram bot access token provided by BotFather. + * Create telegram bot with https://telegram.me/BotFather and use access token from it. + * @var string + */ + private $apiKey; + + /** + * Telegram channel name. + * Since to start with '@' symbol as prefix. + * @var string + */ + private $channel; + + /** + * The kind of formatting that is used for the message. + * See available options at https://core.telegram.org/bots/api#formatting-options + * or in AVAILABLE_PARSE_MODES + * @var string|null + */ + private $parseMode; + + /** + * Disables link previews for links in the message. + * @var bool|null + */ + private $disableWebPagePreview; + + /** + * Sends the message silently. Users will receive a notification with no sound. + * @var bool|null + */ + private $disableNotification; + + /** + * @param string $apiKey Telegram bot access token provided by BotFather + * @param string $channel Telegram channel name + * @inheritDoc + */ + public function __construct( + string $apiKey, + string $channel, + $level = Logger::DEBUG, + bool $bubble = true, + string $parseMode = null, + bool $disableWebPagePreview = null, + bool $disableNotification = null + ) { + parent::__construct($level, $bubble); + + $this->apiKey = $apiKey; + $this->channel = $channel; + $this->setParseMode($parseMode); + $this->disableWebPagePreview($disableWebPagePreview); + $this->disableNotification($disableNotification); + } + + public function setParseMode(string $parseMode = null): self + { + if ($parseMode !== null && !in_array($parseMode, self::AVAILABLE_PARSE_MODES)) { + throw new \InvalidArgumentException('Unknown parseMode, use one of these: ' . implode(', ', self::AVAILABLE_PARSE_MODES) . '.'); + } + + $this->parseMode = $parseMode; + return $this; + } + + public function disableWebPagePreview(bool $disableWebPagePreview = null): self + { + $this->disableWebPagePreview = $disableWebPagePreview; + return $this; + } + + public function disableNotification(bool $disableNotification = null): self + { + $this->disableNotification = $disableNotification; + return $this; + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $messages = []; + + foreach ($records as $record) { + if (!$this->isHandling($record)) { + continue; + } + + if ($this->processors) { + $record = $this->processRecord($record); + } + + $messages[] = $record; + } + + if (!empty($messages)) { + $this->send((string) $this->getFormatter()->formatBatch($messages)); + } + } + + /** + * @inheritDoc + */ + protected function write(array $record): void + { + $this->send($record['formatted']); + } + + /** + * Send request to @link https://api.telegram.org/bot on SendMessage action. + * @param string $message + */ + protected function send(string $message): void + { + $ch = curl_init(); + $url = self::BOT_API . $this->apiKey . '/SendMessage'; + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ + 'text' => $message, + 'chat_id' => $this->channel, + 'parse_mode' => $this->parseMode, + 'disable_web_page_preview' => $this->disableWebPagePreview, + 'disable_notification' => $this->disableNotification, + ])); + + $result = Curl\Util::execute($ch); + $result = json_decode($result, true); + + if ($result['ok'] === false) { + throw new RuntimeException('Telegram API error. Description: ' . $result['description']); + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php new file mode 100644 index 0000000..9fa77b9 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + +/** + * Used for testing purposes. + * + * It records all records and gives you access to them for verification. + * + * @author Jordi Boggiano + * + * @method bool hasEmergency($record) + * @method bool hasAlert($record) + * @method bool hasCritical($record) + * @method bool hasError($record) + * @method bool hasWarning($record) + * @method bool hasNotice($record) + * @method bool hasInfo($record) + * @method bool hasDebug($record) + * + * @method bool hasEmergencyRecords() + * @method bool hasAlertRecords() + * @method bool hasCriticalRecords() + * @method bool hasErrorRecords() + * @method bool hasWarningRecords() + * @method bool hasNoticeRecords() + * @method bool hasInfoRecords() + * @method bool hasDebugRecords() + * + * @method bool hasEmergencyThatContains($message) + * @method bool hasAlertThatContains($message) + * @method bool hasCriticalThatContains($message) + * @method bool hasErrorThatContains($message) + * @method bool hasWarningThatContains($message) + * @method bool hasNoticeThatContains($message) + * @method bool hasInfoThatContains($message) + * @method bool hasDebugThatContains($message) + * + * @method bool hasEmergencyThatMatches($message) + * @method bool hasAlertThatMatches($message) + * @method bool hasCriticalThatMatches($message) + * @method bool hasErrorThatMatches($message) + * @method bool hasWarningThatMatches($message) + * @method bool hasNoticeThatMatches($message) + * @method bool hasInfoThatMatches($message) + * @method bool hasDebugThatMatches($message) + * + * @method bool hasEmergencyThatPasses($message) + * @method bool hasAlertThatPasses($message) + * @method bool hasCriticalThatPasses($message) + * @method bool hasErrorThatPasses($message) + * @method bool hasWarningThatPasses($message) + * @method bool hasNoticeThatPasses($message) + * @method bool hasInfoThatPasses($message) + * @method bool hasDebugThatPasses($message) + */ +class TestHandler extends AbstractProcessingHandler +{ + protected $records = []; + protected $recordsByLevel = []; + private $skipReset = false; + + public function getRecords() + { + return $this->records; + } + + public function clear() + { + $this->records = []; + $this->recordsByLevel = []; + } + + public function reset() + { + if (!$this->skipReset) { + $this->clear(); + } + } + + public function setSkipReset(bool $skipReset) + { + $this->skipReset = $skipReset; + } + + /** + * @param string|int $level Logging level value or name + */ + public function hasRecords($level): bool + { + return isset($this->recordsByLevel[Logger::toMonologLevel($level)]); + } + + /** + * @param string|array $record Either a message string or an array containing message and optionally context keys that will be checked against all records + * @param string|int $level Logging level value or name + */ + public function hasRecord($record, $level): bool + { + if (is_string($record)) { + $record = array('message' => $record); + } + + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + + return true; + }, $level); + } + + /** + * @param string|int $level Logging level value or name + */ + public function hasRecordThatContains(string $message, $level): bool + { + return $this->hasRecordThatPasses(function ($rec) use ($message) { + return strpos($rec['message'], $message) !== false; + }, $level); + } + + /** + * @param string|int $level Logging level value or name + */ + public function hasRecordThatMatches(string $regex, $level): bool + { + return $this->hasRecordThatPasses(function (array $rec) use ($regex): bool { + return preg_match($regex, $rec['message']) > 0; + }, $level); + } + + /** + * @psalm-param callable(array, int): mixed $predicate + * + * @param string|int $level Logging level value or name + * @return bool + */ + public function hasRecordThatPasses(callable $predicate, $level) + { + $level = Logger::toMonologLevel($level); + + if (!isset($this->recordsByLevel[$level])) { + return false; + } + + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if ($predicate($rec, $i)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + public function __call($method, $args) + { + if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { + $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; + $level = constant('Monolog\Logger::' . strtoupper($matches[2])); + if (method_exists($this, $genericMethod)) { + $args[] = $level; + + return call_user_func_array([$this, $genericMethod], $args); + } + } + + throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/WebRequestRecognizerTrait.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/WebRequestRecognizerTrait.php new file mode 100644 index 0000000..c818352 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/WebRequestRecognizerTrait.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +trait WebRequestRecognizerTrait +{ + /** + * Checks if PHP's serving a web request + * @return bool + */ + protected function isWebRequest(): bool + { + return 'cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php new file mode 100644 index 0000000..16c3cbb --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +/** + * Forwards records to multiple handlers suppressing failures of each handler + * and continuing through to give every handler a chance to succeed. + * + * @author Craig D'Amelio + */ +class WhatFailureGroupHandler extends GroupHandler +{ + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + if ($this->processors) { + $record = $this->processRecord($record); + } + + foreach ($this->handlers as $handler) { + try { + $handler->handle($record); + } catch (\Throwable $e) { + // What failure? + } + } + + return false === $this->bubble; + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + if ($this->processors) { + $processed = array(); + foreach ($records as $record) { + $processed[] = $this->processRecord($record); + } + $records = $processed; + } + + foreach ($this->handlers as $handler) { + try { + $handler->handleBatch($records); + } catch (\Throwable $e) { + // What failure? + } + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php new file mode 100644 index 0000000..34fe80f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Logger; + +/** + * Handler sending logs to Zend Monitor + * + * @author Christian Bergau + * @author Jason Davis + */ +class ZendMonitorHandler extends AbstractProcessingHandler +{ + /** + * Monolog level / ZendMonitor Custom Event priority map + * + * @var array + */ + protected $levelMap = []; + + /** + * @param string|int $level The minimum logging level at which this handler will be triggered. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not. + * @throws MissingExtensionException + */ + public function __construct($level = Logger::DEBUG, bool $bubble = true) + { + if (!function_exists('zend_monitor_custom_event')) { + throw new MissingExtensionException( + 'You must have Zend Server installed with Zend Monitor enabled in order to use this handler' + ); + } + //zend monitor constants are not defined if zend monitor is not enabled. + $this->levelMap = [ + Logger::DEBUG => \ZEND_MONITOR_EVENT_SEVERITY_INFO, + Logger::INFO => \ZEND_MONITOR_EVENT_SEVERITY_INFO, + Logger::NOTICE => \ZEND_MONITOR_EVENT_SEVERITY_INFO, + Logger::WARNING => \ZEND_MONITOR_EVENT_SEVERITY_WARNING, + Logger::ERROR => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, + Logger::CRITICAL => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, + Logger::ALERT => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, + Logger::EMERGENCY => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, + ]; + parent::__construct($level, $bubble); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $this->writeZendMonitorCustomEvent( + Logger::getLevelName($record['level']), + $record['message'], + $record['formatted'], + $this->levelMap[$record['level']] + ); + } + + /** + * Write to Zend Monitor Events + * @param string $type Text displayed in "Class Name (custom)" field + * @param string $message Text displayed in "Error String" + * @param array $formatted Displayed in Custom Variables tab + * @param int $severity Set the event severity level (-1,0,1) + */ + protected function writeZendMonitorCustomEvent(string $type, string $message, array $formatted, int $severity): void + { + zend_monitor_custom_event($type, $message, $formatted, $severity); + } + + /** + * {@inheritdoc} + */ + public function getDefaultFormatter(): FormatterInterface + { + return new NormalizerFormatter(); + } + + public function getLevelMap(): array + { + return $this->levelMap; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Logger.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Logger.php new file mode 100644 index 0000000..927c312 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Logger.php @@ -0,0 +1,611 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use DateTimeZone; +use Monolog\Handler\HandlerInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\InvalidArgumentException; +use Throwable; + +/** + * Monolog log channel + * + * It contains a stack of Handlers and a stack of Processors, + * and uses them to store records that are added to it. + * + * @author Jordi Boggiano + */ +class Logger implements LoggerInterface, ResettableInterface +{ + /** + * Detailed debug information + */ + public const DEBUG = 100; + + /** + * Interesting events + * + * Examples: User logs in, SQL logs. + */ + public const INFO = 200; + + /** + * Uncommon events + */ + public const NOTICE = 250; + + /** + * Exceptional occurrences that are not errors + * + * Examples: Use of deprecated APIs, poor use of an API, + * undesirable things that are not necessarily wrong. + */ + public const WARNING = 300; + + /** + * Runtime errors + */ + public const ERROR = 400; + + /** + * Critical conditions + * + * Example: Application component unavailable, unexpected exception. + */ + public const CRITICAL = 500; + + /** + * Action must be taken immediately + * + * Example: Entire website down, database unavailable, etc. + * This should trigger the SMS alerts and wake you up. + */ + public const ALERT = 550; + + /** + * Urgent alert. + */ + public const EMERGENCY = 600; + + /** + * Monolog API version + * + * This is only bumped when API breaks are done and should + * follow the major version of the library + * + * @var int + */ + public const API = 2; + + /** + * This is a static variable and not a constant to serve as an extension point for custom levels + * + * @var array $levels Logging levels with the levels as key + */ + protected static $levels = [ + self::DEBUG => 'DEBUG', + self::INFO => 'INFO', + self::NOTICE => 'NOTICE', + self::WARNING => 'WARNING', + self::ERROR => 'ERROR', + self::CRITICAL => 'CRITICAL', + self::ALERT => 'ALERT', + self::EMERGENCY => 'EMERGENCY', + ]; + + /** + * @var string + */ + protected $name; + + /** + * The handler stack + * + * @var HandlerInterface[] + */ + protected $handlers; + + /** + * Processors that will process all log records + * + * To process records of a single handler instead, add the processor on that specific handler + * + * @var callable[] + */ + protected $processors; + + /** + * @var bool + */ + protected $microsecondTimestamps = true; + + /** + * @var DateTimeZone + */ + protected $timezone; + + /** + * @var callable|null + */ + protected $exceptionHandler; + + /** + * @psalm-param array $processors + * + * @param string $name The logging channel, a simple descriptive name that is attached to all log records + * @param HandlerInterface[] $handlers Optional stack of handlers, the first one in the array is called first, etc. + * @param callable[] $processors Optional array of processors + * @param DateTimeZone|null $timezone Optional timezone, if not provided date_default_timezone_get() will be used + */ + public function __construct(string $name, array $handlers = [], array $processors = [], ?DateTimeZone $timezone = null) + { + $this->name = $name; + $this->setHandlers($handlers); + $this->processors = $processors; + $this->timezone = $timezone ?: new DateTimeZone(date_default_timezone_get() ?: 'UTC'); + } + + public function getName(): string + { + return $this->name; + } + + /** + * Return a new cloned instance with the name changed + */ + public function withName(string $name): self + { + $new = clone $this; + $new->name = $name; + + return $new; + } + + /** + * Pushes a handler on to the stack. + */ + public function pushHandler(HandlerInterface $handler): self + { + array_unshift($this->handlers, $handler); + + return $this; + } + + /** + * Pops a handler from the stack + * + * @throws \LogicException If empty handler stack + */ + public function popHandler(): HandlerInterface + { + if (!$this->handlers) { + throw new \LogicException('You tried to pop from an empty handler stack.'); + } + + return array_shift($this->handlers); + } + + /** + * Set handlers, replacing all existing ones. + * + * If a map is passed, keys will be ignored. + * + * @param HandlerInterface[] $handlers + */ + public function setHandlers(array $handlers): self + { + $this->handlers = []; + foreach (array_reverse($handlers) as $handler) { + $this->pushHandler($handler); + } + + return $this; + } + + /** + * @return HandlerInterface[] + */ + public function getHandlers(): array + { + return $this->handlers; + } + + /** + * Adds a processor on to the stack. + */ + public function pushProcessor(callable $callback): self + { + array_unshift($this->processors, $callback); + + return $this; + } + + /** + * Removes the processor on top of the stack and returns it. + * + * @throws \LogicException If empty processor stack + * @return callable + */ + public function popProcessor(): callable + { + if (!$this->processors) { + throw new \LogicException('You tried to pop from an empty processor stack.'); + } + + return array_shift($this->processors); + } + + /** + * @return callable[] + */ + public function getProcessors(): array + { + return $this->processors; + } + + /** + * Control the use of microsecond resolution timestamps in the 'datetime' + * member of new records. + * + * As of PHP7.1 microseconds are always included by the engine, so + * there is no performance penalty and Monolog 2 enabled microseconds + * by default. This function lets you disable them though in case you want + * to suppress microseconds from the output. + * + * @param bool $micro True to use microtime() to create timestamps + */ + public function useMicrosecondTimestamps(bool $micro): void + { + $this->microsecondTimestamps = $micro; + } + + /** + * Adds a log record. + * + * @param int $level The logging level + * @param string $message The log message + * @param mixed[] $context The log context + * @return bool Whether the record has been processed + */ + public function addRecord(int $level, string $message, array $context = []): bool + { + $offset = 0; + $record = null; + + foreach ($this->handlers as $handler) { + if (null === $record) { + // skip creating the record as long as no handler is going to handle it + if (!$handler->isHandling(['level' => $level])) { + continue; + } + + $levelName = static::getLevelName($level); + + $record = [ + 'message' => $message, + 'context' => $context, + 'level' => $level, + 'level_name' => $levelName, + 'channel' => $this->name, + 'datetime' => new DateTimeImmutable($this->microsecondTimestamps, $this->timezone), + 'extra' => [], + ]; + + try { + foreach ($this->processors as $processor) { + $record = $processor($record); + } + } catch (Throwable $e) { + $this->handleException($e, $record); + + return true; + } + } + + // once the record exists, send it to all handlers as long as the bubbling chain is not interrupted + try { + if (true === $handler->handle($record)) { + break; + } + } catch (Throwable $e) { + $this->handleException($e, $record); + + return true; + } + } + + return null !== $record; + } + + /** + * Ends a log cycle and frees all resources used by handlers. + * + * Closing a Handler means flushing all buffers and freeing any open resources/handles. + * Handlers that have been closed should be able to accept log records again and re-open + * themselves on demand, but this may not always be possible depending on implementation. + * + * This is useful at the end of a request and will be called automatically on every handler + * when they get destructed. + */ + public function close(): void + { + foreach ($this->handlers as $handler) { + $handler->close(); + } + } + + /** + * Ends a log cycle and resets all handlers and processors to their initial state. + * + * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal + * state, and getting it back to a state in which it can receive log records again. + * + * This is useful in case you want to avoid logs leaking between two requests or jobs when you + * have a long running process like a worker or an application server serving multiple requests + * in one process. + */ + public function reset(): void + { + foreach ($this->handlers as $handler) { + if ($handler instanceof ResettableInterface) { + $handler->reset(); + } + } + + foreach ($this->processors as $processor) { + if ($processor instanceof ResettableInterface) { + $processor->reset(); + } + } + } + + /** + * Gets all supported logging levels. + * + * @return array Assoc array with human-readable level names => level codes. + */ + public static function getLevels(): array + { + return array_flip(static::$levels); + } + + /** + * Gets the name of the logging level. + * + * @throws \Psr\Log\InvalidArgumentException If level is not defined + */ + public static function getLevelName(int $level): string + { + if (!isset(static::$levels[$level])) { + throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels))); + } + + return static::$levels[$level]; + } + + /** + * Converts PSR-3 levels to Monolog ones if necessary + * + * @param string|int $level Level number (monolog) or name (PSR-3) + * @throws \Psr\Log\InvalidArgumentException If level is not defined + */ + public static function toMonologLevel($level): int + { + if (is_string($level)) { + if (is_numeric($level)) { + return intval($level); + } + + // Contains chars of all log levels and avoids using strtoupper() which may have + // strange results depending on locale (for example, "i" will become "İ" in Turkish locale) + $upper = strtr($level, 'abcdefgilmnortuwy', 'ABCDEFGILMNORTUWY'); + if (defined(__CLASS__.'::'.$upper)) { + return constant(__CLASS__ . '::' . $upper); + } + + throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels))); + } + + if (!is_int($level)) { + throw new InvalidArgumentException('Level "'.var_export($level, true).'" is not defined, use one of: '.implode(', ', array_keys(static::$levels))); + } + + return $level; + } + + /** + * Checks whether the Logger has a handler that listens on the given level + */ + public function isHandling(int $level): bool + { + $record = [ + 'level' => $level, + ]; + + foreach ($this->handlers as $handler) { + if ($handler->isHandling($record)) { + return true; + } + } + + return false; + } + + /** + * Set a custom exception handler that will be called if adding a new record fails + * + * The callable will receive an exception object and the record that failed to be logged + */ + public function setExceptionHandler(?callable $callback): self + { + $this->exceptionHandler = $callback; + + return $this; + } + + public function getExceptionHandler(): ?callable + { + return $this->exceptionHandler; + } + + /** + * Adds a log record at an arbitrary level. + * + * This method allows for compatibility with common interfaces. + * + * @param mixed $level The log level + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function log($level, $message, array $context = []): void + { + $level = static::toMonologLevel($level); + + $this->addRecord($level, (string) $message, $context); + } + + /** + * Adds a log record at the DEBUG level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function debug($message, array $context = []): void + { + $this->addRecord(static::DEBUG, (string) $message, $context); + } + + /** + * Adds a log record at the INFO level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function info($message, array $context = []): void + { + $this->addRecord(static::INFO, (string) $message, $context); + } + + /** + * Adds a log record at the NOTICE level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function notice($message, array $context = []): void + { + $this->addRecord(static::NOTICE, (string) $message, $context); + } + + /** + * Adds a log record at the WARNING level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function warning($message, array $context = []): void + { + $this->addRecord(static::WARNING, (string) $message, $context); + } + + /** + * Adds a log record at the ERROR level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function error($message, array $context = []): void + { + $this->addRecord(static::ERROR, (string) $message, $context); + } + + /** + * Adds a log record at the CRITICAL level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function critical($message, array $context = []): void + { + $this->addRecord(static::CRITICAL, (string) $message, $context); + } + + /** + * Adds a log record at the ALERT level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function alert($message, array $context = []): void + { + $this->addRecord(static::ALERT, (string) $message, $context); + } + + /** + * Adds a log record at the EMERGENCY level. + * + * This method allows for compatibility with common interfaces. + * + * @param string $message The log message + * @param mixed[] $context The log context + */ + public function emergency($message, array $context = []): void + { + $this->addRecord(static::EMERGENCY, (string) $message, $context); + } + + /** + * Sets the timezone to be used for the timestamp of log records. + */ + public function setTimezone(DateTimeZone $tz): self + { + $this->timezone = $tz; + + return $this; + } + + /** + * Returns the timezone to be used for the timestamp of log records. + */ + public function getTimezone(): DateTimeZone + { + return $this->timezone; + } + + /** + * Delegates exception management to the custom exception handler, + * or throws the exception if no custom handler is set. + */ + protected function handleException(Throwable $e, array $record): void + { + if (!$this->exceptionHandler) { + throw $e; + } + + ($this->exceptionHandler)($e, $record); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php new file mode 100644 index 0000000..6abd3d8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Logger; + +/** + * Injects Git branch and Git commit SHA in all records + * + * @author Nick Otter + * @author Jordi Boggiano + */ +class GitProcessor implements ProcessorInterface +{ + private $level; + private static $cache; + + /** + * @param string|int $level The minimum logging level at which this Processor will be triggered + */ + public function __construct($level = Logger::DEBUG) + { + $this->level = Logger::toMonologLevel($level); + } + + public function __invoke(array $record): array + { + // return if the level is not high enough + if ($record['level'] < $this->level) { + return $record; + } + + $record['extra']['git'] = self::getGitInfo(); + + return $record; + } + + private static function getGitInfo(): array + { + if (self::$cache) { + return self::$cache; + } + + $branches = `git branch -v --no-abbrev`; + if ($branches && preg_match('{^\* (.+?)\s+([a-f0-9]{40})(?:\s|$)}m', $branches, $matches)) { + return self::$cache = [ + 'branch' => $matches[1], + 'commit' => $matches[2], + ]; + } + + return self::$cache = []; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/HostnameProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/HostnameProcessor.php new file mode 100644 index 0000000..7c23db8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/HostnameProcessor.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects value of gethostname in all records + */ +class HostnameProcessor implements ProcessorInterface +{ + private static $host; + + public function __construct() + { + self::$host = (string) gethostname(); + } + + public function __invoke(array $record): array + { + $record['extra']['hostname'] = self::$host; + + return $record; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php new file mode 100644 index 0000000..c0cc014 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Logger; + +/** + * Injects line/file:class/function where the log message came from + * + * Warning: This only works if the handler processes the logs directly. + * If you put the processor on a handler that is behind a FingersCrossedHandler + * for example, the processor will only be called once the trigger level is reached, + * and all the log records will have the same file/line/.. data from the call that + * triggered the FingersCrossedHandler. + * + * @author Jordi Boggiano + */ +class IntrospectionProcessor implements ProcessorInterface +{ + private $level; + + private $skipClassesPartials; + + private $skipStackFramesCount; + + private $skipFunctions = [ + 'call_user_func', + 'call_user_func_array', + ]; + + /** + * @param string|int $level The minimum logging level at which this Processor will be triggered + */ + public function __construct($level = Logger::DEBUG, array $skipClassesPartials = [], int $skipStackFramesCount = 0) + { + $this->level = Logger::toMonologLevel($level); + $this->skipClassesPartials = array_merge(['Monolog\\'], $skipClassesPartials); + $this->skipStackFramesCount = $skipStackFramesCount; + } + + public function __invoke(array $record): array + { + // return if the level is not high enough + if ($record['level'] < $this->level) { + return $record; + } + + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + // skip first since it's always the current method + array_shift($trace); + // the call_user_func call is also skipped + array_shift($trace); + + $i = 0; + + while ($this->isTraceClassOrSkippedFunction($trace, $i)) { + if (isset($trace[$i]['class'])) { + foreach ($this->skipClassesPartials as $part) { + if (strpos($trace[$i]['class'], $part) !== false) { + $i++; + + continue 2; + } + } + } elseif (in_array($trace[$i]['function'], $this->skipFunctions)) { + $i++; + + continue; + } + + break; + } + + $i += $this->skipStackFramesCount; + + // we should have the call source now + $record['extra'] = array_merge( + $record['extra'], + [ + 'file' => isset($trace[$i - 1]['file']) ? $trace[$i - 1]['file'] : null, + 'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null, + 'class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : null, + 'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null, + ] + ); + + return $record; + } + + private function isTraceClassOrSkippedFunction(array $trace, int $index) + { + if (!isset($trace[$index])) { + return false; + } + + return isset($trace[$index]['class']) || in_array($trace[$index]['function'], $this->skipFunctions); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php new file mode 100644 index 0000000..a1eef61 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects memory_get_peak_usage in all records + * + * @see Monolog\Processor\MemoryProcessor::__construct() for options + * @author Rob Jensen + */ +class MemoryPeakUsageProcessor extends MemoryProcessor +{ + public function __invoke(array $record): array + { + $usage = memory_get_peak_usage($this->realUsage); + + if ($this->useFormatting) { + $usage = $this->formatBytes($usage); + } + + $record['extra']['memory_peak_usage'] = $usage; + + return $record; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php new file mode 100644 index 0000000..227deb7 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Some methods that are common for all memory processors + * + * @author Rob Jensen + */ +abstract class MemoryProcessor implements ProcessorInterface +{ + /** + * @var bool If true, get the real size of memory allocated from system. Else, only the memory used by emalloc() is reported. + */ + protected $realUsage; + + /** + * @var bool If true, then format memory size to human readable string (MB, KB, B depending on size) + */ + protected $useFormatting; + + /** + * @param bool $realUsage Set this to true to get the real size of memory allocated from system. + * @param bool $useFormatting If true, then format memory size to human readable string (MB, KB, B depending on size) + */ + public function __construct(bool $realUsage = true, bool $useFormatting = true) + { + $this->realUsage = $realUsage; + $this->useFormatting = $useFormatting; + } + + /** + * Formats bytes into a human readable string if $this->useFormatting is true, otherwise return $bytes as is + * + * @param int $bytes + * @return string|int Formatted string if $this->useFormatting is true, otherwise return $bytes as int + */ + protected function formatBytes(int $bytes) + { + if (!$this->useFormatting) { + return $bytes; + } + + if ($bytes > 1024 * 1024) { + return round($bytes / 1024 / 1024, 2).' MB'; + } elseif ($bytes > 1024) { + return round($bytes / 1024, 2).' KB'; + } + + return $bytes . ' B'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php new file mode 100644 index 0000000..653c76d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects memory_get_usage in all records + * + * @see Monolog\Processor\MemoryProcessor::__construct() for options + * @author Rob Jensen + */ +class MemoryUsageProcessor extends MemoryProcessor +{ + public function __invoke(array $record): array + { + $usage = memory_get_usage($this->realUsage); + + if ($this->useFormatting) { + $usage = $this->formatBytes($usage); + } + + $record['extra']['memory_usage'] = $usage; + + return $record; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.php new file mode 100644 index 0000000..d50f713 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Logger; + +/** + * Injects Hg branch and Hg revision number in all records + * + * @author Jonathan A. Schweder + */ +class MercurialProcessor implements ProcessorInterface +{ + private $level; + private static $cache; + + /** + * @param string|int $level The minimum logging level at which this Processor will be triggered + */ + public function __construct($level = Logger::DEBUG) + { + $this->level = Logger::toMonologLevel($level); + } + + public function __invoke(array $record): array + { + // return if the level is not high enough + if ($record['level'] < $this->level) { + return $record; + } + + $record['extra']['hg'] = self::getMercurialInfo(); + + return $record; + } + + private static function getMercurialInfo(): array + { + if (self::$cache) { + return self::$cache; + } + + $result = explode(' ', trim(`hg id -nb`)); + + if (count($result) >= 3) { + return self::$cache = [ + 'branch' => $result[1], + 'revision' => $result[2], + ]; + } + + return self::$cache = []; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php new file mode 100644 index 0000000..7851fff --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Adds value of getmypid into records + * + * @author Andreas Hörnicke + */ +class ProcessIdProcessor implements ProcessorInterface +{ + public function __invoke(array $record): array + { + $record['extra']['process_id'] = getmypid(); + + return $record; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.php new file mode 100644 index 0000000..9e2ded1 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * An optional interface to allow labelling Monolog processors. + * + * @author Nicolas Grekas + */ +interface ProcessorInterface +{ + /** + * @return array The processed record + */ + public function __invoke(array $record); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php new file mode 100644 index 0000000..7b80e57 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Utils; + +/** + * Processes a record's message according to PSR-3 rules + * + * It replaces {foo} with the value from $context['foo'] + * + * @author Jordi Boggiano + */ +class PsrLogMessageProcessor implements ProcessorInterface +{ + public const SIMPLE_DATE = "Y-m-d\TH:i:s.uP"; + + /** @var string|null */ + private $dateFormat; + + /** @var bool */ + private $removeUsedContextFields; + + /** + * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format + * @param bool $removeUsedContextFields If set to true the fields interpolated into message gets unset + */ + public function __construct(?string $dateFormat = null, bool $removeUsedContextFields = false) + { + $this->dateFormat = $dateFormat; + $this->removeUsedContextFields = $removeUsedContextFields; + } + + /** + * @param array $record + * @return array + */ + public function __invoke(array $record): array + { + if (false === strpos($record['message'], '{')) { + return $record; + } + + $replacements = []; + foreach ($record['context'] as $key => $val) { + $placeholder = '{' . $key . '}'; + if (strpos($record['message'], $placeholder) === false) { + continue; + } + + if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { + $replacements[$placeholder] = $val; + } elseif ($val instanceof \DateTimeInterface) { + if (!$this->dateFormat && $val instanceof \Monolog\DateTimeImmutable) { + // handle monolog dates using __toString if no specific dateFormat was asked for + // so that it follows the useMicroseconds flag + $replacements[$placeholder] = (string) $val; + } else { + $replacements[$placeholder] = $val->format($this->dateFormat ?: static::SIMPLE_DATE); + } + } elseif (is_object($val)) { + $replacements[$placeholder] = '[object '.Utils::getClass($val).']'; + } elseif (is_array($val)) { + $replacements[$placeholder] = 'array'.Utils::jsonEncode($val, null, true); + } else { + $replacements[$placeholder] = '['.gettype($val).']'; + } + + if ($this->removeUsedContextFields) { + unset($record['context'][$key]); + } + } + + $record['message'] = strtr($record['message'], $replacements); + + return $record; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.php new file mode 100644 index 0000000..199760a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Adds a tags array into record + * + * @author Martijn Riemers + */ +class TagProcessor implements ProcessorInterface +{ + private $tags; + + public function __construct(array $tags = []) + { + $this->setTags($tags); + } + + public function addTags(array $tags = []): self + { + $this->tags = array_merge($this->tags, $tags); + + return $this; + } + + public function setTags(array $tags = []): self + { + $this->tags = $tags; + + return $this; + } + + public function __invoke(array $record): array + { + $record['extra']['tags'] = $this->tags; + + return $record; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php new file mode 100644 index 0000000..0c97ab6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\ResettableInterface; + +/** + * Adds a unique identifier into records + * + * @author Simon Mönch + */ +class UidProcessor implements ProcessorInterface, ResettableInterface +{ + private $uid; + + public function __construct(int $length = 7) + { + if ($length > 32 || $length < 1) { + throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32'); + } + + $this->uid = $this->generateUid($length); + } + + public function __invoke(array $record): array + { + $record['extra']['uid'] = $this->uid; + + return $record; + } + + public function getUid(): string + { + return $this->uid; + } + + public function reset() + { + $this->uid = $this->generateUid(strlen($this->uid)); + } + + private function generateUid(int $length): string + { + return substr(bin2hex(random_bytes((int) ceil($length / 2))), 0, $length); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php new file mode 100644 index 0000000..6c32b2d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects url/method and remote IP of the current web request in all records + * + * @author Jordi Boggiano + */ +class WebProcessor implements ProcessorInterface +{ + /** + * @var array|\ArrayAccess + */ + protected $serverData; + + /** + * Default fields + * + * Array is structured as [key in record.extra => key in $serverData] + * + * @var array + */ + protected $extraFields = [ + 'url' => 'REQUEST_URI', + 'ip' => 'REMOTE_ADDR', + 'http_method' => 'REQUEST_METHOD', + 'server' => 'SERVER_NAME', + 'referrer' => 'HTTP_REFERER', + ]; + + /** + * @param array|\ArrayAccess|null $serverData Array or object w/ ArrayAccess that provides access to the $_SERVER data + * @param array|null $extraFields Field names and the related key inside $serverData to be added. If not provided it defaults to: url, ip, http_method, server, referrer + */ + public function __construct($serverData = null, array $extraFields = null) + { + if (null === $serverData) { + $this->serverData = &$_SERVER; + } elseif (is_array($serverData) || $serverData instanceof \ArrayAccess) { + $this->serverData = $serverData; + } else { + throw new \UnexpectedValueException('$serverData must be an array or object implementing ArrayAccess.'); + } + + if (isset($this->serverData['UNIQUE_ID'])) { + $this->extraFields['unique_id'] = 'UNIQUE_ID'; + } + + if (null !== $extraFields) { + if (isset($extraFields[0])) { + foreach (array_keys($this->extraFields) as $fieldName) { + if (!in_array($fieldName, $extraFields)) { + unset($this->extraFields[$fieldName]); + } + } + } else { + $this->extraFields = $extraFields; + } + } + } + + public function __invoke(array $record): array + { + // skip processing if for some reason request data + // is not present (CLI or wonky SAPIs) + if (!isset($this->serverData['REQUEST_URI'])) { + return $record; + } + + $record['extra'] = $this->appendExtraFields($record['extra']); + + return $record; + } + + public function addExtraField(string $extraName, string $serverName): self + { + $this->extraFields[$extraName] = $serverName; + + return $this; + } + + private function appendExtraFields(array $extra): array + { + foreach ($this->extraFields as $extraName => $serverName) { + $extra[$extraName] = $this->serverData[$serverName] ?? null; + } + + return $extra; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Registry.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Registry.php new file mode 100644 index 0000000..78fb97e --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Registry.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use InvalidArgumentException; + +/** + * Monolog log registry + * + * Allows to get `Logger` instances in the global scope + * via static method calls on this class. + * + * + * $application = new Monolog\Logger('application'); + * $api = new Monolog\Logger('api'); + * + * Monolog\Registry::addLogger($application); + * Monolog\Registry::addLogger($api); + * + * function testLogger() + * { + * Monolog\Registry::api()->error('Sent to $api Logger instance'); + * Monolog\Registry::application()->error('Sent to $application Logger instance'); + * } + * + * + * @author Tomas Tatarko + */ +class Registry +{ + /** + * List of all loggers in the registry (by named indexes) + * + * @var Logger[] + */ + private static $loggers = []; + + /** + * Adds new logging channel to the registry + * + * @param Logger $logger Instance of the logging channel + * @param string|null $name Name of the logging channel ($logger->getName() by default) + * @param bool $overwrite Overwrite instance in the registry if the given name already exists? + * @throws \InvalidArgumentException If $overwrite set to false and named Logger instance already exists + */ + public static function addLogger(Logger $logger, ?string $name = null, bool $overwrite = false) + { + $name = $name ?: $logger->getName(); + + if (isset(self::$loggers[$name]) && !$overwrite) { + throw new InvalidArgumentException('Logger with the given name already exists'); + } + + self::$loggers[$name] = $logger; + } + + /** + * Checks if such logging channel exists by name or instance + * + * @param string|Logger $logger Name or logger instance + */ + public static function hasLogger($logger): bool + { + if ($logger instanceof Logger) { + $index = array_search($logger, self::$loggers, true); + + return false !== $index; + } + + return isset(self::$loggers[$logger]); + } + + /** + * Removes instance from registry by name or instance + * + * @param string|Logger $logger Name or logger instance + */ + public static function removeLogger($logger): void + { + if ($logger instanceof Logger) { + if (false !== ($idx = array_search($logger, self::$loggers, true))) { + unset(self::$loggers[$idx]); + } + } else { + unset(self::$loggers[$logger]); + } + } + + /** + * Clears the registry + */ + public static function clear(): void + { + self::$loggers = []; + } + + /** + * Gets Logger instance from the registry + * + * @param string $name Name of the requested Logger instance + * @throws \InvalidArgumentException If named Logger instance is not in the registry + */ + public static function getInstance($name): Logger + { + if (!isset(self::$loggers[$name])) { + throw new InvalidArgumentException(sprintf('Requested "%s" logger instance is not in the registry', $name)); + } + + return self::$loggers[$name]; + } + + /** + * Gets Logger instance from the registry via static method call + * + * @param string $name Name of the requested Logger instance + * @param array $arguments Arguments passed to static method call + * @throws \InvalidArgumentException If named Logger instance is not in the registry + * @return Logger Requested instance of Logger + */ + public static function __callStatic($name, $arguments) + { + return self::getInstance($name); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/ResettableInterface.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/ResettableInterface.php new file mode 100644 index 0000000..2c5fd78 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/ResettableInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +/** + * Handler or Processor implementing this interface will be reset when Logger::reset() is called. + * + * Resetting ends a log cycle gets them back to their initial state. + * + * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal + * state, and getting it back to a state in which it can receive log records again. + * + * This is useful in case you want to avoid logs leaking between two requests or jobs when you + * have a long running process like a worker or an application server serving multiple requests + * in one process. + * + * @author Grégoire Pineau + */ +interface ResettableInterface +{ + /** + * @return void + */ + public function reset(); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/SignalHandler.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/SignalHandler.php new file mode 100644 index 0000000..517ab54 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/SignalHandler.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use ReflectionExtension; + +/** + * Monolog POSIX signal handler + * + * @author Robert Gust-Bardon + */ +class SignalHandler +{ + private $logger; + + private $previousSignalHandler = []; + private $signalLevelMap = []; + private $signalRestartSyscalls = []; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function registerSignalHandler($signo, $level = LogLevel::CRITICAL, bool $callPrevious = true, bool $restartSyscalls = true, ?bool $async = true): self + { + if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { + return $this; + } + + if ($callPrevious) { + $handler = pcntl_signal_get_handler($signo); + $this->previousSignalHandler[$signo] = $handler; + } else { + unset($this->previousSignalHandler[$signo]); + } + $this->signalLevelMap[$signo] = $level; + $this->signalRestartSyscalls[$signo] = $restartSyscalls; + + if ($async !== null) { + pcntl_async_signals($async); + } + + pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls); + + return $this; + } + + public function handleSignal($signo, array $siginfo = null): void + { + static $signals = []; + + if (!$signals && extension_loaded('pcntl')) { + $pcntl = new ReflectionExtension('pcntl'); + // HHVM 3.24.2 returns an empty array. + foreach ($pcntl->getConstants() ?: get_defined_constants(true)['Core'] as $name => $value) { + if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { + $signals[$value] = $name; + } + } + } + + $level = $this->signalLevelMap[$signo] ?? LogLevel::CRITICAL; + $signal = $signals[$signo] ?? $signo; + $context = $siginfo ?? []; + $this->logger->log($level, sprintf('Program received signal %s', $signal), $context); + + if (!isset($this->previousSignalHandler[$signo])) { + return; + } + + if ($this->previousSignalHandler[$signo] === true || $this->previousSignalHandler[$signo] === SIG_DFL) { + if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch') + && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill') + ) { + $restartSyscalls = $this->signalRestartSyscalls[$signo] ?? true; + pcntl_signal($signo, SIG_DFL, $restartSyscalls); + pcntl_sigprocmask(SIG_UNBLOCK, [$signo], $oldset); + posix_kill(posix_getpid(), $signo); + pcntl_signal_dispatch(); + pcntl_sigprocmask(SIG_SETMASK, $oldset); + pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls); + } + } elseif (is_callable($this->previousSignalHandler[$signo])) { + $this->previousSignalHandler[$signo]($signo, $siginfo); + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Test/TestCase.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Test/TestCase.php new file mode 100644 index 0000000..b996bbc --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Test/TestCase.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Test; + +use Monolog\Logger; +use Monolog\DateTimeImmutable; +use Monolog\Formatter\FormatterInterface; + +/** + * Lets you easily generate log records and a dummy formatter for testing purposes + * * + * @author Jordi Boggiano + */ +class TestCase extends \PHPUnit\Framework\TestCase +{ + /** + * @return array Record + */ + protected function getRecord($level = Logger::WARNING, $message = 'test', array $context = []): array + { + return [ + 'message' => (string) $message, + 'context' => $context, + 'level' => $level, + 'level_name' => Logger::getLevelName($level), + 'channel' => 'test', + 'datetime' => new DateTimeImmutable(true), + 'extra' => [], + ]; + } + + protected function getMultipleRecords(): array + { + return [ + $this->getRecord(Logger::DEBUG, 'debug message 1'), + $this->getRecord(Logger::DEBUG, 'debug message 2'), + $this->getRecord(Logger::INFO, 'information'), + $this->getRecord(Logger::WARNING, 'warning'), + $this->getRecord(Logger::ERROR, 'error'), + ]; + } + + protected function getIdentityFormatter(): FormatterInterface + { + $formatter = $this->createMock(FormatterInterface::class); + $formatter->expects($this->any()) + ->method('format') + ->will($this->returnCallback(function ($record) { + return $record['message']; + })); + + return $formatter; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Utils.php b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Utils.php new file mode 100644 index 0000000..7986819 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/monolog/monolog/src/Monolog/Utils.php @@ -0,0 +1,196 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +final class Utils +{ + const DEFAULT_JSON_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR; + + public static function getClass(object $object): string + { + $class = \get_class($object); + + return 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class; + } + + public static function substr(string $string, int $start, ?int $length = null): string + { + if (extension_loaded('mbstring')) { + return mb_strcut($string, $start, $length); + } + + return substr($string, $start, (null === $length) ? strlen($string) : $length); + } + + /** + * Makes sure if a relative path is passed in it is turned into an absolute path + * + * @param string $streamUrl stream URL or path without protocol + */ + public static function canonicalizePath(string $streamUrl): string + { + $prefix = ''; + if ('file://' === substr($streamUrl, 0, 7)) { + $streamUrl = substr($streamUrl, 7); + $prefix = 'file://'; + } + + // other type of stream, not supported + if (false !== strpos($streamUrl, '://')) { + return $streamUrl; + } + + // already absolute + if (substr($streamUrl, 0, 1) === '/' || substr($streamUrl, 1, 1) === ':' || substr($streamUrl, 0, 2) === '\\\\') { + return $prefix.$streamUrl; + } + + $streamUrl = getcwd() . '/' . $streamUrl; + + return $prefix.$streamUrl; + } + + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @param int $encodeFlags flags to pass to json encode, defaults to DEFAULT_JSON_FLAGS + * @param bool $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null + * @throws \RuntimeException if encoding fails and errors are not ignored + * @return string when errors are ignored and the encoding fails, "null" is returned which is valid json for null + */ + public static function jsonEncode($data, ?int $encodeFlags = null, bool $ignoreErrors = false): string + { + if (null === $encodeFlags) { + $encodeFlags = self::DEFAULT_JSON_FLAGS; + } + + if ($ignoreErrors) { + $json = @json_encode($data, $encodeFlags); + if (false === $json) { + return 'null'; + } + + return $json; + } + + $json = json_encode($data, $encodeFlags); + if (false === $json) { + $json = self::handleJsonError(json_last_error(), $data); + } + + return $json; + } + + /** + * Handle a json_encode failure. + * + * If the failure is due to invalid string encoding, try to clean the + * input and encode again. If the second encoding attempt fails, the + * initial error is not encoding related or the input can't be cleaned then + * raise a descriptive exception. + * + * @param int $code return code of json_last_error function + * @param mixed $data data that was meant to be encoded + * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION + * @throws \RuntimeException if failure can't be corrected + * @return string JSON encoded data after error correction + */ + public static function handleJsonError(int $code, $data, ?int $encodeFlags = null): string + { + if ($code !== JSON_ERROR_UTF8) { + self::throwEncodeError($code, $data); + } + + if (is_string($data)) { + self::detectAndCleanUtf8($data); + } elseif (is_array($data)) { + array_walk_recursive($data, array('Monolog\Utils', 'detectAndCleanUtf8')); + } else { + self::throwEncodeError($code, $data); + } + + if (null === $encodeFlags) { + $encodeFlags = self::DEFAULT_JSON_FLAGS; + } + + $json = json_encode($data, $encodeFlags); + + if ($json === false) { + self::throwEncodeError(json_last_error(), $data); + } + + return $json; + } + + /** + * Throws an exception according to a given code with a customized message + * + * @param int $code return code of json_last_error function + * @param mixed $data data that was meant to be encoded + * @throws \RuntimeException + */ + private static function throwEncodeError(int $code, $data): void + { + switch ($code) { + case JSON_ERROR_DEPTH: + $msg = 'Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $msg = 'Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $msg = 'Unexpected control character found'; + break; + case JSON_ERROR_UTF8: + $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $msg = 'Unknown error'; + } + + throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); + } + + /** + * Detect invalid UTF-8 string characters and convert to valid UTF-8. + * + * Valid UTF-8 input will be left unmodified, but strings containing + * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed + * original encoding of ISO-8859-15. This conversion may result in + * incorrect output if the actual encoding was not ISO-8859-15, but it + * will be clean UTF-8 output and will not rely on expensive and fragile + * detection algorithms. + * + * Function converts the input in place in the passed variable so that it + * can be used as a callback for array_walk_recursive. + * + * @param mixed $data Input to check and convert if needed, passed by ref + */ + private static function detectAndCleanUtf8(&$data): void + { + if (is_string($data) && !preg_match('//u', $data)) { + $data = preg_replace_callback( + '/[\x80-\xFF]+/', + function ($m) { + return utf8_encode($m[0]); + }, + $data + ); + $data = str_replace( + ['¤', '¦', '¨', '´', '¸', '¼', '½', '¾'], + ['€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'], + $data + ); + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/CHANGELOG.md new file mode 100644 index 0000000..759bdbf --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/CHANGELOG.md @@ -0,0 +1,115 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.4.0 + +### Removed + +The `final` keyword was replaced by `@final` annotation. + +## 1.3.2 + +### Fixed + +- `Stream::read()` must not return boolean. +- Improved exception message when using wrong HTTP status code. + +## 1.3.1 + +### Fixed + +- Allow installation on PHP8 + +## 1.3.0 + +### Added + +- Make Stream::__toString() compatible with throwing exceptions on PHP 7.4. + +### Fixed + +- Support for UTF-8 hostnames +- Support for numeric header names + +## 1.2.1 + +### Changed + +- Added `.github` and `phpstan.neon.dist` to `.gitattributes`. + +## 1.2.0 + +### Changed + +- Change minimal port number to 0 (unix socket) +- Updated `Psr17Factory::createResponse` to respect the specification. If second + argument is not used, a standard reason phrase. If an empty string is passed, + then the reason phrase will be empty. + +### Fixed + +- Check for seekable on the stream resource. +- Fixed the `Response::$reason` should never be null. + +## 1.1.0 + +### Added + +- Improved performance +- More tests for `UploadedFile` and `HttplugFactory` + +### Removed + +- Dead code + +## 1.0.1 + +### Fixed + +- Handle `fopen` failing in createStreamFromFile according to PSR-7. +- Reduce execution path to speed up performance. +- Fixed typos. +- Code style. + +## 1.0.0 + +### Added + +- Support for final PSR-17 (HTTP factories). (`Psr17Factory`) +- Support for numeric header values. +- Support for empty header values. +- All classes are final +- `HttplugFactory` that implements factory interfaces from HTTPlug. + +### Changed + +- `ServerRequest` does not extend `Request`. + +### Removed + +- The HTTPlug discovery strategy was removed since it is included in php-http/discovery 1.4. +- `UploadedFileFactory()` was removed in favor for `Psr17Factory`. +- `ServerRequestFactory()` was removed in favor for `Psr17Factory`. +- `StreamFactory`, `UriFactory`, abd `MessageFactory`. Use `HttplugFactory` instead. +- `ServerRequestFactory::createServerRequestFromArray`, `ServerRequestFactory::createServerRequestFromArrays` and + `ServerRequestFactory::createServerRequestFromGlobals`. Please use the new `nyholm/psr7-server` instead. + +## 0.3.0 + +### Added + +- Return types. +- Many `InvalidArgumentException`s are thrown when you use invalid arguments. +- Integration tests for `UploadedFile` and `ServerRequest`. + +### Changed + +- We dropped PHP7.0 support. +- PSR-17 factories have been marked as internal. They do not fall under our BC promise until PSR-17 is accepted. +- `UploadedFileFactory::createUploadedFile` does not accept a string file path. + +## 0.2.3 + +No changelog before this release + diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/LICENSE new file mode 100644 index 0000000..d6c5231 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Tobias Nyholm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/README.md b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/README.md new file mode 100644 index 0000000..abe3248 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/README.md @@ -0,0 +1,110 @@ +# PSR-7 implementation + +[![Latest Version](https://img.shields.io/github/release/Nyholm/psr7.svg?style=flat-square)](https://github.com/Nyholm/psr7/releases) +[![Build Status](https://img.shields.io/travis/Nyholm/psr7/master.svg?style=flat-square)](https://travis-ci.org/Nyholm/psr7) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/Nyholm/psr7.svg?style=flat-square)](https://scrutinizer-ci.com/g/Nyholm/psr7) +[![Quality Score](https://img.shields.io/scrutinizer/g/Nyholm/psr7.svg?style=flat-square)](https://scrutinizer-ci.com/g/Nyholm/psr7) +[![Total Downloads](https://poser.pugx.org/nyholm/psr7/downloads)](https://packagist.org/packages/nyholm/psr7) +[![Monthly Downloads](https://poser.pugx.org/nyholm/psr7/d/monthly.png)](https://packagist.org/packages/nyholm/psr7) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) + + +A super lightweight PSR-7 implementation. Very strict and very fast. + +| Description | Guzzle | Laminas | Slim | Nyholm | +| ---- | ------ | ---- | ---- | ------ | +| Lines of code | 3 000 | 3 000 | 1 700 | 1 000 | +| PSR-7* | 66% | 100% | 75% | 100% | +| PSR-17 | No | Yes | Yes | Yes | +| HTTPlug | No | No | No | Yes | +| Performance** | 1.34x | 1x | 1.16x | 1.75x | + +\* Percent of completed tests in https://github.com/php-http/psr7-integration-tests + +\** See benchmark at https://github.com/Nyholm/http-client-benchmark (higher is better) + +## Installation + +```bash +composer require nyholm/psr7 +``` + +If you are using Symfony Flex then you get all message factories registered as services. + +## Usage + +The PSR-7 objects do not contain any other public methods than those defined in +the [PSR-7 specification](https://www.php-fig.org/psr/psr-7/). + +### Create objects + +Use the PSR-17 factory to create requests, streams, URIs etc. + +```php +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); +$request = $psr17Factory->createRequest('GET', 'http://tnyholm.se'); +$stream = $psr17Factory->createStream('foobar'); +``` + +### Sending a request + +With [HTTPlug](http://httplug.io/) or any other PSR-18 (HTTP client) you may send +requests like: + +```bash +composer require kriswallsmith/buzz +``` + +```php +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); +$psr18Client = new \Buzz\Client\Curl($psr17Factory); + +$request = $psr17Factory->createRequest('GET', 'http://tnyholm.se'); +$response = $psr18Client->sendRequest($request); +``` + +### Create server requests + +The [`nyholm/psr7-server`](https://github.com/Nyholm/psr7-server) package can be used +to create server requests from PHP superglobals. + +```bash +composer require nyholm/psr7-server +``` + +```php +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); + +$creator = new \Nyholm\Psr7Server\ServerRequestCreator( + $psr17Factory, // ServerRequestFactory + $psr17Factory, // UriFactory + $psr17Factory, // UploadedFileFactory + $psr17Factory // StreamFactory +); + +$serverRequest = $creator->fromGlobals(); +``` + +### Emitting a response + +```bash +composer require laminas/laminas-httphandlerrunner +``` + +```php +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); + +$responseBody = $psr17Factory->createStream('Hello world'); +$response = $psr17Factory->createResponse(200)->withBody($responseBody); +(new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter())->emit($response); +``` + +## Our goal + +This package is currently maintained by [Tobias Nyholm](http://nyholm.se) and +[Martijn van der Ven](https://vanderven.se/martijn/). They have decided that the +goal of this library should be to provide a super strict implementation of +[PSR-7](https://www.php-fig.org/psr/psr-7/) that is blazing fast. + +The package will never include any extra features nor helper methods. All our classes +and functions exist because they are required to fulfill the PSR-7 specification. diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/composer.json b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/composer.json new file mode 100644 index 0000000..0741e74 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/composer.json @@ -0,0 +1,48 @@ +{ + "name": "nyholm/psr7", + "description": "A fast PHP7 implementation of PSR-7", + "license": "MIT", + "keywords": ["psr-7", "psr-17"], + "homepage": "https://tnyholm.se", + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0", + "php-http/message-factory": "^1.0", + "psr/http-factory": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || 8.5 || 9.4", + "php-http/psr7-integration-tests": "^1.0", + "http-interop/http-factory-tests": "^0.8", + "symfony/error-handler": "^4.4" + }, + "provide": { + "psr/http-message-implementation": "1.0", + "psr/http-factory-implementation": "1.0" + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\Nyholm\\Psr7\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/doc/final.md b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/doc/final.md new file mode 100644 index 0000000..b5b1942 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/doc/final.md @@ -0,0 +1,20 @@ +# Final classes + +The `final` keyword was removed in version 1.4.0. It was replaced by `@final` annotation. +This was done due popular demand, not because it is a good technical reason to +extend the classes. + +This document will show the correct way to work with PSR-7 classes. The "correct way" +refers to best practices and good software design. I strongly believe that one should +be aware of how a problem *should* be solved, however, it is not needed to always +implement that solution. + +## Extending classes + +You should never extend the classes, you should rather use composition or implement +the interface yourself. Please refer to the [decorator pattern](https://refactoring.guru/design-patterns/decorator). + +## Mocking classes + +The PSR-7 classes are all value objects and they can be used without mocking. If +one really needs to create a special scenario, one can mock the interface instead. diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/psalm.xml b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/psalm.xml new file mode 100644 index 0000000..d234691 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Factory/HttplugFactory.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Factory/HttplugFactory.php new file mode 100644 index 0000000..cc64780 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Factory/HttplugFactory.php @@ -0,0 +1,42 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class HttplugFactory implements MessageFactory, StreamFactory, UriFactory +{ + public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1') + { + return new Request($method, $uri, $headers, $body, $protocolVersion); + } + + public function createResponse($statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $version = '1.1') + { + return new Response((int) $statusCode, $headers, $body, $version, $reasonPhrase); + } + + public function createStream($body = null) + { + return Stream::create($body ?? ''); + } + + public function createUri($uri = ''): UriInterface + { + if ($uri instanceof UriInterface) { + return $uri; + } + + return new Uri($uri); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Factory/Psr17Factory.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Factory/Psr17Factory.php new file mode 100644 index 0000000..0da5acd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Factory/Psr17Factory.php @@ -0,0 +1,75 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface +{ + public function createRequest(string $method, $uri): RequestInterface + { + return new Request($method, $uri); + } + + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + if (2 > \func_num_args()) { + // This will make the Response class to use a custom reasonPhrase + $reasonPhrase = null; + } + + return new Response($code, [], null, '1.1', $reasonPhrase); + } + + public function createStream(string $content = ''): StreamInterface + { + return Stream::create($content); + } + + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + $resource = @\fopen($filename, $mode); + if (false === $resource) { + if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) { + throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.'); + } + + throw new \RuntimeException('The file ' . $filename . ' cannot be opened.'); + } + + return Stream::create($resource); + } + + public function createStreamFromResource($resource): StreamInterface + { + return Stream::create($resource); + } + + public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface + { + if (null === $size) { + $size = $stream->getSize(); + } + + return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); + } + + public function createUri(string $uri = ''): UriInterface + { + return new Uri($uri); + } + + public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface + { + return new ServerRequest($method, $uri, [], null, '1.1', $serverParams); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/MessageTrait.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/MessageTrait.php new file mode 100644 index 0000000..595258b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/MessageTrait.php @@ -0,0 +1,207 @@ + + * @author Martijn van der Ven + * + * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise + */ +trait MessageTrait +{ + /** @var array Map of all registered headers, as original name => array of values */ + private $headers = []; + + /** @var array Map of lowercase header name => original name at registration */ + private $headerNames = []; + + /** @var string */ + private $protocol = '1.1'; + + /** @var StreamInterface|null */ + private $stream; + + public function getProtocolVersion(): string + { + return $this->protocol; + } + + public function withProtocolVersion($version): self + { + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = $version; + + return $new; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader($header): bool + { + return isset($this->headerNames[\strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]); + } + + public function getHeader($header): array + { + $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function getHeaderLine($header): string + { + return \implode(', ', $this->getHeader($header)); + } + + public function withHeader($header, $value): self + { + $value = $this->validateAndTrimHeader($header, $value); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + public function withAddedHeader($header, $value): self + { + if (!\is_string($header) || '' === $header) { + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.'); + } + + $new = clone $this; + $new->setHeaders([$header => $value]); + + return $new; + } + + public function withoutHeader($header): self + { + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getBody(): StreamInterface + { + if (null === $this->stream) { + $this->stream = Stream::create(''); + } + + return $this->stream; + } + + public function withBody(StreamInterface $body): self + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + + return $new; + } + + private function setHeaders(array $headers): void + { + foreach ($headers as $header => $value) { + if (\is_int($header)) { + // If a header name was set to a numeric string, PHP will cast the key to an int. + // We must cast it back to a string in order to comply with validation. + $header = (string) $header; + } + $value = $this->validateAndTrimHeader($header, $value); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + if (isset($this->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $this->headers[$header] = \array_merge($this->headers[$header], $value); + } else { + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + } + } + + /** + * Make sure the header complies with RFC 7230. + * + * Header names must be a non-empty string consisting of token characters. + * + * Header values must be strings consisting of visible characters with all optional + * leading and trailing whitespace stripped. This method will always strip such + * optional whitespace. Note that the method does not allow folding whitespace within + * the values as this was deprecated for almost all instances by the RFC. + * + * header-field = field-name ":" OWS field-value OWS + * field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" + * / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) ) + * OWS = *( SP / HTAB ) + * field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] ) + * + * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + */ + private function validateAndTrimHeader($header, $values): array + { + if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) { + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.'); + } + + if (!\is_array($values)) { + // This is simple, just one value. + if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) { + throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.'); + } + + return [\trim((string) $values, " \t")]; + } + + if (empty($values)) { + throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.'); + } + + // Assert Non empty array + $returnValues = []; + foreach ($values as $v) { + if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) { + throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.'); + } + + $returnValues[] = \trim((string) $v, " \t"); + } + + return $returnValues; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Request.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Request.php new file mode 100644 index 0000000..d50744e --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Request.php @@ -0,0 +1,47 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Request implements RequestInterface +{ + use MessageTrait; + use RequestTrait; + + /** + * @param string $method HTTP method + * @param string|UriInterface $uri URI + * @param array $headers Request headers + * @param string|resource|StreamInterface|null $body Request body + * @param string $version Protocol version + */ + public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1') + { + if (!($uri instanceof UriInterface)) { + $uri = new Uri($uri); + } + + $this->method = $method; + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + + if (!$this->hasHeader('Host')) { + $this->updateHostFromUri(); + } + + // If we got no body, defer initialization of the stream until Request::getBody() + if ('' !== $body && null !== $body) { + $this->stream = Stream::create($body); + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/RequestTrait.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/RequestTrait.php new file mode 100644 index 0000000..f39993a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/RequestTrait.php @@ -0,0 +1,113 @@ + + * @author Martijn van der Ven + * + * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise + */ +trait RequestTrait +{ + /** @var string */ + private $method; + + /** @var string|null */ + private $requestTarget; + + /** @var UriInterface|null */ + private $uri; + + public function getRequestTarget(): string + { + if (null !== $this->requestTarget) { + return $this->requestTarget; + } + + if ('' === $target = $this->uri->getPath()) { + $target = '/'; + } + if ('' !== $this->uri->getQuery()) { + $target .= '?' . $this->uri->getQuery(); + } + + return $target; + } + + public function withRequestTarget($requestTarget): self + { + if (\preg_match('#\s#', $requestTarget)) { + throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); + } + + $new = clone $this; + $new->requestTarget = $requestTarget; + + return $new; + } + + public function getMethod(): string + { + return $this->method; + } + + public function withMethod($method): self + { + if (!\is_string($method)) { + throw new \InvalidArgumentException('Method must be a string'); + } + + $new = clone $this; + $new->method = $method; + + return $new; + } + + public function getUri(): UriInterface + { + return $this->uri; + } + + public function withUri(UriInterface $uri, $preserveHost = false): self + { + if ($uri === $this->uri) { + return $this; + } + + $new = clone $this; + $new->uri = $uri; + + if (!$preserveHost || !$this->hasHeader('Host')) { + $new->updateHostFromUri(); + } + + return $new; + } + + private function updateHostFromUri(): void + { + if ('' === $host = $this->uri->getHost()) { + return; + } + + if (null !== ($port = $this->uri->getPort())) { + $host .= ':' . $port; + } + + if (isset($this->headerNames['host'])) { + $header = $this->headerNames['host']; + } else { + $this->headerNames['host'] = $header = 'Host'; + } + + // Ensure Host is the first header. + // See: http://tools.ietf.org/html/rfc7230#section-5.4 + $this->headers = [$header => [$host]] + $this->headers; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Response.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Response.php new file mode 100644 index 0000000..9a26d2c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Response.php @@ -0,0 +1,90 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Response implements ResponseInterface +{ + use MessageTrait; + + /** @var array Map of standard HTTP status code/reason phrases */ + private const PHRASES = [ + 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', + 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', + 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', + 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required', + ]; + + /** @var string */ + private $reasonPhrase = ''; + + /** @var int */ + private $statusCode; + + /** + * @param int $status Status code + * @param array $headers Response headers + * @param string|resource|StreamInterface|null $body Response body + * @param string $version Protocol version + * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) + */ + public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', string $reason = null) + { + // If we got no body, defer initialization of the stream until Response::getBody() + if ('' !== $body && null !== $body) { + $this->stream = Stream::create($body); + } + + $this->statusCode = $status; + $this->setHeaders($headers); + if (null === $reason && isset(self::PHRASES[$this->statusCode])) { + $this->reasonPhrase = self::PHRASES[$status]; + } else { + $this->reasonPhrase = $reason ?? ''; + } + + $this->protocol = $version; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + + public function withStatus($code, $reasonPhrase = ''): self + { + if (!\is_int($code) && !\is_string($code)) { + throw new \InvalidArgumentException('Status code has to be an integer'); + } + + $code = (int) $code; + if ($code < 100 || $code > 599) { + throw new \InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); + } + + $new = clone $this; + $new->statusCode = $code; + if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) { + $reasonPhrase = self::PHRASES[$new->statusCode]; + } + $new->reasonPhrase = $reasonPhrase; + + return $new; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/ServerRequest.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/ServerRequest.php new file mode 100644 index 0000000..1ad8792 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/ServerRequest.php @@ -0,0 +1,164 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class ServerRequest implements ServerRequestInterface +{ + use MessageTrait; + use RequestTrait; + + /** @var array */ + private $attributes = []; + + /** @var array */ + private $cookieParams = []; + + /** @var array|object|null */ + private $parsedBody; + + /** @var array */ + private $queryParams = []; + + /** @var array */ + private $serverParams; + + /** @var UploadedFileInterface[] */ + private $uploadedFiles = []; + + /** + * @param string $method HTTP method + * @param string|UriInterface $uri URI + * @param array $headers Request headers + * @param string|resource|StreamInterface|null $body Request body + * @param string $version Protocol version + * @param array $serverParams Typically the $_SERVER superglobal + */ + public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = []) + { + $this->serverParams = $serverParams; + + if (!($uri instanceof UriInterface)) { + $uri = new Uri($uri); + } + + $this->method = $method; + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + + if (!$this->hasHeader('Host')) { + $this->updateHostFromUri(); + } + + // If we got no body, defer initialization of the stream until ServerRequest::getBody() + if ('' !== $body && null !== $body) { + $this->stream = Stream::create($body); + } + } + + public function getServerParams(): array + { + return $this->serverParams; + } + + public function getUploadedFiles(): array + { + return $this->uploadedFiles; + } + + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->uploadedFiles = $uploadedFiles; + + return $new; + } + + public function getCookieParams(): array + { + return $this->cookieParams; + } + + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookieParams = $cookies; + + return $new; + } + + public function getQueryParams(): array + { + return $this->queryParams; + } + + public function withQueryParams(array $query) + { + $new = clone $this; + $new->queryParams = $query; + + return $new; + } + + public function getParsedBody() + { + return $this->parsedBody; + } + + public function withParsedBody($data) + { + if (!\is_array($data) && !\is_object($data) && null !== $data) { + throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null'); + } + + $new = clone $this; + $new->parsedBody = $data; + + return $new; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getAttribute($attribute, $default = null) + { + if (false === \array_key_exists($attribute, $this->attributes)) { + return $default; + } + + return $this->attributes[$attribute]; + } + + public function withAttribute($attribute, $value): self + { + $new = clone $this; + $new->attributes[$attribute] = $value; + + return $new; + } + + public function withoutAttribute($attribute): self + { + if (false === \array_key_exists($attribute, $this->attributes)) { + return $this; + } + + $new = clone $this; + unset($new->attributes[$attribute]); + + return $new; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Stream.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Stream.php new file mode 100644 index 0000000..bcc59ba --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Stream.php @@ -0,0 +1,287 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Stream implements StreamInterface +{ + /** @var resource|null A resource reference */ + private $stream; + + /** @var bool */ + private $seekable; + + /** @var bool */ + private $readable; + + /** @var bool */ + private $writable; + + /** @var array|mixed|void|bool|null */ + private $uri; + + /** @var int|null */ + private $size; + + /** @var array Hash of readable and writable stream types */ + private const READ_WRITE_HASH = [ + 'read' => [ + 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, + 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, + 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a+' => true, + ], + 'write' => [ + 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, + 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, + 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true, + ], + ]; + + private function __construct() + { + } + + /** + * Creates a new PSR-7 stream. + * + * @param string|resource|StreamInterface $body + * + * @throws \InvalidArgumentException + */ + public static function create($body = ''): StreamInterface + { + if ($body instanceof StreamInterface) { + return $body; + } + + if (\is_string($body)) { + $resource = \fopen('php://temp', 'rw+'); + \fwrite($resource, $body); + $body = $resource; + } + + if (\is_resource($body)) { + $new = new self(); + $new->stream = $body; + $meta = \stream_get_meta_data($new->stream); + $new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR); + $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); + $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); + + return $new; + } + + throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.'); + } + + /** + * Closes the stream when the destructed. + */ + public function __destruct() + { + $this->close(); + } + + /** + * @return string + */ + public function __toString() + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + + if (\is_array($errorHandler = \set_error_handler('var_dump'))) { + $errorHandler = $errorHandler[0] ?? null; + } + \restore_error_handler(); + + if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) { + return \trigger_error((string) $e, \E_USER_ERROR); + } + + return ''; + } + } + + public function close(): void + { + if (isset($this->stream)) { + if (\is_resource($this->stream)) { + \fclose($this->stream); + } + $this->detach(); + } + } + + public function detach() + { + if (!isset($this->stream)) { + return null; + } + + $result = $this->stream; + unset($this->stream); + $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + + return $result; + } + + private function getUri() + { + if (false !== $this->uri) { + $this->uri = $this->getMetadata('uri') ?? false; + } + + return $this->uri; + } + + public function getSize(): ?int + { + if (null !== $this->size) { + return $this->size; + } + + if (!isset($this->stream)) { + return null; + } + + // Clear the stat cache if the stream has a URI + if ($uri = $this->getUri()) { + \clearstatcache(true, $uri); + } + + $stats = \fstat($this->stream); + if (isset($stats['size'])) { + $this->size = $stats['size']; + + return $this->size; + } + + return null; + } + + public function tell(): int + { + if (false === $result = \ftell($this->stream)) { + throw new \RuntimeException('Unable to determine stream position'); + } + + return $result; + } + + public function eof(): bool + { + return !$this->stream || \feof($this->stream); + } + + public function isSeekable(): bool + { + return $this->seekable; + } + + public function seek($offset, $whence = \SEEK_SET): void + { + if (!$this->seekable) { + throw new \RuntimeException('Stream is not seekable'); + } + + if (-1 === \fseek($this->stream, $offset, $whence)) { + throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true)); + } + } + + public function rewind(): void + { + $this->seek(0); + } + + public function isWritable(): bool + { + return $this->writable; + } + + public function write($string): int + { + if (!$this->writable) { + throw new \RuntimeException('Cannot write to a non-writable stream'); + } + + // We can't know the size after writing anything + $this->size = null; + + if (false === $result = \fwrite($this->stream, $string)) { + throw new \RuntimeException('Unable to write to stream'); + } + + return $result; + } + + public function isReadable(): bool + { + return $this->readable; + } + + public function read($length): string + { + if (!$this->readable) { + throw new \RuntimeException('Cannot read from non-readable stream'); + } + + if (false === $result = \fread($this->stream, $length)) { + throw new \RuntimeException('Unable to read from stream'); + } + + return $result; + } + + public function getContents(): string + { + if (!isset($this->stream)) { + throw new \RuntimeException('Unable to read stream contents'); + } + + if (false === $contents = \stream_get_contents($this->stream)) { + throw new \RuntimeException('Unable to read stream contents'); + } + + return $contents; + } + + public function getMetadata($key = null) + { + if (!isset($this->stream)) { + return $key ? null : []; + } + + $meta = \stream_get_meta_data($this->stream); + + if (null === $key) { + return $meta; + } + + return $meta[$key] ?? null; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/UploadedFile.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/UploadedFile.php new file mode 100644 index 0000000..415bf87 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/UploadedFile.php @@ -0,0 +1,173 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class UploadedFile implements UploadedFileInterface +{ + /** @var array */ + private const ERRORS = [ + \UPLOAD_ERR_OK => 1, + \UPLOAD_ERR_INI_SIZE => 1, + \UPLOAD_ERR_FORM_SIZE => 1, + \UPLOAD_ERR_PARTIAL => 1, + \UPLOAD_ERR_NO_FILE => 1, + \UPLOAD_ERR_NO_TMP_DIR => 1, + \UPLOAD_ERR_CANT_WRITE => 1, + \UPLOAD_ERR_EXTENSION => 1, + ]; + + /** @var string */ + private $clientFilename; + + /** @var string */ + private $clientMediaType; + + /** @var int */ + private $error; + + /** @var string|null */ + private $file; + + /** @var bool */ + private $moved = false; + + /** @var int */ + private $size; + + /** @var StreamInterface|null */ + private $stream; + + /** + * @param StreamInterface|string|resource $streamOrFile + * @param int $size + * @param int $errorStatus + * @param string|null $clientFilename + * @param string|null $clientMediaType + */ + public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null) + { + if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) { + throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.'); + } + + if (false === \is_int($size)) { + throw new \InvalidArgumentException('Upload file size must be an integer'); + } + + if (null !== $clientFilename && !\is_string($clientFilename)) { + throw new \InvalidArgumentException('Upload file client filename must be a string or null'); + } + + if (null !== $clientMediaType && !\is_string($clientMediaType)) { + throw new \InvalidArgumentException('Upload file client media type must be a string or null'); + } + + $this->error = $errorStatus; + $this->size = $size; + $this->clientFilename = $clientFilename; + $this->clientMediaType = $clientMediaType; + + if (\UPLOAD_ERR_OK === $this->error) { + // Depending on the value set file or stream variable. + if (\is_string($streamOrFile)) { + $this->file = $streamOrFile; + } elseif (\is_resource($streamOrFile)) { + $this->stream = Stream::create($streamOrFile); + } elseif ($streamOrFile instanceof StreamInterface) { + $this->stream = $streamOrFile; + } else { + throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile'); + } + } + } + + /** + * @throws \RuntimeException if is moved or not ok + */ + private function validateActive(): void + { + if (\UPLOAD_ERR_OK !== $this->error) { + throw new \RuntimeException('Cannot retrieve stream due to upload error'); + } + + if ($this->moved) { + throw new \RuntimeException('Cannot retrieve stream after it has already been moved'); + } + } + + public function getStream(): StreamInterface + { + $this->validateActive(); + + if ($this->stream instanceof StreamInterface) { + return $this->stream; + } + + $resource = \fopen($this->file, 'r'); + + return Stream::create($resource); + } + + public function moveTo($targetPath): void + { + $this->validateActive(); + + if (!\is_string($targetPath) || '' === $targetPath) { + throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string'); + } + + if (null !== $this->file) { + $this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath); + } else { + $stream = $this->getStream(); + if ($stream->isSeekable()) { + $stream->rewind(); + } + + // Copy the contents of a stream into another stream until end-of-file. + $dest = Stream::create(\fopen($targetPath, 'w')); + while (!$stream->eof()) { + if (!$dest->write($stream->read(1048576))) { + break; + } + } + + $this->moved = true; + } + + if (false === $this->moved) { + throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath)); + } + } + + public function getSize(): int + { + return $this->size; + } + + public function getError(): int + { + return $this->error; + } + + public function getClientFilename(): ?string + { + return $this->clientFilename; + } + + public function getClientMediaType(): ?string + { + return $this->clientMediaType; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Uri.php b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Uri.php new file mode 100644 index 0000000..eee304f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/nyholm/psr7/src/Uri.php @@ -0,0 +1,312 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Uri implements UriInterface +{ + private const SCHEMES = ['http' => 80, 'https' => 443]; + + private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; + + private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; + + /** @var string Uri scheme. */ + private $scheme = ''; + + /** @var string Uri user info. */ + private $userInfo = ''; + + /** @var string Uri host. */ + private $host = ''; + + /** @var int|null Uri port. */ + private $port; + + /** @var string Uri path. */ + private $path = ''; + + /** @var string Uri query string. */ + private $query = ''; + + /** @var string Uri fragment. */ + private $fragment = ''; + + public function __construct(string $uri = '') + { + if ('' !== $uri) { + if (false === $parts = \parse_url($uri)) { + throw new \InvalidArgumentException("Unable to parse URI: $uri"); + } + + // Apply parse_url parts to a URI. + $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->userInfo = $parts['user'] ?? ''; + $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; + $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; + $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; + $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; + if (isset($parts['pass'])) { + $this->userInfo .= ':' . $parts['pass']; + } + } + } + + public function __toString(): string + { + return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getAuthority(): string + { + if ('' === $this->host) { + return ''; + } + + $authority = $this->host; + if ('' !== $this->userInfo) { + $authority = $this->userInfo . '@' . $authority; + } + + if (null !== $this->port) { + $authority .= ':' . $this->port; + } + + return $authority; + } + + public function getUserInfo(): string + { + return $this->userInfo; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function getPath(): string + { + return $this->path; + } + + public function getQuery(): string + { + return $this->query; + } + + public function getFragment(): string + { + return $this->fragment; + } + + public function withScheme($scheme): self + { + if (!\is_string($scheme)) { + throw new \InvalidArgumentException('Scheme must be a string'); + } + + if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + return $this; + } + + $new = clone $this; + $new->scheme = $scheme; + $new->port = $new->filterPort($new->port); + + return $new; + } + + public function withUserInfo($user, $password = null): self + { + $info = $user; + if (null !== $password && '' !== $password) { + $info .= ':' . $password; + } + + if ($this->userInfo === $info) { + return $this; + } + + $new = clone $this; + $new->userInfo = $info; + + return $new; + } + + public function withHost($host): self + { + if (!\is_string($host)) { + throw new \InvalidArgumentException('Host must be a string'); + } + + if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + return $this; + } + + $new = clone $this; + $new->host = $host; + + return $new; + } + + public function withPort($port): self + { + if ($this->port === $port = $this->filterPort($port)) { + return $this; + } + + $new = clone $this; + $new->port = $port; + + return $new; + } + + public function withPath($path): self + { + if ($this->path === $path = $this->filterPath($path)) { + return $this; + } + + $new = clone $this; + $new->path = $path; + + return $new; + } + + public function withQuery($query): self + { + if ($this->query === $query = $this->filterQueryAndFragment($query)) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + public function withFragment($fragment): self + { + if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + /** + * Create a URI string from its various parts. + */ + private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string + { + $uri = ''; + if ('' !== $scheme) { + $uri .= $scheme . ':'; + } + + if ('' !== $authority) { + $uri .= '//' . $authority; + } + + if ('' !== $path) { + if ('/' !== $path[0]) { + if ('' !== $authority) { + // If the path is rootless and an authority is present, the path MUST be prefixed by "/" + $path = '/' . $path; + } + } elseif (isset($path[1]) && '/' === $path[1]) { + if ('' === $authority) { + // If the path is starting with more than one "/" and no authority is present, the + // starting slashes MUST be reduced to one. + $path = '/' . \ltrim($path, '/'); + } + } + + $uri .= $path; + } + + if ('' !== $query) { + $uri .= '?' . $query; + } + + if ('' !== $fragment) { + $uri .= '#' . $fragment; + } + + return $uri; + } + + /** + * Is a given port non-standard for the current scheme? + */ + private static function isNonStandardPort(string $scheme, int $port): bool + { + return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme]; + } + + private function filterPort($port): ?int + { + if (null === $port) { + return null; + } + + $port = (int) $port; + if (0 > $port || 0xffff < $port) { + throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); + } + + return self::isNonStandardPort($this->scheme, $port) ? $port : null; + } + + private function filterPath($path): string + { + if (!\is_string($path)) { + throw new \InvalidArgumentException('Path must be a string'); + } + + return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); + } + + private function filterQueryAndFragment($str): string + { + if (!\is_string($str)) { + throw new \InvalidArgumentException('Query and fragment must be a string'); + } + + return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); + } + + private static function rawurlencodeMatchZero(array $match): string + { + return \rawurlencode($match[0]); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/.php_cs.dist b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/.php_cs.dist new file mode 100644 index 0000000..24e9bb9 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/.php_cs.dist @@ -0,0 +1,24 @@ +in('src') + ->in('spec') +; +return PhpCsFixer\Config::create() + ->setRules([ + '@PSR2' => true, + '@Symfony' => true, + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'no_empty_phpdoc' => true, + 'phpdoc_to_comment' => false, + 'single_line_throw' => false, + ]) + ->setFinder($finder); diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/CHANGELOG.md new file mode 100644 index 0000000..1a7b201 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/CHANGELOG.md @@ -0,0 +1,247 @@ +# Change Log + +## 2.3.0 - 2020-07-21 + +### Fixed + +- HttpMethodsClient with PSR RequestFactory +- Bug in the cookie plugin with empty cookies +- Bug when parsing null-valued date headers + +### Changed + +- Deprecation when constructing a HttpMethodsClient with PSR RequestFactory but without a StreamFactory + +## 2.2.1 - 2020-07-13 + +### Fixed + +- Support for PHP 8 +- Plugin callable phpdoc + +## 2.2.0 - 2020-07-02 + +### Added + +- Plugin client builder for making a `PluginClient` +- Support for the PSR-17 request factory in `HttpMethodsClient` + +### Changed + +- Restored support for `symfony/options-resolver: ^2.6` +- Consistent implementation of union type checking + +### Fixed + +- Memory leak when using the `PluginClient` with plugins + +## 2.1.0 - 2019-11-18 + +### Added + +- Support Symfony 5 + +## 2.0.0 - 2019-02-03 + +### Changed + +- HttpClientRouter now throws a HttpClientNoMatchException instead of a RequestException if it can not find a client for the request. +- RetryPlugin will only retry exceptions when there is no response, or a response in the 5xx HTTP code range. +- RetryPlugin also retries when no exception is thrown if the responses has HTTP code in the 5xx range. + The callbacks for exception handling have been renamed and callbacks for response handling have been added. +- Abstract method `HttpClientPool::chooseHttpClient()` has now an explicit return type (`Http\Client\Common\HttpClientPoolItem`) +- Interface method `Plugin::handleRequest(...)` has now an explicit return type (`Http\Promise\Promise`) +- Made classes final that are not intended to be extended. +- Added interfaces for BatchClient, HttpClientRouter and HttpMethodsClient. + (These interfaces use the `Interface` suffix to avoid name collisions.) +- Added an interface for HttpClientPool and moved the abstract class to the HttpClientPool sub namespace. +- AddPathPlugin: Do not add the prefix if the URL already has the same prefix. +- All exceptions in `Http\Client\Common\Exception` are final. + +### Removed + +- Deprecated option `debug_plugins` has been removed from `PluginClient` +- Deprecated options `decider` and `delay` have been removed from `RetryPlugin`, use `exception_decider` and `exception_delay` instead. + +## 1.9.1 - 2019-02-02 + +### Added + +- Updated type hints in doc blocks. + +## 1.9.0 - 2019-01-03 + +### Added + +- Support for PSR-18 clients +- Added traits `VersionBridgePlugin` and `VersionBridgeClient` to help plugins and clients to support both + 1.x and 2.x version of `php-http/client-common` and `php-http/httplug`. + +### Changed + +- RetryPlugin: Renamed the configuration options for the exception retry callback from `decider` to `exception_decider` + and `delay` to `exception_delay`. The old names still work but are deprecated. + +## 1.8.2 - 2018-12-14 + +### Changed + +- When multiple cookies exist, a single header with all cookies is sent as per RFC 6265 Section 5.4 +- AddPathPlugin will now trim of ending slashes in paths + +## 1.8.1 - 2018-10-09 + +### Fixed + +- Reverted change to RetryPlugin so it again waits when retrying to avoid "can only throw objects" error. + +## 1.8.0 - 2018-09-21 + +### Added + + - Add an option on ErrorPlugin to only throw exception on response with 5XX status code. + +### Changed + +- AddPathPlugin no longer add prefix multiple times if a request is restarted - it now only adds the prefix if that request chain has not yet passed through the AddPathPlugin +- RetryPlugin no longer wait for retried requests and use a deferred promise instead + +### Fixed + +- Decoder plugin will now remove header when there is no more encoding, instead of setting to an empty array + +## 1.7.0 - 2017-11-30 + +### Added + +- Symfony 4 support + +### Changed + +- Strict comparison in DecoderPlugin + +## 1.6.0 - 2017-10-16 + +### Added + +- Add HttpClientPool client to leverage load balancing and fallback mechanism [see the documentation](http://docs.php-http.org/en/latest/components/client-common.html) for more details. +- `PluginClientFactory` to create `PluginClient` instances. +- Added new option 'delay' for `RetryPlugin`. +- Added new option 'decider' for `RetryPlugin`. +- Supports more cookie date formats in the Cookie Plugin + +### Changed + +- The `RetryPlugin` does now wait between retries. To disable/change this feature you must write something like: + +```php +$plugin = new RetryPlugin(['delay' => function(RequestInterface $request, Exception $e, $retries) { + return 0; +}); +``` + +### Deprecated + +- The `debug_plugins` option for `PluginClient` is deprecated and will be removed in 2.0. Use the decorator design pattern instead like in [ProfilePlugin](https://github.com/php-http/HttplugBundle/blob/de33f9c14252f22093a5ec7d84f17535ab31a384/Collector/ProfilePlugin.php). + +## 1.5.0 - 2017-03-30 + +### Added + +- `QueryDefaultsPlugin` to add default query parameters. + +## 1.4.2 - 2017-03-18 + +### Deprecated + +- `DecoderPlugin` does not longer claim to support `compress` content encoding + +### Fixed + +- `CookiePlugin` allows main domain cookies to be sent/stored for subdomains +- `DecoderPlugin` uses the right `FilteredStream` to handle `deflate` content encoding + + +## 1.4.1 - 2017-02-20 + +### Fixed + +- Cast return value of `StreamInterface::getSize` to string in `ContentLengthPlugin` + + +## 1.4.0 - 2016-11-04 + +### Added + +- Add Path plugin +- Base URI plugin that combines Add Host and Add Path plugins + + +## 1.3.0 - 2016-10-16 + +### Changed + +- Fix Emulated Trait to use Http based promise which respect the HttpAsyncClient interface +- Require Httplug 1.1 where we use HTTP specific promises. +- RedirectPlugin: use the full URL instead of the URI to properly keep track of redirects +- Add AddPathPlugin for API URLs with base path +- Add BaseUriPlugin that combines AddHostPlugin and AddPathPlugin + + +## 1.2.1 - 2016-07-26 + +### Changed + +- AddHostPlugin also sets the port if specified + + +## 1.2.0 - 2016-07-14 + +### Added + +- Suggest separate plugins in composer.json +- Introduced `debug_plugins` option for `PluginClient` + + +## 1.1.0 - 2016-05-04 + +### Added + +- Add a flexible http client providing both contract, and only emulating what's necessary +- HTTP Client Router: route requests to underlying clients +- Plugin client and core plugins moved here from `php-http/plugins` + +### Deprecated + +- Extending client classes, they will be made final in version 2.0 + + +## 1.0.0 - 2016-01-27 + +### Changed + +- Remove useless interface in BatchException + + +## 0.2.0 - 2016-01-12 + +### Changed + +- Updated package files +- Updated HTTPlug to RC1 + + +## 0.1.1 - 2015-12-26 + +### Added + +- Emulated clients + + +## 0.1.0 - 2015-12-25 + +### Added + +- Batch client from utils +- Methods client from utils +- Emulators and decorators from client-tools diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/LICENSE new file mode 100644 index 0000000..4558d6f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2016 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/README.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/README.md new file mode 100644 index 0000000..7d13658 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/README.md @@ -0,0 +1,55 @@ +# HTTP Client Common + +[![Latest Version](https://img.shields.io/github/release/php-http/client-common.svg?style=flat-square)](https://github.com/php-http/client-common/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Build Status](https://img.shields.io/travis/php-http/client-common/master.svg?style=flat-square)](https://travis-ci.org/php-http/client-common) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/client-common.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client-common) +[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/client-common.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client-common) +[![Total Downloads](https://img.shields.io/packagist/dt/php-http/client-common.svg?style=flat-square)](https://packagist.org/packages/php-http/client-common) + +**Common HTTP Client implementations and tools for HTTPlug.** + + +## Install + +Via Composer + +``` bash +$ composer require php-http/client-common +``` + + +## Usage + +This package provides common tools for HTTP Clients: + +- BatchClient to handle sending requests in parallel +- A convenience client with HTTP method names as class methods +- Emulator, decorator layers for sync/async clients + + +## Documentation + +Please see the [official documentation](http://docs.php-http.org/en/latest/components/client-common.html). + + +## Testing + +``` bash +$ composer test +``` + + +## Contributing + +Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). + + +## Security + +If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). + + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/composer.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/composer.json new file mode 100644 index 0000000..7e746f0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/composer.json @@ -0,0 +1,67 @@ +{ + "name": "php-http/client-common", + "description": "Common HTTP Client implementations and tools for HTTPlug", + "license": "MIT", + "keywords": ["http", "client", "httplug", "common"], + "homepage": "http://httplug.io", + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message-factory": "^1.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "symfony/options-resolver": "^2.6 || ^3.4.20 || ~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.0", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "spec\\Http\\Client\\Common\\": "spec/" + } + }, + "scripts": { + "test": [ + "vendor/bin/phpspec run", + "vendor/bin/phpunit" + ], + "test-ci": [ + "vendor/bin/phpspec run -c phpspec.ci.yml", + "vendor/bin/phpunit" + ] + }, + "config": { + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchClient.php new file mode 100644 index 0000000..51f13ff --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchClient.php @@ -0,0 +1,42 @@ +client = $client; + } + + public function sendRequests(array $requests): BatchResult + { + $batchResult = new BatchResult(); + + foreach ($requests as $request) { + try { + $response = $this->client->sendRequest($request); + $batchResult = $batchResult->addResponse($request, $response); + } catch (ClientExceptionInterface $e) { + $batchResult = $batchResult->addException($request, $e); + } + } + + if ($batchResult->hasExceptions()) { + throw new BatchException($batchResult); + } + + return $batchResult; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchClientInterface.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchClientInterface.php new file mode 100644 index 0000000..d42eb2d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchClientInterface.php @@ -0,0 +1,34 @@ + + */ +interface BatchClientInterface +{ + /** + * Send several requests. + * + * You may not assume that the requests are executed in a particular order. If the order matters + * for your application, use sendRequest sequentially. + * + * @param RequestInterface[] $requests The requests to send + * + * @return BatchResult Containing one result per request + * + * @throws BatchException If one or more requests fails. The exception gives access to the + * BatchResult with a map of request to result for success, request to + * exception for failures + */ + public function sendRequests(array $requests): BatchResult; +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchResult.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchResult.php new file mode 100644 index 0000000..45e1535 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/BatchResult.php @@ -0,0 +1,157 @@ + + */ +final class BatchResult +{ + /** + * @var \SplObjectStorage + */ + private $responses; + + /** + * @var \SplObjectStorage + */ + private $exceptions; + + public function __construct() + { + $this->responses = new \SplObjectStorage(); + $this->exceptions = new \SplObjectStorage(); + } + + /** + * Checks if there are any successful responses at all. + */ + public function hasResponses(): bool + { + return $this->responses->count() > 0; + } + + /** + * Returns all successful responses. + * + * @return ResponseInterface[] + */ + public function getResponses(): array + { + $responses = []; + + foreach ($this->responses as $request) { + $responses[] = $this->responses[$request]; + } + + return $responses; + } + + /** + * Checks if there is a successful response for a request. + */ + public function isSuccessful(RequestInterface $request): bool + { + return $this->responses->contains($request); + } + + /** + * Returns the response for a successful request. + * + * @throws \UnexpectedValueException If request was not part of the batch or failed + */ + public function getResponseFor(RequestInterface $request): ResponseInterface + { + try { + return $this->responses[$request]; + } catch (\UnexpectedValueException $e) { + throw new \UnexpectedValueException('Request not found', $e->getCode(), $e); + } + } + + /** + * Adds a response in an immutable way. + * + * @return BatchResult the new BatchResult with this request-response pair added to it + */ + public function addResponse(RequestInterface $request, ResponseInterface $response): self + { + $new = clone $this; + $new->responses->attach($request, $response); + + return $new; + } + + /** + * Checks if there are any unsuccessful requests at all. + */ + public function hasExceptions(): bool + { + return $this->exceptions->count() > 0; + } + + /** + * Returns all exceptions for the unsuccessful requests. + * + * @return ClientExceptionInterface[] + */ + public function getExceptions(): array + { + $exceptions = []; + + foreach ($this->exceptions as $request) { + $exceptions[] = $this->exceptions[$request]; + } + + return $exceptions; + } + + /** + * Checks if there is an exception for a request, meaning the request failed. + */ + public function isFailed(RequestInterface $request): bool + { + return $this->exceptions->contains($request); + } + + /** + * Returns the exception for a failed request. + * + * @throws \UnexpectedValueException If request was not part of the batch or was successful + */ + public function getExceptionFor(RequestInterface $request): ClientExceptionInterface + { + try { + return $this->exceptions[$request]; + } catch (\UnexpectedValueException $e) { + throw new \UnexpectedValueException('Request not found', $e->getCode(), $e); + } + } + + /** + * Adds an exception in an immutable way. + * + * @return BatchResult the new BatchResult with this request-exception pair added to it + */ + public function addException(RequestInterface $request, ClientExceptionInterface $exception): self + { + $new = clone $this; + $new->exceptions->attach($request, $exception); + + return $new; + } + + public function __clone() + { + $this->responses = clone $this->responses; + $this->exceptions = clone $this->exceptions; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Deferred.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Deferred.php new file mode 100644 index 0000000..ef2b309 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Deferred.php @@ -0,0 +1,152 @@ +waitCallback = $waitCallback; + $this->state = Promise::PENDING; + $this->onFulfilledCallbacks = []; + $this->onRejectedCallbacks = []; + } + + /** + * {@inheritdoc} + */ + public function then(callable $onFulfilled = null, callable $onRejected = null): Promise + { + $deferred = new self($this->waitCallback); + + $this->onFulfilledCallbacks[] = function (ResponseInterface $response) use ($onFulfilled, $deferred) { + try { + if (null !== $onFulfilled) { + $response = $onFulfilled($response); + } + $deferred->resolve($response); + } catch (ClientExceptionInterface $exception) { + $deferred->reject($exception); + } + }; + + $this->onRejectedCallbacks[] = function (ClientExceptionInterface $exception) use ($onRejected, $deferred) { + try { + if (null !== $onRejected) { + $response = $onRejected($exception); + $deferred->resolve($response); + + return; + } + $deferred->reject($exception); + } catch (ClientExceptionInterface $newException) { + $deferred->reject($newException); + } + }; + + return $deferred; + } + + /** + * {@inheritdoc} + */ + public function getState(): string + { + return $this->state; + } + + /** + * Resolve this deferred with a Response. + */ + public function resolve(ResponseInterface $response): void + { + if (Promise::PENDING !== $this->state) { + return; + } + + $this->value = $response; + $this->state = Promise::FULFILLED; + + foreach ($this->onFulfilledCallbacks as $onFulfilledCallback) { + $onFulfilledCallback($response); + } + } + + /** + * Reject this deferred with an Exception. + */ + public function reject(ClientExceptionInterface $exception): void + { + if (Promise::PENDING !== $this->state) { + return; + } + + $this->failure = $exception; + $this->state = Promise::REJECTED; + + foreach ($this->onRejectedCallbacks as $onRejectedCallback) { + $onRejectedCallback($exception); + } + } + + /** + * {@inheritdoc} + */ + public function wait($unwrap = true) + { + if (Promise::PENDING === $this->state) { + $callback = $this->waitCallback; + $callback(); + } + + if (!$unwrap) { + return null; + } + + if (Promise::FULFILLED === $this->state) { + return $this->value; + } + + /** @var ClientExceptionInterface */ + throw $this->failure; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/EmulatedHttpAsyncClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/EmulatedHttpAsyncClient.php new file mode 100644 index 0000000..008f888 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/EmulatedHttpAsyncClient.php @@ -0,0 +1,25 @@ + + */ +final class EmulatedHttpAsyncClient implements HttpClient, HttpAsyncClient +{ + use HttpAsyncClientEmulator; + use HttpClientDecorator; + + public function __construct(ClientInterface $httpClient) + { + $this->httpClient = $httpClient; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/EmulatedHttpClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/EmulatedHttpClient.php new file mode 100644 index 0000000..5c2d8c4 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/EmulatedHttpClient.php @@ -0,0 +1,24 @@ + + */ +final class EmulatedHttpClient implements HttpClient, HttpAsyncClient +{ + use HttpAsyncClientDecorator; + use HttpClientEmulator; + + public function __construct(HttpAsyncClient $httpAsyncClient) + { + $this->httpAsyncClient = $httpAsyncClient; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/BatchException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/BatchException.php new file mode 100644 index 0000000..a9cb08c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/BatchException.php @@ -0,0 +1,37 @@ + + */ +final class BatchException extends TransferException +{ + /** + * @var BatchResult + */ + private $result; + + public function __construct(BatchResult $result) + { + $this->result = $result; + parent::__construct(); + } + + /** + * Returns the BatchResult that contains all responses and exceptions. + */ + public function getResult(): BatchResult + { + return $this->result; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/CircularRedirectionException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/CircularRedirectionException.php new file mode 100644 index 0000000..9db927c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/CircularRedirectionException.php @@ -0,0 +1,16 @@ + + */ +final class CircularRedirectionException extends HttpException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/ClientErrorException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/ClientErrorException.php new file mode 100644 index 0000000..c657a3f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/ClientErrorException.php @@ -0,0 +1,16 @@ + + */ +final class ClientErrorException extends HttpException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/HttpClientNoMatchException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/HttpClientNoMatchException.php new file mode 100644 index 0000000..682c5dd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/HttpClientNoMatchException.php @@ -0,0 +1,33 @@ + + */ +final class HttpClientNoMatchException extends TransferException +{ + /** + * @var RequestInterface + */ + private $request; + + public function __construct(string $message, RequestInterface $request, \Exception $previous = null) + { + $this->request = $request; + + parent::__construct($message, 0, $previous); + } + + public function getRequest(): RequestInterface + { + return $this->request; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/HttpClientNotFoundException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/HttpClientNotFoundException.php new file mode 100644 index 0000000..509daa5 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/HttpClientNotFoundException.php @@ -0,0 +1,16 @@ + + */ +final class HttpClientNotFoundException extends TransferException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/LoopException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/LoopException.php new file mode 100644 index 0000000..f4e173f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/LoopException.php @@ -0,0 +1,16 @@ + + */ +final class LoopException extends RequestException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/MultipleRedirectionException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/MultipleRedirectionException.php new file mode 100644 index 0000000..bf6c9f7 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/MultipleRedirectionException.php @@ -0,0 +1,16 @@ + + */ +final class MultipleRedirectionException extends HttpException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/ServerErrorException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/ServerErrorException.php new file mode 100644 index 0000000..774b97f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Exception/ServerErrorException.php @@ -0,0 +1,16 @@ + + */ +final class ServerErrorException extends HttpException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/FlexibleHttpClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/FlexibleHttpClient.php new file mode 100644 index 0000000..c1e327f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/FlexibleHttpClient.php @@ -0,0 +1,36 @@ + + */ +final class FlexibleHttpClient implements HttpClient, HttpAsyncClient +{ + use HttpClientDecorator; + use HttpAsyncClientDecorator; + + /** + * @param ClientInterface|HttpAsyncClient $client + */ + public function __construct($client) + { + if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) { + throw new \TypeError( + sprintf('%s::__construct(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + + $this->httpClient = $client instanceof ClientInterface ? $client : new EmulatedHttpClient($client); + $this->httpAsyncClient = $client instanceof HttpAsyncClient ? $client : new EmulatedHttpAsyncClient($client); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpAsyncClientDecorator.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpAsyncClientDecorator.php new file mode 100644 index 0000000..2714b4a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpAsyncClientDecorator.php @@ -0,0 +1,31 @@ + + */ +trait HttpAsyncClientDecorator +{ + /** + * @var HttpAsyncClient + */ + protected $httpAsyncClient; + + /** + * {@inheritdoc} + * + * @see HttpAsyncClient::sendAsyncRequest + */ + public function sendAsyncRequest(RequestInterface $request) + { + return $this->httpAsyncClient->sendAsyncRequest($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpAsyncClientEmulator.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpAsyncClientEmulator.php new file mode 100644 index 0000000..53c2535 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpAsyncClientEmulator.php @@ -0,0 +1,39 @@ + + */ +trait HttpAsyncClientEmulator +{ + /** + * {@inheritdoc} + * + * @see HttpClient::sendRequest + */ + abstract public function sendRequest(RequestInterface $request): ResponseInterface; + + /** + * {@inheritdoc} + * + * @see HttpAsyncClient::sendAsyncRequest + */ + public function sendAsyncRequest(RequestInterface $request) + { + try { + return new Promise\HttpFulfilledPromise($this->sendRequest($request)); + } catch (Exception $e) { + return new Promise\HttpRejectedPromise($e); + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientDecorator.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientDecorator.php new file mode 100644 index 0000000..c00ba6f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientDecorator.php @@ -0,0 +1,32 @@ + + */ +trait HttpClientDecorator +{ + /** + * @var ClientInterface + */ + protected $httpClient; + + /** + * {@inheritdoc} + * + * @see ClientInterface::sendRequest + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->httpClient->sendRequest($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientEmulator.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientEmulator.php new file mode 100644 index 0000000..51e2c05 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientEmulator.php @@ -0,0 +1,35 @@ + + */ +trait HttpClientEmulator +{ + /** + * {@inheritdoc} + * + * @see HttpClient::sendRequest + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + $promise = $this->sendAsyncRequest($request); + + return $promise->wait(); + } + + /** + * {@inheritdoc} + * + * @see HttpAsyncClient::sendAsyncRequest + */ + abstract public function sendAsyncRequest(RequestInterface $request); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool.php new file mode 100644 index 0000000..24ab421 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool.php @@ -0,0 +1,24 @@ +clientPool[] = $client; + } + + /** + * Return an http client given a specific strategy. + * + * @throws HttpClientNotFoundException When no http client has been found into the pool + * + * @return HttpClientPoolItem Return a http client that can do both sync or async + */ + abstract protected function chooseHttpClient(): HttpClientPoolItem; + + /** + * {@inheritdoc} + */ + public function sendAsyncRequest(RequestInterface $request) + { + return $this->chooseHttpClient()->sendAsyncRequest($request); + } + + /** + * {@inheritdoc} + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->chooseHttpClient()->sendRequest($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/HttpClientPoolItem.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/HttpClientPoolItem.php new file mode 100644 index 0000000..f29d065 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/HttpClientPoolItem.php @@ -0,0 +1,181 @@ + + */ +class HttpClientPoolItem implements HttpClient, HttpAsyncClient +{ + /** + * @var int Number of request this client is currently sending + */ + private $sendingRequestCount = 0; + + /** + * @var \DateTime|null Time when this client has been disabled or null if enable + */ + private $disabledAt; + + /** + * Number of seconds until this client is enabled again after an error. + * + * null: never reenable this client. + * + * @var int|null + */ + private $reenableAfter; + + /** + * @var FlexibleHttpClient A http client responding to async and sync request + */ + private $client; + + /** + * @param ClientInterface|HttpAsyncClient $client + * @param int|null $reenableAfter Number of seconds until this client is enabled again after an error + */ + public function __construct($client, int $reenableAfter = null) + { + if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) { + throw new \TypeError( + sprintf('%s::__construct(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + + $this->client = new FlexibleHttpClient($client); + $this->reenableAfter = $reenableAfter; + } + + /** + * {@inheritdoc} + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + if ($this->isDisabled()) { + throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request); + } + + try { + $this->incrementRequestCount(); + $response = $this->client->sendRequest($request); + $this->decrementRequestCount(); + } catch (Exception $e) { + $this->disable(); + $this->decrementRequestCount(); + + throw $e; + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function sendAsyncRequest(RequestInterface $request) + { + if ($this->isDisabled()) { + throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request); + } + + $this->incrementRequestCount(); + + return $this->client->sendAsyncRequest($request)->then(function ($response) { + $this->decrementRequestCount(); + + return $response; + }, function ($exception) { + $this->disable(); + $this->decrementRequestCount(); + + throw $exception; + }); + } + + /** + * Whether this client is disabled or not. + * + * If the client was disabled, calling this method checks if the client can + * be reenabled and if so enables it. + */ + public function isDisabled(): bool + { + if (null !== $this->reenableAfter && null !== $this->disabledAt) { + // Reenable after a certain time + $now = new \DateTime(); + + if (($now->getTimestamp() - $this->disabledAt->getTimestamp()) >= $this->reenableAfter) { + $this->enable(); + + return false; + } + + return true; + } + + return null !== $this->disabledAt; + } + + /** + * Get current number of request that are currently being sent by the underlying HTTP client. + */ + public function getSendingRequestCount(): int + { + return $this->sendingRequestCount; + } + + /** + * Increment the request count. + */ + private function incrementRequestCount(): void + { + ++$this->sendingRequestCount; + } + + /** + * Decrement the request count. + */ + private function decrementRequestCount(): void + { + --$this->sendingRequestCount; + } + + /** + * Enable the current client. + */ + private function enable(): void + { + $this->disabledAt = null; + } + + /** + * Disable the current client. + */ + private function disable(): void + { + $this->disabledAt = new \DateTime('now'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/LeastUsedClientPool.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/LeastUsedClientPool.php new file mode 100644 index 0000000..789c357 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/LeastUsedClientPool.php @@ -0,0 +1,45 @@ + + */ +final class LeastUsedClientPool extends HttpClientPool +{ + /** + * {@inheritdoc} + */ + protected function chooseHttpClient(): HttpClientPoolItem + { + $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { + return !$clientPoolItem->isDisabled(); + }); + + if (0 === count($clientPool)) { + throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); + } + + usort($clientPool, function (HttpClientPoolItem $clientA, HttpClientPoolItem $clientB) { + if ($clientA->getSendingRequestCount() === $clientB->getSendingRequestCount()) { + return 0; + } + + if ($clientA->getSendingRequestCount() < $clientB->getSendingRequestCount()) { + return -1; + } + + return 1; + }); + + return reset($clientPool); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/RandomClientPool.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/RandomClientPool.php new file mode 100644 index 0000000..789ba42 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/RandomClientPool.php @@ -0,0 +1,31 @@ + + */ +final class RandomClientPool extends HttpClientPool +{ + /** + * {@inheritdoc} + */ + protected function chooseHttpClient(): HttpClientPoolItem + { + $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { + return !$clientPoolItem->isDisabled(); + }); + + if (0 === count($clientPool)) { + throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); + } + + return $clientPool[array_rand($clientPool)]; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/RoundRobinClientPool.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/RoundRobinClientPool.php new file mode 100644 index 0000000..7c7b191 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientPool/RoundRobinClientPool.php @@ -0,0 +1,42 @@ + + */ +final class RoundRobinClientPool extends HttpClientPool +{ + /** + * {@inheritdoc} + */ + protected function chooseHttpClient(): HttpClientPoolItem + { + $last = current($this->clientPool); + + do { + $client = next($this->clientPool); + + if (false === $client) { + $client = reset($this->clientPool); + + if (false === $client) { + throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); + } + } + + // Case when there is only one and the last one has been disabled + if ($last === $client && $client->isDisabled()) { + throw new HttpClientNotFoundException('Cannot choose a http client as there is no one enabled in the pool'); + } + } while ($client->isDisabled()); + + return $client; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientRouter.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientRouter.php new file mode 100644 index 0000000..040d893 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientRouter.php @@ -0,0 +1,74 @@ + + */ +final class HttpClientRouter implements HttpClientRouterInterface +{ + /** + * @var (array{matcher: RequestMatcher, client: FlexibleHttpClient})[] + */ + private $clients = []; + + /** + * {@inheritdoc} + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->chooseHttpClient($request)->sendRequest($request); + } + + /** + * {@inheritdoc} + */ + public function sendAsyncRequest(RequestInterface $request) + { + return $this->chooseHttpClient($request)->sendAsyncRequest($request); + } + + /** + * Add a client to the router. + * + * @param ClientInterface|HttpAsyncClient $client + */ + public function addClient($client, RequestMatcher $requestMatcher): void + { + if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) { + throw new \TypeError( + sprintf('%s::addClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + + $this->clients[] = [ + 'matcher' => $requestMatcher, + 'client' => new FlexibleHttpClient($client), + ]; + } + + /** + * Choose an HTTP client given a specific request. + */ + private function chooseHttpClient(RequestInterface $request): FlexibleHttpClient + { + foreach ($this->clients as $client) { + if ($client['matcher']->matches($request)) { + return $client['client']; + } + } + + throw new HttpClientNoMatchException('No client found for the specified request', $request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientRouterInterface.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientRouterInterface.php new file mode 100644 index 0000000..ae012cf --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpClientRouterInterface.php @@ -0,0 +1,27 @@ + + */ +interface HttpClientRouterInterface extends HttpClient, HttpAsyncClient +{ + /** + * Add a client to the router. + * + * @param ClientInterface|HttpAsyncClient $client + */ + public function addClient($client, RequestMatcher $requestMatcher): void; +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpMethodsClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpMethodsClient.php new file mode 100644 index 0000000..f62b187 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpMethodsClient.php @@ -0,0 +1,150 @@ +httpClient = $httpClient; + $this->requestFactory = $requestFactory; + $this->streamFactory = $streamFactory; + } + + public function get($uri, array $headers = []): ResponseInterface + { + return $this->send('GET', $uri, $headers, null); + } + + public function head($uri, array $headers = []): ResponseInterface + { + return $this->send('HEAD', $uri, $headers, null); + } + + public function trace($uri, array $headers = []): ResponseInterface + { + return $this->send('TRACE', $uri, $headers, null); + } + + public function post($uri, array $headers = [], $body = null): ResponseInterface + { + return $this->send('POST', $uri, $headers, $body); + } + + public function put($uri, array $headers = [], $body = null): ResponseInterface + { + return $this->send('PUT', $uri, $headers, $body); + } + + public function patch($uri, array $headers = [], $body = null): ResponseInterface + { + return $this->send('PATCH', $uri, $headers, $body); + } + + public function delete($uri, array $headers = [], $body = null): ResponseInterface + { + return $this->send('DELETE', $uri, $headers, $body); + } + + public function options($uri, array $headers = [], $body = null): ResponseInterface + { + return $this->send('OPTIONS', $uri, $headers, $body); + } + + public function send(string $method, $uri, array $headers = [], $body = null): ResponseInterface + { + if (!is_string($uri) && !$uri instanceof UriInterface) { + throw new \TypeError( + sprintf('%s::send(): Argument #2 ($uri) must be of type string|%s, %s given', self::class, UriInterface::class, get_debug_type($uri)) + ); + } + + if (!is_string($body) && !$body instanceof StreamInterface && null !== $body) { + throw new \TypeError( + sprintf('%s::send(): Argument #4 ($body) must be of type string|%s|null, %s given', self::class, StreamInterface::class, get_debug_type($body)) + ); + } + + return $this->sendRequest( + self::createRequest($method, $uri, $headers, $body) + ); + } + + /** + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + */ + private function createRequest(string $method, $uri, array $headers = [], $body = null): RequestInterface + { + if ($this->requestFactory instanceof RequestFactory) { + return $this->requestFactory->createRequest( + $method, + $uri, + $headers, + $body + ); + } + + if (is_string($body) && '' !== $body && null === $this->streamFactory) { + throw new \RuntimeException('Cannot create request: A stream factory is required to create a request with a non-empty string body.'); + } + + $request = $this->requestFactory->createRequest($method, $uri); + + foreach ($headers as $key => $value) { + $request = $request->withHeader($key, $value); + } + + if (null !== $body && '' !== $body) { + $request = $request->withBody( + is_string($body) ? $this->streamFactory->createStream($body) : $body + ); + } + + return $request; + } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->httpClient->sendRequest($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpMethodsClientInterface.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpMethodsClientInterface.php new file mode 100644 index 0000000..bc0829a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/HttpMethodsClientInterface.php @@ -0,0 +1,116 @@ +get('/foo') + * ->post('/bar') + * ; + * + * The client also exposes the sendRequest methods of the wrapped HttpClient. + * + * @author Márk Sági-Kazár + * @author David Buchmann + */ +interface HttpMethodsClientInterface extends HttpClient +{ + /** + * Sends a GET request. + * + * @param string|UriInterface $uri + * + * @throws Exception + */ + public function get($uri, array $headers = []): ResponseInterface; + + /** + * Sends an HEAD request. + * + * @param string|UriInterface $uri + * + * @throws Exception + */ + public function head($uri, array $headers = []): ResponseInterface; + + /** + * Sends a TRACE request. + * + * @param string|UriInterface $uri + * + * @throws Exception + */ + public function trace($uri, array $headers = []): ResponseInterface; + + /** + * Sends a POST request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function post($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends a PUT request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function put($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends a PATCH request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function patch($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends a DELETE request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function delete($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends an OPTIONS request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function options($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends a request with any HTTP method. + * + * @param string $method HTTP method to use + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function send(string $method, $uri, array $headers = [], $body = null): ResponseInterface; +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin.php new file mode 100644 index 0000000..99898b9 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin.php @@ -0,0 +1,33 @@ + + */ +interface Plugin +{ + /** + * Handle the request and return the response coming from the next callable. + * + * @see http://docs.php-http.org/en/latest/plugins/build-your-own.html + * + * @param callable(RequestInterface): Promise $next Next middleware in the chain, the request is passed as the first argument + * @param callable(RequestInterface): Promise $first First middleware in the chain, used to to restart a request + * + * @return Promise Resolves a PSR-7 Response or fails with an Http\Client\Exception (The same as HttpAsyncClient) + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise; +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AddHostPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AddHostPlugin.php new file mode 100644 index 0000000..33ede17 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AddHostPlugin.php @@ -0,0 +1,76 @@ + + */ +final class AddHostPlugin implements Plugin +{ + /** + * @var UriInterface + */ + private $host; + + /** + * @var bool + */ + private $replace; + + /** + * @param array $config { + * + * @var bool $replace True will replace all hosts, false will only add host when none is specified. + * } + */ + public function __construct(UriInterface $host, array $config = []) + { + if ('' === $host->getHost()) { + throw new \LogicException('Host can not be empty'); + } + + $this->host = $host; + + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + $options = $resolver->resolve($config); + + $this->replace = $options['replace']; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + if ($this->replace || '' === $request->getUri()->getHost()) { + $uri = $request->getUri() + ->withHost($this->host->getHost()) + ->withScheme($this->host->getScheme()) + ->withPort($this->host->getPort()) + ; + + $request = $request->withUri($uri); + } + + return $next($request); + } + + private function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'replace' => false, + ]); + $resolver->setAllowedTypes('replace', 'bool'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AddPathPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AddPathPlugin.php new file mode 100644 index 0000000..9d43104 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AddPathPlugin.php @@ -0,0 +1,78 @@ + + */ +final class AddPathPlugin implements Plugin +{ + /** + * @var UriInterface + */ + private $uri; + + public function __construct(UriInterface $uri) + { + if ('' === $uri->getPath()) { + throw new \LogicException('URI path cannot be empty'); + } + + if ('/' === substr($uri->getPath(), -1)) { + $uri = $uri->withPath(rtrim($uri->getPath(), '/')); + } + + $this->uri = $uri; + } + + /** + * Adds a prefix in the beginning of the URL's path. + * + * The prefix is not added if that prefix is already on the URL's path. This will fail on the edge + * case of the prefix being repeated, for example if `https://example.com/api/api/foo` is a valid + * URL on the server and the configured prefix is `/api`. + * + * We looked at other solutions, but they are all much more complicated, while still having edge + * cases: + * - Doing an spl_object_hash on `$first` will lead to collisions over time because over time the + * hash can collide. + * - Have the PluginClient provide a magic header to identify the request chain and only apply + * this plugin once. + * + * There are 2 reasons for the AddPathPlugin to be executed twice on the same request: + * - A plugin can restart the chain by calling `$first`, e.g. redirect + * - A plugin can call `$next` more than once, e.g. retry + * + * Depending on the scenario, the path should or should not be added. E.g. `$first` could + * be called after a redirect response from the server. The server likely already has the + * correct path. + * + * No solution fits all use cases. This implementation will work fine for the common use cases. + * If you have a specific situation where this is not the right thing, you can build a custom plugin + * that does exactly what you need. + * + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $prepend = $this->uri->getPath(); + $path = $request->getUri()->getPath(); + + if (substr($path, 0, strlen($prepend)) !== $prepend) { + $request = $request->withUri($request->getUri() + ->withPath($prepend.$path) + ); + } + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AuthenticationPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AuthenticationPlugin.php new file mode 100644 index 0000000..ce9d4bd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/AuthenticationPlugin.php @@ -0,0 +1,38 @@ + + */ +final class AuthenticationPlugin implements Plugin +{ + /** + * @var Authentication An authentication system + */ + private $authentication; + + public function __construct(Authentication $authentication) + { + $this->authentication = $authentication; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $request = $this->authentication->authenticate($request); + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/BaseUriPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/BaseUriPlugin.php new file mode 100644 index 0000000..34c3b64 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/BaseUriPlugin.php @@ -0,0 +1,57 @@ + + */ +final class BaseUriPlugin implements Plugin +{ + /** + * @var AddHostPlugin + */ + private $addHostPlugin; + + /** + * @var AddPathPlugin|null + */ + private $addPathPlugin = null; + + /** + * @param UriInterface $uri Has to contain a host name and can have a path + * @param array $hostConfig Config for AddHostPlugin. @see AddHostPlugin::configureOptions + */ + public function __construct(UriInterface $uri, array $hostConfig = []) + { + $this->addHostPlugin = new AddHostPlugin($uri, $hostConfig); + + if (rtrim($uri->getPath(), '/')) { + $this->addPathPlugin = new AddPathPlugin($uri); + } + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $addHostNext = function (RequestInterface $request) use ($next, $first) { + return $this->addHostPlugin->handleRequest($request, $next, $first); + }; + + if ($this->addPathPlugin) { + return $this->addPathPlugin->handleRequest($request, $addHostNext, $first); + } + + return $addHostNext($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ContentLengthPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ContentLengthPlugin.php new file mode 100644 index 0000000..f313c33 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ContentLengthPlugin.php @@ -0,0 +1,39 @@ + + */ +final class ContentLengthPlugin implements Plugin +{ + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + if (!$request->hasHeader('Content-Length')) { + $stream = $request->getBody(); + + // Cannot determine the size so we use a chunk stream + if (null === $stream->getSize()) { + $stream = new ChunkStream($stream); + $request = $request->withBody($stream); + $request = $request->withAddedHeader('Transfer-Encoding', 'chunked'); + } else { + $request = $request->withHeader('Content-Length', (string) $stream->getSize()); + } + } + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ContentTypePlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ContentTypePlugin.php new file mode 100644 index 0000000..c3fb25c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ContentTypePlugin.php @@ -0,0 +1,122 @@ + + */ +final class ContentTypePlugin implements Plugin +{ + /** + * Allow to disable the content type detection when stream is too large (as it can consume a lot of resource). + * + * @var bool + * + * true skip the content type detection + * false detect the content type (default value) + */ + private $skipDetection; + + /** + * Determine the size stream limit for which the detection as to be skipped (default to 16Mb). + * + * @var int + */ + private $sizeLimit; + + /** + * @param array $config { + * + * @var bool $skip_detection true skip detection if stream size is bigger than $size_limit + * @var int $size_limit size stream limit for which the detection as to be skipped. + * } + */ + public function __construct(array $config = []) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'skip_detection' => false, + 'size_limit' => 16000000, + ]); + $resolver->setAllowedTypes('skip_detection', 'bool'); + $resolver->setAllowedTypes('size_limit', 'int'); + + $options = $resolver->resolve($config); + + $this->skipDetection = $options['skip_detection']; + $this->sizeLimit = $options['size_limit']; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + if (!$request->hasHeader('Content-Type')) { + $stream = $request->getBody(); + $streamSize = $stream->getSize(); + + if (!$stream->isSeekable()) { + return $next($request); + } + + if (0 === $streamSize) { + return $next($request); + } + + if ($this->skipDetection && (null === $streamSize || $streamSize >= $this->sizeLimit)) { + return $next($request); + } + + if ($this->isJson($stream)) { + $request = $request->withHeader('Content-Type', 'application/json'); + + return $next($request); + } + + if ($this->isXml($stream)) { + $request = $request->withHeader('Content-Type', 'application/xml'); + + return $next($request); + } + } + + return $next($request); + } + + private function isJson(StreamInterface $stream): bool + { + if (!function_exists('json_decode')) { + return false; + } + $stream->rewind(); + + json_decode($stream->getContents()); + + return JSON_ERROR_NONE === json_last_error(); + } + + private function isXml(StreamInterface $stream): bool + { + if (!function_exists('simplexml_load_string')) { + return false; + } + $stream->rewind(); + + $previousValue = libxml_use_internal_errors(true); + $isXml = simplexml_load_string($stream->getContents()); + libxml_use_internal_errors($previousValue); + + return false !== $isXml; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/CookiePlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/CookiePlugin.php new file mode 100644 index 0000000..d0c862b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/CookiePlugin.php @@ -0,0 +1,180 @@ + + */ +final class CookiePlugin implements Plugin +{ + /** + * Cookie storage. + * + * @var CookieJar + */ + private $cookieJar; + + public function __construct(CookieJar $cookieJar) + { + $this->cookieJar = $cookieJar; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $cookies = []; + foreach ($this->cookieJar->getCookies() as $cookie) { + if ($cookie->isExpired()) { + continue; + } + + if (!$cookie->matchDomain($request->getUri()->getHost())) { + continue; + } + + if (!$cookie->matchPath($request->getUri()->getPath())) { + continue; + } + + if ($cookie->isSecure() && ('https' !== $request->getUri()->getScheme())) { + continue; + } + + $cookies[] = sprintf('%s=%s', $cookie->getName(), $cookie->getValue()); + } + + if (!empty($cookies)) { + $request = $request->withAddedHeader('Cookie', implode('; ', array_unique($cookies))); + } + + return $next($request)->then(function (ResponseInterface $response) use ($request) { + if ($response->hasHeader('Set-Cookie')) { + $setCookies = $response->getHeader('Set-Cookie'); + + foreach ($setCookies as $setCookie) { + $cookie = $this->createCookie($request, $setCookie); + + // Cookie invalid do not use it + if (null === $cookie) { + continue; + } + + // Restrict setting cookie from another domain + if (!preg_match("/\.{$cookie->getDomain()}$/", '.'.$request->getUri()->getHost())) { + continue; + } + + $this->cookieJar->addCookie($cookie); + } + } + + return $response; + }); + } + + /** + * Creates a cookie from a string. + * + * @throws TransferException + */ + private function createCookie(RequestInterface $request, string $setCookieHeader): ?Cookie + { + $parts = array_map('trim', explode(';', $setCookieHeader)); + + if (empty($parts) || !strpos($parts[0], '=')) { + return null; + } + + list($name, $cookieValue) = $this->createValueKey(array_shift($parts)); + + $maxAge = null; + $expires = null; + $domain = $request->getUri()->getHost(); + $path = $request->getUri()->getPath(); + $secure = false; + $httpOnly = false; + + // Add the cookie pieces into the parsed data array + foreach ($parts as $part) { + list($key, $value) = $this->createValueKey($part); + + switch (strtolower($key)) { + case 'expires': + try { + $expires = CookieUtil::parseDate((string) $value); + } catch (UnexpectedValueException $e) { + throw new TransferException( + sprintf( + 'Cookie header `%s` expires value `%s` could not be converted to date', + $name, + $value + ), + 0, + $e + ); + } + + break; + + case 'max-age': + $maxAge = (int) $value; + + break; + + case 'domain': + $domain = $value; + + break; + + case 'path': + $path = $value; + + break; + + case 'secure': + $secure = true; + + break; + + case 'httponly': + $httpOnly = true; + + break; + } + } + + return new Cookie($name, $cookieValue, $maxAge, $domain, $path, $secure, $httpOnly, $expires); + } + + /** + * Separates key/value pair from cookie. + * + * @param string $part A single cookie value in format key=value + * + * @return array{0:string, 1:?string} + */ + private function createValueKey(string $part): array + { + $parts = explode('=', $part, 2); + $key = trim($parts[0]); + $value = isset($parts[1]) ? trim($parts[1]) : null; + + return [$key, $value]; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/DecoderPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/DecoderPlugin.php new file mode 100644 index 0000000..271ad3c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/DecoderPlugin.php @@ -0,0 +1,135 @@ + + */ +final class DecoderPlugin implements Plugin +{ + /** + * @var bool Whether this plugin decode stream with value in the Content-Encoding header (default to true). + * + * If set to false only the Transfer-Encoding header will be used + */ + private $useContentEncoding; + + /** + * @param array $config { + * + * @var bool $use_content_encoding Whether this plugin should look at the Content-Encoding header first or only at the Transfer-Encoding (defaults to true). + * } + */ + public function __construct(array $config = []) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'use_content_encoding' => true, + ]); + $resolver->setAllowedTypes('use_content_encoding', 'bool'); + $options = $resolver->resolve($config); + + $this->useContentEncoding = $options['use_content_encoding']; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $encodings = extension_loaded('zlib') ? ['gzip', 'deflate'] : ['identity']; + + if ($this->useContentEncoding) { + $request = $request->withHeader('Accept-Encoding', $encodings); + } + $encodings[] = 'chunked'; + $request = $request->withHeader('TE', $encodings); + + return $next($request)->then(function (ResponseInterface $response) { + return $this->decodeResponse($response); + }); + } + + /** + * Decode a response body given its Transfer-Encoding or Content-Encoding value. + */ + private function decodeResponse(ResponseInterface $response): ResponseInterface + { + $response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response); + + if ($this->useContentEncoding) { + $response = $this->decodeOnEncodingHeader('Content-Encoding', $response); + } + + return $response; + } + + /** + * Decode a response on a specific header (content encoding or transfer encoding mainly). + */ + private function decodeOnEncodingHeader(string $headerName, ResponseInterface $response): ResponseInterface + { + if ($response->hasHeader($headerName)) { + $encodings = $response->getHeader($headerName); + $newEncodings = []; + + while ($encoding = array_pop($encodings)) { + $stream = $this->decorateStream($encoding, $response->getBody()); + + if (false === $stream) { + array_unshift($newEncodings, $encoding); + + continue; + } + + $response = $response->withBody($stream); + } + + if (\count($newEncodings) > 0) { + $response = $response->withHeader($headerName, $newEncodings); + } else { + $response = $response->withoutHeader($headerName); + } + } + + return $response; + } + + /** + * Decorate a stream given an encoding. + * + * @return StreamInterface|false A new stream interface or false if encoding is not supported + */ + private function decorateStream(string $encoding, StreamInterface $stream) + { + if ('chunked' === strtolower($encoding)) { + return new Encoding\DechunkStream($stream); + } + + if ('deflate' === strtolower($encoding)) { + return new Encoding\DecompressStream($stream); + } + + if ('gzip' === strtolower($encoding)) { + return new Encoding\GzipDecodeStream($stream); + } + + return false; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ErrorPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ErrorPlugin.php new file mode 100644 index 0000000..a187295 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ErrorPlugin.php @@ -0,0 +1,92 @@ + + */ +final class ErrorPlugin implements Plugin +{ + /** + * @var bool Whether this plugin should only throw 5XX Exceptions (default to false). + * + * If set to true 4XX Responses code will never throw an exception + */ + private $onlyServerException; + + /** + * @param array $config { + * + * @var bool only_server_exception Whether this plugin should only throw 5XX Exceptions (default to false). + * } + */ + public function __construct(array $config = []) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'only_server_exception' => false, + ]); + $resolver->setAllowedTypes('only_server_exception', 'bool'); + $options = $resolver->resolve($config); + + $this->onlyServerException = $options['only_server_exception']; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $promise = $next($request); + + return $promise->then(function (ResponseInterface $response) use ($request) { + return $this->transformResponseToException($request, $response); + }); + } + + /** + * Transform response to an error if possible. + * + * @param RequestInterface $request Request of the call + * @param ResponseInterface $response Response of the call + * + * @throws ClientErrorException If response status code is a 4xx + * @throws ServerErrorException If response status code is a 5xx + * + * @return ResponseInterface If status code is not in 4xx or 5xx return response + */ + private function transformResponseToException(RequestInterface $request, ResponseInterface $response): ResponseInterface + { + if (!$this->onlyServerException && $response->getStatusCode() >= 400 && $response->getStatusCode() < 500) { + throw new ClientErrorException($response->getReasonPhrase(), $request, $response); + } + + if ($response->getStatusCode() >= 500 && $response->getStatusCode() < 600) { + throw new ServerErrorException($response->getReasonPhrase(), $request, $response); + } + + return $response; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderAppendPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderAppendPlugin.php new file mode 100644 index 0000000..95ea673 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderAppendPlugin.php @@ -0,0 +1,48 @@ + + */ +final class HeaderAppendPlugin implements Plugin +{ + /** + * @var array + */ + private $headers; + + /** + * @param array $headers Hashmap of header name to header value + */ + public function __construct(array $headers) + { + $this->headers = $headers; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + foreach ($this->headers as $header => $headerValue) { + $request = $request->withAddedHeader($header, $headerValue); + } + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderDefaultsPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderDefaultsPlugin.php new file mode 100644 index 0000000..bf58070 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderDefaultsPlugin.php @@ -0,0 +1,46 @@ + + */ +final class HeaderDefaultsPlugin implements Plugin +{ + /** + * @var array + */ + private $headers = []; + + /** + * @param array $headers Hashmap of header name to header value + */ + public function __construct(array $headers) + { + $this->headers = $headers; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + foreach ($this->headers as $header => $headerValue) { + if (!$request->hasHeader($header)) { + $request = $request->withHeader($header, $headerValue); + } + } + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderRemovePlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderRemovePlugin.php new file mode 100644 index 0000000..9f4ca44 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderRemovePlugin.php @@ -0,0 +1,44 @@ + + */ +final class HeaderRemovePlugin implements Plugin +{ + /** + * @var array + */ + private $headers = []; + + /** + * @param array $headers List of header names to remove from the request + */ + public function __construct(array $headers) + { + $this->headers = $headers; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + foreach ($this->headers as $header) { + if ($request->hasHeader($header)) { + $request = $request->withoutHeader($header); + } + } + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderSetPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderSetPlugin.php new file mode 100644 index 0000000..06f00eb --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HeaderSetPlugin.php @@ -0,0 +1,44 @@ + + */ +final class HeaderSetPlugin implements Plugin +{ + /** + * @var array + */ + private $headers; + + /** + * @param array $headers Hashmap of header name to header value + */ + public function __construct(array $headers) + { + $this->headers = $headers; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + foreach ($this->headers as $header => $headerValue) { + $request = $request->withHeader($header, $headerValue); + } + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HistoryPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HistoryPlugin.php new file mode 100644 index 0000000..a1796a6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/HistoryPlugin.php @@ -0,0 +1,49 @@ + + */ +final class HistoryPlugin implements Plugin +{ + /** + * Journal use to store request / responses / exception. + * + * @var Journal + */ + private $journal; + + public function __construct(Journal $journal) + { + $this->journal = $journal; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $journal = $this->journal; + + return $next($request)->then(function (ResponseInterface $response) use ($request, $journal) { + $journal->addSuccess($request, $response); + + return $response; + }, function (ClientExceptionInterface $exception) use ($request, $journal) { + $journal->addFailure($request, $exception); + + throw $exception; + }); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/Journal.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/Journal.php new file mode 100644 index 0000000..9faa938 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/Journal.php @@ -0,0 +1,33 @@ + + */ +interface Journal +{ + /** + * Record a successful call. + * + * @param RequestInterface $request Request use to make the call + * @param ResponseInterface $response Response returned by the call + */ + public function addSuccess(RequestInterface $request, ResponseInterface $response); + + /** + * Record a failed call. + * + * @param RequestInterface $request Request use to make the call + * @param ClientExceptionInterface $exception Exception returned by the call + */ + public function addFailure(RequestInterface $request, ClientExceptionInterface $exception); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/QueryDefaultsPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/QueryDefaultsPlugin.php new file mode 100644 index 0000000..4c8087c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/QueryDefaultsPlugin.php @@ -0,0 +1,50 @@ + + */ +final class QueryDefaultsPlugin implements Plugin +{ + /** + * @var array + */ + private $queryParams = []; + + /** + * @param array $queryParams Hashmap of query name to query value. Names and values must not be url encoded as + * this plugin will encode them + */ + public function __construct(array $queryParams) + { + $this->queryParams = $queryParams; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $uri = $request->getUri(); + + parse_str($uri->getQuery(), $query); + $query += $this->queryParams; + + $request = $request->withUri( + $uri->withQuery(http_build_query($query)) + ); + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RedirectPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RedirectPlugin.php new file mode 100644 index 0000000..061f35b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RedirectPlugin.php @@ -0,0 +1,258 @@ + + */ +final class RedirectPlugin implements Plugin +{ + /** + * Rule on how to redirect, change method for the new request. + * + * @var array + */ + private $redirectCodes = [ + 300 => [ + 'switch' => [ + 'unless' => ['GET', 'HEAD'], + 'to' => 'GET', + ], + 'multiple' => true, + 'permanent' => false, + ], + 301 => [ + 'switch' => [ + 'unless' => ['GET', 'HEAD'], + 'to' => 'GET', + ], + 'multiple' => false, + 'permanent' => true, + ], + 302 => [ + 'switch' => [ + 'unless' => ['GET', 'HEAD'], + 'to' => 'GET', + ], + 'multiple' => false, + 'permanent' => false, + ], + 303 => [ + 'switch' => [ + 'unless' => ['GET', 'HEAD'], + 'to' => 'GET', + ], + 'multiple' => false, + 'permanent' => false, + ], + 307 => [ + 'switch' => false, + 'multiple' => false, + 'permanent' => false, + ], + 308 => [ + 'switch' => false, + 'multiple' => false, + 'permanent' => true, + ], + ]; + + /** + * Determine how header should be preserved from old request. + * + * @var bool|array + * + * true will keep all previous headers (default value) + * false will ditch all previous headers + * string[] will keep only headers with the specified names + */ + private $preserveHeader; + + /** + * Store all previous redirect from 301 / 308 status code. + * + * @var array + */ + private $redirectStorage = []; + + /** + * Whether the location header must be directly used for a multiple redirection status code (300). + * + * @var bool + */ + private $useDefaultForMultiple; + + /** + * @var string[][] Chain identifier => list of URLs for this chain + */ + private $circularDetection = []; + + /** + * @param array $config { + * + * @var bool|string[] $preserve_header True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep + * @var bool $use_default_for_multiple Whether the location header must be directly used for a multiple redirection status code (300). + * } + */ + public function __construct(array $config = []) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'preserve_header' => true, + 'use_default_for_multiple' => true, + ]); + $resolver->setAllowedTypes('preserve_header', ['bool', 'array']); + $resolver->setAllowedTypes('use_default_for_multiple', 'bool'); + $resolver->setNormalizer('preserve_header', function (OptionsResolver $resolver, $value) { + if (is_bool($value) && false === $value) { + return []; + } + + return $value; + }); + $options = $resolver->resolve($config); + + $this->preserveHeader = $options['preserve_header']; + $this->useDefaultForMultiple = $options['use_default_for_multiple']; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + // Check in storage + if (array_key_exists((string) $request->getUri(), $this->redirectStorage)) { + $uri = $this->redirectStorage[(string) $request->getUri()]['uri']; + $statusCode = $this->redirectStorage[(string) $request->getUri()]['status']; + $redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode); + + return $first($redirectRequest); + } + + return $next($request)->then(function (ResponseInterface $response) use ($request, $first): ResponseInterface { + $statusCode = $response->getStatusCode(); + + if (!array_key_exists($statusCode, $this->redirectCodes)) { + return $response; + } + + $uri = $this->createUri($response, $request); + $redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode); + $chainIdentifier = spl_object_hash((object) $first); + + if (!array_key_exists($chainIdentifier, $this->circularDetection)) { + $this->circularDetection[$chainIdentifier] = []; + } + + $this->circularDetection[$chainIdentifier][] = (string) $request->getUri(); + + if (in_array((string) $redirectRequest->getUri(), $this->circularDetection[$chainIdentifier])) { + throw new CircularRedirectionException('Circular redirection detected', $request, $response); + } + + if ($this->redirectCodes[$statusCode]['permanent']) { + $this->redirectStorage[(string) $request->getUri()] = [ + 'uri' => $uri, + 'status' => $statusCode, + ]; + } + + // Call redirect request synchronously + $redirectPromise = $first($redirectRequest); + + return $redirectPromise->wait(); + }); + } + + private function buildRedirectRequest(RequestInterface $originalRequest, UriInterface $targetUri, int $statusCode): RequestInterface + { + $originalRequest = $originalRequest->withUri($targetUri); + + if (false !== $this->redirectCodes[$statusCode]['switch'] && !in_array($originalRequest->getMethod(), $this->redirectCodes[$statusCode]['switch']['unless'])) { + $originalRequest = $originalRequest->withMethod($this->redirectCodes[$statusCode]['switch']['to']); + } + + if (is_array($this->preserveHeader)) { + $headers = array_keys($originalRequest->getHeaders()); + + foreach ($headers as $name) { + if (!in_array($name, $this->preserveHeader)) { + $originalRequest = $originalRequest->withoutHeader($name); + } + } + } + + return $originalRequest; + } + + /** + * Creates a new Uri from the old request and the location header. + * + * @throws HttpException If location header is not usable (missing or incorrect) + * @throws MultipleRedirectionException If a 300 status code is received and default location cannot be resolved (doesn't use the location header or not present) + */ + private function createUri(ResponseInterface $redirectResponse, RequestInterface $originalRequest): UriInterface + { + if ($this->redirectCodes[$redirectResponse->getStatusCode()]['multiple'] && (!$this->useDefaultForMultiple || !$redirectResponse->hasHeader('Location'))) { + throw new MultipleRedirectionException('Cannot choose a redirection', $originalRequest, $redirectResponse); + } + + if (!$redirectResponse->hasHeader('Location')) { + throw new HttpException('Redirect status code, but no location header present in the response', $originalRequest, $redirectResponse); + } + + $location = $redirectResponse->getHeaderLine('Location'); + $parsedLocation = parse_url($location); + + if (false === $parsedLocation) { + throw new HttpException(sprintf('Location %s could not be parsed', $location), $originalRequest, $redirectResponse); + } + + $uri = $originalRequest->getUri(); + + if (array_key_exists('scheme', $parsedLocation)) { + $uri = $uri->withScheme($parsedLocation['scheme']); + } + + if (array_key_exists('host', $parsedLocation)) { + $uri = $uri->withHost($parsedLocation['host']); + } + + if (array_key_exists('port', $parsedLocation)) { + $uri = $uri->withPort($parsedLocation['port']); + } + + if (array_key_exists('path', $parsedLocation)) { + $uri = $uri->withPath($parsedLocation['path']); + } + + if (array_key_exists('query', $parsedLocation)) { + $uri = $uri->withQuery($parsedLocation['query']); + } else { + $uri = $uri->withQuery(''); + } + + if (array_key_exists('fragment', $parsedLocation)) { + $uri = $uri->withFragment($parsedLocation['fragment']); + } else { + $uri = $uri->withFragment(''); + } + + return $uri; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RequestMatcherPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RequestMatcherPlugin.php new file mode 100644 index 0000000..45d4375 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RequestMatcherPlugin.php @@ -0,0 +1,56 @@ + + */ +final class RequestMatcherPlugin implements Plugin +{ + /** + * @var RequestMatcher + */ + private $requestMatcher; + + /** + * @var Plugin|null + */ + private $successPlugin; + + /** + * @var Plugin|null + */ + private $failurePlugin; + + public function __construct(RequestMatcher $requestMatcher, ?Plugin $delegateOnMatch, Plugin $delegateOnNoMatch = null) + { + $this->requestMatcher = $requestMatcher; + $this->successPlugin = $delegateOnMatch; + $this->failurePlugin = $delegateOnNoMatch; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + if ($this->requestMatcher->matches($request)) { + if (null !== $this->successPlugin) { + return $this->successPlugin->handleRequest($request, $next, $first); + } + } elseif (null !== $this->failurePlugin) { + return $this->failurePlugin->handleRequest($request, $next, $first); + } + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RequestSeekableBodyPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RequestSeekableBodyPlugin.php new file mode 100644 index 0000000..1b6c528 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RequestSeekableBodyPlugin.php @@ -0,0 +1,29 @@ + + */ +final class RequestSeekableBodyPlugin extends SeekableBodyPlugin +{ + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + if (!$request->getBody()->isSeekable()) { + $request = $request->withBody(new BufferedStream($request->getBody(), $this->useFileBuffer, $this->memoryBufferSize)); + } + + return $next($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ResponseSeekableBodyPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ResponseSeekableBodyPlugin.php new file mode 100644 index 0000000..6f941b6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/ResponseSeekableBodyPlugin.php @@ -0,0 +1,32 @@ + + */ +final class ResponseSeekableBodyPlugin extends SeekableBodyPlugin +{ + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + return $next($request)->then(function (ResponseInterface $response) { + if ($response->getBody()->isSeekable()) { + return $response; + } + + return $response->withBody(new BufferedStream($response->getBody(), $this->useFileBuffer, $this->memoryBufferSize)); + }); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RetryPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RetryPlugin.php new file mode 100644 index 0000000..92b7cbf --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/RetryPlugin.php @@ -0,0 +1,177 @@ + + */ +final class RetryPlugin implements Plugin +{ + /** + * Number of retry before sending an exception. + * + * @var int + */ + private $retry; + + /** + * @var callable + */ + private $errorResponseDelay; + + /** + * @var callable + */ + private $errorResponseDecider; + + /** + * @var callable + */ + private $exceptionDecider; + + /** + * @var callable + */ + private $exceptionDelay; + + /** + * Store the retry counter for each request. + * + * @var array + */ + private $retryStorage = []; + + /** + * @param array $config { + * + * @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up + * @var callable $error_response_decider A callback that gets a request and response to decide whether the request should be retried + * @var callable $exception_decider A callback that gets a request and an exception to decide after a failure whether the request should be retried + * @var callable $error_response_delay A callback that gets a request and response and the current number of retries and returns how many microseconds we should wait before trying again + * @var callable $exception_delay A callback that gets a request, an exception and the current number of retries and returns how many microseconds we should wait before trying again + * } + */ + public function __construct(array $config = []) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'retries' => 1, + 'error_response_decider' => function (RequestInterface $request, ResponseInterface $response) { + // do not retry client errors + return $response->getStatusCode() >= 500 && $response->getStatusCode() < 600; + }, + 'exception_decider' => function (RequestInterface $request, ClientExceptionInterface $e) { + // do not retry client errors + return !$e instanceof HttpException || $e->getCode() >= 500 && $e->getCode() < 600; + }, + 'error_response_delay' => __CLASS__.'::defaultErrorResponseDelay', + 'exception_delay' => __CLASS__.'::defaultExceptionDelay', + ]); + + $resolver->setAllowedTypes('retries', 'int'); + $resolver->setAllowedTypes('error_response_decider', 'callable'); + $resolver->setAllowedTypes('exception_decider', 'callable'); + $resolver->setAllowedTypes('error_response_delay', 'callable'); + $resolver->setAllowedTypes('exception_delay', 'callable'); + $options = $resolver->resolve($config); + + $this->retry = $options['retries']; + $this->errorResponseDecider = $options['error_response_decider']; + $this->errorResponseDelay = $options['error_response_delay']; + $this->exceptionDecider = $options['exception_decider']; + $this->exceptionDelay = $options['exception_delay']; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $chainIdentifier = spl_object_hash((object) $first); + + return $next($request)->then(function (ResponseInterface $response) use ($request, $next, $first, $chainIdentifier) { + if (!array_key_exists($chainIdentifier, $this->retryStorage)) { + $this->retryStorage[$chainIdentifier] = 0; + } + + if ($this->retryStorage[$chainIdentifier] >= $this->retry) { + unset($this->retryStorage[$chainIdentifier]); + + return $response; + } + + if (call_user_func($this->errorResponseDecider, $request, $response)) { + $time = call_user_func($this->errorResponseDelay, $request, $response, $this->retryStorage[$chainIdentifier]); + $response = $this->retry($request, $next, $first, $chainIdentifier, $time); + } + + if (array_key_exists($chainIdentifier, $this->retryStorage)) { + unset($this->retryStorage[$chainIdentifier]); + } + + return $response; + }, function (ClientExceptionInterface $exception) use ($request, $next, $first, $chainIdentifier) { + if (!array_key_exists($chainIdentifier, $this->retryStorage)) { + $this->retryStorage[$chainIdentifier] = 0; + } + + if ($this->retryStorage[$chainIdentifier] >= $this->retry) { + unset($this->retryStorage[$chainIdentifier]); + + throw $exception; + } + + if (!call_user_func($this->exceptionDecider, $request, $exception)) { + throw $exception; + } + + $time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]); + + return $this->retry($request, $next, $first, $chainIdentifier, $time); + }); + } + + /** + * @param int $retries The number of retries we made before. First time this get called it will be 0. + */ + public static function defaultErrorResponseDelay(RequestInterface $request, ResponseInterface $response, int $retries): int + { + return pow(2, $retries) * 500000; + } + + /** + * @param int $retries The number of retries we made before. First time this get called it will be 0. + */ + public static function defaultExceptionDelay(RequestInterface $request, ClientExceptionInterface $e, int $retries): int + { + return pow(2, $retries) * 500000; + } + + /** + * @throws \Exception if retrying returns a failed promise + */ + private function retry(RequestInterface $request, callable $next, callable $first, string $chainIdentifier, int $delay): ResponseInterface + { + usleep($delay); + + // Retry synchronously + ++$this->retryStorage[$chainIdentifier]; + $promise = $this->handleRequest($request, $next, $first); + + return $promise->wait(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/SeekableBodyPlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/SeekableBodyPlugin.php new file mode 100644 index 0000000..5c20af3 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/SeekableBodyPlugin.php @@ -0,0 +1,47 @@ +setDefaults([ + 'use_file_buffer' => true, + 'memory_buffer_size' => 2097152, + ]); + $resolver->setAllowedTypes('use_file_buffer', 'bool'); + $resolver->setAllowedTypes('memory_buffer_size', 'int'); + + $options = $resolver->resolve($config); + + $this->useFileBuffer = $options['use_file_buffer']; + $this->memoryBufferSize = $options['memory_buffer_size']; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/VersionBridgePlugin.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/VersionBridgePlugin.php new file mode 100644 index 0000000..0a2c714 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/Plugin/VersionBridgePlugin.php @@ -0,0 +1,24 @@ + + */ +trait VersionBridgePlugin +{ + abstract protected function doHandleRequest(RequestInterface $request, callable $next, callable $first); + + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + return $this->doHandleRequest($request, $next, $first); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginChain.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginChain.php new file mode 100644 index 0000000..db72332 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginChain.php @@ -0,0 +1,65 @@ +plugins = $plugins; + $this->clientCallable = $clientCallable; + $this->maxRestarts = (int) ($options['max_restarts'] ?? 0); + } + + private function createChain(): callable + { + $lastCallable = $this->clientCallable; + $reversedPlugins = array_reverse($this->plugins); + + foreach ($reversedPlugins as $plugin) { + $lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable) { + return $plugin->handleRequest($request, $lastCallable, $this); + }; + } + + return $lastCallable; + } + + public function __invoke(RequestInterface $request): Promise + { + if ($this->restarts > $this->maxRestarts) { + throw new LoopException('Too many restarts in plugin client', $request); + } + + ++$this->restarts; + + return $this->createChain()($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClient.php new file mode 100644 index 0000000..f3c9b23 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClient.php @@ -0,0 +1,133 @@ + + */ +final class PluginClient implements HttpClient, HttpAsyncClient +{ + /** + * An HTTP async client. + * + * @var HttpAsyncClient + */ + private $client; + + /** + * The plugin chain. + * + * @var Plugin[] + */ + private $plugins; + + /** + * A list of options. + * + * @var array + */ + private $options; + + /** + * @param ClientInterface|HttpAsyncClient $client An HTTP async client + * @param Plugin[] $plugins A plugin chain + * @param array $options { + * + * @var int $max_restarts + * } + */ + public function __construct($client, array $plugins = [], array $options = []) + { + if ($client instanceof HttpAsyncClient) { + $this->client = $client; + } elseif ($client instanceof ClientInterface) { + $this->client = new EmulatedHttpAsyncClient($client); + } else { + throw new \TypeError( + sprintf('%s::__construct(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + + $this->plugins = $plugins; + $this->options = $this->configure($options); + } + + /** + * {@inheritdoc} + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + // If the client doesn't support sync calls, call async + if (!$this->client instanceof ClientInterface) { + return $this->sendAsyncRequest($request)->wait(); + } + + // Else we want to use the synchronous call of the underlying client, + // and not the async one in the case we have both an async and sync call + $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) { + try { + return new HttpFulfilledPromise($this->client->sendRequest($request)); + } catch (HttplugException $exception) { + return new HttpRejectedPromise($exception); + } + }); + + return $pluginChain($request)->wait(); + } + + /** + * {@inheritdoc} + */ + public function sendAsyncRequest(RequestInterface $request) + { + $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) { + return $this->client->sendAsyncRequest($request); + }); + + return $pluginChain($request); + } + + /** + * Configure the plugin client. + */ + private function configure(array $options = []): array + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'max_restarts' => 10, + ]); + + $resolver->setAllowedTypes('max_restarts', 'int'); + + return $resolver->resolve($options); + } + + /** + * Create the plugin chain. + * + * @param Plugin[] $plugins A plugin chain + * @param callable $clientCallable Callable making the HTTP call + * + * @return callable(RequestInterface): Promise + */ + private function createPluginChain(array $plugins, callable $clientCallable): callable + { + /** @var callable(RequestInterface): Promise */ + return new PluginChain($plugins, $clientCallable, $this->options); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClientBuilder.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClientBuilder.php new file mode 100644 index 0000000..8746498 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClientBuilder.php @@ -0,0 +1,76 @@ + + */ +final class PluginClientBuilder +{ + /** @var Plugin[][] List of plugins ordered by priority [priority => Plugin[]]). */ + private $plugins = []; + + /** @var array Array of options to give to the plugin client */ + private $options = []; + + /** + * @param int $priority Priority of the plugin. The higher comes first. + */ + public function addPlugin(Plugin $plugin, int $priority = 0): self + { + $this->plugins[$priority][] = $plugin; + + return $this; + } + + /** + * @param mixed $value + */ + public function setOption(string $name, $value): self + { + $this->options[$name] = $value; + + return $this; + } + + public function removeOption(string $name): self + { + unset($this->options[$name]); + + return $this; + } + + /** + * @param ClientInterface|HttpAsyncClient $client + */ + public function createClient($client): PluginClient + { + if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) { + throw new \TypeError( + sprintf('%s::createClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + + $plugins = $this->plugins; + + if (0 === count($plugins)) { + $plugins[] = []; + } + + krsort($plugins); + $plugins = array_merge(...$plugins); + + return new PluginClient( + $client, + array_values($plugins), + $this->options + ); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClientFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClientFactory.php new file mode 100644 index 0000000..0c91fc3 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/PluginClientFactory.php @@ -0,0 +1,68 @@ + + */ +final class PluginClientFactory +{ + /** + * @var (callable(ClientInterface|HttpAsyncClient, Plugin[], array): PluginClient)|null + */ + private static $factory; + + /** + * Set the factory to use. + * The callable to provide must have the same arguments and return type as PluginClientFactory::createClient. + * This is used by the HTTPlugBundle to provide a better Symfony integration. + * Unlike the createClient method, this one is static to allow zero configuration profiling by hooking into early + * application execution. + * + * @internal + * + * @param callable(ClientInterface|HttpAsyncClient, Plugin[], array): PluginClient $factory + */ + public static function setFactory(callable $factory): void + { + static::$factory = $factory; + } + + /** + * @param ClientInterface|HttpAsyncClient $client + * @param Plugin[] $plugins + * @param array $options { + * + * @var string $client_name to give client a name which may be used when displaying client information like in + * the HTTPlugBundle profiler. + * } + * + * @see PluginClient constructor for PluginClient specific $options. + */ + public function createClient($client, array $plugins = [], array $options = []): PluginClient + { + if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) { + throw new \TypeError( + sprintf('%s::createClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + + if (static::$factory) { + $factory = static::$factory; + + return $factory($client, $plugins, $options); + } + + unset($options['client_name']); + + return new PluginClient($client, $plugins, $options); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/VersionBridgeClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/VersionBridgeClient.php new file mode 100644 index 0000000..a608edd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/client-common/src/VersionBridgeClient.php @@ -0,0 +1,24 @@ + + */ +trait VersionBridgeClient +{ + abstract protected function doSendRequest(RequestInterface $request); + + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->doSendRequest($request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/.github/workflows/Build-Test.yml b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/.github/workflows/Build-Test.yml new file mode 100644 index 0000000..1c0dde3 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/.github/workflows/Build-Test.yml @@ -0,0 +1,90 @@ + +# Run this workflow every time a new commit pushed to your repository +on: + push: + +jobs: + tests: + if: "! contains(toJSON(github.event.commits.*.msg), 'skip') && ! contains(toJSON(github.event.commits.*.msg), 'ci')" #skip ci... + runs-on: ${{ matrix.operating-system }} + + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-20.04] + php-versions: ['7.2', '7.3', '7.4', '8.0'] + include: + - operating-system: ubuntu-16.04 + php-versions: '7.1' + COMPOSER_FLAGS: '--prefer-stable --prefer-lowest' + COVERAGE: 'true' + PHPUNIT_FLAGS: '--coverage-clover build/coverage.xml' + + name: PHP ${{ matrix.php-versions }} - ${{ matrix.operating-system }} + + env: + extensions: curl json libxml dom + key: cache-v1 # can be any string, change to clear the extension cache. + + steps: + # Checks out a copy of your repository on the ubuntu machine + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-versions }} + extensions: ${{ env.extensions }} + key: ${{ env.key }} + + - name: Cache PHP Extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: Cache Composer Dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP Action + uses: shivammathur/setup-php@2.8.0 + with: + php-version: ${{ matrix.php-versions }} + extensions: ${{ env.extensions }} + coverage: xdebug + tools: pecl, composer + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Composer self update + run: composer self-update + + - name: Install Composer dependencies + run: composer update ${{ matrix.COMPOSER_FLAGS }} --prefer-source --no-interaction + + - name: boot test server + run: vendor/bin/http_test_server > /dev/null 2>&1 & + + - name: Apply tests + run: composer test + + - name: Apply coverage + if: ${{ matrix.COVERAGE == 'true' }} + run: | + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/.php_cs b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/.php_cs new file mode 100644 index 0000000..febeee5 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/.php_cs @@ -0,0 +1,9 @@ +level(Symfony\CS\FixerInterface::PSR2_LEVEL) + ->fixers([]) + ->finder( + Symfony\CS\Finder\DefaultFinder::create()->in(__DIR__ . '/src') + ) +; diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/CHANGELOG.md new file mode 100644 index 0000000..41294d9 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/CHANGELOG.md @@ -0,0 +1,204 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 2.2.0 - 2020-14-12 + +### Added + +- PHP 8.0 support + +## 2.1.0 - 2019-12-27 + +### Added + +- Symfony 5 support + +## 2.0.0 - 2019-03-05 + +### Removed + +- HHVM support removed. + +### Changed + +- Minimal PHP version changed to 7.1. +- `Client::__construct` now expects PSR-17 factories instead of HTTPlug ones. + +### Added + +- #41: Support [PSR-17](https://www.php-fig.org/psr/psr-17/) and + [PSR-18](https://www.php-fig.org/psr/psr-18/). + + +## 1.7.1 - 2018-03-26 + +### Fixed + +- #36: Failure evaluating code: is_resource($handle) (string assertions are deprecated in PHP 7.2) + + +## 1.7 - 2017-02-09 + +### Changed + +- #30: Make sure we rewind streams + +## 1.6.2 - 2017-01-02 + +### Fixed + +- #29: Request not using CURLOPT_POSTFIELDS have content-length set to + +### Changed + +- Use binary mode to create response body stream. + + +## 1.6.1 - 2016-11-11 + +### Fixed + +- #27: ErrorPlugin and sendAsyncRequest() incompatibility + + +## 1.6 - 2016-09-12 + +### Changed + +- `Client::sendRequest` now throws `Http\Client\Exception\NetworkException` on network errors. +- `\UnexpectedValueException` replaced with `Http\Client\Exception\RequestException` in + `Client::sendRequest` and `Client::sendAsyncRequest` + + +## 1.5.1 - 2016-08-29 + +### Fixed + +- #26: Combining CurlClient with StopwatchPlugin causes Promise onRejected handler to never be + invoked. + + +## 1.5 - 2016-08-03 + +### Changed + +- Request body can be send with any method except GET, HEAD and TRACE. +- #25: Make discovery a hard dependency. + + +## 1.4.2 - 2016-06-14 + +### Added + +- #23: "php-http/async-client-implementation" added to "provide" section. + + +## 1.4.1 - 2016-05-30 + +### Fixed + +- #22: Cannot create the client using `HttpClientDiscovery`. + + +## 1.4 - 2016-03-30 + +### Changed + +- #20: Minimize memory usage when reading large response body. + + +## 1.3 - 2016-03-14 + +### Fixed + +- #18: Invalid "Expect" header. + +### Removed + +- #13: Remove HeaderParser. + + +## 1.2 - 2016-03-09 + +### Added + +- #16: Make sure discovery can find the curl client + +### Fixed + +- #15: "Out of memory" sending large files. + + +## 1.1.0 - 2016-01-29 + +### Changed + +- Switch to php-http/message 1.0. + + +## 1.0.0 - 2016-01-28 + +First stable release. + + +## 0.7.0 - 2016-01-26 + +### Changed + +- Migrate from `php-http/discovery` and `php-http/utils` to `php-http/message`. + +## 0.6.0 - 2016-01-12 + +### Changed + +- Root namespace changed from `Http\Curl` to `Http\Client\Curl`. +- Main client class name renamed from `CurlHttpClient` to `Client`. +- Minimum required [php-http/discovery](https://packagist.org/packages/php-http/discovery) + version changed to 0.5. + + +## 0.5.0 - 2015-12-18 + +### Changed + +- Compatibility with php-http/httplug 1.0 beta +- Switch to php-http/discovery 0.4 + + +## 0.4.0 - 2015-12-16 + +### Changed + +- Switch to php-http/message-factory 1.0 + + +## 0.3.1 - 2015-12-14 + +### Changed + +- Requirements fixed. + + +## 0.3.0 - 2015-11-24 + +### Changed + +- Use cURL constants as options keys. + + +## 0.2.0 - 2015-11-17 + +### Added + +- HttpAsyncClient support. + + +## 0.1.0 - 2015-11-11 + +### Added + +- Initial release diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/LICENSE new file mode 100644 index 0000000..8e2c4a0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/README.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/README.md new file mode 100644 index 0000000..fc60b7e --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/README.md @@ -0,0 +1,44 @@ +# Curl client for PHP HTTP + +[![Latest Version](https://img.shields.io/github/release/php-http/curl-client.svg?style=flat-square)](https://github.com/php-http/curl-client/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Build Status](https://img.shields.io/travis/php-http/curl-client.svg?style=flat-square)](https://travis-ci.org/php-http/curl-client) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/curl-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/curl-client) +[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/curl-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/curl-client) +[![Total Downloads](https://img.shields.io/packagist/dt/php-http/curl-client.svg?style=flat-square)](https://packagist.org/packages/php-http/curl-client) + +The cURL client use the cURL PHP extension which must be activated in your `php.ini`. + + +## Install + +Via Composer + +``` bash +$ composer require php-http/curl-client +``` + +## Documentation + +Please see the [official documentation](http://docs.php-http.org/en/latest/clients/curl-client.html). + +## Testing + +``` bash +$ composer test +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. + + +## Security + +If you discover any security related issues, please contact us at +[security@php-http.org](mailto:security@php-http.org). + + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/composer.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/composer.json new file mode 100644 index 0000000..55f90d7 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/composer.json @@ -0,0 +1,57 @@ +{ + "name": "php-http/curl-client", + "description": "PSR-18 and HTTPlug Async client with cURL", + "license": "MIT", + "keywords": [ + "curl", + "http", + "psr-18" + ], + "homepage": "http://php-http.org", + "authors": [{ + "name": "Михаил Красильников", + "email": "m.krasilnikov@yandex.ru" + }], + "prefer-stable": true, + "minimum-stability": "dev", + "require": { + "php": "^7.1 || ^8.0", + "ext-curl": "*", + "php-http/discovery": "^1.6", + "php-http/httplug": "^2.0", + "php-http/message": "^1.2", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/options-resolver": "^3.4 || ^4.0 || ^5.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^1.0", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^7.5 || ^9.4", + "laminas/laminas-diactoros": "^2.0" + }, + "autoload": { + "psr-4": { + "Http\\Client\\Curl\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Http\\Client\\Curl\\Tests\\": "tests/" + } + }, + "provide": { + "php-http/client-implementation": "1.0", + "php-http/async-client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-ci": "vendor/bin/phpunit --coverage-clover build/coverage.xml" + }, + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + } +} \ No newline at end of file diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/puli.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/puli.json new file mode 100644 index 0000000..b35768d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/puli.json @@ -0,0 +1,242 @@ +{ + "version": "1.0", + "name": "php-http/curl-client", + "bindings": { + "98239b8b-103b-4f47-94c7-4cba49a05a1f": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Client\\Curl\\Client", + "type": "Http\\Client\\HttpAsyncClient" + }, + "a6a79968-2aa5-427c-bbe1-a581d9a48321": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Client\\Curl\\Client", + "type": "Http\\Client\\HttpClient" + } + }, + "config": { + "bootstrap-file": "vendor/autoload.php" + }, + "packages": { + "clue/stream-filter": { + "install-path": "vendor/clue/stream-filter", + "installer": "composer", + "env": "dev" + }, + "doctrine/instantiator": { + "install-path": "vendor/doctrine/instantiator", + "installer": "composer", + "env": "dev" + }, + "guzzlehttp/psr7": { + "install-path": "vendor/guzzlehttp/psr7", + "installer": "composer", + "env": "dev" + }, + "justinrainbow/json-schema": { + "install-path": "vendor/justinrainbow/json-schema", + "installer": "composer", + "env": "dev" + }, + "paragonie/random_compat": { + "install-path": "vendor/paragonie/random_compat", + "installer": "composer", + "env": "dev" + }, + "php-http/adapter-integration-tests": { + "install-path": "vendor/php-http/adapter-integration-tests", + "installer": "composer", + "env": "dev" + }, + "php-http/discovery": { + "install-path": "vendor/php-http/discovery", + "installer": "composer", + "env": "dev" + }, + "php-http/httplug": { + "install-path": "vendor/php-http/httplug", + "installer": "composer" + }, + "php-http/message": { + "install-path": "vendor/php-http/message", + "installer": "composer", + "env": "dev" + }, + "php-http/message-factory": { + "install-path": "vendor/php-http/message-factory", + "installer": "composer" + }, + "php-http/promise": { + "install-path": "vendor/php-http/promise", + "installer": "composer" + }, + "phpdocumentor/reflection-docblock": { + "install-path": "vendor/phpdocumentor/reflection-docblock", + "installer": "composer", + "env": "dev" + }, + "phpspec/prophecy": { + "install-path": "vendor/phpspec/prophecy", + "installer": "composer", + "env": "dev" + }, + "phpunit/php-code-coverage": { + "install-path": "vendor/phpunit/php-code-coverage", + "installer": "composer", + "env": "dev" + }, + "phpunit/php-file-iterator": { + "install-path": "vendor/phpunit/php-file-iterator", + "installer": "composer", + "env": "dev" + }, + "phpunit/php-text-template": { + "install-path": "vendor/phpunit/php-text-template", + "installer": "composer", + "env": "dev" + }, + "phpunit/php-timer": { + "install-path": "vendor/phpunit/php-timer", + "installer": "composer", + "env": "dev" + }, + "phpunit/php-token-stream": { + "install-path": "vendor/phpunit/php-token-stream", + "installer": "composer", + "env": "dev" + }, + "phpunit/phpunit": { + "install-path": "vendor/phpunit/phpunit", + "installer": "composer", + "env": "dev" + }, + "phpunit/phpunit-mock-objects": { + "install-path": "vendor/phpunit/phpunit-mock-objects", + "installer": "composer", + "env": "dev" + }, + "psr/http-message": { + "install-path": "vendor/psr/http-message", + "installer": "composer" + }, + "psr/log": { + "install-path": "vendor/psr/log", + "installer": "composer", + "env": "dev" + }, + "puli/composer-plugin": { + "install-path": "vendor/puli/composer-plugin", + "installer": "composer", + "env": "dev" + }, + "puli/discovery": { + "install-path": "vendor/puli/discovery", + "installer": "composer", + "env": "dev" + }, + "puli/repository": { + "install-path": "vendor/puli/repository", + "installer": "composer", + "env": "dev" + }, + "puli/url-generator": { + "install-path": "vendor/puli/url-generator", + "installer": "composer", + "env": "dev" + }, + "ramsey/uuid": { + "install-path": "vendor/ramsey/uuid", + "installer": "composer", + "env": "dev" + }, + "sebastian/comparator": { + "install-path": "vendor/sebastian/comparator", + "installer": "composer", + "env": "dev" + }, + "sebastian/diff": { + "install-path": "vendor/sebastian/diff", + "installer": "composer", + "env": "dev" + }, + "sebastian/environment": { + "install-path": "vendor/sebastian/environment", + "installer": "composer", + "env": "dev" + }, + "sebastian/exporter": { + "install-path": "vendor/sebastian/exporter", + "installer": "composer", + "env": "dev" + }, + "sebastian/global-state": { + "install-path": "vendor/sebastian/global-state", + "installer": "composer", + "env": "dev" + }, + "sebastian/recursion-context": { + "install-path": "vendor/sebastian/recursion-context", + "installer": "composer", + "env": "dev" + }, + "sebastian/version": { + "install-path": "vendor/sebastian/version", + "installer": "composer", + "env": "dev" + }, + "seld/jsonlint": { + "install-path": "vendor/seld/jsonlint", + "installer": "composer", + "env": "dev" + }, + "symfony/filesystem": { + "install-path": "vendor/symfony/filesystem", + "installer": "composer", + "env": "dev" + }, + "symfony/process": { + "install-path": "vendor/symfony/process", + "installer": "composer", + "env": "dev" + }, + "symfony/yaml": { + "install-path": "vendor/symfony/yaml", + "installer": "composer", + "env": "dev" + }, + "th3n3rd/cartesian-product": { + "install-path": "vendor/th3n3rd/cartesian-product", + "installer": "composer", + "env": "dev" + }, + "webmozart/assert": { + "install-path": "vendor/webmozart/assert", + "installer": "composer", + "env": "dev" + }, + "webmozart/expression": { + "install-path": "vendor/webmozart/expression", + "installer": "composer", + "env": "dev" + }, + "webmozart/glob": { + "install-path": "vendor/webmozart/glob", + "installer": "composer", + "env": "dev" + }, + "webmozart/json": { + "install-path": "vendor/webmozart/json", + "installer": "composer", + "env": "dev" + }, + "webmozart/path-util": { + "install-path": "vendor/webmozart/path-util", + "installer": "composer", + "env": "dev" + }, + "zendframework/zend-diactoros": { + "install-path": "vendor/zendframework/zend-diactoros", + "installer": "composer", + "env": "dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/Client.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/Client.php new file mode 100644 index 0000000..739fdc2 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/Client.php @@ -0,0 +1,398 @@ + + * @author Blake Williams + * + * @api + * + * @since 1.0 + */ +class Client implements HttpClient, HttpAsyncClient +{ + /** + * cURL options. + * + * @var array + */ + private $curlOptions; + + /** + * PSR-17 response factory. + * + * @var ResponseFactoryInterface + */ + private $responseFactory; + + /** + * PSR-17 stream factory. + * + * @var StreamFactoryInterface + */ + private $streamFactory; + + /** + * cURL synchronous requests handle. + * + * @var resource|\CurlHandle|null + */ + private $handle; + + /** + * Simultaneous requests runner. + * + * @var MultiRunner|null + */ + private $multiRunner; + + /** + * Create HTTP client. + * + * @param ResponseFactoryInterface|null $responseFactory PSR-17 HTTP response factory. + * @param StreamFactoryInterface|null $streamFactory PSR-17 HTTP stream factory. + * @param array $options cURL options + * {@link http://php.net/curl_setopt}. + * + * @throws NotFoundException If factory discovery failed. + * + * @since 2.0 Accepts PSR-17 factories instead of HTTPlug ones. + */ + public function __construct( + ResponseFactoryInterface $responseFactory = null, + StreamFactoryInterface $streamFactory = null, + array $options = [] + ) { + $this->responseFactory = $responseFactory ?: Psr17FactoryDiscovery::findResponseFactory(); + $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory(); + $resolver = new OptionsResolver(); + $resolver->setDefaults( + [ + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => false, + CURLOPT_FOLLOWLOCATION => false + ] + ); + + // Our parsing will fail if this is set to true. + $resolver->setAllowedValues( + (string)CURLOPT_HEADER, + [false] + ); + + // Our parsing will fail if this is set to true. + $resolver->setAllowedValues( + (string)CURLOPT_RETURNTRANSFER, + [false] + ); + + // We do not know what everything curl supports and might support in the future. + // Make sure that we accept everything that is in the options. + $resolver->setDefined(array_keys($options)); + + $this->curlOptions = $resolver->resolve($options); + } + + /** + * Release resources if still active. + */ + public function __destruct() + { + if (is_resource($this->handle)) { + curl_close($this->handle); + } + } + + /** + * Sends a PSR-7 request and returns a PSR-7 response. + * + * @param RequestInterface $request + * + * @return ResponseInterface + * + * @throws \InvalidArgumentException For invalid header names or values. + * @throws \RuntimeException If creating the body stream fails. + * @throws Exception\NetworkException In case of network problems. + * @throws Exception\RequestException On invalid request. + * + * @since 1.6 \UnexpectedValueException replaced with RequestException + * @since 1.6 Throw NetworkException on network errors + * @since 1.0 + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + $responseBuilder = $this->createResponseBuilder(); + $requestOptions = $this->prepareRequestOptions($request, $responseBuilder); + + if (is_resource($this->handle)) { + curl_reset($this->handle); + } else { + $this->handle = curl_init(); + } + + curl_setopt_array($this->handle, $requestOptions); + curl_exec($this->handle); + + $errno = curl_errno($this->handle); + switch ($errno) { + case CURLE_OK: + // All OK, no actions needed. + break; + case CURLE_COULDNT_RESOLVE_PROXY: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_COULDNT_CONNECT: + case CURLE_OPERATION_TIMEOUTED: + case CURLE_SSL_CONNECT_ERROR: + throw new Exception\NetworkException(curl_error($this->handle), $request); + default: + throw new Exception\RequestException(curl_error($this->handle), $request); + } + + $response = $responseBuilder->getResponse(); + $response->getBody()->seek(0); + + return $response; + } + + /** + * Create builder to use for building response object. + * + * @return ResponseBuilder + */ + private function createResponseBuilder(): ResponseBuilder + { + $body = $this->streamFactory->createStreamFromFile('php://temp', 'w+b'); + + $response = $this->responseFactory + ->createResponse(200) + ->withBody($body); + + return new ResponseBuilder($response); + } + + /** + * Update cURL options for given request and hook in the response builder. + * + * @param RequestInterface $request Request on which to create options. + * @param ResponseBuilder $responseBuilder Builder to use for building response. + * + * @return array cURL options based on request. + * + * @throws \InvalidArgumentException For invalid header names or values. + * @throws \RuntimeException If can not read body. + * @throws Exception\RequestException On invalid request. + */ + private function prepareRequestOptions( + RequestInterface $request, + ResponseBuilder $responseBuilder + ): array { + $curlOptions = $this->curlOptions; + + try { + $curlOptions[CURLOPT_HTTP_VERSION] + = $this->getProtocolVersion($request->getProtocolVersion()); + } catch (\UnexpectedValueException $e) { + throw new Exception\RequestException($e->getMessage(), $request); + } + $curlOptions[CURLOPT_URL] = (string)$request->getUri(); + + $curlOptions = $this->addRequestBodyOptions($request, $curlOptions); + + $curlOptions[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $curlOptions); + + if ($request->getUri()->getUserInfo()) { + $curlOptions[CURLOPT_USERPWD] = $request->getUri()->getUserInfo(); + } + + $curlOptions[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) { + $str = trim($data); + if ('' !== $str) { + if (stripos($str, 'http/') === 0) { + $responseBuilder->setStatus($str)->getResponse(); + } else { + $responseBuilder->addHeader($str); + } + } + + return strlen($data); + }; + + $curlOptions[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) { + return $responseBuilder->getResponse()->getBody()->write($data); + }; + + return $curlOptions; + } + + /** + * Return cURL constant for specified HTTP version. + * + * @param string $requestVersion HTTP version ("1.0", "1.1" or "2.0"). + * + * @return int Respective CURL_HTTP_VERSION_x_x constant. + * + * @throws \UnexpectedValueException If unsupported version requested. + */ + private function getProtocolVersion(string $requestVersion): int + { + switch ($requestVersion) { + case '1.0': + return CURL_HTTP_VERSION_1_0; + case '1.1': + return CURL_HTTP_VERSION_1_1; + case '2.0': + if (defined('CURL_HTTP_VERSION_2_0')) { + return CURL_HTTP_VERSION_2_0; + } + throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support'); + } + + return CURL_HTTP_VERSION_NONE; + } + + /** + * Add request body related cURL options. + * + * @param RequestInterface $request Request on which to create options. + * @param array $curlOptions Options created by prepareRequestOptions(). + * + * @return array cURL options based on request. + */ + private function addRequestBodyOptions(RequestInterface $request, array $curlOptions): array + { + /* + * Some HTTP methods cannot have payload: + * + * - GET — cURL will automatically change method to PUT or POST if we set CURLOPT_UPLOAD or + * CURLOPT_POSTFIELDS. + * - HEAD — cURL treats HEAD as GET request with a same restrictions. + * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request. + */ + if (!in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE'], true)) { + $body = $request->getBody(); + $bodySize = $body->getSize(); + if ($bodySize !== 0) { + if ($body->isSeekable()) { + $body->rewind(); + } + + // Message has non empty body. + if (null === $bodySize || $bodySize > 1024 * 1024) { + // Avoid full loading large or unknown size body into memory + $curlOptions[CURLOPT_UPLOAD] = true; + if (null !== $bodySize) { + $curlOptions[CURLOPT_INFILESIZE] = $bodySize; + } + $curlOptions[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) { + return $body->read($length); + }; + } else { + // Small body can be loaded into memory + $curlOptions[CURLOPT_POSTFIELDS] = (string)$body; + } + } + } + + if ($request->getMethod() === 'HEAD') { + // This will set HTTP method to "HEAD". + $curlOptions[CURLOPT_NOBODY] = true; + } elseif ($request->getMethod() !== 'GET') { + // GET is a default method. Other methods should be specified explicitly. + $curlOptions[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); + } + + return $curlOptions; + } + + /** + * Create headers array for CURLOPT_HTTPHEADER. + * + * @param RequestInterface $request Request on which to create headers. + * @param array $curlOptions Options created by prepareRequestOptions(). + * + * @return string[] + */ + private function createHeaders(RequestInterface $request, array $curlOptions): array + { + $curlHeaders = []; + $headers = $request->getHeaders(); + foreach ($headers as $name => $values) { + $header = strtolower($name); + if ('expect' === $header) { + // curl-client does not support "Expect-Continue", so dropping "expect" headers + continue; + } + if ('content-length' === $header) { + if (array_key_exists(CURLOPT_POSTFIELDS, $curlOptions)) { + // Small body content length can be calculated here. + $values = [strlen($curlOptions[CURLOPT_POSTFIELDS])]; + } elseif (!array_key_exists(CURLOPT_READFUNCTION, $curlOptions)) { + // Else if there is no body, forcing "Content-length" to 0 + $values = [0]; + } + } + foreach ($values as $value) { + $curlHeaders[] = $name . ': ' . $value; + } + } + /* + * curl-client does not support "Expect-Continue", but cURL adds "Expect" header by default. + * We can not suppress it, but we can set it to empty. + */ + $curlHeaders[] = 'Expect:'; + + return $curlHeaders; + } + + /** + * Sends a PSR-7 request in an asynchronous way. + * + * Exceptions related to processing the request are available from the returned Promise. + * + * @param RequestInterface $request + * + * @return Promise Resolves a PSR-7 Response or fails with an Http\Client\Exception. + * + * @throws \InvalidArgumentException For invalid header names or values. + * @throws \RuntimeException If creating the body stream fails. + * @throws Exception\RequestException On invalid request. + * + * @since 1.6 \UnexpectedValueException replaced with RequestException + * @since 1.0 + */ + public function sendAsyncRequest(RequestInterface $request) + { + if (!$this->multiRunner instanceof MultiRunner) { + $this->multiRunner = new MultiRunner(); + } + + $handle = curl_init(); + $responseBuilder = $this->createResponseBuilder(); + $requestOptions = $this->prepareRequestOptions($request, $responseBuilder); + curl_setopt_array($handle, $requestOptions); + + $core = new PromiseCore($request, $handle, $responseBuilder); + $promise = new CurlPromise($core, $this->multiRunner); + $this->multiRunner->add($core); + + return $promise; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/CurlPromise.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/CurlPromise.php new file mode 100644 index 0000000..f04ccf6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/CurlPromise.php @@ -0,0 +1,111 @@ + + */ +class CurlPromise implements Promise +{ + /** + * Shared promise core. + * + * @var PromiseCore + */ + private $core; + + /** + * Requests runner. + * + * @var MultiRunner + */ + private $runner; + + /** + * Create new promise. + * + * @param PromiseCore $core Shared promise core + * @param MultiRunner $runner Simultaneous requests runner + */ + public function __construct(PromiseCore $core, MultiRunner $runner) + { + $this->core = $core; + $this->runner = $runner; + } + + /** + * Add behavior for when the promise is resolved or rejected. + * + * If you do not care about one of the cases, you can set the corresponding callable to null + * The callback will be called when the response or exception arrived and never more than once. + * + * @param callable $onFulfilled Called when a response will be available + * @param callable $onRejected Called when an error happens. + * + * You must always return the Response in the interface or throw an Exception + * + * @return Promise Always returns a new promise which is resolved with value of the executed + * callback (onFulfilled / onRejected) + */ + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if ($onFulfilled) { + $this->core->addOnFulfilled($onFulfilled); + } + if ($onRejected) { + $this->core->addOnRejected($onRejected); + } + + return new self($this->core, $this->runner); + } + + /** + * Get the state of the promise, one of PENDING, FULFILLED or REJECTED. + * + * @return string + */ + public function getState() + { + return $this->core->getState(); + } + + /** + * Wait for the promise to be fulfilled or rejected. + * + * When this method returns, the request has been resolved and the appropriate callable has terminated. + * + * When called with the unwrap option + * + * @param bool $unwrap Whether to return resolved value / throw reason or not + * + * @return \Psr\Http\Message\ResponseInterface|null Resolved value, null if $unwrap is set to false + * + * @throws \Http\Client\Exception The rejection reason + */ + public function wait($unwrap = true) + { + $this->runner->wait($this->core); + + if ($unwrap) { + if ($this->core->getState() === self::REJECTED) { + throw $this->core->getException(); + } + + return $this->core->getResponse(); + } + + return null; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/MultiRunner.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/MultiRunner.php new file mode 100644 index 0000000..7a2c50c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/MultiRunner.php @@ -0,0 +1,131 @@ + + */ +class MultiRunner +{ + /** + * cURL multi handle. + * + * @var resource|null + */ + private $multiHandle; + + /** + * Awaiting cores. + * + * @var PromiseCore[] + */ + private $cores = []; + + /** + * Release resources if still active. + */ + public function __destruct() + { + if (is_resource($this->multiHandle)) { + curl_multi_close($this->multiHandle); + } + } + + /** + * Add promise to runner. + * + * @param PromiseCore $core + */ + public function add(PromiseCore $core): void + { + foreach ($this->cores as $existed) { + if ($existed === $core) { + return; + } + } + + $this->cores[] = $core; + + if (null === $this->multiHandle) { + $this->multiHandle = curl_multi_init(); + } + curl_multi_add_handle($this->multiHandle, $core->getHandle()); + } + + /** + * Remove promise from runner. + * + * @param PromiseCore $core + */ + public function remove(PromiseCore $core): void + { + foreach ($this->cores as $index => $existed) { + if ($existed === $core) { + curl_multi_remove_handle($this->multiHandle, $core->getHandle()); + unset($this->cores[$index]); + + return; + } + } + } + + /** + * Wait for request(s) to be completed. + * + * @param PromiseCore|null $targetCore + */ + public function wait(PromiseCore $targetCore = null): void + { + do { + $status = curl_multi_exec($this->multiHandle, $active); + $info = curl_multi_info_read($this->multiHandle); + if (false !== $info) { + $core = $this->findCoreByHandle($info['handle']); + + if (null === $core) { + // We have no promise for this handle. Drop it. + curl_multi_remove_handle($this->multiHandle, $info['handle']); + continue; + } + + if (CURLE_OK === $info['result']) { + $core->fulfill(); + } else { + $error = curl_error($core->getHandle()); + $core->reject(new RequestException($error, $core->getRequest())); + } + $this->remove($core); + + // This is a promise we are waited for. So exiting wait(). + if ($core === $targetCore) { + return; + } + } + } while ($status === CURLM_CALL_MULTI_PERFORM || $active); + } + + /** + * Find core by handle. + * + * @param resource $handle + * + * @return PromiseCore|null + */ + private function findCoreByHandle($handle): ?PromiseCore + { + foreach ($this->cores as $core) { + if ($core->getHandle() === $handle) { + return $core; + } + } + + return null; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/PromiseCore.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/PromiseCore.php new file mode 100644 index 0000000..b58776f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/PromiseCore.php @@ -0,0 +1,252 @@ + + */ +class PromiseCore +{ + /** + * HTTP request. + * + * @var RequestInterface + */ + private $request; + + /** + * cURL handle. + * + * @var resource + */ + private $handle; + + /** + * Response builder. + * + * @var ResponseBuilder + */ + private $responseBuilder; + + /** + * Promise state. + * + * @var string + */ + private $state; + + /** + * Exception. + * + * @var Exception|null + */ + private $exception = null; + + /** + * Functions to call when a response will be available. + * + * @var callable[] + */ + private $onFulfilled = []; + + /** + * Functions to call when an error happens. + * + * @var callable[] + */ + private $onRejected = []; + + /** + * Create shared core. + * + * @param RequestInterface $request HTTP request. + * @param resource|\CurlHandle $handle cURL handle. + * @param ResponseBuilder $responseBuilder Response builder. + * + * @throws \InvalidArgumentException If $handle is not a cURL resource. + */ + public function __construct( + RequestInterface $request, + $handle, + ResponseBuilder $responseBuilder + ) { + if (PHP_MAJOR_VERSION === 7) { + if (!is_resource($handle)) { + throw new \InvalidArgumentException( + sprintf( + 'Parameter $handle expected to be a cURL resource, %s given', + gettype($handle) + ) + ); + } elseif (get_resource_type($handle) !== 'curl') { + throw new \InvalidArgumentException( + sprintf( + 'Parameter $handle expected to be a cURL resource, %s resource given', + get_resource_type($handle) + ) + ); + } + } + + if (PHP_MAJOR_VERSION > 7 && !$handle instanceof \CurlHandle) { + throw new \InvalidArgumentException( + sprintf( + 'Parameter $handle expected to be a cURL resource, %s given', + get_debug_type($handle) + ) + ); + } + + $this->request = $request; + $this->handle = $handle; + $this->responseBuilder = $responseBuilder; + $this->state = Promise::PENDING; + } + + /** + * Add on fulfilled callback. + * + * @param callable $callback + */ + public function addOnFulfilled(callable $callback): void + { + if ($this->getState() === Promise::PENDING) { + $this->onFulfilled[] = $callback; + } elseif ($this->getState() === Promise::FULFILLED) { + $response = call_user_func($callback, $this->responseBuilder->getResponse()); + if ($response instanceof ResponseInterface) { + $this->responseBuilder->setResponse($response); + } + } + } + + /** + * Add on rejected callback. + * + * @param callable $callback + */ + public function addOnRejected(callable $callback): void + { + if ($this->getState() === Promise::PENDING) { + $this->onRejected[] = $callback; + } elseif ($this->getState() === Promise::REJECTED) { + $this->exception = call_user_func($callback, $this->exception); + } + } + + /** + * Return cURL handle. + * + * @return resource|\CurlHandle + */ + public function getHandle() + { + return $this->handle; + } + + /** + * Get the state of the promise, one of PENDING, FULFILLED or REJECTED. + * + * @return string + */ + public function getState(): string + { + return $this->state; + } + + /** + * Return request. + * + * @return RequestInterface + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * Return the value of the promise (fulfilled). + * + * @return ResponseInterface Response object only when the Promise is fulfilled + */ + public function getResponse(): ResponseInterface + { + return $this->responseBuilder->getResponse(); + } + + /** + * Get the reason why the promise was rejected. + * + * If the exception is an instance of Http\Client\Exception\HttpException it will contain + * the response object with the status code and the http reason. + * + * @return \Throwable Exception Object only when the Promise is rejected + * + * @throws \LogicException When the promise is not rejected + */ + public function getException(): \Throwable + { + if (null === $this->exception) { + throw new \LogicException('Promise is not rejected'); + } + + return $this->exception; + } + + /** + * Fulfill promise. + */ + public function fulfill(): void + { + $this->state = Promise::FULFILLED; + $response = $this->responseBuilder->getResponse(); + try { + $response->getBody()->seek(0); + } catch (\RuntimeException $e) { + $exception = new Exception\TransferException($e->getMessage(), $e->getCode(), $e); + $this->reject($exception); + + return; + } + + while (count($this->onFulfilled) > 0) { + $callback = array_shift($this->onFulfilled); + $response = call_user_func($callback, $response); + } + + if ($response instanceof ResponseInterface) { + $this->responseBuilder->setResponse($response); + } + } + + /** + * Reject promise. + * + * @param Exception $exception Reject reason + */ + public function reject(Exception $exception): void + { + $this->exception = $exception; + $this->state = Promise::REJECTED; + + while (count($this->onRejected) > 0) { + $callback = array_shift($this->onRejected); + try { + $exception = call_user_func($callback, $this->exception); + $this->exception = $exception; + } catch (Exception $exception) { + $this->exception = $exception; + } + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/ResponseBuilder.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/ResponseBuilder.php new file mode 100644 index 0000000..c7250e7 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/curl-client/src/ResponseBuilder.php @@ -0,0 +1,24 @@ +response = $response; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/CHANGELOG.md new file mode 100644 index 0000000..94e1a64 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/CHANGELOG.md @@ -0,0 +1,307 @@ +# Change Log + +## 1.13.0 - 2020-11-27 + +- Support discovering PSR-17 factories of `slim/psr7` package https://github.com/php-http/discovery/pull/192 + +## 1.12.0 - 2020-09-22 + +- Support discovering HttpClient of `php-http/guzzle7-adapter` https://github.com/php-http/discovery/pull/189 + +## 1.11.0 - 2020-09-22 + +- Use correct method name to find Uri Factory in PSR17 https://github.com/php-http/discovery/pull/181 + +## 1.10.0 - 2020-09-04 + +- Discover PSR-18 implementation of phalcon + +## 1.9.1 - 2020-07-13 + +### Fixed + +- Support PHP 7.4 and 8.0 + +## 1.9.0 - 2020-07-02 + +### Added + +- Support discovering PSR-18 factories of `guzzlehttp/guzzle` 7+ + +## 1.8.0 - 2020-06-14 + +### Added + +- Support discovering PSR-17 factories of `guzzlehttp/psr7` package +- Support discovering PSR-17 factories of `laminas/laminas-diactoros` package +- `ClassDiscovery::getStrategies()` to retrieve the list of current strategies. + +### Fixed + +- Ignore exception during discovery when Symfony HttplugClient checks if HTTPlug is available. + +## 1.7.4 - 2020-01-03 + +### Fixed + +- Improve conditions on Symfony's async HTTPlug client. + +## 1.7.3 - 2019-12-27 + +### Fixed + +- Enough conditions to only use Symfony HTTP client if all needed components are available. + +## 1.7.2 - 2019-12-27 + +### Fixed + +- Allow a condition to specify an interface and not just classes. + +## 1.7.1 - 2019-12-26 + +### Fixed + +- Better conditions to see if Symfony's HTTP clients are available. + +## 1.7.0 - 2019-06-30 + +### Added + +- Dropped support for PHP < 7.1 +- Support for `symfony/http-client` + +## 1.6.1 - 2019-02-23 + +### Fixed + +- MockClientStrategy also provides the mock client when requesting an async client + +## 1.6.0 - 2019-01-23 + +### Added + +- Support for PSR-17 factories +- Support for PSR-18 clients + +## 1.5.2 - 2018-12-31 + +Corrected mistakes in 1.5.1. The different between 1.5.2 and 1.5.0 is that +we removed some PHP 7 code. + +https://github.com/php-http/discovery/compare/1.5.0...1.5.2 + +## 1.5.1 - 2018-12-31 + +This version added new features by mistake. These are reverted in 1.5.2. + +Do not use 1.5.1. + +### Fixed + +- Removed PHP 7 code + +## 1.5.0 - 2018-12-30 + +### Added + +- Support for `nyholm/psr7` version 1.0. +- `ClassDiscovery::safeClassExists` which will help Magento users. +- Support for HTTPlug 2.0 +- Support for Buzz 1.0 +- Better error message when nothing found by introducing a new exception: `NoCandidateFoundException`. + +### Fixed + +- Fixed condition evaluation, it should stop after first invalid condition. + +## 1.4.0 - 2018-02-06 + +### Added + +- Discovery support for nyholm/psr7 + +## 1.3.0 - 2017-08-03 + +### Added + +- Discovery support for CakePHP adapter +- Discovery support for Zend adapter +- Discovery support for Artax adapter + +## 1.2.1 - 2017-03-02 + +### Fixed + +- Fixed minor issue with `MockClientStrategy`, also added more tests. + +## 1.2.0 - 2017-02-12 + +### Added + +- MockClientStrategy class. + +## 1.1.1 - 2016-11-27 + +### Changed + +- Made exception messages clearer. `StrategyUnavailableException` is no longer the previous exception to `DiscoveryFailedException`. +- `CommonClassesStrategy` is using `self` instead of `static`. Using `static` makes no sense when `CommonClassesStrategy` is final. + +## 1.1.0 - 2016-10-20 + +### Added + +- Discovery support for Slim Framework factories + +## 1.0.0 - 2016-07-18 + +### Added + +- Added back `Http\Discovery\NotFoundException` to preserve BC with 0.8 version. You may upgrade from 0.8.x and 0.9.x to 1.0.0 without any BC breaks. +- Added interface `Http\Discovery\Exception` which is implemented by all our exceptions + +### Changed + +- Puli strategy renamed to Puli Beta strategy to prevent incompatibility with a future Puli stable + +### Deprecated + +- For BC reasons, the old `Http\Discovery\NotFoundException` (extending the new exception) will be thrown until version 2.0 + + +## 0.9.1 - 2016-06-28 + +### Changed + +- Dropping PHP 5.4 support because we use the ::class constant. + + +## 0.9.0 - 2016-06-25 + +### Added + +- Discovery strategies to find classes + +### Changed + +- [Puli](http://puli.io) made optional +- Improved exceptions +- **[BC] `NotFoundException` moved to `Http\Discovery\Exception\NotFoundException`** + + +## 0.8.0 - 2016-02-11 + +### Changed + +- Puli composer plugin must be installed separately + + +## 0.7.0 - 2016-01-15 + +### Added + +- Temporary puli.phar (Beta 10) executable + +### Changed + +- Updated HTTPlug dependencies +- Updated Puli dependencies +- Local configuration to make tests passing + +### Removed + +- Puli CLI dependency + + +## 0.6.4 - 2016-01-07 + +### Fixed + +- Puli [not working](https://twitter.com/PuliPHP/status/685132540588507137) with the latest json-schema + + +## 0.6.3 - 2016-01-04 + +### Changed + +- Adjust Puli dependencies + + +## 0.6.2 - 2016-01-04 + +### Changed + +- Make Puli CLI a requirement + + +## 0.6.1 - 2016-01-03 + +### Changed + +- More flexible Puli requirement + + +## 0.6.0 - 2015-12-30 + +### Changed + +- Use [Puli](http://puli.io) for discovery +- Improved exception messages + + +## 0.5.0 - 2015-12-25 + +### Changed + +- Updated message factory dependency (php-http/message) + + +## 0.4.0 - 2015-12-17 + +### Added + +- Array condition evaluation in the Class Discovery + +### Removed + +- Message factories (moved to php-http/utils) + + +## 0.3.0 - 2015-11-18 + +### Added + +- HTTP Async Client Discovery +- Stream factories + +### Changed + +- Discoveries and Factories are final +- Message and Uri factories have the type in their names +- Diactoros Message factory uses Stream factory internally + +### Fixed + +- Improved docblocks for API documentation generation + + +## 0.2.0 - 2015-10-31 + +### Changed + +- Renamed AdapterDiscovery to ClientDiscovery + + +## 0.1.1 - 2015-06-13 + +### Fixed + +- Bad HTTP Adapter class name for Guzzle 5 + + +## 0.1.0 - 2015-06-12 + +### Added + +- Initial release diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/LICENSE new file mode 100644 index 0000000..4558d6f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2016 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/README.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/README.md new file mode 100644 index 0000000..6a3d18b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/README.md @@ -0,0 +1,46 @@ +# HTTPlug Discovery + +[![Latest Version](https://img.shields.io/github/release/php-http/discovery.svg?style=flat-square)](https://github.com/php-http/discovery/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Build Status](https://img.shields.io/travis/php-http/discovery/master.svg?style=flat-square)](https://travis-ci.org/php-http/discovery) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/discovery.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/discovery) +[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/discovery.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/discovery) +[![Total Downloads](https://img.shields.io/packagist/dt/php-http/discovery.svg?style=flat-square)](https://packagist.org/packages/php-http/discovery) + +**Finds installed HTTPlug implementations and PSR-7 message factories.** + + +## Install + +Via Composer + +``` bash +$ composer require php-http/discovery +``` + + +## Documentation + +Please see the [official documentation](http://php-http.readthedocs.org/en/latest/discovery.html). + + +## Testing + +``` bash +$ composer test +``` + + +## Contributing + +Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). + + +## Security + +If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). + + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/composer.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/composer.json new file mode 100644 index 0000000..3a8cc7c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/composer.json @@ -0,0 +1,51 @@ +{ + "name": "php-http/discovery", + "description": "Finds installed HTTPlug implementations and PSR-7 message factories", + "license": "MIT", + "keywords": ["http", "discovery", "client", "adapter", "message", "factory", "psr7"], + "homepage": "http://php-http.org", + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1", + "puli/composer-plugin": "1.0.0-beta10" + }, + "suggest": { + "puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details.", + "php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories" + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "spec\\Http\\Discovery\\": "spec/" + } + }, + "scripts": { + "test": "vendor/bin/phpspec run", + "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" + }, + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "conflict": { + "nyholm/psr7": "<1.0" + }, + "prefer-stable": true, + "minimum-stability": "beta" +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/ClassDiscovery.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/ClassDiscovery.php new file mode 100644 index 0000000..ee280f3 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/ClassDiscovery.php @@ -0,0 +1,246 @@ + + * @author Márk Sági-Kazár + * @author Tobias Nyholm + */ +abstract class ClassDiscovery +{ + /** + * A list of strategies to find classes. + * + * @var array + */ + private static $strategies = [ + Strategy\PuliBetaStrategy::class, + Strategy\CommonClassesStrategy::class, + Strategy\CommonPsr17ClassesStrategy::class, + ]; + + /** + * Discovery cache to make the second time we use discovery faster. + * + * @var array + */ + private static $cache = []; + + /** + * Finds a class. + * + * @param string $type + * + * @return string|\Closure + * + * @throws DiscoveryFailedException + */ + protected static function findOneByType($type) + { + // Look in the cache + if (null !== ($class = self::getFromCache($type))) { + return $class; + } + + $exceptions = []; + foreach (self::$strategies as $strategy) { + try { + $candidates = call_user_func($strategy.'::getCandidates', $type); + } catch (StrategyUnavailableException $e) { + $exceptions[] = $e; + + continue; + } + + foreach ($candidates as $candidate) { + if (isset($candidate['condition'])) { + if (!self::evaluateCondition($candidate['condition'])) { + continue; + } + } + + // save the result for later use + self::storeInCache($type, $candidate); + + return $candidate['class']; + } + + $exceptions[] = new NoCandidateFoundException($strategy, $candidates); + } + + throw DiscoveryFailedException::create($exceptions); + } + + /** + * Get a value from cache. + * + * @param string $type + * + * @return string|null + */ + private static function getFromCache($type) + { + if (!isset(self::$cache[$type])) { + return; + } + + $candidate = self::$cache[$type]; + if (isset($candidate['condition'])) { + if (!self::evaluateCondition($candidate['condition'])) { + return; + } + } + + return $candidate['class']; + } + + /** + * Store a value in cache. + * + * @param string $type + * @param string $class + */ + private static function storeInCache($type, $class) + { + self::$cache[$type] = $class; + } + + /** + * Set new strategies and clear the cache. + * + * @param array $strategies string array of fully qualified class name to a DiscoveryStrategy + */ + public static function setStrategies(array $strategies) + { + self::$strategies = $strategies; + self::clearCache(); + } + + /** + * Returns the currently configured discovery strategies as fully qualified class names. + * + * @return string[] + */ + public static function getStrategies(): iterable + { + return self::$strategies; + } + + /** + * Append a strategy at the end of the strategy queue. + * + * @param string $strategy Fully qualified class name to a DiscoveryStrategy + */ + public static function appendStrategy($strategy) + { + self::$strategies[] = $strategy; + self::clearCache(); + } + + /** + * Prepend a strategy at the beginning of the strategy queue. + * + * @param string $strategy Fully qualified class name to a DiscoveryStrategy + */ + public static function prependStrategy($strategy) + { + array_unshift(self::$strategies, $strategy); + self::clearCache(); + } + + /** + * Clear the cache. + */ + public static function clearCache() + { + self::$cache = []; + } + + /** + * Evaluates conditions to boolean. + * + * @param mixed $condition + * + * @return bool + */ + protected static function evaluateCondition($condition) + { + if (is_string($condition)) { + // Should be extended for functions, extensions??? + return self::safeClassExists($condition); + } + if (is_callable($condition)) { + return (bool) $condition(); + } + if (is_bool($condition)) { + return $condition; + } + if (is_array($condition)) { + foreach ($condition as $c) { + if (false === static::evaluateCondition($c)) { + // Immediately stop execution if the condition is false + return false; + } + } + + return true; + } + + return false; + } + + /** + * Get an instance of the $class. + * + * @param string|\Closure $class A FQCN of a class or a closure that instantiate the class. + * + * @return object + * + * @throws ClassInstantiationFailedException + */ + protected static function instantiateClass($class) + { + try { + if (is_string($class)) { + return new $class(); + } + + if (is_callable($class)) { + return $class(); + } + } catch (\Exception $e) { + throw new ClassInstantiationFailedException('Unexpected exception when instantiating class.', 0, $e); + } + + throw new ClassInstantiationFailedException('Could not instantiate class because parameter is neither a callable nor a string'); + } + + /** + * We want to do a "safe" version of PHP's "class_exists" because Magento has a bug + * (or they call it a "feature"). Magento is throwing an exception if you do class_exists() + * on a class that ends with "Factory" and if that file does not exits. + * + * This function will catch all potential exceptions and make sure it returns a boolean. + * + * @param string $class + * @param bool $autoload + * + * @return bool + */ + public static function safeClassExists($class) + { + try { + return class_exists($class) || interface_exists($class); + } catch (\Exception $e) { + return false; + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception.php new file mode 100644 index 0000000..973c908 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception.php @@ -0,0 +1,12 @@ + + */ +interface Exception +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/ClassInstantiationFailedException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/ClassInstantiationFailedException.php new file mode 100644 index 0000000..e95bf5d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/ClassInstantiationFailedException.php @@ -0,0 +1,14 @@ + + */ +final class ClassInstantiationFailedException extends \RuntimeException implements Exception +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/DiscoveryFailedException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/DiscoveryFailedException.php new file mode 100644 index 0000000..304b727 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/DiscoveryFailedException.php @@ -0,0 +1,51 @@ + + */ +final class DiscoveryFailedException extends \Exception implements Exception +{ + /** + * @var \Exception[] + */ + private $exceptions; + + /** + * @param string $message + * @param \Exception[] $exceptions + */ + public function __construct($message, array $exceptions = []) + { + $this->exceptions = $exceptions; + + parent::__construct($message); + } + + /** + * @param \Exception[] $exceptions + */ + public static function create($exceptions) + { + $message = 'Could not find resource using any discovery strategy. Find more information at http://docs.php-http.org/en/latest/discovery.html#common-errors'; + foreach ($exceptions as $e) { + $message .= "\n - ".$e->getMessage(); + } + $message .= "\n\n"; + + return new self($message, $exceptions); + } + + /** + * @return \Exception[] + */ + public function getExceptions() + { + return $this->exceptions; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/NoCandidateFoundException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/NoCandidateFoundException.php new file mode 100644 index 0000000..32f65db --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/NoCandidateFoundException.php @@ -0,0 +1,47 @@ + + */ +final class NoCandidateFoundException extends \Exception implements Exception +{ + /** + * @param string $strategy + */ + public function __construct($strategy, array $candidates) + { + $classes = array_map( + function ($a) { + return $a['class']; + }, + $candidates + ); + + $message = sprintf( + 'No valid candidate found using strategy "%s". We tested the following candidates: %s.', + $strategy, + implode(', ', array_map([$this, 'stringify'], $classes)) + ); + + parent::__construct($message); + } + + private function stringify($mixed) + { + if (is_string($mixed)) { + return $mixed; + } + + if (is_array($mixed) && 2 === count($mixed)) { + return sprintf('%s::%s', $this->stringify($mixed[0]), $mixed[1]); + } + + return is_object($mixed) ? get_class($mixed) : gettype($mixed); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/NotFoundException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/NotFoundException.php new file mode 100644 index 0000000..befbf48 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/NotFoundException.php @@ -0,0 +1,16 @@ + + */ +/*final */class NotFoundException extends \RuntimeException implements Exception +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/PuliUnavailableException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/PuliUnavailableException.php new file mode 100644 index 0000000..a6ade73 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/PuliUnavailableException.php @@ -0,0 +1,12 @@ + + */ +final class PuliUnavailableException extends StrategyUnavailableException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/StrategyUnavailableException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/StrategyUnavailableException.php new file mode 100644 index 0000000..89ecf35 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Exception/StrategyUnavailableException.php @@ -0,0 +1,15 @@ + + */ +class StrategyUnavailableException extends \RuntimeException implements Exception +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/HttpAsyncClientDiscovery.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/HttpAsyncClientDiscovery.php new file mode 100644 index 0000000..a0c4d5b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/HttpAsyncClientDiscovery.php @@ -0,0 +1,32 @@ + + */ +final class HttpAsyncClientDiscovery extends ClassDiscovery +{ + /** + * Finds an HTTP Async Client. + * + * @return HttpAsyncClient + * + * @throws Exception\NotFoundException + */ + public static function find() + { + try { + $asyncClient = static::findOneByType(HttpAsyncClient::class); + } catch (DiscoveryFailedException $e) { + throw new NotFoundException('No HTTPlug async clients found. Make sure to install a package providing "php-http/async-client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e); + } + + return static::instantiateClass($asyncClient); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/HttpClientDiscovery.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/HttpClientDiscovery.php new file mode 100644 index 0000000..2072b94 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/HttpClientDiscovery.php @@ -0,0 +1,32 @@ + + */ +final class HttpClientDiscovery extends ClassDiscovery +{ + /** + * Finds an HTTP Client. + * + * @return HttpClient + * + * @throws Exception\NotFoundException + */ + public static function find() + { + try { + $client = static::findOneByType(HttpClient::class); + } catch (DiscoveryFailedException $e) { + throw new NotFoundException('No HTTPlug clients found. Make sure to install a package providing "php-http/client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e); + } + + return static::instantiateClass($client); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/MessageFactoryDiscovery.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/MessageFactoryDiscovery.php new file mode 100644 index 0000000..659916d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/MessageFactoryDiscovery.php @@ -0,0 +1,34 @@ + + * + * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. + */ +final class MessageFactoryDiscovery extends ClassDiscovery +{ + /** + * Finds a Message Factory. + * + * @return MessageFactory + * + * @throws Exception\NotFoundException + */ + public static function find() + { + try { + $messageFactory = static::findOneByType(MessageFactory::class); + } catch (DiscoveryFailedException $e) { + throw new NotFoundException('No message factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e); + } + + return static::instantiateClass($messageFactory); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/NotFoundException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/NotFoundException.php new file mode 100644 index 0000000..d59dadb --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/NotFoundException.php @@ -0,0 +1,14 @@ + + * + * @deprecated since since version 1.0, and will be removed in 2.0. Use {@link \Http\Discovery\Exception\NotFoundException} instead. + */ +final class NotFoundException extends \Http\Discovery\Exception\NotFoundException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Psr17FactoryDiscovery.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Psr17FactoryDiscovery.php new file mode 100644 index 0000000..a73c641 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Psr17FactoryDiscovery.php @@ -0,0 +1,136 @@ + + */ +final class Psr17FactoryDiscovery extends ClassDiscovery +{ + private static function createException($type, Exception $e) + { + return new \Http\Discovery\Exception\NotFoundException( + 'No PSR-17 '.$type.' found. Install a package from this list: https://packagist.org/providers/psr/http-factory-implementation', + 0, + $e + ); + } + + /** + * @return RequestFactoryInterface + * + * @throws Exception\NotFoundException + */ + public static function findRequestFactory() + { + try { + $messageFactory = static::findOneByType(RequestFactoryInterface::class); + } catch (DiscoveryFailedException $e) { + throw self::createException('request factory', $e); + } + + return static::instantiateClass($messageFactory); + } + + /** + * @return ResponseFactoryInterface + * + * @throws Exception\NotFoundException + */ + public static function findResponseFactory() + { + try { + $messageFactory = static::findOneByType(ResponseFactoryInterface::class); + } catch (DiscoveryFailedException $e) { + throw self::createException('response factory', $e); + } + + return static::instantiateClass($messageFactory); + } + + /** + * @return ServerRequestFactoryInterface + * + * @throws Exception\NotFoundException + */ + public static function findServerRequestFactory() + { + try { + $messageFactory = static::findOneByType(ServerRequestFactoryInterface::class); + } catch (DiscoveryFailedException $e) { + throw self::createException('server request factory', $e); + } + + return static::instantiateClass($messageFactory); + } + + /** + * @return StreamFactoryInterface + * + * @throws Exception\NotFoundException + */ + public static function findStreamFactory() + { + try { + $messageFactory = static::findOneByType(StreamFactoryInterface::class); + } catch (DiscoveryFailedException $e) { + throw self::createException('stream factory', $e); + } + + return static::instantiateClass($messageFactory); + } + + /** + * @return UploadedFileFactoryInterface + * + * @throws Exception\NotFoundException + */ + public static function findUploadedFileFactory() + { + try { + $messageFactory = static::findOneByType(UploadedFileFactoryInterface::class); + } catch (DiscoveryFailedException $e) { + throw self::createException('uploaded file factory', $e); + } + + return static::instantiateClass($messageFactory); + } + + /** + * @return UriFactoryInterface + * + * @throws Exception\NotFoundException + */ + public static function findUriFactory() + { + try { + $messageFactory = static::findOneByType(UriFactoryInterface::class); + } catch (DiscoveryFailedException $e) { + throw self::createException('url factory', $e); + } + + return static::instantiateClass($messageFactory); + } + + /** + * @return UriFactoryInterface + * + * @throws Exception\NotFoundException + * + * @deprecated This will be removed in 2.0. Consider using the findUriFactory() method. + */ + public static function findUrlFactory() + { + return static::findUriFactory(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Psr18ClientDiscovery.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Psr18ClientDiscovery.php new file mode 100644 index 0000000..7fcdb17 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Psr18ClientDiscovery.php @@ -0,0 +1,32 @@ + + */ +final class Psr18ClientDiscovery extends ClassDiscovery +{ + /** + * Finds a PSR-18 HTTP Client. + * + * @return ClientInterface + * + * @throws Exception\NotFoundException + */ + public static function find() + { + try { + $client = static::findOneByType(ClientInterface::class); + } catch (DiscoveryFailedException $e) { + throw new \Http\Discovery\Exception\NotFoundException('No PSR-18 clients found. Make sure to install a package providing "psr/http-client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e); + } + + return static::instantiateClass($client); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/CommonClassesStrategy.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/CommonClassesStrategy.php new file mode 100644 index 0000000..0137c91 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/CommonClassesStrategy.php @@ -0,0 +1,181 @@ + + */ +final class CommonClassesStrategy implements DiscoveryStrategy +{ + /** + * @var array + */ + private static $classes = [ + MessageFactory::class => [ + ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], + ['class' => GuzzleMessageFactory::class, 'condition' => [GuzzleRequest::class, GuzzleMessageFactory::class]], + ['class' => DiactorosMessageFactory::class, 'condition' => [DiactorosRequest::class, DiactorosMessageFactory::class]], + ['class' => SlimMessageFactory::class, 'condition' => [SlimRequest::class, SlimMessageFactory::class]], + ], + StreamFactory::class => [ + ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], + ['class' => GuzzleStreamFactory::class, 'condition' => [GuzzleRequest::class, GuzzleStreamFactory::class]], + ['class' => DiactorosStreamFactory::class, 'condition' => [DiactorosRequest::class, DiactorosStreamFactory::class]], + ['class' => SlimStreamFactory::class, 'condition' => [SlimRequest::class, SlimStreamFactory::class]], + ], + UriFactory::class => [ + ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], + ['class' => GuzzleUriFactory::class, 'condition' => [GuzzleRequest::class, GuzzleUriFactory::class]], + ['class' => DiactorosUriFactory::class, 'condition' => [DiactorosRequest::class, DiactorosUriFactory::class]], + ['class' => SlimUriFactory::class, 'condition' => [SlimRequest::class, SlimUriFactory::class]], + ], + HttpAsyncClient::class => [ + ['class' => SymfonyHttplug::class, 'condition' => [SymfonyHttplug::class, Promise::class, RequestFactory::class, [self::class, 'isPsr17FactoryInstalled']]], + ['class' => Guzzle7::class, 'condition' => Guzzle7::class], + ['class' => Guzzle6::class, 'condition' => Guzzle6::class], + ['class' => Curl::class, 'condition' => Curl::class], + ['class' => React::class, 'condition' => React::class], + ], + HttpClient::class => [ + ['class' => SymfonyHttplug::class, 'condition' => [SymfonyHttplug::class, RequestFactory::class, [self::class, 'isPsr17FactoryInstalled']]], + ['class' => Guzzle7::class, 'condition' => Guzzle7::class], + ['class' => Guzzle6::class, 'condition' => Guzzle6::class], + ['class' => Guzzle5::class, 'condition' => Guzzle5::class], + ['class' => Curl::class, 'condition' => Curl::class], + ['class' => Socket::class, 'condition' => Socket::class], + ['class' => Buzz::class, 'condition' => Buzz::class], + ['class' => React::class, 'condition' => React::class], + ['class' => Cake::class, 'condition' => Cake::class], + ['class' => Zend::class, 'condition' => Zend::class], + ['class' => Artax::class, 'condition' => Artax::class], + [ + 'class' => [self::class, 'buzzInstantiate'], + 'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class], + ], + ], + Psr18Client::class => [ + [ + 'class' => [self::class, 'symfonyPsr18Instantiate'], + 'condition' => [SymfonyPsr18::class, Psr17RequestFactory::class], + ], + [ + 'class' => GuzzleHttp::class, + 'condition' => [self::class, 'isGuzzleImplementingPsr18'], + ], + [ + 'class' => [self::class, 'buzzInstantiate'], + 'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class], + ], + ], + ]; + + /** + * {@inheritdoc} + */ + public static function getCandidates($type) + { + if (Psr18Client::class === $type) { + return self::getPsr18Candidates(); + } + + return self::$classes[$type] ?? []; + } + + /** + * @return array The return value is always an array with zero or more elements. Each + * element is an array with two keys ['class' => string, 'condition' => mixed]. + */ + private static function getPsr18Candidates() + { + $candidates = self::$classes[Psr18Client::class]; + + // HTTPlug 2.0 clients implements PSR18Client too. + foreach (self::$classes[HttpClient::class] as $c) { + try { + if (is_subclass_of($c['class'], Psr18Client::class)) { + $candidates[] = $c; + } + } catch (\Throwable $e) { + trigger_error(sprintf('Got exception "%s (%s)" while checking if a PSR-18 Client is available', get_class($e), $e->getMessage()), E_USER_WARNING); + } + } + + return $candidates; + } + + public static function buzzInstantiate() + { + return new \Buzz\Client\FileGetContents(MessageFactoryDiscovery::find()); + } + + public static function symfonyPsr18Instantiate() + { + return new SymfonyPsr18(null, Psr17FactoryDiscovery::findResponseFactory(), Psr17FactoryDiscovery::findStreamFactory()); + } + + public static function isGuzzleImplementingPsr18() + { + return defined('GuzzleHttp\ClientInterface::MAJOR_VERSION'); + } + + /** + * Can be used as a condition. + * + * @return bool + */ + public static function isPsr17FactoryInstalled() + { + try { + Psr17FactoryDiscovery::findResponseFactory(); + } catch (NotFoundException $e) { + return false; + } catch (\Throwable $e) { + trigger_error(sprintf('Got exception "%s (%s)" while checking if a PSR-17 ResponseFactory is available', get_class($e), $e->getMessage()), E_USER_WARNING); + + return false; + } + + return true; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/CommonPsr17ClassesStrategy.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/CommonPsr17ClassesStrategy.php new file mode 100644 index 0000000..fc26778 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/CommonPsr17ClassesStrategy.php @@ -0,0 +1,105 @@ + + */ +final class CommonPsr17ClassesStrategy implements DiscoveryStrategy +{ + /** + * @var array + */ + private static $classes = [ + RequestFactoryInterface::class => [ + 'Phalcon\Http\Message\RequestFactory', + 'Nyholm\Psr7\Factory\Psr17Factory', + 'Zend\Diactoros\RequestFactory', + 'GuzzleHttp\Psr7\HttpFactory', + 'Http\Factory\Diactoros\RequestFactory', + 'Http\Factory\Guzzle\RequestFactory', + 'Http\Factory\Slim\RequestFactory', + 'Laminas\Diactoros\RequestFactory', + 'Slim\Psr7\Factory\RequestFactory', + ], + ResponseFactoryInterface::class => [ + 'Phalcon\Http\Message\ResponseFactory', + 'Nyholm\Psr7\Factory\Psr17Factory', + 'Zend\Diactoros\ResponseFactory', + 'GuzzleHttp\Psr7\HttpFactory', + 'Http\Factory\Diactoros\ResponseFactory', + 'Http\Factory\Guzzle\ResponseFactory', + 'Http\Factory\Slim\ResponseFactory', + 'Laminas\Diactoros\ResponseFactory', + 'Slim\Psr7\Factory\ResponseFactory', + ], + ServerRequestFactoryInterface::class => [ + 'Phalcon\Http\Message\ServerRequestFactory', + 'Nyholm\Psr7\Factory\Psr17Factory', + 'Zend\Diactoros\ServerRequestFactory', + 'GuzzleHttp\Psr7\HttpFactory', + 'Http\Factory\Diactoros\ServerRequestFactory', + 'Http\Factory\Guzzle\ServerRequestFactory', + 'Http\Factory\Slim\ServerRequestFactory', + 'Laminas\Diactoros\ServerRequestFactory', + 'Slim\Psr7\Factory\ServerRequestFactory', + ], + StreamFactoryInterface::class => [ + 'Phalcon\Http\Message\StreamFactory', + 'Nyholm\Psr7\Factory\Psr17Factory', + 'Zend\Diactoros\StreamFactory', + 'GuzzleHttp\Psr7\HttpFactory', + 'Http\Factory\Diactoros\StreamFactory', + 'Http\Factory\Guzzle\StreamFactory', + 'Http\Factory\Slim\StreamFactory', + 'Laminas\Diactoros\StreamFactory', + 'Slim\Psr7\Factory\StreamFactory', + ], + UploadedFileFactoryInterface::class => [ + 'Phalcon\Http\Message\UploadedFileFactory', + 'Nyholm\Psr7\Factory\Psr17Factory', + 'Zend\Diactoros\UploadedFileFactory', + 'GuzzleHttp\Psr7\HttpFactory', + 'Http\Factory\Diactoros\UploadedFileFactory', + 'Http\Factory\Guzzle\UploadedFileFactory', + 'Http\Factory\Slim\UploadedFileFactory', + 'Laminas\Diactoros\UploadedFileFactory', + 'Slim\Psr7\Factory\UploadedFileFactory', + ], + UriFactoryInterface::class => [ + 'Phalcon\Http\Message\UriFactory', + 'Nyholm\Psr7\Factory\Psr17Factory', + 'Zend\Diactoros\UriFactory', + 'GuzzleHttp\Psr7\HttpFactory', + 'Http\Factory\Diactoros\UriFactory', + 'Http\Factory\Guzzle\UriFactory', + 'Http\Factory\Slim\UriFactory', + 'Laminas\Diactoros\UriFactory', + 'Slim\Psr7\Factory\UriFactory', + ], + ]; + + /** + * {@inheritdoc} + */ + public static function getCandidates($type) + { + $candidates = []; + if (isset(self::$classes[$type])) { + foreach (self::$classes[$type] as $class) { + $candidates[] = ['class' => $class, 'condition' => [$class]]; + } + } + + return $candidates; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/DiscoveryStrategy.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/DiscoveryStrategy.php new file mode 100644 index 0000000..641485a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/DiscoveryStrategy.php @@ -0,0 +1,23 @@ + + */ +interface DiscoveryStrategy +{ + /** + * Find a resource of a specific type. + * + * @param string $type + * + * @return array The return value is always an array with zero or more elements. Each + * element is an array with two keys ['class' => string, 'condition' => mixed]. + * + * @throws StrategyUnavailableException if we cannot use this strategy. + */ + public static function getCandidates($type); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/MockClientStrategy.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/MockClientStrategy.php new file mode 100644 index 0000000..0cee3f7 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/MockClientStrategy.php @@ -0,0 +1,27 @@ + + */ +final class MockClientStrategy implements DiscoveryStrategy +{ + /** + * {@inheritdoc} + */ + public static function getCandidates($type) + { + if (is_a(HttpClient::class, $type, true) || is_a(HttpAsyncClient::class, $type, true)) { + return [['class' => Mock::class, 'condition' => Mock::class]]; + } + + return []; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/PuliBetaStrategy.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/PuliBetaStrategy.php new file mode 100644 index 0000000..c1e1fb7 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/Strategy/PuliBetaStrategy.php @@ -0,0 +1,92 @@ + + * @author Márk Sági-Kazár + */ +class PuliBetaStrategy implements DiscoveryStrategy +{ + /** + * @var GeneratedPuliFactory + */ + protected static $puliFactory; + + /** + * @var Discovery + */ + protected static $puliDiscovery; + + /** + * @return GeneratedPuliFactory + * + * @throws PuliUnavailableException + */ + private static function getPuliFactory() + { + if (null === self::$puliFactory) { + if (!defined('PULI_FACTORY_CLASS')) { + throw new PuliUnavailableException('Puli Factory is not available'); + } + + $puliFactoryClass = PULI_FACTORY_CLASS; + + if (!ClassDiscovery::safeClassExists($puliFactoryClass)) { + throw new PuliUnavailableException('Puli Factory class does not exist'); + } + + self::$puliFactory = new $puliFactoryClass(); + } + + return self::$puliFactory; + } + + /** + * Returns the Puli discovery layer. + * + * @return Discovery + * + * @throws PuliUnavailableException + */ + private static function getPuliDiscovery() + { + if (!isset(self::$puliDiscovery)) { + $factory = self::getPuliFactory(); + $repository = $factory->createRepository(); + + self::$puliDiscovery = $factory->createDiscovery($repository); + } + + return self::$puliDiscovery; + } + + /** + * {@inheritdoc} + */ + public static function getCandidates($type) + { + $returnData = []; + $bindings = self::getPuliDiscovery()->findBindings($type); + + foreach ($bindings as $binding) { + $condition = true; + if ($binding->hasParameterValue('depends')) { + $condition = $binding->getParameterValue('depends'); + } + $returnData[] = ['class' => $binding->getClassName(), 'condition' => $condition]; + } + + return $returnData; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/StreamFactoryDiscovery.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/StreamFactoryDiscovery.php new file mode 100644 index 0000000..e11c49a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/StreamFactoryDiscovery.php @@ -0,0 +1,34 @@ + + * + * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. + */ +final class StreamFactoryDiscovery extends ClassDiscovery +{ + /** + * Finds a Stream Factory. + * + * @return StreamFactory + * + * @throws Exception\NotFoundException + */ + public static function find() + { + try { + $streamFactory = static::findOneByType(StreamFactory::class); + } catch (DiscoveryFailedException $e) { + throw new NotFoundException('No stream factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e); + } + + return static::instantiateClass($streamFactory); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/UriFactoryDiscovery.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/UriFactoryDiscovery.php new file mode 100644 index 0000000..db3add2 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/discovery/src/UriFactoryDiscovery.php @@ -0,0 +1,34 @@ + + * + * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. + */ +final class UriFactoryDiscovery extends ClassDiscovery +{ + /** + * Finds a URI Factory. + * + * @return UriFactory + * + * @throws Exception\NotFoundException + */ + public static function find() + { + try { + $uriFactory = static::findOneByType(UriFactory::class); + } catch (DiscoveryFailedException $e) { + throw new NotFoundException('No uri factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e); + } + + return static::instantiateClass($uriFactory); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/CHANGELOG.md new file mode 100644 index 0000000..29b03e0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/CHANGELOG.md @@ -0,0 +1,129 @@ +# Change Log + + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + + +## [Unreleased] + +## [2.2.0] - 2020-07-13 + +### Changed + +- Support PHP 7.1-8.0 + +## [2.1.0] - 2019-12-27 + +### Changed + +- `Http\Client\Exception\NetworkException` no longer extends `Http\Client\Exception\RequestException`, + in accordance with [PSR-18](https://www.php-fig.org/psr/psr-18/) + +## [2.0.0] - 2018-10-31 + +This version is no BC break for consumers using HTTPlug. However, HTTP clients that +implement HTTPlug need to adjust because we add return type declarations. + +### Added + +- Support for PSR-18 (HTTP client). + +### Changed + +- **BC Break:** `HttpClient::sendRequest(RequestInterface $request)` has a return type annotation. The new +signature is `HttpClient::sendRequest(RequestInterface $request): ResponseInterface`. +- **BC Break:** `RequestException::getRequest()` has a return type annotation. The new +signature is `RequestException::getRequest(): RequestInterface`. + +### Removed + +- PHP 5 support + + +## [1.1.0] - 2016-08-31 + +### Added + +- HttpFulfilledPromise and HttpRejectedPromise which respect the HttpAsyncClient interface + + +## [1.0.0] - 2016-01-26 + +### Removed + +- Stability configuration from composer + + +## [1.0.0-RC1] - 2016-01-12 + +### Changed + +- Updated package files +- Updated promise dependency to RC1 + + +## [1.0.0-beta] - 2015-12-17 + +### Added + +- Puli configuration and binding types + +### Changed + +- Exception concept + + +## [1.0.0-alpha3] - 2015-12-13 + +### Changed + +- Async client does not throw exceptions + +### Removed + +- Promise interface moved to its own repository: [php-http/promise](https://github.com/php-http/promise) + + +## [1.0.0-alpha2] - 2015-11-16 + +### Added + +- Async client and Promise interface + + +## [1.0.0-alpha] - 2015-10-26 + +### Added + +- Better domain exceptions. + +### Changed + +- Purpose of the library: general HTTP CLient abstraction. + +### Removed + +- Request options: they should be configured at construction time. +- Multiple request sending: should be done asynchronously using Async Client. +- `getName` method + + +## 0.1.0 - 2015-06-03 + +### Added + +- Initial release + + +[Unreleased]: https://github.com/php-http/httplug/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/php-http/httplug/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/php-http/httplug/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/php-http/httplug/compare/v1.0.0-RC1...v1.0.0 +[1.0.0-RC1]: https://github.com/php-http/httplug/compare/v1.0.0-beta...v1.0.0-RC1 +[1.0.0-beta]: https://github.com/php-http/httplug/compare/v1.0.0-alpha3...v1.0.0-beta +[1.0.0-alpha3]: https://github.com/php-http/httplug/compare/v1.0.0-alpha2...v1.0.0-alpha3 +[1.0.0-alpha2]: https://github.com/php-http/httplug/compare/v1.0.0-alpha...v1.0.0-alpha2 +[1.0.0-alpha]: https://github.com/php-http/httplug/compare/v0.1.0...v1.0.0-alpha diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/LICENSE new file mode 100644 index 0000000..8cd264c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Eric GELOEN +Copyright (c) 2015 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/README.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/README.md new file mode 100644 index 0000000..aabcf25 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/README.md @@ -0,0 +1,63 @@ +# HTTPlug + +[![Latest Version](https://img.shields.io/github/release/php-http/httplug.svg?style=flat-square)](https://github.com/php-http/httplug/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Build Status](https://img.shields.io/travis/php-http/httplug/master.svg?style=flat-square)](https://travis-ci.org/php-http/httplug) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/httplug.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/httplug) +[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/httplug.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/httplug) +[![Total Downloads](https://img.shields.io/packagist/dt/php-http/httplug.svg?style=flat-square)](https://packagist.org/packages/php-http/httplug) + +[![Slack Status](http://slack.httplug.io/badge.svg)](http://slack.httplug.io) +[![Email](https://img.shields.io/badge/email-team@httplug.io-blue.svg?style=flat-square)](mailto:team@httplug.io) + +**HTTPlug, the HTTP client abstraction for PHP.** + + +## Intro + +HTTP client standard built on [PSR-7](http://www.php-fig.org/psr/psr-7/) HTTP +messages. The HTTPlug client interface is compatible with the official standard +for the HTTP client interface, [PSR-18](http://www.php-fig.org/psr/psr-18/). +HTTPlug adds an interface for asynchronous HTTP requests, which PSR-18 does not +cover. + +Since HTTPlug has already been widely adopted and a whole ecosystem has been +built around it, we will keep maintaining this package for the time being. +HTTPlug 2.0 and newer extend the PSR-18 interface to allow for a convenient +migration path. + +New client implementations and consumers should use the PSR-18 interfaces +directly. In the long term, we expect PSR-18 to completely replace the need +for HTTPlug. + + +## History + +HTTPlug is the official successor of the [ivory http adapter](https://github.com/egeloen/ivory-http-adapter). +HTTPlug is a predecessor of [PSR-18](http://www.php-fig.org/psr/psr-18/) + + +## Install + +Via Composer + +``` bash +$ composer require php-http/httplug +``` + + +## Documentation + +Please see the [official documentation](http://docs.php-http.org). + + +## Testing + +``` bash +$ composer test +``` + + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/composer.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/composer.json new file mode 100644 index 0000000..268b27e --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/composer.json @@ -0,0 +1,45 @@ +{ + "name": "php-http/httplug", + "description": "HTTPlug, the HTTP client abstraction for PHP", + "keywords": [ + "http", + "client" + ], + "homepage": "http://httplug.io", + "license": "MIT", + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1", + "phpspec/phpspec": "^5.1 || ^6.0" + }, + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "scripts": { + "test": "vendor/bin/phpspec run", + "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/puli.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/puli.json new file mode 100644 index 0000000..4168331 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/puli.json @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "name": "php-http/httplug", + "binding-types": { + "Http\\Client\\HttpAsyncClient": { + "description": "Async HTTP Client" + }, + "Http\\Client\\HttpClient": { + "description": "HTTP Client" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception.php new file mode 100644 index 0000000..4df164c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception.php @@ -0,0 +1,14 @@ + + */ +interface Exception extends PsrClientException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/HttpException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/HttpException.php new file mode 100644 index 0000000..6c2a007 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/HttpException.php @@ -0,0 +1,65 @@ + + */ +class HttpException extends RequestException +{ + /** + * @var ResponseInterface + */ + protected $response; + + /** + * @param string $message + */ + public function __construct( + $message, + RequestInterface $request, + ResponseInterface $response, + \Exception $previous = null + ) { + parent::__construct($message, $request, $previous); + + $this->response = $response; + $this->code = $response->getStatusCode(); + } + + /** + * Returns the response. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } + + /** + * Factory method to create a new exception with a normalized error message. + */ + public static function create( + RequestInterface $request, + ResponseInterface $response, + \Exception $previous = null + ) { + $message = sprintf( + '[url] %s [http method] %s [status code] %s [reason phrase] %s', + $request->getRequestTarget(), + $request->getMethod(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + return new static($message, $request, $response, $previous); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/NetworkException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/NetworkException.php new file mode 100644 index 0000000..9b4f1e8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/NetworkException.php @@ -0,0 +1,28 @@ + + */ +class NetworkException extends TransferException implements PsrNetworkException +{ + use RequestAwareTrait; + + /** + * @param string $message + */ + public function __construct($message, RequestInterface $request, \Exception $previous = null) + { + $this->setRequest($request); + + parent::__construct($message, 0, $previous); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/RequestAwareTrait.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/RequestAwareTrait.php new file mode 100644 index 0000000..71b4bb8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/RequestAwareTrait.php @@ -0,0 +1,26 @@ +request = $request; + } + + /** + * {@inheritdoc} + */ + public function getRequest(): RequestInterface + { + return $this->request; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/RequestException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/RequestException.php new file mode 100644 index 0000000..f6c60ce --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/RequestException.php @@ -0,0 +1,29 @@ + + */ +class RequestException extends TransferException implements PsrRequestException +{ + use RequestAwareTrait; + + /** + * @param string $message + */ + public function __construct($message, RequestInterface $request, \Exception $previous = null) + { + $this->setRequest($request); + + parent::__construct($message, 0, $previous); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/TransferException.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/TransferException.php new file mode 100644 index 0000000..a858cf5 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Exception/TransferException.php @@ -0,0 +1,14 @@ + + */ +class TransferException extends \RuntimeException implements Exception +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/HttpAsyncClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/HttpAsyncClient.php new file mode 100644 index 0000000..c3b9d61 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/HttpAsyncClient.php @@ -0,0 +1,25 @@ + + */ +interface HttpAsyncClient +{ + /** + * Sends a PSR-7 request in an asynchronous way. + * + * Exceptions related to processing the request are available from the returned Promise. + * + * @return Promise resolves a PSR-7 Response or fails with an Http\Client\Exception + * + * @throws \Exception If processing the request is impossible (eg. bad configuration). + */ + public function sendAsyncRequest(RequestInterface $request); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/HttpClient.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/HttpClient.php new file mode 100644 index 0000000..4442bd0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/HttpClient.php @@ -0,0 +1,15 @@ +response = $response; + } + + /** + * {@inheritdoc} + */ + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if (null === $onFulfilled) { + return $this; + } + + try { + return new self($onFulfilled($this->response)); + } catch (Exception $e) { + return new HttpRejectedPromise($e); + } + } + + /** + * {@inheritdoc} + */ + public function getState() + { + return Promise::FULFILLED; + } + + /** + * {@inheritdoc} + */ + public function wait($unwrap = true) + { + if ($unwrap) { + return $this->response; + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Promise/HttpRejectedPromise.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Promise/HttpRejectedPromise.php new file mode 100644 index 0000000..8af97de --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/httplug/src/Promise/HttpRejectedPromise.php @@ -0,0 +1,53 @@ +exception = $exception; + } + + /** + * {@inheritdoc} + */ + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if (null === $onRejected) { + return $this; + } + + try { + return new HttpFulfilledPromise($onRejected($this->exception)); + } catch (Exception $e) { + return new self($e); + } + } + + /** + * {@inheritdoc} + */ + public function getState() + { + return Promise::REJECTED; + } + + /** + * {@inheritdoc} + */ + public function wait($unwrap = true) + { + if ($unwrap) { + throw $this->exception; + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/CHANGELOG.md new file mode 100644 index 0000000..4711924 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/CHANGELOG.md @@ -0,0 +1,65 @@ +# Change Log + + +## 1.0.2 - 2015-12-19 + +### Added + +- Request and Response factory binding types to Puli + + +## 1.0.1 - 2015-12-17 + +### Added + +- Puli configuration and binding types + + +## 1.0.0 - 2015-12-15 + +### Added + +- Response Factory in order to be reused in Message and Server Message factories +- Request Factory + +### Changed + +- Message Factory extends Request and Response factories + + +## 1.0.0-RC1 - 2015-12-14 + +### Added + +- CS check + +### Changed + +- RuntimeException is thrown when the StreamFactory cannot write to the underlying stream + + +## 0.3.0 - 2015-11-16 + +### Removed + +- Client Context Factory +- Factory Awares and Templates + + +## 0.2.0 - 2015-11-16 + +### Changed + +- Reordered the parameters when creating a message to have the protocol last, +as its the least likely to need to be changed. + + +## 0.1.0 - 2015-06-01 + +### Added + +- Initial release + +### Changed + +- Helpers are renamed to templates diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/LICENSE new file mode 100644 index 0000000..8e2c4a0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/README.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/README.md new file mode 100644 index 0000000..4654495 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/README.md @@ -0,0 +1,36 @@ +# PSR-7 Message Factory + +[![Latest Version](https://img.shields.io/github/release/php-http/message-factory.svg?style=flat-square)](https://github.com/php-http/message-factory/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Total Downloads](https://img.shields.io/packagist/dt/php-http/message-factory.svg?style=flat-square)](https://packagist.org/packages/php-http/message-factory) + +**Factory interfaces for PSR-7 HTTP Message.** + + +## Install + +Via Composer + +``` bash +$ composer require php-http/message-factory +``` + + +## Documentation + +Please see the [official documentation](http://php-http.readthedocs.org/en/latest/message-factory/). + + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. + + +## Security + +If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). + + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/composer.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/composer.json new file mode 100644 index 0000000..7c72feb --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/composer.json @@ -0,0 +1,27 @@ +{ + "name": "php-http/message-factory", + "description": "Factory interfaces for PSR-7 HTTP Message", + "license": "MIT", + "keywords": ["http", "factory", "message", "stream", "uri"], + "homepage": "http://php-http.org", + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/puli.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/puli.json new file mode 100644 index 0000000..08d3762 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/puli.json @@ -0,0 +1,43 @@ +{ + "version": "1.0", + "binding-types": { + "Http\\Message\\MessageFactory": { + "description": "PSR-7 Message Factory", + "parameters": { + "depends": { + "description": "Optional class dependency which can be checked by consumers" + } + } + }, + "Http\\Message\\RequestFactory": { + "parameters": { + "depends": { + "description": "Optional class dependency which can be checked by consumers" + } + } + }, + "Http\\Message\\ResponseFactory": { + "parameters": { + "depends": { + "description": "Optional class dependency which can be checked by consumers" + } + } + }, + "Http\\Message\\StreamFactory": { + "description": "PSR-7 Stream Factory", + "parameters": { + "depends": { + "description": "Optional class dependency which can be checked by consumers" + } + } + }, + "Http\\Message\\UriFactory": { + "description": "PSR-7 URI Factory", + "parameters": { + "depends": { + "description": "Optional class dependency which can be checked by consumers" + } + } + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/MessageFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/MessageFactory.php new file mode 100644 index 0000000..965aaa8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/MessageFactory.php @@ -0,0 +1,12 @@ + + */ +interface MessageFactory extends RequestFactory, ResponseFactory +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/RequestFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/RequestFactory.php new file mode 100644 index 0000000..624e82f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/RequestFactory.php @@ -0,0 +1,34 @@ + + */ +interface RequestFactory +{ + /** + * Creates a new PSR-7 request. + * + * @param string $method + * @param string|UriInterface $uri + * @param array $headers + * @param resource|string|StreamInterface|null $body + * @param string $protocolVersion + * + * @return RequestInterface + */ + public function createRequest( + $method, + $uri, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/ResponseFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/ResponseFactory.php new file mode 100644 index 0000000..2411ed3 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/ResponseFactory.php @@ -0,0 +1,35 @@ + + */ +interface ResponseFactory +{ + /** + * Creates a new PSR-7 response. + * + * @param int $statusCode + * @param string|null $reasonPhrase + * @param array $headers + * @param resource|string|StreamInterface|null $body + * @param string $protocolVersion + * + * @return ResponseInterface + */ + public function createResponse( + $statusCode = 200, + $reasonPhrase = null, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/StreamFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/StreamFactory.php new file mode 100644 index 0000000..327a902 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/StreamFactory.php @@ -0,0 +1,25 @@ + + */ +interface StreamFactory +{ + /** + * Creates a new PSR-7 stream. + * + * @param string|resource|StreamInterface|null $body + * + * @return StreamInterface + * + * @throws \InvalidArgumentException If the stream body is invalid. + * @throws \RuntimeException If creating the stream from $body fails. + */ + public function createStream($body = null); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/UriFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/UriFactory.php new file mode 100644 index 0000000..f05e625 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message-factory/src/UriFactory.php @@ -0,0 +1,24 @@ + + */ +interface UriFactory +{ + /** + * Creates an PSR-7 URI. + * + * @param string|UriInterface $uri + * + * @return UriInterface + * + * @throws \InvalidArgumentException If the $uri argument can not be converted into a valid URI. + */ + public function createUri($uri); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/CHANGELOG.md new file mode 100644 index 0000000..b36da2d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/CHANGELOG.md @@ -0,0 +1,237 @@ +# Change Log + + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [1.11.0] - 2020-02-01 + +- Migrated from `zendframework/zend-diactoros` to `laminas/laminas-diactoros`. + Users are encouraged to update their dependencies by simply replacing the Zend package with the Laminas package. + Due to the [laminas-zendframework-brige](https://github.com/laminas/laminas-zendframework-bridge), BC changes + are not expected and legacy code does not need to be refactored (though it is + [recommended and simple](https://docs.laminas.dev/migration/)). +- The diactoros factories of `php-http/message` will return objects from the `Laminas\Diactoros\` namespace, if + the respective classes are available via autoloading, but continue to return objects from `Zend\Diactoros\` + namespace otherwise. + +- Allow to specify the hashing algorithm for WSSE authentication. + +## [1.10.0] - 2020-11-11 + +- Added support for PHP 8.0. + +## [1.9.1] - 2020-10-13 + +- Improved detection of binary stream to not consider newlines, carriage return or tabs as binary. + +## [1.9.0] - 2020-08-17 + +- Omitted binary body in FullHttpMessageFormatter and CurlCommandFormatter. + `[binary stream omitted]` will be shown instead. + +### Added + +- New Header authentication method for arbitrary header authentication. + +## [1.8.0] - 2019-08-05 + +### Changed + +- Raised minimum PHP version to 7.1 + +### Fixed + +- Fatal error on `CurlCommandFormatter` when body is larger than `escapeshellarg` allowed length. +- Do not read stream in message formatter if stream is not seekable. + +## [1.7.2] - 2018-10-30 + +### Fixed + +- FilteredStream uses `@trigger_error` instead of throwing exceptions to not + break careless users. You still need to fix your stream code to respect + `isSeekable`. Seeking does not work as expected, and we will add exceptions + in version 2. + +## [1.7.1] - 2018-10-29 + +### Fixed + +- FilteredStream is not actually seekable + + +## [1.7.0] - 2018-08-15 + +### Fixed + +- Fix CurlCommandFormatter for binary request payloads +- Fix QueryParam authentication to assemble proper URL regardless of PHP `arg_separator.output` directive +- Do not pass `null` parameters to `Clue\StreamFilter\fun` + +### Changed + +- Dropped tests on HHVM + + +## [1.6.0] - 2017-07-05 + +### Added + +- CookieUtil::parseDate to create a date from cookie date string + +### Fixed + +- Fix curl command of CurlFormatter when there is an user-agent header + + +## [1.5.0] - 2017-02-14 + +### Added + +- Check for empty string in Stream factories +- Cookie::createWithoutValidation Static constructor to create a cookie. Will not perform any attribute validation during instantiation. +- Cookie::isValid Method to check if cookie attributes are valid. + +### Fixed + +- FilteredStream::getSize returns null because the contents size is unknown. +- Stream factories does not rewinds streams. The previous behavior was not coherent between factories and inputs. + +### Deprecated + +- FilteredStream::getReadFilter The read filter is internal and should never be used by consuming code. +- FilteredStream::getWriteFilter We did not implement writing to the streams at all. And if we do, the filter is an internal information and should not be used by consuming code. + + +## [1.4.1] - 2016-12-16 + +### Fixed + +- Cookie::matchPath Cookie with root path (`/`) will not match sub path (e.g. `/cookie`). + + +## [1.4.0] - 2016-10-20 + +### Added + +- Message, stream and URI factories for [Slim Framework](https://github.com/slimphp/Slim) +- BufferedStream that allow you to decorate a non-seekable stream with a seekable one. +- cUrlFormatter to be able to redo the request with a cURL command + + +## [1.3.1] - 2016-07-15 + +### Fixed + +- FullHttpMessageFormatter will not read from streams that you cannot rewind (non-seekable) +- FullHttpMessageFormatter will not read from the stream if $maxBodyLength is zero +- FullHttpMessageFormatter rewinds streams after they are read + + +## [1.3.0] - 2016-07-14 + +### Added + +- FullHttpMessageFormatter to include headers and body in the formatted message + +### Fixed + +- #41: Response builder broke header value + + +## [1.2.0] - 2016-03-29 + +### Added + +- The RequestMatcher is built after the Symfony RequestMatcher and separates + scheme, host and path expressions and provides an option to filter on the + method +- New RequestConditional authentication method using request matchers +- Add automatic basic auth info detection based on the URL + +### Changed + +- Improved ResponseBuilder + +### Deprecated + +- RegexRequestMatcher, use RequestMatcher instead +- Matching authenitcation method, use RequestConditional instead + + +## [1.1.0] - 2016-02-25 + +### Added + + - Add a request matcher interface and regex implementation + - Add a callback request matcher implementation + - Add a ResponseBuilder, to create PSR7 Response from a string + +### Fixed + + - Fix casting string on a FilteredStream not filtering the output + + +## [1.0.0] - 2016-01-27 + + +## [0.2.0] - 2015-12-29 + +### Added + +- Autoregistration of stream filters using Composer autoload +- Cookie +- [Apigen](http://www.apigen.org/) configuration + + +## [0.1.2] - 2015-12-26 + +### Added + +- Request and response factory bindings + +### Fixed + +- Chunk filter namespace in Dechunk stream + + +## [0.1.1] - 2015-12-25 + +### Added + +- Formatter + + +## 0.1.0 - 2015-12-24 + +### Added + +- Authentication +- Encoding +- Message decorator +- Message factory (Guzzle, Diactoros) + + +[Unreleased]: https://github.com/php-http/message/compare/1.10.0...HEAD +[1.10.0]: https://github.com/php-http/message/compare/1.9.1...1.10.0 +[1.9.1]: https://github.com/php-http/message/compare/1.9.0...1.9.1 +[1.9.0]: https://github.com/php-http/message/compare/1.8.0...1.9.0 +[1.8.0]: https://github.com/php-http/message/compare/1.7.2...1.8.0 +[1.7.2]: https://github.com/php-http/message/compare/v1.7.1...1.7.2 +[1.7.1]: https://github.com/php-http/message/compare/1.7.0...v1.7.1 +[1.7.0]: https://github.com/php-http/message/compare/1.6.0...1.7.0 +[1.6.0]: https://github.com/php-http/message/compare/1.5.0...1.6.0 +[1.5.0]: https://github.com/php-http/message/compare/v1.4.1...1.5.0 +[1.4.1]: https://github.com/php-http/message/compare/v1.4.0...v1.4.1 +[1.4.0]: https://github.com/php-http/message/compare/v1.3.1...v1.4.0 +[1.3.1]: https://github.com/php-http/message/compare/v1.3.0...v1.3.1 +[1.3.0]: https://github.com/php-http/message/compare/v1.2.0...v1.3.0 +[1.2.0]: https://github.com/php-http/message/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.com/php-http/message/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/php-http/message/compare/0.2.0...v1.0.0 +[0.2.0]: https://github.com/php-http/message/compare/v0.1.2...0.2.0 +[0.1.2]: https://github.com/php-http/message/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/php-http/message/compare/v0.1.0...v0.1.1 diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/LICENSE new file mode 100644 index 0000000..4558d6f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2016 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/README.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/README.md new file mode 100644 index 0000000..df1a7d5 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/README.md @@ -0,0 +1,51 @@ +# HTTP Message + +[![Latest Version](https://img.shields.io/github/release/php-http/message.svg?style=flat-square)](https://github.com/php-http/message/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/php-http/message/CI?style=flat-square)](https://github.com/php-http/message/actions?query=workflow%3ACI+branch%3Amaster) +[![Total Downloads](https://img.shields.io/packagist/dt/php-http/message.svg?style=flat-square)](https://packagist.org/packages/php-http/message) + +**HTTP Message related tools.** + + +## Install + +Via Composer + +``` bash +$ composer require php-http/message +``` + + +## Intro + +This package contains various PSR-7 tools which might be useful in an HTTP workflow: + +- Authentication method implementations +- Various Stream encoding tools +- Message decorators +- Message factory implementations for Guzzle PSR-7 and Diactoros +- Cookie implementation +- Request matchers + + +## Documentation + +Please see the [official documentation](http://docs.php-http.org/en/latest/message.html). + + +## Testing + +``` bash +$ composer test +``` + + +## Credits + +Thanks to [Cuzzle](https://github.com/namshi/cuzzle) for inpiration for the `CurlCommandFormatter`. + + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/apigen.neon b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/apigen.neon new file mode 100644 index 0000000..0ba1a18 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/apigen.neon @@ -0,0 +1,6 @@ +source: + - src/ + +destination: build/api/ + +templateTheme: bootstrap diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/composer.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/composer.json new file mode 100644 index 0000000..fc67cd0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/composer.json @@ -0,0 +1,65 @@ +{ + "name": "php-http/message", + "description": "HTTP Message related tools", + "keywords": [ + "message", + "http", + "psr-7" + ], + "homepage": "http://php-http.org", + "license": "MIT", + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "require": { + "php": "^7.1 || ^8.0", + "clue/stream-filter": "^1.5", + "php-http/message-factory": "^1.0.2", + "psr/http-message": "^1.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "ergebnis/composer-normalize": "^2.6", + "guzzlehttp/psr7": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.3", + "slim/slim": "^3.0", + "laminas/laminas-diactoros": "^2.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "config": { + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + }, + "files": [ + "src/filters.php" + ] + }, + "autoload-dev": { + "psr-4": { + "spec\\Http\\Message\\": "spec/" + } + }, + "scripts": { + "test": "vendor/bin/phpspec run", + "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/puli.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/puli.json new file mode 100644 index 0000000..024a85d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/puli.json @@ -0,0 +1,111 @@ +{ + "version": "1.0", + "name": "php-http/message", + "bindings": { + "064d003d-78a1-48c4-8f3b-1f92ff25da69": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory", + "type": "Http\\Message\\MessageFactory", + "parameters": { + "depends": "Zend\\Diactoros\\Request" + } + }, + "0836751e-6558-4d1b-8993-4a52012947c3": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", + "type": "Http\\Message\\ResponseFactory" + }, + "1d127622-dc61-4bfa-b9da-d221548d72c3": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", + "type": "Http\\Message\\RequestFactory" + }, + "2438c2d0-0658-441f-8855-ddaf0f87d54d": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory", + "type": "Http\\Message\\MessageFactory", + "parameters": { + "depends": "GuzzleHttp\\Psr7\\Request" + } + }, + "253aa08c-d705-46e7-b1d2-e28c97eef792": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory", + "type": "Http\\Message\\RequestFactory", + "parameters": { + "depends": "GuzzleHttp\\Psr7\\Request" + } + }, + "273a34f9-62f4-4ba1-9801-b1284d49ff89": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\StreamFactory\\GuzzleStreamFactory", + "type": "Http\\Message\\StreamFactory", + "parameters": { + "depends": "GuzzleHttp\\Psr7\\Stream" + } + }, + "304b83db-b594-4d83-ae75-1f633adf92f7": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\UriFactory\\GuzzleUriFactory", + "type": "Http\\Message\\UriFactory", + "parameters": { + "depends": "GuzzleHttp\\Psr7\\Uri" + } + }, + "3f4bc1cd-aa95-4702-9fa7-65408e471691": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\UriFactory\\DiactorosUriFactory", + "type": "Http\\Message\\UriFactory", + "parameters": { + "depends": "Zend\\Diactoros\\Uri" + } + }, + "4672a6ee-ad9e-4109-a5d1-b7d46f26c7a1": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", + "type": "Http\\Message\\MessageFactory" + }, + "6234e947-d3bd-43eb-97d5-7f9e22e6bb1b": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory", + "type": "Http\\Message\\ResponseFactory", + "parameters": { + "depends": "Zend\\Diactoros\\Response" + } + }, + "6a9ad6ce-d82c-470f-8e30-60f21d9d95bf": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\UriFactory\\SlimUriFactory", + "type": "Http\\Message\\UriFactory" + }, + "72c2afa0-ea56-4d03-adb6-a9f241a8a734": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\StreamFactory\\SlimStreamFactory", + "type": "Http\\Message\\StreamFactory" + }, + "95c1be8f-39fe-4abd-8351-92cb14379a75": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\StreamFactory\\DiactorosStreamFactory", + "type": "Http\\Message\\StreamFactory", + "parameters": { + "depends": "Zend\\Diactoros\\Stream" + } + }, + "a018af27-7590-4dcf-83a1-497f95604cd6": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory", + "type": "Http\\Message\\ResponseFactory", + "parameters": { + "depends": "GuzzleHttp\\Psr7\\Response" + } + }, + "c07955b1-de46-43db-923b-d07fae9382cb": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory", + "type": "Http\\Message\\RequestFactory", + "parameters": { + "depends": "Zend\\Diactoros\\Request" + } + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication.php new file mode 100644 index 0000000..0fe0cb3 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication.php @@ -0,0 +1,25 @@ + + */ +interface Authentication +{ + /** + * Alter the request to add the authentication credentials. + * + * To do that, the implementation might use pre-stored credentials or do + * separate HTTP requests to obtain a valid token. + * + * @param RequestInterface $request The request without authentication information + * + * @return RequestInterface The request with added authentication information + */ + public function authenticate(RequestInterface $request); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/AutoBasicAuth.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/AutoBasicAuth.php new file mode 100644 index 0000000..7b6a429 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/AutoBasicAuth.php @@ -0,0 +1,48 @@ + + */ +final class AutoBasicAuth implements Authentication +{ + /** + * Whether user info should be removed from the URI. + * + * @var bool + */ + private $shouldRemoveUserInfo; + + /** + * @param bool|true $shouldRremoveUserInfo + */ + public function __construct($shouldRremoveUserInfo = true) + { + $this->shouldRemoveUserInfo = (bool) $shouldRremoveUserInfo; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + $uri = $request->getUri(); + $userInfo = $uri->getUserInfo(); + + if (!empty($userInfo)) { + if ($this->shouldRemoveUserInfo) { + $request = $request->withUri($uri->withUserInfo('')); + } + + $request = $request->withHeader('Authorization', sprintf('Basic %s', base64_encode($userInfo))); + } + + return $request; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/BasicAuth.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/BasicAuth.php new file mode 100644 index 0000000..23618a5 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/BasicAuth.php @@ -0,0 +1,44 @@ + + */ +final class BasicAuth implements Authentication +{ + /** + * @var string + */ + private $username; + + /** + * @var string + */ + private $password; + + /** + * @param string $username + * @param string $password + */ + public function __construct($username, $password) + { + $this->username = $username; + $this->password = $password; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + $header = sprintf('Basic %s', base64_encode(sprintf('%s:%s', $this->username, $this->password))); + + return $request->withHeader('Authorization', $header); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Bearer.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Bearer.php new file mode 100644 index 0000000..a8fb21a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Bearer.php @@ -0,0 +1,37 @@ + + */ +final class Bearer implements Authentication +{ + /** + * @var string + */ + private $token; + + /** + * @param string $token + */ + public function __construct($token) + { + $this->token = $token; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + $header = sprintf('Bearer %s', $this->token); + + return $request->withHeader('Authorization', $header); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Chain.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Chain.php new file mode 100644 index 0000000..71002bb --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Chain.php @@ -0,0 +1,47 @@ + + */ +final class Chain implements Authentication +{ + /** + * @var Authentication[] + */ + private $authenticationChain = []; + + /** + * @param Authentication[] $authenticationChain + */ + public function __construct(array $authenticationChain = []) + { + foreach ($authenticationChain as $authentication) { + if (!$authentication instanceof Authentication) { + throw new \InvalidArgumentException( + 'Members of the authentication chain must be of type Http\Message\Authentication' + ); + } + } + + $this->authenticationChain = $authenticationChain; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + foreach ($this->authenticationChain as $authentication) { + $request = $authentication->authenticate($request); + } + + return $request; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Header.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Header.php new file mode 100644 index 0000000..97a04dd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Header.php @@ -0,0 +1,33 @@ +name = $name; + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + return $request->withHeader($this->name, $this->value); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Matching.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Matching.php new file mode 100644 index 0000000..7a5c247 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Matching.php @@ -0,0 +1,69 @@ + + * + * @deprecated since since version 1.2, and will be removed in 2.0. Use {@link RequestConditional} instead. + */ +final class Matching implements Authentication +{ + /** + * @var Authentication + */ + private $authentication; + + /** + * @var CallbackRequestMatcher + */ + private $matcher; + + public function __construct(Authentication $authentication, callable $matcher = null) + { + if (is_null($matcher)) { + $matcher = function () { + return true; + }; + } + + $this->authentication = $authentication; + $this->matcher = new CallbackRequestMatcher($matcher); + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + if ($this->matcher->matches($request)) { + return $this->authentication->authenticate($request); + } + + return $request; + } + + /** + * Creates a matching authentication for an URL. + * + * @param string $url + * + * @return self + */ + public static function createUrlMatcher(Authentication $authentication, $url) + { + $matcher = function (RequestInterface $request) use ($url) { + return preg_match($url, $request->getRequestTarget()); + }; + + return new static($authentication, $matcher); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/QueryParam.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/QueryParam.php new file mode 100644 index 0000000..7f95d44 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/QueryParam.php @@ -0,0 +1,47 @@ + + */ +final class QueryParam implements Authentication +{ + /** + * @var array + */ + private $params = []; + + public function __construct(array $params) + { + $this->params = $params; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + $uri = $request->getUri(); + $query = $uri->getQuery(); + $params = []; + + parse_str($query, $params); + + $params = array_merge($params, $this->params); + + $query = http_build_query($params, null, '&'); + + $uri = $uri->withQuery($query); + + return $request->withUri($uri); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/RequestConditional.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/RequestConditional.php new file mode 100644 index 0000000..01062cf --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/RequestConditional.php @@ -0,0 +1,43 @@ + + */ +final class RequestConditional implements Authentication +{ + /** + * @var RequestMatcher + */ + private $requestMatcher; + + /** + * @var Authentication + */ + private $authentication; + + public function __construct(RequestMatcher $requestMatcher, Authentication $authentication) + { + $this->requestMatcher = $requestMatcher; + $this->authentication = $authentication; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + if ($this->requestMatcher->matches($request)) { + return $this->authentication->authenticate($request); + } + + return $request; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Wsse.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Wsse.php new file mode 100644 index 0000000..f343633 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Authentication/Wsse.php @@ -0,0 +1,68 @@ + + */ +final class Wsse implements Authentication +{ + /** + * @var string + */ + private $username; + + /** + * @var string + */ + private $password; + + /** + * @var string + */ + private $hashAlgorithm; + + /** + * @param string $username + * @param string $password + * @param string $hashAlgorithm To use a better hashing algorithm than the weak sha1, pass the algorithm to use, e.g. "sha512" + */ + public function __construct($username, $password, $hashAlgorithm = 'sha1') + { + $this->username = $username; + $this->password = $password; + if (false === in_array($hashAlgorithm, hash_algos())) { + throw new InvalidArgumentException(sprintf('Unaccepted hashing algorithm: %s', $hashAlgorithm)); + } + $this->hashAlgorithm = $hashAlgorithm; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestInterface $request) + { + $nonce = substr(md5(uniqid(uniqid().'_', true)), 0, 16); + $created = date('c'); + $digest = base64_encode(hash($this->hashAlgorithm, base64_decode($nonce).$created.$this->password, true)); + + $wsse = sprintf( + 'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"', + $this->username, + $digest, + $nonce, + $created + ); + + return $request + ->withHeader('Authorization', 'WSSE profile="UsernameToken"') + ->withHeader('X-WSSE', $wsse) + ; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Builder/ResponseBuilder.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Builder/ResponseBuilder.php new file mode 100644 index 0000000..4c3ecfc --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Builder/ResponseBuilder.php @@ -0,0 +1,146 @@ +response = $response; + } + + /** + * Return response. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } + + /** + * Add headers represented by an array of header lines. + * + * @param string[] $headers response headers as array of header lines + * + * @return $this + * + * @throws \UnexpectedValueException for invalid header values + * @throws \InvalidArgumentException for invalid status code arguments + */ + public function setHeadersFromArray(array $headers) + { + $status = array_shift($headers); + $this->setStatus($status); + + foreach ($headers as $headerLine) { + $headerLine = trim($headerLine); + if ('' === $headerLine) { + continue; + } + + $this->addHeader($headerLine); + } + + return $this; + } + + /** + * Add headers represented by a single string. + * + * @param string $headers response headers as single string + * + * @return $this + * + * @throws \InvalidArgumentException if $headers is not a string on object with __toString() + * @throws \UnexpectedValueException for invalid header values + */ + public function setHeadersFromString($headers) + { + if (!(is_string($headers) + || (is_object($headers) && method_exists($headers, '__toString'))) + ) { + throw new \InvalidArgumentException( + sprintf( + '%s expects parameter 1 to be a string, %s given', + __METHOD__, + is_object($headers) ? get_class($headers) : gettype($headers) + ) + ); + } + + $this->setHeadersFromArray(explode("\r\n", $headers)); + + return $this; + } + + /** + * Set response status from a status string. + * + * @param string $statusLine response status as a string + * + * @return $this + * + * @throws \InvalidArgumentException for invalid status line + */ + public function setStatus($statusLine) + { + $parts = explode(' ', $statusLine, 3); + if (count($parts) < 2 || 0 !== strpos(strtolower($parts[0]), 'http/')) { + throw new \InvalidArgumentException( + sprintf('"%s" is not a valid HTTP status line', $statusLine) + ); + } + + $reasonPhrase = count($parts) > 2 ? $parts[2] : ''; + $this->response = $this->response + ->withStatus((int) $parts[1], $reasonPhrase) + ->withProtocolVersion(substr($parts[0], 5)); + + return $this; + } + + /** + * Add header represented by a string. + * + * @param string $headerLine response header as a string + * + * @return $this + * + * @throws \InvalidArgumentException for invalid header names or values + */ + public function addHeader($headerLine) + { + $parts = explode(':', $headerLine, 2); + if (2 !== count($parts)) { + throw new \InvalidArgumentException( + sprintf('"%s" is not a valid HTTP header line', $headerLine) + ); + } + $name = trim($parts[0]); + $value = trim($parts[1]); + if ($this->response->hasHeader($name)) { + $this->response = $this->response->withAddedHeader($name, $value); + } else { + $this->response = $this->response->withHeader($name, $value); + } + + return $this; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Cookie.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Cookie.php new file mode 100644 index 0000000..0cc2d43 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Cookie.php @@ -0,0 +1,524 @@ + + * + * @see http://tools.ietf.org/search/rfc6265 + */ +final class Cookie +{ + /** + * @var string + */ + private $name; + + /** + * @var string|null + */ + private $value; + + /** + * @var int|null + */ + private $maxAge; + + /** + * @var string|null + */ + private $domain; + + /** + * @var string + */ + private $path; + + /** + * @var bool + */ + private $secure; + + /** + * @var bool + */ + private $httpOnly; + + /** + * Expires attribute is HTTP 1.0 only and should be avoided. + * + * @var \DateTime|null + */ + private $expires; + + /** + * @param string $name + * @param string|null $value + * @param int|null $maxAge + * @param string|null $domain + * @param string|null $path + * @param bool $secure + * @param bool $httpOnly + * @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided. + * + * @throws \InvalidArgumentException if name, value or max age is not valid + */ + public function __construct( + $name, + $value = null, + $maxAge = null, + $domain = null, + $path = null, + $secure = false, + $httpOnly = false, + \DateTime $expires = null + ) { + $this->validateName($name); + $this->validateValue($value); + $this->validateMaxAge($maxAge); + + $this->name = $name; + $this->value = $value; + $this->maxAge = $maxAge; + $this->expires = $expires; + $this->domain = $this->normalizeDomain($domain); + $this->path = $this->normalizePath($path); + $this->secure = (bool) $secure; + $this->httpOnly = (bool) $httpOnly; + } + + /** + * Creates a new cookie without any attribute validation. + * + * @param string $name + * @param string|null $value + * @param int $maxAge + * @param string|null $domain + * @param string|null $path + * @param bool $secure + * @param bool $httpOnly + * @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided. + */ + public static function createWithoutValidation( + $name, + $value = null, + $maxAge = null, + $domain = null, + $path = null, + $secure = false, + $httpOnly = false, + \DateTime $expires = null + ) { + $cookie = new self('name', null, null, $domain, $path, $secure, $httpOnly, $expires); + $cookie->name = $name; + $cookie->value = $value; + $cookie->maxAge = $maxAge; + + return $cookie; + } + + /** + * Returns the name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the value. + * + * @return string|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Checks if there is a value. + * + * @return bool + */ + public function hasValue() + { + return isset($this->value); + } + + /** + * Sets the value. + * + * @param string|null $value + * + * @return Cookie + */ + public function withValue($value) + { + $this->validateValue($value); + + $new = clone $this; + $new->value = $value; + + return $new; + } + + /** + * Returns the max age. + * + * @return int|null + */ + public function getMaxAge() + { + return $this->maxAge; + } + + /** + * Checks if there is a max age. + * + * @return bool + */ + public function hasMaxAge() + { + return isset($this->maxAge); + } + + /** + * Sets the max age. + * + * @param int|null $maxAge + * + * @return Cookie + */ + public function withMaxAge($maxAge) + { + $this->validateMaxAge($maxAge); + + $new = clone $this; + $new->maxAge = $maxAge; + + return $new; + } + + /** + * Returns the expiration time. + * + * @return \DateTime|null + */ + public function getExpires() + { + return $this->expires; + } + + /** + * Checks if there is an expiration time. + * + * @return bool + */ + public function hasExpires() + { + return isset($this->expires); + } + + /** + * Sets the expires. + * + * @return Cookie + */ + public function withExpires(\DateTime $expires = null) + { + $new = clone $this; + $new->expires = $expires; + + return $new; + } + + /** + * Checks if the cookie is expired. + * + * @return bool + */ + public function isExpired() + { + return isset($this->expires) and $this->expires < new \DateTime(); + } + + /** + * Returns the domain. + * + * @return string|null + */ + public function getDomain() + { + return $this->domain; + } + + /** + * Checks if there is a domain. + * + * @return bool + */ + public function hasDomain() + { + return isset($this->domain); + } + + /** + * Sets the domain. + * + * @param string|null $domain + * + * @return Cookie + */ + public function withDomain($domain) + { + $new = clone $this; + $new->domain = $this->normalizeDomain($domain); + + return $new; + } + + /** + * Checks whether this cookie is meant for this domain. + * + * @see http://tools.ietf.org/html/rfc6265#section-5.1.3 + * + * @param string $domain + * + * @return bool + */ + public function matchDomain($domain) + { + // Domain is not set or exact match + if (!$this->hasDomain() || 0 === strcasecmp($domain, $this->domain)) { + return true; + } + + // Domain is not an IP address + if (filter_var($domain, FILTER_VALIDATE_IP)) { + return false; + } + + return (bool) preg_match(sprintf('/\b%s$/i', preg_quote($this->domain)), $domain); + } + + /** + * Returns the path. + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Sets the path. + * + * @param string|null $path + * + * @return Cookie + */ + public function withPath($path) + { + $new = clone $this; + $new->path = $this->normalizePath($path); + + return $new; + } + + /** + * Checks whether this cookie is meant for this path. + * + * @see http://tools.ietf.org/html/rfc6265#section-5.1.4 + * + * @param string $path + * + * @return bool + */ + public function matchPath($path) + { + return $this->path === $path || (0 === strpos($path, rtrim($this->path, '/').'/')); + } + + /** + * Checks whether this cookie may only be sent over HTTPS. + * + * @return bool + */ + public function isSecure() + { + return $this->secure; + } + + /** + * Sets whether this cookie should only be sent over HTTPS. + * + * @param bool $secure + * + * @return Cookie + */ + public function withSecure($secure) + { + $new = clone $this; + $new->secure = (bool) $secure; + + return $new; + } + + /** + * Check whether this cookie may not be accessed through Javascript. + * + * @return bool + */ + public function isHttpOnly() + { + return $this->httpOnly; + } + + /** + * Sets whether this cookie may not be accessed through Javascript. + * + * @param bool $httpOnly + * + * @return Cookie + */ + public function withHttpOnly($httpOnly) + { + $new = clone $this; + $new->httpOnly = (bool) $httpOnly; + + return $new; + } + + /** + * Checks if this cookie represents the same cookie as $cookie. + * + * This does not compare the values, only name, domain and path. + * + * @param Cookie $cookie + * + * @return bool + */ + public function match(self $cookie) + { + return $this->name === $cookie->name && $this->domain === $cookie->domain and $this->path === $cookie->path; + } + + /** + * Validates cookie attributes. + * + * @return bool + */ + public function isValid() + { + try { + $this->validateName($this->name); + $this->validateValue($this->value); + $this->validateMaxAge($this->maxAge); + } catch (\InvalidArgumentException $e) { + return false; + } + + return true; + } + + /** + * Validates the name attribute. + * + * @see http://tools.ietf.org/search/rfc2616#section-2.2 + * + * @param string $name + * + * @throws \InvalidArgumentException if the name is empty or contains invalid characters + */ + private function validateName($name) + { + if (strlen($name) < 1) { + throw new \InvalidArgumentException('The name cannot be empty'); + } + + // Name attribute is a token as per spec in RFC 2616 + if (preg_match('/[\x00-\x20\x22\x28-\x29\x2C\x2F\x3A-\x40\x5B-\x5D\x7B\x7D\x7F]/', $name)) { + throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); + } + } + + /** + * Validates a value. + * + * @see http://tools.ietf.org/html/rfc6265#section-4.1.1 + * + * @param string|null $value + * + * @throws \InvalidArgumentException if the value contains invalid characters + */ + private function validateValue($value) + { + if (isset($value)) { + if (preg_match('/[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/', $value)) { + throw new \InvalidArgumentException(sprintf('The cookie value "%s" contains invalid characters.', $value)); + } + } + } + + /** + * Validates a Max-Age attribute. + * + * @param int|null $maxAge + * + * @throws \InvalidArgumentException if the Max-Age is not an empty or integer value + */ + private function validateMaxAge($maxAge) + { + if (isset($maxAge)) { + if (!is_int($maxAge)) { + throw new \InvalidArgumentException('Max-Age must be integer'); + } + } + } + + /** + * Remove the leading '.' and lowercase the domain as per spec in RFC 6265. + * + * @see http://tools.ietf.org/html/rfc6265#section-4.1.2.3 + * @see http://tools.ietf.org/html/rfc6265#section-5.1.3 + * @see http://tools.ietf.org/html/rfc6265#section-5.2.3 + * + * @param string|null $domain + * + * @return string + */ + private function normalizeDomain($domain) + { + if (isset($domain)) { + $domain = ltrim(strtolower($domain), '.'); + } + + return $domain; + } + + /** + * Processes path as per spec in RFC 6265. + * + * @see http://tools.ietf.org/html/rfc6265#section-5.1.4 + * @see http://tools.ietf.org/html/rfc6265#section-5.2.4 + * + * @param string|null $path + * + * @return string + */ + private function normalizePath($path) + { + $path = rtrim($path, '/'); + + if (empty($path) or '/' !== substr($path, 0, 1)) { + $path = '/'; + } + + return $path; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/CookieJar.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/CookieJar.php new file mode 100644 index 0000000..1479cc6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/CookieJar.php @@ -0,0 +1,210 @@ + + */ +final class CookieJar implements \Countable, \IteratorAggregate +{ + /** + * @var \SplObjectStorage + */ + private $cookies; + + public function __construct() + { + $this->cookies = new \SplObjectStorage(); + } + + /** + * Checks if there is a cookie. + * + * @return bool + */ + public function hasCookie(Cookie $cookie) + { + return $this->cookies->contains($cookie); + } + + /** + * Adds a cookie. + */ + public function addCookie(Cookie $cookie) + { + if (!$this->hasCookie($cookie)) { + $cookies = $this->getMatchingCookies($cookie); + + foreach ($cookies as $matchingCookie) { + if ($cookie->getValue() !== $matchingCookie->getValue() || $cookie->getMaxAge() > $matchingCookie->getMaxAge()) { + $this->removeCookie($matchingCookie); + + continue; + } + } + + if ($cookie->hasValue()) { + $this->cookies->attach($cookie); + } + } + } + + /** + * Removes a cookie. + */ + public function removeCookie(Cookie $cookie) + { + $this->cookies->detach($cookie); + } + + /** + * Returns the cookies. + * + * @return Cookie[] + */ + public function getCookies() + { + $match = function ($matchCookie) { + return true; + }; + + return $this->findMatchingCookies($match); + } + + /** + * Returns all matching cookies. + * + * @return Cookie[] + */ + public function getMatchingCookies(Cookie $cookie) + { + $match = function ($matchCookie) use ($cookie) { + return $matchCookie->match($cookie); + }; + + return $this->findMatchingCookies($match); + } + + /** + * Finds matching cookies based on a callable. + * + * @return Cookie[] + */ + private function findMatchingCookies(callable $match) + { + $cookies = []; + + foreach ($this->cookies as $cookie) { + if ($match($cookie)) { + $cookies[] = $cookie; + } + } + + return $cookies; + } + + /** + * Checks if there are cookies. + * + * @return bool + */ + public function hasCookies() + { + return $this->cookies->count() > 0; + } + + /** + * Sets the cookies and removes any previous one. + * + * @param Cookie[] $cookies + */ + public function setCookies(array $cookies) + { + $this->clear(); + $this->addCookies($cookies); + } + + /** + * Adds some cookies. + * + * @param Cookie[] $cookies + */ + public function addCookies(array $cookies) + { + foreach ($cookies as $cookie) { + $this->addCookie($cookie); + } + } + + /** + * Removes some cookies. + * + * @param Cookie[] $cookies + */ + public function removeCookies(array $cookies) + { + foreach ($cookies as $cookie) { + $this->removeCookie($cookie); + } + } + + /** + * Removes cookies which match the given parameters. + * + * Null means that parameter should not be matched + * + * @param string|null $name + * @param string|null $domain + * @param string|null $path + */ + public function removeMatchingCookies($name = null, $domain = null, $path = null) + { + $match = function ($cookie) use ($name, $domain, $path) { + $match = true; + + if (isset($name)) { + $match = $match && ($cookie->getName() === $name); + } + + if (isset($domain)) { + $match = $match && $cookie->matchDomain($domain); + } + + if (isset($path)) { + $match = $match && $cookie->matchPath($path); + } + + return $match; + }; + + $cookies = $this->findMatchingCookies($match); + + $this->removeCookies($cookies); + } + + /** + * Removes all cookies. + */ + public function clear() + { + $this->cookies = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function count() + { + return $this->cookies->count(); + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + return clone $this->cookies; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/CookieUtil.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/CookieUtil.php new file mode 100644 index 0000000..44c5314 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/CookieUtil.php @@ -0,0 +1,53 @@ + + */ +trait MessageDecorator +{ + /** + * @var MessageInterface + */ + private $message; + + /** + * Returns the decorated message. + * + * Since the underlying Message is immutable as well + * exposing it is not an issue, because it's state cannot be altered + * + * @return MessageInterface + */ + public function getMessage() + { + return $this->message; + } + + /** + * {@inheritdoc} + */ + public function getProtocolVersion() + { + return $this->message->getProtocolVersion(); + } + + /** + * {@inheritdoc} + */ + public function withProtocolVersion($version) + { + $new = clone $this; + $new->message = $this->message->withProtocolVersion($version); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getHeaders() + { + return $this->message->getHeaders(); + } + + /** + * {@inheritdoc} + */ + public function hasHeader($header) + { + return $this->message->hasHeader($header); + } + + /** + * {@inheritdoc} + */ + public function getHeader($header) + { + return $this->message->getHeader($header); + } + + /** + * {@inheritdoc} + */ + public function getHeaderLine($header) + { + return $this->message->getHeaderLine($header); + } + + /** + * {@inheritdoc} + */ + public function withHeader($header, $value) + { + $new = clone $this; + $new->message = $this->message->withHeader($header, $value); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withAddedHeader($header, $value) + { + $new = clone $this; + $new->message = $this->message->withAddedHeader($header, $value); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withoutHeader($header) + { + $new = clone $this; + $new->message = $this->message->withoutHeader($header); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getBody() + { + return $this->message->getBody(); + } + + /** + * {@inheritdoc} + */ + public function withBody(StreamInterface $body) + { + $new = clone $this; + $new->message = $this->message->withBody($body); + + return $new; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/RequestDecorator.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/RequestDecorator.php new file mode 100644 index 0000000..bd254a8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/RequestDecorator.php @@ -0,0 +1,86 @@ + + */ +trait RequestDecorator +{ + use MessageDecorator { + getMessage as getRequest; + } + + /** + * Exchanges the underlying request with another. + * + * @return self + */ + public function withRequest(RequestInterface $request) + { + $new = clone $this; + $new->message = $request; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getRequestTarget() + { + return $this->message->getRequestTarget(); + } + + /** + * {@inheritdoc} + */ + public function withRequestTarget($requestTarget) + { + $new = clone $this; + $new->message = $this->message->withRequestTarget($requestTarget); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getMethod() + { + return $this->message->getMethod(); + } + + /** + * {@inheritdoc} + */ + public function withMethod($method) + { + $new = clone $this; + $new->message = $this->message->withMethod($method); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getUri() + { + return $this->message->getUri(); + } + + /** + * {@inheritdoc} + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + $new = clone $this; + $new->message = $this->message->withUri($uri, $preserveHost); + + return $new; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/ResponseDecorator.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/ResponseDecorator.php new file mode 100644 index 0000000..20319ed --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/ResponseDecorator.php @@ -0,0 +1,55 @@ + + */ +trait ResponseDecorator +{ + use MessageDecorator { + getMessage as getResponse; + } + + /** + * Exchanges the underlying response with another. + * + * @return self + */ + public function withResponse(ResponseInterface $response) + { + $new = clone $this; + $new->message = $response; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getStatusCode() + { + return $this->message->getStatusCode(); + } + + /** + * {@inheritdoc} + */ + public function withStatus($code, $reasonPhrase = '') + { + $new = clone $this; + $new->message = $this->message->withStatus($code, $reasonPhrase); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getReasonPhrase() + { + return $this->message->getReasonPhrase(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/StreamDecorator.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/StreamDecorator.php new file mode 100644 index 0000000..f405c7a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Decorator/StreamDecorator.php @@ -0,0 +1,138 @@ + + */ +trait StreamDecorator +{ + /** + * @var StreamInterface + */ + protected $stream; + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->stream->__toString(); + } + + /** + * {@inheritdoc} + */ + public function close() + { + $this->stream->close(); + } + + /** + * {@inheritdoc} + */ + public function detach() + { + return $this->stream->detach(); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return $this->stream->getSize(); + } + + /** + * {@inheritdoc} + */ + public function tell() + { + return $this->stream->tell(); + } + + /** + * {@inheritdoc} + */ + public function eof() + { + return $this->stream->eof(); + } + + /** + * {@inheritdoc} + */ + public function isSeekable() + { + return $this->stream->isSeekable(); + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + $this->stream->seek($offset, $whence); + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->stream->rewind(); + } + + /** + * {@inheritdoc} + */ + public function isWritable() + { + return $this->stream->isWritable(); + } + + /** + * {@inheritdoc} + */ + public function write($string) + { + return $this->stream->write($string); + } + + /** + * {@inheritdoc} + */ + public function isReadable() + { + return $this->stream->isReadable(); + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + return $this->stream->read($length); + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + return $this->stream->getContents(); + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/ChunkStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/ChunkStream.php new file mode 100644 index 0000000..74c2fbd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/ChunkStream.php @@ -0,0 +1,39 @@ + + */ +class ChunkStream extends FilteredStream +{ + /** + * {@inheritdoc} + */ + protected function readFilter() + { + return 'chunk'; + } + + /** + * {@inheritdoc} + */ + protected function writeFilter() + { + return 'dechunk'; + } + + /** + * {@inheritdoc} + */ + protected function fill() + { + parent::fill(); + + if ($this->stream->eof()) { + $this->buffer .= "0\r\n\r\n"; + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/CompressStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/CompressStream.php new file mode 100644 index 0000000..bdb740a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/CompressStream.php @@ -0,0 +1,45 @@ + + */ +class CompressStream extends FilteredStream +{ + /** + * @param int $level + */ + public function __construct(StreamInterface $stream, $level = -1) + { + if (!extension_loaded('zlib')) { + throw new \RuntimeException('The zlib extension must be enabled to use this stream'); + } + + parent::__construct($stream, ['window' => 15, 'level' => $level]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15]); + } + + /** + * {@inheritdoc} + */ + protected function readFilter() + { + return 'zlib.deflate'; + } + + /** + * {@inheritdoc} + */ + protected function writeFilter() + { + return 'zlib.inflate'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DechunkStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DechunkStream.php new file mode 100644 index 0000000..4cade83 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DechunkStream.php @@ -0,0 +1,29 @@ + + */ +class DechunkStream extends FilteredStream +{ + /** + * {@inheritdoc} + */ + protected function readFilter() + { + return 'dechunk'; + } + + /** + * {@inheritdoc} + */ + protected function writeFilter() + { + return 'chunk'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DecompressStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DecompressStream.php new file mode 100644 index 0000000..ab3a345 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DecompressStream.php @@ -0,0 +1,45 @@ + + */ +class DecompressStream extends FilteredStream +{ + /** + * @param int $level + */ + public function __construct(StreamInterface $stream, $level = -1) + { + if (!extension_loaded('zlib')) { + throw new \RuntimeException('The zlib extension must be enabled to use this stream'); + } + + parent::__construct($stream, ['window' => 15]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15, 'level' => $level]); + } + + /** + * {@inheritdoc} + */ + protected function readFilter() + { + return 'zlib.inflate'; + } + + /** + * {@inheritdoc} + */ + protected function writeFilter() + { + return 'zlib.deflate'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DeflateStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DeflateStream.php new file mode 100644 index 0000000..2ab3e00 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/DeflateStream.php @@ -0,0 +1,41 @@ + + */ +class DeflateStream extends FilteredStream +{ + /** + * @param int $level + */ + public function __construct(StreamInterface $stream, $level = -1) + { + parent::__construct($stream, ['window' => -15, 'level' => $level]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15]); + } + + /** + * {@inheritdoc} + */ + protected function readFilter() + { + return 'zlib.deflate'; + } + + /** + * {@inheritdoc} + */ + protected function writeFilter() + { + return 'zlib.inflate'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/Filter/Chunk.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/Filter/Chunk.php new file mode 100644 index 0000000..0f8f53b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/Filter/Chunk.php @@ -0,0 +1,30 @@ + + */ +class Chunk extends \php_user_filter +{ + /** + * {@inheritdoc} + */ + public function filter($in, $out, &$consumed, $closing) + { + while ($bucket = stream_bucket_make_writeable($in)) { + $lenbucket = stream_bucket_new($this->stream, dechex($bucket->datalen)."\r\n"); + stream_bucket_append($out, $lenbucket); + + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + + $lenbucket = stream_bucket_new($this->stream, "\r\n"); + stream_bucket_append($out, $lenbucket); + } + + return PSFS_PASS_ON; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/FilteredStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/FilteredStream.php new file mode 100644 index 0000000..a937c82 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/FilteredStream.php @@ -0,0 +1,234 @@ + + */ +abstract class FilteredStream implements StreamInterface +{ + use StreamDecorator { + rewind as private doRewind; + seek as private doSeek; + } + const BUFFER_SIZE = 8192; + + /** + * @var callable + */ + protected $readFilterCallback; + + /** + * @var resource + * + * @deprecated since version 1.5, will be removed in 2.0 + */ + protected $readFilter; + + /** + * @var callable + * + * @deprecated since version 1.5, will be removed in 2.0 + */ + protected $writeFilterCallback; + + /** + * @var resource + * + * @deprecated since version 1.5, will be removed in 2.0 + */ + protected $writeFilter; + + /** + * Internal buffer. + * + * @var string + */ + protected $buffer = ''; + + /** + * @param mixed|null $readFilterOptions + * @param mixed|null $writeFilterOptions deprecated since 1.5, will be removed in 2.0 + */ + public function __construct(StreamInterface $stream, $readFilterOptions = null, $writeFilterOptions = null) + { + if (null !== $readFilterOptions) { + $this->readFilterCallback = Filter\fun($this->readFilter(), $readFilterOptions); + } else { + $this->readFilterCallback = Filter\fun($this->readFilter()); + } + + if (null !== $writeFilterOptions) { + $this->writeFilterCallback = Filter\fun($this->writeFilter(), $writeFilterOptions); + + @trigger_error('The $writeFilterOptions argument is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); + } else { + $this->writeFilterCallback = Filter\fun($this->writeFilter()); + } + + $this->stream = $stream; + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + if (strlen($this->buffer) >= $length) { + $read = substr($this->buffer, 0, $length); + $this->buffer = substr($this->buffer, $length); + + return $read; + } + + if ($this->stream->eof()) { + $buffer = $this->buffer; + $this->buffer = ''; + + return $buffer; + } + + $read = $this->buffer; + $this->buffer = ''; + $this->fill(); + + return $read.$this->read($length - strlen($read)); + } + + /** + * {@inheritdoc} + */ + public function eof() + { + return $this->stream->eof() && '' === $this->buffer; + } + + /** + * Buffer is filled by reading underlying stream. + * + * Callback is reading once more even if the stream is ended. + * This allow to get last data in the PHP buffer otherwise this + * bug is present : https://bugs.php.net/bug.php?id=48725 + */ + protected function fill() + { + $readFilterCallback = $this->readFilterCallback; + $this->buffer .= $readFilterCallback($this->stream->read(self::BUFFER_SIZE)); + + if ($this->stream->eof()) { + $this->buffer .= $readFilterCallback(); + } + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + $buffer = ''; + + while (!$this->eof()) { + $buf = $this->read(self::BUFFER_SIZE); + // Using a loose equality here to match on '' and false. + if (null == $buf) { + break; + } + + $buffer .= $buf; + } + + return $buffer; + } + + /** + * Always returns null because we can't tell the size of a stream when we filter. + */ + public function getSize() + { + return null; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->getContents(); + } + + /** + * Filtered streams are not seekable. + * + * We would need to buffer and process everything to allow seeking. + */ + public function isSeekable() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + @trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED); + $this->doRewind(); + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + @trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED); + $this->doSeek($offset, $whence); + } + + /** + * Returns the read filter name. + * + * @return string + * + * @deprecated since version 1.5, will be removed in 2.0 + */ + public function getReadFilter() + { + @trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); + + return $this->readFilter(); + } + + /** + * Returns the write filter name. + * + * @return string + */ + abstract protected function readFilter(); + + /** + * Returns the write filter name. + * + * @return string + * + * @deprecated since version 1.5, will be removed in 2.0 + */ + public function getWriteFilter() + { + @trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); + + return $this->writeFilter(); + } + + /** + * Returns the write filter name. + * + * @return string + */ + abstract protected function writeFilter(); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/GzipDecodeStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/GzipDecodeStream.php new file mode 100644 index 0000000..92b5dad --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/GzipDecodeStream.php @@ -0,0 +1,45 @@ + + */ +class GzipDecodeStream extends FilteredStream +{ + /** + * @param int $level + */ + public function __construct(StreamInterface $stream, $level = -1) + { + if (!extension_loaded('zlib')) { + throw new \RuntimeException('The zlib extension must be enabled to use this stream'); + } + + parent::__construct($stream, ['window' => 31]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31, 'level' => $level]); + } + + /** + * {@inheritdoc} + */ + protected function readFilter() + { + return 'zlib.inflate'; + } + + /** + * {@inheritdoc} + */ + protected function writeFilter() + { + return 'zlib.deflate'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/GzipEncodeStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/GzipEncodeStream.php new file mode 100644 index 0000000..13f097a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/GzipEncodeStream.php @@ -0,0 +1,45 @@ + + */ +class GzipEncodeStream extends FilteredStream +{ + /** + * @param int $level + */ + public function __construct(StreamInterface $stream, $level = -1) + { + if (!extension_loaded('zlib')) { + throw new \RuntimeException('The zlib extension must be enabled to use this stream'); + } + + parent::__construct($stream, ['window' => 31, 'level' => $level]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31]); + } + + /** + * {@inheritdoc} + */ + protected function readFilter() + { + return 'zlib.deflate'; + } + + /** + * {@inheritdoc} + */ + protected function writeFilter() + { + return 'zlib.inflate'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/InflateStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/InflateStream.php new file mode 100644 index 0000000..06c5187 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Encoding/InflateStream.php @@ -0,0 +1,45 @@ + + */ +class InflateStream extends FilteredStream +{ + /** + * @param int $level + */ + public function __construct(StreamInterface $stream, $level = -1) + { + if (!extension_loaded('zlib')) { + throw new \RuntimeException('The zlib extension must be enabled to use this stream'); + } + + parent::__construct($stream, ['window' => -15]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15, 'level' => $level]); + } + + /** + * {@inheritdoc} + */ + protected function readFilter() + { + return 'zlib.inflate'; + } + + /** + * {@inheritdoc} + */ + protected function writeFilter() + { + return 'zlib.deflate'; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Exception.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Exception.php new file mode 100644 index 0000000..80d4cd9 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Exception.php @@ -0,0 +1,10 @@ + + */ +interface Formatter +{ + /** + * Formats a request. + * + * @return string + */ + public function formatRequest(RequestInterface $request); + + /** + * Formats a response. + * + * @return string + */ + public function formatResponse(ResponseInterface $response); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/CurlCommandFormatter.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/CurlCommandFormatter.php new file mode 100644 index 0000000..78b1d55 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/CurlCommandFormatter.php @@ -0,0 +1,93 @@ + + */ +class CurlCommandFormatter implements Formatter +{ + /** + * {@inheritdoc} + */ + public function formatRequest(RequestInterface $request) + { + $command = sprintf('curl %s', escapeshellarg((string) $request->getUri()->withFragment(''))); + if ('1.0' === $request->getProtocolVersion()) { + $command .= ' --http1.0'; + } elseif ('2.0' === $request->getProtocolVersion()) { + $command .= ' --http2'; + } + + $method = strtoupper($request->getMethod()); + if ('HEAD' === $method) { + $command .= ' --head'; + } elseif ('GET' !== $method) { + $command .= ' --request '.$method; + } + + $command .= $this->getHeadersAsCommandOptions($request); + + $body = $request->getBody(); + if ($body->getSize() > 0) { + // escapeshellarg argument max length on Windows, but longer body in curl command would be impractical anyways + if ($body->getSize() > 8192) { + $data = '[too long stream omitted]'; + } elseif ($body->isSeekable()) { + $data = $body->__toString(); + $body->rewind(); + // all non-printable ASCII characters and except for \t, \r, \n + if (preg_match('/([\x00-\x09\x0C\x0E-\x1F\x7F])/', $data)) { + $data = '[binary stream omitted]'; + } + } else { + $data = '[non-seekable stream omitted]'; + } + $escapedData = @escapeshellarg($data); + if (empty($escapedData)) { + $escapedData = 'We couldn\'t not escape the data properly'; + } + + $command .= sprintf(' --data %s', $escapedData); + } + + return $command; + } + + /** + * {@inheritdoc} + */ + public function formatResponse(ResponseInterface $response) + { + return ''; + } + + /** + * @return string + */ + private function getHeadersAsCommandOptions(RequestInterface $request) + { + $command = ''; + foreach ($request->getHeaders() as $name => $values) { + if ('host' === strtolower($name) && $values[0] === $request->getUri()->getHost()) { + continue; + } + + if ('user-agent' === strtolower($name)) { + $command .= sprintf(' -A %s', escapeshellarg($values[0])); + + continue; + } + + $command .= sprintf(' -H %s', escapeshellarg($name.': '.$request->getHeaderLine($name))); + } + + return $command; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/FullHttpMessageFormatter.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/FullHttpMessageFormatter.php new file mode 100644 index 0000000..64ce3ce --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/FullHttpMessageFormatter.php @@ -0,0 +1,100 @@ + + */ +class FullHttpMessageFormatter implements Formatter +{ + /** + * The maximum length of the body. + * + * @var int|null + */ + private $maxBodyLength; + + /** + * @param int|null $maxBodyLength + */ + public function __construct($maxBodyLength = 1000) + { + $this->maxBodyLength = $maxBodyLength; + } + + /** + * {@inheritdoc} + */ + public function formatRequest(RequestInterface $request) + { + $message = sprintf( + "%s %s HTTP/%s\n", + $request->getMethod(), + $request->getRequestTarget(), + $request->getProtocolVersion() + ); + + foreach ($request->getHeaders() as $name => $values) { + $message .= $name.': '.implode(', ', $values)."\n"; + } + + return $this->addBody($request, $message); + } + + /** + * {@inheritdoc} + */ + public function formatResponse(ResponseInterface $response) + { + $message = sprintf( + "HTTP/%s %s %s\n", + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + foreach ($response->getHeaders() as $name => $values) { + $message .= $name.': '.implode(', ', $values)."\n"; + } + + return $this->addBody($response, $message); + } + + /** + * Add the message body if the stream is seekable. + * + * @param string $message + * + * @return string + */ + private function addBody(MessageInterface $request, $message) + { + $message .= "\n"; + $stream = $request->getBody(); + if (!$stream->isSeekable() || 0 === $this->maxBodyLength) { + // Do not read the stream + return $message; + } + + $data = $stream->__toString(); + $stream->rewind(); + + // all non-printable ASCII characters and except for \t, \r, \n + if (preg_match('/([\x00-\x09\x0C\x0E-\x1F\x7F])/', $data)) { + return $message.'[binary stream omitted]'; + } + + if (null === $this->maxBodyLength) { + return $message.$data; + } + + return $message.mb_substr($data, 0, $this->maxBodyLength); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/SimpleFormatter.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/SimpleFormatter.php new file mode 100644 index 0000000..b1fcabd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Formatter/SimpleFormatter.php @@ -0,0 +1,42 @@ + + * @author Márk Sági-Kazár + */ +class SimpleFormatter implements Formatter +{ + /** + * {@inheritdoc} + */ + public function formatRequest(RequestInterface $request) + { + return sprintf( + '%s %s %s', + $request->getMethod(), + $request->getUri()->__toString(), + $request->getProtocolVersion() + ); + } + + /** + * {@inheritdoc} + */ + public function formatResponse(ResponseInterface $response) + { + return sprintf( + '%s %s %s', + $response->getStatusCode(), + $response->getReasonPhrase(), + $response->getProtocolVersion() + ); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/DiactorosMessageFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/DiactorosMessageFactory.php new file mode 100644 index 0000000..6d54d35 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/DiactorosMessageFactory.php @@ -0,0 +1,82 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory + */ +final class DiactorosMessageFactory implements MessageFactory +{ + /** + * @var DiactorosStreamFactory + */ + private $streamFactory; + + public function __construct() + { + $this->streamFactory = new DiactorosStreamFactory(); + } + + /** + * {@inheritdoc} + */ + public function createRequest( + $method, + $uri, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ) { + if (class_exists(LaminasRequest::class)) { + return (new LaminasRequest( + $uri, + $method, + $this->streamFactory->createStream($body), + $headers + ))->withProtocolVersion($protocolVersion); + } + + return (new ZendRequest( + $uri, + $method, + $this->streamFactory->createStream($body), + $headers + ))->withProtocolVersion($protocolVersion); + } + + /** + * {@inheritdoc} + */ + public function createResponse( + $statusCode = 200, + $reasonPhrase = null, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ) { + if (class_exists(LaminasResponse::class)) { + return (new LaminasResponse( + $this->streamFactory->createStream($body), + $statusCode, + $headers + ))->withProtocolVersion($protocolVersion); + } + + return (new ZendResponse( + $this->streamFactory->createStream($body), + $statusCode, + $headers + ))->withProtocolVersion($protocolVersion); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/GuzzleMessageFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/GuzzleMessageFactory.php new file mode 100644 index 0000000..02989d9 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/GuzzleMessageFactory.php @@ -0,0 +1,55 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory + */ +final class GuzzleMessageFactory implements MessageFactory +{ + /** + * {@inheritdoc} + */ + public function createRequest( + $method, + $uri, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ) { + return new Request( + $method, + $uri, + $headers, + $body, + $protocolVersion + ); + } + + /** + * {@inheritdoc} + */ + public function createResponse( + $statusCode = 200, + $reasonPhrase = null, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ) { + return new Response( + $statusCode, + $headers, + $body, + $protocolVersion, + $reasonPhrase + ); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/SlimMessageFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/SlimMessageFactory.php new file mode 100644 index 0000000..bee9322 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/MessageFactory/SlimMessageFactory.php @@ -0,0 +1,74 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory + */ +final class SlimMessageFactory implements MessageFactory +{ + /** + * @var SlimStreamFactory + */ + private $streamFactory; + + /** + * @var SlimUriFactory + */ + private $uriFactory; + + public function __construct() + { + $this->streamFactory = new SlimStreamFactory(); + $this->uriFactory = new SlimUriFactory(); + } + + /** + * {@inheritdoc} + */ + public function createRequest( + $method, + $uri, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ) { + return (new Request( + $method, + $this->uriFactory->createUri($uri), + new Headers($headers), + [], + [], + $this->streamFactory->createStream($body), + [] + ))->withProtocolVersion($protocolVersion); + } + + /** + * {@inheritdoc} + */ + public function createResponse( + $statusCode = 200, + $reasonPhrase = null, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ) { + return (new Response( + $statusCode, + new Headers($headers), + $this->streamFactory->createStream($body) + ))->withProtocolVersion($protocolVersion); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher.php new file mode 100644 index 0000000..94fe532 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher.php @@ -0,0 +1,26 @@ + + */ +interface RequestMatcher +{ + /** + * Decides whether the rule(s) implemented by the strategy matches the supplied request. + * + * @param RequestInterface $request The PSR7 request to check for a match + * + * @return bool true if the request matches, false otherwise + */ + public function matches(RequestInterface $request); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/CallbackRequestMatcher.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/CallbackRequestMatcher.php new file mode 100644 index 0000000..1197dd9 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/CallbackRequestMatcher.php @@ -0,0 +1,32 @@ + + */ +final class CallbackRequestMatcher implements RequestMatcher +{ + /** + * @var callable + */ + private $callback; + + public function __construct(callable $callback) + { + $this->callback = $callback; + } + + /** + * {@inheritdoc} + */ + public function matches(RequestInterface $request) + { + return (bool) call_user_func($this->callback, $request); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/RegexRequestMatcher.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/RegexRequestMatcher.php new file mode 100644 index 0000000..91f3729 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/RegexRequestMatcher.php @@ -0,0 +1,41 @@ + + * + * @deprecated since version 1.2 and will be removed in 2.0. Use {@link RequestMatcher} instead. + */ +final class RegexRequestMatcher implements RequestMatcher +{ + /** + * Matching regex. + * + * @var string + */ + private $regex; + + /** + * @param string $regex + */ + public function __construct($regex) + { + $this->regex = $regex; + } + + /** + * {@inheritdoc} + */ + public function matches(RequestInterface $request) + { + return (bool) preg_match($this->regex, (string) $request->getUri()); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/RequestMatcher.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/RequestMatcher.php new file mode 100644 index 0000000..e2aa021 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/RequestMatcher/RequestMatcher.php @@ -0,0 +1,78 @@ + + * @author Joel Wurtz + */ +final class RequestMatcher implements RequestMatcherInterface +{ + /** + * @var string + */ + private $path; + + /** + * @var string + */ + private $host; + + /** + * @var array + */ + private $methods = []; + + /** + * @var string[] + */ + private $schemes = []; + + /** + * The regular expressions used for path or host must be specified without delimiter. + * You do not need to escape the forward slash / to match it. + * + * @param string|null $path Regular expression for the path + * @param string|null $host Regular expression for the hostname + * @param string|string[]|null $methods Method or list of methods to match + * @param string|string[]|null $schemes Scheme or list of schemes to match (e.g. http or https) + */ + public function __construct($path = null, $host = null, $methods = [], $schemes = []) + { + $this->path = $path; + $this->host = $host; + $this->methods = array_map('strtoupper', (array) $methods); + $this->schemes = array_map('strtolower', (array) $schemes); + } + + /** + * {@inheritdoc} + * + * @api + */ + public function matches(RequestInterface $request) + { + if ($this->schemes && !in_array($request->getUri()->getScheme(), $this->schemes)) { + return false; + } + + if ($this->methods && !in_array($request->getMethod(), $this->methods)) { + return false; + } + + if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getUri()->getPath()))) { + return false; + } + + if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getUri()->getHost())) { + return false; + } + + return true; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Stream/BufferedStream.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Stream/BufferedStream.php new file mode 100644 index 0000000..1eac974 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/Stream/BufferedStream.php @@ -0,0 +1,270 @@ +stream = $stream; + $this->size = $stream->getSize(); + + if ($useFileBuffer) { + $this->resource = fopen('php://temp/maxmemory:'.$memoryBuffer, 'rw+'); + } else { + $this->resource = fopen('php://memory', 'rw+'); + } + + if (false === $this->resource) { + throw new \RuntimeException('Cannot create a resource over temp or memory implementation'); + } + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + try { + $this->rewind(); + + return $this->getContents(); + } catch (\Throwable $throwable) { + return ''; + } catch (\Exception $exception) { // Layer to be BC with PHP 5, remove this when we only support PHP 7+ + return ''; + } + } + + /** + * {@inheritdoc} + */ + public function close() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot close on a detached stream'); + } + + $this->stream->close(); + fclose($this->resource); + } + + /** + * {@inheritdoc} + */ + public function detach() + { + if (null === $this->resource) { + return; + } + + // Force reading the remaining data of the stream + $this->getContents(); + + $resource = $this->resource; + $this->stream->close(); + $this->stream = null; + $this->resource = null; + + return $resource; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + if (null === $this->resource) { + return; + } + + if (null === $this->size && $this->stream->eof()) { + return $this->written; + } + + return $this->size; + } + + /** + * {@inheritdoc} + */ + public function tell() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot tell on a detached stream'); + } + + return ftell($this->resource); + } + + /** + * {@inheritdoc} + */ + public function eof() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot call eof on a detached stream'); + } + + // We are at the end only when both our resource and underlying stream are at eof + return $this->stream->eof() && (ftell($this->resource) === $this->written); + } + + /** + * {@inheritdoc} + */ + public function isSeekable() + { + return null !== $this->resource; + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot seek on a detached stream'); + } + + fseek($this->resource, $offset, $whence); + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot rewind on a detached stream'); + } + + rewind($this->resource); + } + + /** + * {@inheritdoc} + */ + public function isWritable() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function write($string) + { + throw new \RuntimeException('Cannot write on this stream'); + } + + /** + * {@inheritdoc} + */ + public function isReadable() + { + return null !== $this->resource; + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot read on a detached stream'); + } + + $read = ''; + + // First read from the resource + if (ftell($this->resource) !== $this->written) { + $read = fread($this->resource, $length); + } + + $bytesRead = strlen($read); + + if ($bytesRead < $length) { + $streamRead = $this->stream->read($length - $bytesRead); + + // Write on the underlying stream what we read + $this->written += fwrite($this->resource, $streamRead); + $read .= $streamRead; + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot read on a detached stream'); + } + + $read = ''; + + while (!$this->eof()) { + $read .= $this->read(8192); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + if (null === $this->resource) { + if (null === $key) { + return []; + } + + return; + } + + $metadata = stream_get_meta_data($this->resource); + + if (null === $key) { + return $metadata; + } + + if (!array_key_exists($key, $metadata)) { + return; + } + + return $metadata[$key]; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/DiactorosStreamFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/DiactorosStreamFactory.php new file mode 100644 index 0000000..8ae2b28 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/DiactorosStreamFactory.php @@ -0,0 +1,48 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory + */ +final class DiactorosStreamFactory implements StreamFactory +{ + /** + * {@inheritdoc} + */ + public function createStream($body = null) + { + if ($body instanceof StreamInterface) { + return $body; + } + + if (is_resource($body)) { + if (class_exists(LaminasStream::class)) { + return new LaminasStream($body); + } + + return new ZendStream($body); + } + + if (class_exists(LaminasStream::class)) { + $stream = new LaminasStream('php://memory', 'rw'); + } else { + $stream = new ZendStream('php://memory', 'rw'); + } + + if (null !== $body && '' !== $body) { + $stream->write((string) $body); + } + + return $stream; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/GuzzleStreamFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/GuzzleStreamFactory.php new file mode 100644 index 0000000..9adeeb5 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/GuzzleStreamFactory.php @@ -0,0 +1,23 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory + */ +final class GuzzleStreamFactory implements StreamFactory +{ + /** + * {@inheritdoc} + */ + public function createStream($body = null) + { + return \GuzzleHttp\Psr7\stream_for($body); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/SlimStreamFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/SlimStreamFactory.php new file mode 100644 index 0000000..9274aae --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/StreamFactory/SlimStreamFactory.php @@ -0,0 +1,39 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory + */ +final class SlimStreamFactory implements StreamFactory +{ + /** + * {@inheritdoc} + */ + public function createStream($body = null) + { + if ($body instanceof StreamInterface) { + return $body; + } + + if (is_resource($body)) { + return new Stream($body); + } + + $resource = fopen('php://memory', 'r+'); + $stream = new Stream($resource); + if (null !== $body && '' !== $body) { + $stream->write((string) $body); + } + + return $stream; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/DiactorosUriFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/DiactorosUriFactory.php new file mode 100644 index 0000000..be883de --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/DiactorosUriFactory.php @@ -0,0 +1,36 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory + */ +final class DiactorosUriFactory implements UriFactory +{ + /** + * {@inheritdoc} + */ + public function createUri($uri) + { + if ($uri instanceof UriInterface) { + return $uri; + } elseif (is_string($uri)) { + if (class_exists(LaminasUri::class)) { + return new LaminasUri($uri); + } + + return new ZendUri($uri); + } + + throw new \InvalidArgumentException('URI must be a string or UriInterface'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/GuzzleUriFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/GuzzleUriFactory.php new file mode 100644 index 0000000..b16ca52 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/GuzzleUriFactory.php @@ -0,0 +1,24 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory + */ +final class GuzzleUriFactory implements UriFactory +{ + /** + * {@inheritdoc} + */ + public function createUri($uri) + { + return Psr7\uri_for($uri); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/SlimUriFactory.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/SlimUriFactory.php new file mode 100644 index 0000000..e5bef03 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/UriFactory/SlimUriFactory.php @@ -0,0 +1,33 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory + */ +final class SlimUriFactory implements UriFactory +{ + /** + * {@inheritdoc} + */ + public function createUri($uri) + { + if ($uri instanceof UriInterface) { + return $uri; + } + + if (is_string($uri)) { + return Uri::createFromString($uri); + } + + throw new \InvalidArgumentException('URI must be a string or UriInterface'); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/filters.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/filters.php new file mode 100644 index 0000000..15ed73d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/message/src/filters.php @@ -0,0 +1,6 @@ + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/README.md b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/README.md new file mode 100644 index 0000000..ee95e09 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/README.md @@ -0,0 +1,48 @@ +# Promise + +[![Latest Version](https://img.shields.io/github/release/php-http/promise.svg?style=flat-square)](https://github.com/php-http/promise/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Build Status](https://img.shields.io/travis/php-http/promise.svg?style=flat-square)](https://travis-ci.org/php-http/promise) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/promise.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/promise) +[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/promise.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/promise) +[![Total Downloads](https://img.shields.io/packagist/dt/php-http/promise.svg?style=flat-square)](https://packagist.org/packages/php-http/promise) + +**Promise used for asynchronous HTTP requests.** + +**Note:** This will eventually be removed/deprecated and replaced with the upcoming Promise PSR. + + +## Install + +Via Composer + +``` bash +$ composer require php-http/promise +``` + + +## Documentation + +Please see the [official documentation](http://docs.php-http.org/en/latest/components/promise.html). + + +## Testing + +``` bash +$ composer test +``` + + +## Contributing + +Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). + + +## Security + +If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). + + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/composer.json b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/composer.json new file mode 100644 index 0000000..812167b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/composer.json @@ -0,0 +1,38 @@ +{ + "name": "php-http/promise", + "description": "Promise used for asynchronous HTTP requests", + "license": "MIT", + "keywords": ["promise"], + "homepage": "http://httplug.io", + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "require": { + "php" : "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage" : "^4.3.2", + "phpspec/phpspec": "^5.1.2 || ^6.2" + }, + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "scripts": { + "test": "vendor/bin/phpspec run", + "test-ci": "vendor/bin/phpspec run -c phpspec.yml.ci" + }, + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/FulfilledPromise.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/FulfilledPromise.php new file mode 100644 index 0000000..f60f686 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/FulfilledPromise.php @@ -0,0 +1,58 @@ + + */ +final class FulfilledPromise implements Promise +{ + /** + * @var mixed + */ + private $result; + + /** + * @param $result + */ + public function __construct($result) + { + $this->result = $result; + } + + /** + * {@inheritdoc} + */ + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if (null === $onFulfilled) { + return $this; + } + + try { + return new self($onFulfilled($this->result)); + } catch (\Exception $e) { + return new RejectedPromise($e); + } + } + + /** + * {@inheritdoc} + */ + public function getState() + { + return Promise::FULFILLED; + } + + /** + * {@inheritdoc} + */ + public function wait($unwrap = true) + { + if ($unwrap) { + return $this->result; + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/Promise.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/Promise.php new file mode 100644 index 0000000..3258ed0 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/Promise.php @@ -0,0 +1,69 @@ + + * @author Márk Sági-Kazár + */ +interface Promise +{ + /** + * Promise has not been fulfilled or rejected. + */ + const PENDING = 'pending'; + + /** + * Promise has been fulfilled. + */ + const FULFILLED = 'fulfilled'; + + /** + * Promise has been rejected. + */ + const REJECTED = 'rejected'; + + /** + * Adds behavior for when the promise is resolved or rejected (response will be available, or error happens). + * + * If you do not care about one of the cases, you can set the corresponding callable to null + * The callback will be called when the value arrived and never more than once. + * + * @param callable|null $onFulfilled called when a response will be available + * @param callable|null $onRejected called when an exception occurs + * + * @return Promise a new resolved promise with value of the executed callback (onFulfilled / onRejected) + */ + public function then(callable $onFulfilled = null, callable $onRejected = null); + + /** + * Returns the state of the promise, one of PENDING, FULFILLED or REJECTED. + * + * @return string + */ + public function getState(); + + /** + * Wait for the promise to be fulfilled or rejected. + * + * When this method returns, the request has been resolved and if callables have been + * specified, the appropriate one has terminated. + * + * When $unwrap is true (the default), the response is returned, or the exception thrown + * on failure. Otherwise, nothing is returned or thrown. + * + * @param bool $unwrap Whether to return resolved value / throw reason or not + * + * @return mixed Resolved value, null if $unwrap is set to false + * + * @throws \Exception the rejection reason if $unwrap is set to true and the request failed + */ + public function wait($unwrap = true); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/RejectedPromise.php b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/RejectedPromise.php new file mode 100644 index 0000000..e396a40 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/php-http/promise/src/RejectedPromise.php @@ -0,0 +1,58 @@ + + */ +final class RejectedPromise implements Promise +{ + /** + * @var \Exception + */ + private $exception; + + /** + * @param \Exception $exception + */ + public function __construct(\Exception $exception) + { + $this->exception = $exception; + } + + /** + * {@inheritdoc} + */ + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if (null === $onRejected) { + return $this; + } + + try { + return new FulfilledPromise($onRejected($this->exception)); + } catch (\Exception $e) { + return new self($e); + } + } + + /** + * {@inheritdoc} + */ + public function getState() + { + return Promise::REJECTED; + } + + /** + * {@inheritdoc} + */ + public function wait($unwrap = true) + { + if ($unwrap) { + throw $this->exception; + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/CHANGELOG.md new file mode 100644 index 0000000..e2dc25f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.0.1 + +Allow installation with PHP 8. No code changes. + +## 1.0.0 + +First stable release. No changes since 0.3.0. + +## 0.3.0 + +Added Interface suffix on exceptions + +## 0.2.0 + +All exceptions are in `Psr\Http\Client` namespace + +## 0.1.0 + +First release diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/LICENSE new file mode 100644 index 0000000..cd5e002 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/README.md b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/README.md new file mode 100644 index 0000000..6876b84 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/README.md @@ -0,0 +1,12 @@ +HTTP Client +=========== + +This repository holds all the common code related to [PSR-18 (HTTP Client)][psr-url]. + +Note that this is not a HTTP Client implementation of its own. It is merely abstractions that describe the components of a HTTP Client. + +The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. + +[psr-url]: http://www.php-fig.org/psr/psr-18 +[package-url]: https://packagist.org/packages/psr/http-client +[implementation-url]: https://packagist.org/providers/psr/http-client-implementation diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/composer.json b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/composer.json new file mode 100644 index 0000000..c195f8f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/composer.json @@ -0,0 +1,27 @@ +{ + "name": "psr/http-client", + "description": "Common interface for HTTP clients", + "keywords": ["psr", "psr-18", "http", "http-client"], + "homepage": "https://github.com/php-fig/http-client", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/src/ClientExceptionInterface.php b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/src/ClientExceptionInterface.php new file mode 100644 index 0000000..aa0b9cf --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-client/src/ClientExceptionInterface.php @@ -0,0 +1,10 @@ +=7.0.0", + "psr/http-message": "^1.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/RequestFactoryInterface.php b/includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/RequestFactoryInterface.php new file mode 100644 index 0000000..cb39a08 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-factory/src/RequestFactoryInterface.php @@ -0,0 +1,18 @@ +=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/MessageInterface.php b/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 0000000..dd46e5e --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,187 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(); + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name); + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name); + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value); + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value); + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader($name); + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(); + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/RequestInterface.php b/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 0000000..a96d4fd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,129 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(); + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query); + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(); + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles); + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data); + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(); + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute($name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute($name, $value); + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute($name); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/StreamInterface.php b/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 0000000..f68f391 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,158 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(); + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(); + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(); + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(); + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(); + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(); + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(); + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme($scheme); + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo($user, $password = null); + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost($host); + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort($port); + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath($path); + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery($query); + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment($fragment); + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/AbstractLogger.php b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/AbstractLogger.php new file mode 100644 index 0000000..90e721a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/AbstractLogger.php @@ -0,0 +1,128 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/InvalidArgumentException.php b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/InvalidArgumentException.php new file mode 100644 index 0000000..67f852d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +logger = $logger; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/LoggerInterface.php b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/LoggerInterface.php new file mode 100644 index 0000000..2206cfd --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/LoggerInterface.php @@ -0,0 +1,125 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, $message, array $context = array()); +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/NullLogger.php b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/NullLogger.php new file mode 100644 index 0000000..c8f7293 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/NullLogger.php @@ -0,0 +1,30 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/DummyTest.php b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/DummyTest.php new file mode 100644 index 0000000..9638c11 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/DummyTest.php @@ -0,0 +1,18 @@ + ". + * + * Example ->error('Foo') would yield "error Foo". + * + * @return string[] + */ + abstract public function getLogs(); + + public function testImplements() + { + $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger()); + } + + /** + * @dataProvider provideLevelsAndMessages + */ + public function testLogsAtAllLevels($level, $message) + { + $logger = $this->getLogger(); + $logger->{$level}($message, array('user' => 'Bob')); + $logger->log($level, $message, array('user' => 'Bob')); + + $expected = array( + $level.' message of level '.$level.' with context: Bob', + $level.' message of level '.$level.' with context: Bob', + ); + $this->assertEquals($expected, $this->getLogs()); + } + + public function provideLevelsAndMessages() + { + return array( + LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), + LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), + LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), + LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), + LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), + LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), + LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), + LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), + ); + } + + /** + * @expectedException \Psr\Log\InvalidArgumentException + */ + public function testThrowsOnInvalidLevel() + { + $logger = $this->getLogger(); + $logger->log('invalid level', 'Foo'); + } + + public function testContextReplacement() + { + $logger = $this->getLogger(); + $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); + + $expected = array('info {Message {nothing} Bob Bar a}'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testObjectCastToString() + { + if (method_exists($this, 'createPartialMock')) { + $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString')); + } else { + $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString')); + } + $dummy->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('DUMMY')); + + $this->getLogger()->warning($dummy); + + $expected = array('warning DUMMY'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextCanContainAnything() + { + $closed = fopen('php://memory', 'r'); + fclose($closed); + + $context = array( + 'bool' => true, + 'null' => null, + 'string' => 'Foo', + 'int' => 0, + 'float' => 0.5, + 'nested' => array('with object' => new DummyTest), + 'object' => new \DateTime, + 'resource' => fopen('php://memory', 'r'), + 'closed' => $closed, + ); + + $this->getLogger()->warning('Crazy context data', $context); + + $expected = array('warning Crazy context data'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextExceptionKeyCanBeExceptionOrOtherValues() + { + $logger = $this->getLogger(); + $logger->warning('Random message', array('exception' => 'oops')); + $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); + + $expected = array( + 'warning Random message', + 'critical Uncaught Exception!' + ); + $this->assertEquals($expected, $this->getLogs()); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/TestLogger.php b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/TestLogger.php new file mode 100644 index 0000000..1be3230 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/Psr/Log/Test/TestLogger.php @@ -0,0 +1,147 @@ + $level, + 'message' => $message, + 'context' => $context, + ]; + + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + public function hasRecords($level) + { + return isset($this->recordsByLevel[$level]); + } + + public function hasRecord($record, $level) + { + if (is_string($record)) { + $record = ['message' => $record]; + } + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + return true; + }, $level); + } + + public function hasRecordThatContains($message, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($message) { + return strpos($rec['message'], $message) !== false; + }, $level); + } + + public function hasRecordThatMatches($regex, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($regex) { + return preg_match($regex, $rec['message']) > 0; + }, $level); + } + + public function hasRecordThatPasses(callable $predicate, $level) + { + if (!isset($this->recordsByLevel[$level])) { + return false; + } + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if (call_user_func($predicate, $rec, $i)) { + return true; + } + } + return false; + } + + public function __call($method, $args) + { + if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { + $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; + $level = strtolower($matches[2]); + if (method_exists($this, $genericMethod)) { + $args[] = $level; + return call_user_func_array([$this, $genericMethod], $args); + } + } + throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); + } + + public function reset() + { + $this->records = []; + $this->recordsByLevel = []; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/README.md b/includes/libraries/typesensesearch-client-php/vendor/psr/log/README.md new file mode 100644 index 0000000..a9f20c4 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/README.md @@ -0,0 +1,58 @@ +PSR Log +======= + +This repository holds all interfaces/classes/traits related to +[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +Note that this is not a logger of its own. It is merely an interface that +describes a logger. See the specification for more details. + +Installation +------------ + +```bash +composer require psr/log +``` + +Usage +----- + +If you need a logger, you can use the interface like this: + +```php +logger = $logger; + } + + public function doSomething() + { + if ($this->logger) { + $this->logger->info('Doing work'); + } + + try { + $this->doSomethingElse(); + } catch (Exception $exception) { + $this->logger->error('Oh no!', array('exception' => $exception)); + } + + // do something useful + } +} +``` + +You can then pick one of the implementations of the interface to get a logger. + +If you want to implement the interface, you can require this package and +implement `Psr\Log\LoggerInterface` in your code. Please read the +[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) +for details. diff --git a/includes/libraries/typesensesearch-client-php/vendor/psr/log/composer.json b/includes/libraries/typesensesearch-client-php/vendor/psr/log/composer.json new file mode 100644 index 0000000..3f6d4ee --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/psr/log/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/log", + "description": "Common interface for logging libraries", + "keywords": ["psr", "psr-3", "log"], + "homepage": "https://github.com/php-fig/log", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/.gitignore b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/CHANGELOG.md new file mode 100644 index 0000000..e984777 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/master/CHANGELOG.md diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/LICENSE new file mode 100644 index 0000000..5593b1d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/README.md b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/README.md new file mode 100644 index 0000000..4957933 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/README.md @@ -0,0 +1,26 @@ +Symfony Deprecation Contracts +============================= + +A generic function and convention to trigger deprecation notices. + +This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. + +By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, +the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. + +The function requires at least 3 arguments: + - the name of the Composer package that is triggering the deprecation + - the version of the package that introduced the deprecation + - the message of the deprecation + - more arguments can be provided: they will be inserted in the message using `printf()` formatting + +Example: +```php +trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); +``` + +This will generate the following message: +`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` + +While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty +`function trigger_deprecation() {}` in your application. diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/composer.json b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/composer.json new file mode 100644 index 0000000..052541c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/deprecation-contracts", + "type": "library", + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/function.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/function.php new file mode 100644 index 0000000..d437150 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/deprecation-contracts/function.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!function_exists('trigger_deprecation')) { + /** + * Triggers a silenced deprecation notice. + * + * @param string $package The name of the Composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The message of the deprecation + * @param mixed ...$args Values to insert in the message using printf() formatting + * + * @author Nicolas Grekas + */ + function trigger_deprecation(string $package, string $version, string $message, ...$args): void + { + @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/CHANGELOG.md b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/CHANGELOG.md new file mode 100644 index 0000000..d996e30 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/CHANGELOG.md @@ -0,0 +1,76 @@ +CHANGELOG +========= + +5.1.0 +----- + + * added fluent configuration of options using `OptionResolver::define()` + * added `setInfo()` and `getInfo()` methods + * updated the signature of method `OptionsResolver::setDeprecated()` to `OptionsResolver::setDeprecation(string $option, string $package, string $version, $message)` + * deprecated `OptionsResolverIntrospector::getDeprecationMessage()`, use `OptionsResolverIntrospector::getDeprecation()` instead + +5.0.0 +----- + + * added argument `$triggerDeprecation` to `OptionsResolver::offsetGet()` + +4.3.0 +----- + + * added `OptionsResolver::addNormalizer` method + +4.2.0 +----- + + * added support for nested options definition + * added `setDeprecated` and `isDeprecated` methods + +3.4.0 +----- + + * added `OptionsResolverIntrospector` to inspect options definitions inside an `OptionsResolver` instance + * added array of types support in allowed types (e.g int[]) + +2.6.0 +----- + + * deprecated OptionsResolverInterface + * [BC BREAK] removed "array" type hint from OptionsResolverInterface methods + setRequired(), setAllowedValues(), addAllowedValues(), setAllowedTypes() and + addAllowedTypes() + * added OptionsResolver::setDefault() + * added OptionsResolver::hasDefault() + * added OptionsResolver::setNormalizer() + * added OptionsResolver::isRequired() + * added OptionsResolver::getRequiredOptions() + * added OptionsResolver::isMissing() + * added OptionsResolver::getMissingOptions() + * added OptionsResolver::setDefined() + * added OptionsResolver::isDefined() + * added OptionsResolver::getDefinedOptions() + * added OptionsResolver::remove() + * added OptionsResolver::clear() + * deprecated OptionsResolver::replaceDefaults() + * deprecated OptionsResolver::setOptional() in favor of setDefined() + * deprecated OptionsResolver::isKnown() in favor of isDefined() + * [BC BREAK] OptionsResolver::isRequired() returns true now if a required + option has a default value set + * [BC BREAK] merged Options into OptionsResolver and turned Options into an + interface + * deprecated Options::overload() (now in OptionsResolver) + * deprecated Options::set() (now in OptionsResolver) + * deprecated Options::get() (now in OptionsResolver) + * deprecated Options::has() (now in OptionsResolver) + * deprecated Options::replace() (now in OptionsResolver) + * [BC BREAK] Options::get() (now in OptionsResolver) can only be used within + lazy option/normalizer closures now + * [BC BREAK] removed Traversable interface from Options since using within + lazy option/normalizer closures resulted in exceptions + * [BC BREAK] removed Options::all() since using within lazy option/normalizer + closures resulted in exceptions + * [BC BREAK] OptionDefinitionException now extends LogicException instead of + RuntimeException + * [BC BREAK] normalizers are not executed anymore for unset options + * normalizers are executed after validating the options now + * [BC BREAK] an UndefinedOptionsException is now thrown instead of an + InvalidOptionsException when non-existing options are passed diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Debug/OptionsResolverIntrospector.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Debug/OptionsResolverIntrospector.php new file mode 100644 index 0000000..95909f3 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Debug/OptionsResolverIntrospector.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Debug; + +use Symfony\Component\OptionsResolver\Exception\NoConfigurationException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Maxime Steinhausser + * + * @final + */ +class OptionsResolverIntrospector +{ + private $get; + + public function __construct(OptionsResolver $optionsResolver) + { + $this->get = \Closure::bind(function ($property, $option, $message) { + /** @var OptionsResolver $this */ + if (!$this->isDefined($option)) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist.', $option)); + } + + if (!\array_key_exists($option, $this->{$property})) { + throw new NoConfigurationException($message); + } + + return $this->{$property}[$option]; + }, $optionsResolver, $optionsResolver); + } + + /** + * @return mixed + * + * @throws NoConfigurationException on no configured value + */ + public function getDefault(string $option) + { + return ($this->get)('defaults', $option, sprintf('No default value was set for the "%s" option.', $option)); + } + + /** + * @return \Closure[] + * + * @throws NoConfigurationException on no configured closures + */ + public function getLazyClosures(string $option): array + { + return ($this->get)('lazy', $option, sprintf('No lazy closures were set for the "%s" option.', $option)); + } + + /** + * @return string[] + * + * @throws NoConfigurationException on no configured types + */ + public function getAllowedTypes(string $option): array + { + return ($this->get)('allowedTypes', $option, sprintf('No allowed types were set for the "%s" option.', $option)); + } + + /** + * @return mixed[] + * + * @throws NoConfigurationException on no configured values + */ + public function getAllowedValues(string $option): array + { + return ($this->get)('allowedValues', $option, sprintf('No allowed values were set for the "%s" option.', $option)); + } + + /** + * @throws NoConfigurationException on no configured normalizer + */ + public function getNormalizer(string $option): \Closure + { + return current($this->getNormalizers($option)); + } + + /** + * @throws NoConfigurationException when no normalizer is configured + */ + public function getNormalizers(string $option): array + { + return ($this->get)('normalizers', $option, sprintf('No normalizer was set for the "%s" option.', $option)); + } + + /** + * @return string|\Closure + * + * @throws NoConfigurationException on no configured deprecation + * + * @deprecated since Symfony 5.1, use "getDeprecation()" instead. + */ + public function getDeprecationMessage(string $option) + { + trigger_deprecation('symfony/options-resolver', '5.1', 'The "%s()" method is deprecated, use "getDeprecation()" instead.', __METHOD__); + + return $this->getDeprecation($option)['message']; + } + + /** + * @throws NoConfigurationException on no configured deprecation + */ + public function getDeprecation(string $option): array + { + return ($this->get)('deprecated', $option, sprintf('No deprecation was set for the "%s" option.', $option)); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/AccessException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/AccessException.php new file mode 100644 index 0000000..c12b680 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/AccessException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option outside of or write it inside of + * {@link \Symfony\Component\OptionsResolver\Options::resolve()}. + * + * @author Bernhard Schussek + */ +class AccessException extends \LogicException implements ExceptionInterface +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/ExceptionInterface.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/ExceptionInterface.php new file mode 100644 index 0000000..ea99d05 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Marker interface for all exceptions thrown by the OptionsResolver component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/InvalidArgumentException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..6d421d6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when an argument is invalid. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/InvalidOptionsException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/InvalidOptionsException.php new file mode 100644 index 0000000..6fd4f12 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/InvalidOptionsException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when the value of an option does not match its validation rules. + * + * You should make sure a valid value is passed to the option. + * + * @author Bernhard Schussek + */ +class InvalidOptionsException extends InvalidArgumentException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/MissingOptionsException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/MissingOptionsException.php new file mode 100644 index 0000000..faa487f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/MissingOptionsException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when a required option is missing. + * + * Add the option to the passed options array. + * + * @author Bernhard Schussek + */ +class MissingOptionsException extends InvalidArgumentException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/NoConfigurationException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/NoConfigurationException.php new file mode 100644 index 0000000..6693ec1 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/NoConfigurationException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; + +/** + * Thrown when trying to introspect an option definition property + * for which no value was configured inside the OptionsResolver instance. + * + * @see OptionsResolverIntrospector + * + * @author Maxime Steinhausser + */ +class NoConfigurationException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/NoSuchOptionException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/NoSuchOptionException.php new file mode 100644 index 0000000..4c3280f --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/NoSuchOptionException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option that has no value set. + * + * When accessing optional options from within a lazy option or normalizer you should first + * check whether the optional option is set. You can do this with `isset($options['optional'])`. + * In contrast to the {@link UndefinedOptionsException}, this is a runtime exception that can + * occur when evaluating lazy options. + * + * @author Tobias Schultze + */ +class NoSuchOptionException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/OptionDefinitionException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/OptionDefinitionException.php new file mode 100644 index 0000000..e8e339d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/OptionDefinitionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when two lazy options have a cyclic dependency. + * + * @author Bernhard Schussek + */ +class OptionDefinitionException extends \LogicException implements ExceptionInterface +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/UndefinedOptionsException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/UndefinedOptionsException.php new file mode 100644 index 0000000..6ca3fce --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Exception/UndefinedOptionsException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when an undefined option is passed. + * + * You should remove the options in question from your code or define them + * beforehand. + * + * @author Bernhard Schussek + */ +class UndefinedOptionsException extends InvalidArgumentException +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/LICENSE new file mode 100644 index 0000000..9ff2d0d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/OptionConfigurator.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/OptionConfigurator.php new file mode 100644 index 0000000..47f5bea --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/OptionConfigurator.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\AccessException; + +final class OptionConfigurator +{ + private $name; + private $resolver; + + public function __construct(string $name, OptionsResolver $resolver) + { + $this->name = $name; + $this->resolver = $resolver; + $this->resolver->setDefined($name); + } + + /** + * Adds allowed types for this option. + * + * @param string ...$types One or more accepted types + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function allowedTypes(string ...$types): self + { + $this->resolver->setAllowedTypes($this->name, $types); + + return $this; + } + + /** + * Sets allowed values for this option. + * + * @param mixed ...$values One or more acceptable values/closures + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function allowedValues(...$values): self + { + $this->resolver->setAllowedValues($this->name, $values); + + return $this; + } + + /** + * Sets the default value for this option. + * + * @param mixed $value The default value of the option + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function default($value): self + { + $this->resolver->setDefault($this->name, $value); + + return $this; + } + + /** + * Defines an option configurator with the given name. + */ + public function define(string $option): self + { + return $this->resolver->define($option); + } + + /** + * Marks this option as deprecated. + * + * @param string $package The name of the composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string|\Closure $message The deprecation message to use + * + * @return $this + */ + public function deprecated(string $package, string $version, $message = 'The option "%name%" is deprecated.'): self + { + $this->resolver->setDeprecated($this->name, $package, $version, $message); + + return $this; + } + + /** + * Sets the normalizer for this option. + * + * @param \Closure $normalizer The normalizer + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function normalize(\Closure $normalizer): self + { + $this->resolver->setNormalizer($this->name, $normalizer); + + return $this; + } + + /** + * Marks this option as required. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function required(): self + { + $this->resolver->setRequired($this->name); + + return $this; + } + + /** + * Sets an info message for an option. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function info(string $info): self + { + $this->resolver->setInfo($this->name, $info); + + return $this; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Options.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Options.php new file mode 100644 index 0000000..d444ec4 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/Options.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +/** + * Contains resolved option values. + * + * @author Bernhard Schussek + * @author Tobias Schultze + */ +interface Options extends \ArrayAccess, \Countable +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/OptionsResolver.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/OptionsResolver.php new file mode 100644 index 0000000..e369615 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/OptionsResolver.php @@ -0,0 +1,1305 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\AccessException; +use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; + +/** + * Validates options and merges them with default values. + * + * @author Bernhard Schussek + * @author Tobias Schultze + */ +class OptionsResolver implements Options +{ + private const VALIDATION_FUNCTIONS = [ + 'bool' => 'is_bool', + 'boolean' => 'is_bool', + 'int' => 'is_int', + 'integer' => 'is_int', + 'long' => 'is_int', + 'float' => 'is_float', + 'double' => 'is_float', + 'real' => 'is_float', + 'numeric' => 'is_numeric', + 'string' => 'is_string', + 'scalar' => 'is_scalar', + 'array' => 'is_array', + 'iterable' => 'is_iterable', + 'countable' => 'is_countable', + 'callable' => 'is_callable', + 'object' => 'is_object', + 'resource' => 'is_resource', + ]; + + /** + * The names of all defined options. + */ + private $defined = []; + + /** + * The default option values. + */ + private $defaults = []; + + /** + * A list of closure for nested options. + * + * @var \Closure[][] + */ + private $nested = []; + + /** + * The names of required options. + */ + private $required = []; + + /** + * The resolved option values. + */ + private $resolved = []; + + /** + * A list of normalizer closures. + * + * @var \Closure[][] + */ + private $normalizers = []; + + /** + * A list of accepted values for each option. + */ + private $allowedValues = []; + + /** + * A list of accepted types for each option. + */ + private $allowedTypes = []; + + /** + * A list of info messages for each option. + */ + private $info = []; + + /** + * A list of closures for evaluating lazy options. + */ + private $lazy = []; + + /** + * A list of lazy options whose closure is currently being called. + * + * This list helps detecting circular dependencies between lazy options. + */ + private $calling = []; + + /** + * A list of deprecated options. + */ + private $deprecated = []; + + /** + * The list of options provided by the user. + */ + private $given = []; + + /** + * Whether the instance is locked for reading. + * + * Once locked, the options cannot be changed anymore. This is + * necessary in order to avoid inconsistencies during the resolving + * process. If any option is changed after being read, all evaluated + * lazy options that depend on this option would become invalid. + */ + private $locked = false; + + private $parentsOptions = []; + + /** + * Sets the default value of a given option. + * + * If the default value should be set based on other options, you can pass + * a closure with the following signature: + * + * function (Options $options) { + * // ... + * } + * + * The closure will be evaluated when {@link resolve()} is called. The + * closure has access to the resolved values of other options through the + * passed {@link Options} instance: + * + * function (Options $options) { + * if (isset($options['port'])) { + * // ... + * } + * } + * + * If you want to access the previously set default value, add a second + * argument to the closure's signature: + * + * $options->setDefault('name', 'Default Name'); + * + * $options->setDefault('name', function (Options $options, $previousValue) { + * // 'Default Name' === $previousValue + * }); + * + * This is mostly useful if the configuration of the {@link Options} object + * is spread across different locations of your code, such as base and + * sub-classes. + * + * If you want to define nested options, you can pass a closure with the + * following signature: + * + * $options->setDefault('database', function (OptionsResolver $resolver) { + * $resolver->setDefined(['dbname', 'host', 'port', 'user', 'pass']); + * } + * + * To get access to the parent options, add a second argument to the closure's + * signature: + * + * function (OptionsResolver $resolver, Options $parent) { + * // 'default' === $parent['connection'] + * } + * + * @param string $option The name of the option + * @param mixed $value The default value of the option + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefault(string $option, $value) + { + // Setting is not possible once resolving starts, because then lazy + // options could manipulate the state of the object, leading to + // inconsistent results. + if ($this->locked) { + throw new AccessException('Default values cannot be set from a lazy option or normalizer.'); + } + + // If an option is a closure that should be evaluated lazily, store it + // in the "lazy" property. + if ($value instanceof \Closure) { + $reflClosure = new \ReflectionFunction($value); + $params = $reflClosure->getParameters(); + + if (isset($params[0]) && Options::class === $this->getParameterClassName($params[0])) { + // Initialize the option if no previous value exists + if (!isset($this->defaults[$option])) { + $this->defaults[$option] = null; + } + + // Ignore previous lazy options if the closure has no second parameter + if (!isset($this->lazy[$option]) || !isset($params[1])) { + $this->lazy[$option] = []; + } + + // Store closure for later evaluation + $this->lazy[$option][] = $value; + $this->defined[$option] = true; + + // Make sure the option is processed and is not nested anymore + unset($this->resolved[$option], $this->nested[$option]); + + return $this; + } + + if (isset($params[0]) && null !== ($type = $params[0]->getType()) && self::class === $type->getName() && (!isset($params[1]) || (($type = $params[1]->getType()) instanceof \ReflectionNamedType && Options::class === $type->getName()))) { + // Store closure for later evaluation + $this->nested[$option][] = $value; + $this->defaults[$option] = []; + $this->defined[$option] = true; + + // Make sure the option is processed and is not lazy anymore + unset($this->resolved[$option], $this->lazy[$option]); + + return $this; + } + } + + // This option is not lazy nor nested anymore + unset($this->lazy[$option], $this->nested[$option]); + + // Yet undefined options can be marked as resolved, because we only need + // to resolve options with lazy closures, normalizers or validation + // rules, none of which can exist for undefined options + // If the option was resolved before, update the resolved value + if (!isset($this->defined[$option]) || \array_key_exists($option, $this->resolved)) { + $this->resolved[$option] = $value; + } + + $this->defaults[$option] = $value; + $this->defined[$option] = true; + + return $this; + } + + /** + * Sets a list of default values. + * + * @param array $defaults The default values to set + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefaults(array $defaults) + { + foreach ($defaults as $option => $value) { + $this->setDefault($option, $value); + } + + return $this; + } + + /** + * Returns whether a default value is set for an option. + * + * Returns true if {@link setDefault()} was called for this option. + * An option is also considered set if it was set to null. + * + * @param string $option The option name + * + * @return bool Whether a default value is set + */ + public function hasDefault(string $option) + { + return \array_key_exists($option, $this->defaults); + } + + /** + * Marks one or more options as required. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setRequired($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be made required from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + $this->required[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is required. + * + * An option is required if it was passed to {@link setRequired()}. + * + * @param string $option The name of the option + * + * @return bool Whether the option is required + */ + public function isRequired(string $option) + { + return isset($this->required[$option]); + } + + /** + * Returns the names of all required options. + * + * @return string[] The names of the required options + * + * @see isRequired() + */ + public function getRequiredOptions() + { + return array_keys($this->required); + } + + /** + * Returns whether an option is missing a default value. + * + * An option is missing if it was passed to {@link setRequired()}, but not + * to {@link setDefault()}. This option must be passed explicitly to + * {@link resolve()}, otherwise an exception will be thrown. + * + * @param string $option The name of the option + * + * @return bool Whether the option is missing + */ + public function isMissing(string $option) + { + return isset($this->required[$option]) && !\array_key_exists($option, $this->defaults); + } + + /** + * Returns the names of all options missing a default value. + * + * @return string[] The names of the missing options + * + * @see isMissing() + */ + public function getMissingOptions() + { + return array_keys(array_diff_key($this->required, $this->defaults)); + } + + /** + * Defines a valid option name. + * + * Defines an option name without setting a default value. The option will + * be accepted when passed to {@link resolve()}. When not passed, the + * option will not be included in the resolved options. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefined($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be defined from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is defined. + * + * Returns true for any option passed to {@link setDefault()}, + * {@link setRequired()} or {@link setDefined()}. + * + * @param string $option The option name + * + * @return bool Whether the option is defined + */ + public function isDefined(string $option) + { + return isset($this->defined[$option]); + } + + /** + * Returns the names of all defined options. + * + * @return string[] The names of the defined options + * + * @see isDefined() + */ + public function getDefinedOptions() + { + return array_keys($this->defined); + } + + public function isNested(string $option): bool + { + return isset($this->nested[$option]); + } + + /** + * Deprecates an option, allowed types or values. + * + * Instead of passing the message, you may also pass a closure with the + * following signature: + * + * function (Options $options, $value): string { + * // ... + * } + * + * The closure receives the value as argument and should return a string. + * Return an empty string to ignore the option deprecation. + * + * The closure is invoked when {@link resolve()} is called. The parameter + * passed to the closure is the value of the option after validating it + * and before normalizing it. + * + * @param string $package The name of the composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string|\Closure $message The deprecation message to use + */ + public function setDeprecated(string $option/*, string $package, string $version, $message = 'The option "%name%" is deprecated.' */): self + { + if ($this->locked) { + throw new AccessException('Options cannot be deprecated from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $args = \func_get_args(); + + if (\func_num_args() < 3) { + trigger_deprecation('symfony/options-resolver', '5.1', 'The signature of method "%s()" requires 2 new arguments: "string $package, string $version", not defining them is deprecated.', __METHOD__); + + $message = $args[1] ?? 'The option "%name%" is deprecated.'; + $package = $version = ''; + } else { + $package = $args[1]; + $version = $args[2]; + $message = $args[3] ?? 'The option "%name%" is deprecated.'; + } + + if (!\is_string($message) && !$message instanceof \Closure) { + throw new InvalidArgumentException(sprintf('Invalid type for deprecation message argument, expected string or \Closure, but got "%s".', get_debug_type($message))); + } + + // ignore if empty string + if ('' === $message) { + return $this; + } + + $this->deprecated[$option] = [ + 'package' => $package, + 'version' => $version, + 'message' => $message, + ]; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + public function isDeprecated(string $option): bool + { + return isset($this->deprecated[$option]); + } + + /** + * Sets the normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * function (Options $options, $value) { + * // ... + * } + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @param string $option The option name + * @param \Closure $normalizer The normalizer + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setNormalizer(string $option, \Closure $normalizer) + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->normalizers[$option] = [$normalizer]; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds a normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * function (Options $options, $value): mixed { + * // ... + * } + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @param string $option The option name + * @param \Closure $normalizer The normalizer + * @param bool $forcePrepend If set to true, prepend instead of appending + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addNormalizer(string $option, \Closure $normalizer, bool $forcePrepend = false): self + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + if ($forcePrepend) { + $this->normalizers[$option] = $this->normalizers[$option] ?? []; + array_unshift($this->normalizers[$option], $normalizer); + } else { + $this->normalizers[$option][] = $normalizer; + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed values for an option. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedValues(string $option, $allowedValues) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : [$allowedValues]; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed values for an option. + * + * The values are merged with the allowed values defined previously. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedValues(string $option, $allowedValues) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + if (!\is_array($allowedValues)) { + $allowedValues = [$allowedValues]; + } + + if (!isset($this->allowedValues[$option])) { + $this->allowedValues[$option] = $allowedValues; + } else { + $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed types for an option. + * + * Any type for which a corresponding is_() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedTypes(string $option, $allowedTypes) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->allowedTypes[$option] = (array) $allowedTypes; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed types for an option. + * + * The types are merged with the allowed types defined previously. + * + * Any type for which a corresponding is_() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedTypes(string $option, $allowedTypes) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + if (!isset($this->allowedTypes[$option])) { + $this->allowedTypes[$option] = (array) $allowedTypes; + } else { + $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Defines an option configurator with the given name. + */ + public function define(string $option): OptionConfigurator + { + if (isset($this->defined[$option])) { + throw new OptionDefinitionException(sprintf('The option "%s" is already defined.', $option)); + } + + return new OptionConfigurator($option, $this); + } + + /** + * Sets an info message for an option. + * + * @return $this + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setInfo(string $option, string $info): self + { + if ($this->locked) { + throw new AccessException('The Info message cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + $this->info[$option] = $info; + + return $this; + } + + /** + * Gets the info message for an option. + */ + public function getInfo(string $option): ?string + { + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + return $this->info[$option] ?? null; + } + + /** + * Removes the option with the given name. + * + * Undefined options are ignored. + * + * @param string|string[] $optionNames One or more option names + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function remove($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be removed from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); + unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option], $this->info[$option]); + } + + return $this; + } + + /** + * Removes all options. + * + * @return $this + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function clear() + { + if ($this->locked) { + throw new AccessException('Options cannot be cleared from a lazy option or normalizer.'); + } + + $this->defined = []; + $this->defaults = []; + $this->nested = []; + $this->required = []; + $this->resolved = []; + $this->lazy = []; + $this->normalizers = []; + $this->allowedTypes = []; + $this->allowedValues = []; + $this->deprecated = []; + $this->info = []; + + return $this; + } + + /** + * Merges options with the default values stored in the container and + * validates them. + * + * Exceptions are thrown if: + * + * - Undefined options are passed; + * - Required options are missing; + * - Options have invalid types; + * - Options have invalid values. + * + * @param array $options A map of option names to values + * + * @return array The merged and validated options + * + * @throws UndefinedOptionsException If an option name is undefined + * @throws InvalidOptionsException If an option doesn't fulfill the + * specified validation rules + * @throws MissingOptionsException If a required option is missing + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + * @throws NoSuchOptionException If a lazy option reads an unavailable option + * @throws AccessException If called from a lazy option or normalizer + */ + public function resolve(array $options = []) + { + if ($this->locked) { + throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); + } + + // Allow this method to be called multiple times + $clone = clone $this; + + // Make sure that no unknown options are passed + $diff = array_diff_key($options, $clone->defined); + + if (\count($diff) > 0) { + ksort($clone->defined); + ksort($diff); + + throw new UndefinedOptionsException(sprintf((\count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', $this->formatOptions(array_keys($diff)), implode('", "', array_keys($clone->defined)))); + } + + // Override options set by the user + foreach ($options as $option => $value) { + $clone->given[$option] = true; + $clone->defaults[$option] = $value; + unset($clone->resolved[$option], $clone->lazy[$option]); + } + + // Check whether any required option is missing + $diff = array_diff_key($clone->required, $clone->defaults); + + if (\count($diff) > 0) { + ksort($diff); + + throw new MissingOptionsException(sprintf(\count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', $this->formatOptions(array_keys($diff)))); + } + + // Lock the container + $clone->locked = true; + + // Now process the individual options. Use offsetGet(), which resolves + // the option itself and any options that the option depends on + foreach ($clone->defaults as $option => $_) { + $clone->offsetGet($option); + } + + return $clone->resolved; + } + + /** + * Returns the resolved value of an option. + * + * @param string $option The option name + * @param bool $triggerDeprecation Whether to trigger the deprecation or not (true by default) + * + * @return mixed The option value + * + * @throws AccessException If accessing this method outside of + * {@link resolve()} + * @throws NoSuchOptionException If the option is not set + * @throws InvalidOptionsException If the option doesn't fulfill the + * specified validation rules + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + */ + public function offsetGet($option, bool $triggerDeprecation = true) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + // Shortcut for resolved options + if (isset($this->resolved[$option]) || \array_key_exists($option, $this->resolved)) { + if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || $this->calling) && \is_string($this->deprecated[$option]['message'])) { + trigger_deprecation($this->deprecated[$option]['package'], $this->deprecated[$option]['version'], strtr($this->deprecated[$option]['message'], ['%name%' => $option])); + } + + return $this->resolved[$option]; + } + + // Check whether the option is set at all + if (!isset($this->defaults[$option]) && !\array_key_exists($option, $this->defaults)) { + if (!isset($this->defined[$option])) { + throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); + } + + throw new NoSuchOptionException(sprintf('The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', $this->formatOptions([$option]))); + } + + $value = $this->defaults[$option]; + + // Resolve the option if it is a nested definition + if (isset($this->nested[$option])) { + // If the closure is already being called, we have a cyclic dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + if (!\is_array($value)) { + throw new InvalidOptionsException(sprintf('The nested option "%s" with value %s is expected to be of type array, but is of type "%s".', $this->formatOptions([$option]), $this->formatValue($value), get_debug_type($value))); + } + + // The following section must be protected from cyclic calls. + $this->calling[$option] = true; + try { + $resolver = new self(); + $resolver->parentsOptions = $this->parentsOptions; + $resolver->parentsOptions[] = $option; + foreach ($this->nested[$option] as $closure) { + $closure($resolver, $this); + } + $value = $resolver->resolve($value); + } finally { + unset($this->calling[$option]); + } + } + + // Resolve the option if the default value is lazily evaluated + if (isset($this->lazy[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + foreach ($this->lazy[$option] as $closure) { + $value = $closure($this, $value); + } + } finally { + unset($this->calling[$option]); + } + // END + } + + // Validate the type of the resolved option + if (isset($this->allowedTypes[$option])) { + $valid = true; + $invalidTypes = []; + + foreach ($this->allowedTypes[$option] as $type) { + if ($valid = $this->verifyTypes($type, $value, $invalidTypes)) { + break; + } + } + + if (!$valid) { + $fmtActualValue = $this->formatValue($value); + $fmtAllowedTypes = implode('" or "', $this->allowedTypes[$option]); + $fmtProvidedTypes = implode('|', array_keys($invalidTypes)); + $allowedContainsArrayType = \count(array_filter($this->allowedTypes[$option], static function ($item) { + return '[]' === substr($item, -2); + })) > 0; + + if (\is_array($value) && $allowedContainsArrayType) { + throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but one of the elements is of type "%s".', $this->formatOptions([$option]), $fmtActualValue, $fmtAllowedTypes, $fmtProvidedTypes)); + } + + throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but is of type "%s".', $this->formatOptions([$option]), $fmtActualValue, $fmtAllowedTypes, $fmtProvidedTypes)); + } + } + + // Validate the value of the resolved option + if (isset($this->allowedValues[$option])) { + $success = false; + $printableAllowedValues = []; + + foreach ($this->allowedValues[$option] as $allowedValue) { + if ($allowedValue instanceof \Closure) { + if ($allowedValue($value)) { + $success = true; + break; + } + + // Don't include closures in the exception message + continue; + } + + if ($value === $allowedValue) { + $success = true; + break; + } + + $printableAllowedValues[] = $allowedValue; + } + + if (!$success) { + $message = sprintf( + 'The option "%s" with value %s is invalid.', + $option, + $this->formatValue($value) + ); + + if (\count($printableAllowedValues) > 0) { + $message .= sprintf( + ' Accepted values are: %s.', + $this->formatValues($printableAllowedValues) + ); + } + + if (isset($this->info[$option])) { + $message .= sprintf(' Info: %s.', $this->info[$option]); + } + + throw new InvalidOptionsException($message); + } + } + + // Check whether the option is deprecated + // and it is provided by the user or is being called from a lazy evaluation + if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || ($this->calling && \is_string($this->deprecated[$option]['message'])))) { + $deprecation = $this->deprecated[$option]; + $message = $this->deprecated[$option]['message']; + + if ($message instanceof \Closure) { + // If the closure is already being called, we have a cyclic dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + $this->calling[$option] = true; + try { + if (!\is_string($message = $message($this, $value))) { + throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", return an empty string to ignore.', get_debug_type($message))); + } + } finally { + unset($this->calling[$option]); + } + } + + if ('' !== $message) { + trigger_deprecation($deprecation['package'], $deprecation['version'], strtr($message, ['%name%' => $option])); + } + } + + // Normalize the validated option + if (isset($this->normalizers[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); + } + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + foreach ($this->normalizers[$option] as $normalizer) { + $value = $normalizer($this, $value); + } + } finally { + unset($this->calling[$option]); + } + // END + } + + // Mark as resolved + $this->resolved[$option] = $value; + + return $value; + } + + private function verifyTypes(string $type, $value, array &$invalidTypes, int $level = 0): bool + { + if (\is_array($value) && '[]' === substr($type, -2)) { + $type = substr($type, 0, -2); + $valid = true; + + foreach ($value as $val) { + if (!$this->verifyTypes($type, $val, $invalidTypes, $level + 1)) { + $valid = false; + } + } + + return $valid; + } + + if (('null' === $type && null === $value) || (isset(self::VALIDATION_FUNCTIONS[$type]) ? self::VALIDATION_FUNCTIONS[$type]($value) : $value instanceof $type)) { + return true; + } + + if (!$invalidTypes || $level > 0) { + $invalidTypes[get_debug_type($value)] = true; + } + + return false; + } + + /** + * Returns whether a resolved option with the given name exists. + * + * @param string $option The option name + * + * @return bool Whether the option is set + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \ArrayAccess::offsetExists() + */ + public function offsetExists($option) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + return \array_key_exists($option, $this->defaults); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetSet($option, $value) + { + throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetUnset($option) + { + throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); + } + + /** + * Returns the number of set options. + * + * This may be only a subset of the defined options. + * + * @return int Number of options + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \Countable::count() + */ + public function count() + { + if (!$this->locked) { + throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); + } + + return \count($this->defaults); + } + + /** + * Returns a string representation of the value. + * + * This method returns the equivalent PHP tokens for most scalar types + * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped + * in double quotes ("). + * + * @param mixed $value The value to format as string + */ + private function formatValue($value): string + { + if (\is_object($value)) { + return \get_class($value); + } + + if (\is_array($value)) { + return 'array'; + } + + if (\is_string($value)) { + return '"'.$value.'"'; + } + + if (\is_resource($value)) { + return 'resource'; + } + + if (null === $value) { + return 'null'; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + return (string) $value; + } + + /** + * Returns a string representation of a list of values. + * + * Each of the values is converted to a string using + * {@link formatValue()}. The values are then concatenated with commas. + * + * @see formatValue() + */ + private function formatValues(array $values): string + { + foreach ($values as $key => $value) { + $values[$key] = $this->formatValue($value); + } + + return implode(', ', $values); + } + + private function formatOptions(array $options): string + { + if ($this->parentsOptions) { + $prefix = array_shift($this->parentsOptions); + if ($this->parentsOptions) { + $prefix .= sprintf('[%s]', implode('][', $this->parentsOptions)); + } + + $options = array_map(static function (string $option) use ($prefix): string { + return sprintf('%s[%s]', $prefix, $option); + }, $options); + } + + return implode('", "', $options); + } + + private function getParameterClassName(\ReflectionParameter $parameter): ?string + { + if (!($type = $parameter->getType()) instanceof \ReflectionNamedType || $type->isBuiltin()) { + return null; + } + + return $type->getName(); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/README.md b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/README.md new file mode 100644 index 0000000..245e69b --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/README.md @@ -0,0 +1,15 @@ +OptionsResolver Component +========================= + +The OptionsResolver component is `array_replace` on steroids. It allows you to +create an options system with required options, defaults, validation (type, +value), normalization and more. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/options_resolver.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/composer.json b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/composer.json new file mode 100644 index 0000000..7679819 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/options-resolver/composer.json @@ -0,0 +1,31 @@ +{ + "name": "symfony/options-resolver", + "type": "library", + "description": "Provides an improved replacement for the array_replace PHP function", + "keywords": ["options", "config", "configuration"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php73": "~1.0", + "symfony/polyfill-php80": "^1.15" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/LICENSE new file mode 100644 index 0000000..3f853aa --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/Php73.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/Php73.php new file mode 100644 index 0000000..65c35a6 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/Php73.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php73; + +/** + * @author Gabriel Caruso + * @author Ion Bazan + * + * @internal + */ +final class Php73 +{ + public static $startAt = 1533462603; + + /** + * @param bool $asNum + * + * @return array|float|int + */ + public static function hrtime($asNum = false) + { + $ns = microtime(false); + $s = substr($ns, 11) - self::$startAt; + $ns = 1E9 * (float) $ns; + + if ($asNum) { + $ns += $s * 1E9; + + return \PHP_INT_SIZE === 4 ? $ns : (int) $ns; + } + + return [$s, (int) $ns]; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/README.md b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/README.md new file mode 100644 index 0000000..b3ebbce --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/README.md @@ -0,0 +1,18 @@ +Symfony Polyfill / Php73 +======================== + +This component provides functions added to PHP 7.3 core: + +- [`array_key_first`](https://php.net/array_key_first) +- [`array_key_last`](https://php.net/array_key_last) +- [`hrtime`](https://php.net/function.hrtime) +- [`is_countable`](https://php.net/is_countable) +- [`JsonException`](https://php.net/JsonException) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php new file mode 100644 index 0000000..673d100 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class JsonException extends Exception +{ +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/bootstrap.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/bootstrap.php new file mode 100644 index 0000000..d6b2153 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/bootstrap.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php73 as p; + +if (\PHP_VERSION_ID >= 70300) { + return; +} + +if (!function_exists('is_countable')) { + function is_countable($value) { return is_array($value) || $value instanceof Countable || $value instanceof ResourceBundle || $value instanceof SimpleXmlElement; } +} +if (!function_exists('hrtime')) { + require_once __DIR__.'/Php73.php'; + p\Php73::$startAt = (int) microtime(true); + function hrtime($as_number = false) { return p\Php73::hrtime($as_number); } +} +if (!function_exists('array_key_first')) { + function array_key_first(array $array) { foreach ($array as $key => $value) { return $key; } } +} +if (!function_exists('array_key_last')) { + function array_key_last(array $array) { return key(array_slice($array, -1, 1, true)); } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/composer.json b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/composer.json new file mode 100644 index 0000000..8b99ab8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php73/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/polyfill-php73", + "type": "library", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php73\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/LICENSE new file mode 100644 index 0000000..5593b1d --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Php80.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Php80.php new file mode 100644 index 0000000..5fef511 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Php80.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Ion Bazan + * @author Nico Oelgart + * @author Nicolas Grekas + * + * @internal + */ +final class Php80 +{ + public static function fdiv(float $dividend, float $divisor): float + { + return @($dividend / $divisor); + } + + public static function get_debug_type($value): string + { + switch (true) { + case null === $value: return 'null'; + case \is_bool($value): return 'bool'; + case \is_string($value): return 'string'; + case \is_array($value): return 'array'; + case \is_int($value): return 'int'; + case \is_float($value): return 'float'; + case \is_object($value): break; + case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; + default: + if (null === $type = @get_resource_type($value)) { + return 'unknown'; + } + + if ('Unknown' === $type) { + $type = 'closed'; + } + + return "resource ($type)"; + } + + $class = \get_class($value); + + if (false === strpos($class, '@')) { + return $class; + } + + return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; + } + + public static function get_resource_id($res): int + { + if (!\is_resource($res) && null === @get_resource_type($res)) { + throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); + } + + return (int) $res; + } + + public static function preg_last_error_msg(): string + { + switch (preg_last_error()) { + case \PREG_INTERNAL_ERROR: + return 'Internal error'; + case \PREG_BAD_UTF8_ERROR: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + case \PREG_BAD_UTF8_OFFSET_ERROR: + return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; + case \PREG_BACKTRACK_LIMIT_ERROR: + return 'Backtrack limit exhausted'; + case \PREG_RECURSION_LIMIT_ERROR: + return 'Recursion limit exhausted'; + case \PREG_JIT_STACKLIMIT_ERROR: + return 'JIT stack limit exhausted'; + case \PREG_NO_ERROR: + return 'No error'; + default: + return 'Unknown error'; + } + } + + public static function str_contains(string $haystack, string $needle): bool + { + return '' === $needle || false !== strpos($haystack, $needle); + } + + public static function str_starts_with(string $haystack, string $needle): bool + { + return 0 === strncmp($haystack, $needle, \strlen($needle)); + } + + public static function str_ends_with(string $haystack, string $needle): bool + { + return '' === $needle || ('' !== $haystack && 0 === substr_compare($haystack, $needle, -\strlen($needle))); + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/README.md b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/README.md new file mode 100644 index 0000000..eaa3050 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/README.md @@ -0,0 +1,24 @@ +Symfony Polyfill / Php80 +======================== + +This component provides features added to PHP 8.0 core: + +- `Stringable` interface +- [`fdiv`](https://php.net/fdiv) +- `ValueError` class +- `UnhandledMatchError` class +- `FILTER_VALIDATE_BOOL` constant +- [`get_debug_type`](https://php.net/get_debug_type) +- [`preg_last_error_msg`](https://php.net/preg_last_error_msg) +- [`str_contains`](https://php.net/str_contains) +- [`str_starts_with`](https://php.net/str_starts_with) +- [`str_ends_with`](https://php.net/str_ends_with) +- [`get_resource_id`](https://php.net/get_resource_id) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php new file mode 100644 index 0000000..7ea6d27 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php @@ -0,0 +1,22 @@ +flags = $flags; + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php new file mode 100644 index 0000000..77e037c --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php @@ -0,0 +1,11 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php80 as p; + +if (\PHP_VERSION_ID >= 80000) { + return; +} + +if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { + define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); +} + +if (!function_exists('fdiv')) { + function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } +} +if (!function_exists('preg_last_error_msg')) { + function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } +} +if (!function_exists('str_contains')) { + function str_contains(string $haystack, string $needle): bool { return p\Php80::str_contains($haystack, $needle); } +} +if (!function_exists('str_starts_with')) { + function str_starts_with(string $haystack, string $needle): bool { return p\Php80::str_starts_with($haystack, $needle); } +} +if (!function_exists('str_ends_with')) { + function str_ends_with(string $haystack, string $needle): bool { return p\Php80::str_ends_with($haystack, $needle); } +} +if (!function_exists('get_debug_type')) { + function get_debug_type($value): string { return p\Php80::get_debug_type($value); } +} +if (!function_exists('get_resource_id')) { + function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/composer.json b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/composer.json new file mode 100644 index 0000000..a9e6813 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/symfony/polyfill-php80/composer.json @@ -0,0 +1,40 @@ +{ + "name": "symfony/polyfill-php80", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/.gitignore b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/.gitignore new file mode 100644 index 0000000..8228c84 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/.gitignore @@ -0,0 +1,4 @@ +.idea +.tmp +/composer.lock +vendor diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/LICENSE b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/README.md b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/README.md new file mode 100644 index 0000000..1398e93 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/README.md @@ -0,0 +1,55 @@ +# Typesense PHP Client + +Official PHP client for the Typesense API: https://github.com/typesense/typesense + +## Installation + +``` +$ composer require typesense/typesense-php +``` + +You can also add `typesense/typesense-php` to your project's `composer.json`. + +## Usage + +Read the documentation here: [https://typesense.org/api/](https://typesense.org/api/) + +Here are some examples that walk you through how to use the client: [doc/examples](examples) + +Typesense uses [HTTPlug](http://httplug.io/) as an abstraction layer for an HTTP client. You'll find the List of supported HTTP clients & adapters [here](http://docs.php-http.org/en/latest/clients.html). Please be sure to install a supported client. + +## Compatibility + +| Typesense Server | typesense-php | +|------------------|----------------| +| \>= v0.19.0 | \>= v4.5.0 | +| \>= v0.18.0 | \>= v4.4.0 | +| \>= v0.17.0 | \>= v4.2.0 | +| \>= v0.16.0 | \>= v4.1.0 | +| \>= v0.15.0 | \>= v4.0.0 | + +## Contributing + +Bug reports and pull requests are welcome on GitHub at [https://github.com/typesense/typesense-php]. + +## Development + +Run linter: + +```shell script +composer run-script lint:fix +``` + +Run Typesense Server: + +```shell script +composer run-script typesenseServer +``` + +## Credits + +This client was originally developed by [Abdullah Al-Faqeir](https://github.org/abdullahfaqeir) from +[DevLoops](https://github.com/devloopsnet) and was +[adopted](https://github.com/devloopsnet/typesense-php/issues/4) as the official PHP client library for Typesense in Oct 2020. + +Ongoing development and support is now provided by Typesense, in addition to our collaborators. diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/composer.json b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/composer.json new file mode 100644 index 0000000..deaa50a --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/composer.json @@ -0,0 +1,63 @@ +{ + "name": "typesense/typesense-php", + "description": "PHP client for Typesense Search Server: https://github.com/typesense/typesense", + "type": "library", + "homepage": "https://github.com/typesense/typesense-php", + "license": "Apache-2.0", + "authors": [ + { + "name": "Typesense", + "email": "contact@typesense.org", + "homepage": "https://typesense.org", + "role": "Developer" + }, + { + "name": "Abdullah Al-Faqeir", + "email": "abdullah@devloops.net", + "homepage": "https://www.devloops.net", + "role": "Developer" + } + ], + "support": { + "docs": "https://typesense.org/api", + "source": "https://github.com/typesense/typesense-php", + "issues": "https://github.com/typesense/typesense-php/issues" + }, + "minimum-stability": "stable", + "autoload": { + "psr-4": { + "Typesense\\": "src/" + } + }, + "require": { + "php": ">=7.4", + "ext-json": "*", + "monolog/monolog": "^1.0|^2.1", + "nyholm/psr7": "^1.3", + "php-http/client-common": "^1.0|^2.3", + "php-http/discovery": "^1.0", + "php-http/httplug": "^1.0|^2.2", + "php-http/message-factory": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "squizlabs/php_codesniffer": "3.*", + "symfony/http-client": "^5.2" + }, + "config": { + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "scripts": { + "typesenseServer": [ + "Composer\\Config::disableProcessTimeout", + "docker-compose up" + ], + "lint": "phpcs -v", + "lint:fix": "phpcbf" + } +} diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/docker-compose.yml b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/docker-compose.yml new file mode 100644 index 0000000..8707ece --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.5' + +services: + typesense: + image: typesense/typesense:0.19.0.rc18 + environment: + TYPESENSE_DATA_DIR: /data + TYPESENSE_API_KEY: xyz + volumes: + - /tmp/typesense-server-data:/data + ports: + - 8108:8108 + restart: "no" diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/README.md b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/README.md new file mode 100644 index 0000000..91e8743 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/README.md @@ -0,0 +1,14 @@ +### Running the examples + +Start a local typesense server via docker: + +```shell script +composer run-script typesenseServer +``` + +Then: + +```shell script +cd examples +php .php +``` \ No newline at end of file diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/alias_operations.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/alias_operations.php new file mode 100644 index 0000000..47f51e8 --- /dev/null +++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/alias_operations.php @@ -0,0 +1,142 @@ + 'xyz', + 'nodes' => [ + [ + 'host' => 'localhost', + 'port' => '8108', + 'protocol' => 'http', + ], + ], + 'client' => new HttplugClient(), + ] + ); + echo '
';
+    try {
+        print_r($client->aliases['books']->delete());
+    } catch (Exception $e) {
+        // Don't error out if the collection was not found
+    }
+    try {
+        print_r($client->collections['books_january']->delete());
+    } catch (Exception $e) {
+        // Don't error out if the collection was not found
+    }
+    echo "--------Create Collection-------\n";
+    print_r(
+        $client->collections->create(
+            [
+                'name' => 'books_january',
+                'fields' => [
+                    [
+                        'name' => 'title',
+                        'type' => 'string',
+                    ],
+                    [
+                        'name' => 'authors',
+                        'type' => 'string[]',
+                    ],
+                    [
+                        'name' => 'authors_facet',
+                        'type' => 'string[]',
+                        'facet' => true,
+                    ],
+                    [
+                        'name' => 'publication_year',
+                        'type' => 'int32',
+                    ],
+                    [
+                        'name' => 'publication_year_facet',
+                        'type' => 'string',
+                        'facet' => true,
+                    ],
+                    [
+                        'name' => 'ratings_count',
+                        'type' => 'int32',
+                    ],
+                    [
+                        'name' => 'average_rating',
+                        'type' => 'float',
+                    ],
+                    [
+                        'name' => 'image_url',
+                        'type' => 'string',
+                    ],
+                ],
+                'default_sorting_field' => 'ratings_count',
+            ]
+        )
+    );
+    echo "--------Create Collection-------\n";
+    echo "\n";
+    echo "--------Create Collection Alias-------\n";
+    print_r(
+        $client->aliases->upsert(
+            'books',
+            [
+                'collection_name' => 'books_january',
+            ]
+        )
+    );
+    echo "--------Create Collection Alias-------\n";
+    echo "\n";
+    echo "--------Create Document on Alias-------\n";
+    print_r(
+        $client->collections['books']->documents->create(
+            [
+                'id' => '1',
+                'original_publication_year' => 2008,
+                'authors' => [
+                    'Suzanne Collins',
+                ],
+                'average_rating' => 4.34,
+                'publication_year' => 2008,
+                'publication_year_facet' => '2008',
+                'authors_facet' => [
+                    'Suzanne Collins',
+                ],
+                'title' => 'The Hunger Games',
+                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
+                'ratings_count' => 4780653,
+            ]
+        )
+    );
+    echo "--------Create Document on Alias-------\n";
+    echo "\n";
+    echo "--------Search Document on Alias-------\n";
+    print_r(
+        $client->collections['books']->documents->search(
+            [
+                'q' => 'hunger',
+                'query_by' => 'title',
+                'sort_by' => 'ratings_count:desc',
+            ]
+        )
+    );
+    echo "--------Search Document on Alias-------\n";
+    echo "\n";
+    echo "--------Retrieve All Aliases-------\n";
+    print_r($client->aliases->retrieve());
+    echo "--------Retrieve All Aliases-------\n";
+    echo "\n";
+    echo "--------Retrieve All Alias Documents-------\n";
+    print_r($client->aliases['books']->retrieve());
+    echo "--------Retrieve All Alias Documents-------\n";
+    echo "\n";
+    echo "--------Delete Alias-------\n";
+    print_r($client->aliases['books']->delete());
+    echo "--------Delete Alias-------\n";
+    echo "\n";
+} catch (Exception $e) {
+    echo $e->getMessage();
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/cluster_operations.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/cluster_operations.php
new file mode 100644
index 0000000..9889350
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/cluster_operations.php
@@ -0,0 +1,29 @@
+ 'xyz',
+            'nodes' => [
+                [
+                    'host' => 'localhost',
+                    'port' => '8108',
+                    'protocol' => 'http',
+                ],
+            ],
+            'client' => new HttplugClient(),
+        ]
+    );
+    echo '
';
+
+    print_r($client->operations->perform('snapshot', ['snapshot_path' => '/tmp/snapshot']));
+} catch (Exception $e) {
+    echo $e->getMessage();
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/collection_operations.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/collection_operations.php
new file mode 100644
index 0000000..3a2b1ee
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/collection_operations.php
@@ -0,0 +1,229 @@
+ 'xyz',
+            'nodes' => [
+                [
+                    'host' => 'localhost',
+                    'port' => '8108',
+                    'protocol' => 'http',
+                ],
+            ],
+            'client' => new HttplugClient(),
+        ]
+    );
+    echo '
';
+
+    try {
+        print_r($client->collections['books']->delete());
+    } catch (Exception $e) {
+        // Don't error out if the collection was not found
+    }
+
+    echo "--------Create Collection-------\n";
+    print_r(
+        $client->collections->create(
+            [
+                'name' => 'books',
+                'fields' => [
+                    [
+                        'name' => 'title',
+                        'type' => 'string',
+                    ],
+                    [
+                        'name' => 'authors',
+                        'type' => 'string[]',
+                    ],
+                    [
+                        'name' => 'authors_facet',
+                        'type' => 'string[]',
+                        'facet' => true,
+                    ],
+                    [
+                        'name' => 'publication_year',
+                        'type' => 'int32',
+                    ],
+                    [
+                        'name' => 'publication_year_facet',
+                        'type' => 'string',
+                        'facet' => true,
+                    ],
+                    [
+                        'name' => 'ratings_count',
+                        'type' => 'int32',
+                    ],
+                    [
+                        'name' => 'average_rating',
+                        'type' => 'float',
+                    ],
+                    [
+                        'name' => 'image_url',
+                        'type' => 'string',
+                    ],
+                ],
+                'default_sorting_field' => 'ratings_count',
+            ]
+        )
+    );
+    echo "--------Create Collection-------\n";
+    echo "\n";
+    echo "--------Retrieve Collection-------\n";
+    print_r($client->collections['books']->retrieve());
+    echo "--------Retrieve Collection-------\n";
+    echo "\n";
+    echo "--------Retrieve All Collections-------\n";
+    print_r($client->collections->retrieve());
+    echo "--------Retrieve All Collections-------\n";
+    echo "\n";
+    echo "--------Create Document-------\n";
+    print_r(
+        $client->collections['books']->documents->create(
+            [
+                'id' => '1',
+                'original_publication_year' => 2008,
+                'authors' => [
+                    'Suzanne Collins',
+                ],
+                'average_rating' => 4.34,
+                'publication_year' => 2008,
+                'publication_year_facet' => '2008',
+                'authors_facet' => [
+                    'Suzanne Collins',
+                ],
+                'title' => 'The Hunger Games',
+                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
+                'ratings_count' => 4780653,
+            ]
+        )
+    );
+    echo "--------Create Document-------\n";
+    echo "\n";
+
+    echo "--------Upsert Document-------\n";
+    print_r(
+        $client->collections['books']->documents->upsert(
+            [
+                'id' => '1',
+                'original_publication_year' => 2008,
+                'authors' => [
+                    'Suzanne Collins',
+                ],
+                'average_rating' => 4.6,
+                'publication_year' => 2008,
+                'publication_year_facet' => '2008',
+                'authors_facet' => [
+                    'Suzanne Collins',
+                ],
+                'title' => 'The Hunger Games',
+                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
+                'ratings_count' => 4780653,
+            ]
+        )
+    );
+    echo "--------Upsert Document-------\n";
+    echo "\n";
+
+    echo "--------Export Documents-------\n";
+    $exportedDocStrs = $client->collections['books']->documents->export();
+    print_r($exportedDocStrs);
+    echo "--------Export Documents-------\n";
+    echo "\n";
+    echo "--------Update Single Document-------\n";
+    print_r($client->collections['books']->documents['1']->update([
+        'average_rating' => 4.5,
+    ]));
+    echo "--------Update Single Document-------\n";
+    echo "\n";
+    echo "--------Fetch Single Document-------\n";
+    print_r($client->collections['books']->documents['1']->retrieve());
+    echo "--------Fetch Single Document-------\n";
+    echo "\n";
+    echo "--------Search Document-------\n";
+    print_r(
+        $client->collections['books']->documents->search(
+            [
+                'q' => 'hunger',
+                'query_by' => 'title',
+                'sort_by' => 'ratings_count:desc',
+            ]
+        )
+    );
+    echo "--------Search Document-------\n";
+    echo "\n";
+    echo "--------Multi search-------\n";
+    print_r(
+        $client->multiSearch->perform(
+            [
+                'searches' => [
+                    [
+                        'q' => 'hunger',
+                        'sort_by' => 'ratings_count:desc',
+                    ],
+                    [
+                        'q' => 'game',
+                        'sort_by' => 'ratings_count:asc',
+                    ]
+                ]
+            ],
+            [
+                'query_by' => 'title',
+                'collection' => 'books'
+            ]
+        )
+    );
+    echo "--------Multi Search-------\n";
+    echo "\n";
+    echo "--------Delete Document-------\n";
+    print_r($client->collections['books']->documents['1']->delete());
+    echo "--------Delete Document-------\n";
+    echo "\n";
+    echo "--------Import Documents-------\n";
+    $docsToImport = [];
+    $exportedDocStrsArray = explode('\n', $exportedDocStrs);
+    foreach ($exportedDocStrsArray as $exportedDocStr) {
+        $docsToImport[] = json_decode($exportedDocStr, true);
+    }
+    $importRes =
+        $client->collections['books']->documents->import($docsToImport);
+    print_r($importRes);
+
+    // Or if you have documents in JSONL format, and want to save the overhead of parsing JSON,
+    // you can also pass in a JSONL string of documents
+    // $client->collections['books']->documents->import($exportedDocStrsArray);
+    echo "--------Import Documents-------\n";
+    echo "\n";
+    echo "--------Upsert Documents-------\n";
+    $upsertRes =
+        $client->collections['books']->documents->import($docsToImport, [
+            'action' => 'upsert'
+        ]);
+    print_r($upsertRes);
+    echo "--------Upsert Documents-------\n";
+    echo "\n";
+    echo "--------Update Documents-------\n";
+    $upsertRes =
+        $client->collections['books']->documents->import($docsToImport, [
+            'action' => 'update'
+        ]);
+    print_r($upsertRes);
+    echo "--------Upsert Documents-------\n";
+    echo "\n";
+    echo "--------Bulk Delete Documents-------\n";
+    print_r($client->collections['books']->documents->delete(['filter_by' => 'publication_year:=2008']));
+    echo "--------Bulk Delete Documents-------\n";
+    echo "\n";
+    echo "--------Delete Collection-------\n";
+    print_r($client->collections['books']->delete());
+    echo "--------Delete Collection-------\n";
+} catch (Exception $e) {
+    echo $e->getMessage();
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/curation_operations.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/curation_operations.php
new file mode 100644
index 0000000..13c3874
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/curation_operations.php
@@ -0,0 +1,148 @@
+ 'xyz',
+            'nodes' => [
+                [
+                    'host' => 'localhost',
+                    'port' => '8108',
+                    'protocol' => 'http',
+                ],
+            ],
+            'client' => new HttplugClient(),
+        ]
+    );
+    echo '
';
+    try {
+        print_r($client->collections['books']->delete());
+    } catch (Exception $e) {
+        // Don't error out if the collection was not found
+    }
+    echo "--------Create Collection-------\n";
+    print_r(
+        $client->collections->create(
+            [
+                'name' => 'books',
+                'fields' => [
+                    [
+                        'name' => 'title',
+                        'type' => 'string',
+                    ],
+                    [
+                        'name' => 'authors',
+                        'type' => 'string[]',
+                    ],
+                    [
+                        'name' => 'authors_facet',
+                        'type' => 'string[]',
+                        'facet' => true,
+                    ],
+                    [
+                        'name' => 'publication_year',
+                        'type' => 'int32',
+                    ],
+                    [
+                        'name' => 'publication_year_facet',
+                        'type' => 'string',
+                        'facet' => true,
+                    ],
+                    [
+                        'name' => 'ratings_count',
+                        'type' => 'int32',
+                    ],
+                    [
+                        'name' => 'average_rating',
+                        'type' => 'float',
+                    ],
+                    [
+                        'name' => 'image_url',
+                        'type' => 'string',
+                    ],
+                ],
+                'default_sorting_field' => 'ratings_count',
+            ]
+        )
+    );
+    echo "--------Create Collection-------\n";
+    echo "\n";
+    echo "--------Create or Update Override-------\n";
+    print_r(
+        $client->collections['books']->overrides->upsert(
+            'hermione-exact',
+            [
+                'rule' => [
+                    'query' => 'hermione',
+                    'match' => 'exact',
+                ],
+                'includes' => [
+                    [
+                        'id' => '1',
+                        'position' => 1,
+                    ],
+                ],
+            ]
+        )
+    );
+    echo "--------Create or Update Override-------\n";
+    echo "\n";
+    echo "--------Get All Overrides-------\n";
+    print_r($client->collections['books']->overrides->retrieve());
+    echo "--------Get All Overrides-------\n";
+    echo "\n";
+    echo "--------Get Single Override-------\n";
+    print_r(
+        $client->collections['books']->overrides['hermione-exact']->retrieve()
+    );
+    echo "--------Get Single Override-------\n";
+    echo "\n";
+    echo "--------Create Document-------\n";
+    print_r(
+        $client->collections['books']->documents->create(
+            [
+                'id' => '1',
+                'original_publication_year' => 2008,
+                'authors' => [
+                    'Suzanne Collins',
+                ],
+                'average_rating' => 4.34,
+                'publication_year' => 2008,
+                'publication_year_facet' => '2008',
+                'authors_facet' => [
+                    'Suzanne Collins',
+                ],
+                'title' => 'The Hunger Games',
+                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
+                'ratings_count' => 4780653,
+            ]
+        )
+    );
+    echo "--------Create Document-------\n";
+    echo "\n";
+    echo "--------Search Document-------\n";
+    print_r(
+        $client->collections['books']->documents->search(
+            [
+                'q' => 'hermione',
+                'query_by' => 'title',
+                'sort_by' => 'ratings_count:desc',
+            ]
+        )
+    );
+    echo "--------Search Document-------\n";
+    echo "\n";
+    echo "--------Delete Override-------\n";
+    print_r(
+        $client->collections['books']->getOverrides()['hermione-exact']->delete()
+    );
+    echo "--------Delete Override-------\n";
+    echo "\n";
+} catch (Exception $e) {
+    echo $e->getMessage();
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/info_operations.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/info_operations.php
new file mode 100644
index 0000000..ba00a3e
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/info_operations.php
@@ -0,0 +1,31 @@
+ 'xyz',
+            'nodes' => [
+                [
+                    'host' => 'localhost',
+                    'port' => '8108',
+                    'protocol' => 'http',
+                ],
+            ],
+            'client' => new HttplugClient(),
+        ]
+    );
+    echo '
';
+
+    print_r($client->debug->retrieve());
+    print_r($client->metrics->retrieve());
+    print_r($client->health->retrieve());
+} catch (Exception $e) {
+    echo $e->getMessage();
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/keys_operations.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/keys_operations.php
new file mode 100644
index 0000000..b4dcc9c
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/keys_operations.php
@@ -0,0 +1,160 @@
+ 'xyz',
+            'nodes' => [
+                [
+                    'host' => 'localhost',
+                    'port' => '8108',
+                    'protocol' => 'http',
+                ],
+            ],
+            'client' => new HttplugClient(),
+        ]
+    );
+    echo '
';
+    try {
+        print_r($client->collections['users']->delete());
+    } catch (Exception $e) {
+        // Don't error out if the collection was not found
+    }
+    echo "--------Create Collection-------\n";
+    print_r(
+        $client->collections->create(
+            [
+                'name' => 'users',
+                'fields' => [
+                    [
+                        'name' => 'company_id',
+                        'type' => 'int32',
+                        'facet' => false
+                    ],
+                    [
+                        'name' => 'user_name',
+                        'type' => 'string',
+                        'facet' => false
+                    ],
+                    [
+                        'name' => 'login_count',
+                        'type' => 'int32',
+                        'facet' => false
+                    ],
+                    [
+                        'name' => 'country',
+                        'type' => 'string',
+                        'facet' => true
+                    ]
+                ],
+                'default_sorting_field' => 'company_id'
+            ]
+        )
+    );
+    echo "--------Create Collection-------\n";
+    echo "\n";
+    echo "--------Create Documents-------\n";
+    print_r(
+        $client->collections['users']->documents->createMany([
+            [
+                'company_id' => 124,
+                'user_name' => 'Hilary Bradford',
+                'login_count' => 10,
+                'country' => 'USA'
+            ],
+            [
+                'company_id' => 124,
+                'user_name' => 'Nile Carty',
+                'login_count' => 100,
+                'country' => 'USA'
+            ],
+            [
+                'company_id' => 126,
+                'user_name' => 'Tahlia Maxwell',
+                'login_count' => 1,
+                'country' => 'France'
+            ],
+            [
+                'company_id' => 126,
+                'user_name' => 'Karl Roy',
+                'login_count' => 2,
+                'country' => 'Germany'
+            ]
+        ])
+    );
+    echo "--------Create Documents-------\n";
+    echo "\n";
+    echo "--------Create a search only API key-------\n";
+    $searchOnlyApiKeyResponse = $client->keys->create([
+        'description' => 'Search-only key.',
+        'actions' => ['documents:search'],
+        'collections' => ['*']
+    ]);
+    print_r($searchOnlyApiKeyResponse);
+    echo "--------Create a search only API key-------\n";
+    echo "\n";
+    echo "--------Get All Keys-------\n";
+    print_r($client->keys->retrieve());
+    echo "--------Get All Keys-------\n";
+    echo "\n";
+    echo "--------Get Single Key-------\n";
+    print_r(
+        $client->keys[$searchOnlyApiKeyResponse['id']]->retrieve()
+    );
+    echo "--------Get Single Key-------\n";
+    echo "\n";
+    echo "--------Generate Scoped API Key-------\n";
+    $scopedAPIKey = $client->keys->generateScopedSearchKey($searchOnlyApiKeyResponse['value'], ['filter_by' => 'company_id:124']);
+    print_r($scopedAPIKey);
+    echo "\n";
+    echo "--------Generate Scoped API Key-------\n";
+    echo "\n";
+    echo "--------Search Documents with scoped Key-------\n";
+    $scopedClient = new Client(
+        [
+            'api_key' => $scopedAPIKey,
+            'nodes' => [
+                [
+                    'host' => 'localhost',
+                    'port' => '8108',
+                    'protocol' => 'http',
+                ],
+            ]
+        ]
+    );
+
+    print_r(
+        $scopedClient->collections['users']->documents->search(
+            [
+                'q' => 'Hilary',
+                'query_by' => 'user_name'
+            ]
+        )
+    );
+    echo "--------Search Documents with scoped Key-------\n";
+    echo "\n";
+    echo "--------Search for document outside of scope for scoped Key-------\n";
+    print_r(
+        $scopedClient->collections['users']->documents->search(
+            [
+                'q' => 'Maxwell',
+                'query_by' => 'user_name'
+            ]
+        )
+    );
+    echo "--------Search for document outside of scope for scoped Key-------\n";
+    echo "\n";
+    echo "--------Delete Key-------\n";
+    print_r(
+        $client->keys[$searchOnlyApiKeyResponse['id']]->delete()
+    );
+    echo "--------Delete Key-------\n";
+    echo "\n";
+} catch (Exception $e) {
+    echo $e->getMessage();
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/synonym_operations.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/synonym_operations.php
new file mode 100644
index 0000000..a883e8e
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/examples/synonym_operations.php
@@ -0,0 +1,150 @@
+ 'xyz',
+            'nodes' => [
+                [
+                    'host' => 'localhost',
+                    'port' => '8108',
+                    'protocol' => 'http',
+                ],
+            ],
+            'client' => new HttplugClient(),
+        ]
+    );
+    echo '
';
+    try {
+        print_r($client->collections['books']->delete());
+    } catch (Exception $e) {
+        // Don't error out if the collection was not found
+    }
+    echo "--------Create Collection-------\n";
+    print_r(
+        $client->collections->create(
+            [
+                'name' => 'books',
+                'fields' => [
+                    [
+                        'name' => 'title',
+                        'type' => 'string',
+                    ],
+                    [
+                        'name' => 'authors',
+                        'type' => 'string[]',
+                        'facet' => true
+                    ],
+                    [
+                        'name' => 'publication_year',
+                        'type' => 'int32',
+                        'facet' => true,
+                    ],
+                    [
+                        'name' => 'ratings_count',
+                        'type' => 'int32',
+                    ],
+                    [
+                        'name' => 'average_rating',
+                        'type' => 'float',
+                    ],
+                    [
+                        'name' => 'image_url',
+                        'type' => 'string',
+                    ],
+                ],
+                'default_sorting_field' => 'ratings_count',
+            ]
+        )
+    );
+    echo "--------Create Collection-------\n";
+    echo "\n";
+    echo "--------Upsert Synonym-------\n";
+    print_r(
+        $client->collections['books']->synonyms->upsert(
+            'synonym-set-1',
+            [
+                'synonyms' => ['Hunger', 'Katniss'],
+            ]
+        )
+    );
+    echo "--------Upsert Synonym-------\n";
+    echo "\n";
+    echo "--------Get All Synonyms-------\n";
+    print_r($client->collections['books']->synonyms->retrieve());
+    echo "--------Get All Synonyms-------\n";
+    echo "\n";
+    echo "--------Get Single Synonym-------\n";
+    print_r(
+        $client->collections['books']->synonyms['synonym-set-1']->retrieve()
+    );
+    echo "--------Get Single Synonym-------\n";
+    echo "\n";
+    echo "--------Create Document-------\n";
+    print_r(
+        $client->collections['books']->documents->create(
+            [
+                'id' => '1',
+                'original_publication_year' => 2008,
+                'authors' => [
+                    'Suzanne Collins',
+                ],
+                'average_rating' => 4.34,
+                'publication_year' => 2008,
+                'title' => 'The Hunger Games',
+                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
+                'ratings_count' => 4780653,
+            ]
+        )
+    );
+    echo "--------Create Document-------\n";
+    echo "\n";
+    echo "--------Search Document, using a synonym-------\n";
+    print_r(
+        $client->collections['books']->documents->search(
+            [
+                'q' => 'Katniss',
+                'query_by' => 'title'
+            ]
+        )
+    );
+    echo "--------Search Document, using a synonym-------\n";
+    echo "\n";
+    echo "--------Upsert 1-way synonym-------\n";
+    print_r(
+        $client->collections['books']->synonyms->upsert(
+            'synonym-set-1',
+            [
+                'root' => 'Katniss',
+                'synonyms' => ['Hunger', 'Peeta'],
+            ]
+        )
+    );
+    echo "--------Upsert 1-way synonym-------\n";
+    echo "\n";
+    echo "--------Search Document, using a synonym-------\n";
+    // Won't return any results
+    print_r(
+        $client->collections['books']->documents->search(
+            [
+                'q' => 'Peeta',
+                'query_by' => 'title'
+            ]
+        )
+    );
+    echo "--------Search Document, using a synonym-------\n";
+    echo "\n";
+    echo "--------Delete Synonym-------\n";
+    print_r(
+        $client->collections['books']->getSynonyms()['synonym-set-1']->delete()
+    );
+    echo "--------Delete Synonym-------\n";
+    echo "\n";
+} catch (Exception $e) {
+    echo $e->getMessage();
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/phpcs.xml b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/phpcs.xml
new file mode 100644
index 0000000..e7ff5da
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/phpcs.xml
@@ -0,0 +1,45 @@
+
+
+    The coding standard for PHP_CodeSniffer itself.
+
+    src
+    examples
+
+    */src/Standards/*/Tests/*\.(inc|css|js)$
+    */tests/Core/*/*\.(inc|css|js)$
+
+    
+    
+    
+    
+
+    
+    
+        error
+    
+
+    
+    
+
+    
+    
+        
+            
+            
+        
+    
+
+    
+    
+        
+            
+                
+                
+                
+                
+                
+            
+        
+    
+
\ No newline at end of file
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Alias.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Alias.php
new file mode 100644
index 0000000..5dd7618
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Alias.php
@@ -0,0 +1,65 @@
+
+ */
+class Alias
+{
+
+    /**
+     * @var string
+     */
+    private string $name;
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * Alias constructor.
+     *
+     * @param string $name
+     * @param ApiCall $apiCall
+     */
+    public function __construct(string $name, ApiCall $apiCall)
+    {
+        $this->name    = $name;
+        $this->apiCall = $apiCall;
+    }
+
+    /**
+     * @return string
+     */
+    public function endPointPath(): string
+    {
+        return sprintf('%s/%s', Aliases::RESOURCE_PATH, $this->name);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endPointPath(), []);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function delete(): array
+    {
+        return $this->apiCall->delete($this->endPointPath());
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Aliases.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Aliases.php
new file mode 100644
index 0000000..75190ed
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Aliases.php
@@ -0,0 +1,124 @@
+
+ */
+class Aliases implements \ArrayAccess
+{
+
+    public const RESOURCE_PATH = '/aliases';
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * @var array
+     */
+    private array $aliases = [];
+
+    /**
+     * Aliases constructor.
+     *
+     * @param ApiCall $apiCall
+     */
+    public function __construct(ApiCall $apiCall)
+    {
+        $this->apiCall = $apiCall;
+    }
+
+    /**
+     * @param string $aliasName
+     *
+     * @return string
+     */
+    public function endPointPath(string $aliasName): string
+    {
+        return sprintf('%s/%s', static::RESOURCE_PATH, $aliasName);
+    }
+
+    /**
+     * @param string $name
+     * @param array $mapping
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function upsert(string $name, array $mapping): array
+    {
+        return $this->apiCall->put($this->endPointPath($name), $mapping);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get(static::RESOURCE_PATH, []);
+    }
+
+    /**
+     * @param $name
+     *
+     * @return mixed
+     */
+    public function __get($name)
+    {
+        if (isset($this->{$name})) {
+            return $this->{$name};
+        }
+
+        if (!isset($this->aliases[$name])) {
+            $this->aliases[$name] = new Alias($name, $this->apiCall);
+        }
+
+        return $this->aliases[$name];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetExists($offset): bool
+    {
+        return isset($this->aliases[$offset]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetGet($offset)
+    {
+        if (!isset($this->aliases[$offset])) {
+            $this->aliases[$offset] = new Alias($offset, $this->apiCall);
+        }
+
+        return $this->aliases[$offset];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetSet($offset, $value): void
+    {
+        $this->aliases[$offset] = $value;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetUnset($offset): void
+    {
+        unset($this->aliases[$offset]);
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/ApiCall.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/ApiCall.php
new file mode 100644
index 0000000..28465b3
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/ApiCall.php
@@ -0,0 +1,368 @@
+
+ */
+class ApiCall
+{
+
+    private const API_KEY_HEADER_NAME = 'X-TYPESENSE-API-KEY';
+
+    /**
+     * @var HttpClient
+     */
+    private HttpClient $client;
+
+    /**
+     * @var Configuration
+     */
+    private Configuration $config;
+
+    /**
+     * @var array|Node[]
+     */
+    private static array $nodes;
+
+    /**
+     * @var Node|null
+     */
+    private static ?Node $nearestNode;
+
+    /**
+     * @var int
+     */
+    private int $nodeIndex;
+
+    /**
+     * @var LoggerInterface
+     */
+    public LoggerInterface $logger;
+
+    /**
+     * ApiCall constructor.
+     *
+     * @param Configuration $config
+     */
+    public function __construct(Configuration $config)
+    {
+        $this->config        = $config;
+        $this->logger        = $config->getLogger();
+        $this->client        = $config->getClient();
+        static::$nodes       = $this->config->getNodes();
+        static::$nearestNode = $this->config->getNearestNode();
+        $this->nodeIndex     = 0;
+        $this->initializeNodes();
+    }
+
+    /**
+     *  Initialize Nodes
+     */
+    private function initializeNodes(): void
+    {
+        if (static::$nearestNode !== null) {
+            $this->setNodeHealthCheck(static::$nearestNode, true);
+        }
+
+        foreach (static::$nodes as &$node) {
+            $this->setNodeHealthCheck($node, true);
+        }
+    }
+
+    /**
+     * @param string $endPoint
+     * @param array $params
+     * @param bool $asJson
+     *
+     * @return string|array
+     * @throws TypesenseClientError
+     * @throws Exception|HttpClientException
+     */
+    public function get(string $endPoint, array $params, bool $asJson = true)
+    {
+        return $this->makeRequest('get', $endPoint, $asJson, [
+            'query' => $params ?? [],
+        ]);
+    }
+
+    /**
+     * @param string $endPoint
+     * @param mixed $body
+     *
+     * @param bool $asJson
+     * @param array $queryParameters
+     *
+     * @return array|string
+     * @throws TypesenseClientError
+     * @throws HttpClientException
+     */
+    public function post(string $endPoint, $body, bool $asJson = true, array $queryParameters = [])
+    {
+        return $this->makeRequest('post', $endPoint, $asJson, [
+            'data' => $body ?? [],
+            'query' => $queryParameters ?? []
+        ]);
+    }
+
+    /**
+     * @param string $endPoint
+     * @param array $body
+     *
+     * @param bool $asJson
+     * @param array $queryParameters
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function put(string $endPoint, array $body, bool $asJson = true, array $queryParameters = []): array
+    {
+        return $this->makeRequest('put', $endPoint, $asJson, [
+            'data' => $body ?? [],
+            'query' => $queryParameters ?? []
+        ]);
+    }
+
+    /**
+     * @param string $endPoint
+     * @param array $body
+     *
+     * @param bool $asJson
+     * @param array $queryParameters
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function patch(string $endPoint, array $body, bool $asJson = true, array $queryParameters = []): array
+    {
+        return $this->makeRequest('patch', $endPoint, $asJson, [
+            'data' => $body ?? [],
+            'query' => $queryParameters ?? []
+        ]);
+    }
+
+    /**
+     * @param string $endPoint
+     *
+     * @param bool $asJson
+     * @param array $queryParameters
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function delete(string $endPoint, bool $asJson = true, array $queryParameters = []): array
+    {
+        return $this->makeRequest('delete', $endPoint, $asJson, [
+            'query' => $queryParameters ?? []
+        ]);
+    }
+
+    /**
+     * Makes the actual http request, along with retries
+     *
+     * @param string $method
+     * @param string $endPoint
+     * @param bool $asJson
+     * @param array $options
+     *
+     * @return string|array
+     * @throws TypesenseClientError|HttpClientException
+     * @throws Exception
+     */
+    private function makeRequest(string $method, string $endPoint, bool $asJson, array $options)
+    {
+        $numRetries    = 0;
+        $lastException = null;
+        while ($numRetries < $this->config->getNumRetries() + 1) {
+            $numRetries++;
+            $node = $this->getNode();
+
+            try {
+                $url   = $node->url() . $endPoint;
+                $reqOp = $this->getRequestOptions();
+                if (isset($options['data'])) {
+                    if (is_string($options['data'])) {
+                        $reqOp['body'] = $options['data'];
+                    } else {
+                        $reqOp['body'] = \json_encode($options['data']);
+                    }
+                }
+
+                if (isset($options['query'])) {
+                    foreach ($options['query'] as $key => $value) :
+                        if (is_bool($value)) {
+                            $options['query'][$key] = ($value) ? 'true' : 'false';
+                        }
+                    endforeach;
+                    $reqOp['query'] = http_build_query($options['query']);
+                }
+
+                $response = $this->client->send(
+                    \strtoupper($method),
+                    $url . '?' . ($reqOp['query'] ?? ''),
+                    $reqOp['headers'] ?? [],
+                    $reqOp['body'] ?? null
+                );
+
+                $statusCode = $response->getStatusCode();
+                if (0 < $statusCode && $statusCode < 500) {
+                    $this->setNodeHealthCheck($node, true);
+                }
+
+                if (!(200 <= $statusCode && $statusCode < 300)) {
+                    $errorMessage = json_decode($response->getBody()
+                            ->getContents(), true, 512, JSON_THROW_ON_ERROR)['message'] ?? 'API error.';
+                    throw $this->getException($statusCode)
+                        ->setMessage($errorMessage);
+                }
+
+                return $asJson ? json_decode($response->getBody()
+                    ->getContents(), true, 512, JSON_THROW_ON_ERROR) : $response->getBody()
+                    ->getContents();
+            } catch (HttpException $exception) {
+                if (
+                    $exception->getResponse()
+                        ->getStatusCode() === 408
+                ) {
+                    continue;
+                }
+                $this->setNodeHealthCheck($node, false);
+                throw $this->getException($exception->getResponse()
+                    ->getStatusCode())
+                    ->setMessage($exception->getMessage());
+            } catch (TypesenseClientError | HttpClientException $exception) {
+                $this->setNodeHealthCheck($node, false);
+                throw $exception;
+            } catch (Exception $exception) {
+                $this->setNodeHealthCheck($node, false);
+                $lastException = $exception;
+                sleep($this->config->getRetryIntervalSeconds());
+            }
+        }
+
+        if ($lastException) {
+            throw $lastException;
+        }
+    }
+
+    /**
+     * @return array
+     */
+    private function getRequestOptions(): array
+    {
+        return [
+            'headers' => [
+                static::API_KEY_HEADER_NAME => $this->config->getApiKey(),
+            ]
+        ];
+    }
+
+    /**
+     * @param Node $node
+     *
+     * @return bool
+     */
+    private function nodeDueForHealthCheck(Node $node): bool
+    {
+        $currentTimestamp = time();
+        return ($currentTimestamp - $node->getLastAccessTs()) > $this->config->getHealthCheckIntervalSeconds();
+    }
+
+    /**
+     * @param Node $node
+     * @param bool $isHealthy
+     */
+    public function setNodeHealthCheck(Node $node, bool $isHealthy): void
+    {
+        $node->setHealthy($isHealthy);
+        $node->setLastAccessTs(time());
+    }
+
+    /**
+     * Returns a healthy host from the pool in a round-robin fashion
+     * Might return an unhealthy host periodically to check for recovery.
+     *
+     * @return Node
+     */
+    public function getNode(): Lib\Node
+    {
+        if (static::$nearestNode !== null) {
+            if (static::$nearestNode->isHealthy() || $this->nodeDueForHealthCheck(static::$nearestNode)) {
+                return static::$nearestNode;
+            }
+        }
+        $i = 0;
+        while ($i < count(static::$nodes)) {
+            $i++;
+            $node            = static::$nodes[$this->nodeIndex];
+            $this->nodeIndex = ($this->nodeIndex + 1) % count(static::$nodes);
+            if ($node->isHealthy() || $this->nodeDueForHealthCheck($node)) {
+                return $node;
+            }
+        }
+
+        /**
+         * None of the nodes are marked healthy, but some of them could have become healthy since last health check.
+         * So we will just return the next node.
+         */
+        return static::$nodes[$this->nodeIndex];
+    }
+
+    /**
+     * @param int $httpCode
+     *
+     * @return TypesenseClientError
+     */
+    public function getException(int $httpCode): TypesenseClientError
+    {
+        switch ($httpCode) {
+            case 0:
+                return new HTTPStatus0Error();
+            case 400:
+                return new RequestMalformed();
+            case 401:
+                return new RequestUnauthorized();
+            case 404:
+                return new ObjectNotFound();
+            case 409:
+                return new ObjectAlreadyExists();
+            case 422:
+                return new ObjectUnprocessable();
+            case 500:
+                return new ServerError();
+            case 503:
+                return new ServiceUnavailable();
+            default:
+                return new TypesenseClientError();
+        }
+    }
+
+    /**
+     * @return LoggerInterface
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Client.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Client.php
new file mode 100644
index 0000000..2a300d7
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Client.php
@@ -0,0 +1,153 @@
+
+ */
+class Client
+{
+
+    /**
+     * @var Configuration
+     */
+    private Configuration $config;
+
+    /**
+     * @var Collections
+     */
+    public Collections $collections;
+
+    /**
+     * @var Aliases
+     */
+    public Aliases $aliases;
+
+    /**
+     * @var Keys
+     */
+    public Keys $keys;
+
+    /**
+     * @var Debug
+     */
+    public Debug $debug;
+
+    /**
+     * @var Metrics
+     */
+    public Metrics $metrics;
+
+    /**
+     * @var Health
+     */
+    public Health $health;
+
+    /**
+     * @var Operations
+     */
+    public Operations $operations;
+
+    /**
+     * @var MultiSearch
+     */
+    public MultiSearch $multiSearch;
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * Client constructor.
+     *
+     * @param array $config
+     *
+     * @throws ConfigError
+     */
+    public function __construct(array $config)
+    {
+        $this->config  = new Configuration($config);
+        $this->apiCall = new ApiCall($this->config);
+
+        $this->collections = new Collections($this->apiCall);
+        $this->aliases     = new Aliases($this->apiCall);
+        $this->keys        = new Keys($this->apiCall);
+        $this->debug       = new Debug($this->apiCall);
+        $this->metrics     = new Metrics($this->apiCall);
+        $this->health      = new Health($this->apiCall);
+        $this->operations  = new Operations($this->apiCall);
+        $this->multiSearch = new MultiSearch($this->apiCall);
+    }
+
+    /**
+     * @return Collections
+     */
+    public function getCollections(): Collections
+    {
+        return $this->collections;
+    }
+
+    /**
+     * @return Aliases
+     */
+    public function getAliases(): Aliases
+    {
+        return $this->aliases;
+    }
+
+    /**
+     * @return Keys
+     */
+    public function getKeys(): Keys
+    {
+        return $this->keys;
+    }
+
+    /**
+     * @return Debug
+     */
+    public function getDebug(): Debug
+    {
+        return $this->debug;
+    }
+
+    /**
+     * @return Metrics
+     */
+    public function getMetrics(): Metrics
+    {
+        return $this->metrics;
+    }
+
+    /**
+     * @return Health
+     */
+    public function getHealth(): Health
+    {
+        return $this->health;
+    }
+
+    /**
+     * @return Operations
+     */
+    public function getOperations(): Operations
+    {
+        return $this->operations;
+    }
+
+    /**
+     * @return MultiSearch
+     */
+    public function getMultiSearch(): MultiSearch
+    {
+        return $this->multiSearch;
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Collection.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Collection.php
new file mode 100644
index 0000000..55cc76a
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Collection.php
@@ -0,0 +1,107 @@
+
+ */
+class Collection
+{
+
+    /**
+     * @var string
+     */
+    private string $name;
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * @var Documents
+     */
+    public Documents $documents;
+
+    /**
+     * @var Overrides
+     */
+    public Overrides $overrides;
+
+    /**
+     * @var Synonyms
+     */
+    public Synonyms $synonyms;
+
+    /**
+     * Collection constructor.
+     *
+     * @param string $name
+     * @param ApiCall $apiCall
+     */
+    public function __construct(string $name, ApiCall $apiCall)
+    {
+        $this->name      = $name;
+        $this->apiCall   = $apiCall;
+        $this->documents = new Documents($name, $this->apiCall);
+        $this->overrides = new Overrides($name, $this->apiCall);
+        $this->synonyms  = new Synonyms($name, $this->apiCall);
+    }
+
+    /**
+     * @return string
+     */
+    public function endPointPath(): string
+    {
+        return sprintf('%s/%s', Collections::RESOURCE_PATH, $this->name);
+    }
+
+    /**
+     * @return Documents
+     */
+    public function getDocuments(): Documents
+    {
+        return $this->documents;
+    }
+
+    /**
+     * @return Overrides
+     */
+    public function getOverrides(): Overrides
+    {
+        return $this->overrides;
+    }
+
+    /**
+     * @return Synonyms
+     */
+    public function getSynonyms(): Synonyms
+    {
+        return $this->synonyms;
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endPointPath(), []);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function delete(): array
+    {
+        return $this->apiCall->delete($this->endPointPath());
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Collections.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Collections.php
new file mode 100644
index 0000000..9385aa3
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Collections.php
@@ -0,0 +1,112 @@
+
+ */
+class Collections implements \ArrayAccess
+{
+
+    public const RESOURCE_PATH = '/collections';
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * @var array
+     */
+    private array $collections = [];
+
+    /**
+     * Collections constructor.
+     *
+     * @param ApiCall $apiCall
+     */
+    public function __construct(ApiCall $apiCall)
+    {
+        $this->apiCall = $apiCall;
+    }
+
+    /**
+     * @param $collectionName
+     *
+     * @return mixed
+     */
+    public function __get($collectionName)
+    {
+        if (isset($this->{$collectionName})) {
+            return $this->{$collectionName};
+        }
+        if (!isset($this->collections[$collectionName])) {
+            $this->collections[$collectionName] = new Collection($collectionName, $this->apiCall);
+        }
+
+        return $this->collections[$collectionName];
+    }
+
+    /**
+     * @param array $schema
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function create(array $schema): array
+    {
+        return $this->apiCall->post(static::RESOURCE_PATH, $schema);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get(static::RESOURCE_PATH, []);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetExists($offset): bool
+    {
+        return isset($this->collections[$offset]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetGet($offset): Collection
+    {
+        if (!isset($this->collections[$offset])) {
+            $this->collections[$offset] = new Collection($offset, $this->apiCall);
+        }
+
+        return $this->collections[$offset];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetSet($offset, $value): void
+    {
+        $this->collections[$offset] = $value;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetUnset($offset): void
+    {
+        unset($this->collections[$offset]);
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Debug.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Debug.php
new file mode 100644
index 0000000..c698738
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Debug.php
@@ -0,0 +1,41 @@
+apiCall = $apiCall;
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get(Debug::RESOURCE_PATH, []);
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Document.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Document.php
new file mode 100644
index 0000000..f0e7d8d
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Document.php
@@ -0,0 +1,89 @@
+
+ */
+class Document
+{
+
+    /**
+     * @var string
+     */
+    private string $collectionName;
+
+    /**
+     * @var string
+     */
+    private string $documentId;
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * Document constructor.
+     *
+     * @param string $collectionName
+     * @param string $documentId
+     * @param ApiCall $apiCall
+     */
+    public function __construct(string $collectionName, string $documentId, ApiCall $apiCall)
+    {
+        $this->collectionName = $collectionName;
+        $this->documentId     = $documentId;
+        $this->apiCall        = $apiCall;
+    }
+
+    /**
+     * @return string
+     */
+    private function endpointPath(): string
+    {
+        return sprintf(
+            '%s/%s/%s/%s',
+            Collections::RESOURCE_PATH,
+            $this->collectionName,
+            Documents::RESOURCE_PATH,
+            $this->documentId
+        );
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endpointPath(), []);
+    }
+
+    /**
+     * @param array $partialDocument
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function update(array $partialDocument): array
+    {
+        return $this->apiCall->patch($this->endpointPath(), $partialDocument);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function delete(): array
+    {
+        return $this->apiCall->delete($this->endpointPath());
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Documents.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Documents.php
new file mode 100644
index 0000000..bc27708
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Documents.php
@@ -0,0 +1,214 @@
+
+ */
+class Documents implements \ArrayAccess
+{
+
+    public const RESOURCE_PATH = 'documents';
+
+    /**
+     * @var string
+     */
+    private string $collectionName;
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * @var array
+     */
+    private array $documents = [];
+
+    /**
+     * Documents constructor.
+     *
+     * @param string $collectionName
+     * @param ApiCall $apiCall
+     */
+    public function __construct(string $collectionName, ApiCall $apiCall)
+    {
+        $this->collectionName = $collectionName;
+        $this->apiCall        = $apiCall;
+    }
+
+    /**
+     * @param string $action
+     *
+     * @return string
+     */
+    private function endPointPath(string $action = ''): string
+    {
+        return sprintf('%s/%s/%s/%s', Collections::RESOURCE_PATH, $this->collectionName, static::RESOURCE_PATH, $action);
+    }
+
+    /**
+     * @param array $document
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function create(array $document): array
+    {
+        return $this->apiCall->post($this->endPointPath(''), $document, true);
+    }
+
+    /**
+     * @param array $document
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function upsert(array $document): array
+    {
+        return $this->apiCall->post($this->endPointPath(''), $document, true, ['action' => 'upsert']);
+    }
+
+    /**
+     * @param array $document
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function update(array $document): array
+    {
+        return $this->apiCall->post($this->endPointPath(''), $document, true, ['action' => 'update']);
+    }
+
+    /**
+     * @param array $documents
+     * @param array $options
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException|\JsonException
+     */
+    public function createMany(array $documents, array $options = []): array
+    {
+        $this->apiCall->getLogger()->warning(
+            "createMany is deprecated and will be removed in a future version. " .
+            "Use import instead, which now takes both an array of documents or a JSONL string of documents"
+        );
+        return $this->import($documents, $options);
+    }
+
+    /**
+     * @param string|array $documents
+     * @param array $options
+     *
+     * @return string|array
+     * @throws TypesenseClientError
+     * @throws GuzzleException
+     * @throws \JsonException
+     */
+    public function import($documents, array $options = [])
+    {
+        if (is_array($documents)) {
+            $documentsInJSONLFormat = implode(
+                "\n",
+                array_map(
+                    static fn(array $document) => json_encode($document, JSON_THROW_ON_ERROR),
+                    $documents
+                )
+            );
+        } else {
+            $documentsInJSONLFormat = $documents;
+        }
+        $resultsInJSONLFormat = $this->apiCall->post(
+            $this->endPointPath('import'),
+            $documentsInJSONLFormat,
+            false,
+            $options
+        );
+
+        if (is_array($documents)) {
+            return array_map(static function ($item) {
+                return json_decode($item, true, 512, JSON_THROW_ON_ERROR);
+            }, explode("\n", $resultsInJSONLFormat));
+        } else {
+            return $resultsInJSONLFormat;
+        }
+    }
+
+    /**
+     * @return string
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function export(): string
+    {
+        return $this->apiCall->get($this->endPointPath('export'), [], false);
+    }
+
+    /**
+     * @param array $queryParams
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function delete(array $queryParams = []): array
+    {
+        return $this->apiCall->delete($this->endPointPath(), true, $queryParams);
+    }
+
+    /**
+     * @param array $searchParams
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function search(array $searchParams): array
+    {
+        return $this->apiCall->get($this->endPointPath('search'), $searchParams);
+    }
+
+    /**
+     * @param mixed $documentId
+     *
+     * @return bool
+     */
+    public function offsetExists($documentId): bool
+    {
+        return isset($this->documents[$documentId]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetGet($documentId): Document
+    {
+        if (!isset($this->documents[$documentId])) {
+            $this->documents[$documentId] = new Document($this->collectionName, $documentId, $this->apiCall);
+        }
+
+        return $this->documents[$documentId];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetUnset($documentId): void
+    {
+        if (isset($this->documents[$documentId])) {
+            unset($this->documents[$documentId]);
+        }
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetSet($offset, $value): void
+    {
+        $this->documents[$offset] = $value;
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ConfigError.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ConfigError.php
new file mode 100644
index 0000000..b4fcc7c
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ConfigError.php
@@ -0,0 +1,15 @@
+
+ */
+class ConfigError extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/HTTPStatus0Error.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/HTTPStatus0Error.php
new file mode 100644
index 0000000..e2f2752
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/HTTPStatus0Error.php
@@ -0,0 +1,14 @@
+
+ */
+class HTTPStatus0Error extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectAlreadyExists.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectAlreadyExists.php
new file mode 100644
index 0000000..956cffd
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectAlreadyExists.php
@@ -0,0 +1,15 @@
+
+ */
+class ObjectAlreadyExists extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectNotFound.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectNotFound.php
new file mode 100644
index 0000000..7cae4b8
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectNotFound.php
@@ -0,0 +1,15 @@
+
+ */
+class ObjectNotFound extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectUnprocessable.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectUnprocessable.php
new file mode 100644
index 0000000..89a2f92
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ObjectUnprocessable.php
@@ -0,0 +1,15 @@
+
+ */
+class ObjectUnprocessable extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/RequestMalformed.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/RequestMalformed.php
new file mode 100644
index 0000000..2a7c42b
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/RequestMalformed.php
@@ -0,0 +1,15 @@
+
+ */
+class RequestMalformed extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/RequestUnauthorized.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/RequestUnauthorized.php
new file mode 100644
index 0000000..47ec353
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/RequestUnauthorized.php
@@ -0,0 +1,15 @@
+
+ */
+class RequestUnauthorized extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ServerError.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ServerError.php
new file mode 100644
index 0000000..e13d18d
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ServerError.php
@@ -0,0 +1,15 @@
+
+ */
+class ServerError extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ServiceUnavailable.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ServiceUnavailable.php
new file mode 100644
index 0000000..1b32bcb
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/ServiceUnavailable.php
@@ -0,0 +1,15 @@
+
+ */
+class ServiceUnavailable extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/Timeout.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/Timeout.php
new file mode 100644
index 0000000..359eb33
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/Timeout.php
@@ -0,0 +1,15 @@
+
+ */
+class Timeout extends TypesenseClientError
+{
+
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/TypesenseClientError.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/TypesenseClientError.php
new file mode 100644
index 0000000..2f20c2a
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Exceptions/TypesenseClientError.php
@@ -0,0 +1,22 @@
+
+ */
+class TypesenseClientError extends Exception
+{
+
+    public function setMessage(string $message): TypesenseClientError
+    {
+        $this->message = $message;
+        return $this;
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Health.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Health.php
new file mode 100644
index 0000000..f0db09e
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Health.php
@@ -0,0 +1,41 @@
+apiCall = $apiCall;
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get(Health::RESOURCE_PATH, []);
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Key.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Key.php
new file mode 100644
index 0000000..af7a9b1
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Key.php
@@ -0,0 +1,65 @@
+
+ */
+class Key
+{
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * @var string
+     */
+    private string $keyId;
+
+    /**
+     * Key constructor.
+     *
+     * @param string $keyId
+     * @param ApiCall $apiCall
+     */
+    public function __construct(string $keyId, ApiCall $apiCall)
+    {
+        $this->keyId   = $keyId;
+        $this->apiCall = $apiCall;
+    }
+
+    /**
+     * @return string
+     */
+    private function endpointPath(): string
+    {
+        return sprintf('%s/%s', Keys::RESOURCE_PATH, $this->keyId);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endpointPath(), []);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function delete(): array
+    {
+        return $this->apiCall->delete($this->endpointPath());
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Keys.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Keys.php
new file mode 100644
index 0000000..32e4d01
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Keys.php
@@ -0,0 +1,118 @@
+
+ */
+class Keys implements \ArrayAccess
+{
+
+    public const RESOURCE_PATH = '/keys';
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * @var array
+     */
+    private array $keys = [];
+
+    /**
+     * Keys constructor.
+     *
+     * @param ApiCall $apiCall
+     */
+    public function __construct(ApiCall $apiCall)
+    {
+        $this->apiCall = $apiCall;
+    }
+
+    /**
+     * @param array $schema
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function create(array $schema): array
+    {
+        return $this->apiCall->post(static::RESOURCE_PATH, $schema);
+    }
+
+    /**
+     * @param string $searchKey
+     * @param array $parameters
+     *
+     * @return string
+     * @throws \JsonException
+     */
+    public function generateScopedSearchKey(
+        string $searchKey,
+        array $parameters
+    ): string {
+        $paramStr     = json_encode($parameters, JSON_THROW_ON_ERROR);
+        $digest       = base64_encode(hash_hmac('sha256', utf8_encode($paramStr), utf8_encode($searchKey), true));
+        $keyPrefix    = substr($searchKey, 0, 4);
+        $rawScopedKey = sprintf('%s%s%s', utf8_decode($digest), $keyPrefix, $paramStr);
+        return base64_encode(utf8_encode($rawScopedKey));
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get(static::RESOURCE_PATH, []);
+    }
+
+    /**
+     * @param mixed $offset
+     *
+     * @return bool
+     */
+    public function offsetExists($offset): bool
+    {
+        return isset($this->keys[$offset]);
+    }
+
+    /**
+     * @param mixed $offset
+     *
+     * @return \Typesense\Key
+     */
+    public function offsetGet($offset): Key
+    {
+        if (!isset($this->keys[$offset])) {
+            $this->keys[$offset] = new Key($offset, $this->apiCall);
+        }
+
+        return $this->keys[$offset];
+    }
+
+    /**
+     * @param mixed $offset
+     * @param mixed $value
+     */
+    public function offsetSet($offset, $value): void
+    {
+        $this->keys[$offset] = $value;
+    }
+
+    /**
+     * @param mixed $offset
+     */
+    public function offsetUnset($offset): void
+    {
+        unset($this->keys[$offset]);
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Lib/Configuration.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Lib/Configuration.php
new file mode 100644
index 0000000..118ee66
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Lib/Configuration.php
@@ -0,0 +1,225 @@
+
+ */
+class Configuration
+{
+
+    /**
+     * @var Node[]
+     */
+    private array $nodes;
+
+    /**
+     * @var Node|null
+     */
+    private ?Node $nearestNode;
+
+    /**
+     * @var string
+     */
+    private string $apiKey;
+
+    /**
+     * @var float
+     */
+    private float $numRetries;
+
+    /**
+     * @var float
+     */
+    private float $retryIntervalSeconds;
+
+    /**
+     * @var int
+     */
+    private int $healthCheckIntervalSeconds;
+
+    /**
+     * @var LoggerInterface
+     */
+    private LoggerInterface $logger;
+
+    /**
+     * @var null|HttpClient
+     */
+    private ?HttpClient $client = null;
+
+    /**
+     * @var int
+     */
+    private int $logLevel;
+
+    /**
+     * Configuration constructor.
+     *
+     * @param array $config
+     *
+     * @throws ConfigError
+     */
+    public function __construct(array $config)
+    {
+        $this->validateConfigArray($config);
+
+        $nodes = $config['nodes'] ?? [];
+
+        foreach ($nodes as $node) {
+            $this->nodes[] = new Node($node['host'], $node['port'], $node['path'] ?? '', $node['protocol']);
+        }
+
+        $nearestNode       = $config['nearest_node'] ?? null;
+        $this->nearestNode = null;
+        if (null !== $nearestNode) {
+            $this->nearestNode =
+                new Node(
+                    $nearestNode['host'],
+                    $nearestNode['port'],
+                    $nearestNode['path'] ?? '',
+                    $nearestNode['protocol']
+                );
+        }
+
+        $this->apiKey = $config['api_key'] ?? '';
+        $this->healthCheckIntervalSeconds = (int)($config['healthcheck_interval_seconds'] ?? 60);
+        $this->numRetries           = (float)($config['num_retries'] ?? 3);
+        $this->retryIntervalSeconds = (float)($config['retry_interval_seconds'] ?? 1.0);
+
+        $this->logLevel = $config['log_level'] ?? Logger::WARNING;
+        $this->logger   = new Logger('typesense');
+        $this->logger->pushHandler(new StreamHandler('php://stdout', $this->logLevel));
+
+        if (true === \array_key_exists('client', $config) && $config['client'] instanceof HttpClient) {
+            $this->client = $config['client'];
+        }
+    }
+
+    /**
+     * @param array $config
+     *
+     * @throws ConfigError
+     */
+    private function validateConfigArray(array $config): void
+    {
+        $nodes = $config['nodes'] ?? false;
+        if (!$nodes) {
+            throw new ConfigError('`nodes` is not defined.');
+        }
+
+        $apiKey = $config['api_key'] ?? false;
+        if (!$apiKey) {
+            throw new ConfigError('`api_key` is not defined.');
+        }
+
+        foreach ($nodes as $node) {
+            if (!$this->validateNodeFields($node)) {
+                throw new ConfigError(
+                    '`node` entry be a dictionary with the following required keys: host, port, protocol, api_key'
+                );
+            }
+        }
+        $nearestNode = $config['nearest_node'] ?? [];
+        if (!empty($nearestNode) && !$this->validateNodeFields($nearestNode)) {
+            throw new ConfigError(
+                '`nearest_node` entry be a dictionary with the following required keys: host, port, protocol, api_key'
+            );
+        }
+    }
+
+    /**
+     * @param array $node
+     *
+     * @return bool
+     */
+    public function validateNodeFields(array $node): bool
+    {
+        $keys = [
+            'host',
+            'port',
+            'protocol',
+        ];
+        return !array_diff_key(array_flip($keys), $node);
+    }
+
+    /**
+     * @return Node[]
+     */
+    public function getNodes(): array
+    {
+        return $this->nodes;
+    }
+
+    /**
+     * @return Node
+     */
+    public function getNearestNode(): ?Node
+    {
+        return $this->nearestNode;
+    }
+
+    /**
+     * @return mixed|string
+     */
+    public function getApiKey()
+    {
+        return $this->apiKey;
+    }
+
+    /**
+     * @return float
+     */
+    public function getNumRetries(): float
+    {
+        return $this->numRetries;
+    }
+
+    /**
+     * @return float
+     */
+    public function getRetryIntervalSeconds(): float
+    {
+        return $this->retryIntervalSeconds;
+    }
+
+    /**
+     * @return float|mixed
+     */
+    public function getHealthCheckIntervalSeconds()
+    {
+        return $this->healthCheckIntervalSeconds;
+    }
+
+    /**
+     * @return LoggerInterface
+     */
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    /**
+     * @return HttpClient
+     */
+    public function getClient(): HttpClient
+    {
+        return new HttpMethodsClient(
+            $this->client ?? HttpClientDiscovery::find(),
+            MessageFactoryDiscovery::find()
+        );
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Lib/Node.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Lib/Node.php
new file mode 100644
index 0000000..3500baa
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Lib/Node.php
@@ -0,0 +1,105 @@
+
+ */
+class Node
+{
+
+    /**
+     * @var string
+     */
+    private string $host;
+
+    /**
+     * @var string
+     */
+    private string $port;
+
+    /**
+     * @var string
+     */
+    private string $path;
+
+    /**
+     * @var string
+     */
+    private string $protocol;
+
+    /**
+     * @var bool
+     */
+    private bool $healthy = false;
+
+    /**
+     * @var int
+     */
+    private int $lastAccessTs;
+
+    /**
+     * Node constructor.
+     *
+     * @param string $host
+     * @param string $port
+     * @param string $path
+     * @param string $protocol
+     */
+    public function __construct(
+        string $host,
+        string $port,
+        string $path,
+        string $protocol
+    ) {
+        $this->host         = $host;
+        $this->port         = $port;
+        $this->path         = $path;
+        $this->protocol     = $protocol;
+        $this->lastAccessTs = time();
+    }
+
+    /**
+     * @return string
+     */
+    public function url(): string
+    {
+        return sprintf('%s://%s:%s%s', $this->protocol, $this->host, $this->port, $this->path);
+    }
+
+    /**
+     * @return bool
+     */
+    public function isHealthy(): bool
+    {
+        return $this->healthy;
+    }
+
+    /**
+     * @param bool $healthy
+     */
+    public function setHealthy(bool $healthy): void
+    {
+        $this->healthy = $healthy;
+    }
+
+    /**
+     * @return int
+     */
+    public function getLastAccessTs(): int
+    {
+        return $this->lastAccessTs;
+    }
+
+    /**
+     * @param int $lastAccessTs
+     */
+    public function setLastAccessTs(int $lastAccessTs): void
+    {
+        $this->lastAccessTs = $lastAccessTs;
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Metrics.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Metrics.php
new file mode 100644
index 0000000..fb3c8b5
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Metrics.php
@@ -0,0 +1,41 @@
+apiCall = $apiCall;
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get(Metrics::RESOURCE_PATH, []);
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/MultiSearch.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/MultiSearch.php
new file mode 100644
index 0000000..d24f9e9
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/MultiSearch.php
@@ -0,0 +1,48 @@
+apiCall = $apiCall;
+    }
+
+    /**
+     * @param string $searches
+     * @param array $queryParameters
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function perform(array $searches, array $queryParameters = []): array
+    {
+        return $this->apiCall->post(
+            sprintf('%s', static::RESOURCE_PATH),
+            $searches,
+            true,
+            $queryParameters
+        );
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Operations.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Operations.php
new file mode 100644
index 0000000..661c597
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Operations.php
@@ -0,0 +1,48 @@
+apiCall = $apiCall;
+    }
+
+    /**
+     * @param string $operationName
+     * @param array $queryParameters
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function perform(string $operationName, array $queryParameters = []): array
+    {
+        return $this->apiCall->post(
+            sprintf('%s/%s', static::RESOURCE_PATH, $operationName),
+            null,
+            true,
+            $queryParameters
+        );
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Override.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Override.php
new file mode 100644
index 0000000..d50855f
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Override.php
@@ -0,0 +1,78 @@
+
+ */
+class Override
+{
+
+    /**
+     * @var string
+     */
+    private string $collectionName;
+
+    /**
+     * @var string
+     */
+    private string $overrideId;
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * Override constructor.
+     *
+     * @param string $collectionName
+     * @param string $overrideId
+     * @param ApiCall $apiCall
+     */
+    public function __construct(string $collectionName, string $overrideId, ApiCall $apiCall)
+    {
+        $this->collectionName = $collectionName;
+        $this->overrideId     = $overrideId;
+        $this->apiCall        = $apiCall;
+    }
+
+    /**
+     * @return string
+     */
+    private function endPointPath(): string
+    {
+        return sprintf(
+            '%s/%s/%s/%s',
+            Collections::RESOURCE_PATH,
+            $this->collectionName,
+            Overrides::RESOURCE_PATH,
+            $this->overrideId
+        );
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endPointPath(), []);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function delete(): array
+    {
+        return $this->apiCall->delete($this->endPointPath());
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Overrides.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Overrides.php
new file mode 100644
index 0000000..3fb1709
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Overrides.php
@@ -0,0 +1,119 @@
+
+ */
+class Overrides implements \ArrayAccess
+{
+
+    public const RESOURCE_PATH = 'overrides';
+
+    /**
+     * @var ApiCall
+     */
+    private ApiCall $apiCall;
+
+    /**
+     * @var string
+     */
+    private string $collectionName;
+
+    /**
+     * @var array
+     */
+    private array $overrides = [];
+
+    /**
+     * Overrides constructor.
+     *
+     * @param string $collectionName
+     * @param ApiCall $apiCall
+     */
+    public function __construct(string $collectionName, ApiCall $apiCall)
+    {
+        $this->collectionName = $collectionName;
+        $this->apiCall        = $apiCall;
+    }
+
+    /**
+     * @param string $overrideId
+     *
+     * @return string
+     */
+    public function endPointPath(string $overrideId = ''): string
+    {
+        return sprintf(
+            '%s/%s/%s/%s',
+            Collections::RESOURCE_PATH,
+            $this->collectionName,
+            static::RESOURCE_PATH,
+            $overrideId
+        );
+    }
+
+    /**
+     * @param string $overrideId
+     * @param array $config
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function upsert(string $overrideId, array $config): array
+    {
+        return $this->apiCall->put($this->endPointPath($overrideId), $config);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endPointPath(), []);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetExists($overrideId): bool
+    {
+        return isset($this->overrides[$overrideId]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetGet($overrideId)
+    {
+        if (!isset($this->overrides[$overrideId])) {
+            $this->overrides[$overrideId] = new Override($this->collectionName, $overrideId, $this->apiCall);
+        }
+
+        return $this->overrides[$overrideId];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetSet($overrideId, $value): void
+    {
+        $this->overrides[$overrideId] = $value;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetUnset($overrideId): void
+    {
+        unset($this->overrides[$overrideId]);
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Synonym.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Synonym.php
new file mode 100644
index 0000000..5ce46c1
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Synonym.php
@@ -0,0 +1,76 @@
+collectionName = $collectionName;
+        $this->synonymId      = $synonymId;
+        $this->apiCall        = $apiCall;
+    }
+
+    /**
+     * @return string
+     */
+    private function endPointPath(): string
+    {
+        return sprintf(
+            '%s/%s/%s/%s',
+            Collections::RESOURCE_PATH,
+            $this->collectionName,
+            synonyms::RESOURCE_PATH,
+            $this->synonymId
+        );
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endPointPath(), []);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function delete(): array
+    {
+        return $this->apiCall->delete($this->endPointPath());
+    }
+}
diff --git a/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Synonyms.php b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Synonyms.php
new file mode 100644
index 0000000..62d9746
--- /dev/null
+++ b/includes/libraries/typesensesearch-client-php/vendor/typesense/typesense-php/src/Synonyms.php
@@ -0,0 +1,117 @@
+collectionName = $collectionName;
+        $this->apiCall        = $apiCall;
+    }
+
+    /**
+     * @param string $synonymId
+     *
+     * @return string
+     */
+    public function endPointPath(string $synonymId = ''): string
+    {
+        return sprintf(
+            '%s/%s/%s/%s',
+            Collections::RESOURCE_PATH,
+            $this->collectionName,
+            static::RESOURCE_PATH,
+            $synonymId
+        );
+    }
+
+    /**
+     * @param string $synonymId
+     * @param array $config
+     *
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function upsert(string $synonymId, array $config): array
+    {
+        return $this->apiCall->put($this->endPointPath($synonymId), $config);
+    }
+
+    /**
+     * @return array
+     * @throws TypesenseClientError|HttpClientException
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endPointPath(), []);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetExists($synonymId): bool
+    {
+        return isset($this->synonyms[$synonymId]);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetGet($synonymId)
+    {
+        if (!isset($this->synonyms[$synonymId])) {
+            $this->synonyms[$synonymId] = new Synonym($this->collectionName, $synonymId, $this->apiCall);
+        }
+
+        return $this->synonyms[$synonymId];
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetSet($synonymId, $value): void
+    {
+        $this->synonyms[$synonymId] = $value;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function offsetUnset($synonymId): void
+    {
+        unset($this->synonyms[$synonymId]);
+    }
+}
diff --git a/includes/watchers/class-algolia-term-changes-watcher.php b/includes/watchers/class-algolia-term-changes-watcher.php
new file mode 100644
index 0000000..c80ec59
--- /dev/null
+++ b/includes/watchers/class-algolia-term-changes-watcher.php
@@ -0,0 +1,133 @@
+
+ * @since   1.0.0
+ *
+ * @package WebDevStudios\WPSWA
+ */
+
+use Algolia\AlgoliaSearch\Exceptions\AlgoliaException;
+
+/**
+ * Class Algolia_Term_Changes_Watcher
+ *
+ * @since 1.0.0
+ */
+class Algolia_Term_Changes_Watcher implements Algolia_Changes_Watcher {
+
+	/**
+	 * Algolia_Index instance.
+	 *
+	 * @author WebDevStudios 
+	 * @since  1.0.0
+	 *
+	 * @var Algolia_Index
+	 */
+	private $index;
+
+	/**
+	 * Algolia_Term_Changes_Watcher constructor.
+	 *
+	 * @author WebDevStudios 
+	 * @since  1.0.0
+	 *
+	 * @param Algolia_Index $index Algolia_Index instance.
+	 */
+	public function __construct( Algolia_Index $index ) {
+		$this->index = $index;
+	}
+
+	/**
+	 * Watch WordPress events.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 */
+	public function watch() {
+		// Fires immediately after the given terms are edited.
+		add_action( 'edited_term', array( $this, 'sync_item' ) );
+
+		// Fires after an object's terms have been set.
+		add_action( 'set_object_terms', array( $this, 'handle_changes' ), 10, 6 );
+
+		// Fires after a term is deleted from the database and the cache is cleaned.
+		add_action( 'delete_term', array( $this, 'on_delete_term' ), 10, 4 );
+	}
+
+	/**
+	 * Sync item.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int $term_id The term ID to sync.
+	 *
+	 * @return void
+	 */
+	public function sync_item( $term_id ) {
+		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
+			return;
+		}
+
+		$term = get_term( (int) $term_id );
+
+		if ( ! $term || ! $this->index->supports( $term ) ) {
+			return;
+		}
+
+		try {
+			$this->index->sync( $term );
+		} catch ( AlgoliaException $exception ) {
+			error_log( $exception->getMessage() ); // phpcs:ignore -- Legacy.
+		}
+	}
+
+	/**
+	 * Handle term changes.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int    $object_id  Object ID.
+	 * @param array  $terms      An array of object terms.
+	 * @param array  $tt_ids     An array of term taxonomy IDs.
+	 * @param string $taxonomy   Taxonomy slug.
+	 * @param bool   $append     Whether to append new terms to the old terms.
+	 * @param array  $old_tt_ids Old array of term taxonomy IDs.
+	 */
+	public function handle_changes( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
+		$terms_to_sync = array_unique( array_merge( $terms, $old_tt_ids ) );
+
+		foreach ( $terms_to_sync as $term_id ) {
+			$this->sync_item( $term_id );
+		}
+	}
+
+	/**
+	 * Delete item.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int    $term         Term ID.
+	 * @param int    $tt_id        Term taxonomy ID.
+	 * @param string $taxonomy     Taxonomy slug.
+	 * @param mixed  $deleted_term Copy of the already-deleted term, in the form specified
+	 *                             by the parent function. WP_Error otherwise.
+	 *
+	 * @return void
+	 */
+	public function on_delete_term( $term, $tt_id, $taxonomy, $deleted_term ) {
+		if ( ! $this->index->supports( $deleted_term ) ) {
+			return;
+		}
+
+		try {
+			$this->index->delete_item( $deleted_term );
+		} catch ( AlgoliaException $exception ) {
+			error_log( $exception->getMessage() ); // phpcs:ignore -- Legacy.
+		}
+	}
+}
diff --git a/includes/watchers/class-algolia-user-changes-watcher.php b/includes/watchers/class-algolia-user-changes-watcher.php
new file mode 100644
index 0000000..7953461
--- /dev/null
+++ b/includes/watchers/class-algolia-user-changes-watcher.php
@@ -0,0 +1,161 @@
+
+ * @since   1.0.0
+ *
+ * @package WebDevStudios\WPSWA
+ */
+
+use Algolia\AlgoliaSearch\Exceptions\AlgoliaException;
+
+/**
+ * Class Algolia_User_Changes_Watcher
+ *
+ * @since 1.0.0
+ */
+class Algolia_User_Changes_Watcher implements Algolia_Changes_Watcher {
+
+	/**
+	 * Algolia_Index instance.
+	 *
+	 * @author WebDevStudios 
+	 * @since  1.0.0
+	 *
+	 * @var Algolia_Index
+	 */
+	private $index;
+
+	/**
+	 * Algolia_User_Changes_Watcher constructor.
+	 *
+	 * @author WebDevStudios 
+	 * @since  1.0.0
+	 *
+	 * @param Algolia_Index $index Algolia_Index instance.
+	 */
+	public function __construct( Algolia_Index $index ) {
+		$this->index = $index;
+	}
+
+	/**
+	 * Watch WordPress events.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 */
+	public function watch() {
+		// Fires immediately after an existing user is updated.
+		add_action( 'profile_update', array( $this, 'sync_item' ) );
+
+		// Fires immediately after a new user is registered.
+		add_action( 'user_register', array( $this, 'sync_item' ) );
+
+		// Fires immediately before a user is deleted.
+		add_action( 'delete_user', array( $this, 'delete_item' ) );
+
+		// Fires once a post has been saved.
+		add_action( 'save_post', array( $this, 'on_save_post' ), 10, 2 );
+
+		// Fires before a post is deleted, at the start of wp_delete_post().
+		// At this stage the post metas are still available, and we need them.
+		add_action( 'before_delete_post', array( $this, 'on_delete_post' ) );
+	}
+
+	/**
+	 * Sync item.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int $user_id User ID.
+	 *
+	 * @return void
+	 */
+	public function sync_item( $user_id ) {
+		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
+			return;
+		}
+
+		$user = get_user_by( 'id', $user_id );
+
+		if ( ! $user || ! $this->index->supports( $user ) ) {
+			return;
+		}
+
+		try {
+			$this->index->sync( $user );
+		} catch ( AlgoliaException $exception ) {
+			error_log( $exception->getMessage() ); // phpcs:ignore -- Legacy.
+		}
+	}
+
+	/**
+	 * Delete item.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int $user_id ID of the user to delete.
+	 *
+	 * @return void
+	 */
+	public function delete_item( $user_id ) {
+		$user = get_user_by( 'id', $user_id );
+
+		if ( ! $user || ! $this->index->supports( $user ) ) {
+			return;
+		}
+
+		try {
+			$this->index->delete_item( $user );
+		} catch ( AlgoliaException $exception ) {
+			error_log( $exception->getMessage() ); // phpcs:ignore -- Legacy.
+		}
+	}
+
+	/**
+	 * Ensures that the user post count gets updated.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int     $post_id Post ID.
+	 * @param WP_Post $post    Post object.
+	 */
+	public function on_save_post( $post_id, WP_Post $post ) {
+		$this->sync_item( (int) $post->post_author );
+	}
+
+	/**
+	 * Ensures that the user post count gets updated.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int $post_id Post ID.
+	 *
+	 * @return void
+	 */
+	public function on_delete_post( $post_id ) {
+		$post = get_post( (int) $post_id );
+
+		if ( ! $post ) {
+			return;
+		}
+
+		$watcher   = $this;
+		$author_id = $post->post_author;
+
+		// We delay the sync until after the post was deleted to propagate the change
+		// posts count change for the author.
+		// Todo: this is not optimal given it would be triggered for every future triggered hook.
+		// Todo: needs to be changed.
+		add_action(
+			'after_delete_post', function() use ( $watcher, $author_id ) {
+				$watcher->sync_item( $author_id );
+			}
+		);
+	}
+}
diff --git a/includes/watchers/class-typesense-changes-watcher.php b/includes/watchers/class-typesense-changes-watcher.php
new file mode 100644
index 0000000..cdc1f4d
--- /dev/null
+++ b/includes/watchers/class-typesense-changes-watcher.php
@@ -0,0 +1,25 @@
+
+ * @since   1.0.0
+ *
+ * @package WebDevStudios\WPSWA
+ */
+
+/**
+ * Interface Typesense_Changes_Watcher
+ *
+ * @since 1.0.0
+ */
+interface Typesense_Changes_Watcher {
+
+	/**
+	 * Watch WordPress events.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 */
+	public function watch();
+}
diff --git a/includes/watchers/class-typesense-post-changes-watcher.php b/includes/watchers/class-typesense-post-changes-watcher.php
new file mode 100644
index 0000000..a9ed9bb
--- /dev/null
+++ b/includes/watchers/class-typesense-post-changes-watcher.php
@@ -0,0 +1,173 @@
+
+ * @since   1.0.0
+ *
+ * @package WebDevStudios\WPSWA
+ */
+
+/**
+ * Class Typesense_Post_Changes_Watcher
+ *
+ * @since 1.0.0
+ */
+use Typesense\Client;
+
+class Typesense_Post_Changes_Watcher implements Typesense_Changes_Watcher {
+
+	/**
+	 * Typesense_Index instance.
+	 *
+	 * @author WebDevStudios 
+	 * @since  1.0.0
+	 *
+	 * @var Typesense_Index
+	 */
+	private $index;
+
+	/**
+	 * Deleted posts array.
+	 *
+	 * @author WebDevStudios 
+	 * @since  1.0.0
+	 *
+	 * @var Array
+	 */
+	private $posts_deleted = array();
+
+	/**
+	 * hanges_Watcher constructor.
+	 *
+	 * @author WebDevStudios 
+	 * @since  1.0.0
+	 *
+	 * @param Typesense_Index $index Typesense_Index instance.
+	 */
+	public function __construct( Typesense_Index $index ) {
+		$this->index = $index;
+	}
+
+	/**
+	 * Watch WordPress events.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 */
+	public function watch() {
+		// Fires once a post has been saved.
+		add_action( 'publish_post', array( $this, 'sync_item' ) );
+
+		// Fires before a post is deleted, at the start of wp_delete_post().
+		// At this stage the post metas are still available, and we need them.
+		//add_action( 'before_delete_post', array( $this, 'delete_item' ) );
+
+		// Handle meta changes after the change occurred.
+		//add_action( 'added_post_meta', array( $this, 'on_meta_change' ), 10, 4 );
+		//add_action( 'updated_post_meta', array( $this, 'on_meta_change' ), 10, 4 );
+		//add_action( 'deleted_post_meta', array( $this, 'on_meta_change' ), 10, 4 );
+
+		// Handle attachment changes. These are required because the other post hooks are not triggered.
+		//add_action( 'add_attachment', array( $this, 'sync_item' ) );
+		//add_action( 'attachment_updated', array( $this, 'sync_item' ) );
+		//add_action( 'delete_attachment', array( $this, 'delete_item' ) );
+	}
+
+	/**
+	 * Sync item.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int $post_id The post ID to sync.
+	 *
+	 * @return void
+	 */
+	public function sync_item( $post_id ) {
+/*
+		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
+			return;
+		}
+
+		if ( in_array( $post_id, $this->posts_deleted, true ) ) {
+			return;
+		}
+*/
+/*		if ( ! $post || ! $this->index->supports( $post ) ) {
+			return;
+		}
+*/
+		$document = [
+			'post_id' => '1',
+			'id' => '3',
+			'post_content' => 'New world',
+			'post_title' => 'Dummy text 2',
+			'post_excerpt' => 'dcd',
+			'post_type' => 'wecfwec',
+			'is_sticky' => 1,
+
+			'post_modified' => 'fwecfwe',
+			'post_date' => 'cwe',
+
+			'comment_count' => 2,
+
+		];
+		try {
+			$post = get_post( (int) $post_id );
+			//error_log('egwerhwnrum');
+			$this->index->sync( $post );
+			//throw new RuntimeException($post->post_title);
+		} catch ( Exception $exception ) {
+			error_log( $exception->getMessage() ); // phpcs:ignore -- Legacy.
+		}
+	}
+
+	/**
+	 * Delete item.
+	 *
+	 * @author  WebDevStudios 
+	 * @since   1.0.0
+	 *
+	 * @param int $post_id The post ID to delete.
+	 *
+	 * @return void
+	 */
+	public function delete_item( $post_id ) {
+
+		$post = get_post( (int) $post_id );
+		if ( ! $post || ! $this->index->supports( $post ) ) {
+			return;
+		}
+
+		try {
+			$this->index->delete_item( $post );
+			$this->posts_deleted[] = $post->ID;
+		} catch ( TypesenseException $exception ) {
+			error_log( $exception->getMessage() ); // phpcs:ignore -- Legacy.
+		}
+	}
+
+	/**
+	 * Watch meta changes for item.
+	 *
+	 * @author WebDevStudios 
+	 * @since  1.0.0
+	 *
+	 * @param string|array $meta_id   The meta ID.
+	 * @param int          $object_id The post ID.
+	 * @param string       $meta_key  The meta key.
+	 *
+	 * @return void
+	 */
+	public function on_meta_change( $meta_id, $object_id, $meta_key ) {
+		$keys = array( '_thumbnail_id' );
+		$keys = (array) apply_filters( '_watch_post_meta_keys', $keys, $object_id );
+
+		if ( ! in_array( $meta_key, $keys, true ) ) {
+			return;
+		}
+
+		$this->sync_item( $object_id );
+	}
+}
diff --git a/includes/watchers/index.php b/includes/watchers/index.php
new file mode 100644
index 0000000..8142269
--- /dev/null
+++ b/includes/watchers/index.php
@@ -0,0 +1 @@
+2019-10-08
+
+* FIX: extra comma on node v8 engine (#790)
+
+## 3.35.0
+>2019-09-26
+
+* FEAT(remaining-validity): adds remaining valitity method (#778)
+  * `client.getSecuredApiKeyRemainingValidity('securedAPIKey')`: Gets remaining validity seconds of an secured API Key
+* FEAT(unit-testing-relevance): adds unit testing relevance methods (#777)
+  * `index.findObject(hit => hit.firstname == 'Jimmie')`: Find an object by the given condition
+  * `index.getObjectPosition(results, 'a-unique-identifier')`: Retrieve the given object position in the given results set
+* FEAT(adds-assign-user-ids): adds assignUserIDs method and tests (#783)
+  * `client.assignUserIDs({ cluster: 'c1-test', userIDs: ['some-user-1', 'some-user-2'] })`: Assign a array of userIDs to a cluster
+
+## 3.34.0
+>2019-08-29
+
+* FEAT(index-exists): adds exists method into index (#773)
+  * `index.exist()`: returns whether an index exists or not
+
+## 3.33.0
+>2019-05-09
+
+* FEAT(api-keys): add restore api key method (#754)
+  * `client.restoreApiKey('APIKEY')`: restore a deleted API key
+  * see also https://www.algolia.com/doc/api-reference/api-methods/restore-api-key
+* FIX(ua): change the User-Agent to use the new specs lib (version) (#747)
+
+## 3.32.1
+>2019-03-14
+
+* FIX(errors): grammar change in error message (#737)
+* FIX(dependencies): bump debug requirement (#746)
+
+## 3.32.0
+>2018-12-17
+
+* FEAT(personalization): add new methods for the strategy
+  * `getPersonalizationStrategy(cb)`: retrieve the currently set strategy
+  * `setPersonalizationStrategy(strategy, cb)`: set the personalization strategy
+    * a `strategy` is an object containing `eventsScoring` and `facetsScoring`
+  * see also https://www.algolia.com/doc/rest-api/personalization
+
+## 3.31.0
+>2018-12-04
+
+* FEAT(places): add `.reverse({ aroundLatLng: '' })` to reverse geo-code
+* CHORE: deprecated `similarSearch()`, instead use `.search({ similarQuery: '' })`
+
+## 3.30.0
+>2018-08-01
+
+* FIX(multi-query): property pass the `strategy` argument
+  * as in [docs](https://www.algolia.com/doc/rest-api/search#method-param-strategy)
+  * was originally put into url parameters instead of request body
+
+## 3.29.0
+>2018-06-20
+
+* FEAT(analytics): add API methods for AB testing
+  * find more info about the new feature here: https://blog.algolia.com/ab-testing-search/
+  * adds a new method on client:
+      import algoliasearch from 'algoliasearch';
+      const client = algoliasearch('appid', 'apikey');
+      const analytics = client.initAnalytics();
+      analytics.getABTests().then(console.log)
+  * see documentation here: https://www.algolia.com/doc/api-client/ab-test/
+* CHORE(parse): remove parse build, not needed.
+  * You can just install and `require('algoliasearch');`
+
+## 3.28.0
+>2018-06-13
+
+* FEAT(cache): cache the requests instead of responses (#694)
+
+  If you pass `_useRequestCache: true` as an option to the client then the cache behaviour will change. If you launch two requests at the same time then only one request will be done while the two callbacks/promises will still be called/resolved.
+
+  Previously, the client would have made two requests instead of one.
+
+  This is implemented by filling the cache as soon as the request launches instead of waiting for the response to fill it.
+* FIX(parse): correct path to debug module (#702)
+
+  fixes parse build, weirdly webpack is not throwing when importing an
+  unknown module using target node
+* TESTS: Make this CI green, fix unit and integration tests on every platform
+
+## 3.27.1
+>2018-05-03
+
+* FIX: `client.searchForFacetValues` in older browsers
+  * we use the polyfill implementation of Promise.all now instead of the native in 3.27.0
+* FIX: `index.saveRule` throw when no objectID is given
+* FEAT: deprecate index API key methods in favor of client ones
+  * these methods will be removed, since you can add index restrictions to client API keys
+
+## 3.27.0
+>2018-04-16
+
+* FEAT: add client.searchForFacetValues
+  * this allows to request multiple search for facet values in a single call
+  * note that this will do multiple requests (one per query)
+
+## 3.26.0
+>2018-03-21
+
+* FEAT: allow disabling of dsn
+  * in some cases (backend search while you do have global DSN), you want to disable DSN to avoid a round-robin around the whole world if DNS can't be resolved as expected, for example on GCP. (#675)
+
+## 3.25.1
+>2018-03-08
+
+* FIX: fix protocol detection introduced in 3.25.0
+
+## 3.25.0
+>2018-03-07
+
+* FEAT: use https by default (#670)
+  * for very old devices https can be slow
+  * we defaulted to sending API requests over http on http sites
+  * this is now not really a performance bottleneck anymore
+  * https is now default, unless specified differently when initializing
+
+## 3.24.12
+>2018-02-28
+
+* FIX: Handle Node.js network errors more precisely (#669)
+
+## 3.24.11
+>2017-02-08
+
+* FIX: allow `index.getSettings` without any arguments
+  * this was a regression in 3.24.10
+  * added tests
+
+## 3.24.10
+>2017-02-07
+
+* FIX: correct `index.exportSynonyms` and `index.exportRules`
+  * they had the wrong implementation until now
+  * add some tests to prevent regression
+
+## 3.24.9
+>2017-12-29
+
+* FIX: make sure long API keys are sent via the POST body
+  * this wasn't the case in 3.24.8 because of a typo
+
+## 3.24.8
+>2017-12-13
+
+* add multi cluster management methods
+  * [ Assign or Move a userID to a cluster](https://algolia.com/doc/api-reference/api-methods/assign-user-id/): `assignUserID`
+  * [Get userID with highest number of records per cluster](https://algolia.com/doc/api-reference/api-methods/get-top-user-id/): `getTopUserID`
+  * [Returns the userID data stored in the mapping](https://algolia.com/doc/api-reference/api-methods/get-user-id/): `getUserID`
+  * [List the clusters available in a multi-clusters setup for a single appID](https://algolia.com/doc/api-reference/api-methods/list-clusters/): `listClusters`
+  * [List the userIDs assigned to a multi-clusters appID](https://algolia.com/doc/api-reference/api-methods/list-user-id/): `listUserIDs`
+  * [Remove a userID and associated data from the multi-clusters](https://algolia.com/doc/api-reference/api-methods/remove-user-id/): `removeUserID`
+  * [Search for userIDs](https://algolia.com/doc/api-reference/api-methods/search-user-id/): `searchUserIDs`
+
+## 3.24.7
+>2017-11-29
+
+* don't throw an error when `copyIndex` without scope is used
+  * if no scope is given, `settings`, as the default will be chosen
+
+## 3.24.6
+>2017-11-16
+
+* add `index.exportSynonyms` and `index.exportRules`
+  * they will get all of the synonyms or query rules on an index
+* add a new `scope` argument to `copyIndex`
+  * this is the third argument (from, to, scope)
+  * it's an array with settings, synonyms or rules
+  * if specified, that data will also be copied if you copy the index
+
+## 3.24.5
+>2017-10-02
+
+* feat(deleteBy): add deleteBy
+  * deleteByQuery is deprecated now
+  * the same, but now it happens at the indexing side
+  * no major changes should be seen, please report if any
+  * https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated#indexdeletebyquery
+
+## 3.24.4
+>2017-09-22
+
+* chore(build): provide jsDelivr the right file (#599)
+
+## 3.24.3
+>2017-07-24
+
+* chore(deprecation): remove deprecation from singular/plural methods; all these methods will stay existing
+  * index.addObject(obj) or index.addObjects([obj])
+  * index.partialUpdateObject(obj) or partialUpdateObjects([obj])
+  * index.saveObject(obj) or index.saveObjects([obj])
+  * index.deleteObject(objectID) or index.deleteObjects([objectID])
+  * index.getObject(objectID) or index.getObjects([objectID])
+
+## 3.24.2
+>2017-07-24
+
+* chore(deprecation): add deprecation message to all methods that will be removed in v4 (#573)
+  * index.addObject(obj) --> index.addObjects([obj])
+  * index.ttAdapter --> autocomplete.js
+  * index.partialUpdateObject(obj) --> partialUpdateObjects([obj])
+  * index.saveObject(obj) --> index.saveObjects([obj])
+  * index.deleteObject(objectID) --> index.deleteObjects([objectID])
+  * index.getObject(objectID) --> index.getObjects([objectID])
+  * see https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated for more information
+
+## 3.24.1
+>2017-07-20
+
+* feat(headers): add functions to remove and get extra headers (#572)
+  * client.setExtraHeader('X-cool-header','hello there');
+  * client.getExtraHeader('X-cool-header'); //hello there
+  * client.unsetExtraHeader('X-cool-header');
+  * client.getExtraHeader('X-cool-header'); //undefined
+* feat(deprecation): use console.warn to be more visible
+* refact(rules): Adapt to latest JSON schema for Query Rules
+
+## 3.24.0
+>2017-06-21
+
+* feat(rules): Add query rules beta version, you cannot use this for now
+
+## 3.23.0
+>2017-06-08
+
+* feat(places): Add places.getObject(); fixes algolia/places#381
+
+## 3.22.3
+>2017-05-29
+
+* fix(dependency): Do not pin debug dependency anymore
+fixes #556
+
+## 3.22.2
+>2017-05-18
+
+* fix(partialUpdateObjects): allow createIfNotExists (#552)
+  Much like partialUpdateObject, allow createIfNotExists
+  fixes #551
+
+## 3.22.1
+>2017-03-13
+
+* fix(packaging): put back envify in deps
+  * browserify transforms are applied for node_modules/pkg
+
+## 3.22.0
+>2017-03-13
+
+
+* feat(API): rename all *userKey[s] methods to *apiKey[s]
+  * client/index.listUserKeys() => client/index.listApiKeys()
+  * client/index.getUserKeyACL() => client/index.getApiKey()
+  * client/index.deleteUserKey() => client/index.deleteApiKey()
+  * client/index.addUserKey() => client/index.addApiKey()
+  * client/index.udpateUserKey() => client/index.updateApiKey()
+* fix(packaging): remove useless files for packaging (reduce package file size)
+
+## 3.21.1
+>2017-02-08
+
+
+* fix(browse*): use POST instead of GET to avoid limits (#503)
+
+## 3.21.0
+>2017-02-06
+
+
+* feat(x-algolia-agent): specify x-algolia-agent at search time
+* fix(parse): check for `global` existence before erasing
+
+## 3.20.4
+>2017-01-16
+
+
+* fix(retry strategy): handle cases were localStorage fails after success (#474)
+
+  Before this commit we only checked for localStorage failures at:
+  - page load
+  - localStorage.setItem
+
+  While in some situations websites could erase localStorage for the
+  whole page at any moment (between requests) and we were not resilient to that.
+
+* chore(forwardToSlave): deprecate forwardToSlaves in favour or forwardToReplicas
+
+## 3.20.3
+>2017-01-04
+
+
+* fix(agent): ensure algolia agent is not duplicated by successive calls
+
+## 3.20.2
+>2016-12-19
+
+
+* fix(nodejs): do not use let, 0.12 does not support it
+
+## 3.20.1
+>2016-12-17
+
+
+* fix(nodejs): on timeout, destroy the right response
+
+## 3.20.0
+>2016-12-14
+
+
+* feat(retry strategy): adjust retry strategy for all implementations
+  - Retry strategy now shares the last known host for a specific appId across the current domain (browsers)
+  or the current process (browsers without localStorage, Node.js). After 2 minutes we try to target back
+  the first host (Usually DSN)
+  - Retry strategy now shares the last known timeout multiplier that worked and set it back to default
+  after 2 minutes
+  - Retry strategy on Node.js now has a connect timeout of 2s
+  - Retry strategy on browsers (JavaScript, not jQuery, not Angular.js, not React Native for now) now
+  has a connect timeout of 1s
+  - You can now get and set timeouts per client with .setTimeouts({connect, read, write}), .getTimeouts().
+  Values are in ms.
+
+## 3.19.2
+>2016-11-28
+
+
+* fix(facet search): rename index.searchFacet to index.searchForFacetValues
+
+## 3.19.1
+>2016-11-11
+
+
+* fix(build): use regular debug module, issue with yui compressor was fixed here:
+https://github.com/visionmedia/debug/pull/315
+
+## 3.19.0
+>2016-10-26
+
+
+* feat(index.searchFacet): add method #345
+
+## 3.18.1
+>2016-08-31
+
+
+* fix(client.search): accept very long API keys
+  fixes #319
+
+  also fix uglify-js version because it's buggy in IE8:
+  https://github.com/mishoo/UglifyJS2/issues/1039
+
+## 3.18.0
+>2016-07-22
+
+
+* fix(debug): only activate debug messages on NODE_ENV==='debug'
+* feat(lite): add getObjects
+
+## 3.17.0
+>2016-07-06
+
+
+* feat(errors): add statusCode to errors
+* chore(shrinkwrap): completely remove shrinkwrap
+
+## 3.16.0
+>2016-06-22
+
+
+* feat(index.setSettings): add forwardToSlaves option
+index.setSettings({settings}, {forwardToSlaves}, cb);
+see https://www.algolia.com/doc/rest#change-index-settings
+
+## 3.15.1
+>2016-06-16
+
+
+* fix(getLogs): allow using the type parameter
+  The syntax is now getLogs(params[, cb])
+  fixes #232
+* fix(json): avoid throwing when late JSON response
+  fixes #284
+* fix(nodejs): allow universal lite applications
+  require('algoliasearch/lite') should work to
+  facilitate universal applications builds using
+  the lite build on frontend
+
+fixes #283
+
+## 3.15.0
+>2016-06-07
+
+
+* feat(synonyms): add new synonyms API
+
+## 3.14.6
+>2016-05-30
+
+
+* fix(places): allow empty credentials
+
+## 3.14.5
+>2016-05-26
+
+
+* fix(window): don't assume window is here
+
+When required in a node context, we may load the browser build without
+using it. Just for testing other parts of it.
+
+## 3.14.4
+>2016-05-25
+
+
+* fix(lite): lite package should have browse and browseFrom
+
+## 3.14.3
+>2016-05-25
+
+
+* fix(retry): also retry on non search methods when DNS failure
+
+  Before this commit, methods with no fallback support (basically every
+  method) would fail at retry if the DNS error occurred before the API
+  client timeout.
+
+  The behavior is now:
+  - when a method with a fallback errors because of DNS failure we will
+  switch to JSONP right away
+  - when a method with no fallback errors because of DNS failure we will
+  still retry
+
+  We could use the same mechanism for both (= always try all hosts
+  before JSONP) but I am not confident doing this change within a patch
+  or minor version. We have too litle data on how blocked XHRS are
+  triggered (async, sync?).
+
+  fixes #250
+
+  * fix(request strategy): comply with retry spec, no early JSONP switch
+
+    This commit brings more conformance with request strategy
+    specification by only raising timeout when there's a timeout.
+
+    It also stop trying to switch to JSONP asap and always try all hosts
+    using XHRS before.
+
+## 3.14.2
+>2016-05-24
+
+
+* fix(request strategy): increments hostIndex (host address) on fallback
+
+  When we are switching to fallback, increment host index so that JSONP
+  will use another host.
+
+  This issue was caused because we cannot distinguish CORS errors from DNS
+  resolution errors in the browser.
+
+  And the code for incrementing the host index was moved into the part
+  that switches to the fallback in
+  4ec5e6a1f8cd92924ce025d60646b3a47b7d8dca.
+
+  Truth is that the current solution is not optimal as, in the browser,
+  we are switching to JSONP when we can't resolve the server name.
+
+  This is only done because we are not sure how CORS request can be
+  blocked (synchronously, asynchronously).
+
+  In a next version we would drop it and wait for real issues to show up.
+
+## 3.14.1
+>2016-05-12
+
+
+* fix(retry strategy): retry on timeout was only using two hosts
+* fix(errors): provide err.debugData with necessary debugging information
+* fix(errors): force AlgoliaSearchError as default name
+fixes #241
+
+## 3.14.0
+>2016-05-04
+
+
+* feat(client.search): add strategy, fixes #208
+* fix(shuffle): shuffle host array like we did
+* feat(filesize): reduce filesize by removing lodash
+  All build size down 26% (20kb => 14kb)
+  New search-only build "algoliasearchLite[.min].js",
+  weights 9.5Kb (down 50% from normal build)
+  Also available as require('algoliasearch/lite');
+* fix(parse): handle parse cloud env having process.env defined
+
+## 3.13.1
+>2016-03-24
+
+
+* chore(dev): use phantomjs-prebuilt instead of phantomjs
+fixes #209
+
+## 3.13.0
+>2016-02-23
+
+
+* feat(initPlaces): add a static algoliasearch.initPlaces method
+
+## 3.12.0
+>2016-02-05
+
+
+* fix(apiKey): put the apiKey in the POST body when feasible and key > 500 chars
+* feat(falback): provide opts.useFallback to avoid using JSONP fallback
+
+## 3.11.0
+>2016-01-28
+
+
+* feat(partialUpdateObject): add createIfNotExists option: partialUpdateObject(object, createIfNotExists) https://www.algolia.com/doc/rest#partially-update-an-object
+* chore(dev): various fixes to the dev env
+
+## 3.10.2
+>2015-12-14
+
+
+* fix(request strategy): always use XHR first then switch to fallback
+
+  + add fallback for client.search
+
+  Context: before this commit, when a user lost connectivity (or XHR was
+  blocked, we cannot distinguish those events in browsers) then we
+  forced using JSONP for the whole session.
+
+  This led to a bug where you were stuck in a loop of non-available
+  fallback even if connectivity was restored.
+
+  This commit fixes this bug by always trying XHRS and fallbacking to
+  JSONP on a per request basis.
+* refactor(map): use lodash map instead of custom fn
+
+## 3.10.1
+>2015-12-11
+
+* fix(nodejs): consistent timeouts between nodejs versions
+  We now use basic setTimeout functionnality instead of
+  req.setTimeout in nodejs to avoid inconsistencies between nodejs engines.
+  In node 0.10, req.setTimeout was a socket inactivity timeout
+  In node > 0.10? req.setTimeout is now a global timeout
+  + Node.js timeout is now 30s global per request (then incremental)
+  + Fixed an edge case where we had an uncaught exception in nodejs
+  + We only support 0.10+ now, node 0.8 never worked
+
+## 3.10.0
+>2015-12-08
+
+* feat(gzip): ask the API for gzipped answers (nodejs)
+
+## 3.9.4
+>2015-12-04
+
+* fix(process.env): set process back to normal in node v4
+
+## 3.9.3
+>2015-12-01
+
+* fix(parse): set default timeout to 7.5s (indexing purposes)
+
+## 3.9.2
+>2015-11-02
+
+* feat: react native build beta
+
+## 3.9.1
+>2015-11-02
+
+* fix: throw on really bad usage instead of silently failing
+* fix: make angular build cache work
+
+## 3.9.0
+>2015-10-23
+
+* feat: add similarSearch beta method
+* feat: add new way to generate secured api key using query parameters
+
+## 3.8.1
+>2015-09-21
+
+
+* fix: disable chunked encoding on empty body DELETE requests in Node.js
+
+## 3.8.0
+>2015-09-01
+
+
+* test: node v4 is now the default testing env
+* fix: always send a unique response to the user
+* fix: use `lodash` by default, only use `lodash-compat` when building for the browser
+* feat: add client.addAlgoliaAgent() to augment the sent x-algolia-agent
+* chore: move all deps to ^version and add a shrinkwrap to allow reproducible builds
+
+## 3.7.8
+>2015-09-01
+
+
+* fix: ignore $http.defaults.headers.common in angular build
+* fix: force distinct false in deleteByQuery search
+* docs: precise initialization option values
+
+## 3.7.7
+>2015-08-27
+
+* fix: listen to socket errors in nodejs keepalive strategy
+
+## 3.7.6
+>2015-08-24
+
+* fix: encodeURIComponent(cursor) in browseFrom()
+
+## 3.7.5
+>2015-07-23
+
+* fix: withCredentials forced to false in Angular
+
+## 3.7.4
+>2015-07-20
+
+* fix: JSON.stringify fix when Array.prototype.toJSON is defined (prototype.js < 1.7 bug)
+
+## 3.7.3
+>2015-07-09
+
+* fix: make https over http proxy work
+
+## 3.7.2
+>2015-07-08
+
+* fix: remove a git clone dependency on install (debug/ms/yui compressor issue)
+
+## 3.7.1
+>2015-07-08
+
+* fix: do not call agent.destroy when there's no such method
+* fix: do not rely on any "smart" port computing
+
+## 3.7.0
+>2015-06-18
+
+* feat: allow passing `httpAgent` option to the nodejs client
+allow more flexible proxy/keepAlive agents in some more complex environments
+
+## 3.6.3
+>2015-06-18
+
+* fix: allow getting the debug log from the outside
+  will be used by diag tool and possibly anyone aware of https://github.com/visionmedia/debug
+
+## 3.6.2
+>2015-06-17
+
+* chore: add window.__algolia for easy debugging anywhere
+
+## 3.6.1
+>2015-06-16
+
+* fix: parse cloud build fix
+* fix: YUI compressor fix
+  fixes #113
+* test: automatic parse cloud build testing
+* fix: add application id to final error message ("Cannot connect to..")
+
+## 3.6.0
+>2015-06-05
+
+* feat: easy commonJS require of plugins
+  fixes #109
+* test: add dependency-check step in tests
+* refactor: use lodash-compat instead of less known/used/shared modules
+* chore: use uglifyjs instead of closure compiler
+* feat: allow passing hosts as hosts.read, hosts.write
+* feat: allow passing Algolia Agent as an option
+* fix: CORS simple request for all browser builds
+  fixes #111
+
+## 3.5.0
+>2015-06-03
+
+* fix: send a descriptive timeout error when it occurs
+* chore: add headers debugging
+* fix: incremental wait waitTask
+  will now wait 100ms * loopTickNumber * loopTickNumber
+  fixes #102
+* fix: do not use global when we know we will be in a browser
+  browserify `global` is not always `window` can be 
+ fixes #99 +* feat: new browse()/browseFrom()/browseAll() + - `browse(query, queryParameters)` now has the same signature than + search(). You can use any `query` and `queryParameters`. + - `browseFrom(cursor)` can be used as an efficient way to + continue (next page) a previous `browse()` call. All browse responses now have a `cursor` property. + - `browseAll(query, queryParameters)` can be used to get all + the content of your index + It returns an [EventEmitter](https://nodejs.org/api/events.html). + + Available events: + - `result` + - `end` + - `error` (you should listen to it or it will throw) + + There's also `stop()` method on the event emitter so that you can + stop browsing at any point. + + fixes #101 +* fix: support typeahead 0.11 + fixes #105 + +## 3.4.0 +>2015-05-23 + +* FIX: Remove debugging messages from builds + fixes #91 + fixes #86 +* FIX: Handle badly formatted JSON + fixes #89 + fixes #90 +* FIX: Stop bytes sent/received debug on node 0.12 when socket closes +* test: Test on only 3 browsers in travis + fixes #61 +* test: split desktop/mobile into a travis matrix, reduces travis timeouts +* feat: add client.batch() +* feature: getObjects now accepts attributesToRetrieve: + signature: getObjects(objectIDs, attributesToRetrieve, callback) +* fix: send `x-algolia-agent` instead of `x-user-agent` +* fix: clone cached results hits sent + fixes #79 +* feat: New parameters for API keys + Added description, referers, queryParameters + ref: https://www.algolia.com/changes#released-## + >2015-05-08 + +* fix: IE11 xhr cache was fixed by adding cache-control: no-cache in API headers +* test: add integration testing and rework the travis tasks + +## 3.3.2 +>2015-05-14 + +* FIX: more fixes to the use of $q() in the angular build, now compatible with 1.2.xx and tested + +## 3.3.1 +>2015-05-12 + +* FIX: Compatibility with AngularJS old promise implementation + We now use $q.defer() instead of $q() + +## 3.3.0 +>2015-05-12 + +* FEATURE: expose window.algoliasearch = algoliasearch in jquery/angular plugins. So that +you can use the original algoliasearch() implem in plugins. Otherwise you would have to load both + +## 3.2.4 +>2015-05-11 + +* FIX: 15s inactivity timeout for nodejs implementation, should help high latency/low connection users +* CHORE: test on iojs2 +* CHORE: debugging messages now more focused and less verbose +* PERF: use JSON.stringify only once per request + +## 3.2.3 +>2015-05-09 + +* FIX: Parse build requires a charset on requests + +## 3.2.2 +>2015-05-06 + +* FIX : missing require (crypto) for node.js client (#84) + +## 3.2.1 +>2015-04-29 + +* add dist/ to npmjs repository, so cdnjs autoupdate works + +## 3.2.0 +>2015-04-24 + +* FEATURE: Parse build now uses V3 implementation +* FEATURE: Parse build now returns parse promises. https://parse.com/docs/js_guide#promises +* FEATURE: Automatically handle HTTP_PROXY HTTPS_PROXY environment variables +in Node.js client + +## 3.1.0 +>2015-04-14 + +* CHANGE: Node.js will now use http headers instead of inlining them into the api call as browsers are doing +* CHANGE: Ensure all headers (http, querystrings) are lowercased. Both are supported by our API, reduces FUD +* FEATURE: add index.getObjects() +* FEATURE: add index.deleteObjects() +* FIX: waitTask failure case when using promises, was not going through +* FEATURE: add index.deleteByQuery() +* FEATURE (Node.js): add client.enableRateLimitForward() +* FEATURE (Node.js): add client.disableRateLimitForward() +* FEATURE (Node.js): add client.useSecuredAPIKey() +* FEATURE (Node.js): add client.disableSecuredAPIKey() +* BREAKING CHANGE (Node.js): client.setTimeout renamed to client.setRequestTimeout +* TEST: add test around setRequestTimeout +* FEATURE (Node.js): add client.generateSecuredApiKey() +* FEATURE: + - all clients are now exposing algoliasearch.ua + - all requests are now sending x-user-agent containing for example 'Algoliasearch for vanilla JavaScript 3.0.7' +* FEATURE: add client.search() + Search against multiple indices, equivalent of, multipleQueries, or + startQueriesBatch+addQueryInBatch+sendQueriesBatch +* BREAKING CHANGE (Node.js): removed client.multipleQueries(), use client.search(queries) +* DEPRECIATION (browser): client.startQueriesBatch/addQueryInBatch/sendQueriesBatch + Use client.search(queries) +* FEATURE: new HA implementation + We now use two different DNS to perform all requests + Removed the tld option as it's no more needed nor compatible with having a new HA implementation with different tlds (.com/.net) +* DEPRECIATION: client.addUserKeyWithValidity(), index.addUserKeyWithValidity() + You can now use client.addUserKey(acls, params, cb), index.addUserKey(acls, params, cb) +* FEATURE: client|index.updateUserKey(key, acls, params, callback) + Update a user key, provide acls and optional params +* BREAKING CHANGE (Node.js): + As we now have addUserKey and updateUserKey, we removed: + - client|index.addUserKeyWithValidityAndIndexes + - client|index.updateUserKeyWithValidity + - client|index.updateUserKeyWithValidityAndIndexes + You can use client|index.addUserKey or client|index.updateUserKey to do + deal with all you keys needs + +## 3.0.7 +>2015-04-10 + +* MIGRATION: throw on V2 .search() usage: + - .search(query, cb, params) + - .search(cb, params) +* FEATURE: add client.destroy() in Node.js implementation + will destroy all keepalived connections and let the process exits if needed + +## 3.0.6 +>2015-04-03 + +* FIX: Webpack compatibility by removing packageify + webpack does not interprets for now node_modules/* browserify transforms +* MISC: add release script + +## 3.0.5 +>2015-04-02 + +* FIX: lower the build size by requiring the good version/browser.js in browser + builds. Previously we got the full `package.json` inlined + +## 3.0.4 +>2015-04-02 + +* FIX: Defaults to http when the protocol of the page is not http or + https +* FIX: XDomainRequests no more aborted in IE9 #76 + +## 3.0.3 +>2015-03-28 + + +* FIX: Handle module loaders + cdn.jsdelivr.net/algoliasearch/latest usage + When in this situation, the module loader would prevent the code + detecting and loading the V2 to execute. + Now fixed by prepending the migration-layer to the browserify bundle. + It also removes the migration-layer code from the package managers (npm, bower) builds, + where it makes no sense to have it +* FIX: load V2 using a DOMElement when V3 is loaded ASYNCHRONOUSLY with /latest/ + * tested on all majors browsers: + Chrome stable + Firefox stable + IE 8, 9, 10, 11 + * tested on corresponding customer websites +* FIX: better warning messages when using `latest` or trying to use V3 as V2 + +## 3.0.2 +>2015-03-26 + + +* temp /latest/ fix for some clients loading /latest/ in async mode + +## 3.0.1 +>2015-03-26 + + +* fix npm usage, was missing a dependency + +## 3.0.0 +>2015-03-25 + + +* V2 users, see the migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x +* BREAKING CHANGES: + * `new AlgoliaSearch()` => `algoliasearch()` #40 + - The only exported global property is now `algoliasearch` + - `opts`: + - `dsnHost` removed, use `hosts:[dsnHost, other hosts]` + - `dsn` removed, use `hosts:[non-dsn-host, non-dsn-host]` + - `requestTimeoutInMs` renamed to `timeout` + - `method` renamed to `protocol`. Protocol should be specified as `http:` or `https:` + - `hosts` behavior changed, when using custom hosts: + - no shuffling + - no dsn host is prepended to the list + - removed `AlgoliaExplainResults`, no obvious use ATM + - calling algoliasearch() without an applicationID or apiKey will throw + - Helper: `new AlgoliaSearchHelper()` => `algoliasearch.helper()`, same function signature + * no more window.ALGOLIA_VERSION + - you can find the version in algoliasearch.version + * all api methods now use the (err, content) convention #43 + It means that instead of doing: + ```js + index.search(function found(success, content) { + if (!success) { + console.log('Error was:', content); + return; + } + + console.log('OK!', content); + }); + ``` + You must do: + ```js + index.search(function found(err, content) { + if (err) { + console.log('Error was:', err); + return; + } + + console.log('OK!', content); + }); + ``` + This is in par with most asynchronous APIS accepted/de vfacto conventions. + It helps being compatible with control flow libraries like `async` and + helps reduce WTF moments of Node.JS developers used to cb(err, content) + * client.getLogs(cb[, offset, length]) is now .getLogs([offset, length, cb]) + * client.listIndexes(cb[, page]) => .listIndexes([page, cb]) + * client|index.addUserKeyWithValidity(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, cb) => client|index.addUserKeyWithValidity(acls, params, cb) where params = {validity:, maxQueriesPerIPPerHour:, maxHitsPerQuery:} + * client.sendQueriesBatch(cb, delay) => client.sendQueriesBatch(cb) + * index.addObject(content[, callback, objectID]) => index.addObject(content[, objectID, callback]) + * index.getObject(objectID[, callback, attributes]) => index.getObject(objectID[, attrs, callback]) + * index.search(query, callback[, params, delay]) => index.search(query[, params callback]) or index.search(params[, cb]) with params = {query: 'some thing'} + We removed the delay option which should really be implemented by the module consumer. It's really a throttle/debounce of the search functions + * index.browse(page, cb[, hitsPerPage]) => index.browse(page[, hitsPerPage, cb]) + * no more opts.jsonp in algoliasearch() + Previously used as a way to force/disable jsonp. The request strategy is now more robust and does not + requires jsonp: param + * JSON2.js is no more included in the main build. + If you need to support IE7 <= or IE8 quirks, add this: + ```html + + ``` + * removed AlgoliaSearchHelper + Please see https://github.com/algolia/algoliasearch-helper-js if you need the helper + * NEW FEATURES: + * UMD compatibility #41 + - algoliasearch can now be used with any module loader + - build is now done with browserify + - no more grunt + * All calls are now returning promises #45 + If there's a callback given to an API call, you won't get a promise. + Here are the different promises implementations we use: + - Native promises by default (https://github.com/jakearchibald/es6-promise/) + - AngularJS promises for AngularJS plugin + - jQuery promises for jQuery plugin +* FIXES: + * do not retry when server sends 4xx or 1xx + * distinguish jQuery/AngularJS request timeouts from errors and thus retry when timeout + * JSONP fallback when jQuery/AngularJS request error +* MISC: + * Externalize plugins and request implementations + * some linting fixes + * test suite, including request strategy test suite + * IE10 should now behave as a CORS supported XMLHttpRequest browser, as it is + * running tests on many browsers, using saucelabs + * removed vendor/ + - vendor/json2.js + - vendor/jquery.typeahead.js now on cdnjs (examples) + +## 2.9.2 +>2015-02-03 + +* Fixed calls to `.search(function() {})`, `.search(null, function() {})`, `.search(undefined, function() {}) +* Fixed shared cache amongst instances + +## 2.9.1 +>2015-02-03 + +* Fixed listIndexes pagination, not working if page=0 + +## 2.9.0 +>2015-01-13 + +* Angular.js & jQuery compatibility (returning promises) +* Helper: ability to exclude facets + +## 2.8.6 +>2015-01-13 + +* Search helper: ability to set default facet filters + +## 2.8.5 +>2015-01-07 + +* Avoid CORS preflight request + +## 2.8.4 +>2014-12-31 + +* Fixed a bug in JSONP fallback on multi-queries and getObject when a secured API Key was used (X-Algolia-TagFilters parameter was not set, resulting in a 403 permission denied error) + +## 2.8.3 +>2014-12-30 + +* Helper optimization: number of nested queries is now driven by the number of refined disjunctive facets only + +## 2.8.2 +>2014-12-23 + +* Increase the request timeout after each retry + +## 2.8.1 +>2014-12-09 + +* Enabled DSN by default (is working even if only one datacenter is selected) + +## 2.8.0 +>2014-11-28 + +* Move to algolia.net by default + +## 2.7.5 +>2014-11-25 + +* Expose one option to select the TTL (prepare migration to .net, allow to gain some milliseconds compared to .io) + +## 2.7.4 +>2014-11-12 + +* Expose more options (jsonp & requestTimeoutInMs) +* Change the way the JSONP fallback works + +## 2.7.3 +>2014-10-29 + +* Reintroduced Algolia Custom Headers (X-Algolia-*) on queries. It was removed to try to remove the CORS OPTIONS query but this has no impact because this is a POST action. + +## 2.7.2 +>2014-10-27 + +* Fixed browse method (author @muertet) + +## 2.7.{0,1} +>2014-10-15 + +* Refactor the AlgoliaSearch constructor to allows optional/named arguments + +## 2.6.6 +>2014-10-07 + +* Fixed bower integration + +## 2.6.5 +>2014-09-26 + +* Optimize disjunctive queries that were retrieving useless attributes + +## 2.6.4 +>2014-09-23 + + * Fixed CORS handling issue on IE8/IE9 + +## 2.6.3 +>2014-09-23 + + * Fixed IE11-based AJAX calls + +## 2.6.2 +>2014-09-20 + + * Updated package manager description files: npm and jsdelivr + +## 2.6.1 +>2014-09-20 + + * Fixed JSONP call: 'X-Algolia-TagFilters' and 'X-Algolia-UserToken' were not included in the URL. + +## 2.6.0 +>2014-09-06 + + * Improved retry strategy with implementation of a timeout on network call. + Upgrade to this version is highly recommended + +## 2.5.4 +>2014-09-04 + +* Removed the OPTIONS request for the first isalive query of for all search queries + +## 2.5.3 +>2014-07-23 + +* AlgoliaSearchHelper: add missing getIndex/setIndex +* listIndexes is now paginable + +## 2.5.2 +>2014-06-05 + +* AlgoliaSearchHelper: add getter/setter methods + +## 2.5.1 +>2014-05-14 + +* Fixed listIndexes call (trailing slash not compatible with our API anymore) + +## 2.5.0 +>2014-05-09 + +* If CORS is not available, use JSONP + +## 2.4.7 +>2014-04-23 + +* Ability to instantiate multiple AlgoliaSearchHelper + +## 2.4.6 +>2014-04-05 + +* Fixed IE 8/9 support of secured API keys + +## 2.4.5 +>2014-03-30 + +* Improved AlgoliaExplainResults helper handling array-based attributes as well. + +## 2.4.4 +>2014-03-19 + +* Improved the `waitTask` method, ensuring it sleeps 100ms before recalling the API. + +## 2.4.3 +>2014-03-12 + +* Removed invalid character from source code (could cause a parse error in IE8/9) + +## 2.4.2 +>2014-02-24 + +* Added support of secured API Key (SecurityTags & UserToken headers) + +## 2.4.1 +>2014-02-18 + +* Removed dependency on jquery + +## 2.4.0 +>2014-02-10 + + +* Move to official Typeahead.js release (0.10.1) +* Remove getTypeAheadTransport* functions + +## 2.3.8 +>2014-01-16 + + +* Ability to customize getTypeAheadTransport parameters once instantiated + +## 2.3.7 +>2014-01-08 + + +* Remove extra encodeURI +* Travis integration + +## 2.3.6 +>2014-01-06 + + +* Missing encodeURI +* Added "distinct" documentation + +## 2.3.5 +>2013-12-06 + + +* Added browse methods + +## 2.3.4-1 +>2013-12-04 + + +* Fixed IE9 bug while reading onload's event +* Here and there typos + +## 2.3.0 +>2013-11-07 + + +* Added new ACL features (maxQueriesPerIPPerHour & maxHitsPerQuery) + +## 2.2.0 +>2013-11-07 + + +* Added clearIndex method + +## 2.1.0 +>2013-11-03 + + +* Auto-detect protocol to used based on current location +* Added auto-complete & instant-search examples + +## 2.0.1 +>2013-10-15 + + +* Gruntification +* Embed typeahead.js diff --git a/js/algoliasearch/LICENSE.txt b/js/algoliasearch/LICENSE.txt new file mode 100644 index 0000000..8c315f4 --- /dev/null +++ b/js/algoliasearch/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2013 Algolia +http://www.algolia.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/js/algoliasearch/bower.json b/js/algoliasearch/bower.json new file mode 100644 index 0000000..93111c6 --- /dev/null +++ b/js/algoliasearch/bower.json @@ -0,0 +1,24 @@ +{ + "name": "algoliasearch", + "version": "3.35.1", + "homepage": "https://github.com/algolia/algoliasearch-client-js", + "authors": [ + "Algolia Team " + ], + "description": "Algolia Search is a search API that provides hosted full-text, numerical and faceted search.", + "main": "dist/algoliasearch.js", + "keywords": [ + "js", + "algolia", + "search", + "api", + "rest" + ], + "license": "MIT", + "ignore": [ + "examples", + "node_modules", + "bower_components", + "test" + ] +} diff --git a/js/algoliasearch/dist/algoliasearch.angular.js b/js/algoliasearch/dist/algoliasearch.angular.js new file mode 100644 index 0000000..2856434 --- /dev/null +++ b/js/algoliasearch/dist/algoliasearch.angular.js @@ -0,0 +1,7398 @@ +/*! algoliasearch 3.35.1 | © 2014, 2015 Algolia SAS | github.com/algolia/algoliasearch-client-js */ +(function(f){var g;if(typeof window!=='undefined'){g=window}else if(typeof self!=='undefined'){g=self}g.ALGOLIA_MIGRATION_LAYER=f()})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;owindow.ALGOLIA_SUPPORTS_DOCWRITE = true\x3C/script>'); + + if (window.ALGOLIA_SUPPORTS_DOCWRITE === true) { + document.write('\x3Cscript src="' + v2ScriptUrl + '">\x3C/script>'); + scriptLoaded('document.write')(); + } else { + loadScript(v2ScriptUrl, scriptLoaded('DOMElement')); + } + } catch (e) { + loadScript(v2ScriptUrl, scriptLoaded('DOMElement')); + } +} + +function scriptLoaded(method) { + return function log() { + var message = 'AlgoliaSearch: loaded V2 script using ' + method; + + if (window.console && window.console.log) { + window.console.log(message); + } + }; +} + +},{"1":1}],4:[function(require,module,exports){ +'use strict'; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +module.exports = oldGlobals; + +// put old window.AlgoliaSearch.. into window. again so that +// users upgrading to V3 without changing their code, will be warned +function oldGlobals() { + var message = '-- AlgoliaSearch V2 => V3 error --\n' + + 'You are trying to use a new version of the AlgoliaSearch JavaScript client with an old notation.\n' + + 'Please read our migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x\n' + + '-- /AlgoliaSearch V2 => V3 error --'; + + window.AlgoliaSearch = function() { + throw new Error(message); + }; + + window.AlgoliaSearchHelper = function() { + throw new Error(message); + }; + + window.AlgoliaExplainResults = function() { + throw new Error(message); + }; +} + +},{}],5:[function(require,module,exports){ +'use strict'; + +// This script will be browserified and prepended to the normal build +// directly in window, not wrapped in any module definition +// To avoid cases where we are loaded with /latest/ along with +migrationLayer("algoliasearch.angular"); + +// Now onto the V2 related code: +// If the client is using /latest/$BUILDNAME.min.js, load V2 of the library +// +// Otherwise, setup a migration layer that will throw on old constructors like +// new AlgoliaSearch(). +// So that users upgrading from v2 to v3 will have a clear information +// message on what to do if they did not read the migration guide +function migrationLayer(buildName) { + var isUsingLatest = require(2); + var loadV2 = require(3); + var oldGlobals = require(4); + + if (isUsingLatest(buildName)) { + loadV2(buildName); + } else { + oldGlobals(); + } +} + +},{"2":2,"3":3,"4":4}]},{},[5])(5) +});(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { + return true; + } + + // is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); +} + +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + +exports.formatters.j = function(v) { + try { + return JSON.stringify(v); + } catch (err) { + return '[UnexpectedJSONParseError]: ' + err.message; + } +}; + + +/** + * Colorize log arguments if enabled. + * + * @api public + */ + +function formatArgs(args) { + var useColors = this.useColors; + + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); + + if (!useColors) return; + + var c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit') + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); +} + +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; +} + +/** + * Enable namespaces listed in `localStorage.debug` initially. + */ + +exports.enable(load()); + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + +function localstorage() { + try { + return window.localStorage; + } catch (e) {} +} + +}).call(this,require(12)) +},{"12":12,"2":2}],2:[function(require,module,exports){ + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = require(9); + +/** + * The currently active debug mode names, and names to skip. + */ + +exports.names = []; +exports.skips = []; + +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + +exports.formatters = {}; + +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * @param {String} namespace + * @return {Number} + * @api private + */ + +function selectColor(namespace) { + var hash = 0, i; + + for (i in namespace) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return exports.colors[Math.abs(hash) % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function createDebug(namespace) { + + function debug() { + // disabled? + if (!debug.enabled) return; + + var self = debug; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // turn the `arguments` into a proper Array + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %O + args.unshift('%O'); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // apply env-specific formatting (colors, etc.) + exports.formatArgs.call(self, args); + + var logFn = debug.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.enabled = exports.enabled(namespace); + debug.useColors = exports.useColors(); + debug.color = selectColor(namespace); + + // env-specific initialization logic for debug instances + if ('function' === typeof exports.init) { + exports.init(debug); + } + + return debug; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + exports.names = []; + exports.skips = []; + + var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + +},{"9":9}],3:[function(require,module,exports){ +(function (process,global){ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE + * @version 4.1.1 + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.ES6Promise = factory()); +}(this, (function () { 'use strict'; + +function objectOrFunction(x) { + var type = typeof x; + return x !== null && (type === 'object' || type === 'function'); +} + +function isFunction(x) { + return typeof x === 'function'; +} + +var _isArray = undefined; +if (Array.isArray) { + _isArray = Array.isArray; +} else { + _isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; +} + +var isArray = _isArray; + +var len = 0; +var vertxNext = undefined; +var customSchedulerFn = undefined; + +var asap = function asap(callback, arg) { + queue[len] = callback; + queue[len + 1] = arg; + len += 2; + if (len === 2) { + // If len is 2, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + if (customSchedulerFn) { + customSchedulerFn(flush); + } else { + scheduleFlush(); + } + } +}; + +function setScheduler(scheduleFn) { + customSchedulerFn = scheduleFn; +} + +function setAsap(asapFn) { + asap = asapFn; +} + +var browserWindow = typeof window !== 'undefined' ? window : undefined; +var browserGlobal = browserWindow || {}; +var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; +var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]'; + +// test for web worker but not in IE10 +var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined'; + +// node +function useNextTick() { + // node version 0.10.x displays a deprecation warning when nextTick is used recursively + // see https://github.com/cujojs/when/issues/410 for details + return function () { + return process.nextTick(flush); + }; +} + +// vertx +function useVertxTimer() { + if (typeof vertxNext !== 'undefined') { + return function () { + vertxNext(flush); + }; + } + + return useSetTimeout(); +} + +function useMutationObserver() { + var iterations = 0; + var observer = new BrowserMutationObserver(flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function () { + node.data = iterations = ++iterations % 2; + }; +} + +// web worker +function useMessageChannel() { + var channel = new MessageChannel(); + channel.port1.onmessage = flush; + return function () { + return channel.port2.postMessage(0); + }; +} + +function useSetTimeout() { + // Store setTimeout reference so es6-promise will be unaffected by + // other code modifying setTimeout (like sinon.useFakeTimers()) + var globalSetTimeout = setTimeout; + return function () { + return globalSetTimeout(flush, 1); + }; +} + +var queue = new Array(1000); +function flush() { + for (var i = 0; i < len; i += 2) { + var callback = queue[i]; + var arg = queue[i + 1]; + + callback(arg); + + queue[i] = undefined; + queue[i + 1] = undefined; + } + + len = 0; +} + +function attemptVertx() { + try { + var r = require; + var vertx = r('vertx'); + vertxNext = vertx.runOnLoop || vertx.runOnContext; + return useVertxTimer(); + } catch (e) { + return useSetTimeout(); + } +} + +var scheduleFlush = undefined; +// Decide what async method to use to triggering processing of queued callbacks: +if (isNode) { + scheduleFlush = useNextTick(); +} else if (BrowserMutationObserver) { + scheduleFlush = useMutationObserver(); +} else if (isWorker) { + scheduleFlush = useMessageChannel(); +} else if (browserWindow === undefined && typeof require === 'function') { + scheduleFlush = attemptVertx(); +} else { + scheduleFlush = useSetTimeout(); +} + +function then(onFulfillment, onRejection) { + var _arguments = arguments; + + var parent = this; + + var child = new this.constructor(noop); + + if (child[PROMISE_ID] === undefined) { + makePromise(child); + } + + var _state = parent._state; + + if (_state) { + (function () { + var callback = _arguments[_state - 1]; + asap(function () { + return invokeCallback(_state, child, callback, parent._result); + }); + })(); + } else { + subscribe(parent, child, onFulfillment, onRejection); + } + + return child; +} + +/** + `Promise.resolve` returns a promise that will become resolved with the + passed `value`. It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + resolve(1); + }); + + promise.then(function(value){ + // value === 1 + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.resolve(1); + + promise.then(function(value){ + // value === 1 + }); + ``` + + @method resolve + @static + @param {Any} value value that the returned promise will be resolved with + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` +*/ +function resolve$1(object) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + var promise = new Constructor(noop); + resolve(promise, object); + return promise; +} + +var PROMISE_ID = Math.random().toString(36).substring(16); + +function noop() {} + +var PENDING = void 0; +var FULFILLED = 1; +var REJECTED = 2; + +var GET_THEN_ERROR = new ErrorObject(); + +function selfFulfillment() { + return new TypeError("You cannot resolve a promise with itself"); +} + +function cannotReturnOwn() { + return new TypeError('A promises callback cannot return that same promise.'); +} + +function getThen(promise) { + try { + return promise.then; + } catch (error) { + GET_THEN_ERROR.error = error; + return GET_THEN_ERROR; + } +} + +function tryThen(then$$1, value, fulfillmentHandler, rejectionHandler) { + try { + then$$1.call(value, fulfillmentHandler, rejectionHandler); + } catch (e) { + return e; + } +} + +function handleForeignThenable(promise, thenable, then$$1) { + asap(function (promise) { + var sealed = false; + var error = tryThen(then$$1, thenable, function (value) { + if (sealed) { + return; + } + sealed = true; + if (thenable !== value) { + resolve(promise, value); + } else { + fulfill(promise, value); + } + }, function (reason) { + if (sealed) { + return; + } + sealed = true; + + reject(promise, reason); + }, 'Settle: ' + (promise._label || ' unknown promise')); + + if (!sealed && error) { + sealed = true; + reject(promise, error); + } + }, promise); +} + +function handleOwnThenable(promise, thenable) { + if (thenable._state === FULFILLED) { + fulfill(promise, thenable._result); + } else if (thenable._state === REJECTED) { + reject(promise, thenable._result); + } else { + subscribe(thenable, undefined, function (value) { + return resolve(promise, value); + }, function (reason) { + return reject(promise, reason); + }); + } +} + +function handleMaybeThenable(promise, maybeThenable, then$$1) { + if (maybeThenable.constructor === promise.constructor && then$$1 === then && maybeThenable.constructor.resolve === resolve$1) { + handleOwnThenable(promise, maybeThenable); + } else { + if (then$$1 === GET_THEN_ERROR) { + reject(promise, GET_THEN_ERROR.error); + GET_THEN_ERROR.error = null; + } else if (then$$1 === undefined) { + fulfill(promise, maybeThenable); + } else if (isFunction(then$$1)) { + handleForeignThenable(promise, maybeThenable, then$$1); + } else { + fulfill(promise, maybeThenable); + } + } +} + +function resolve(promise, value) { + if (promise === value) { + reject(promise, selfFulfillment()); + } else if (objectOrFunction(value)) { + handleMaybeThenable(promise, value, getThen(value)); + } else { + fulfill(promise, value); + } +} + +function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._result); + } + + publish(promise); +} + +function fulfill(promise, value) { + if (promise._state !== PENDING) { + return; + } + + promise._result = value; + promise._state = FULFILLED; + + if (promise._subscribers.length !== 0) { + asap(publish, promise); + } +} + +function reject(promise, reason) { + if (promise._state !== PENDING) { + return; + } + promise._state = REJECTED; + promise._result = reason; + + asap(publishRejection, promise); +} + +function subscribe(parent, child, onFulfillment, onRejection) { + var _subscribers = parent._subscribers; + var length = _subscribers.length; + + parent._onerror = null; + + _subscribers[length] = child; + _subscribers[length + FULFILLED] = onFulfillment; + _subscribers[length + REJECTED] = onRejection; + + if (length === 0 && parent._state) { + asap(publish, parent); + } +} + +function publish(promise) { + var subscribers = promise._subscribers; + var settled = promise._state; + + if (subscribers.length === 0) { + return; + } + + var child = undefined, + callback = undefined, + detail = promise._result; + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + if (child) { + invokeCallback(settled, child, callback, detail); + } else { + callback(detail); + } + } + + promise._subscribers.length = 0; +} + +function ErrorObject() { + this.error = null; +} + +var TRY_CATCH_ERROR = new ErrorObject(); + +function tryCatch(callback, detail) { + try { + return callback(detail); + } catch (e) { + TRY_CATCH_ERROR.error = e; + return TRY_CATCH_ERROR; + } +} + +function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value = undefined, + error = undefined, + succeeded = undefined, + failed = undefined; + + if (hasCallback) { + value = tryCatch(callback, detail); + + if (value === TRY_CATCH_ERROR) { + failed = true; + error = value.error; + value.error = null; + } else { + succeeded = true; + } + + if (promise === value) { + reject(promise, cannotReturnOwn()); + return; + } + } else { + value = detail; + succeeded = true; + } + + if (promise._state !== PENDING) { + // noop + } else if (hasCallback && succeeded) { + resolve(promise, value); + } else if (failed) { + reject(promise, error); + } else if (settled === FULFILLED) { + fulfill(promise, value); + } else if (settled === REJECTED) { + reject(promise, value); + } +} + +function initializePromise(promise, resolver) { + try { + resolver(function resolvePromise(value) { + resolve(promise, value); + }, function rejectPromise(reason) { + reject(promise, reason); + }); + } catch (e) { + reject(promise, e); + } +} + +var id = 0; +function nextId() { + return id++; +} + +function makePromise(promise) { + promise[PROMISE_ID] = id++; + promise._state = undefined; + promise._result = undefined; + promise._subscribers = []; +} + +function Enumerator$1(Constructor, input) { + this._instanceConstructor = Constructor; + this.promise = new Constructor(noop); + + if (!this.promise[PROMISE_ID]) { + makePromise(this.promise); + } + + if (isArray(input)) { + this.length = input.length; + this._remaining = input.length; + + this._result = new Array(this.length); + + if (this.length === 0) { + fulfill(this.promise, this._result); + } else { + this.length = this.length || 0; + this._enumerate(input); + if (this._remaining === 0) { + fulfill(this.promise, this._result); + } + } + } else { + reject(this.promise, validationError()); + } +} + +function validationError() { + return new Error('Array Methods must be provided an Array'); +} + +Enumerator$1.prototype._enumerate = function (input) { + for (var i = 0; this._state === PENDING && i < input.length; i++) { + this._eachEntry(input[i], i); + } +}; + +Enumerator$1.prototype._eachEntry = function (entry, i) { + var c = this._instanceConstructor; + var resolve$$1 = c.resolve; + + if (resolve$$1 === resolve$1) { + var _then = getThen(entry); + + if (_then === then && entry._state !== PENDING) { + this._settledAt(entry._state, i, entry._result); + } else if (typeof _then !== 'function') { + this._remaining--; + this._result[i] = entry; + } else if (c === Promise$2) { + var promise = new c(noop); + handleMaybeThenable(promise, entry, _then); + this._willSettleAt(promise, i); + } else { + this._willSettleAt(new c(function (resolve$$1) { + return resolve$$1(entry); + }), i); + } + } else { + this._willSettleAt(resolve$$1(entry), i); + } +}; + +Enumerator$1.prototype._settledAt = function (state, i, value) { + var promise = this.promise; + + if (promise._state === PENDING) { + this._remaining--; + + if (state === REJECTED) { + reject(promise, value); + } else { + this._result[i] = value; + } + } + + if (this._remaining === 0) { + fulfill(promise, this._result); + } +}; + +Enumerator$1.prototype._willSettleAt = function (promise, i) { + var enumerator = this; + + subscribe(promise, undefined, function (value) { + return enumerator._settledAt(FULFILLED, i, value); + }, function (reason) { + return enumerator._settledAt(REJECTED, i, reason); + }); +}; + +/** + `Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = resolve(2); + let promise3 = resolve(3); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` + + If any of the `promises` given to `all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = reject(new Error("2")); + let promise3 = reject(new Error("3")); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` + + @method all + @static + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static +*/ +function all$1(entries) { + return new Enumerator$1(this, entries).promise; +} + +/** + `Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. + + Example: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 2'); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // result === 'promise 2' because it was resolved before promise1 + // was resolved. + }); + ``` + + `Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error('promise 2')); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === 'promise 2' because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` + + An example real-world use case is implementing timeouts: + + ```javascript + Promise.race([ajax('foo.json'), timeout(5000)]) + ``` + + @method race + @static + @param {Array} promises array of promises to observe + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. +*/ +function race$1(entries) { + /*jshint validthis:true */ + var Constructor = this; + + if (!isArray(entries)) { + return new Constructor(function (_, reject) { + return reject(new TypeError('You must pass an array to race.')); + }); + } else { + return new Constructor(function (resolve, reject) { + var length = entries.length; + for (var i = 0; i < length; i++) { + Constructor.resolve(entries[i]).then(resolve, reject); + } + }); + } +} + +/** + `Promise.reject` returns a promise rejected with the passed `reason`. + It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.reject(new Error('WHOOPS')); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + @method reject + @static + @param {Any} reason value that the returned promise will be rejected with. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. +*/ +function reject$1(reason) { + /*jshint validthis:true */ + var Constructor = this; + var promise = new Constructor(noop); + reject(promise, reason); + return promise; +} + +function needsResolver() { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); +} + +function needsNew() { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); +} + +/** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise's eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + let promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + let xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {function} resolver + Useful for tooling. + @constructor +*/ +function Promise$2(resolver) { + this[PROMISE_ID] = nextId(); + this._result = this._state = undefined; + this._subscribers = []; + + if (noop !== resolver) { + typeof resolver !== 'function' && needsResolver(); + this instanceof Promise$2 ? initializePromise(this, resolver) : needsNew(); + } +} + +Promise$2.all = all$1; +Promise$2.race = race$1; +Promise$2.resolve = resolve$1; +Promise$2.reject = reject$1; +Promise$2._setScheduler = setScheduler; +Promise$2._setAsap = setAsap; +Promise$2._asap = asap; + +Promise$2.prototype = { + constructor: Promise$2, + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + + Chaining + -------- + + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + + Assimilation + ------------ + + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + + If the assimliated promise rejects, then the downstream promise will also reject. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + + Simple Example + -------------- + + Synchronous Example + + ```javascript + let result; + + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + + Promise Example; + + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + + Advanced Example + -------------- + + Synchronous Example + + ```javascript + let author, books; + + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + + function foundBooks(books) { + + } + + function failure(reason) { + + } + + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + + Promise Example; + + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + then: then, + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + 'catch': function _catch(onRejection) { + return this.then(null, onRejection); + } +}; + +/*global self*/ +function polyfill$1() { + var local = undefined; + + if (typeof global !== 'undefined') { + local = global; + } else if (typeof self !== 'undefined') { + local = self; + } else { + try { + local = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + } + + var P = local.Promise; + + if (P) { + var promiseToString = null; + try { + promiseToString = Object.prototype.toString.call(P.resolve()); + } catch (e) { + // silently ignored + } + + if (promiseToString === '[object Promise]' && !P.cast) { + return; + } + } + + local.Promise = Promise$2; +} + +// Strange compat.. +Promise$2.polyfill = polyfill$1; +Promise$2.Promise = Promise$2; + +return Promise$2; + +}))); + + + +}).call(this,require(12),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"12":12}],4:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + // At least give some kind of context to the user + var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); + err.context = er; + throw err; + } + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + } else if (isObject(handler)) { + args = Array.prototype.slice.call(arguments, 1); + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else if (listeners) { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.prototype.listenerCount = function(type) { + if (this._events) { + var evlistener = this._events[type]; + + if (isFunction(evlistener)) + return 1; + else if (evlistener) + return evlistener.length; + } + return 0; +}; + +EventEmitter.listenerCount = function(emitter, type) { + return emitter.listenerCount(type); +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],5:[function(require,module,exports){ + +var hasOwn = Object.prototype.hasOwnProperty; +var toString = Object.prototype.toString; + +module.exports = function forEach (obj, fn, ctx) { + if (toString.call(fn) !== '[object Function]') { + throw new TypeError('iterator must be a function'); + } + var l = obj.length; + if (l === +l) { + for (var i = 0; i < l; i++) { + fn.call(ctx, obj[i], i, obj); + } + } else { + for (var k in obj) { + if (hasOwn.call(obj, k)) { + fn.call(ctx, obj[k], k, obj); + } + } + } +}; + + +},{}],6:[function(require,module,exports){ +(function (global){ +var win; + +if (typeof window !== "undefined") { + win = window; +} else if (typeof global !== "undefined") { + win = global; +} else if (typeof self !== "undefined"){ + win = self; +} else { + win = {}; +} + +module.exports = win; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],7:[function(require,module,exports){ +if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }; +} else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } +} + +},{}],8:[function(require,module,exports){ +var toString = {}.toString; + +module.exports = Array.isArray || function (arr) { + return toString.call(arr) == '[object Array]'; +}; + +},{}],9:[function(require,module,exports){ +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isNaN(val) === false) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtShort(ms) { + if (ms >= d) { + return Math.round(ms / d) + 'd'; + } + if (ms >= h) { + return Math.round(ms / h) + 'h'; + } + if (ms >= m) { + return Math.round(ms / m) + 'm'; + } + if (ms >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtLong(ms) { + return plural(ms, d, 'day') || + plural(ms, h, 'hour') || + plural(ms, m, 'minute') || + plural(ms, s, 'second') || + ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) { + return; + } + if (ms < n * 1.5) { + return Math.floor(ms / n) + ' ' + name; + } + return Math.ceil(ms / n) + ' ' + name + 's'; +} + +},{}],10:[function(require,module,exports){ +'use strict'; + +// modified from https://github.com/es-shims/es5-shim +var has = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; +var slice = Array.prototype.slice; +var isArgs = require(11); +var isEnumerable = Object.prototype.propertyIsEnumerable; +var hasDontEnumBug = !isEnumerable.call({ toString: null }, 'toString'); +var hasProtoEnumBug = isEnumerable.call(function () {}, 'prototype'); +var dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' +]; +var equalsConstructorPrototype = function (o) { + var ctor = o.constructor; + return ctor && ctor.prototype === o; +}; +var excludedKeys = { + $console: true, + $external: true, + $frame: true, + $frameElement: true, + $frames: true, + $innerHeight: true, + $innerWidth: true, + $outerHeight: true, + $outerWidth: true, + $pageXOffset: true, + $pageYOffset: true, + $parent: true, + $scrollLeft: true, + $scrollTop: true, + $scrollX: true, + $scrollY: true, + $self: true, + $webkitIndexedDB: true, + $webkitStorageInfo: true, + $window: true +}; +var hasAutomationEqualityBug = (function () { + /* global window */ + if (typeof window === 'undefined') { return false; } + for (var k in window) { + try { + if (!excludedKeys['$' + k] && has.call(window, k) && window[k] !== null && typeof window[k] === 'object') { + try { + equalsConstructorPrototype(window[k]); + } catch (e) { + return true; + } + } + } catch (e) { + return true; + } + } + return false; +}()); +var equalsConstructorPrototypeIfNotBuggy = function (o) { + /* global window */ + if (typeof window === 'undefined' || !hasAutomationEqualityBug) { + return equalsConstructorPrototype(o); + } + try { + return equalsConstructorPrototype(o); + } catch (e) { + return false; + } +}; + +var keysShim = function keys(object) { + var isObject = object !== null && typeof object === 'object'; + var isFunction = toStr.call(object) === '[object Function]'; + var isArguments = isArgs(object); + var isString = isObject && toStr.call(object) === '[object String]'; + var theKeys = []; + + if (!isObject && !isFunction && !isArguments) { + throw new TypeError('Object.keys called on a non-object'); + } + + var skipProto = hasProtoEnumBug && isFunction; + if (isString && object.length > 0 && !has.call(object, 0)) { + for (var i = 0; i < object.length; ++i) { + theKeys.push(String(i)); + } + } + + if (isArguments && object.length > 0) { + for (var j = 0; j < object.length; ++j) { + theKeys.push(String(j)); + } + } else { + for (var name in object) { + if (!(skipProto && name === 'prototype') && has.call(object, name)) { + theKeys.push(String(name)); + } + } + } + + if (hasDontEnumBug) { + var skipConstructor = equalsConstructorPrototypeIfNotBuggy(object); + + for (var k = 0; k < dontEnums.length; ++k) { + if (!(skipConstructor && dontEnums[k] === 'constructor') && has.call(object, dontEnums[k])) { + theKeys.push(dontEnums[k]); + } + } + } + return theKeys; +}; + +keysShim.shim = function shimObjectKeys() { + if (Object.keys) { + var keysWorksWithArguments = (function () { + // Safari 5.0 bug + return (Object.keys(arguments) || '').length === 2; + }(1, 2)); + if (!keysWorksWithArguments) { + var originalKeys = Object.keys; + Object.keys = function keys(object) { + if (isArgs(object)) { + return originalKeys(slice.call(object)); + } else { + return originalKeys(object); + } + }; + } + } else { + Object.keys = keysShim; + } + return Object.keys || keysShim; +}; + +module.exports = keysShim; + +},{"11":11}],11:[function(require,module,exports){ +'use strict'; + +var toStr = Object.prototype.toString; + +module.exports = function isArguments(value) { + var str = toStr.call(value); + var isArgs = str === '[object Arguments]'; + if (!isArgs) { + isArgs = str !== '[object Array]' && + value !== null && + typeof value === 'object' && + typeof value.length === 'number' && + value.length >= 0 && + toStr.call(value.callee) === '[object Function]'; + } + return isArgs; +}; + +},{}],12:[function(require,module,exports){ +// shim for using process in browser +var process = module.exports = {}; + +// cached from whatever global is present so that test runners that stub it +// don't break things. But we need to wrap it in a try catch in case it is +// wrapped in strict mode code which doesn't define any globals. It's inside a +// function because try/catches deoptimize in certain engines. + +var cachedSetTimeout; +var cachedClearTimeout; + +function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); +} +function defaultClearTimeout () { + throw new Error('clearTimeout has not been defined'); +} +(function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } +} ()) +function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch(e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch(e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + + +} +function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + + + +} +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],13:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +// If obj.hasOwnProperty has been overridden, then calling +// obj.hasOwnProperty(prop) will break. +// See: https://github.com/joyent/node/issues/1707 +function hasOwnProperty(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +module.exports = function(qs, sep, eq, options) { + sep = sep || '&'; + eq = eq || '='; + var obj = {}; + + if (typeof qs !== 'string' || qs.length === 0) { + return obj; + } + + var regexp = /\+/g; + qs = qs.split(sep); + + var maxKeys = 1000; + if (options && typeof options.maxKeys === 'number') { + maxKeys = options.maxKeys; + } + + var len = qs.length; + // maxKeys <= 0 means that we should not limit keys count + if (maxKeys > 0 && len > maxKeys) { + len = maxKeys; + } + + for (var i = 0; i < len; ++i) { + var x = qs[i].replace(regexp, '%20'), + idx = x.indexOf(eq), + kstr, vstr, k, v; + + if (idx >= 0) { + kstr = x.substr(0, idx); + vstr = x.substr(idx + 1); + } else { + kstr = x; + vstr = ''; + } + + k = decodeURIComponent(kstr); + v = decodeURIComponent(vstr); + + if (!hasOwnProperty(obj, k)) { + obj[k] = v; + } else if (isArray(obj[k])) { + obj[k].push(v); + } else { + obj[k] = [obj[k], v]; + } + } + + return obj; +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +},{}],14:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +var stringifyPrimitive = function(v) { + switch (typeof v) { + case 'string': + return v; + + case 'boolean': + return v ? 'true' : 'false'; + + case 'number': + return isFinite(v) ? v : ''; + + default: + return ''; + } +}; + +module.exports = function(obj, sep, eq, name) { + sep = sep || '&'; + eq = eq || '='; + if (obj === null) { + obj = undefined; + } + + if (typeof obj === 'object') { + return map(objectKeys(obj), function(k) { + var ks = encodeURIComponent(stringifyPrimitive(k)) + eq; + if (isArray(obj[k])) { + return map(obj[k], function(v) { + return ks + encodeURIComponent(stringifyPrimitive(v)); + }).join(sep); + } else { + return ks + encodeURIComponent(stringifyPrimitive(obj[k])); + } + }).join(sep); + + } + + if (!name) return ''; + return encodeURIComponent(stringifyPrimitive(name)) + eq + + encodeURIComponent(stringifyPrimitive(obj)); +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +function map (xs, f) { + if (xs.map) return xs.map(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + res.push(f(xs[i], i)); + } + return res; +} + +var objectKeys = Object.keys || function (obj) { + var res = []; + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) res.push(key); + } + return res; +}; + +},{}],15:[function(require,module,exports){ +'use strict'; + +exports.decode = exports.parse = require(13); +exports.encode = exports.stringify = require(14); + +},{"13":13,"14":14}],16:[function(require,module,exports){ +module.exports = AlgoliaSearch; + +var Index = require(18); +var deprecate = require(29); +var deprecatedMessage = require(30); +var AlgoliaSearchCore = require(17); +var inherits = require(7); +var errors = require(31); + +function AlgoliaSearch() { + AlgoliaSearchCore.apply(this, arguments); +} + +inherits(AlgoliaSearch, AlgoliaSearchCore); + +/* + * Delete an index + * + * @param indexName the name of index to delete + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.deleteIndex = function(indexName, callback) { + return this._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexName), + hostType: 'write', + callback: callback + }); +}; + +/** + * Move an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of + * srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.moveIndex = function(srcIndexName, dstIndexName, callback) { + var postObj = { + operation: 'move', destination: dstIndexName + }; + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Copy an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy + * of srcIndexName (destination will be overriten if it already exist). + * @param scope an array of scopes to copy: ['settings', 'synonyms', 'rules'] + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.copyIndex = function(srcIndexName, dstIndexName, scopeOrCallback, _callback) { + var postObj = { + operation: 'copy', + destination: dstIndexName + }; + var callback = _callback; + if (typeof scopeOrCallback === 'function') { + // oops, old behaviour of third argument being a function + callback = scopeOrCallback; + } else if (Array.isArray(scopeOrCallback) && scopeOrCallback.length > 0) { + postObj.scope = scopeOrCallback; + } else if (typeof scopeOrCallback !== 'undefined') { + throw new Error('the scope given to `copyIndex` was not an array with settings, synonyms or rules'); + } + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Return last log entries. + * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). + * @param length Specify the maximum number of entries to retrieve starting + * at offset. Maximum allowed value: 1000. + * @param type Specify the maximum number of entries to retrieve starting + * at offset. Maximum allowed value: 1000. + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.getLogs = function(offset, length, callback) { + var clone = require(27); + var params = {}; + if (typeof offset === 'object') { + // getLogs(params) + params = clone(offset); + callback = length; + } else if (arguments.length === 0 || typeof offset === 'function') { + // getLogs([cb]) + callback = offset; + } else if (arguments.length === 1 || typeof length === 'function') { + // getLogs(1, [cb)] + callback = length; + params.offset = offset; + } else { + // getLogs(1, 2, [cb]) + params.offset = offset; + params.length = length; + } + + if (params.offset === undefined) params.offset = 0; + if (params.length === undefined) params.length = 10; + + return this._jsonRequest({ + method: 'GET', + url: '/1/logs?' + this._getSearchParams(params, ''), + hostType: 'read', + callback: callback + }); +}; + +/* + * List all existing indexes (paginated) + * + * @param page The page to retrieve, starting at 0. + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with index list + */ +AlgoliaSearch.prototype.listIndexes = function(page, callback) { + var params = ''; + + if (page === undefined || typeof page === 'function') { + callback = page; + } else { + params = '?page=' + page; + } + + return this._jsonRequest({ + method: 'GET', + url: '/1/indexes' + params, + hostType: 'read', + callback: callback + }); +}; + +/* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ +AlgoliaSearch.prototype.initIndex = function(indexName) { + return new Index(this, indexName); +}; + +AlgoliaSearch.prototype.initAnalytics = function(opts) { + // the actual require must be inside the function, when put outside then you have a cyclic dependency + // not well resolved that ends up making the main "./index.js" (main module, the agloliasearch function) + // export an object instead of a function + // Other workarounds: + // - rewrite the lib in ES6, cyclic dependencies may be better supported + // - move initAnalytics to a property on the main module (algoliasearch.initAnalytics), + // same as places. + // The current API was made mostly to mimic the one made in PHP + var createAnalyticsClient = require(28); + return createAnalyticsClient(this.applicationID, this.apiKey, opts); +}; + +/* + * @deprecated use client.listApiKeys + */ +AlgoliaSearch.prototype.listUserKeys = deprecate(function(callback) { + return this.listApiKeys(callback); +}, deprecatedMessage('client.listUserKeys()', 'client.listApiKeys()')); + +/* + * List all existing api keys with their associated ACLs + * + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with api keys list + */ +AlgoliaSearch.prototype.listApiKeys = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/keys', + hostType: 'read', + callback: callback + }); +}; + +/* + * @deprecated see client.getApiKey + */ +AlgoliaSearch.prototype.getUserKeyACL = deprecate(function(key, callback) { + return this.getApiKey(key, callback); +}, deprecatedMessage('client.getUserKeyACL()', 'client.getApiKey()')); + +/* + * Get an API key + * + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the right API key + */ +AlgoliaSearch.prototype.getApiKey = function(key, callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/keys/' + key, + hostType: 'read', + callback: callback + }); +}; + +/* + * @deprecated see client.deleteApiKey + */ +AlgoliaSearch.prototype.deleteUserKey = deprecate(function(key, callback) { + return this.deleteApiKey(key, callback); +}, deprecatedMessage('client.deleteUserKey()', 'client.deleteApiKey()')); + +/* + * Delete an existing API key + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the date of deletion + */ +AlgoliaSearch.prototype.deleteApiKey = function(key, callback) { + return this._jsonRequest({ + method: 'DELETE', + url: '/1/keys/' + key, + hostType: 'write', + callback: callback + }); +}; + +/** + * Restore a deleted API key + * + * @param {String} key - The key to restore + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the restored API key + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.restoreApiKey('APIKEY') + * @see {@link https://www.algolia.com/doc/rest-api/search/#restore-api-key|Algolia REST API Documentation} + */ +AlgoliaSearch.prototype.restoreApiKey = function(key, callback) { + return this._jsonRequest({ + method: 'POST', + url: '/1/keys/' + key + '/restore', + hostType: 'write', + callback: callback + }); +}; + +/* + @deprecated see client.addApiKey + */ +AlgoliaSearch.prototype.addUserKey = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('client.addUserKey()', 'client.addApiKey()')); + +/* + * Add a new global API key + * + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string[]} params.indexes - Allowed targeted indexes for this key + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the added API key + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.addApiKey(['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * indexes: ['fruits'], + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#AddKey|Algolia REST API Documentation} + */ +AlgoliaSearch.prototype.addApiKey = function(acls, params, callback) { + var isArray = require(8); + var usage = 'Usage: client.addApiKey(arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = null; + } + + var postObj = { + acl: acls + }; + + if (params) { + postObj.validity = params.validity; + postObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + postObj.maxHitsPerQuery = params.maxHitsPerQuery; + postObj.indexes = params.indexes; + postObj.description = params.description; + + if (params.queryParameters) { + postObj.queryParameters = this._getSearchParams(params.queryParameters, ''); + } + + postObj.referers = params.referers; + } + + return this._jsonRequest({ + method: 'POST', + url: '/1/keys', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * @deprecated Please use client.addApiKey() + */ +AlgoliaSearch.prototype.addUserKeyWithValidity = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('client.addUserKeyWithValidity()', 'client.addApiKey()')); + +/** + * @deprecated Please use client.updateApiKey() + */ +AlgoliaSearch.prototype.updateUserKey = deprecate(function(key, acls, params, callback) { + return this.updateApiKey(key, acls, params, callback); +}, deprecatedMessage('client.updateUserKey()', 'client.updateApiKey()')); + +/** + * Update an existing API key + * @param {string} key - The key to update + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string[]} params.indexes - Allowed targeted indexes for this key + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the modified API key + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.updateApiKey('APIKEY', ['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * indexes: ['fruits'], + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#UpdateIndexKey|Algolia REST API Documentation} + */ +AlgoliaSearch.prototype.updateApiKey = function(key, acls, params, callback) { + var isArray = require(8); + var usage = 'Usage: client.updateApiKey(key, arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 2 || typeof params === 'function') { + callback = params; + params = null; + } + + var putObj = { + acl: acls + }; + + if (params) { + putObj.validity = params.validity; + putObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + putObj.maxHitsPerQuery = params.maxHitsPerQuery; + putObj.indexes = params.indexes; + putObj.description = params.description; + + if (params.queryParameters) { + putObj.queryParameters = this._getSearchParams(params.queryParameters, ''); + } + + putObj.referers = params.referers; + } + + return this._jsonRequest({ + method: 'PUT', + url: '/1/keys/' + key, + body: putObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Initialize a new batch of search queries + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.startQueriesBatch = deprecate(function startQueriesBatchDeprecated() { + this._batch = []; +}, deprecatedMessage('client.startQueriesBatch()', 'client.search()')); + +/** + * Add a search query in the batch + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.addQueryInBatch = deprecate(function addQueryInBatchDeprecated(indexName, query, args) { + this._batch.push({ + indexName: indexName, + query: query, + params: args + }); +}, deprecatedMessage('client.addQueryInBatch()', 'client.search()')); + +/** + * Launch the batch of queries using XMLHttpRequest. + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.sendQueriesBatch = deprecate(function sendQueriesBatchDeprecated(callback) { + return this.search(this._batch, callback); +}, deprecatedMessage('client.sendQueriesBatch()', 'client.search()')); + +/** + * Perform write operations across multiple indexes. + * + * To reduce the amount of time spent on network round trips, + * you can create, update, or delete several objects in one call, + * using the batch endpoint (all operations are done in the given order). + * + * Available actions: + * - addObject + * - updateObject + * - partialUpdateObject + * - partialUpdateObjectNoCreate + * - deleteObject + * + * https://www.algolia.com/doc/rest_api#Indexes + * @param {Object[]} operations An array of operations to perform + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.batch([{ + * action: 'addObject', + * indexName: 'clients', + * body: { + * name: 'Bill' + * } + * }, { + * action: 'udpateObject', + * indexName: 'fruits', + * body: { + * objectID: '29138', + * name: 'banana' + * } + * }], cb) + */ +AlgoliaSearch.prototype.batch = function(operations, callback) { + var isArray = require(8); + var usage = 'Usage: client.batch(operations[, callback])'; + + if (!isArray(operations)) { + throw new Error(usage); + } + + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/*/batch', + body: { + requests: operations + }, + hostType: 'write', + callback: callback + }); +}; + +/** + * Assign or Move a userID to a cluster + * + * @param {string} data.userID The userID to assign to a new cluster + * @param {string} data.cluster The cluster to assign the user to + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.assignUserID({ cluster: 'c1-test', userID: 'some-user' }); + */ +AlgoliaSearch.prototype.assignUserID = function(data, callback) { + if (!data.userID || !data.cluster) { + throw new errors.AlgoliaSearchError('You have to provide both a userID and cluster', data); + } + return this._jsonRequest({ + method: 'POST', + url: '/1/clusters/mapping', + hostType: 'write', + body: {cluster: data.cluster}, + callback: callback, + headers: { + 'x-algolia-user-id': data.userID + } + }); +}; + +/** + * Assign a array of userIDs to a cluster. + * + * @param {Array} data.userIDs The array of userIDs to assign to a new cluster + * @param {string} data.cluster The cluster to assign the user to + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.assignUserIDs({ cluster: 'c1-test', userIDs: ['some-user-1', 'some-user-2'] }); + */ +AlgoliaSearch.prototype.assignUserIDs = function(data, callback) { + if (!data.userIDs || !data.cluster) { + throw new errors.AlgoliaSearchError('You have to provide both an array of userIDs and cluster', data); + } + return this._jsonRequest({ + method: 'POST', + url: '/1/clusters/mapping/batch', + hostType: 'write', + body: { + cluster: data.cluster, + users: data.userIDs + }, + callback: callback + }); +}; + +/** + * Get the top userIDs + * + * (the callback is the second argument) + * + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.getTopUserID(); + */ +AlgoliaSearch.prototype.getTopUserID = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping/top', + hostType: 'read', + callback: callback + }); +}; + +/** + * Get userID + * + * @param {string} data.userID The userID to get info about + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.getUserID({ userID: 'some-user' }); + */ +AlgoliaSearch.prototype.getUserID = function(data, callback) { + if (!data.userID) { + throw new errors.AlgoliaSearchError('You have to provide a userID', {debugData: data}); + } + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping/' + data.userID, + hostType: 'read', + callback: callback + }); +}; + +/** + * List all the clusters + * + * (the callback is the second argument) + * + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.listClusters(); + */ +AlgoliaSearch.prototype.listClusters = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters', + hostType: 'read', + callback: callback + }); +}; + +/** + * List the userIDs + * + * (the callback is the second argument) + * + * @param {string} data.hitsPerPage How many hits on every page + * @param {string} data.page The page to retrieve + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.listClusters(); + * client.listClusters({ page: 3, hitsPerPage: 30}); + */ +AlgoliaSearch.prototype.listUserIDs = function(data, callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping', + body: data, + hostType: 'read', + callback: callback + }); +}; + +/** + * Remove an userID + * + * @param {string} data.userID The userID to assign to a new cluster + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.removeUserID({ userID: 'some-user' }); + */ +AlgoliaSearch.prototype.removeUserID = function(data, callback) { + if (!data.userID) { + throw new errors.AlgoliaSearchError('You have to provide a userID', {debugData: data}); + } + return this._jsonRequest({ + method: 'DELETE', + url: '/1/clusters/mapping', + hostType: 'write', + callback: callback, + headers: { + 'x-algolia-user-id': data.userID + } + }); +}; + +/** + * Search for userIDs + * + * @param {string} data.cluster The cluster to target + * @param {string} data.query The query to execute + * @param {string} data.hitsPerPage How many hits on every page + * @param {string} data.page The page to retrieve + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.searchUserIDs({ cluster: 'c1-test', query: 'some-user' }); + * client.searchUserIDs({ + * cluster: "c1-test", + * query: "some-user", + * page: 3, + * hitsPerPage: 2 + * }); + */ +AlgoliaSearch.prototype.searchUserIDs = function(data, callback) { + return this._jsonRequest({ + method: 'POST', + url: '/1/clusters/mapping/search', + body: data, + hostType: 'read', + callback: callback + }); +}; + +/** + * Set strategy for personalization + * + * @param {Object} data + * @param {Object} data.eventsScoring Associate a score to an event + * @param {Object} data.eventsScoring. The name of the event + * @param {Number} data.eventsScoring..score The score to associate to + * @param {String} data.eventsScoring..type Either "click", "conversion" or "view" + * @param {Object} data.facetsScoring Associate a score to a facet + * @param {Object} data.facetsScoring. The name of the facet + * @param {Number} data.facetsScoring..score The score to associate to + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.setPersonalizationStrategy({ + * eventsScoring: { + * "Add to cart": { score: 50, type: "conversion" }, + * Purchase: { score: 100, type: "conversion" } + * }, + * facetsScoring: { + * brand: { score: 100 }, + * categories: { score: 10 } + * } + * }); + */ +AlgoliaSearch.prototype.setPersonalizationStrategy = function(data, callback) { + return this._jsonRequest({ + method: 'POST', + url: '/1/recommendation/personalization/strategy', + body: data, + hostType: 'write', + callback: callback + }); +}; + +/** + * Get strategy for personalization + * + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.getPersonalizationStrategy(); + */ + +AlgoliaSearch.prototype.getPersonalizationStrategy = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/recommendation/personalization/strategy', + hostType: 'read', + callback: callback + }); +}; + +// environment specific methods +AlgoliaSearch.prototype.destroy = notImplemented; +AlgoliaSearch.prototype.enableRateLimitForward = notImplemented; +AlgoliaSearch.prototype.disableRateLimitForward = notImplemented; +AlgoliaSearch.prototype.useSecuredAPIKey = notImplemented; +AlgoliaSearch.prototype.disableSecuredAPIKey = notImplemented; +AlgoliaSearch.prototype.generateSecuredApiKey = notImplemented; +AlgoliaSearch.prototype.getSecuredApiKeyRemainingValidity = notImplemented; + +function notImplemented() { + var message = 'Not implemented in this environment.\n' + + 'If you feel this is a mistake, write to support@algolia.com'; + + throw new errors.AlgoliaSearchError(message); +} + +},{"17":17,"18":18,"27":27,"28":28,"29":29,"30":30,"31":31,"7":7,"8":8}],17:[function(require,module,exports){ +(function (process){ +module.exports = AlgoliaSearchCore; + +var errors = require(31); +var exitPromise = require(32); +var IndexCore = require(20); +var store = require(37); + +// We will always put the API KEY in the JSON body in case of too long API KEY, +// to avoid query string being too long and failing in various conditions (our server limit, browser limit, +// proxies limit) +var MAX_API_KEY_LENGTH = 500; +var RESET_APP_DATA_TIMER = + process.env.RESET_APP_DATA_TIMER && parseInt(process.env.RESET_APP_DATA_TIMER, 10) || + 60 * 2 * 1000; // after 2 minutes reset to first host + +/* + * Algolia Search library initialization + * https://www.algolia.com/ + * + * @param {string} applicationID - Your applicationID, found in your dashboard + * @param {string} apiKey - Your API key, found in your dashboard + * @param {Object} [opts] + * @param {number} [opts.timeout=2000] - The request timeout set in milliseconds, + * another request will be issued after this timeout + * @param {string} [opts.protocol='https:'] - The protocol used to query Algolia Search API. + * Set to 'http:' to force using http. + * @param {Object|Array} [opts.hosts={ + * read: [this.applicationID + '-dsn.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]), + * write: [this.applicationID + '.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]) - The hosts to use for Algolia Search API. + * If you provide them, you will less benefit from our HA implementation + */ +function AlgoliaSearchCore(applicationID, apiKey, opts) { + var debug = require(1)('algoliasearch'); + + var clone = require(27); + var isArray = require(8); + var map = require(33); + + var usage = 'Usage: algoliasearch(applicationID, apiKey, opts)'; + + if (opts._allowEmptyCredentials !== true && !applicationID) { + throw new errors.AlgoliaSearchError('Please provide an application ID. ' + usage); + } + + if (opts._allowEmptyCredentials !== true && !apiKey) { + throw new errors.AlgoliaSearchError('Please provide an API key. ' + usage); + } + + this.applicationID = applicationID; + this.apiKey = apiKey; + + this.hosts = { + read: [], + write: [] + }; + + opts = opts || {}; + + this._timeouts = opts.timeouts || { + connect: 1 * 1000, // 500ms connect is GPRS latency + read: 2 * 1000, + write: 30 * 1000 + }; + + // backward compat, if opts.timeout is passed, we use it to configure all timeouts like before + if (opts.timeout) { + this._timeouts.connect = this._timeouts.read = this._timeouts.write = opts.timeout; + } + + var protocol = opts.protocol || 'https:'; + // while we advocate for colon-at-the-end values: 'http:' for `opts.protocol` + // we also accept `http` and `https`. It's a common error. + if (!/:$/.test(protocol)) { + protocol = protocol + ':'; + } + + if (protocol !== 'http:' && protocol !== 'https:') { + throw new errors.AlgoliaSearchError('protocol must be `http:` or `https:` (was `' + opts.protocol + '`)'); + } + + this._checkAppIdData(); + + if (!opts.hosts) { + var defaultHosts = map(this._shuffleResult, function(hostNumber) { + return applicationID + '-' + hostNumber + '.algolianet.com'; + }); + + // no hosts given, compute defaults + var mainSuffix = (opts.dsn === false ? '' : '-dsn') + '.algolia.net'; + this.hosts.read = [this.applicationID + mainSuffix].concat(defaultHosts); + this.hosts.write = [this.applicationID + '.algolia.net'].concat(defaultHosts); + } else if (isArray(opts.hosts)) { + // when passing custom hosts, we need to have a different host index if the number + // of write/read hosts are different. + this.hosts.read = clone(opts.hosts); + this.hosts.write = clone(opts.hosts); + } else { + this.hosts.read = clone(opts.hosts.read); + this.hosts.write = clone(opts.hosts.write); + } + + // add protocol and lowercase hosts + this.hosts.read = map(this.hosts.read, prepareHost(protocol)); + this.hosts.write = map(this.hosts.write, prepareHost(protocol)); + + this.extraHeaders = {}; + + // In some situations you might want to warm the cache + this.cache = opts._cache || {}; + + this._ua = opts._ua; + this._useCache = opts._useCache === undefined || opts._cache ? true : opts._useCache; + this._useRequestCache = this._useCache && opts._useRequestCache; + this._useFallback = opts.useFallback === undefined ? true : opts.useFallback; + + this._setTimeout = opts._setTimeout; + + debug('init done, %j', this); +} + +/* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ +AlgoliaSearchCore.prototype.initIndex = function(indexName) { + return new IndexCore(this, indexName); +}; + +/** +* Add an extra field to the HTTP request +* +* @param name the header field name +* @param value the header field value +*/ +AlgoliaSearchCore.prototype.setExtraHeader = function(name, value) { + this.extraHeaders[name.toLowerCase()] = value; +}; + +/** +* Get the value of an extra HTTP header +* +* @param name the header field name +*/ +AlgoliaSearchCore.prototype.getExtraHeader = function(name) { + return this.extraHeaders[name.toLowerCase()]; +}; + +/** +* Remove an extra field from the HTTP request +* +* @param name the header field name +*/ +AlgoliaSearchCore.prototype.unsetExtraHeader = function(name) { + delete this.extraHeaders[name.toLowerCase()]; +}; + +/** +* Augment sent x-algolia-agent with more data, each agent part +* is automatically separated from the others by a semicolon; +* +* @param algoliaAgent the agent to add +*/ +AlgoliaSearchCore.prototype.addAlgoliaAgent = function(algoliaAgent) { + var algoliaAgentWithDelimiter = '; ' + algoliaAgent; + + if (this._ua.indexOf(algoliaAgentWithDelimiter) === -1) { + this._ua += algoliaAgentWithDelimiter; + } +}; + +/* + * Wrapper that try all hosts to maximize the quality of service + */ +AlgoliaSearchCore.prototype._jsonRequest = function(initialOpts) { + this._checkAppIdData(); + + var requestDebug = require(1)('algoliasearch:' + initialOpts.url); + + + var body; + var cacheID; + var additionalUA = initialOpts.additionalUA || ''; + var cache = initialOpts.cache; + var client = this; + var tries = 0; + var usingFallback = false; + var hasFallback = client._useFallback && client._request.fallback && initialOpts.fallback; + var headers; + + if ( + this.apiKey.length > MAX_API_KEY_LENGTH && + initialOpts.body !== undefined && + (initialOpts.body.params !== undefined || // index.search() + initialOpts.body.requests !== undefined) // client.search() + ) { + initialOpts.body.apiKey = this.apiKey; + headers = this._computeRequestHeaders({ + additionalUA: additionalUA, + withApiKey: false, + headers: initialOpts.headers + }); + } else { + headers = this._computeRequestHeaders({ + additionalUA: additionalUA, + headers: initialOpts.headers + }); + } + + if (initialOpts.body !== undefined) { + body = safeJSONStringify(initialOpts.body); + } + + requestDebug('request start'); + var debugData = []; + + + function doRequest(requester, reqOpts) { + client._checkAppIdData(); + + var startTime = new Date(); + + if (client._useCache && !client._useRequestCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && !client._useRequestCache && body) { + cacheID += '_body_' + reqOpts.body; + } + + // handle cache existence + if (isCacheValidWithCurrentID(!client._useRequestCache, cache, cacheID)) { + requestDebug('serving response from cache'); + + var responseText = cache[cacheID]; + + // Cache response must match the type of the original one + return client._promise.resolve({ + body: JSON.parse(responseText), + responseText: responseText + }); + } + + // if we reached max tries + if (tries >= client.hosts[initialOpts.hostType].length) { + if (!hasFallback || usingFallback) { + requestDebug('could not get any response'); + // then stop + return client._promise.reject(new errors.AlgoliaSearchError( + 'Cannot connect to the AlgoliaSearch API.' + + ' Send an email to support@algolia.com to report and resolve the issue.' + + ' Application id was: ' + client.applicationID, {debugData: debugData} + )); + } + + requestDebug('switching to fallback'); + + // let's try the fallback starting from here + tries = 0; + + // method, url and body are fallback dependent + reqOpts.method = initialOpts.fallback.method; + reqOpts.url = initialOpts.fallback.url; + reqOpts.jsonBody = initialOpts.fallback.body; + if (reqOpts.jsonBody) { + reqOpts.body = safeJSONStringify(reqOpts.jsonBody); + } + // re-compute headers, they could be omitting the API KEY + headers = client._computeRequestHeaders({ + additionalUA: additionalUA, + headers: initialOpts.headers + }); + + reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType); + client._setHostIndexByType(0, initialOpts.hostType); + usingFallback = true; // the current request is now using fallback + return doRequest(client._request.fallback, reqOpts); + } + + var currentHost = client._getHostByType(initialOpts.hostType); + + var url = currentHost + reqOpts.url; + var options = { + body: reqOpts.body, + jsonBody: reqOpts.jsonBody, + method: reqOpts.method, + headers: headers, + timeouts: reqOpts.timeouts, + debug: requestDebug, + forceAuthHeaders: reqOpts.forceAuthHeaders + }; + + requestDebug('method: %s, url: %s, headers: %j, timeouts: %d', + options.method, url, options.headers, options.timeouts); + + if (requester === client._request.fallback) { + requestDebug('using fallback'); + } + + // `requester` is any of this._request or this._request.fallback + // thus it needs to be called using the client as context + return requester.call(client, url, options).then(success, tryFallback); + + function success(httpResponse) { + // compute the status of the response, + // + // When in browser mode, using XDR or JSONP, we have no statusCode available + // So we rely on our API response `status` property. + // But `waitTask` can set a `status` property which is not the statusCode (it's the task status) + // So we check if there's a `message` along `status` and it means it's an error + // + // That's the only case where we have a response.status that's not the http statusCode + var status = httpResponse && httpResponse.body && httpResponse.body.message && httpResponse.body.status || + + // this is important to check the request statusCode AFTER the body eventual + // statusCode because some implementations (jQuery XDomainRequest transport) may + // send statusCode 200 while we had an error + httpResponse.statusCode || + + // When in browser mode, using XDR or JSONP + // we default to success when no error (no response.status && response.message) + // If there was a JSON.parse() error then body is null and it fails + httpResponse && httpResponse.body && 200; + + requestDebug('received response: statusCode: %s, computed statusCode: %d, headers: %j', + httpResponse.statusCode, status, httpResponse.headers); + + var httpResponseOk = Math.floor(status / 100) === 2; + + var endTime = new Date(); + debugData.push({ + currentHost: currentHost, + headers: removeCredentials(headers), + content: body || null, + contentLength: body !== undefined ? body.length : null, + method: reqOpts.method, + timeouts: reqOpts.timeouts, + url: reqOpts.url, + startTime: startTime, + endTime: endTime, + duration: endTime - startTime, + statusCode: status + }); + + if (httpResponseOk) { + if (client._useCache && !client._useRequestCache && cache) { + cache[cacheID] = httpResponse.responseText; + } + + return { + responseText: httpResponse.responseText, + body: httpResponse.body + }; + } + + var shouldRetry = Math.floor(status / 100) !== 4; + + if (shouldRetry) { + tries += 1; + return retryRequest(); + } + + requestDebug('unrecoverable error'); + + // no success and no retry => fail + var unrecoverableError = new errors.AlgoliaSearchError( + httpResponse.body && httpResponse.body.message, {debugData: debugData, statusCode: status} + ); + + return client._promise.reject(unrecoverableError); + } + + function tryFallback(err) { + // error cases: + // While not in fallback mode: + // - CORS not supported + // - network error + // While in fallback mode: + // - timeout + // - network error + // - badly formatted JSONP (script loaded, did not call our callback) + // In both cases: + // - uncaught exception occurs (TypeError) + requestDebug('error: %s, stack: %s', err.message, err.stack); + + var endTime = new Date(); + debugData.push({ + currentHost: currentHost, + headers: removeCredentials(headers), + content: body || null, + contentLength: body !== undefined ? body.length : null, + method: reqOpts.method, + timeouts: reqOpts.timeouts, + url: reqOpts.url, + startTime: startTime, + endTime: endTime, + duration: endTime - startTime + }); + + if (!(err instanceof errors.AlgoliaSearchError)) { + err = new errors.Unknown(err && err.message, err); + } + + tries += 1; + + // stop the request implementation when: + if ( + // we did not generate this error, + // it comes from a throw in some other piece of code + err instanceof errors.Unknown || + + // server sent unparsable JSON + err instanceof errors.UnparsableJSON || + + // max tries and already using fallback or no fallback + tries >= client.hosts[initialOpts.hostType].length && + (usingFallback || !hasFallback)) { + // stop request implementation for this command + err.debugData = debugData; + return client._promise.reject(err); + } + + // When a timeout occurred, retry by raising timeout + if (err instanceof errors.RequestTimeout) { + return retryRequestWithHigherTimeout(); + } + + return retryRequest(); + } + + function retryRequest() { + requestDebug('retrying request'); + client._incrementHostIndex(initialOpts.hostType); + return doRequest(requester, reqOpts); + } + + function retryRequestWithHigherTimeout() { + requestDebug('retrying request with higher timeout'); + client._incrementHostIndex(initialOpts.hostType); + client._incrementTimeoutMultipler(); + reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType); + return doRequest(requester, reqOpts); + } + } + + function isCacheValidWithCurrentID( + useRequestCache, + currentCache, + currentCacheID + ) { + return ( + client._useCache && + useRequestCache && + currentCache && + currentCache[currentCacheID] !== undefined + ); + } + + + function interopCallbackReturn(request, callback) { + if (isCacheValidWithCurrentID(client._useRequestCache, cache, cacheID)) { + request.catch(function() { + // Release the cache on error + delete cache[cacheID]; + }); + } + + if (typeof initialOpts.callback === 'function') { + // either we have a callback + request.then(function okCb(content) { + exitPromise(function() { + initialOpts.callback(null, callback(content)); + }, client._setTimeout || setTimeout); + }, function nookCb(err) { + exitPromise(function() { + initialOpts.callback(err); + }, client._setTimeout || setTimeout); + }); + } else { + // either we are using promises + return request.then(callback); + } + } + + if (client._useCache && client._useRequestCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && client._useRequestCache && body) { + cacheID += '_body_' + body; + } + + if (isCacheValidWithCurrentID(client._useRequestCache, cache, cacheID)) { + requestDebug('serving request from cache'); + + var maybePromiseForCache = cache[cacheID]; + + // In case the cache is warmup with value that is not a promise + var promiseForCache = typeof maybePromiseForCache.then !== 'function' + ? client._promise.resolve({responseText: maybePromiseForCache}) + : maybePromiseForCache; + + return interopCallbackReturn(promiseForCache, function(content) { + // In case of the cache request, return the original value + return JSON.parse(content.responseText); + }); + } + + var request = doRequest( + client._request, { + url: initialOpts.url, + method: initialOpts.method, + body: body, + jsonBody: initialOpts.body, + timeouts: client._getTimeoutsForRequest(initialOpts.hostType), + forceAuthHeaders: initialOpts.forceAuthHeaders + } + ); + + if (client._useCache && client._useRequestCache && cache) { + cache[cacheID] = request; + } + + return interopCallbackReturn(request, function(content) { + // In case of the first request, return the JSON value + return content.body; + }); +}; + +/* +* Transform search param object in query string +* @param {object} args arguments to add to the current query string +* @param {string} params current query string +* @return {string} the final query string +*/ +AlgoliaSearchCore.prototype._getSearchParams = function(args, params) { + if (args === undefined || args === null) { + return params; + } + for (var key in args) { + if (key !== null && args[key] !== undefined && args.hasOwnProperty(key)) { + params += params === '' ? '' : '&'; + params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? safeJSONStringify(args[key]) : args[key]); + } + } + return params; +}; + +/** + * Compute the headers for a request + * + * @param [string] options.additionalUA semi-colon separated string with other user agents to add + * @param [boolean=true] options.withApiKey Send the api key as a header + * @param [Object] options.headers Extra headers to send + */ +AlgoliaSearchCore.prototype._computeRequestHeaders = function(options) { + var forEach = require(5); + + var ua = options.additionalUA ? + this._ua + '; ' + options.additionalUA : + this._ua; + + var requestHeaders = { + 'x-algolia-agent': ua, + 'x-algolia-application-id': this.applicationID + }; + + // browser will inline headers in the url, node.js will use http headers + // but in some situations, the API KEY will be too long (big secured API keys) + // so if the request is a POST and the KEY is very long, we will be asked to not put + // it into headers but in the JSON body + if (options.withApiKey !== false) { + requestHeaders['x-algolia-api-key'] = this.apiKey; + } + + if (this.userToken) { + requestHeaders['x-algolia-usertoken'] = this.userToken; + } + + if (this.securityTags) { + requestHeaders['x-algolia-tagfilters'] = this.securityTags; + } + + forEach(this.extraHeaders, function addToRequestHeaders(value, key) { + requestHeaders[key] = value; + }); + + if (options.headers) { + forEach(options.headers, function addToRequestHeaders(value, key) { + requestHeaders[key] = value; + }); + } + + return requestHeaders; +}; + +/** + * Search through multiple indices at the same time + * @param {Object[]} queries An array of queries you want to run. + * @param {string} queries[].indexName The index name you want to target + * @param {string} [queries[].query] The query to issue on this index. Can also be passed into `params` + * @param {Object} queries[].params Any search param like hitsPerPage, .. + * @param {Function} callback Callback to be called + * @return {Promise|undefined} Returns a promise if no callback given + */ +AlgoliaSearchCore.prototype.search = function(queries, opts, callback) { + var isArray = require(8); + var map = require(33); + + var usage = 'Usage: client.search(arrayOfQueries[, callback])'; + + if (!isArray(queries)) { + throw new Error(usage); + } + + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var client = this; + + var postObj = { + requests: map(queries, function prepareRequest(query) { + var params = ''; + + // allow query.query + // so we are mimicing the index.search(query, params) method + // {indexName:, query:, params:} + if (query.query !== undefined) { + params += 'query=' + encodeURIComponent(query.query); + } + + return { + indexName: query.indexName, + params: client._getSearchParams(query.params, params) + }; + }) + }; + + var JSONPParams = map(postObj.requests, function prepareJSONPParams(request, requestId) { + return requestId + '=' + + encodeURIComponent( + '/1/indexes/' + encodeURIComponent(request.indexName) + '?' + + request.params + ); + }).join('&'); + + var url = '/1/indexes/*/queries'; + + if (opts.strategy !== undefined) { + postObj.strategy = opts.strategy; + } + + return this._jsonRequest({ + cache: this.cache, + method: 'POST', + url: url, + body: postObj, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/*', + body: { + params: JSONPParams + } + }, + callback: callback + }); +}; + +/** +* Search for facet values +* https://www.algolia.com/doc/rest-api/search#search-for-facet-values +* This is the top-level API for SFFV. +* +* @param {object[]} queries An array of queries to run. +* @param {string} queries[].indexName Index name, name of the index to search. +* @param {object} queries[].params Query parameters. +* @param {string} queries[].params.facetName Facet name, name of the attribute to search for values in. +* Must be declared as a facet +* @param {string} queries[].params.facetQuery Query for the facet search +* @param {string} [queries[].params.*] Any search parameter of Algolia, +* see https://www.algolia.com/doc/api-client/javascript/search#search-parameters +* Pagination is not supported. The page and hitsPerPage parameters will be ignored. +*/ +AlgoliaSearchCore.prototype.searchForFacetValues = function(queries) { + var isArray = require(8); + var map = require(33); + + var usage = 'Usage: client.searchForFacetValues([{indexName, params: {facetName, facetQuery, ...params}}, ...queries])'; // eslint-disable-line max-len + + if (!isArray(queries)) { + throw new Error(usage); + } + + var client = this; + + return client._promise.all(map(queries, function performQuery(query) { + if ( + !query || + query.indexName === undefined || + query.params.facetName === undefined || + query.params.facetQuery === undefined + ) { + throw new Error(usage); + } + + var clone = require(27); + var omit = require(35); + + var indexName = query.indexName; + var params = query.params; + + var facetName = params.facetName; + var filteredParams = omit(clone(params), function(keyName) { + return keyName === 'facetName'; + }); + var searchParameters = client._getSearchParams(filteredParams, ''); + + return client._jsonRequest({ + cache: client.cache, + method: 'POST', + url: + '/1/indexes/' + + encodeURIComponent(indexName) + + '/facets/' + + encodeURIComponent(facetName) + + '/query', + hostType: 'read', + body: {params: searchParameters} + }); + })); +}; + +/** + * Set the extra security tagFilters header + * @param {string|array} tags The list of tags defining the current security filters + */ +AlgoliaSearchCore.prototype.setSecurityTags = function(tags) { + if (Object.prototype.toString.call(tags) === '[object Array]') { + var strTags = []; + for (var i = 0; i < tags.length; ++i) { + if (Object.prototype.toString.call(tags[i]) === '[object Array]') { + var oredTags = []; + for (var j = 0; j < tags[i].length; ++j) { + oredTags.push(tags[i][j]); + } + strTags.push('(' + oredTags.join(',') + ')'); + } else { + strTags.push(tags[i]); + } + } + tags = strTags.join(','); + } + + this.securityTags = tags; +}; + +/** + * Set the extra user token header + * @param {string} userToken The token identifying a uniq user (used to apply rate limits) + */ +AlgoliaSearchCore.prototype.setUserToken = function(userToken) { + this.userToken = userToken; +}; + +/** + * Clear all queries in client's cache + * @return undefined + */ +AlgoliaSearchCore.prototype.clearCache = function() { + this.cache = {}; +}; + +/** +* Set the number of milliseconds a request can take before automatically being terminated. +* @deprecated +* @param {Number} milliseconds +*/ +AlgoliaSearchCore.prototype.setRequestTimeout = function(milliseconds) { + if (milliseconds) { + this._timeouts.connect = this._timeouts.read = this._timeouts.write = milliseconds; + } +}; + +/** +* Set the three different (connect, read, write) timeouts to be used when requesting +* @param {Object} timeouts +*/ +AlgoliaSearchCore.prototype.setTimeouts = function(timeouts) { + this._timeouts = timeouts; +}; + +/** +* Get the three different (connect, read, write) timeouts to be used when requesting +* @param {Object} timeouts +*/ +AlgoliaSearchCore.prototype.getTimeouts = function() { + return this._timeouts; +}; + +AlgoliaSearchCore.prototype._getAppIdData = function() { + var data = store.get(this.applicationID); + if (data !== null) this._cacheAppIdData(data); + return data; +}; + +AlgoliaSearchCore.prototype._setAppIdData = function(data) { + data.lastChange = (new Date()).getTime(); + this._cacheAppIdData(data); + return store.set(this.applicationID, data); +}; + +AlgoliaSearchCore.prototype._checkAppIdData = function() { + var data = this._getAppIdData(); + var now = (new Date()).getTime(); + if (data === null || now - data.lastChange > RESET_APP_DATA_TIMER) { + return this._resetInitialAppIdData(data); + } + + return data; +}; + +AlgoliaSearchCore.prototype._resetInitialAppIdData = function(data) { + var newData = data || {}; + newData.hostIndexes = {read: 0, write: 0}; + newData.timeoutMultiplier = 1; + newData.shuffleResult = newData.shuffleResult || shuffle([1, 2, 3]); + return this._setAppIdData(newData); +}; + +AlgoliaSearchCore.prototype._cacheAppIdData = function(data) { + this._hostIndexes = data.hostIndexes; + this._timeoutMultiplier = data.timeoutMultiplier; + this._shuffleResult = data.shuffleResult; +}; + +AlgoliaSearchCore.prototype._partialAppIdDataUpdate = function(newData) { + var foreach = require(5); + var currentData = this._getAppIdData(); + foreach(newData, function(value, key) { + currentData[key] = value; + }); + + return this._setAppIdData(currentData); +}; + +AlgoliaSearchCore.prototype._getHostByType = function(hostType) { + return this.hosts[hostType][this._getHostIndexByType(hostType)]; +}; + +AlgoliaSearchCore.prototype._getTimeoutMultiplier = function() { + return this._timeoutMultiplier; +}; + +AlgoliaSearchCore.prototype._getHostIndexByType = function(hostType) { + return this._hostIndexes[hostType]; +}; + +AlgoliaSearchCore.prototype._setHostIndexByType = function(hostIndex, hostType) { + var clone = require(27); + var newHostIndexes = clone(this._hostIndexes); + newHostIndexes[hostType] = hostIndex; + this._partialAppIdDataUpdate({hostIndexes: newHostIndexes}); + return hostIndex; +}; + +AlgoliaSearchCore.prototype._incrementHostIndex = function(hostType) { + return this._setHostIndexByType( + (this._getHostIndexByType(hostType) + 1) % this.hosts[hostType].length, hostType + ); +}; + +AlgoliaSearchCore.prototype._incrementTimeoutMultipler = function() { + var timeoutMultiplier = Math.max(this._timeoutMultiplier + 1, 4); + return this._partialAppIdDataUpdate({timeoutMultiplier: timeoutMultiplier}); +}; + +AlgoliaSearchCore.prototype._getTimeoutsForRequest = function(hostType) { + return { + connect: this._timeouts.connect * this._timeoutMultiplier, + complete: this._timeouts[hostType] * this._timeoutMultiplier + }; +}; + +function prepareHost(protocol) { + return function prepare(host) { + return protocol + '//' + host.toLowerCase(); + }; +} + +// Prototype.js < 1.7, a widely used library, defines a weird +// Array.prototype.toJSON function that will fail to stringify our content +// appropriately +// refs: +// - https://groups.google.com/forum/#!topic/prototype-core/E-SAVvV_V9Q +// - https://github.com/sstephenson/prototype/commit/038a2985a70593c1a86c230fadbdfe2e4898a48c +// - http://stackoverflow.com/a/3148441/147079 +function safeJSONStringify(obj) { + /* eslint no-extend-native:0 */ + + if (Array.prototype.toJSON === undefined) { + return JSON.stringify(obj); + } + + var toJSON = Array.prototype.toJSON; + delete Array.prototype.toJSON; + var out = JSON.stringify(obj); + Array.prototype.toJSON = toJSON; + + return out; +} + +function shuffle(array) { + var currentIndex = array.length; + var temporaryValue; + var randomIndex; + + // While there remain elements to shuffle... + while (currentIndex !== 0) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; +} + +function removeCredentials(headers) { + var newHeaders = {}; + + for (var headerName in headers) { + if (Object.prototype.hasOwnProperty.call(headers, headerName)) { + var value; + + if (headerName === 'x-algolia-api-key' || headerName === 'x-algolia-application-id') { + value = '**hidden for security purposes**'; + } else { + value = headers[headerName]; + } + + newHeaders[headerName] = value; + } + } + + return newHeaders; +} + +}).call(this,require(12)) +},{"1":1,"12":12,"20":20,"27":27,"31":31,"32":32,"33":33,"35":35,"37":37,"5":5,"8":8}],18:[function(require,module,exports){ +var inherits = require(7); +var IndexCore = require(20); +var deprecate = require(29); +var deprecatedMessage = require(30); +var exitPromise = require(32); +var errors = require(31); + +var deprecateForwardToSlaves = deprecate( + function() {}, + deprecatedMessage('forwardToSlaves', 'forwardToReplicas') +); + +module.exports = Index; + +function Index() { + IndexCore.apply(this, arguments); +} + +inherits(Index, IndexCore); + +/* +* Add an object in this index +* +* @param content contains the javascript object to add inside the index +* @param objectID (optional) an objectID you want to attribute to this object +* (if the attribute already exist the old object will be overwrite) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.addObject = function(content, objectID, callback) { + var indexObj = this; + + if (arguments.length === 1 || typeof objectID === 'function') { + callback = objectID; + objectID = undefined; + } + + return this.as._jsonRequest({ + method: objectID !== undefined ? + 'PUT' : // update or create + 'POST', // create (API generates an objectID) + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + // create + (objectID !== undefined ? '/' + encodeURIComponent(objectID) : ''), // update or create + body: content, + hostType: 'write', + callback: callback + }); +}; + +/* +* Add several objects +* +* @param objects contains an array of objects to add +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.addObjects = function(objects, callback) { + var isArray = require(8); + var usage = 'Usage: index.addObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'addObject', + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Update partially an object (only update attributes passed in argument) +* +* @param partialObject contains the javascript attributes to override, the +* object must contains an objectID attribute +* @param createIfNotExists (optional) if false, avoid an automatic creation of the object +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.partialUpdateObject = function(partialObject, createIfNotExists, callback) { + if (arguments.length === 1 || typeof createIfNotExists === 'function') { + callback = createIfNotExists; + createIfNotExists = undefined; + } + + var indexObj = this; + var url = '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial'; + if (createIfNotExists === false) { + url += '?createIfNotExists=false'; + } + + return this.as._jsonRequest({ + method: 'POST', + url: url, + body: partialObject, + hostType: 'write', + callback: callback + }); +}; + +/* +* Partially Override the content of several objects +* +* @param objects contains an array of objects to update (each object must contains a objectID attribute) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.partialUpdateObjects = function(objects, createIfNotExists, callback) { + if (arguments.length === 1 || typeof createIfNotExists === 'function') { + callback = createIfNotExists; + createIfNotExists = true; + } + + var isArray = require(8); + var usage = 'Usage: index.partialUpdateObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: createIfNotExists === true ? 'partialUpdateObject' : 'partialUpdateObjectNoCreate', + objectID: objects[i].objectID, + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Override the content of object +* +* @param object contains the javascript object to save, the object must contains an objectID attribute +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.saveObject = function(object, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID), + body: object, + hostType: 'write', + callback: callback + }); +}; + +/* +* Override the content of several objects +* +* @param objects contains an array of objects to update (each object must contains a objectID attribute) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.saveObjects = function(objects, callback) { + var isArray = require(8); + var usage = 'Usage: index.saveObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'updateObject', + objectID: objects[i].objectID, + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete an object from the index +* +* @param objectID the unique identifier of object to delete +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.deleteObject = function(objectID, callback) { + if (typeof objectID === 'function' || typeof objectID !== 'string' && typeof objectID !== 'number') { + var err = new errors.AlgoliaSearchError( + objectID && typeof objectID !== 'function' + ? 'ObjectID must be a string' + : 'Cannot delete an object without an objectID' + ); + callback = objectID; + if (typeof callback === 'function') { + return callback(err); + } + + return this.as._promise.reject(err); + } + + var indexObj = this; + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete several objects from an index +* +* @param objectIDs contains an array of objectID to delete +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.deleteObjects = function(objectIDs, callback) { + var isArray = require(8); + var map = require(33); + + var usage = 'Usage: index.deleteObjects(arrayOfObjectIDs[, callback])'; + + if (!isArray(objectIDs)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: map(objectIDs, function prepareRequest(objectID) { + return { + action: 'deleteObject', + objectID: objectID, + body: { + objectID: objectID + } + }; + }) + }; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete all objects matching a query +* +* @param query the query string +* @param params the optional query parameters +* @param callback (optional) the result callback called with one argument +* error: null or Error('message') +* @deprecated see index.deleteBy +*/ +Index.prototype.deleteByQuery = deprecate(function(query, params, callback) { + var clone = require(27); + var map = require(33); + + var indexObj = this; + var client = indexObj.as; + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = {}; + } else { + params = clone(params); + } + + params.attributesToRetrieve = 'objectID'; + params.hitsPerPage = 1000; + params.distinct = false; + + // when deleting, we should never use cache to get the + // search results + this.clearCache(); + + // there's a problem in how we use the promise chain, + // see how waitTask is done + var promise = this + .search(query, params) + .then(stopOrDelete); + + function stopOrDelete(searchContent) { + // stop here + if (searchContent.nbHits === 0) { + // return indexObj.as._request.resolve(); + return searchContent; + } + + // continue and do a recursive call + var objectIDs = map(searchContent.hits, function getObjectID(object) { + return object.objectID; + }); + + return indexObj + .deleteObjects(objectIDs) + .then(waitTask) + .then(doDeleteByQuery); + } + + function waitTask(deleteObjectsContent) { + return indexObj.waitTask(deleteObjectsContent.taskID); + } + + function doDeleteByQuery() { + return indexObj.deleteByQuery(query, params); + } + + if (!callback) { + return promise; + } + + promise.then(success, failure); + + function success() { + exitPromise(function exit() { + callback(null); + }, client._setTimeout || setTimeout); + } + + function failure(err) { + exitPromise(function exit() { + callback(err); + }, client._setTimeout || setTimeout); + } +}, deprecatedMessage('index.deleteByQuery()', 'index.deleteBy()')); + +/** +* Delete all objects matching a query +* +* the query parameters that can be used are: +* - filters (numeric, facet, tag) +* - geo +* +* you can not send an empty query or filters +* +* @param params the optional query parameters +* @param callback (optional) the result callback called with one argument +* error: null or Error('message') +*/ +Index.prototype.deleteBy = function(params, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/deleteByQuery', + body: {params: indexObj.as._getSearchParams(params, '')}, + hostType: 'write', + callback: callback + }); +}; + +/* +* Browse all content from an index using events. Basically this will do +* .browse() -> .browseFrom -> .browseFrom -> .. until all the results are returned +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @return {EventEmitter} +* @example +* var browser = index.browseAll('cool songs', { +* tagFilters: 'public,comments', +* hitsPerPage: 500 +* }); +* +* browser.on('result', function resultCallback(content) { +* console.log(content.hits); +* }); +* +* // if any error occurs, you get it +* browser.on('error', function(err) { +* throw err; +* }); +* +* // when you have browsed the whole index, you get this event +* browser.on('end', function() { +* console.log('finished'); +* }); +* +* // at any point if you want to stop the browsing process, you can stop it manually +* // otherwise it will go on and on +* browser.stop(); +* +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +Index.prototype.browseAll = function(query, queryParameters) { + if (typeof query === 'object') { + queryParameters = query; + query = undefined; + } + + var merge = require(34); + + var IndexBrowser = require(19); + + var browser = new IndexBrowser(); + var client = this.as; + var index = this; + var params = client._getSearchParams( + merge({}, queryParameters || {}, { + query: query + }), '' + ); + + // start browsing + browseLoop(); + + function browseLoop(cursor) { + if (browser._stopped) { + return; + } + + var body; + + if (cursor !== undefined) { + body = { + cursor: cursor + }; + } else { + body = { + params: params + }; + } + + client._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(index.indexName) + '/browse', + hostType: 'read', + body: body, + callback: browseCallback + }); + } + + function browseCallback(err, content) { + if (browser._stopped) { + return; + } + + if (err) { + browser._error(err); + return; + } + + browser._result(content); + + // no cursor means we are finished browsing + if (content.cursor === undefined) { + browser._end(); + return; + } + + browseLoop(content.cursor); + } + + return browser; +}; + +/* +* Get a Typeahead.js adapter +* @param searchParams contains an object with query parameters (see search for details) +*/ +Index.prototype.ttAdapter = deprecate(function(params) { + var self = this; + return function ttAdapter(query, syncCb, asyncCb) { + var cb; + + if (typeof asyncCb === 'function') { + // typeahead 0.11 + cb = asyncCb; + } else { + // pre typeahead 0.11 + cb = syncCb; + } + + self.search(query, params, function searchDone(err, content) { + if (err) { + cb(err); + return; + } + + cb(content.hits); + }); + }; +}, +'ttAdapter is not necessary anymore and will be removed in the next version,\n' + +'have a look at autocomplete.js (https://github.com/algolia/autocomplete.js)'); + +/* +* Wait the publication of a task on the server. +* All server task are asynchronous and you can check with this method that the task is published. +* +* @param taskID the id of the task returned by server +* @param callback the result callback with with two arguments: +* error: null or Error('message') +* content: the server answer that contains the list of results +*/ +Index.prototype.waitTask = function(taskID, callback) { + // wait minimum 100ms before retrying + var baseDelay = 100; + // wait maximum 5s before retrying + var maxDelay = 5000; + var loop = 0; + + // waitTask() must be handled differently from other methods, + // it's a recursive method using a timeout + var indexObj = this; + var client = indexObj.as; + + var promise = retryLoop(); + + function retryLoop() { + return client._jsonRequest({ + method: 'GET', + hostType: 'read', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID + }).then(function success(content) { + loop++; + var delay = baseDelay * loop * loop; + if (delay > maxDelay) { + delay = maxDelay; + } + + if (content.status !== 'published') { + return client._promise.delay(delay).then(retryLoop); + } + + return content; + }); + } + + if (!callback) { + return promise; + } + + promise.then(successCb, failureCb); + + function successCb(content) { + exitPromise(function exit() { + callback(null, content); + }, client._setTimeout || setTimeout); + } + + function failureCb(err) { + exitPromise(function exit() { + callback(err); + }, client._setTimeout || setTimeout); + } +}; + +/* +* This function deletes the index content. Settings and index specific API keys are kept untouched. +* +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the settings object or the error message if a failure occurred +*/ +Index.prototype.clearIndex = function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear', + hostType: 'write', + callback: callback + }); +}; + +/* +* Get settings of this index +* +* @param opts an object of options to add +* @param opts.advanced get more settings like nbShards (useful for Enterprise) +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the settings object or the error message if a failure occurred +*/ +Index.prototype.getSettings = function(opts, callback) { + if (arguments.length === 1 && typeof opts === 'function') { + callback = opts; + opts = {}; + } + opts = opts || {}; + + var indexName = encodeURIComponent(this.indexName); + return this.as._jsonRequest({ + method: 'GET', + url: + '/1/indexes/' + + indexName + + '/settings?getVersion=2' + + (opts.advanced ? '&advanced=' + opts.advanced : ''), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.searchSynonyms = function(params, callback) { + if (typeof params === 'function') { + callback = params; + params = {}; + } else if (params === undefined) { + params = {}; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/search', + body: params, + hostType: 'read', + callback: callback + }); +}; + +function exportData(method, _hitsPerPage, callback) { + function search(page, _previous) { + var options = { + page: page || 0, + hitsPerPage: _hitsPerPage || 100 + }; + var previous = _previous || []; + + return method(options).then(function(result) { + var hits = result.hits; + var nbHits = result.nbHits; + var current = hits.map(function(s) { + delete s._highlightResult; + return s; + }); + var synonyms = previous.concat(current); + if (synonyms.length < nbHits) { + return search(options.page + 1, synonyms); + } + return synonyms; + }); + } + return search().then(function(data) { + if (typeof callback === 'function') { + callback(data); + return undefined; + } + return data; + }); +} + +/** + * Retrieve all the synonyms in an index + * @param [number=100] hitsPerPage The amount of synonyms to retrieve per batch + * @param [function] callback will be called after all synonyms are retrieved + */ +Index.prototype.exportSynonyms = function(hitsPerPage, callback) { + return exportData(this.searchSynonyms.bind(this), hitsPerPage, callback); +}; + +Index.prototype.saveSynonym = function(synonym, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(synonym.objectID) + + '?forwardToReplicas=' + forwardToReplicas, + body: synonym, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.getSynonym = function(objectID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(objectID), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.deleteSynonym = function(objectID, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(objectID) + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.clearSynonyms = function(opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/clear' + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.batchSynonyms = function(synonyms, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/batch' + + '?forwardToReplicas=' + forwardToReplicas + + '&replaceExistingSynonyms=' + (opts.replaceExistingSynonyms ? 'true' : 'false'), + hostType: 'write', + body: synonyms, + callback: callback + }); +}; + +Index.prototype.searchRules = function(params, callback) { + if (typeof params === 'function') { + callback = params; + params = {}; + } else if (params === undefined) { + params = {}; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/search', + body: params, + hostType: 'read', + callback: callback + }); +}; +/** + * Retrieve all the query rules in an index + * @param [number=100] hitsPerPage The amount of query rules to retrieve per batch + * @param [function] callback will be called after all query rules are retrieved + * error: null or Error('message') + */ +Index.prototype.exportRules = function(hitsPerPage, callback) { + return exportData(this.searchRules.bind(this), hitsPerPage, callback); +}; + +Index.prototype.saveRule = function(rule, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (!rule.objectID) { + throw new errors.AlgoliaSearchError('Missing or empty objectID field for rule'); + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(rule.objectID) + + '?forwardToReplicas=' + forwardToReplicas, + body: rule, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.getRule = function(objectID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(objectID), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.deleteRule = function(objectID, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(objectID) + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.clearRules = function(opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/clear' + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.batchRules = function(rules, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/batch' + + '?forwardToReplicas=' + forwardToReplicas + + '&clearExistingRules=' + (opts.clearExistingRules === true ? 'true' : 'false'), + hostType: 'write', + body: rules, + callback: callback + }); +}; + +Index.prototype.exists = function(callback) { + var result = this.getSettings().then(function() { + return true; + }).catch(function(err) { + if (err instanceof errors.AlgoliaSearchError && err.statusCode === 404) { + return false; + } + + throw err; + }); + + if (typeof callback !== 'function') { + return result; + } + + result.then(function(res) { + callback(null, res); + }).catch(function(err) { + callback(err); + }); +}; + +Index.prototype.findObject = function(findCallback, requestOptions, callback) { + requestOptions = requestOptions === undefined ? {} : requestOptions; + var paginate = requestOptions.paginate !== undefined ? requestOptions.paginate : true; + var query = requestOptions.query !== undefined ? requestOptions.query : ''; + + var that = this; + var page = 0; + + var paginateLoop = function() { + requestOptions.page = page; + + return that.search(query, requestOptions).then(function(result) { + var hits = result.hits; + + for (var position = 0; position < hits.length; position++) { + var hit = hits[position]; + if (findCallback(hit)) { + return { + object: hit, + position: position, + page: page + }; + } + } + + page += 1; + + // paginate if option was set and has next page + if (!paginate || page >= result.nbPages) { + throw new errors.ObjectNotFound('Object not found'); + } + + return paginateLoop(); + }); + }; + + var promise = paginateLoop(page); + + if (callback === undefined) { + return promise; + } + + promise + .then(function(res) { + callback(null, res); + }) + .catch(function(err) { + callback(err); + }); +}; + +Index.prototype.getObjectPosition = function(result, objectID) { + var hits = result.hits; + + for (var position = 0; position < hits.length; position++) { + if (hits[position].objectID === objectID) { + return position; + } + } + + return -1; +}; + +/* +* Set settings for this index +* +* @param settings the settings object that can contains : +* - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). +* - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). +* - hitsPerPage: (integer) the number of hits per page (default = 10). +* - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. +* If set to null, all attributes are retrieved. +* - attributesToHighlight: (array of strings) default list of attributes to highlight. +* If set to null, all indexed attributes are highlighted. +* - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number +* of words to return (syntax is attributeName:nbWords). +* By default no snippet is computed. If set to null, no snippet is computed. +* - attributesToIndex: (array of strings) the list of fields you want to index. +* If set to null, all textual and numerical attributes of your objects are indexed, +* but you should update it to get optimal results. +* This parameter has two important uses: +* - Limit the attributes to index: For example if you store a binary image in base64, +* you want to store it and be able to +* retrieve it but you don't want to search in the base64 string. +* - Control part of the ranking*: (see the ranking parameter for full explanation) +* Matches in attributes at the beginning of +* the list will be considered more important than matches in attributes further down the list. +* In one attribute, matching text at the beginning of the attribute will be +* considered more important than text after, you can disable +* this behavior if you add your attribute inside `unordered(AttributeName)`, +* for example attributesToIndex: ["title", "unordered(text)"]. +* - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. +* All strings in the attribute selected for faceting are extracted and added as a facet. +* If set to null, no attribute is used for faceting. +* - attributeForDistinct: (string) The attribute name used for the Distinct feature. +* This feature is similar to the SQL "distinct" keyword: when enabled +* in query with the distinct=1 parameter, all hits containing a duplicate +* value for this attribute are removed from results. +* For example, if the chosen attribute is show_name and several hits have +* the same value for show_name, then only the best one is kept and others are removed. +* - ranking: (array of strings) controls the way results are sorted. +* We have six available criteria: +* - typo: sort according to number of typos, +* - geo: sort according to decreassing distance when performing a geo-location based search, +* - proximity: sort according to the proximity of query words in hits, +* - attribute: sort according to the order of attributes defined by attributesToIndex, +* - exact: +* - if the user query contains one word: sort objects having an attribute +* that is exactly the query word before others. +* For example if you search for the "V" TV show, you want to find it +* with the "V" query and avoid to have all popular TV +* show starting by the v letter before it. +* - if the user query contains multiple words: sort according to the +* number of words that matched exactly (and not as a prefix). +* - custom: sort according to a user defined formula set in **customRanking** attribute. +* The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] +* - customRanking: (array of strings) lets you specify part of the ranking. +* The syntax of this condition is an array of strings containing attributes +* prefixed by asc (ascending order) or desc (descending order) operator. +* For example `"customRanking" => ["desc(population)", "asc(name)"]` +* - queryType: Select how the query words are interpreted, it can be one of the following value: +* - prefixAll: all query words are interpreted as prefixes, +* - prefixLast: only the last word is interpreted as a prefix (default behavior), +* - prefixNone: no query word is interpreted as a prefix. This option is not recommended. +* - highlightPreTag: (string) Specify the string that is inserted before +* the highlighted parts in the query result (default to ""). +* - highlightPostTag: (string) Specify the string that is inserted after +* the highlighted parts in the query result (default to ""). +* - optionalWords: (array of strings) Specify a list of words that should +* be considered as optional when found in the query. +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the server answer or the error message if a failure occurred +*/ +Index.prototype.setSettings = function(settings, opts, callback) { + if (arguments.length === 1 || typeof opts === 'function') { + callback = opts; + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + var indexObj = this; + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings?forwardToReplicas=' + + forwardToReplicas, + hostType: 'write', + body: settings, + callback: callback + }); +}; + +/* +* @deprecated see client.listApiKeys() +*/ +Index.prototype.listUserKeys = deprecate(function(callback) { + return this.listApiKeys(callback); +}, deprecatedMessage('index.listUserKeys()', 'client.listApiKeys()')); + +/* +* List all existing API keys to this index +* +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with API keys belonging to the index +* +* @deprecated see client.listApiKeys() +*/ +Index.prototype.listApiKeys = deprecate(function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + hostType: 'read', + callback: callback + }); +}, deprecatedMessage('index.listApiKeys()', 'client.listApiKeys()')); + +/* +* @deprecated see client.getApiKey() +*/ +Index.prototype.getUserKeyACL = deprecate(function(key, callback) { + return this.getApiKey(key, callback); +}, deprecatedMessage('index.getUserKeyACL()', 'client.getApiKey()')); + + +/* +* Get an API key from this index +* +* @param key +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the right API key +* +* @deprecated see client.getApiKey() +*/ +Index.prototype.getApiKey = deprecate(function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + hostType: 'read', + callback: callback + }); +}, deprecatedMessage('index.getApiKey()', 'client.getApiKey()')); + +/* +* @deprecated see client.deleteApiKey() +*/ +Index.prototype.deleteUserKey = deprecate(function(key, callback) { + return this.deleteApiKey(key, callback); +}, deprecatedMessage('index.deleteUserKey()', 'client.deleteApiKey()')); + +/* +* Delete an existing API key associated to this index +* +* @param key +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the deletion date +* +* @deprecated see client.deleteApiKey() +*/ +Index.prototype.deleteApiKey = deprecate(function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.deleteApiKey()', 'client.deleteApiKey()')); + +/* +* @deprecated see client.addApiKey() +*/ +Index.prototype.addUserKey = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('index.addUserKey()', 'client.addApiKey()')); + +/* +* Add a new API key to this index +* +* @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that +* can contains the following values: +* - search: allow to search (https and http) +* - addObject: allows to add/update an object in the index (https only) +* - deleteObject : allows to delete an existing object (https only) +* - deleteIndex : allows to delete index content (https only) +* - settings : allows to get index settings (https only) +* - editSettings : allows to change index settings (https only) +* @param {Object} [params] - Optionnal parameters to set for the key +* @param {number} params.validity - Number of seconds after which the key will +* be automatically removed (0 means no time limit for this key) +* @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour +* @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call +* @param {string} params.description - A description for your key +* @param {string[]} params.referers - A list of authorized referers +* @param {Object} params.queryParameters - Force the key to use specific query parameters +* @param {Function} callback - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the added API key +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.addUserKey(['search'], { +* validity: 300, +* maxQueriesPerIPPerHour: 2000, +* maxHitsPerQuery: 3, +* description: 'Eat three fruits', +* referers: ['*.algolia.com'], +* queryParameters: { +* tagFilters: ['public'], +* } +* }) +* @see {@link https://www.algolia.com/doc/rest_api#AddIndexKey|Algolia REST API Documentation} +* +* @deprecated see client.addApiKey() +*/ +Index.prototype.addApiKey = deprecate(function(acls, params, callback) { + var isArray = require(8); + var usage = 'Usage: index.addApiKey(arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = null; + } + + var postObj = { + acl: acls + }; + + if (params) { + postObj.validity = params.validity; + postObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + postObj.maxHitsPerQuery = params.maxHitsPerQuery; + postObj.description = params.description; + + if (params.queryParameters) { + postObj.queryParameters = this.as._getSearchParams(params.queryParameters, ''); + } + + postObj.referers = params.referers; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/keys', + body: postObj, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.addApiKey()', 'client.addApiKey()')); + +/** +* @deprecated use client.addApiKey() +*/ +Index.prototype.addUserKeyWithValidity = deprecate(function deprecatedAddUserKeyWithValidity(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('index.addUserKeyWithValidity()', 'client.addApiKey()')); + +/* +* @deprecated see client.updateApiKey() +*/ +Index.prototype.updateUserKey = deprecate(function(key, acls, params, callback) { + return this.updateApiKey(key, acls, params, callback); +}, deprecatedMessage('index.updateUserKey()', 'client.updateApiKey()')); + +/** +* Update an existing API key of this index +* @param {string} key - The key to update +* @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that +* can contains the following values: +* - search: allow to search (https and http) +* - addObject: allows to add/update an object in the index (https only) +* - deleteObject : allows to delete an existing object (https only) +* - deleteIndex : allows to delete index content (https only) +* - settings : allows to get index settings (https only) +* - editSettings : allows to change index settings (https only) +* @param {Object} [params] - Optionnal parameters to set for the key +* @param {number} params.validity - Number of seconds after which the key will +* be automatically removed (0 means no time limit for this key) +* @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour +* @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call +* @param {string} params.description - A description for your key +* @param {string[]} params.referers - A list of authorized referers +* @param {Object} params.queryParameters - Force the key to use specific query parameters +* @param {Function} callback - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with user keys list +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.updateApiKey('APIKEY', ['search'], { +* validity: 300, +* maxQueriesPerIPPerHour: 2000, +* maxHitsPerQuery: 3, +* description: 'Eat three fruits', +* referers: ['*.algolia.com'], +* queryParameters: { +* tagFilters: ['public'], +* } +* }) +* @see {@link https://www.algolia.com/doc/rest_api#UpdateIndexKey|Algolia REST API Documentation} +* +* @deprecated see client.updateApiKey() +*/ +Index.prototype.updateApiKey = deprecate(function(key, acls, params, callback) { + var isArray = require(8); + var usage = 'Usage: index.updateApiKey(key, arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 2 || typeof params === 'function') { + callback = params; + params = null; + } + + var putObj = { + acl: acls + }; + + if (params) { + putObj.validity = params.validity; + putObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + putObj.maxHitsPerQuery = params.maxHitsPerQuery; + putObj.description = params.description; + + if (params.queryParameters) { + putObj.queryParameters = this.as._getSearchParams(params.queryParameters, ''); + } + + putObj.referers = params.referers; + } + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/keys/' + key, + body: putObj, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.updateApiKey()', 'client.updateApiKey()')); + +},{"19":19,"20":20,"27":27,"29":29,"30":30,"31":31,"32":32,"33":33,"34":34,"7":7,"8":8}],19:[function(require,module,exports){ +'use strict'; + +// This is the object returned by the `index.browseAll()` method + +module.exports = IndexBrowser; + +var inherits = require(7); +var EventEmitter = require(4).EventEmitter; + +function IndexBrowser() { +} + +inherits(IndexBrowser, EventEmitter); + +IndexBrowser.prototype.stop = function() { + this._stopped = true; + this._clean(); +}; + +IndexBrowser.prototype._end = function() { + this.emit('end'); + this._clean(); +}; + +IndexBrowser.prototype._error = function(err) { + this.emit('error', err); + this._clean(); +}; + +IndexBrowser.prototype._result = function(content) { + this.emit('result', content); +}; + +IndexBrowser.prototype._clean = function() { + this.removeAllListeners('stop'); + this.removeAllListeners('end'); + this.removeAllListeners('error'); + this.removeAllListeners('result'); +}; + +},{"4":4,"7":7}],20:[function(require,module,exports){ +var buildSearchMethod = require(26); +var deprecate = require(29); +var deprecatedMessage = require(30); + +module.exports = IndexCore; + +/* +* Index class constructor. +* You should not use this method directly but use initIndex() function +*/ +function IndexCore(algoliasearch, indexName) { + this.indexName = indexName; + this.as = algoliasearch; + this.typeAheadArgs = null; + this.typeAheadValueOption = null; + + // make sure every index instance has it's own cache + this.cache = {}; +} + +/* +* Clear all queries in cache +*/ +IndexCore.prototype.clearCache = function() { + this.cache = {}; +}; + +/* +* Search inside the index using XMLHttpRequest request (Using a POST query to +* minimize number of OPTIONS queries: Cross-Origin Resource Sharing). +* +* @param {string} [query] the full text query +* @param {object} [args] (optional) if set, contains an object with query parameters: +* - page: (integer) Pagination parameter used to select the page to retrieve. +* Page is zero-based and defaults to 0. Thus, +* to retrieve the 10th page you need to set page=9 +* - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. +* - attributesToRetrieve: a string that contains the list of object attributes +* you want to retrieve (let you minimize the answer size). +* Attributes are separated with a comma (for example "name,address"). +* You can also use an array (for example ["name","address"]). +* By default, all attributes are retrieved. You can also use '*' to retrieve all +* values when an attributesToRetrieve setting is specified for your index. +* - attributesToHighlight: a string that contains the list of attributes you +* want to highlight according to the query. +* Attributes are separated by a comma. You can also use an array (for example ["name","address"]). +* If an attribute has no match for the query, the raw value is returned. +* By default all indexed text attributes are highlighted. +* You can use `*` if you want to highlight all textual attributes. +* Numerical attributes are not highlighted. +* A matchLevel is returned for each highlighted attribute and can contain: +* - full: if all the query terms were found in the attribute, +* - partial: if only some of the query terms were found, +* - none: if none of the query terms were found. +* - attributesToSnippet: a string that contains the list of attributes to snippet alongside +* the number of words to return (syntax is `attributeName:nbWords`). +* Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). +* You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). +* By default no snippet is computed. +* - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. +* Defaults to 3. +* - minWordSizefor2Typos: the minimum number of characters in a query word +* to accept two typos in this word. Defaults to 7. +* - getRankingInfo: if set to 1, the result hits will contain ranking +* information in _rankingInfo attribute. +* - aroundLatLng: search for entries around a given +* latitude/longitude (specified as two floats separated by a comma). +* For example aroundLatLng=47.316669,5.016670). +* You can specify the maximum distance in meters with the aroundRadius parameter (in meters) +* and the precision for ranking with aroundPrecision +* (for example if you set aroundPrecision=100, two objects that are distant of +* less than 100m will be considered as identical for "geo" ranking parameter). +* At indexing, you should specify geoloc of an object with the _geoloc attribute +* (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) +* - insideBoundingBox: search entries inside a given area defined by the two extreme points +* of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). +* For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). +* At indexing, you should specify geoloc of an object with the _geoloc attribute +* (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) +* - numericFilters: a string that contains the list of numeric filters you want to +* apply separated by a comma. +* The syntax of one filter is `attributeName` followed by `operand` followed by `value`. +* Supported operands are `<`, `<=`, `=`, `>` and `>=`. +* You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. +* You can also use an array (for example numericFilters: ["price>100","price<1000"]). +* - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. +* To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). +* You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] +* means tag1 AND (tag2 OR tag3). +* At indexing, tags should be added in the _tags** attribute +* of objects (for example {"_tags":["tag1","tag2"]}). +* - facetFilters: filter the query by a list of facets. +* Facets are separated by commas and each facet is encoded as `attributeName:value`. +* For example: `facetFilters=category:Book,author:John%20Doe`. +* You can also use an array (for example `["category:Book","author:John%20Doe"]`). +* - facets: List of object attributes that you want to use for faceting. +* Comma separated list: `"category,author"` or array `['category','author']` +* Only attributes that have been added in **attributesForFaceting** index setting +* can be used in this parameter. +* You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. +* - queryType: select how the query words are interpreted, it can be one of the following value: +* - prefixAll: all query words are interpreted as prefixes, +* - prefixLast: only the last word is interpreted as a prefix (default behavior), +* - prefixNone: no query word is interpreted as a prefix. This option is not recommended. +* - optionalWords: a string that contains the list of words that should +* be considered as optional when found in the query. +* Comma separated and array are accepted. +* - distinct: If set to 1, enable the distinct feature (disabled by default) +* if the attributeForDistinct index setting is set. +* This feature is similar to the SQL "distinct" keyword: when enabled +* in a query with the distinct=1 parameter, +* all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. +* For example, if the chosen attribute is show_name and several hits have +* the same value for show_name, then only the best +* one is kept and others are removed. +* - restrictSearchableAttributes: List of attributes you want to use for +* textual search (must be a subset of the attributesToIndex index setting) +* either comma separated or as an array +* @param {function} [callback] the result callback called with two arguments: +* error: null or Error('message'). If false, the content contains the error. +* content: the server answer that contains the list of results. +*/ +IndexCore.prototype.search = buildSearchMethod('query'); + +/* +* -- BETA -- +* Search a record similar to the query inside the index using XMLHttpRequest request (Using a POST query to +* minimize number of OPTIONS queries: Cross-Origin Resource Sharing). +* +* @param {string} [query] the similar query +* @param {object} [args] (optional) if set, contains an object with query parameters. +* All search parameters are supported (see search function), restrictSearchableAttributes and facetFilters +* are the two most useful to restrict the similar results and get more relevant content +*/ +IndexCore.prototype.similarSearch = deprecate( + buildSearchMethod('similarQuery'), + deprecatedMessage( + 'index.similarSearch(query[, callback])', + 'index.search({ similarQuery: query }[, callback])' + ) +); + +/* +* Browse index content. The response content will have a `cursor` property that you can use +* to browse subsequent pages for this query. Use `index.browseFrom(cursor)` when you want. +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @param {Function} [callback] - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the browse result +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.browse('cool songs', { +* tagFilters: 'public,comments', +* hitsPerPage: 500 +* }, callback); +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +IndexCore.prototype.browse = function(query, queryParameters, callback) { + var merge = require(34); + + var indexObj = this; + + var page; + var hitsPerPage; + + // we check variadic calls that are not the one defined + // .browse()/.browse(fn) + // => page = 0 + if (arguments.length === 0 || arguments.length === 1 && typeof arguments[0] === 'function') { + page = 0; + callback = arguments[0]; + query = undefined; + } else if (typeof arguments[0] === 'number') { + // .browse(2)/.browse(2, 10)/.browse(2, fn)/.browse(2, 10, fn) + page = arguments[0]; + if (typeof arguments[1] === 'number') { + hitsPerPage = arguments[1]; + } else if (typeof arguments[1] === 'function') { + callback = arguments[1]; + hitsPerPage = undefined; + } + query = undefined; + queryParameters = undefined; + } else if (typeof arguments[0] === 'object') { + // .browse(queryParameters)/.browse(queryParameters, cb) + if (typeof arguments[1] === 'function') { + callback = arguments[1]; + } + queryParameters = arguments[0]; + query = undefined; + } else if (typeof arguments[0] === 'string' && typeof arguments[1] === 'function') { + // .browse(query, cb) + callback = arguments[1]; + queryParameters = undefined; + } + + // otherwise it's a .browse(query)/.browse(query, queryParameters)/.browse(query, queryParameters, cb) + + // get search query parameters combining various possible calls + // to .browse(); + queryParameters = merge({}, queryParameters || {}, { + page: page, + hitsPerPage: hitsPerPage, + query: query + }); + + var params = this.as._getSearchParams(queryParameters, ''); + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse', + body: {params: params}, + hostType: 'read', + callback: callback + }); +}; + +/* +* Continue browsing from a previous position (cursor), obtained via a call to `.browse()`. +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @param {Function} [callback] - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the browse result +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.browseFrom('14lkfsakl32', callback); +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +IndexCore.prototype.browseFrom = function(cursor, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/browse', + body: {cursor: cursor}, + hostType: 'read', + callback: callback + }); +}; + +/* +* Search for facet values +* https://www.algolia.com/doc/rest-api/search#search-for-facet-values +* +* @param {string} params.facetName Facet name, name of the attribute to search for values in. +* Must be declared as a facet +* @param {string} params.facetQuery Query for the facet search +* @param {string} [params.*] Any search parameter of Algolia, +* see https://www.algolia.com/doc/api-client/javascript/search#search-parameters +* Pagination is not supported. The page and hitsPerPage parameters will be ignored. +* @param callback (optional) +*/ +IndexCore.prototype.searchForFacetValues = function(params, callback) { + var clone = require(27); + var omit = require(35); + var usage = 'Usage: index.searchForFacetValues({facetName, facetQuery, ...params}[, callback])'; + + if (params.facetName === undefined || params.facetQuery === undefined) { + throw new Error(usage); + } + + var facetName = params.facetName; + var filteredParams = omit(clone(params), function(keyName) { + return keyName === 'facetName'; + }); + var searchParameters = this.as._getSearchParams(filteredParams, ''); + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + + encodeURIComponent(this.indexName) + '/facets/' + encodeURIComponent(facetName) + '/query', + hostType: 'read', + body: {params: searchParameters}, + callback: callback + }); +}; + +IndexCore.prototype.searchFacet = deprecate(function(params, callback) { + return this.searchForFacetValues(params, callback); +}, deprecatedMessage( + 'index.searchFacet(params[, callback])', + 'index.searchForFacetValues(params[, callback])' +)); + +IndexCore.prototype._search = function(params, url, callback, additionalUA) { + return this.as._jsonRequest({ + cache: this.cache, + method: 'POST', + url: url || '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: {params: params}, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName), + body: {params: params} + }, + callback: callback, + additionalUA: additionalUA + }); +}; + +/* +* Get an object from this index +* +* @param objectID the unique identifier of the object to retrieve +* @param attrs (optional) if set, contains the array of attribute names to retrieve +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the object to retrieve or the error message if a failure occurred +*/ +IndexCore.prototype.getObject = function(objectID, attrs, callback) { + var indexObj = this; + + if (arguments.length === 1 || typeof attrs === 'function') { + callback = attrs; + attrs = undefined; + } + + var params = ''; + if (attrs !== undefined) { + params = '?attributes='; + for (var i = 0; i < attrs.length; ++i) { + if (i !== 0) { + params += ','; + } + params += attrs[i]; + } + } + + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, + hostType: 'read', + callback: callback + }); +}; + +/* +* Get several objects from this index +* +* @param objectIDs the array of unique identifier of objects to retrieve +*/ +IndexCore.prototype.getObjects = function(objectIDs, attributesToRetrieve, callback) { + var isArray = require(8); + var map = require(33); + + var usage = 'Usage: index.getObjects(arrayOfObjectIDs[, callback])'; + + if (!isArray(objectIDs)) { + throw new Error(usage); + } + + var indexObj = this; + + if (arguments.length === 1 || typeof attributesToRetrieve === 'function') { + callback = attributesToRetrieve; + attributesToRetrieve = undefined; + } + + var body = { + requests: map(objectIDs, function prepareRequest(objectID) { + var request = { + indexName: indexObj.indexName, + objectID: objectID + }; + + if (attributesToRetrieve) { + request.attributesToRetrieve = attributesToRetrieve.join(','); + } + + return request; + }) + }; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/*/objects', + hostType: 'read', + body: body, + callback: callback + }); +}; + +IndexCore.prototype.as = null; +IndexCore.prototype.indexName = null; +IndexCore.prototype.typeAheadArgs = null; +IndexCore.prototype.typeAheadValueOption = null; + +},{"26":26,"27":27,"29":29,"30":30,"33":33,"34":34,"35":35,"8":8}],21:[function(require,module,exports){ +(function (process){ +'use strict'; + +// This is the AngularJS Algolia Search module +// It's using $http to do requests with a JSONP fallback +// $q promises are returned + +var inherits = require(7); + +var forEach = require(5); + +var AlgoliaSearch = require(16); +var errors = require(31); +var inlineHeaders = require(24); +var jsonpRequest = require(25); +var places = require(36); + +// expose original algoliasearch fn in window +window.algoliasearch = require(22); + +if (process.env.NODE_ENV === 'debug') { + require(1).enable('algoliasearch*'); +} + +window.angular.module('algoliasearch', []) + .service('algolia', ['$http', '$q', '$timeout', function algoliaSearchService($http, $q, $timeout) { + function algoliasearch(applicationID, apiKey, opts) { + var cloneDeep = require(27); + + opts = cloneDeep(opts || {}); + + opts._ua = opts._ua || algoliasearch.ua; + + return new AlgoliaSearchAngular(applicationID, apiKey, opts); + } + + algoliasearch.version = require(38); + + algoliasearch.ua = + 'Algolia for JavaScript (' + algoliasearch.version + '); ' + + 'AngularJS (' + window.angular.version.full + ')'; + + algoliasearch.initPlaces = places(algoliasearch); + + // we expose into window no matter how we are used, this will allow + // us to easily debug any website running algolia + window.__algolia = { + debug: require(1), + algoliasearch: algoliasearch + }; + + function AlgoliaSearchAngular() { + // call AlgoliaSearch constructor + AlgoliaSearch.apply(this, arguments); + } + + inherits(AlgoliaSearchAngular, AlgoliaSearch); + + AlgoliaSearchAngular.prototype._request = function request(url, opts) { + // Support most Angular.js versions by using $q.defer() instead + // of the new $q() constructor everywhere we need a promise + var deferred = $q.defer(); + var resolve = deferred.resolve; + var reject = deferred.reject; + + var timedOut; + var body = opts.body; + + url = inlineHeaders(url, opts.headers); + + var timeoutDeferred = $q.defer(); + var timeoutPromise = timeoutDeferred.promise; + + $timeout(function timedout() { + timedOut = true; + // will cancel the xhr + timeoutDeferred.resolve('test'); + reject(new errors.RequestTimeout()); + }, opts.timeouts.complete); + + var requestHeaders = {}; + + // "remove" (set to undefined) possible globally set headers + // in $httpProvider.defaults.headers.common + // otherwise we might fail sometimes + // ref: https://github.com/algolia/algoliasearch-client-js/issues/135 + forEach( + $http.defaults.headers.common, + function removeIt(headerValue, headerName) { + requestHeaders[headerName] = undefined; + } + ); + + requestHeaders.accept = 'application/json'; + + if (body) { + if (opts.method === 'POST') { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Simple_requests + requestHeaders['content-type'] = 'application/x-www-form-urlencoded'; + } else { + requestHeaders['content-type'] = 'application/json'; + } + } + + $http({ + url: url, + method: opts.method, + data: body, + cache: false, + timeout: timeoutPromise, + headers: requestHeaders, + transformResponse: transformResponse, + // if client uses $httpProvider.defaults.withCredentials = true, + // we revert it to false to avoid CORS failure + withCredentials: false + }).then(success, error); + + function success(response) { + resolve({ + statusCode: response.status, + headers: response.headers, + body: JSON.parse(response.data), + responseText: response.data + }); + } + + // we force getting the raw data because we need it so + // for cache keys + function transformResponse(data) { + return data; + } + + function error(response) { + if (timedOut) { + return; + } + + // network error + if (response.status === 0) { + reject( + new errors.Network({ + more: response + }) + ); + return; + } + + resolve({ + body: JSON.parse(response.data), + statusCode: response.status + }); + } + + return deferred.promise; + }; + + // using IE8 or IE9 we will always end up here + // AngularJS does not fallback to XDomainRequest + AlgoliaSearchAngular.prototype._request.fallback = function requestFallback(url, opts) { + url = inlineHeaders(url, opts.headers); + + var deferred = $q.defer(); + var resolve = deferred.resolve; + var reject = deferred.reject; + + jsonpRequest(url, opts, function jsonpRequestDone(err, content) { + if (err) { + reject(err); + return; + } + + resolve(content); + }); + + return deferred.promise; + }; + + AlgoliaSearchAngular.prototype._promise = { + reject: function(val) { + return $q.reject(val); + }, + resolve: function(val) { + // http://www.bennadel.com/blog/2735-q-when-is-the-missing-q-resolve-method-in-angularjs.htm + return $q.when(val); + }, + delay: function(ms) { + var deferred = $q.defer(); + var resolve = deferred.resolve; + + $timeout(resolve, ms); + + return deferred.promise; + }, + all: function(promises) { + return $q.all(promises); + } + }; + + return { + Client: function(applicationID, apiKey, options) { + return algoliasearch(applicationID, apiKey, options); + }, + ua: algoliasearch.ua, + version: algoliasearch.version + }; + }]); + +}).call(this,require(12)) +},{"1":1,"12":12,"16":16,"22":22,"24":24,"25":25,"27":27,"31":31,"36":36,"38":38,"5":5,"7":7}],22:[function(require,module,exports){ +'use strict'; + +var AlgoliaSearch = require(16); +var createAlgoliasearch = require(23); + +module.exports = createAlgoliasearch(AlgoliaSearch, 'Browser'); + +},{"16":16,"23":23}],23:[function(require,module,exports){ +(function (process){ +'use strict'; + +var global = require(6); +var Promise = global.Promise || require(3).Promise; + +// This is the standalone browser build entry point +// Browser implementation of the Algolia Search JavaScript client, +// using XMLHttpRequest, XDomainRequest and JSONP as fallback +module.exports = function createAlgoliasearch(AlgoliaSearch, uaSuffix) { + var inherits = require(7); + var errors = require(31); + var inlineHeaders = require(24); + var jsonpRequest = require(25); + var places = require(36); + uaSuffix = uaSuffix || ''; + + if (process.env.NODE_ENV === 'debug') { + require(1).enable('algoliasearch*'); + } + + function algoliasearch(applicationID, apiKey, opts) { + var cloneDeep = require(27); + + opts = cloneDeep(opts || {}); + + opts._ua = opts._ua || algoliasearch.ua; + + return new AlgoliaSearchBrowser(applicationID, apiKey, opts); + } + + algoliasearch.version = require(38); + + algoliasearch.ua = + 'Algolia for JavaScript (' + algoliasearch.version + '); ' + uaSuffix; + + algoliasearch.initPlaces = places(algoliasearch); + + // we expose into window no matter how we are used, this will allow + // us to easily debug any website running algolia + global.__algolia = { + debug: require(1), + algoliasearch: algoliasearch + }; + + var support = { + hasXMLHttpRequest: 'XMLHttpRequest' in global, + hasXDomainRequest: 'XDomainRequest' in global + }; + + if (support.hasXMLHttpRequest) { + support.cors = 'withCredentials' in new XMLHttpRequest(); + } + + function AlgoliaSearchBrowser() { + // call AlgoliaSearch constructor + AlgoliaSearch.apply(this, arguments); + } + + inherits(AlgoliaSearchBrowser, AlgoliaSearch); + + AlgoliaSearchBrowser.prototype._request = function request(url, opts) { + return new Promise(function wrapRequest(resolve, reject) { + // no cors or XDomainRequest, no request + if (!support.cors && !support.hasXDomainRequest) { + // very old browser, not supported + reject(new errors.Network('CORS not supported')); + return; + } + + url = inlineHeaders(url, opts.headers); + + var body = opts.body; + var req = support.cors ? new XMLHttpRequest() : new XDomainRequest(); + var reqTimeout; + var timedOut; + var connected = false; + + reqTimeout = setTimeout(onTimeout, opts.timeouts.connect); + // we set an empty onprogress listener + // so that XDomainRequest on IE9 is not aborted + // refs: + // - https://github.com/algolia/algoliasearch-client-js/issues/76 + // - https://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified?forum=iewebdevelopment + req.onprogress = onProgress; + if ('onreadystatechange' in req) req.onreadystatechange = onReadyStateChange; + req.onload = onLoad; + req.onerror = onError; + + // do not rely on default XHR async flag, as some analytics code like hotjar + // breaks it and set it to false by default + if (req instanceof XMLHttpRequest) { + req.open(opts.method, url, true); + + // The Analytics API never accepts Auth headers as query string + // this option exists specifically for them. + if (opts.forceAuthHeaders) { + req.setRequestHeader( + 'x-algolia-application-id', + opts.headers['x-algolia-application-id'] + ); + req.setRequestHeader( + 'x-algolia-api-key', + opts.headers['x-algolia-api-key'] + ); + } + } else { + req.open(opts.method, url); + } + + // headers are meant to be sent after open + if (support.cors) { + if (body) { + if (opts.method === 'POST') { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Simple_requests + req.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); + } else { + req.setRequestHeader('content-type', 'application/json'); + } + } + req.setRequestHeader('accept', 'application/json'); + } + + if (body) { + req.send(body); + } else { + req.send(); + } + + // event object not received in IE8, at least + // but we do not use it, still important to note + function onLoad(/* event */) { + // When browser does not supports req.timeout, we can + // have both a load and timeout event, since handled by a dumb setTimeout + if (timedOut) { + return; + } + + clearTimeout(reqTimeout); + + var out; + + try { + out = { + body: JSON.parse(req.responseText), + responseText: req.responseText, + statusCode: req.status, + // XDomainRequest does not have any response headers + headers: req.getAllResponseHeaders && req.getAllResponseHeaders() || {} + }; + } catch (e) { + out = new errors.UnparsableJSON({ + more: req.responseText + }); + } + + if (out instanceof errors.UnparsableJSON) { + reject(out); + } else { + resolve(out); + } + } + + function onError(event) { + if (timedOut) { + return; + } + + clearTimeout(reqTimeout); + + // error event is trigerred both with XDR/XHR on: + // - DNS error + // - unallowed cross domain request + reject( + new errors.Network({ + more: event + }) + ); + } + + function onTimeout() { + timedOut = true; + req.abort(); + + reject(new errors.RequestTimeout()); + } + + function onConnect() { + connected = true; + clearTimeout(reqTimeout); + reqTimeout = setTimeout(onTimeout, opts.timeouts.complete); + } + + function onProgress() { + if (!connected) onConnect(); + } + + function onReadyStateChange() { + if (!connected && req.readyState > 1) onConnect(); + } + }); + }; + + AlgoliaSearchBrowser.prototype._request.fallback = function requestFallback(url, opts) { + url = inlineHeaders(url, opts.headers); + + return new Promise(function wrapJsonpRequest(resolve, reject) { + jsonpRequest(url, opts, function jsonpRequestDone(err, content) { + if (err) { + reject(err); + return; + } + + resolve(content); + }); + }); + }; + + AlgoliaSearchBrowser.prototype._promise = { + reject: function rejectPromise(val) { + return Promise.reject(val); + }, + resolve: function resolvePromise(val) { + return Promise.resolve(val); + }, + delay: function delayPromise(ms) { + return new Promise(function resolveOnTimeout(resolve/* , reject*/) { + setTimeout(resolve, ms); + }); + }, + all: function all(promises) { + return Promise.all(promises); + } + }; + + return algoliasearch; +}; + +}).call(this,require(12)) +},{"1":1,"12":12,"24":24,"25":25,"27":27,"3":3,"31":31,"36":36,"38":38,"6":6,"7":7}],24:[function(require,module,exports){ +'use strict'; + +module.exports = inlineHeaders; + +var encode = require(14); + +function inlineHeaders(url, headers) { + if (/\?/.test(url)) { + url += '&'; + } else { + url += '?'; + } + + return url + encode(headers); +} + +},{"14":14}],25:[function(require,module,exports){ +'use strict'; + +module.exports = jsonpRequest; + +var errors = require(31); + +var JSONPCounter = 0; + +function jsonpRequest(url, opts, cb) { + if (opts.method !== 'GET') { + cb(new Error('Method ' + opts.method + ' ' + url + ' is not supported by JSONP.')); + return; + } + + opts.debug('JSONP: start'); + + var cbCalled = false; + var timedOut = false; + + JSONPCounter += 1; + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + var cbName = 'algoliaJSONP_' + JSONPCounter; + var done = false; + + window[cbName] = function(data) { + removeGlobals(); + + if (timedOut) { + opts.debug('JSONP: Late answer, ignoring'); + return; + } + + cbCalled = true; + + clean(); + + cb(null, { + body: data, + responseText: JSON.stringify(data)/* , + // We do not send the statusCode, there's no statusCode in JSONP, it will be + // computed using data.status && data.message like with XDR + statusCode*/ + }); + }; + + // add callback by hand + url += '&callback=' + cbName; + + // add body params manually + if (opts.jsonBody && opts.jsonBody.params) { + url += '&' + opts.jsonBody.params; + } + + var ontimeout = setTimeout(timeout, opts.timeouts.complete); + + // script onreadystatechange needed only for + // <= IE8 + // https://github.com/angular/angular.js/issues/4523 + script.onreadystatechange = readystatechange; + script.onload = success; + script.onerror = error; + + script.async = true; + script.defer = true; + script.src = url; + head.appendChild(script); + + function success() { + opts.debug('JSONP: success'); + + if (done || timedOut) { + return; + } + + done = true; + + // script loaded but did not call the fn => script loading error + if (!cbCalled) { + opts.debug('JSONP: Fail. Script loaded but did not call the callback'); + clean(); + cb(new errors.JSONPScriptFail()); + } + } + + function readystatechange() { + if (this.readyState === 'loaded' || this.readyState === 'complete') { + success(); + } + } + + function clean() { + clearTimeout(ontimeout); + script.onload = null; + script.onreadystatechange = null; + script.onerror = null; + head.removeChild(script); + } + + function removeGlobals() { + try { + delete window[cbName]; + delete window[cbName + '_loaded']; + } catch (e) { + window[cbName] = window[cbName + '_loaded'] = undefined; + } + } + + function timeout() { + opts.debug('JSONP: Script timeout'); + timedOut = true; + clean(); + cb(new errors.RequestTimeout()); + } + + function error() { + opts.debug('JSONP: Script error'); + + if (done || timedOut) { + return; + } + + clean(); + cb(new errors.JSONPScriptError()); + } +} + +},{"31":31}],26:[function(require,module,exports){ +module.exports = buildSearchMethod; + +var errors = require(31); + +/** + * Creates a search method to be used in clients + * @param {string} queryParam the name of the attribute used for the query + * @param {string} url the url + * @return {function} the search method + */ +function buildSearchMethod(queryParam, url) { + /** + * The search method. Prepares the data and send the query to Algolia. + * @param {string} query the string used for query search + * @param {object} args additional parameters to send with the search + * @param {function} [callback] the callback to be called with the client gets the answer + * @return {undefined|Promise} If the callback is not provided then this methods returns a Promise + */ + return function search(query, args, callback) { + // warn V2 users on how to search + if (typeof query === 'function' && typeof args === 'object' || + typeof callback === 'object') { + // .search(query, params, cb) + // .search(cb, params) + throw new errors.AlgoliaSearchError('index.search usage is index.search(query, params, cb)'); + } + + // Normalizing the function signature + if (arguments.length === 0 || typeof query === 'function') { + // Usage : .search(), .search(cb) + callback = query; + query = ''; + } else if (arguments.length === 1 || typeof args === 'function') { + // Usage : .search(query/args), .search(query, cb) + callback = args; + args = undefined; + } + // At this point we have 3 arguments with values + + // Usage : .search(args) // careful: typeof null === 'object' + if (typeof query === 'object' && query !== null) { + args = query; + query = undefined; + } else if (query === undefined || query === null) { // .search(undefined/null) + query = ''; + } + + var params = ''; + + if (query !== undefined) { + params += queryParam + '=' + encodeURIComponent(query); + } + + var additionalUA; + if (args !== undefined) { + if (args.additionalUA) { + additionalUA = args.additionalUA; + delete args.additionalUA; + } + // `_getSearchParams` will augment params, do not be fooled by the = versus += from previous if + params = this.as._getSearchParams(args, params); + } + + + return this._search(params, url, callback, additionalUA); + }; +} + +},{"31":31}],27:[function(require,module,exports){ +module.exports = function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +}; + +},{}],28:[function(require,module,exports){ +module.exports = createAnalyticsClient; + +var algoliasearch = require(22); + +function createAnalyticsClient(appId, apiKey, opts) { + var analytics = {}; + + opts = opts || {}; + // there need to be 4 hosts, like on the client, since if requests fail, + // the counter goes up by 1, so we need to have the same amount of hosts + // 4 because: -dsn, -1, -2, -3 + // This is done because the APPID used for search will be the same for the analytics client created, + // and since the state of available hosts is shared by APPID globally for the module, we had issues + // where the hostIndex would be 1 while the array was only one entry (you got an empty host) + opts.hosts = opts.hosts || [ + 'analytics.algolia.com', + 'analytics.algolia.com', + 'analytics.algolia.com', + 'analytics.algolia.com' + ]; + opts.protocol = opts.protocol || 'https:'; + + analytics.as = algoliasearch(appId, apiKey, opts); + + analytics.getABTests = function(_params, callback) { + var params = params || {}; + var offset = params.offset || 0; + var limit = params.limit || 10; + + return this.as._jsonRequest({ + method: 'GET', + url: '/2/abtests?offset=' + encodeURIComponent(offset) + '&limit=' + encodeURIComponent(limit), + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.getABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/2/abtests/' + encodeURIComponent(abTestID), + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.addABTest = function(abTest, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/2/abtests', + body: abTest, + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.stopABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/2/abtests/' + encodeURIComponent(abTestID) + '/stop', + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.deleteABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'DELETE', + url: '/2/abtests/' + encodeURIComponent(abTestID), + hostType: 'write', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.waitTask = function(indexName, taskID, callback) { + return this.as.initIndex(indexName).waitTask(taskID, callback); + }; + + return analytics; +} + +},{"22":22}],29:[function(require,module,exports){ +module.exports = function deprecate(fn, message) { + var warned = false; + + function deprecated() { + if (!warned) { + /* eslint no-console:0 */ + console.warn(message); + warned = true; + } + + return fn.apply(this, arguments); + } + + return deprecated; +}; + +},{}],30:[function(require,module,exports){ +module.exports = function deprecatedMessage(previousUsage, newUsage) { + var githubAnchorLink = previousUsage.toLowerCase() + .replace(/[\.\(\)]/g, ''); + + return 'algoliasearch: `' + previousUsage + '` was replaced by `' + newUsage + + '`. Please see https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated#' + githubAnchorLink; +}; + +},{}],31:[function(require,module,exports){ +'use strict'; + +// This file hosts our error definitions +// We use custom error "types" so that we can act on them when we need it +// e.g.: if error instanceof errors.UnparsableJSON then.. + +var inherits = require(7); + +function AlgoliaSearchError(message, extraProperties) { + var forEach = require(5); + + var error = this; + + // try to get a stacktrace + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } else { + error.stack = (new Error()).stack || 'Cannot get a stacktrace, browser is too old'; + } + + this.name = 'AlgoliaSearchError'; + this.message = message || 'Unknown error'; + + if (extraProperties) { + forEach(extraProperties, function addToErrorObject(value, key) { + error[key] = value; + }); + } +} + +inherits(AlgoliaSearchError, Error); + +function createCustomError(name, message) { + function AlgoliaSearchCustomError() { + var args = Array.prototype.slice.call(arguments, 0); + + // custom message not set, use default + if (typeof args[0] !== 'string') { + args.unshift(message); + } + + AlgoliaSearchError.apply(this, args); + this.name = 'AlgoliaSearch' + name + 'Error'; + } + + inherits(AlgoliaSearchCustomError, AlgoliaSearchError); + + return AlgoliaSearchCustomError; +} + +// late exports to let various fn defs and inherits take place +module.exports = { + AlgoliaSearchError: AlgoliaSearchError, + UnparsableJSON: createCustomError( + 'UnparsableJSON', + 'Could not parse the incoming response as JSON, see err.more for details' + ), + RequestTimeout: createCustomError( + 'RequestTimeout', + 'Request timed out before getting a response' + ), + Network: createCustomError( + 'Network', + 'Network issue, see err.more for details' + ), + JSONPScriptFail: createCustomError( + 'JSONPScriptFail', + '"),window.ALGOLIA_SUPPORTS_DOCWRITE===!0?(document.write(''),n("document.write")()):r(o,n("DOMElement"))}catch(s){r(o,n("DOMElement"))}}function n(e){return function(){var t="AlgoliaSearch: loaded V2 script using "+e;window.console&&window.console.log&&window.console.log(t)}}t.exports=o},{1:1}],4:[function(e,t,r){"use strict";function o(){var e="-- AlgoliaSearch V2 => V3 error --\nYou are trying to use a new version of the AlgoliaSearch JavaScript client with an old notation.\nPlease read our migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x\n-- /AlgoliaSearch V2 => V3 error --";window.AlgoliaSearch=function(){throw new Error(e)},window.AlgoliaSearchHelper=function(){throw new Error(e)},window.AlgoliaExplainResults=function(){throw new Error(e)}}t.exports=o},{}],5:[function(e,t,r){"use strict";function o(t){var r=e(2),o=e(3),n=e(4);r(t)?o(t):n()}o("algoliasearch.angular")},{2:2,3:3,4:4}]},{},[5])(5)}),function e(t,r,o){function n(s,a){if(!r[s]){if(!t[s]){var c="function"==typeof require&&require;if(!a&&c)return c(s,!0);if(i)return i(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var l=r[s]={exports:{}};t[s][0].call(l.exports,function(e){var r=t[s][1][e];return n(r?r:e)},l,l.exports,e,t,r,o)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;s=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(e){var t=this.useColors;if(e[0]=(t?"%c":"")+this.namespace+(t?" %c":" ")+e[0]+(t?"%c ":" ")+"+"+r.humanize(this.diff),t){var o="color: "+this.color;e.splice(1,0,o,"color: inherit");var n=0,i=0;e[0].replace(/%[a-zA-Z%]/g,function(e){"%%"!==e&&(n++,"%c"===e&&(i=n))}),e.splice(i,0,o)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(e){try{null==e?r.storage.removeItem("debug"):r.storage.debug=e}catch(t){}}function c(){var e;try{e=r.storage.debug}catch(t){}return!e&&"undefined"!=typeof o&&"env"in o&&(e=o.env.DEBUG),e}function u(){try{return window.localStorage}catch(e){}}r=t.exports=e(2),r.log=s,r.formatArgs=i,r.save=a,r.load=c,r.useColors=n,r.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),r.colors=["lightseagreen","forestgreen","goldenrod","dodgerblue","darkorchid","crimson"],r.formatters.j=function(e){try{return JSON.stringify(e)}catch(t){return"[UnexpectedJSONParseError]: "+t.message}},r.enable(c())}).call(this,e(12))},{12:12,2:2}],2:[function(e,t,r){function o(e){var t,o=0;for(t in e)o=(o<<5)-o+e.charCodeAt(t),o|=0;return r.colors[Math.abs(o)%r.colors.length]}function n(e){function t(){if(t.enabled){var e=t,o=+new Date,n=o-(u||o);e.diff=n,e.prev=u,e.curr=o,u=o;for(var i=new Array(arguments.length),s=0;s0&&this._events[e].length>r&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace())),this},o.prototype.on=o.prototype.addListener,o.prototype.once=function(e,t){function r(){this.removeListener(e,r),o||(o=!0,t.apply(this,arguments))}if(!n(t))throw TypeError("listener must be a function");var o=!1;return r.listener=t,this.on(e,r),this},o.prototype.removeListener=function(e,t){var r,o,i,a;if(!n(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(r=this._events[e],i=r.length,o=-1,r===t||n(r.listener)&&r.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(s(r)){for(a=i;a-- >0;)if(r[a]===t||r[a].listener&&r[a].listener===t){o=a;break}if(o<0)return this;1===r.length?(r.length=0,delete this._events[e]):r.splice(o,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},o.prototype.removeAllListeners=function(e){var t,r;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(r=this._events[e],n(r))this.removeListener(e,r);else if(r)for(;r.length;)this.removeListener(e,r[r.length-1]);return delete this._events[e],this},o.prototype.listeners=function(e){var t;return t=this._events&&this._events[e]?n(this._events[e])?[this._events[e]]:this._events[e].slice():[]},o.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(n(t))return 1;if(t)return t.length}return 0},o.listenerCount=function(e,t){return e.listenerCount(t)}},{}],5:[function(e,t,r){var o=Object.prototype.hasOwnProperty,n=Object.prototype.toString;t.exports=function(e,t,r){if("[object Function]"!==n.call(t))throw new TypeError("iterator must be a function");var i=e.length;if(i===+i)for(var s=0;s100)){var t=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(e);if(t){var r=parseFloat(t[1]),o=(t[2]||"ms").toLowerCase();switch(o){case"years":case"year":case"yrs":case"yr":case"y":return r*p;case"days":case"day":case"d":return r*l;case"hours":case"hour":case"hrs":case"hr":case"h":return r*u;case"minutes":case"minute":case"mins":case"min":case"m":return r*c;case"seconds":case"second":case"secs":case"sec":case"s":return r*a;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return r;default:return}}}}function n(e){return e>=l?Math.round(e/l)+"d":e>=u?Math.round(e/u)+"h":e>=c?Math.round(e/c)+"m":e>=a?Math.round(e/a)+"s":e+"ms"}function i(e){return s(e,l,"day")||s(e,u,"hour")||s(e,c,"minute")||s(e,a,"second")||e+" ms"}function s(e,t,r){if(!(e0)return o(e);if("number"===r&&isNaN(e)===!1)return t["long"]?i(e):n(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},{}],10:[function(e,t,r){"use strict";var o=Object.prototype.hasOwnProperty,n=Object.prototype.toString,i=Array.prototype.slice,s=e(11),a=Object.prototype.propertyIsEnumerable,c=!a.call({toString:null},"toString"),u=a.call(function(){},"prototype"),l=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],p=function(e){var t=e.constructor;return t&&t.prototype===e},d={$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},h=function(){if("undefined"==typeof window)return!1;for(var e in window)try{if(!d["$"+e]&&o.call(window,e)&&null!==window[e]&&"object"==typeof window[e])try{p(window[e])}catch(t){return!0}}catch(t){return!0}return!1}(),f=function(e){if("undefined"==typeof window||!h)return p(e);try{return p(e)}catch(t){return!1}},y=function(e){var t=null!==e&&"object"==typeof e,r="[object Function]"===n.call(e),i=s(e),a=t&&"[object String]"===n.call(e),p=[];if(!t&&!r&&!i)throw new TypeError("Object.keys called on a non-object");var d=u&&r;if(a&&e.length>0&&!o.call(e,0))for(var h=0;h0)for(var y=0;y=0&&"[object Function]"===o.call(e.callee)),r}},{}],12:[function(e,t,r){function o(){throw new Error("setTimeout has not been defined")}function n(){throw new Error("clearTimeout has not been defined")}function i(e){if(p===setTimeout)return setTimeout(e,0);if((p===o||!p)&&setTimeout)return p=setTimeout,setTimeout(e,0);try{return p(e,0)}catch(t){try{return p.call(null,e,0)}catch(t){return p.call(this,e,0)}}}function s(e){if(d===clearTimeout)return clearTimeout(e);if((d===n||!d)&&clearTimeout)return d=clearTimeout,clearTimeout(e);try{return d(e)}catch(t){try{return d.call(null,e)}catch(t){return d.call(this,e)}}}function a(){m&&f&&(m=!1,f.length?y=f.concat(y):v=-1,y.length&&c())}function c(){if(!m){var e=i(a);m=!0;for(var t=y.length;t;){for(f=y,y=[];++v1)for(var r=1;r0&&u>c&&(u=c);for(var l=0;l=0?(p=y.substr(0,m),d=y.substr(m+1)):(p=y,d=""),h=decodeURIComponent(p),f=decodeURIComponent(d),o(s,h)?n(s[h])?s[h].push(f):s[h]=[s[h],f]:s[h]=f}return s};var n=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}},{}],14:[function(e,t,r){"use strict";function o(e,t){if(e.map)return e.map(t);for(var r=[],o=0;o0)n.scope=r;else if("undefined"!=typeof r)throw new Error("the scope given to `copyIndex` was not an array with settings, synonyms or rules");return this._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(e)+"/operation",body:n,hostType:"write",callback:i})},o.prototype.getLogs=function(t,r,o){var n=e(27),i={};return"object"==typeof t?(i=n(t),o=r):0===arguments.length||"function"==typeof t?o=t:1===arguments.length||"function"==typeof r?(o=r,i.offset=t):(i.offset=t,i.length=r),void 0===i.offset&&(i.offset=0),void 0===i.length&&(i.length=10),this._jsonRequest({method:"GET",url:"/1/logs?"+this._getSearchParams(i,""),hostType:"read",callback:o})},o.prototype.listIndexes=function(e,t){var r="";return void 0===e||"function"==typeof e?t=e:r="?page="+e,this._jsonRequest({method:"GET",url:"/1/indexes"+r,hostType:"read",callback:t})},o.prototype.initIndex=function(e){return new i(this,e)},o.prototype.initAnalytics=function(t){var r=e(28);return r(this.applicationID,this.apiKey,t)},o.prototype.listUserKeys=s(function(e){return this.listApiKeys(e)},a("client.listUserKeys()","client.listApiKeys()")),o.prototype.listApiKeys=function(e){return this._jsonRequest({method:"GET",url:"/1/keys",hostType:"read",callback:e})},o.prototype.getUserKeyACL=s(function(e,t){return this.getApiKey(e,t)},a("client.getUserKeyACL()","client.getApiKey()")),o.prototype.getApiKey=function(e,t){return this._jsonRequest({method:"GET",url:"/1/keys/"+e,hostType:"read",callback:t})},o.prototype.deleteUserKey=s(function(e,t){return this.deleteApiKey(e,t)},a("client.deleteUserKey()","client.deleteApiKey()")),o.prototype.deleteApiKey=function(e,t){return this._jsonRequest({method:"DELETE",url:"/1/keys/"+e,hostType:"write",callback:t})},o.prototype.restoreApiKey=function(e,t){return this._jsonRequest({method:"POST",url:"/1/keys/"+e+"/restore",hostType:"write",callback:t})},o.prototype.addUserKey=s(function(e,t,r){return this.addApiKey(e,t,r)},a("client.addUserKey()","client.addApiKey()")),o.prototype.addApiKey=function(t,r,o){var n=e(8),i="Usage: client.addApiKey(arrayOfAcls[, params, callback])";if(!n(t))throw new Error(i);1!==arguments.length&&"function"!=typeof r||(o=r,r=null);var s={acl:t};return r&&(s.validity=r.validity,s.maxQueriesPerIPPerHour=r.maxQueriesPerIPPerHour,s.maxHitsPerQuery=r.maxHitsPerQuery,s.indexes=r.indexes,s.description=r.description,r.queryParameters&&(s.queryParameters=this._getSearchParams(r.queryParameters,"")),s.referers=r.referers),this._jsonRequest({method:"POST",url:"/1/keys",body:s,hostType:"write",callback:o})},o.prototype.addUserKeyWithValidity=s(function(e,t,r){return this.addApiKey(e,t,r)},a("client.addUserKeyWithValidity()","client.addApiKey()")),o.prototype.updateUserKey=s(function(e,t,r,o){return this.updateApiKey(e,t,r,o)},a("client.updateUserKey()","client.updateApiKey()")),o.prototype.updateApiKey=function(t,r,o,n){var i=e(8),s="Usage: client.updateApiKey(key, arrayOfAcls[, params, callback])";if(!i(r))throw new Error(s);2!==arguments.length&&"function"!=typeof o||(n=o,o=null);var a={acl:r};return o&&(a.validity=o.validity,a.maxQueriesPerIPPerHour=o.maxQueriesPerIPPerHour,a.maxHitsPerQuery=o.maxHitsPerQuery,a.indexes=o.indexes,a.description=o.description,o.queryParameters&&(a.queryParameters=this._getSearchParams(o.queryParameters,"")),a.referers=o.referers),this._jsonRequest({method:"PUT",url:"/1/keys/"+t,body:a,hostType:"write",callback:n})},o.prototype.startQueriesBatch=s(function(){this._batch=[]},a("client.startQueriesBatch()","client.search()")),o.prototype.addQueryInBatch=s(function(e,t,r){this._batch.push({indexName:e,query:t,params:r})},a("client.addQueryInBatch()","client.search()")),o.prototype.sendQueriesBatch=s(function(e){return this.search(this._batch,e)},a("client.sendQueriesBatch()","client.search()")),o.prototype.batch=function(t,r){var o=e(8),n="Usage: client.batch(operations[, callback])";if(!o(t))throw new Error(n);return this._jsonRequest({method:"POST",url:"/1/indexes/*/batch",body:{requests:t},hostType:"write",callback:r})},o.prototype.assignUserID=function(e,t){if(!e.userID||!e.cluster)throw new l.AlgoliaSearchError("You have to provide both a userID and cluster",e);return this._jsonRequest({method:"POST",url:"/1/clusters/mapping",hostType:"write",body:{cluster:e.cluster},callback:t,headers:{"x-algolia-user-id":e.userID}})},o.prototype.assignUserIDs=function(e,t){if(!e.userIDs||!e.cluster)throw new l.AlgoliaSearchError("You have to provide both an array of userIDs and cluster",e);return this._jsonRequest({method:"POST",url:"/1/clusters/mapping/batch",hostType:"write",body:{cluster:e.cluster,users:e.userIDs},callback:t})},o.prototype.getTopUserID=function(e){return this._jsonRequest({method:"GET",url:"/1/clusters/mapping/top",hostType:"read",callback:e})},o.prototype.getUserID=function(e,t){if(!e.userID)throw new l.AlgoliaSearchError("You have to provide a userID",{debugData:e});return this._jsonRequest({method:"GET",url:"/1/clusters/mapping/"+e.userID,hostType:"read",callback:t})},o.prototype.listClusters=function(e){return this._jsonRequest({method:"GET",url:"/1/clusters",hostType:"read",callback:e})},o.prototype.listUserIDs=function(e,t){return this._jsonRequest({method:"GET",url:"/1/clusters/mapping",body:e,hostType:"read",callback:t})},o.prototype.removeUserID=function(e,t){if(!e.userID)throw new l.AlgoliaSearchError("You have to provide a userID",{debugData:e});return this._jsonRequest({method:"DELETE",url:"/1/clusters/mapping",hostType:"write",callback:t,headers:{"x-algolia-user-id":e.userID}})},o.prototype.searchUserIDs=function(e,t){return this._jsonRequest({method:"POST",url:"/1/clusters/mapping/search",body:e,hostType:"read",callback:t})},o.prototype.setPersonalizationStrategy=function(e,t){return this._jsonRequest({method:"POST",url:"/1/recommendation/personalization/strategy",body:e,hostType:"write",callback:t})},o.prototype.getPersonalizationStrategy=function(e){return this._jsonRequest({method:"GET",url:"/1/recommendation/personalization/strategy",hostType:"read",callback:e})},o.prototype.destroy=n,o.prototype.enableRateLimitForward=n,o.prototype.disableRateLimitForward=n,o.prototype.useSecuredAPIKey=n,o.prototype.disableSecuredAPIKey=n,o.prototype.generateSecuredApiKey=n,o.prototype.getSecuredApiKeyRemainingValidity=n},{17:17,18:18,27:27,28:28,29:29,30:30,31:31,7:7,8:8}],17:[function(e,t,r){(function(r){function o(t,r,o){var i=e(1)("algoliasearch"),s=e(27),a=e(8),u=e(33),l="Usage: algoliasearch(applicationID, apiKey, opts)";if(o._allowEmptyCredentials!==!0&&!t)throw new c.AlgoliaSearchError("Please provide an application ID. "+l);if(o._allowEmptyCredentials!==!0&&!r)throw new c.AlgoliaSearchError("Please provide an API key. "+l); +this.applicationID=t,this.apiKey=r,this.hosts={read:[],write:[]},o=o||{},this._timeouts=o.timeouts||{connect:1e3,read:2e3,write:3e4},o.timeout&&(this._timeouts.connect=this._timeouts.read=this._timeouts.write=o.timeout);var p=o.protocol||"https:";if(/:$/.test(p)||(p+=":"),"http:"!==p&&"https:"!==p)throw new c.AlgoliaSearchError("protocol must be `http:` or `https:` (was `"+o.protocol+"`)");if(this._checkAppIdData(),o.hosts)a(o.hosts)?(this.hosts.read=s(o.hosts),this.hosts.write=s(o.hosts)):(this.hosts.read=s(o.hosts.read),this.hosts.write=s(o.hosts.write));else{var d=u(this._shuffleResult,function(e){return t+"-"+e+".algolianet.com"}),h=(o.dsn===!1?"":"-dsn")+".algolia.net";this.hosts.read=[this.applicationID+h].concat(d),this.hosts.write=[this.applicationID+".algolia.net"].concat(d)}this.hosts.read=u(this.hosts.read,n(p)),this.hosts.write=u(this.hosts.write,n(p)),this.extraHeaders={},this.cache=o._cache||{},this._ua=o._ua,this._useCache=!(void 0!==o._useCache&&!o._cache)||o._useCache,this._useRequestCache=this._useCache&&o._useRequestCache,this._useFallback=void 0===o.useFallback||o.useFallback,this._setTimeout=o._setTimeout,i("init done, %j",this)}function n(e){return function(t){return e+"//"+t.toLowerCase()}}function i(e){if(void 0===Array.prototype.toJSON)return JSON.stringify(e);var t=Array.prototype.toJSON;delete Array.prototype.toJSON;var r=JSON.stringify(e);return Array.prototype.toJSON=t,r}function s(e){for(var t,r,o=e.length;0!==o;)r=Math.floor(Math.random()*o),o-=1,t=e[o],e[o]=e[r],e[r]=t;return e}function a(e){var t={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){var o;o="x-algolia-api-key"===r||"x-algolia-application-id"===r?"**hidden for security purposes**":e[r],t[r]=o}return t}t.exports=o;var c=e(31),u=e(32),l=e(20),p=e(37),d=500,h=r.env.RESET_APP_DATA_TIMER&&parseInt(r.env.RESET_APP_DATA_TIMER,10)||12e4;o.prototype.initIndex=function(e){return new l(this,e)},o.prototype.setExtraHeader=function(e,t){this.extraHeaders[e.toLowerCase()]=t},o.prototype.getExtraHeader=function(e){return this.extraHeaders[e.toLowerCase()]},o.prototype.unsetExtraHeader=function(e){delete this.extraHeaders[e.toLowerCase()]},o.prototype.addAlgoliaAgent=function(e){var t="; "+e;this._ua.indexOf(t)===-1&&(this._ua+=t)},o.prototype._jsonRequest=function(t){function r(e,n){function u(e){var t=e&&e.body&&e.body.message&&e.body.status||e.statusCode||e&&e.body&&200;h("received response: statusCode: %s, computed statusCode: %d, headers: %j",e.statusCode,t,e.headers);var r=2===Math.floor(t/100),o=new Date;if(w.push({currentHost:A,headers:a(p),content:s||null,contentLength:void 0!==s?s.length:null,method:n.method,timeouts:n.timeouts,url:n.url,startTime:x,endTime:o,duration:o-x,statusCode:t}),r)return m._useCache&&!m._useRequestCache&&y&&(y[l]=e.responseText),{responseText:e.responseText,body:e.body};var i=4!==Math.floor(t/100);if(i)return v+=1,_();h("unrecoverable error");var u=new c.AlgoliaSearchError(e.body&&e.body.message,{debugData:w,statusCode:t});return m._promise.reject(u)}function d(e){h("error: %s, stack: %s",e.message,e.stack);var r=new Date;return w.push({currentHost:A,headers:a(p),content:s||null,contentLength:void 0!==s?s.length:null,method:n.method,timeouts:n.timeouts,url:n.url,startTime:x,endTime:r,duration:r-x}),e instanceof c.AlgoliaSearchError||(e=new c.Unknown(e&&e.message,e)),v+=1,e instanceof c.Unknown||e instanceof c.UnparsableJSON||v>=m.hosts[t.hostType].length&&(g||!b)?(e.debugData=w,m._promise.reject(e)):e instanceof c.RequestTimeout?T():_()}function _(){return h("retrying request"),m._incrementHostIndex(t.hostType),r(e,n)}function T(){return h("retrying request with higher timeout"),m._incrementHostIndex(t.hostType),m._incrementTimeoutMultipler(),n.timeouts=m._getTimeoutsForRequest(t.hostType),r(e,n)}m._checkAppIdData();var x=new Date;if(m._useCache&&!m._useRequestCache&&(l=t.url),m._useCache&&!m._useRequestCache&&s&&(l+="_body_"+n.body),o(!m._useRequestCache,y,l)){h("serving response from cache");var R=y[l];return m._promise.resolve({body:JSON.parse(R),responseText:R})}if(v>=m.hosts[t.hostType].length)return!b||g?(h("could not get any response"),m._promise.reject(new c.AlgoliaSearchError("Cannot connect to the AlgoliaSearch API. Send an email to support@algolia.com to report and resolve the issue. Application id was: "+m.applicationID,{debugData:w}))):(h("switching to fallback"),v=0,n.method=t.fallback.method,n.url=t.fallback.url,n.jsonBody=t.fallback.body,n.jsonBody&&(n.body=i(n.jsonBody)),p=m._computeRequestHeaders({additionalUA:f,headers:t.headers}),n.timeouts=m._getTimeoutsForRequest(t.hostType),m._setHostIndexByType(0,t.hostType),g=!0,r(m._request.fallback,n));var A=m._getHostByType(t.hostType),j=A+n.url,S={body:n.body,jsonBody:n.jsonBody,method:n.method,headers:p,timeouts:n.timeouts,debug:h,forceAuthHeaders:n.forceAuthHeaders};return h("method: %s, url: %s, headers: %j, timeouts: %d",S.method,j,S.headers,S.timeouts),e===m._request.fallback&&h("using fallback"),e.call(m,j,S).then(u,d)}function o(e,t,r){return m._useCache&&e&&t&&void 0!==t[r]}function n(e,r){return o(m._useRequestCache,y,l)&&e["catch"](function(){delete y[l]}),"function"!=typeof t.callback?e.then(r):void e.then(function(e){u(function(){t.callback(null,r(e))},m._setTimeout||setTimeout)},function(e){u(function(){t.callback(e)},m._setTimeout||setTimeout)})}this._checkAppIdData();var s,l,p,h=e(1)("algoliasearch:"+t.url),f=t.additionalUA||"",y=t.cache,m=this,v=0,g=!1,b=m._useFallback&&m._request.fallback&&t.fallback;this.apiKey.length>d&&void 0!==t.body&&(void 0!==t.body.params||void 0!==t.body.requests)?(t.body.apiKey=this.apiKey,p=this._computeRequestHeaders({additionalUA:f,withApiKey:!1,headers:t.headers})):p=this._computeRequestHeaders({additionalUA:f,headers:t.headers}),void 0!==t.body&&(s=i(t.body)),h("request start");var w=[];if(m._useCache&&m._useRequestCache&&(l=t.url),m._useCache&&m._useRequestCache&&s&&(l+="_body_"+s),o(m._useRequestCache,y,l)){h("serving request from cache");var _=y[l],T="function"!=typeof _.then?m._promise.resolve({responseText:_}):_;return n(T,function(e){return JSON.parse(e.responseText)})}var x=r(m._request,{url:t.url,method:t.method,body:s,jsonBody:t.body,timeouts:m._getTimeoutsForRequest(t.hostType),forceAuthHeaders:t.forceAuthHeaders});return m._useCache&&m._useRequestCache&&y&&(y[l]=x),n(x,function(e){return e.body})},o.prototype._getSearchParams=function(e,t){if(void 0===e||null===e)return t;for(var r in e)null!==r&&void 0!==e[r]&&e.hasOwnProperty(r)&&(t+=""===t?"":"&",t+=r+"="+encodeURIComponent("[object Array]"===Object.prototype.toString.call(e[r])?i(e[r]):e[r]));return t},o.prototype._computeRequestHeaders=function(t){var r=e(5),o=t.additionalUA?this._ua+"; "+t.additionalUA:this._ua,n={"x-algolia-agent":o,"x-algolia-application-id":this.applicationID};return t.withApiKey!==!1&&(n["x-algolia-api-key"]=this.apiKey),this.userToken&&(n["x-algolia-usertoken"]=this.userToken),this.securityTags&&(n["x-algolia-tagfilters"]=this.securityTags),r(this.extraHeaders,function(e,t){n[t]=e}),t.headers&&r(t.headers,function(e,t){n[t]=e}),n},o.prototype.search=function(t,r,o){var n=e(8),i=e(33),s="Usage: client.search(arrayOfQueries[, callback])";if(!n(t))throw new Error(s);"function"==typeof r?(o=r,r={}):void 0===r&&(r={});var a=this,c={requests:i(t,function(e){var t="";return void 0!==e.query&&(t+="query="+encodeURIComponent(e.query)),{indexName:e.indexName,params:a._getSearchParams(e.params,t)}})},u=i(c.requests,function(e,t){return t+"="+encodeURIComponent("/1/indexes/"+encodeURIComponent(e.indexName)+"?"+e.params)}).join("&"),l="/1/indexes/*/queries";return void 0!==r.strategy&&(c.strategy=r.strategy),this._jsonRequest({cache:this.cache,method:"POST",url:l,body:c,hostType:"read",fallback:{method:"GET",url:"/1/indexes/*",body:{params:u}},callback:o})},o.prototype.searchForFacetValues=function(t){var r=e(8),o=e(33),n="Usage: client.searchForFacetValues([{indexName, params: {facetName, facetQuery, ...params}}, ...queries])";if(!r(t))throw new Error(n);var i=this;return i._promise.all(o(t,function(t){if(!t||void 0===t.indexName||void 0===t.params.facetName||void 0===t.params.facetQuery)throw new Error(n);var r=e(27),o=e(35),s=t.indexName,a=t.params,c=a.facetName,u=o(r(a),function(e){return"facetName"===e}),l=i._getSearchParams(u,"");return i._jsonRequest({cache:i.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(s)+"/facets/"+encodeURIComponent(c)+"/query",hostType:"read",body:{params:l}})}))},o.prototype.setSecurityTags=function(e){if("[object Array]"===Object.prototype.toString.call(e)){for(var t=[],r=0;rh?this._resetInitialAppIdData(e):e},o.prototype._resetInitialAppIdData=function(e){var t=e||{};return t.hostIndexes={read:0,write:0},t.timeoutMultiplier=1,t.shuffleResult=t.shuffleResult||s([1,2,3]),this._setAppIdData(t)},o.prototype._cacheAppIdData=function(e){this._hostIndexes=e.hostIndexes,this._timeoutMultiplier=e.timeoutMultiplier,this._shuffleResult=e.shuffleResult},o.prototype._partialAppIdDataUpdate=function(t){var r=e(5),o=this._getAppIdData();return r(t,function(e,t){o[t]=e}),this._setAppIdData(o)},o.prototype._getHostByType=function(e){return this.hosts[e][this._getHostIndexByType(e)]},o.prototype._getTimeoutMultiplier=function(){return this._timeoutMultiplier},o.prototype._getHostIndexByType=function(e){return this._hostIndexes[e]},o.prototype._setHostIndexByType=function(t,r){var o=e(27),n=o(this._hostIndexes);return n[r]=t,this._partialAppIdDataUpdate({hostIndexes:n}),t},o.prototype._incrementHostIndex=function(e){return this._setHostIndexByType((this._getHostIndexByType(e)+1)%this.hosts[e].length,e)},o.prototype._incrementTimeoutMultipler=function(){var e=Math.max(this._timeoutMultiplier+1,4);return this._partialAppIdDataUpdate({timeoutMultiplier:e})},o.prototype._getTimeoutsForRequest=function(e){return{connect:this._timeouts.connect*this._timeoutMultiplier,complete:this._timeouts[e]*this._timeoutMultiplier}}}).call(this,e(12))},{1:1,12:12,20:20,27:27,31:31,32:32,33:33,35:35,37:37,5:5,8:8}],18:[function(e,t,r){function o(){s.apply(this,arguments)}function n(e,t,r){function o(r,n){var i={page:r||0,hitsPerPage:t||100},s=n||[];return e(i).then(function(e){var t=e.hits,r=e.nbHits,n=t.map(function(e){return delete e._highlightResult,e}),a=s.concat(n);return a.lengths&&(t=s),"published"!==e.status?l._promise.delay(t).then(r):e})}function o(e){u(function(){t(null,e)},l._setTimeout||setTimeout)}function n(e){u(function(){t(e)},l._setTimeout||setTimeout)}var i=100,s=5e3,a=0,c=this,l=c.as,p=r();return t?void p.then(o,n):p},o.prototype.clearIndex=function(e){var t=this;return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(t.indexName)+"/clear",hostType:"write",callback:e})},o.prototype.getSettings=function(e,t){1===arguments.length&&"function"==typeof e&&(t=e,e={}),e=e||{};var r=encodeURIComponent(this.indexName);return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+r+"/settings?getVersion=2"+(e.advanced?"&advanced="+e.advanced:""),hostType:"read",callback:t})},o.prototype.searchSynonyms=function(e,t){return"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/search",body:e,hostType:"read",callback:t})},o.prototype.exportSynonyms=function(e,t){return n(this.searchSynonyms.bind(this),e,t)},o.prototype.saveSynonym=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e.objectID)+"?forwardToReplicas="+o,body:e,hostType:"write",callback:r})},o.prototype.getSynonym=function(e,t){return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e),hostType:"read",callback:t})},o.prototype.deleteSynonym=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e)+"?forwardToReplicas="+o,hostType:"write",callback:r})},o.prototype.clearSynonyms=function(e,t){"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),void 0!==e.forwardToSlaves&&p();var r=e.forwardToSlaves||e.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/clear?forwardToReplicas="+r,hostType:"write",callback:t})},o.prototype.batchSynonyms=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/batch?forwardToReplicas="+o+"&replaceExistingSynonyms="+(t.replaceExistingSynonyms?"true":"false"),hostType:"write",body:e,callback:r})},o.prototype.searchRules=function(e,t){return"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/search",body:e,hostType:"read",callback:t})},o.prototype.exportRules=function(e,t){return n(this.searchRules.bind(this),e,t)},o.prototype.saveRule=function(e,t,r){if("function"==typeof t?(r=t,t={}):void 0===t&&(t={}),!e.objectID)throw new l.AlgoliaSearchError("Missing or empty objectID field for rule");var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e.objectID)+"?forwardToReplicas="+o,body:e,hostType:"write",callback:r})},o.prototype.getRule=function(e,t){return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e),hostType:"read",callback:t})},o.prototype.deleteRule=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={});var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e)+"?forwardToReplicas="+o,hostType:"write",callback:r})},o.prototype.clearRules=function(e,t){"function"==typeof e?(t=e,e={}):void 0===e&&(e={});var r=e.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/clear?forwardToReplicas="+r,hostType:"write",callback:t})},o.prototype.batchRules=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={});var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/batch?forwardToReplicas="+o+"&clearExistingRules="+(t.clearExistingRules===!0?"true":"false"),hostType:"write",body:e,callback:r})},o.prototype.exists=function(e){var t=this.getSettings().then(function(){return!0})["catch"](function(e){if(e instanceof l.AlgoliaSearchError&&404===e.statusCode)return!1;throw e});return"function"!=typeof e?t:void t.then(function(t){e(null,t)})["catch"](function(t){e(t)})},o.prototype.findObject=function(e,t,r){t=void 0===t?{}:t;var o=void 0===t.paginate||t.paginate,n=void 0!==t.query?t.query:"",i=this,s=0,a=function(){return t.page=s,i.search(n,t).then(function(t){for(var r=t.hits,n=0;n=t.nbPages)throw new l.ObjectNotFound("Object not found");return a()})},c=a(s);return void 0===r?c:void c.then(function(e){r(null,e)})["catch"](function(e){r(e)})},o.prototype.getObjectPosition=function(e,t){for(var r=e.hits,o=0;o1&&a()}if(!h.cors&&!h.hasXDomainRequest)return void o(new u.Network("CORS not supported"));e=l(e,t.headers);var d,f,y=t.body,m=h.cors?new XMLHttpRequest:new XDomainRequest,v=!1;d=setTimeout(s,t.timeouts.connect),m.onprogress=c,"onreadystatechange"in m&&(m.onreadystatechange=p),m.onload=n,m.onerror=i,m instanceof XMLHttpRequest?(m.open(t.method,e,!0),t.forceAuthHeaders&&(m.setRequestHeader("x-algolia-application-id",t.headers["x-algolia-application-id"]),m.setRequestHeader("x-algolia-api-key",t.headers["x-algolia-api-key"]))):m.open(t.method,e),h.cors&&(y&&("POST"===t.method?m.setRequestHeader("content-type","application/x-www-form-urlencoded"):m.setRequestHeader("content-type","application/json")),m.setRequestHeader("accept","application/json")),y?m.send(y):m.send()})},a.prototype._request.fallback=function(e,t){return e=l(e,t.headers),new n(function(r,o){p(e,t,function(e,t){return e?void o(e):void r(t)})})},a.prototype._promise={reject:function(e){return n.reject(e)},resolve:function(e){return n.resolve(e)},delay:function(e){return new n(function(t){setTimeout(t,e)})},all:function(e){return n.all(e)}},s}}).call(this,e(12))},{1:1,12:12,24:24,25:25,27:27,3:3,31:31,36:36,38:38,6:6,7:7}],24:[function(e,t,r){"use strict";function o(e,t){return e+=/\?/.test(e)?"&":"?",e+n(t)}t.exports=o;var n=e(14)},{14:14}],25:[function(e,t,r){"use strict";function o(e,t,r){function o(){t.debug("JSONP: success"),m||d||(m=!0,p||(t.debug("JSONP: Fail. Script loaded but did not call the callback"),a(),r(new n.JSONPScriptFail)))}function s(){"loaded"!==this.readyState&&"complete"!==this.readyState||o()}function a(){clearTimeout(v),f.onload=null,f.onreadystatechange=null,f.onerror=null,h.removeChild(f)}function c(){try{delete window[y],delete window[y+"_loaded"]}catch(e){window[y]=window[y+"_loaded"]=void 0}}function u(){t.debug("JSONP: Script timeout"),d=!0,a(),r(new n.RequestTimeout)}function l(){t.debug("JSONP: Script error"),m||d||(a(),r(new n.JSONPScriptError))}if("GET"!==t.method)return void r(new Error("Method "+t.method+" "+e+" is not supported by JSONP."));t.debug("JSONP: start");var p=!1,d=!1;i+=1;var h=document.getElementsByTagName("head")[0],f=document.createElement("script"),y="algoliaJSONP_"+i,m=!1;window[y]=function(e){return c(),d?void t.debug("JSONP: Late answer, ignoring"):(p=!0,a(),void r(null,{body:e,responseText:JSON.stringify(e)}))},e+="&callback="+y,t.jsonBody&&t.jsonBody.params&&(e+="&"+t.jsonBody.params);var v=setTimeout(u,t.timeouts.complete);f.onreadystatechange=s,f.onload=o,f.onerror=l,f.async=!0,f.defer=!0,f.src=e,h.appendChild(f)}t.exports=o;var n=e(31),i=0},{31:31}],26:[function(e,t,r){function o(e,t){return function(r,o,i){if("function"==typeof r&&"object"==typeof o||"object"==typeof i)throw new n.AlgoliaSearchError("index.search usage is index.search(query, params, cb)");0===arguments.length||"function"==typeof r?(i=r,r=""):1!==arguments.length&&"function"!=typeof o||(i=o,o=void 0),"object"==typeof r&&null!==r?(o=r,r=void 0):void 0!==r&&null!==r||(r="");var s="";void 0!==r&&(s+=e+"="+encodeURIComponent(r));var a;return void 0!==o&&(o.additionalUA&&(a=o.additionalUA,delete o.additionalUA),s=this.as._getSearchParams(o,s)),this._search(s,t,i,a)}}t.exports=o;var n=e(31)},{31:31}],27:[function(e,t,r){t.exports=function(e){return JSON.parse(JSON.stringify(e))}},{}],28:[function(e,t,r){function o(e,t,r){var o={};return r=r||{},r.hosts=r.hosts||["analytics.algolia.com","analytics.algolia.com","analytics.algolia.com","analytics.algolia.com"],r.protocol=r.protocol||"https:",o.as=n(e,t,r),o.getABTests=function(e,t){var r=r||{},o=r.offset||0,n=r.limit||10;return this.as._jsonRequest({method:"GET",url:"/2/abtests?offset="+encodeURIComponent(o)+"&limit="+encodeURIComponent(n),hostType:"read",forceAuthHeaders:!0,callback:t})},o.getABTest=function(e,t){return this.as._jsonRequest({method:"GET",url:"/2/abtests/"+encodeURIComponent(e),hostType:"read",forceAuthHeaders:!0,callback:t})},o.addABTest=function(e,t){return this.as._jsonRequest({method:"POST",url:"/2/abtests",body:e,hostType:"read",forceAuthHeaders:!0,callback:t})},o.stopABTest=function(e,t){return this.as._jsonRequest({method:"POST",url:"/2/abtests/"+encodeURIComponent(e)+"/stop",hostType:"read",forceAuthHeaders:!0,callback:t})},o.deleteABTest=function(e,t){return this.as._jsonRequest({method:"DELETE",url:"/2/abtests/"+encodeURIComponent(e),hostType:"write",forceAuthHeaders:!0,callback:t})},o.waitTask=function(e,t,r){return this.as.initIndex(e).waitTask(t,r)},o}t.exports=o;var n=e(22)},{22:22}],29:[function(e,t,r){t.exports=function(e,t){function r(){return o||(console.warn(t),o=!0),e.apply(this,arguments)}var o=!1;return r}},{}],30:[function(e,t,r){t.exports=function(e,t){var r=e.toLowerCase().replace(/[\.\(\)]/g,"");return"algoliasearch: `"+e+"` was replaced by `"+t+"`. Please see https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated#"+r}},{}],31:[function(e,t,r){"use strict";function o(t,r){var o=e(5),n=this;"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):n.stack=(new Error).stack||"Cannot get a stacktrace, browser is too old",this.name="AlgoliaSearchError",this.message=t||"Unknown error",r&&o(r,function(e,t){n[t]=e})}function n(e,t){function r(){var r=Array.prototype.slice.call(arguments,0);"string"!=typeof r[0]&&r.unshift(t),o.apply(this,r),this.name="AlgoliaSearch"+e+"Error"}return i(r,o),r}var i=e(7);i(o,Error),t.exports={AlgoliaSearchError:o,UnparsableJSON:n("UnparsableJSON","Could not parse the incoming response as JSON, see err.more for details"),RequestTimeout:n("RequestTimeout","Request timed out before getting a response"),Network:n("Network","Network issue, see err.more for details"),JSONPScriptFail:n("JSONPScriptFail",""),window.ALGOLIA_SUPPORTS_DOCWRITE===!0?(document.write(''),n("document.write")()):r(o,n("DOMElement"))}catch(s){r(o,n("DOMElement"))}}function n(e){return function(){var t="AlgoliaSearch: loaded V2 script using "+e;window.console&&window.console.log&&window.console.log(t)}}t.exports=o},{1:1}],4:[function(e,t,r){"use strict";function o(){var e="-- AlgoliaSearch V2 => V3 error --\nYou are trying to use a new version of the AlgoliaSearch JavaScript client with an old notation.\nPlease read our migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x\n-- /AlgoliaSearch V2 => V3 error --";window.AlgoliaSearch=function(){throw new Error(e)},window.AlgoliaSearchHelper=function(){throw new Error(e)},window.AlgoliaExplainResults=function(){throw new Error(e)}}t.exports=o},{}],5:[function(e,t,r){"use strict";function o(t){var r=e(2),o=e(3),n=e(4);r(t)?o(t):n()}o("algoliasearch.jquery")},{2:2,3:3,4:4}]},{},[5])(5)}),function e(t,r,o){function n(s,a){if(!r[s]){if(!t[s]){var c="function"==typeof require&&require;if(!a&&c)return c(s,!0);if(i)return i(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var l=r[s]={exports:{}};t[s][0].call(l.exports,function(e){var r=t[s][1][e];return n(r?r:e)},l,l.exports,e,t,r,o)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;s=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(e){var t=this.useColors;if(e[0]=(t?"%c":"")+this.namespace+(t?" %c":" ")+e[0]+(t?"%c ":" ")+"+"+r.humanize(this.diff),t){var o="color: "+this.color;e.splice(1,0,o,"color: inherit");var n=0,i=0;e[0].replace(/%[a-zA-Z%]/g,function(e){"%%"!==e&&(n++,"%c"===e&&(i=n))}),e.splice(i,0,o)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(e){try{null==e?r.storage.removeItem("debug"):r.storage.debug=e}catch(t){}}function c(){var e;try{e=r.storage.debug}catch(t){}return!e&&"undefined"!=typeof o&&"env"in o&&(e=o.env.DEBUG),e}function u(){try{return window.localStorage}catch(e){}}r=t.exports=e(2),r.log=s,r.formatArgs=i,r.save=a,r.load=c,r.useColors=n,r.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),r.colors=["lightseagreen","forestgreen","goldenrod","dodgerblue","darkorchid","crimson"],r.formatters.j=function(e){try{return JSON.stringify(e)}catch(t){return"[UnexpectedJSONParseError]: "+t.message}},r.enable(c())}).call(this,e(12))},{12:12,2:2}],2:[function(e,t,r){function o(e){var t,o=0;for(t in e)o=(o<<5)-o+e.charCodeAt(t),o|=0;return r.colors[Math.abs(o)%r.colors.length]}function n(e){function t(){if(t.enabled){var e=t,o=+new Date,n=o-(u||o);e.diff=n,e.prev=u,e.curr=o,u=o;for(var i=new Array(arguments.length),s=0;s0&&this._events[e].length>r&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace())),this},o.prototype.on=o.prototype.addListener,o.prototype.once=function(e,t){function r(){this.removeListener(e,r),o||(o=!0,t.apply(this,arguments))}if(!n(t))throw TypeError("listener must be a function");var o=!1;return r.listener=t,this.on(e,r),this},o.prototype.removeListener=function(e,t){var r,o,i,a;if(!n(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(r=this._events[e],i=r.length,o=-1,r===t||n(r.listener)&&r.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(s(r)){for(a=i;a-- >0;)if(r[a]===t||r[a].listener&&r[a].listener===t){o=a;break}if(o<0)return this;1===r.length?(r.length=0,delete this._events[e]):r.splice(o,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},o.prototype.removeAllListeners=function(e){var t,r;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(r=this._events[e],n(r))this.removeListener(e,r);else if(r)for(;r.length;)this.removeListener(e,r[r.length-1]);return delete this._events[e],this},o.prototype.listeners=function(e){var t;return t=this._events&&this._events[e]?n(this._events[e])?[this._events[e]]:this._events[e].slice():[]},o.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(n(t))return 1;if(t)return t.length}return 0},o.listenerCount=function(e,t){return e.listenerCount(t)}},{}],5:[function(e,t,r){var o=Object.prototype.hasOwnProperty,n=Object.prototype.toString;t.exports=function(e,t,r){if("[object Function]"!==n.call(t))throw new TypeError("iterator must be a function");var i=e.length;if(i===+i)for(var s=0;s100)){var t=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(e);if(t){var r=parseFloat(t[1]),o=(t[2]||"ms").toLowerCase();switch(o){case"years":case"year":case"yrs":case"yr":case"y":return r*p;case"days":case"day":case"d":return r*l;case"hours":case"hour":case"hrs":case"hr":case"h":return r*u;case"minutes":case"minute":case"mins":case"min":case"m":return r*c;case"seconds":case"second":case"secs":case"sec":case"s":return r*a;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return r;default:return}}}}function n(e){return e>=l?Math.round(e/l)+"d":e>=u?Math.round(e/u)+"h":e>=c?Math.round(e/c)+"m":e>=a?Math.round(e/a)+"s":e+"ms"}function i(e){return s(e,l,"day")||s(e,u,"hour")||s(e,c,"minute")||s(e,a,"second")||e+" ms"}function s(e,t,r){if(!(e0)return o(e);if("number"===r&&isNaN(e)===!1)return t["long"]?i(e):n(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},{}],10:[function(e,t,r){"use strict";var o=Object.prototype.hasOwnProperty,n=Object.prototype.toString,i=Array.prototype.slice,s=e(11),a=Object.prototype.propertyIsEnumerable,c=!a.call({toString:null},"toString"),u=a.call(function(){},"prototype"),l=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],p=function(e){var t=e.constructor;return t&&t.prototype===e},d={$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},h=function(){if("undefined"==typeof window)return!1;for(var e in window)try{if(!d["$"+e]&&o.call(window,e)&&null!==window[e]&&"object"==typeof window[e])try{p(window[e])}catch(t){return!0}}catch(t){return!0}return!1}(),f=function(e){if("undefined"==typeof window||!h)return p(e);try{return p(e)}catch(t){return!1}},y=function(e){var t=null!==e&&"object"==typeof e,r="[object Function]"===n.call(e),i=s(e),a=t&&"[object String]"===n.call(e),p=[];if(!t&&!r&&!i)throw new TypeError("Object.keys called on a non-object");var d=u&&r;if(a&&e.length>0&&!o.call(e,0))for(var h=0;h0)for(var y=0;y=0&&"[object Function]"===o.call(e.callee)),r}},{}],12:[function(e,t,r){function o(){throw new Error("setTimeout has not been defined")}function n(){throw new Error("clearTimeout has not been defined")}function i(e){if(p===setTimeout)return setTimeout(e,0);if((p===o||!p)&&setTimeout)return p=setTimeout,setTimeout(e,0);try{return p(e,0)}catch(t){try{return p.call(null,e,0)}catch(t){return p.call(this,e,0)}}}function s(e){if(d===clearTimeout)return clearTimeout(e);if((d===n||!d)&&clearTimeout)return d=clearTimeout,clearTimeout(e);try{return d(e)}catch(t){try{return d.call(null,e)}catch(t){return d.call(this,e)}}}function a(){m&&f&&(m=!1,f.length?y=f.concat(y):v=-1,y.length&&c())}function c(){if(!m){var e=i(a);m=!0;for(var t=y.length;t;){for(f=y,y=[];++v1)for(var r=1;r0&&u>c&&(u=c);for(var l=0;l=0?(p=y.substr(0,m),d=y.substr(m+1)):(p=y,d=""),h=decodeURIComponent(p),f=decodeURIComponent(d),o(s,h)?n(s[h])?s[h].push(f):s[h]=[s[h],f]:s[h]=f}return s};var n=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}},{}],14:[function(e,t,r){"use strict";function o(e,t){if(e.map)return e.map(t);for(var r=[],o=0;o0)n.scope=r;else if("undefined"!=typeof r)throw new Error("the scope given to `copyIndex` was not an array with settings, synonyms or rules");return this._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(e)+"/operation",body:n,hostType:"write",callback:i})},o.prototype.getLogs=function(t,r,o){var n=e(27),i={};return"object"==typeof t?(i=n(t),o=r):0===arguments.length||"function"==typeof t?o=t:1===arguments.length||"function"==typeof r?(o=r,i.offset=t):(i.offset=t,i.length=r),void 0===i.offset&&(i.offset=0),void 0===i.length&&(i.length=10),this._jsonRequest({method:"GET",url:"/1/logs?"+this._getSearchParams(i,""),hostType:"read",callback:o})},o.prototype.listIndexes=function(e,t){var r="";return void 0===e||"function"==typeof e?t=e:r="?page="+e,this._jsonRequest({method:"GET",url:"/1/indexes"+r,hostType:"read",callback:t})},o.prototype.initIndex=function(e){return new i(this,e)},o.prototype.initAnalytics=function(t){var r=e(28);return r(this.applicationID,this.apiKey,t)},o.prototype.listUserKeys=s(function(e){return this.listApiKeys(e)},a("client.listUserKeys()","client.listApiKeys()")),o.prototype.listApiKeys=function(e){return this._jsonRequest({method:"GET",url:"/1/keys",hostType:"read",callback:e})},o.prototype.getUserKeyACL=s(function(e,t){return this.getApiKey(e,t)},a("client.getUserKeyACL()","client.getApiKey()")),o.prototype.getApiKey=function(e,t){return this._jsonRequest({method:"GET",url:"/1/keys/"+e,hostType:"read",callback:t})},o.prototype.deleteUserKey=s(function(e,t){return this.deleteApiKey(e,t)},a("client.deleteUserKey()","client.deleteApiKey()")),o.prototype.deleteApiKey=function(e,t){return this._jsonRequest({method:"DELETE",url:"/1/keys/"+e,hostType:"write",callback:t})},o.prototype.restoreApiKey=function(e,t){return this._jsonRequest({method:"POST",url:"/1/keys/"+e+"/restore",hostType:"write",callback:t})},o.prototype.addUserKey=s(function(e,t,r){return this.addApiKey(e,t,r)},a("client.addUserKey()","client.addApiKey()")),o.prototype.addApiKey=function(t,r,o){var n=e(8),i="Usage: client.addApiKey(arrayOfAcls[, params, callback])";if(!n(t))throw new Error(i);1!==arguments.length&&"function"!=typeof r||(o=r,r=null);var s={acl:t};return r&&(s.validity=r.validity,s.maxQueriesPerIPPerHour=r.maxQueriesPerIPPerHour,s.maxHitsPerQuery=r.maxHitsPerQuery,s.indexes=r.indexes,s.description=r.description,r.queryParameters&&(s.queryParameters=this._getSearchParams(r.queryParameters,"")),s.referers=r.referers),this._jsonRequest({method:"POST",url:"/1/keys",body:s,hostType:"write",callback:o})},o.prototype.addUserKeyWithValidity=s(function(e,t,r){return this.addApiKey(e,t,r)},a("client.addUserKeyWithValidity()","client.addApiKey()")),o.prototype.updateUserKey=s(function(e,t,r,o){return this.updateApiKey(e,t,r,o)},a("client.updateUserKey()","client.updateApiKey()")),o.prototype.updateApiKey=function(t,r,o,n){var i=e(8),s="Usage: client.updateApiKey(key, arrayOfAcls[, params, callback])";if(!i(r))throw new Error(s);2!==arguments.length&&"function"!=typeof o||(n=o,o=null);var a={acl:r};return o&&(a.validity=o.validity,a.maxQueriesPerIPPerHour=o.maxQueriesPerIPPerHour,a.maxHitsPerQuery=o.maxHitsPerQuery,a.indexes=o.indexes,a.description=o.description,o.queryParameters&&(a.queryParameters=this._getSearchParams(o.queryParameters,"")),a.referers=o.referers),this._jsonRequest({method:"PUT",url:"/1/keys/"+t,body:a,hostType:"write",callback:n})},o.prototype.startQueriesBatch=s(function(){this._batch=[]},a("client.startQueriesBatch()","client.search()")),o.prototype.addQueryInBatch=s(function(e,t,r){this._batch.push({indexName:e,query:t,params:r})},a("client.addQueryInBatch()","client.search()")),o.prototype.sendQueriesBatch=s(function(e){return this.search(this._batch,e)},a("client.sendQueriesBatch()","client.search()")),o.prototype.batch=function(t,r){var o=e(8),n="Usage: client.batch(operations[, callback])";if(!o(t))throw new Error(n);return this._jsonRequest({method:"POST",url:"/1/indexes/*/batch",body:{requests:t},hostType:"write",callback:r})},o.prototype.assignUserID=function(e,t){if(!e.userID||!e.cluster)throw new l.AlgoliaSearchError("You have to provide both a userID and cluster",e);return this._jsonRequest({method:"POST",url:"/1/clusters/mapping",hostType:"write",body:{cluster:e.cluster},callback:t,headers:{"x-algolia-user-id":e.userID}})},o.prototype.assignUserIDs=function(e,t){if(!e.userIDs||!e.cluster)throw new l.AlgoliaSearchError("You have to provide both an array of userIDs and cluster",e);return this._jsonRequest({method:"POST",url:"/1/clusters/mapping/batch",hostType:"write",body:{cluster:e.cluster,users:e.userIDs},callback:t})},o.prototype.getTopUserID=function(e){return this._jsonRequest({method:"GET",url:"/1/clusters/mapping/top",hostType:"read",callback:e})},o.prototype.getUserID=function(e,t){if(!e.userID)throw new l.AlgoliaSearchError("You have to provide a userID",{debugData:e});return this._jsonRequest({method:"GET",url:"/1/clusters/mapping/"+e.userID,hostType:"read",callback:t})},o.prototype.listClusters=function(e){return this._jsonRequest({method:"GET",url:"/1/clusters",hostType:"read",callback:e})},o.prototype.listUserIDs=function(e,t){return this._jsonRequest({method:"GET",url:"/1/clusters/mapping",body:e,hostType:"read",callback:t})},o.prototype.removeUserID=function(e,t){if(!e.userID)throw new l.AlgoliaSearchError("You have to provide a userID",{debugData:e});return this._jsonRequest({method:"DELETE",url:"/1/clusters/mapping",hostType:"write",callback:t,headers:{"x-algolia-user-id":e.userID}})},o.prototype.searchUserIDs=function(e,t){return this._jsonRequest({method:"POST",url:"/1/clusters/mapping/search",body:e,hostType:"read",callback:t})},o.prototype.setPersonalizationStrategy=function(e,t){return this._jsonRequest({method:"POST",url:"/1/recommendation/personalization/strategy",body:e,hostType:"write",callback:t})},o.prototype.getPersonalizationStrategy=function(e){return this._jsonRequest({method:"GET",url:"/1/recommendation/personalization/strategy",hostType:"read",callback:e})},o.prototype.destroy=n,o.prototype.enableRateLimitForward=n,o.prototype.disableRateLimitForward=n,o.prototype.useSecuredAPIKey=n,o.prototype.disableSecuredAPIKey=n,o.prototype.generateSecuredApiKey=n,o.prototype.getSecuredApiKeyRemainingValidity=n},{17:17,18:18,27:27,28:28,29:29,30:30,31:31,7:7,8:8}],17:[function(e,t,r){(function(r){function o(t,r,o){var i=e(1)("algoliasearch"),s=e(27),a=e(8),u=e(33),l="Usage: algoliasearch(applicationID, apiKey, opts)";if(o._allowEmptyCredentials!==!0&&!t)throw new c.AlgoliaSearchError("Please provide an application ID. "+l);if(o._allowEmptyCredentials!==!0&&!r)throw new c.AlgoliaSearchError("Please provide an API key. "+l); +this.applicationID=t,this.apiKey=r,this.hosts={read:[],write:[]},o=o||{},this._timeouts=o.timeouts||{connect:1e3,read:2e3,write:3e4},o.timeout&&(this._timeouts.connect=this._timeouts.read=this._timeouts.write=o.timeout);var p=o.protocol||"https:";if(/:$/.test(p)||(p+=":"),"http:"!==p&&"https:"!==p)throw new c.AlgoliaSearchError("protocol must be `http:` or `https:` (was `"+o.protocol+"`)");if(this._checkAppIdData(),o.hosts)a(o.hosts)?(this.hosts.read=s(o.hosts),this.hosts.write=s(o.hosts)):(this.hosts.read=s(o.hosts.read),this.hosts.write=s(o.hosts.write));else{var d=u(this._shuffleResult,function(e){return t+"-"+e+".algolianet.com"}),h=(o.dsn===!1?"":"-dsn")+".algolia.net";this.hosts.read=[this.applicationID+h].concat(d),this.hosts.write=[this.applicationID+".algolia.net"].concat(d)}this.hosts.read=u(this.hosts.read,n(p)),this.hosts.write=u(this.hosts.write,n(p)),this.extraHeaders={},this.cache=o._cache||{},this._ua=o._ua,this._useCache=!(void 0!==o._useCache&&!o._cache)||o._useCache,this._useRequestCache=this._useCache&&o._useRequestCache,this._useFallback=void 0===o.useFallback||o.useFallback,this._setTimeout=o._setTimeout,i("init done, %j",this)}function n(e){return function(t){return e+"//"+t.toLowerCase()}}function i(e){if(void 0===Array.prototype.toJSON)return JSON.stringify(e);var t=Array.prototype.toJSON;delete Array.prototype.toJSON;var r=JSON.stringify(e);return Array.prototype.toJSON=t,r}function s(e){for(var t,r,o=e.length;0!==o;)r=Math.floor(Math.random()*o),o-=1,t=e[o],e[o]=e[r],e[r]=t;return e}function a(e){var t={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){var o;o="x-algolia-api-key"===r||"x-algolia-application-id"===r?"**hidden for security purposes**":e[r],t[r]=o}return t}t.exports=o;var c=e(31),u=e(32),l=e(20),p=e(37),d=500,h=r.env.RESET_APP_DATA_TIMER&&parseInt(r.env.RESET_APP_DATA_TIMER,10)||12e4;o.prototype.initIndex=function(e){return new l(this,e)},o.prototype.setExtraHeader=function(e,t){this.extraHeaders[e.toLowerCase()]=t},o.prototype.getExtraHeader=function(e){return this.extraHeaders[e.toLowerCase()]},o.prototype.unsetExtraHeader=function(e){delete this.extraHeaders[e.toLowerCase()]},o.prototype.addAlgoliaAgent=function(e){var t="; "+e;this._ua.indexOf(t)===-1&&(this._ua+=t)},o.prototype._jsonRequest=function(t){function r(e,n){function u(e){var t=e&&e.body&&e.body.message&&e.body.status||e.statusCode||e&&e.body&&200;h("received response: statusCode: %s, computed statusCode: %d, headers: %j",e.statusCode,t,e.headers);var r=2===Math.floor(t/100),o=new Date;if(w.push({currentHost:R,headers:a(p),content:s||null,contentLength:void 0!==s?s.length:null,method:n.method,timeouts:n.timeouts,url:n.url,startTime:x,endTime:o,duration:o-x,statusCode:t}),r)return m._useCache&&!m._useRequestCache&&y&&(y[l]=e.responseText),{responseText:e.responseText,body:e.body};var i=4!==Math.floor(t/100);if(i)return v+=1,_();h("unrecoverable error");var u=new c.AlgoliaSearchError(e.body&&e.body.message,{debugData:w,statusCode:t});return m._promise.reject(u)}function d(e){h("error: %s, stack: %s",e.message,e.stack);var r=new Date;return w.push({currentHost:R,headers:a(p),content:s||null,contentLength:void 0!==s?s.length:null,method:n.method,timeouts:n.timeouts,url:n.url,startTime:x,endTime:r,duration:r-x}),e instanceof c.AlgoliaSearchError||(e=new c.Unknown(e&&e.message,e)),v+=1,e instanceof c.Unknown||e instanceof c.UnparsableJSON||v>=m.hosts[t.hostType].length&&(g||!b)?(e.debugData=w,m._promise.reject(e)):e instanceof c.RequestTimeout?T():_()}function _(){return h("retrying request"),m._incrementHostIndex(t.hostType),r(e,n)}function T(){return h("retrying request with higher timeout"),m._incrementHostIndex(t.hostType),m._incrementTimeoutMultipler(),n.timeouts=m._getTimeoutsForRequest(t.hostType),r(e,n)}m._checkAppIdData();var x=new Date;if(m._useCache&&!m._useRequestCache&&(l=t.url),m._useCache&&!m._useRequestCache&&s&&(l+="_body_"+n.body),o(!m._useRequestCache,y,l)){h("serving response from cache");var j=y[l];return m._promise.resolve({body:JSON.parse(j),responseText:j})}if(v>=m.hosts[t.hostType].length)return!b||g?(h("could not get any response"),m._promise.reject(new c.AlgoliaSearchError("Cannot connect to the AlgoliaSearch API. Send an email to support@algolia.com to report and resolve the issue. Application id was: "+m.applicationID,{debugData:w}))):(h("switching to fallback"),v=0,n.method=t.fallback.method,n.url=t.fallback.url,n.jsonBody=t.fallback.body,n.jsonBody&&(n.body=i(n.jsonBody)),p=m._computeRequestHeaders({additionalUA:f,headers:t.headers}),n.timeouts=m._getTimeoutsForRequest(t.hostType),m._setHostIndexByType(0,t.hostType),g=!0,r(m._request.fallback,n));var R=m._getHostByType(t.hostType),A=R+n.url,S={body:n.body,jsonBody:n.jsonBody,method:n.method,headers:p,timeouts:n.timeouts,debug:h,forceAuthHeaders:n.forceAuthHeaders};return h("method: %s, url: %s, headers: %j, timeouts: %d",S.method,A,S.headers,S.timeouts),e===m._request.fallback&&h("using fallback"),e.call(m,A,S).then(u,d)}function o(e,t,r){return m._useCache&&e&&t&&void 0!==t[r]}function n(e,r){return o(m._useRequestCache,y,l)&&e["catch"](function(){delete y[l]}),"function"!=typeof t.callback?e.then(r):void e.then(function(e){u(function(){t.callback(null,r(e))},m._setTimeout||setTimeout)},function(e){u(function(){t.callback(e)},m._setTimeout||setTimeout)})}this._checkAppIdData();var s,l,p,h=e(1)("algoliasearch:"+t.url),f=t.additionalUA||"",y=t.cache,m=this,v=0,g=!1,b=m._useFallback&&m._request.fallback&&t.fallback;this.apiKey.length>d&&void 0!==t.body&&(void 0!==t.body.params||void 0!==t.body.requests)?(t.body.apiKey=this.apiKey,p=this._computeRequestHeaders({additionalUA:f,withApiKey:!1,headers:t.headers})):p=this._computeRequestHeaders({additionalUA:f,headers:t.headers}),void 0!==t.body&&(s=i(t.body)),h("request start");var w=[];if(m._useCache&&m._useRequestCache&&(l=t.url),m._useCache&&m._useRequestCache&&s&&(l+="_body_"+s),o(m._useRequestCache,y,l)){h("serving request from cache");var _=y[l],T="function"!=typeof _.then?m._promise.resolve({responseText:_}):_;return n(T,function(e){return JSON.parse(e.responseText)})}var x=r(m._request,{url:t.url,method:t.method,body:s,jsonBody:t.body,timeouts:m._getTimeoutsForRequest(t.hostType),forceAuthHeaders:t.forceAuthHeaders});return m._useCache&&m._useRequestCache&&y&&(y[l]=x),n(x,function(e){return e.body})},o.prototype._getSearchParams=function(e,t){if(void 0===e||null===e)return t;for(var r in e)null!==r&&void 0!==e[r]&&e.hasOwnProperty(r)&&(t+=""===t?"":"&",t+=r+"="+encodeURIComponent("[object Array]"===Object.prototype.toString.call(e[r])?i(e[r]):e[r]));return t},o.prototype._computeRequestHeaders=function(t){var r=e(5),o=t.additionalUA?this._ua+"; "+t.additionalUA:this._ua,n={"x-algolia-agent":o,"x-algolia-application-id":this.applicationID};return t.withApiKey!==!1&&(n["x-algolia-api-key"]=this.apiKey),this.userToken&&(n["x-algolia-usertoken"]=this.userToken),this.securityTags&&(n["x-algolia-tagfilters"]=this.securityTags),r(this.extraHeaders,function(e,t){n[t]=e}),t.headers&&r(t.headers,function(e,t){n[t]=e}),n},o.prototype.search=function(t,r,o){var n=e(8),i=e(33),s="Usage: client.search(arrayOfQueries[, callback])";if(!n(t))throw new Error(s);"function"==typeof r?(o=r,r={}):void 0===r&&(r={});var a=this,c={requests:i(t,function(e){var t="";return void 0!==e.query&&(t+="query="+encodeURIComponent(e.query)),{indexName:e.indexName,params:a._getSearchParams(e.params,t)}})},u=i(c.requests,function(e,t){return t+"="+encodeURIComponent("/1/indexes/"+encodeURIComponent(e.indexName)+"?"+e.params)}).join("&"),l="/1/indexes/*/queries";return void 0!==r.strategy&&(c.strategy=r.strategy),this._jsonRequest({cache:this.cache,method:"POST",url:l,body:c,hostType:"read",fallback:{method:"GET",url:"/1/indexes/*",body:{params:u}},callback:o})},o.prototype.searchForFacetValues=function(t){var r=e(8),o=e(33),n="Usage: client.searchForFacetValues([{indexName, params: {facetName, facetQuery, ...params}}, ...queries])";if(!r(t))throw new Error(n);var i=this;return i._promise.all(o(t,function(t){if(!t||void 0===t.indexName||void 0===t.params.facetName||void 0===t.params.facetQuery)throw new Error(n);var r=e(27),o=e(35),s=t.indexName,a=t.params,c=a.facetName,u=o(r(a),function(e){return"facetName"===e}),l=i._getSearchParams(u,"");return i._jsonRequest({cache:i.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(s)+"/facets/"+encodeURIComponent(c)+"/query",hostType:"read",body:{params:l}})}))},o.prototype.setSecurityTags=function(e){if("[object Array]"===Object.prototype.toString.call(e)){for(var t=[],r=0;rh?this._resetInitialAppIdData(e):e},o.prototype._resetInitialAppIdData=function(e){var t=e||{};return t.hostIndexes={read:0,write:0},t.timeoutMultiplier=1,t.shuffleResult=t.shuffleResult||s([1,2,3]),this._setAppIdData(t)},o.prototype._cacheAppIdData=function(e){this._hostIndexes=e.hostIndexes,this._timeoutMultiplier=e.timeoutMultiplier,this._shuffleResult=e.shuffleResult},o.prototype._partialAppIdDataUpdate=function(t){var r=e(5),o=this._getAppIdData();return r(t,function(e,t){o[t]=e}),this._setAppIdData(o)},o.prototype._getHostByType=function(e){return this.hosts[e][this._getHostIndexByType(e)]},o.prototype._getTimeoutMultiplier=function(){return this._timeoutMultiplier},o.prototype._getHostIndexByType=function(e){return this._hostIndexes[e]},o.prototype._setHostIndexByType=function(t,r){var o=e(27),n=o(this._hostIndexes);return n[r]=t,this._partialAppIdDataUpdate({hostIndexes:n}),t},o.prototype._incrementHostIndex=function(e){return this._setHostIndexByType((this._getHostIndexByType(e)+1)%this.hosts[e].length,e)},o.prototype._incrementTimeoutMultipler=function(){var e=Math.max(this._timeoutMultiplier+1,4);return this._partialAppIdDataUpdate({timeoutMultiplier:e})},o.prototype._getTimeoutsForRequest=function(e){return{connect:this._timeouts.connect*this._timeoutMultiplier,complete:this._timeouts[e]*this._timeoutMultiplier}}}).call(this,e(12))},{1:1,12:12,20:20,27:27,31:31,32:32,33:33,35:35,37:37,5:5,8:8}],18:[function(e,t,r){function o(){s.apply(this,arguments)}function n(e,t,r){function o(r,n){var i={page:r||0,hitsPerPage:t||100},s=n||[];return e(i).then(function(e){var t=e.hits,r=e.nbHits,n=t.map(function(e){return delete e._highlightResult,e}),a=s.concat(n);return a.lengths&&(t=s),"published"!==e.status?l._promise.delay(t).then(r):e})}function o(e){u(function(){t(null,e)},l._setTimeout||setTimeout)}function n(e){u(function(){t(e)},l._setTimeout||setTimeout)}var i=100,s=5e3,a=0,c=this,l=c.as,p=r();return t?void p.then(o,n):p},o.prototype.clearIndex=function(e){var t=this;return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(t.indexName)+"/clear",hostType:"write",callback:e})},o.prototype.getSettings=function(e,t){1===arguments.length&&"function"==typeof e&&(t=e,e={}),e=e||{};var r=encodeURIComponent(this.indexName);return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+r+"/settings?getVersion=2"+(e.advanced?"&advanced="+e.advanced:""),hostType:"read",callback:t})},o.prototype.searchSynonyms=function(e,t){return"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/search",body:e,hostType:"read",callback:t})},o.prototype.exportSynonyms=function(e,t){return n(this.searchSynonyms.bind(this),e,t)},o.prototype.saveSynonym=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e.objectID)+"?forwardToReplicas="+o,body:e,hostType:"write",callback:r})},o.prototype.getSynonym=function(e,t){return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e),hostType:"read",callback:t})},o.prototype.deleteSynonym=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e)+"?forwardToReplicas="+o,hostType:"write",callback:r})},o.prototype.clearSynonyms=function(e,t){"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),void 0!==e.forwardToSlaves&&p();var r=e.forwardToSlaves||e.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/clear?forwardToReplicas="+r,hostType:"write",callback:t})},o.prototype.batchSynonyms=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/batch?forwardToReplicas="+o+"&replaceExistingSynonyms="+(t.replaceExistingSynonyms?"true":"false"),hostType:"write",body:e,callback:r})},o.prototype.searchRules=function(e,t){return"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/search",body:e,hostType:"read",callback:t})},o.prototype.exportRules=function(e,t){return n(this.searchRules.bind(this),e,t)},o.prototype.saveRule=function(e,t,r){if("function"==typeof t?(r=t,t={}):void 0===t&&(t={}),!e.objectID)throw new l.AlgoliaSearchError("Missing or empty objectID field for rule");var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e.objectID)+"?forwardToReplicas="+o,body:e,hostType:"write",callback:r})},o.prototype.getRule=function(e,t){return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e),hostType:"read",callback:t})},o.prototype.deleteRule=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={});var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e)+"?forwardToReplicas="+o,hostType:"write",callback:r})},o.prototype.clearRules=function(e,t){"function"==typeof e?(t=e,e={}):void 0===e&&(e={});var r=e.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/clear?forwardToReplicas="+r,hostType:"write",callback:t})},o.prototype.batchRules=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={});var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/batch?forwardToReplicas="+o+"&clearExistingRules="+(t.clearExistingRules===!0?"true":"false"),hostType:"write",body:e,callback:r})},o.prototype.exists=function(e){var t=this.getSettings().then(function(){return!0})["catch"](function(e){if(e instanceof l.AlgoliaSearchError&&404===e.statusCode)return!1;throw e});return"function"!=typeof e?t:void t.then(function(t){e(null,t)})["catch"](function(t){e(t)})},o.prototype.findObject=function(e,t,r){t=void 0===t?{}:t;var o=void 0===t.paginate||t.paginate,n=void 0!==t.query?t.query:"",i=this,s=0,a=function(){return t.page=s,i.search(n,t).then(function(t){for(var r=t.hits,n=0;n=t.nbPages)throw new l.ObjectNotFound("Object not found");return a()})},c=a(s);return void 0===r?c:void c.then(function(e){r(null,e)})["catch"](function(e){r(e)})},o.prototype.getObjectPosition=function(e,t){for(var r=e.hits,o=0;o1&&a()}if(!h.cors&&!h.hasXDomainRequest)return void o(new u.Network("CORS not supported")); +e=l(e,t.headers);var d,f,y=t.body,m=h.cors?new XMLHttpRequest:new XDomainRequest,v=!1;d=setTimeout(s,t.timeouts.connect),m.onprogress=c,"onreadystatechange"in m&&(m.onreadystatechange=p),m.onload=n,m.onerror=i,m instanceof XMLHttpRequest?(m.open(t.method,e,!0),t.forceAuthHeaders&&(m.setRequestHeader("x-algolia-application-id",t.headers["x-algolia-application-id"]),m.setRequestHeader("x-algolia-api-key",t.headers["x-algolia-api-key"]))):m.open(t.method,e),h.cors&&(y&&("POST"===t.method?m.setRequestHeader("content-type","application/x-www-form-urlencoded"):m.setRequestHeader("content-type","application/json")),m.setRequestHeader("accept","application/json")),y?m.send(y):m.send()})},a.prototype._request.fallback=function(e,t){return e=l(e,t.headers),new n(function(r,o){p(e,t,function(e,t){return e?void o(e):void r(t)})})},a.prototype._promise={reject:function(e){return n.reject(e)},resolve:function(e){return n.resolve(e)},delay:function(e){return new n(function(t){setTimeout(t,e)})},all:function(e){return n.all(e)}},s}}).call(this,e(12))},{1:1,12:12,24:24,25:25,27:27,3:3,31:31,36:36,38:38,6:6,7:7}],24:[function(e,t,r){"use strict";function o(e,t){return e+=/\?/.test(e)?"&":"?",e+n(t)}t.exports=o;var n=e(14)},{14:14}],25:[function(e,t,r){"use strict";function o(e,t,r){function o(){t.debug("JSONP: success"),m||d||(m=!0,p||(t.debug("JSONP: Fail. Script loaded but did not call the callback"),a(),r(new n.JSONPScriptFail)))}function s(){"loaded"!==this.readyState&&"complete"!==this.readyState||o()}function a(){clearTimeout(v),f.onload=null,f.onreadystatechange=null,f.onerror=null,h.removeChild(f)}function c(){try{delete window[y],delete window[y+"_loaded"]}catch(e){window[y]=window[y+"_loaded"]=void 0}}function u(){t.debug("JSONP: Script timeout"),d=!0,a(),r(new n.RequestTimeout)}function l(){t.debug("JSONP: Script error"),m||d||(a(),r(new n.JSONPScriptError))}if("GET"!==t.method)return void r(new Error("Method "+t.method+" "+e+" is not supported by JSONP."));t.debug("JSONP: start");var p=!1,d=!1;i+=1;var h=document.getElementsByTagName("head")[0],f=document.createElement("script"),y="algoliaJSONP_"+i,m=!1;window[y]=function(e){return c(),d?void t.debug("JSONP: Late answer, ignoring"):(p=!0,a(),void r(null,{body:e,responseText:JSON.stringify(e)}))},e+="&callback="+y,t.jsonBody&&t.jsonBody.params&&(e+="&"+t.jsonBody.params);var v=setTimeout(u,t.timeouts.complete);f.onreadystatechange=s,f.onload=o,f.onerror=l,f.async=!0,f.defer=!0,f.src=e,h.appendChild(f)}t.exports=o;var n=e(31),i=0},{31:31}],26:[function(e,t,r){function o(e,t){return function(r,o,i){if("function"==typeof r&&"object"==typeof o||"object"==typeof i)throw new n.AlgoliaSearchError("index.search usage is index.search(query, params, cb)");0===arguments.length||"function"==typeof r?(i=r,r=""):1!==arguments.length&&"function"!=typeof o||(i=o,o=void 0),"object"==typeof r&&null!==r?(o=r,r=void 0):void 0!==r&&null!==r||(r="");var s="";void 0!==r&&(s+=e+"="+encodeURIComponent(r));var a;return void 0!==o&&(o.additionalUA&&(a=o.additionalUA,delete o.additionalUA),s=this.as._getSearchParams(o,s)),this._search(s,t,i,a)}}t.exports=o;var n=e(31)},{31:31}],27:[function(e,t,r){t.exports=function(e){return JSON.parse(JSON.stringify(e))}},{}],28:[function(e,t,r){function o(e,t,r){var o={};return r=r||{},r.hosts=r.hosts||["analytics.algolia.com","analytics.algolia.com","analytics.algolia.com","analytics.algolia.com"],r.protocol=r.protocol||"https:",o.as=n(e,t,r),o.getABTests=function(e,t){var r=r||{},o=r.offset||0,n=r.limit||10;return this.as._jsonRequest({method:"GET",url:"/2/abtests?offset="+encodeURIComponent(o)+"&limit="+encodeURIComponent(n),hostType:"read",forceAuthHeaders:!0,callback:t})},o.getABTest=function(e,t){return this.as._jsonRequest({method:"GET",url:"/2/abtests/"+encodeURIComponent(e),hostType:"read",forceAuthHeaders:!0,callback:t})},o.addABTest=function(e,t){return this.as._jsonRequest({method:"POST",url:"/2/abtests",body:e,hostType:"read",forceAuthHeaders:!0,callback:t})},o.stopABTest=function(e,t){return this.as._jsonRequest({method:"POST",url:"/2/abtests/"+encodeURIComponent(e)+"/stop",hostType:"read",forceAuthHeaders:!0,callback:t})},o.deleteABTest=function(e,t){return this.as._jsonRequest({method:"DELETE",url:"/2/abtests/"+encodeURIComponent(e),hostType:"write",forceAuthHeaders:!0,callback:t})},o.waitTask=function(e,t,r){return this.as.initIndex(e).waitTask(t,r)},o}t.exports=o;var n=e(22)},{22:22}],29:[function(e,t,r){t.exports=function(e,t){function r(){return o||(console.warn(t),o=!0),e.apply(this,arguments)}var o=!1;return r}},{}],30:[function(e,t,r){t.exports=function(e,t){var r=e.toLowerCase().replace(/[\.\(\)]/g,"");return"algoliasearch: `"+e+"` was replaced by `"+t+"`. Please see https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated#"+r}},{}],31:[function(e,t,r){"use strict";function o(t,r){var o=e(5),n=this;"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):n.stack=(new Error).stack||"Cannot get a stacktrace, browser is too old",this.name="AlgoliaSearchError",this.message=t||"Unknown error",r&&o(r,function(e,t){n[t]=e})}function n(e,t){function r(){var r=Array.prototype.slice.call(arguments,0);"string"!=typeof r[0]&&r.unshift(t),o.apply(this,r),this.name="AlgoliaSearch"+e+"Error"}return i(r,o),r}var i=e(7);i(o,Error),t.exports={AlgoliaSearchError:o,UnparsableJSON:n("UnparsableJSON","Could not parse the incoming response as JSON, see err.more for details"),RequestTimeout:n("RequestTimeout","Request timed out before getting a response"),Network:n("Network","Network issue, see err.more for details"),JSONPScriptFail:n("JSONPScriptFail",""),window.ALGOLIA_SUPPORTS_DOCWRITE===!0?(document.write(''),n("document.write")()):r(o,n("DOMElement"))}catch(s){r(o,n("DOMElement"))}}function n(e){return function(){var t="AlgoliaSearch: loaded V2 script using "+e;window.console&&window.console.log&&window.console.log(t)}}t.exports=o},{1:1}],4:[function(e,t,r){"use strict";function o(){var e="-- AlgoliaSearch V2 => V3 error --\nYou are trying to use a new version of the AlgoliaSearch JavaScript client with an old notation.\nPlease read our migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x\n-- /AlgoliaSearch V2 => V3 error --";window.AlgoliaSearch=function(){throw new Error(e)},window.AlgoliaSearchHelper=function(){throw new Error(e)},window.AlgoliaExplainResults=function(){throw new Error(e)}}t.exports=o},{}],5:[function(e,t,r){"use strict";function o(t){var r=e(2),o=e(3),n=e(4);r(t)?o(t):n()}o("algoliasearch")},{2:2,3:3,4:4}]},{},[5])(5)}),function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.algoliasearch=e()}}(function(){var e;return function t(e,r,o){function n(s,a){if(!r[s]){if(!e[s]){var c="function"==typeof require&&require;if(!a&&c)return c(s,!0);if(i)return i(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var l=r[s]={exports:{}};e[s][0].call(l.exports,function(t){var r=e[s][1][t];return n(r?r:t)},l,l.exports,t,e,r,o)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;s=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(e){var t=this.useColors;if(e[0]=(t?"%c":"")+this.namespace+(t?" %c":" ")+e[0]+(t?"%c ":" ")+"+"+r.humanize(this.diff),t){var o="color: "+this.color;e.splice(1,0,o,"color: inherit");var n=0,i=0;e[0].replace(/%[a-zA-Z%]/g,function(e){"%%"!==e&&(n++,"%c"===e&&(i=n))}),e.splice(i,0,o)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(e){try{null==e?r.storage.removeItem("debug"):r.storage.debug=e}catch(t){}}function c(){var e;try{e=r.storage.debug}catch(t){}return!e&&"undefined"!=typeof o&&"env"in o&&(e=o.env.DEBUG),e}function u(){try{return window.localStorage}catch(e){}}r=t.exports=e(2),r.log=s,r.formatArgs=i,r.save=a,r.load=c,r.useColors=n,r.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),r.colors=["lightseagreen","forestgreen","goldenrod","dodgerblue","darkorchid","crimson"],r.formatters.j=function(e){try{return JSON.stringify(e)}catch(t){return"[UnexpectedJSONParseError]: "+t.message}},r.enable(c())}).call(this,e(12))},{12:12,2:2}],2:[function(e,t,r){function o(e){var t,o=0;for(t in e)o=(o<<5)-o+e.charCodeAt(t),o|=0;return r.colors[Math.abs(o)%r.colors.length]}function n(e){function t(){if(t.enabled){var e=t,o=+new Date,n=o-(u||o);e.diff=n,e.prev=u,e.curr=o,u=o;for(var i=new Array(arguments.length),s=0;s0&&this._events[e].length>r&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace())),this},o.prototype.on=o.prototype.addListener,o.prototype.once=function(e,t){function r(){this.removeListener(e,r),o||(o=!0,t.apply(this,arguments))}if(!n(t))throw TypeError("listener must be a function");var o=!1;return r.listener=t,this.on(e,r),this},o.prototype.removeListener=function(e,t){var r,o,i,a;if(!n(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(r=this._events[e],i=r.length,o=-1,r===t||n(r.listener)&&r.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(s(r)){for(a=i;a-- >0;)if(r[a]===t||r[a].listener&&r[a].listener===t){o=a;break}if(o<0)return this;1===r.length?(r.length=0,delete this._events[e]):r.splice(o,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},o.prototype.removeAllListeners=function(e){var t,r;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(r=this._events[e],n(r))this.removeListener(e,r);else if(r)for(;r.length;)this.removeListener(e,r[r.length-1]);return delete this._events[e],this},o.prototype.listeners=function(e){var t;return t=this._events&&this._events[e]?n(this._events[e])?[this._events[e]]:this._events[e].slice():[]},o.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(n(t))return 1;if(t)return t.length}return 0},o.listenerCount=function(e,t){return e.listenerCount(t)}},{}],5:[function(e,t,r){var o=Object.prototype.hasOwnProperty,n=Object.prototype.toString;t.exports=function(e,t,r){if("[object Function]"!==n.call(t))throw new TypeError("iterator must be a function");var i=e.length;if(i===+i)for(var s=0;s100)){var t=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(e);if(t){var r=parseFloat(t[1]),o=(t[2]||"ms").toLowerCase();switch(o){case"years":case"year":case"yrs":case"yr":case"y":return r*p;case"days":case"day":case"d":return r*l;case"hours":case"hour":case"hrs":case"hr":case"h":return r*u;case"minutes":case"minute":case"mins":case"min":case"m":return r*c;case"seconds":case"second":case"secs":case"sec":case"s":return r*a;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return r;default:return}}}}function n(e){return e>=l?Math.round(e/l)+"d":e>=u?Math.round(e/u)+"h":e>=c?Math.round(e/c)+"m":e>=a?Math.round(e/a)+"s":e+"ms"}function i(e){return s(e,l,"day")||s(e,u,"hour")||s(e,c,"minute")||s(e,a,"second")||e+" ms"}function s(e,t,r){if(!(e0)return o(e);if("number"===r&&isNaN(e)===!1)return t["long"]?i(e):n(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},{}],10:[function(e,t,r){"use strict";var o=Object.prototype.hasOwnProperty,n=Object.prototype.toString,i=Array.prototype.slice,s=e(11),a=Object.prototype.propertyIsEnumerable,c=!a.call({toString:null},"toString"),u=a.call(function(){},"prototype"),l=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],p=function(e){var t=e.constructor;return t&&t.prototype===e},d={$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},h=function(){if("undefined"==typeof window)return!1;for(var e in window)try{if(!d["$"+e]&&o.call(window,e)&&null!==window[e]&&"object"==typeof window[e])try{p(window[e])}catch(t){return!0}}catch(t){return!0}return!1}(),f=function(e){if("undefined"==typeof window||!h)return p(e);try{return p(e)}catch(t){return!1}},y=function(e){var t=null!==e&&"object"==typeof e,r="[object Function]"===n.call(e),i=s(e),a=t&&"[object String]"===n.call(e),p=[];if(!t&&!r&&!i)throw new TypeError("Object.keys called on a non-object");var d=u&&r;if(a&&e.length>0&&!o.call(e,0))for(var h=0;h0)for(var y=0;y=0&&"[object Function]"===o.call(e.callee)),r}},{}],12:[function(e,t,r){function o(){throw new Error("setTimeout has not been defined")}function n(){throw new Error("clearTimeout has not been defined")}function i(e){if(p===setTimeout)return setTimeout(e,0);if((p===o||!p)&&setTimeout)return p=setTimeout,setTimeout(e,0);try{return p(e,0)}catch(t){try{return p.call(null,e,0)}catch(t){return p.call(this,e,0)}}}function s(e){if(d===clearTimeout)return clearTimeout(e);if((d===n||!d)&&clearTimeout)return d=clearTimeout,clearTimeout(e);try{return d(e)}catch(t){try{return d.call(null,e)}catch(t){return d.call(this,e)}}}function a(){m&&f&&(m=!1,f.length?y=f.concat(y):v=-1,y.length&&c())}function c(){if(!m){var e=i(a);m=!0;for(var t=y.length;t;){for(f=y,y=[];++v1)for(var r=1;r0&&u>c&&(u=c);for(var l=0;l=0?(p=y.substr(0,m),d=y.substr(m+1)):(p=y,d=""),h=decodeURIComponent(p),f=decodeURIComponent(d),o(s,h)?n(s[h])?s[h].push(f):s[h]=[s[h],f]:s[h]=f}return s};var n=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}},{}],14:[function(e,t,r){"use strict";function o(e,t){if(e.map)return e.map(t);for(var r=[],o=0;o0)n.scope=r;else if("undefined"!=typeof r)throw new Error("the scope given to `copyIndex` was not an array with settings, synonyms or rules");return this._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(e)+"/operation",body:n,hostType:"write",callback:i})},o.prototype.getLogs=function(t,r,o){var n=e(26),i={};return"object"==typeof t?(i=n(t),o=r):0===arguments.length||"function"==typeof t?o=t:1===arguments.length||"function"==typeof r?(o=r,i.offset=t):(i.offset=t,i.length=r),void 0===i.offset&&(i.offset=0),void 0===i.length&&(i.length=10),this._jsonRequest({method:"GET",url:"/1/logs?"+this._getSearchParams(i,""),hostType:"read",callback:o})},o.prototype.listIndexes=function(e,t){var r="";return void 0===e||"function"==typeof e?t=e:r="?page="+e,this._jsonRequest({method:"GET",url:"/1/indexes"+r,hostType:"read",callback:t})},o.prototype.initIndex=function(e){return new i(this,e)},o.prototype.initAnalytics=function(t){var r=e(27);return r(this.applicationID,this.apiKey,t)},o.prototype.listUserKeys=s(function(e){return this.listApiKeys(e)},a("client.listUserKeys()","client.listApiKeys()")),o.prototype.listApiKeys=function(e){return this._jsonRequest({method:"GET",url:"/1/keys",hostType:"read",callback:e})},o.prototype.getUserKeyACL=s(function(e,t){return this.getApiKey(e,t)},a("client.getUserKeyACL()","client.getApiKey()")),o.prototype.getApiKey=function(e,t){return this._jsonRequest({method:"GET",url:"/1/keys/"+e,hostType:"read",callback:t})},o.prototype.deleteUserKey=s(function(e,t){return this.deleteApiKey(e,t)},a("client.deleteUserKey()","client.deleteApiKey()")),o.prototype.deleteApiKey=function(e,t){return this._jsonRequest({method:"DELETE",url:"/1/keys/"+e,hostType:"write",callback:t})},o.prototype.restoreApiKey=function(e,t){return this._jsonRequest({method:"POST",url:"/1/keys/"+e+"/restore",hostType:"write",callback:t})},o.prototype.addUserKey=s(function(e,t,r){return this.addApiKey(e,t,r)},a("client.addUserKey()","client.addApiKey()")),o.prototype.addApiKey=function(t,r,o){var n=e(8),i="Usage: client.addApiKey(arrayOfAcls[, params, callback])";if(!n(t))throw new Error(i);1!==arguments.length&&"function"!=typeof r||(o=r,r=null);var s={acl:t};return r&&(s.validity=r.validity,s.maxQueriesPerIPPerHour=r.maxQueriesPerIPPerHour,s.maxHitsPerQuery=r.maxHitsPerQuery,s.indexes=r.indexes,s.description=r.description,r.queryParameters&&(s.queryParameters=this._getSearchParams(r.queryParameters,"")),s.referers=r.referers),this._jsonRequest({method:"POST",url:"/1/keys",body:s,hostType:"write",callback:o})},o.prototype.addUserKeyWithValidity=s(function(e,t,r){return this.addApiKey(e,t,r)},a("client.addUserKeyWithValidity()","client.addApiKey()")),o.prototype.updateUserKey=s(function(e,t,r,o){return this.updateApiKey(e,t,r,o)},a("client.updateUserKey()","client.updateApiKey()")),o.prototype.updateApiKey=function(t,r,o,n){var i=e(8),s="Usage: client.updateApiKey(key, arrayOfAcls[, params, callback])";if(!i(r))throw new Error(s);2!==arguments.length&&"function"!=typeof o||(n=o,o=null);var a={acl:r};return o&&(a.validity=o.validity,a.maxQueriesPerIPPerHour=o.maxQueriesPerIPPerHour,a.maxHitsPerQuery=o.maxHitsPerQuery,a.indexes=o.indexes,a.description=o.description,o.queryParameters&&(a.queryParameters=this._getSearchParams(o.queryParameters,"")),a.referers=o.referers),this._jsonRequest({method:"PUT",url:"/1/keys/"+t,body:a,hostType:"write",callback:n})},o.prototype.startQueriesBatch=s(function(){this._batch=[]},a("client.startQueriesBatch()","client.search()")),o.prototype.addQueryInBatch=s(function(e,t,r){this._batch.push({indexName:e,query:t,params:r})},a("client.addQueryInBatch()","client.search()")),o.prototype.sendQueriesBatch=s(function(e){return this.search(this._batch,e)},a("client.sendQueriesBatch()","client.search()")),o.prototype.batch=function(t,r){var o=e(8),n="Usage: client.batch(operations[, callback])";if(!o(t))throw new Error(n);return this._jsonRequest({method:"POST",url:"/1/indexes/*/batch",body:{requests:t},hostType:"write",callback:r})},o.prototype.assignUserID=function(e,t){if(!e.userID||!e.cluster)throw new l.AlgoliaSearchError("You have to provide both a userID and cluster",e);return this._jsonRequest({method:"POST",url:"/1/clusters/mapping",hostType:"write",body:{cluster:e.cluster},callback:t,headers:{"x-algolia-user-id":e.userID}})},o.prototype.assignUserIDs=function(e,t){if(!e.userIDs||!e.cluster)throw new l.AlgoliaSearchError("You have to provide both an array of userIDs and cluster",e);return this._jsonRequest({method:"POST",url:"/1/clusters/mapping/batch",hostType:"write",body:{cluster:e.cluster,users:e.userIDs},callback:t})},o.prototype.getTopUserID=function(e){return this._jsonRequest({method:"GET",url:"/1/clusters/mapping/top",hostType:"read",callback:e})},o.prototype.getUserID=function(e,t){if(!e.userID)throw new l.AlgoliaSearchError("You have to provide a userID",{debugData:e});return this._jsonRequest({method:"GET",url:"/1/clusters/mapping/"+e.userID,hostType:"read",callback:t})},o.prototype.listClusters=function(e){return this._jsonRequest({method:"GET",url:"/1/clusters",hostType:"read",callback:e})},o.prototype.listUserIDs=function(e,t){return this._jsonRequest({method:"GET",url:"/1/clusters/mapping",body:e,hostType:"read",callback:t})},o.prototype.removeUserID=function(e,t){if(!e.userID)throw new l.AlgoliaSearchError("You have to provide a userID",{debugData:e});return this._jsonRequest({method:"DELETE",url:"/1/clusters/mapping",hostType:"write",callback:t,headers:{"x-algolia-user-id":e.userID}})},o.prototype.searchUserIDs=function(e,t){return this._jsonRequest({method:"POST",url:"/1/clusters/mapping/search",body:e,hostType:"read",callback:t})},o.prototype.setPersonalizationStrategy=function(e,t){return this._jsonRequest({method:"POST",url:"/1/recommendation/personalization/strategy",body:e,hostType:"write",callback:t})},o.prototype.getPersonalizationStrategy=function(e){return this._jsonRequest({method:"GET",url:"/1/recommendation/personalization/strategy",hostType:"read",callback:e})},o.prototype.destroy=n,o.prototype.enableRateLimitForward=n,o.prototype.disableRateLimitForward=n,o.prototype.useSecuredAPIKey=n,o.prototype.disableSecuredAPIKey=n,o.prototype.generateSecuredApiKey=n,o.prototype.getSecuredApiKeyRemainingValidity=n},{17:17,18:18,26:26,27:27,28:28,29:29,30:30,7:7,8:8}],17:[function(e,t,r){ +(function(r){function o(t,r,o){var i=e(1)("algoliasearch"),s=e(26),a=e(8),u=e(32),l="Usage: algoliasearch(applicationID, apiKey, opts)";if(o._allowEmptyCredentials!==!0&&!t)throw new c.AlgoliaSearchError("Please provide an application ID. "+l);if(o._allowEmptyCredentials!==!0&&!r)throw new c.AlgoliaSearchError("Please provide an API key. "+l);this.applicationID=t,this.apiKey=r,this.hosts={read:[],write:[]},o=o||{},this._timeouts=o.timeouts||{connect:1e3,read:2e3,write:3e4},o.timeout&&(this._timeouts.connect=this._timeouts.read=this._timeouts.write=o.timeout);var p=o.protocol||"https:";if(/:$/.test(p)||(p+=":"),"http:"!==p&&"https:"!==p)throw new c.AlgoliaSearchError("protocol must be `http:` or `https:` (was `"+o.protocol+"`)");if(this._checkAppIdData(),o.hosts)a(o.hosts)?(this.hosts.read=s(o.hosts),this.hosts.write=s(o.hosts)):(this.hosts.read=s(o.hosts.read),this.hosts.write=s(o.hosts.write));else{var d=u(this._shuffleResult,function(e){return t+"-"+e+".algolianet.com"}),h=(o.dsn===!1?"":"-dsn")+".algolia.net";this.hosts.read=[this.applicationID+h].concat(d),this.hosts.write=[this.applicationID+".algolia.net"].concat(d)}this.hosts.read=u(this.hosts.read,n(p)),this.hosts.write=u(this.hosts.write,n(p)),this.extraHeaders={},this.cache=o._cache||{},this._ua=o._ua,this._useCache=!(void 0!==o._useCache&&!o._cache)||o._useCache,this._useRequestCache=this._useCache&&o._useRequestCache,this._useFallback=void 0===o.useFallback||o.useFallback,this._setTimeout=o._setTimeout,i("init done, %j",this)}function n(e){return function(t){return e+"//"+t.toLowerCase()}}function i(e){if(void 0===Array.prototype.toJSON)return JSON.stringify(e);var t=Array.prototype.toJSON;delete Array.prototype.toJSON;var r=JSON.stringify(e);return Array.prototype.toJSON=t,r}function s(e){for(var t,r,o=e.length;0!==o;)r=Math.floor(Math.random()*o),o-=1,t=e[o],e[o]=e[r],e[r]=t;return e}function a(e){var t={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){var o;o="x-algolia-api-key"===r||"x-algolia-application-id"===r?"**hidden for security purposes**":e[r],t[r]=o}return t}t.exports=o;var c=e(30),u=e(31),l=e(20),p=e(36),d=500,h=r.env.RESET_APP_DATA_TIMER&&parseInt(r.env.RESET_APP_DATA_TIMER,10)||12e4;o.prototype.initIndex=function(e){return new l(this,e)},o.prototype.setExtraHeader=function(e,t){this.extraHeaders[e.toLowerCase()]=t},o.prototype.getExtraHeader=function(e){return this.extraHeaders[e.toLowerCase()]},o.prototype.unsetExtraHeader=function(e){delete this.extraHeaders[e.toLowerCase()]},o.prototype.addAlgoliaAgent=function(e){var t="; "+e;this._ua.indexOf(t)===-1&&(this._ua+=t)},o.prototype._jsonRequest=function(t){function r(e,n){function u(e){var t=e&&e.body&&e.body.message&&e.body.status||e.statusCode||e&&e.body&&200;h("received response: statusCode: %s, computed statusCode: %d, headers: %j",e.statusCode,t,e.headers);var r=2===Math.floor(t/100),o=new Date;if(w.push({currentHost:A,headers:a(p),content:s||null,contentLength:void 0!==s?s.length:null,method:n.method,timeouts:n.timeouts,url:n.url,startTime:x,endTime:o,duration:o-x,statusCode:t}),r)return m._useCache&&!m._useRequestCache&&y&&(y[l]=e.responseText),{responseText:e.responseText,body:e.body};var i=4!==Math.floor(t/100);if(i)return v+=1,_();h("unrecoverable error");var u=new c.AlgoliaSearchError(e.body&&e.body.message,{debugData:w,statusCode:t});return m._promise.reject(u)}function d(e){h("error: %s, stack: %s",e.message,e.stack);var r=new Date;return w.push({currentHost:A,headers:a(p),content:s||null,contentLength:void 0!==s?s.length:null,method:n.method,timeouts:n.timeouts,url:n.url,startTime:x,endTime:r,duration:r-x}),e instanceof c.AlgoliaSearchError||(e=new c.Unknown(e&&e.message,e)),v+=1,e instanceof c.Unknown||e instanceof c.UnparsableJSON||v>=m.hosts[t.hostType].length&&(g||!b)?(e.debugData=w,m._promise.reject(e)):e instanceof c.RequestTimeout?T():_()}function _(){return h("retrying request"),m._incrementHostIndex(t.hostType),r(e,n)}function T(){return h("retrying request with higher timeout"),m._incrementHostIndex(t.hostType),m._incrementTimeoutMultipler(),n.timeouts=m._getTimeoutsForRequest(t.hostType),r(e,n)}m._checkAppIdData();var x=new Date;if(m._useCache&&!m._useRequestCache&&(l=t.url),m._useCache&&!m._useRequestCache&&s&&(l+="_body_"+n.body),o(!m._useRequestCache,y,l)){h("serving response from cache");var R=y[l];return m._promise.resolve({body:JSON.parse(R),responseText:R})}if(v>=m.hosts[t.hostType].length)return!b||g?(h("could not get any response"),m._promise.reject(new c.AlgoliaSearchError("Cannot connect to the AlgoliaSearch API. Send an email to support@algolia.com to report and resolve the issue. Application id was: "+m.applicationID,{debugData:w}))):(h("switching to fallback"),v=0,n.method=t.fallback.method,n.url=t.fallback.url,n.jsonBody=t.fallback.body,n.jsonBody&&(n.body=i(n.jsonBody)),p=m._computeRequestHeaders({additionalUA:f,headers:t.headers}),n.timeouts=m._getTimeoutsForRequest(t.hostType),m._setHostIndexByType(0,t.hostType),g=!0,r(m._request.fallback,n));var A=m._getHostByType(t.hostType),j=A+n.url,S={body:n.body,jsonBody:n.jsonBody,method:n.method,headers:p,timeouts:n.timeouts,debug:h,forceAuthHeaders:n.forceAuthHeaders};return h("method: %s, url: %s, headers: %j, timeouts: %d",S.method,j,S.headers,S.timeouts),e===m._request.fallback&&h("using fallback"),e.call(m,j,S).then(u,d)}function o(e,t,r){return m._useCache&&e&&t&&void 0!==t[r]}function n(e,r){return o(m._useRequestCache,y,l)&&e["catch"](function(){delete y[l]}),"function"!=typeof t.callback?e.then(r):void e.then(function(e){u(function(){t.callback(null,r(e))},m._setTimeout||setTimeout)},function(e){u(function(){t.callback(e)},m._setTimeout||setTimeout)})}this._checkAppIdData();var s,l,p,h=e(1)("algoliasearch:"+t.url),f=t.additionalUA||"",y=t.cache,m=this,v=0,g=!1,b=m._useFallback&&m._request.fallback&&t.fallback;this.apiKey.length>d&&void 0!==t.body&&(void 0!==t.body.params||void 0!==t.body.requests)?(t.body.apiKey=this.apiKey,p=this._computeRequestHeaders({additionalUA:f,withApiKey:!1,headers:t.headers})):p=this._computeRequestHeaders({additionalUA:f,headers:t.headers}),void 0!==t.body&&(s=i(t.body)),h("request start");var w=[];if(m._useCache&&m._useRequestCache&&(l=t.url),m._useCache&&m._useRequestCache&&s&&(l+="_body_"+s),o(m._useRequestCache,y,l)){h("serving request from cache");var _=y[l],T="function"!=typeof _.then?m._promise.resolve({responseText:_}):_;return n(T,function(e){return JSON.parse(e.responseText)})}var x=r(m._request,{url:t.url,method:t.method,body:s,jsonBody:t.body,timeouts:m._getTimeoutsForRequest(t.hostType),forceAuthHeaders:t.forceAuthHeaders});return m._useCache&&m._useRequestCache&&y&&(y[l]=x),n(x,function(e){return e.body})},o.prototype._getSearchParams=function(e,t){if(void 0===e||null===e)return t;for(var r in e)null!==r&&void 0!==e[r]&&e.hasOwnProperty(r)&&(t+=""===t?"":"&",t+=r+"="+encodeURIComponent("[object Array]"===Object.prototype.toString.call(e[r])?i(e[r]):e[r]));return t},o.prototype._computeRequestHeaders=function(t){var r=e(5),o=t.additionalUA?this._ua+"; "+t.additionalUA:this._ua,n={"x-algolia-agent":o,"x-algolia-application-id":this.applicationID};return t.withApiKey!==!1&&(n["x-algolia-api-key"]=this.apiKey),this.userToken&&(n["x-algolia-usertoken"]=this.userToken),this.securityTags&&(n["x-algolia-tagfilters"]=this.securityTags),r(this.extraHeaders,function(e,t){n[t]=e}),t.headers&&r(t.headers,function(e,t){n[t]=e}),n},o.prototype.search=function(t,r,o){var n=e(8),i=e(32),s="Usage: client.search(arrayOfQueries[, callback])";if(!n(t))throw new Error(s);"function"==typeof r?(o=r,r={}):void 0===r&&(r={});var a=this,c={requests:i(t,function(e){var t="";return void 0!==e.query&&(t+="query="+encodeURIComponent(e.query)),{indexName:e.indexName,params:a._getSearchParams(e.params,t)}})},u=i(c.requests,function(e,t){return t+"="+encodeURIComponent("/1/indexes/"+encodeURIComponent(e.indexName)+"?"+e.params)}).join("&"),l="/1/indexes/*/queries";return void 0!==r.strategy&&(c.strategy=r.strategy),this._jsonRequest({cache:this.cache,method:"POST",url:l,body:c,hostType:"read",fallback:{method:"GET",url:"/1/indexes/*",body:{params:u}},callback:o})},o.prototype.searchForFacetValues=function(t){var r=e(8),o=e(32),n="Usage: client.searchForFacetValues([{indexName, params: {facetName, facetQuery, ...params}}, ...queries])";if(!r(t))throw new Error(n);var i=this;return i._promise.all(o(t,function(t){if(!t||void 0===t.indexName||void 0===t.params.facetName||void 0===t.params.facetQuery)throw new Error(n);var r=e(26),o=e(34),s=t.indexName,a=t.params,c=a.facetName,u=o(r(a),function(e){return"facetName"===e}),l=i._getSearchParams(u,"");return i._jsonRequest({cache:i.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(s)+"/facets/"+encodeURIComponent(c)+"/query",hostType:"read",body:{params:l}})}))},o.prototype.setSecurityTags=function(e){if("[object Array]"===Object.prototype.toString.call(e)){for(var t=[],r=0;rh?this._resetInitialAppIdData(e):e},o.prototype._resetInitialAppIdData=function(e){var t=e||{};return t.hostIndexes={read:0,write:0},t.timeoutMultiplier=1,t.shuffleResult=t.shuffleResult||s([1,2,3]),this._setAppIdData(t)},o.prototype._cacheAppIdData=function(e){this._hostIndexes=e.hostIndexes,this._timeoutMultiplier=e.timeoutMultiplier,this._shuffleResult=e.shuffleResult},o.prototype._partialAppIdDataUpdate=function(t){var r=e(5),o=this._getAppIdData();return r(t,function(e,t){o[t]=e}),this._setAppIdData(o)},o.prototype._getHostByType=function(e){return this.hosts[e][this._getHostIndexByType(e)]},o.prototype._getTimeoutMultiplier=function(){return this._timeoutMultiplier},o.prototype._getHostIndexByType=function(e){return this._hostIndexes[e]},o.prototype._setHostIndexByType=function(t,r){var o=e(26),n=o(this._hostIndexes);return n[r]=t,this._partialAppIdDataUpdate({hostIndexes:n}),t},o.prototype._incrementHostIndex=function(e){return this._setHostIndexByType((this._getHostIndexByType(e)+1)%this.hosts[e].length,e)},o.prototype._incrementTimeoutMultipler=function(){var e=Math.max(this._timeoutMultiplier+1,4);return this._partialAppIdDataUpdate({timeoutMultiplier:e})},o.prototype._getTimeoutsForRequest=function(e){return{connect:this._timeouts.connect*this._timeoutMultiplier,complete:this._timeouts[e]*this._timeoutMultiplier}}}).call(this,e(12))},{1:1,12:12,20:20,26:26,30:30,31:31,32:32,34:34,36:36,5:5,8:8}],18:[function(e,t,r){function o(){s.apply(this,arguments)}function n(e,t,r){function o(r,n){var i={page:r||0,hitsPerPage:t||100},s=n||[];return e(i).then(function(e){var t=e.hits,r=e.nbHits,n=t.map(function(e){return delete e._highlightResult,e}),a=s.concat(n);return a.lengths&&(t=s),"published"!==e.status?l._promise.delay(t).then(r):e})}function o(e){u(function(){t(null,e)},l._setTimeout||setTimeout)}function n(e){u(function(){t(e)},l._setTimeout||setTimeout)}var i=100,s=5e3,a=0,c=this,l=c.as,p=r();return t?void p.then(o,n):p},o.prototype.clearIndex=function(e){var t=this;return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(t.indexName)+"/clear",hostType:"write",callback:e})},o.prototype.getSettings=function(e,t){1===arguments.length&&"function"==typeof e&&(t=e,e={}),e=e||{};var r=encodeURIComponent(this.indexName);return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+r+"/settings?getVersion=2"+(e.advanced?"&advanced="+e.advanced:""),hostType:"read",callback:t})},o.prototype.searchSynonyms=function(e,t){return"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/search",body:e,hostType:"read",callback:t})},o.prototype.exportSynonyms=function(e,t){return n(this.searchSynonyms.bind(this),e,t)},o.prototype.saveSynonym=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e.objectID)+"?forwardToReplicas="+o,body:e,hostType:"write",callback:r})},o.prototype.getSynonym=function(e,t){return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e),hostType:"read",callback:t})},o.prototype.deleteSynonym=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e)+"?forwardToReplicas="+o,hostType:"write",callback:r})},o.prototype.clearSynonyms=function(e,t){"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),void 0!==e.forwardToSlaves&&p();var r=e.forwardToSlaves||e.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/clear?forwardToReplicas="+r,hostType:"write",callback:t})},o.prototype.batchSynonyms=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/batch?forwardToReplicas="+o+"&replaceExistingSynonyms="+(t.replaceExistingSynonyms?"true":"false"),hostType:"write",body:e,callback:r})},o.prototype.searchRules=function(e,t){return"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/search",body:e,hostType:"read",callback:t})},o.prototype.exportRules=function(e,t){return n(this.searchRules.bind(this),e,t)},o.prototype.saveRule=function(e,t,r){if("function"==typeof t?(r=t,t={}):void 0===t&&(t={}),!e.objectID)throw new l.AlgoliaSearchError("Missing or empty objectID field for rule");var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e.objectID)+"?forwardToReplicas="+o,body:e,hostType:"write",callback:r})},o.prototype.getRule=function(e,t){return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e),hostType:"read",callback:t})},o.prototype.deleteRule=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={});var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e)+"?forwardToReplicas="+o,hostType:"write",callback:r})},o.prototype.clearRules=function(e,t){"function"==typeof e?(t=e,e={}):void 0===e&&(e={});var r=e.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/clear?forwardToReplicas="+r,hostType:"write",callback:t})},o.prototype.batchRules=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={});var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/batch?forwardToReplicas="+o+"&clearExistingRules="+(t.clearExistingRules===!0?"true":"false"),hostType:"write",body:e,callback:r})},o.prototype.exists=function(e){var t=this.getSettings().then(function(){return!0})["catch"](function(e){if(e instanceof l.AlgoliaSearchError&&404===e.statusCode)return!1;throw e});return"function"!=typeof e?t:void t.then(function(t){e(null,t)})["catch"](function(t){e(t)})},o.prototype.findObject=function(e,t,r){t=void 0===t?{}:t;var o=void 0===t.paginate||t.paginate,n=void 0!==t.query?t.query:"",i=this,s=0,a=function(){return t.page=s,i.search(n,t).then(function(t){for(var r=t.hits,n=0;n=t.nbPages)throw new l.ObjectNotFound("Object not found");return a()})},c=a(s);return void 0===r?c:void c.then(function(e){r(null,e)})["catch"](function(e){r(e)})},o.prototype.getObjectPosition=function(e,t){for(var r=e.hits,o=0;o1&&a()}if(!h.cors&&!h.hasXDomainRequest)return void o(new u.Network("CORS not supported"));e=l(e,t.headers);var d,f,y=t.body,m=h.cors?new XMLHttpRequest:new XDomainRequest,v=!1;d=setTimeout(s,t.timeouts.connect),m.onprogress=c,"onreadystatechange"in m&&(m.onreadystatechange=p),m.onload=n,m.onerror=i,m instanceof XMLHttpRequest?(m.open(t.method,e,!0),t.forceAuthHeaders&&(m.setRequestHeader("x-algolia-application-id",t.headers["x-algolia-application-id"]),m.setRequestHeader("x-algolia-api-key",t.headers["x-algolia-api-key"]))):m.open(t.method,e),h.cors&&(y&&("POST"===t.method?m.setRequestHeader("content-type","application/x-www-form-urlencoded"):m.setRequestHeader("content-type","application/json")),m.setRequestHeader("accept","application/json")),y?m.send(y):m.send()})},a.prototype._request.fallback=function(e,t){return e=l(e,t.headers),new n(function(r,o){p(e,t,function(e,t){return e?void o(e):void r(t)})})},a.prototype._promise={reject:function(e){return n.reject(e)},resolve:function(e){return n.resolve(e)},delay:function(e){return new n(function(t){setTimeout(t,e)})},all:function(e){return n.all(e)}},s}}).call(this,e(12))},{1:1,12:12,23:23,24:24,26:26,3:3,30:30,35:35,37:37,6:6,7:7}],23:[function(e,t,r){"use strict";function o(e,t){return e+=/\?/.test(e)?"&":"?",e+n(t)}t.exports=o;var n=e(14)},{14:14}],24:[function(e,t,r){"use strict";function o(e,t,r){function o(){t.debug("JSONP: success"),m||d||(m=!0,p||(t.debug("JSONP: Fail. Script loaded but did not call the callback"), +a(),r(new n.JSONPScriptFail)))}function s(){"loaded"!==this.readyState&&"complete"!==this.readyState||o()}function a(){clearTimeout(v),f.onload=null,f.onreadystatechange=null,f.onerror=null,h.removeChild(f)}function c(){try{delete window[y],delete window[y+"_loaded"]}catch(e){window[y]=window[y+"_loaded"]=void 0}}function u(){t.debug("JSONP: Script timeout"),d=!0,a(),r(new n.RequestTimeout)}function l(){t.debug("JSONP: Script error"),m||d||(a(),r(new n.JSONPScriptError))}if("GET"!==t.method)return void r(new Error("Method "+t.method+" "+e+" is not supported by JSONP."));t.debug("JSONP: start");var p=!1,d=!1;i+=1;var h=document.getElementsByTagName("head")[0],f=document.createElement("script"),y="algoliaJSONP_"+i,m=!1;window[y]=function(e){return c(),d?void t.debug("JSONP: Late answer, ignoring"):(p=!0,a(),void r(null,{body:e,responseText:JSON.stringify(e)}))},e+="&callback="+y,t.jsonBody&&t.jsonBody.params&&(e+="&"+t.jsonBody.params);var v=setTimeout(u,t.timeouts.complete);f.onreadystatechange=s,f.onload=o,f.onerror=l,f.async=!0,f.defer=!0,f.src=e,h.appendChild(f)}t.exports=o;var n=e(30),i=0},{30:30}],25:[function(e,t,r){function o(e,t){return function(r,o,i){if("function"==typeof r&&"object"==typeof o||"object"==typeof i)throw new n.AlgoliaSearchError("index.search usage is index.search(query, params, cb)");0===arguments.length||"function"==typeof r?(i=r,r=""):1!==arguments.length&&"function"!=typeof o||(i=o,o=void 0),"object"==typeof r&&null!==r?(o=r,r=void 0):void 0!==r&&null!==r||(r="");var s="";void 0!==r&&(s+=e+"="+encodeURIComponent(r));var a;return void 0!==o&&(o.additionalUA&&(a=o.additionalUA,delete o.additionalUA),s=this.as._getSearchParams(o,s)),this._search(s,t,i,a)}}t.exports=o;var n=e(30)},{30:30}],26:[function(e,t,r){t.exports=function(e){return JSON.parse(JSON.stringify(e))}},{}],27:[function(e,t,r){function o(e,t,r){var o={};return r=r||{},r.hosts=r.hosts||["analytics.algolia.com","analytics.algolia.com","analytics.algolia.com","analytics.algolia.com"],r.protocol=r.protocol||"https:",o.as=n(e,t,r),o.getABTests=function(e,t){var r=r||{},o=r.offset||0,n=r.limit||10;return this.as._jsonRequest({method:"GET",url:"/2/abtests?offset="+encodeURIComponent(o)+"&limit="+encodeURIComponent(n),hostType:"read",forceAuthHeaders:!0,callback:t})},o.getABTest=function(e,t){return this.as._jsonRequest({method:"GET",url:"/2/abtests/"+encodeURIComponent(e),hostType:"read",forceAuthHeaders:!0,callback:t})},o.addABTest=function(e,t){return this.as._jsonRequest({method:"POST",url:"/2/abtests",body:e,hostType:"read",forceAuthHeaders:!0,callback:t})},o.stopABTest=function(e,t){return this.as._jsonRequest({method:"POST",url:"/2/abtests/"+encodeURIComponent(e)+"/stop",hostType:"read",forceAuthHeaders:!0,callback:t})},o.deleteABTest=function(e,t){return this.as._jsonRequest({method:"DELETE",url:"/2/abtests/"+encodeURIComponent(e),hostType:"write",forceAuthHeaders:!0,callback:t})},o.waitTask=function(e,t,r){return this.as.initIndex(e).waitTask(t,r)},o}t.exports=o;var n=e(21)},{21:21}],28:[function(e,t,r){t.exports=function(e,t){function r(){return o||(console.warn(t),o=!0),e.apply(this,arguments)}var o=!1;return r}},{}],29:[function(e,t,r){t.exports=function(e,t){var r=e.toLowerCase().replace(/[\.\(\)]/g,"");return"algoliasearch: `"+e+"` was replaced by `"+t+"`. Please see https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated#"+r}},{}],30:[function(e,t,r){"use strict";function o(t,r){var o=e(5),n=this;"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):n.stack=(new Error).stack||"Cannot get a stacktrace, browser is too old",this.name="AlgoliaSearchError",this.message=t||"Unknown error",r&&o(r,function(e,t){n[t]=e})}function n(e,t){function r(){var r=Array.prototype.slice.call(arguments,0);"string"!=typeof r[0]&&r.unshift(t),o.apply(this,r),this.name="AlgoliaSearch"+e+"Error"}return i(r,o),r}var i=e(7);i(o,Error),t.exports={AlgoliaSearchError:o,UnparsableJSON:n("UnparsableJSON","Could not parse the incoming response as JSON, see err.more for details"),RequestTimeout:n("RequestTimeout","Request timed out before getting a response"),Network:n("Network","Network issue, see err.more for details"),JSONPScriptFail:n("JSONPScriptFail"," + + + + diff --git a/js/autocomplete.js/examples/basic_angular.html b/js/autocomplete.js/examples/basic_angular.html new file mode 100644 index 0000000..101bb5e --- /dev/null +++ b/js/autocomplete.js/examples/basic_angular.html @@ -0,0 +1,82 @@ + + + + + + + +
+
+
+ +
+
+
+ + + + + + + + diff --git a/js/autocomplete.js/examples/basic_jquery.html b/js/autocomplete.js/examples/basic_jquery.html new file mode 100644 index 0000000..cd17b5a --- /dev/null +++ b/js/autocomplete.js/examples/basic_jquery.html @@ -0,0 +1,66 @@ + + + + + + + +
+
+
+
+

Basic example

+ +
+
+
+
+ + + + + + + diff --git a/js/autocomplete.js/examples/index.html b/js/autocomplete.js/examples/index.html new file mode 100644 index 0000000..4076f91 --- /dev/null +++ b/js/autocomplete.js/examples/index.html @@ -0,0 +1,24 @@ + + + + + + + Autocomplete.js example + + + + Welcome to the Autocomplete.js examples: +
    +
  1. Basic example
  2. +
  3. Basic Angular.js example
  4. +
  5. Basic jQuery example
  6. +
  7. Test playground
  8. +
  9. Test playground (Angular.js)
  10. +
  11. Test playground (jQuery)
+ + diff --git a/js/autocomplete.js/index.js b/js/autocomplete.js/index.js new file mode 100644 index 0000000..9c197e1 --- /dev/null +++ b/js/autocomplete.js/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./src/standalone/'); diff --git a/js/autocomplete.js/index_angular.js b/js/autocomplete.js/index_angular.js new file mode 100644 index 0000000..523ec3b --- /dev/null +++ b/js/autocomplete.js/index_angular.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./src/angular/directive.js'); diff --git a/js/autocomplete.js/index_jquery.js b/js/autocomplete.js/index_jquery.js new file mode 100644 index 0000000..4d6d0ec --- /dev/null +++ b/js/autocomplete.js/index_jquery.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./src/jquery/plugin.js'); diff --git a/js/autocomplete.js/karma.conf.js b/js/autocomplete.js/karma.conf.js new file mode 100644 index 0000000..73c5076 --- /dev/null +++ b/js/autocomplete.js/karma.conf.js @@ -0,0 +1,55 @@ +'use strict'; + +module.exports = function(config) { + config.set({ + basePath: '', + + frameworks: ['jasmine', 'sinon'], + + reporters: ['progress', 'coverage', 'coveralls'], + + browsers: ['PhantomJS'], + + coverageReporter: { + type: 'lcov', + dir: 'coverage/' + }, + + webpack: { + devtool: 'inline-source-map', + module: { + postLoaders: [{ + test: /\.js$/, + exclude: /(test|node_modules)\//, + loader: 'istanbul-instrumenter' + }] + } + }, + + preprocessors: { + 'test/unit/**/*.js': ['webpack', 'sourcemap'] + }, + + webpackMiddleware: { + noInfo: true + }, + + plugins: [ + 'karma-jasmine', + 'karma-sinon', + 'karma-phantomjs-launcher', + 'karma-chrome-launcher', + 'karma-opera-launcher', + 'karma-safari-launcher', + 'karma-firefox-launcher', + 'karma-coverage', + 'karma-coveralls', + 'karma-sourcemap-loader', + 'karma-webpack' + ], + + files: [ + 'test/unit/**/*.js' + ] + }); +}; diff --git a/js/autocomplete.js/release.sh b/js/autocomplete.js/release.sh new file mode 100644 index 0000000..18adeff --- /dev/null +++ b/js/autocomplete.js/release.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +function error_exit +{ + echo "release: $1" 1>&2 + exit 1 +} + +if [[ $# -eq 0 ]] ; then + error_exit "use ``npm run release [major|minor|patch|x.x.x]``" +fi + +currentVersion=$(json -f package.json version) + +if [[ $1 != 'patch' && $1 != 'minor' && $1 != 'major' ]] +then + nextVersion=$1 +else + nextVersion=$(semver $currentVersion -i $1) +fi + +semver $nextVersion -r ">$currentVersion" || +error_exit "Cannot bump from $currentVersion to $nextVersion" + +if [[ -n $(npm owner add "`npm whoami`") ]]; then + error_exit "Not an owner of the npm repo, ask for it" +fi + +currentBranch=$(git rev-parse --abbrev-ref HEAD) +if [[ $currentBranch != 'master' ]]; then + error_exit "You mut be on master branch" +fi + +if [[ -n $(git status --porcelain) ]]; then + error_exit "Release: Working tree is not clean (git status)" +fi + +echo "module.exports = \"${nextVersion}\";" > version.js + +yarn && +mversion $nextVersion && +yarn build && +conventional-changelog --infile CHANGELOG.md --same-file --preset angular && +doctoc --notitle --maxlevel 3 README.md && +git add README.md CHANGELOG.md package.json bower.json version.js dist/ && +git commit -m $nextVersion && +git tag v$nextVersion && +git push && +git push --tags && +npm publish || error_exit "Something went wrong, check log, be careful and start over" diff --git a/js/autocomplete.js/scripts/netlify-deploy.js b/js/autocomplete.js/scripts/netlify-deploy.js new file mode 100644 index 0000000..5a2209c --- /dev/null +++ b/js/autocomplete.js/scripts/netlify-deploy.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node +'use strict'; +/* eslint-disable no-console */ +const execa = require('execa'); +const replace = require('replace-in-file'); +const NetlifyAPI = require('netlify'); + +const client = new NetlifyAPI(process.env.NETLIFY_API_KEY); + +function logStdOut(opts) { + console.log(opts.stdout); +} + +if (!process.env.NETLIFY_API_KEY || !process.env.NETLIFY_SITE_ID) { + console.warn( + 'Both NETLIFY_API_KEY and NETLIFY_SITE_ID are required. ' + + 'They can be found on ' + + 'https://app.netlify.com/sites/autocompletejs-playgrounds/settings/general' + + ' and https://app.netlify.com/account/applications' + ); + process.exit(0); +} + +execa('yarn', ['build']) + .then(logStdOut) + .then(() => execa('rm', ['-rf', 'netlify-dist'])) + .then(() => execa('mkdir', ['-p', 'netlify-dist/examples'])) + .then(() => execa('cp', ['-r', 'examples', 'netlify-dist'])) + .then(() => execa('mv', ['netlify-dist/examples/index.html', 'netlify-dist'])) + .then(() => + replace({ + files: 'netlify-dist/index.html', + from: /href="\.\./g, + to: 'href=".' + }) + ) + .then(() => execa('mkdir', ['-p', 'netlify-dist/test'])) + .then(() => + execa('cp', [ + 'test/playground.css', + 'test/playground.html', + 'test/playground_angular.html', + 'test/playground_jquery.html', + 'netlify-dist/test' + ]) + ) + .then(() => execa('cp', ['-r', 'dist', 'netlify-dist'])) + .then(() => + replace({ + files: [ + 'netlify-dist/examples/basic.html', + 'netlify-dist/examples/basic_angular.html', + 'netlify-dist/examples/basic_jquery.html' + ], + from: /https:\/\/cdn.jsdelivr.net\/autocomplete.js\/0/g, + to: '../dist' + }) + ) + .then(() => + client.deploy(process.env.NETLIFY_SITE_ID, 'netlify-dist', { + draft: true, + message: process.env.TRAVIS_COMMIT_MESSAGE || '' + }) + ) + .then(({deploy: {id, name, deploy_ssl_url: url}}) => + console.log( + '🕸 site is available at ' + + url + + '\n\n' + + 'Deploy details available at https://app.netlify.com/sites/' + + name + + '/deploys/' + + id + ) + ); + diff --git a/js/autocomplete.js/scripts/release.sh b/js/autocomplete.js/scripts/release.sh new file mode 100644 index 0000000..ecead54 --- /dev/null +++ b/js/autocomplete.js/scripts/release.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +function error_exit +{ + echo "release: $1" 1>&2 + exit 1 +} + +if [[ $# -eq 0 ]] ; then + error_exit "use ``npm run release [major|minor|patch|x.x.x]``" +fi + +currentVersion=$(json -f package.json version) + +if [[ $1 != 'patch' && $1 != 'minor' && $1 != 'major' ]] +then + nextVersion=$1 +else + nextVersion=$(semver $currentVersion -i $1) +fi + +semver $nextVersion -r ">$currentVersion" || +error_exit "Cannot bump from $currentVersion to $nextVersion" + +if ! npm owner ls | grep -q "$(npm whoami)" +then + error_exit "Not an owner of the npm repo, ask for it" +fi + +currentBranch=$(git rev-parse --abbrev-ref HEAD) +if [[ $currentBranch != 'master' ]]; then + error_exit "You must be on master branch" +fi + +if [[ -n $(git status --porcelain) ]]; then + error_exit "Release: Working tree is not clean (git status)" +fi + +echo "module.exports = \"${nextVersion}\";" > version.js + +yarn && +mversion $nextVersion && +yarn build && +conventional-changelog --infile CHANGELOG.md --same-file --preset angular && +doctoc --notitle --maxlevel 3 README.md && +git add README.md CHANGELOG.md package.json bower.json version.js dist/ && +git commit -m $nextVersion && +git tag v$nextVersion && +git push && +git push --tags && +npm publish || error_exit "Something went wrong, check log, be careful and start over" diff --git a/js/autocomplete.js/src/angular/directive.js b/js/autocomplete.js/src/angular/directive.js new file mode 100644 index 0000000..4b79b27 --- /dev/null +++ b/js/autocomplete.js/src/angular/directive.js @@ -0,0 +1,114 @@ +'use strict'; + +var angular = require('angular'); + +// setup DOM element +var DOM = require('../common/dom.js'); +DOM.element = angular.element; + +// setup utils functions +var _ = require('../common/utils.js'); +_.isArray = angular.isArray; +_.isFunction = angular.isFunction; +_.isObject = angular.isObject; +_.bind = angular.element.proxy; +_.each = angular.forEach; +_.map = angular.element.map; +_.mixin = angular.extend; +_.Event = angular.element.Event; + +var EventBus = require('../autocomplete/event_bus.js'); +var Typeahead = require('../autocomplete/typeahead.js'); + +angular.module('algolia.autocomplete', []) + .directive('autocomplete', ['$parse', '$injector', function($parse, $injector) { + // inject the sources in the algolia namespace if available + try { + $injector.get('algolia').sources = Typeahead.sources; + $injector.get('algolia').escapeHighlightedString = _.escapeHighlightedString; + } catch (e) { + // not fatal + } + + return { + restrict: 'AC', // Only apply on an attribute or class + scope: { + options: '&aaOptions', + datasets: '&aaDatasets' + }, + link: function(scope, element, attrs) { + if (!element.hasClass('autocomplete') && attrs.autocomplete !== '') return; + attrs = attrs; // no-unused-vars + scope.options = $parse(scope.options)(scope); + if (!scope.options) { + scope.options = {}; + } + scope.datasets = $parse(scope.datasets)(scope); + if (scope.datasets && !angular.isArray(scope.datasets)) { + scope.datasets = [scope.datasets]; + } + + var eventBus = new EventBus({el: element}); + var autocomplete = null; + + // reinitialization watchers + scope.$watch('options', initialize); + if (angular.isArray(scope.datasets)) { + scope.$watchCollection('datasets', initialize); + } else { + scope.$watch('datasets', initialize); + } + + // init function + function initialize() { + if (autocomplete) { + autocomplete.destroy(); + } + autocomplete = new Typeahead({ + input: element, + dropdownMenuContainer: scope.options.dropdownMenuContainer, + eventBus: eventBus, + hint: scope.options.hint, + minLength: scope.options.minLength, + autoselect: scope.options.autoselect, + autoselectOnBlur: scope.options.autoselectOnBlur, + tabAutocomplete: scope.options.tabAutocomplete, + openOnFocus: scope.options.openOnFocus, + templates: scope.options.templates, + debug: scope.options.debug, + clearOnSelected: scope.options.clearOnSelected, + cssClasses: scope.options.cssClasses, + datasets: scope.datasets, + keyboardShortcuts: scope.options.keyboardShortcuts, + appendTo: scope.options.appendTo, + autoWidth: scope.options.autoWidth + }); + } + + // Propagate the selected event + element.bind('autocomplete:selected', function(object, suggestion, dataset) { + scope.$emit('autocomplete:selected', suggestion, dataset); + }); + + // Propagate the autocompleted event + element.bind('autocomplete:autocompleted', function(object, suggestion, dataset) { + scope.$emit('autocomplete:autocompleted', suggestion, dataset); + }); + + // Propagate the opened event + element.bind('autocomplete:opened', function() { + scope.$emit('autocomplete:opened'); + }); + + // Propagate the closed event + element.bind('autocomplete:closed', function() { + scope.$emit('autocomplete:closed'); + }); + + // Propagate the cursorchanged event + element.bind('autocomplete:cursorchanged', function(event, suggestion, dataset) { + scope.$emit('autocomplete:cursorchanged', event, suggestion, dataset); + }); + } + }; + }]); diff --git a/js/autocomplete.js/src/autocomplete/css.js b/js/autocomplete.js/src/autocomplete/css.js new file mode 100644 index 0000000..dba1630 --- /dev/null +++ b/js/autocomplete.js/src/autocomplete/css.js @@ -0,0 +1,97 @@ +'use strict'; + +var _ = require('../common/utils.js'); + +var css = { + wrapper: { + position: 'relative', + display: 'inline-block' + }, + hint: { + position: 'absolute', + top: '0', + left: '0', + borderColor: 'transparent', + boxShadow: 'none', + // #741: fix hint opacity issue on iOS + opacity: '1' + }, + input: { + position: 'relative', + verticalAlign: 'top', + backgroundColor: 'transparent' + }, + inputWithNoHint: { + position: 'relative', + verticalAlign: 'top' + }, + dropdown: { + position: 'absolute', + top: '100%', + left: '0', + zIndex: '100', + display: 'none' + }, + suggestions: { + display: 'block' + }, + suggestion: { + whiteSpace: 'nowrap', + cursor: 'pointer' + }, + suggestionChild: { + whiteSpace: 'normal' + }, + ltr: { + left: '0', + right: 'auto' + }, + rtl: { + left: 'auto', + right: '0' + }, + defaultClasses: { + root: 'algolia-autocomplete', + prefix: 'aa', + noPrefix: false, + dropdownMenu: 'dropdown-menu', + input: 'input', + hint: 'hint', + suggestions: 'suggestions', + suggestion: 'suggestion', + cursor: 'cursor', + dataset: 'dataset', + empty: 'empty' + }, + // will be merged with the default ones if appendTo is used + appendTo: { + wrapper: { + position: 'absolute', + zIndex: '100', + display: 'none' + }, + input: {}, + inputWithNoHint: {}, + dropdown: { + display: 'block' + } + } +}; + +// ie specific styling +if (_.isMsie()) { + // ie6-8 (and 9?) doesn't fire hover and click events for elements with + // transparent backgrounds, for a workaround, use 1x1 transparent gif + _.mixin(css.input, { + backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)' + }); +} + +// ie7 and under specific styling +if (_.isMsie() && _.isMsie() <= 7) { + // if someone can tell me why this is necessary to align + // the hint with the query in ie7, i'll send you $5 - @JakeHarding + _.mixin(css.input, {marginTop: '-1px'}); +} + +module.exports = css; diff --git a/js/autocomplete.js/src/autocomplete/dataset.js b/js/autocomplete.js/src/autocomplete/dataset.js new file mode 100644 index 0000000..3536557 --- /dev/null +++ b/js/autocomplete.js/src/autocomplete/dataset.js @@ -0,0 +1,307 @@ +'use strict'; + +var datasetKey = 'aaDataset'; +var valueKey = 'aaValue'; +var datumKey = 'aaDatum'; + +var _ = require('../common/utils.js'); +var DOM = require('../common/dom.js'); +var html = require('./html.js'); +var css = require('./css.js'); +var EventEmitter = require('./event_emitter.js'); + +// constructor +// ----------- + +function Dataset(o) { + o = o || {}; + o.templates = o.templates || {}; + + if (!o.source) { + _.error('missing source'); + } + + if (o.name && !isValidName(o.name)) { + _.error('invalid dataset name: ' + o.name); + } + + // tracks the last query the dataset was updated for + this.query = null; + this._isEmpty = true; + + this.highlight = !!o.highlight; + this.name = typeof o.name === 'undefined' || o.name === null ? _.getUniqueId() : o.name; + + this.source = o.source; + this.displayFn = getDisplayFn(o.display || o.displayKey); + + this.debounce = o.debounce; + + this.cache = o.cache !== false; + + this.templates = getTemplates(o.templates, this.displayFn); + + this.css = _.mixin({}, css, o.appendTo ? css.appendTo : {}); + this.cssClasses = o.cssClasses = _.mixin({}, css.defaultClasses, o.cssClasses || {}); + this.cssClasses.prefix = + o.cssClasses.formattedPrefix || _.formatPrefix(this.cssClasses.prefix, this.cssClasses.noPrefix); + + var clazz = _.className(this.cssClasses.prefix, this.cssClasses.dataset); + this.$el = o.$menu && o.$menu.find(clazz + '-' + this.name).length > 0 ? + DOM.element(o.$menu.find(clazz + '-' + this.name)[0]) : + DOM.element( + html.dataset.replace('%CLASS%', this.name) + .replace('%PREFIX%', this.cssClasses.prefix) + .replace('%DATASET%', this.cssClasses.dataset) + ); + + this.$menu = o.$menu; + this.clearCachedSuggestions(); +} + +// static methods +// -------------- + +Dataset.extractDatasetName = function extractDatasetName(el) { + return DOM.element(el).data(datasetKey); +}; + +Dataset.extractValue = function extractValue(el) { + return DOM.element(el).data(valueKey); +}; + +Dataset.extractDatum = function extractDatum(el) { + var datum = DOM.element(el).data(datumKey); + if (typeof datum === 'string') { + // Zepto has an automatic deserialization of the + // JSON encoded data attribute + datum = JSON.parse(datum); + } + return datum; +}; + +// instance methods +// ---------------- + +_.mixin(Dataset.prototype, EventEmitter, { + + // ### private + + _render: function render(query, suggestions) { + if (!this.$el) { + return; + } + var that = this; + + var hasSuggestions; + var renderArgs = [].slice.call(arguments, 2); + this.$el.empty(); + + hasSuggestions = suggestions && suggestions.length; + this._isEmpty = !hasSuggestions; + + if (!hasSuggestions && this.templates.empty) { + this.$el + .html(getEmptyHtml.apply(this, renderArgs)) + .prepend(that.templates.header ? getHeaderHtml.apply(this, renderArgs) : null) + .append(that.templates.footer ? getFooterHtml.apply(this, renderArgs) : null); + } else if (hasSuggestions) { + this.$el + .html(getSuggestionsHtml.apply(this, renderArgs)) + .prepend(that.templates.header ? getHeaderHtml.apply(this, renderArgs) : null) + .append(that.templates.footer ? getFooterHtml.apply(this, renderArgs) : null); + } else if (suggestions && !Array.isArray(suggestions)) { + throw new TypeError('suggestions must be an array'); + } + + if (this.$menu) { + this.$menu.addClass( + this.cssClasses.prefix + (hasSuggestions ? 'with' : 'without') + '-' + this.name + ).removeClass( + this.cssClasses.prefix + (hasSuggestions ? 'without' : 'with') + '-' + this.name + ); + } + + this.trigger('rendered', query); + + function getEmptyHtml() { + var args = [].slice.call(arguments, 0); + args = [{query: query, isEmpty: true}].concat(args); + return that.templates.empty.apply(this, args); + } + + function getSuggestionsHtml() { + var args = [].slice.call(arguments, 0); + var $suggestions; + var nodes; + var self = this; + + var suggestionsHtml = html.suggestions. + replace('%PREFIX%', this.cssClasses.prefix). + replace('%SUGGESTIONS%', this.cssClasses.suggestions); + $suggestions = DOM + .element(suggestionsHtml) + .css(this.css.suggestions); + + // jQuery#append doesn't support arrays as the first argument + // until version 1.8, see http://bugs.jquery.com/ticket/11231 + nodes = _.map(suggestions, getSuggestionNode); + $suggestions.append.apply($suggestions, nodes); + + return $suggestions; + + function getSuggestionNode(suggestion) { + var $el; + + var suggestionHtml = html.suggestion. + replace('%PREFIX%', self.cssClasses.prefix). + replace('%SUGGESTION%', self.cssClasses.suggestion); + $el = DOM.element(suggestionHtml) + .attr({ + role: 'option', + id: ['option', Math.floor(Math.random() * 100000000)].join('-') + }) + .append(that.templates.suggestion.apply(this, [suggestion].concat(args))); + + $el.data(datasetKey, that.name); + $el.data(valueKey, that.displayFn(suggestion) || undefined); // this led to undefined return value + $el.data(datumKey, JSON.stringify(suggestion)); + $el.children().each(function() { DOM.element(this).css(self.css.suggestionChild); }); + + return $el; + } + } + + function getHeaderHtml() { + var args = [].slice.call(arguments, 0); + args = [{query: query, isEmpty: !hasSuggestions}].concat(args); + return that.templates.header.apply(this, args); + } + + function getFooterHtml() { + var args = [].slice.call(arguments, 0); + args = [{query: query, isEmpty: !hasSuggestions}].concat(args); + return that.templates.footer.apply(this, args); + } + }, + + // ### public + + getRoot: function getRoot() { + return this.$el; + }, + + update: function update(query) { + function handleSuggestions(suggestions) { + // if the update has been canceled or if the query has changed + // do not render the suggestions as they've become outdated + if (!this.canceled && query === this.query) { + // concat all the other arguments that could have been passed + // to the render function, and forward them to _render + var extraArgs = [].slice.call(arguments, 1); + this.cacheSuggestions(query, suggestions, extraArgs); + this._render.apply(this, [query, suggestions].concat(extraArgs)); + } + } + + this.query = query; + this.canceled = false; + + if (this.shouldFetchFromCache(query)) { + handleSuggestions.apply(this, [this.cachedSuggestions].concat(this.cachedRenderExtraArgs)); + } else { + var that = this; + var execSource = function() { + // When the call is debounced the condition avoid to do a useless + // request with the last character when the input has been cleared + if (!that.canceled) { + that.source(query, handleSuggestions.bind(that)); + } + }; + + if (this.debounce) { + var later = function() { + that.debounceTimeout = null; + execSource(); + }; + clearTimeout(this.debounceTimeout); + this.debounceTimeout = setTimeout(later, this.debounce); + } else { + execSource(); + } + } + }, + + cacheSuggestions: function cacheSuggestions(query, suggestions, extraArgs) { + this.cachedQuery = query; + this.cachedSuggestions = suggestions; + this.cachedRenderExtraArgs = extraArgs; + }, + + shouldFetchFromCache: function shouldFetchFromCache(query) { + return this.cache && + this.cachedQuery === query && + this.cachedSuggestions && + this.cachedSuggestions.length; + }, + + clearCachedSuggestions: function clearCachedSuggestions() { + delete this.cachedQuery; + delete this.cachedSuggestions; + delete this.cachedRenderExtraArgs; + }, + + cancel: function cancel() { + this.canceled = true; + }, + + clear: function clear() { + if (this.$el) { + this.cancel(); + this.$el.empty(); + this.trigger('rendered', ''); + } + }, + + isEmpty: function isEmpty() { + return this._isEmpty; + }, + + destroy: function destroy() { + this.clearCachedSuggestions(); + this.$el = null; + } +}); + +// helper functions +// ---------------- + +function getDisplayFn(display) { + display = display || 'value'; + + return _.isFunction(display) ? display : displayFn; + + function displayFn(obj) { + return obj[display]; + } +} + +function getTemplates(templates, displayFn) { + return { + empty: templates.empty && _.templatify(templates.empty), + header: templates.header && _.templatify(templates.header), + footer: templates.footer && _.templatify(templates.footer), + suggestion: templates.suggestion || suggestionTemplate + }; + + function suggestionTemplate(context) { + return '

' + displayFn(context) + '

'; + } +} + +function isValidName(str) { + // dashes, underscores, letters, and numbers + return (/^[_a-zA-Z0-9-]+$/).test(str); +} + +module.exports = Dataset; diff --git a/js/autocomplete.js/src/autocomplete/dropdown.js b/js/autocomplete.js/src/autocomplete/dropdown.js new file mode 100644 index 0000000..1516cc0 --- /dev/null +++ b/js/autocomplete.js/src/autocomplete/dropdown.js @@ -0,0 +1,394 @@ +'use strict'; + +var _ = require('../common/utils.js'); +var DOM = require('../common/dom.js'); +var EventEmitter = require('./event_emitter.js'); +var Dataset = require('./dataset.js'); +var css = require('./css.js'); + +// constructor +// ----------- + +function Dropdown(o) { + var that = this; + var onSuggestionClick; + var onSuggestionMouseEnter; + var onSuggestionMouseLeave; + + o = o || {}; + + if (!o.menu) { + _.error('menu is required'); + } + + if (!_.isArray(o.datasets) && !_.isObject(o.datasets)) { + _.error('1 or more datasets required'); + } + if (!o.datasets) { + _.error('datasets is required'); + } + + this.isOpen = false; + this.isEmpty = true; + this.minLength = o.minLength || 0; + this.templates = {}; + this.appendTo = o.appendTo || false; + this.css = _.mixin({}, css, o.appendTo ? css.appendTo : {}); + this.cssClasses = o.cssClasses = _.mixin({}, css.defaultClasses, o.cssClasses || {}); + this.cssClasses.prefix = + o.cssClasses.formattedPrefix || _.formatPrefix(this.cssClasses.prefix, this.cssClasses.noPrefix); + + // bound functions + onSuggestionClick = _.bind(this._onSuggestionClick, this); + onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this); + onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this); + + var cssClass = _.className(this.cssClasses.prefix, this.cssClasses.suggestion); + this.$menu = DOM.element(o.menu) + .on('mouseenter.aa', cssClass, onSuggestionMouseEnter) + .on('mouseleave.aa', cssClass, onSuggestionMouseLeave) + .on('click.aa', cssClass, onSuggestionClick); + + this.$container = o.appendTo ? o.wrapper : this.$menu; + + if (o.templates && o.templates.header) { + this.templates.header = _.templatify(o.templates.header); + this.$menu.prepend(this.templates.header()); + } + + if (o.templates && o.templates.empty) { + this.templates.empty = _.templatify(o.templates.empty); + this.$empty = DOM.element('
' + + '
'); + this.$menu.append(this.$empty); + this.$empty.hide(); + } + + this.datasets = _.map(o.datasets, function(oDataset) { + return initializeDataset(that.$menu, oDataset, o.cssClasses); + }); + _.each(this.datasets, function(dataset) { + var root = dataset.getRoot(); + if (root && root.parent().length === 0) { + that.$menu.append(root); + } + dataset.onSync('rendered', that._onRendered, that); + }); + + if (o.templates && o.templates.footer) { + this.templates.footer = _.templatify(o.templates.footer); + this.$menu.append(this.templates.footer()); + } + + var self = this; + DOM.element(window).resize(function() { + self._redraw(); + }); +} + +// instance methods +// ---------------- + +_.mixin(Dropdown.prototype, EventEmitter, { + + // ### private + + _onSuggestionClick: function onSuggestionClick($e) { + this.trigger('suggestionClicked', DOM.element($e.currentTarget)); + }, + + _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { + var elt = DOM.element($e.currentTarget); + if (elt.hasClass(_.className(this.cssClasses.prefix, this.cssClasses.cursor, true))) { + // we're already on the cursor + // => we're probably entering it again after leaving it for a nested div + return; + } + this._removeCursor(); + + // Fixes iOS double tap behaviour, by modifying the DOM right before the + // native href clicks happens, iOS will requires another tap to follow + // a suggestion that has an element inside + // https://www.google.com/search?q=ios+double+tap+bug+href + var suggestion = this; + setTimeout(function() { + // this exact line, when inside the main loop, will trigger a double tap bug + // on iOS devices + suggestion._setCursor(elt, false); + }, 0); + }, + + _onSuggestionMouseLeave: function onSuggestionMouseLeave($e) { + // $e.relatedTarget is the `EventTarget` the pointing device entered to + if ($e.relatedTarget) { + var elt = DOM.element($e.relatedTarget); + if (elt.closest('.' + _.className(this.cssClasses.prefix, this.cssClasses.cursor, true)).length > 0) { + // our father is a cursor + // => it means we're just leaving the suggestion for a nested div + return; + } + } + this._removeCursor(); + this.trigger('cursorRemoved'); + }, + + _onRendered: function onRendered(e, query) { + this.isEmpty = _.every(this.datasets, isDatasetEmpty); + + if (this.isEmpty) { + if (query.length >= this.minLength) { + this.trigger('empty'); + } + + if (this.$empty) { + if (query.length < this.minLength) { + this._hide(); + } else { + var html = this.templates.empty({ + query: this.datasets[0] && this.datasets[0].query + }); + this.$empty.html(html); + this.$empty.show(); + this._show(); + } + } else if (_.any(this.datasets, hasEmptyTemplate)) { + if (query.length < this.minLength) { + this._hide(); + } else { + this._show(); + } + } else { + this._hide(); + } + } else if (this.isOpen) { + if (this.$empty) { + this.$empty.empty(); + this.$empty.hide(); + } + + if (query.length >= this.minLength) { + this._show(); + } else { + this._hide(); + } + } + + this.trigger('datasetRendered'); + + function isDatasetEmpty(dataset) { + return dataset.isEmpty(); + } + + function hasEmptyTemplate(dataset) { + return dataset.templates && dataset.templates.empty; + } + }, + + _hide: function() { + this.$container.hide(); + }, + + _show: function() { + // can't use jQuery#show because $menu is a span element we want + // display: block; not dislay: inline; + this.$container.css('display', 'block'); + + this._redraw(); + + this.trigger('shown'); + }, + + _redraw: function redraw() { + if (!this.isOpen || !this.appendTo) return; + + this.trigger('redrawn'); + }, + + _getSuggestions: function getSuggestions() { + return this.$menu.find(_.className(this.cssClasses.prefix, this.cssClasses.suggestion)); + }, + + _getCursor: function getCursor() { + return this.$menu.find(_.className(this.cssClasses.prefix, this.cssClasses.cursor)).first(); + }, + + _setCursor: function setCursor($el, updateInput) { + $el.first() + .addClass(_.className(this.cssClasses.prefix, this.cssClasses.cursor, true)) + .attr('aria-selected', 'true'); + this.trigger('cursorMoved', updateInput); + }, + + _removeCursor: function removeCursor() { + this._getCursor() + .removeClass(_.className(this.cssClasses.prefix, this.cssClasses.cursor, true)) + .removeAttr('aria-selected'); + }, + + _moveCursor: function moveCursor(increment) { + var $suggestions; + var $oldCursor; + var newCursorIndex; + var $newCursor; + + if (!this.isOpen) { + return; + } + + $oldCursor = this._getCursor(); + $suggestions = this._getSuggestions(); + + this._removeCursor(); + + // shifting before and after modulo to deal with -1 index + newCursorIndex = $suggestions.index($oldCursor) + increment; + newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1; + + if (newCursorIndex === -1) { + this.trigger('cursorRemoved'); + + return; + } else if (newCursorIndex < -1) { + newCursorIndex = $suggestions.length - 1; + } + + this._setCursor($newCursor = $suggestions.eq(newCursorIndex), true); + + // in the case of scrollable overflow + // make sure the cursor is visible in the menu + this._ensureVisible($newCursor); + }, + + _ensureVisible: function ensureVisible($el) { + var elTop; + var elBottom; + var menuScrollTop; + var menuHeight; + + elTop = $el.position().top; + elBottom = elTop + $el.height() + + parseInt($el.css('margin-top'), 10) + + parseInt($el.css('margin-bottom'), 10); + menuScrollTop = this.$menu.scrollTop(); + menuHeight = this.$menu.height() + + parseInt(this.$menu.css('padding-top'), 10) + + parseInt(this.$menu.css('padding-bottom'), 10); + + if (elTop < 0) { + this.$menu.scrollTop(menuScrollTop + elTop); + } else if (menuHeight < elBottom) { + this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); + } + }, + + // ### public + + close: function close() { + if (this.isOpen) { + this.isOpen = false; + + this._removeCursor(); + this._hide(); + + this.trigger('closed'); + } + }, + + open: function open() { + if (!this.isOpen) { + this.isOpen = true; + + if (!this.isEmpty) { + this._show(); + } + + this.trigger('opened'); + } + }, + + setLanguageDirection: function setLanguageDirection(dir) { + this.$menu.css(dir === 'ltr' ? this.css.ltr : this.css.rtl); + }, + + moveCursorUp: function moveCursorUp() { + this._moveCursor(-1); + }, + + moveCursorDown: function moveCursorDown() { + this._moveCursor(+1); + }, + + getDatumForSuggestion: function getDatumForSuggestion($el) { + var datum = null; + + if ($el.length) { + datum = { + raw: Dataset.extractDatum($el), + value: Dataset.extractValue($el), + datasetName: Dataset.extractDatasetName($el) + }; + } + + return datum; + }, + + getCurrentCursor: function getCurrentCursor() { + return this._getCursor().first(); + }, + + getDatumForCursor: function getDatumForCursor() { + return this.getDatumForSuggestion(this._getCursor().first()); + }, + + getDatumForTopSuggestion: function getDatumForTopSuggestion() { + return this.getDatumForSuggestion(this._getSuggestions().first()); + }, + + cursorTopSuggestion: function cursorTopSuggestion() { + this._setCursor(this._getSuggestions().first(), false); + }, + + update: function update(query) { + _.each(this.datasets, updateDataset); + + function updateDataset(dataset) { + dataset.update(query); + } + }, + + empty: function empty() { + _.each(this.datasets, clearDataset); + this.isEmpty = true; + + function clearDataset(dataset) { + dataset.clear(); + } + }, + + isVisible: function isVisible() { + return this.isOpen && !this.isEmpty; + }, + + destroy: function destroy() { + this.$menu.off('.aa'); + + this.$menu = null; + + _.each(this.datasets, destroyDataset); + + function destroyDataset(dataset) { + dataset.destroy(); + } + } +}); + +// helper functions +// ---------------- +Dropdown.Dataset = Dataset; + +function initializeDataset($menu, oDataset, cssClasses) { + return new Dropdown.Dataset(_.mixin({$menu: $menu, cssClasses: cssClasses}, oDataset)); +} + +module.exports = Dropdown; diff --git a/js/autocomplete.js/src/autocomplete/event_bus.js b/js/autocomplete.js/src/autocomplete/event_bus.js new file mode 100644 index 0000000..fe2e468 --- /dev/null +++ b/js/autocomplete.js/src/autocomplete/event_bus.js @@ -0,0 +1,33 @@ +'use strict'; + +var namespace = 'autocomplete:'; + +var _ = require('../common/utils.js'); +var DOM = require('../common/dom.js'); + +// constructor +// ----------- + +function EventBus(o) { + if (!o || !o.el) { + _.error('EventBus initialized without el'); + } + + this.$el = DOM.element(o.el); +} + +// instance methods +// ---------------- + +_.mixin(EventBus.prototype, { + + // ### public + + trigger: function(type, suggestion, dataset, context) { + var event = _.Event(namespace + type); + this.$el.trigger(event, [suggestion, dataset, context]); + return event; + } +}); + +module.exports = EventBus; diff --git a/js/autocomplete.js/src/autocomplete/event_emitter.js b/js/autocomplete.js/src/autocomplete/event_emitter.js new file mode 100644 index 0000000..a4100ba --- /dev/null +++ b/js/autocomplete.js/src/autocomplete/event_emitter.js @@ -0,0 +1,102 @@ +'use strict'; + +var immediate = require('immediate'); +var splitter = /\s+/; + +module.exports = { + onSync: onSync, + onAsync: onAsync, + off: off, + trigger: trigger +}; + +function on(method, types, cb, context) { + var type; + + if (!cb) { + return this; + } + + types = types.split(splitter); + cb = context ? bindContext(cb, context) : cb; + + this._callbacks = this._callbacks || {}; + + while (type = types.shift()) { + this._callbacks[type] = this._callbacks[type] || {sync: [], async: []}; + this._callbacks[type][method].push(cb); + } + + return this; +} + +function onAsync(types, cb, context) { + return on.call(this, 'async', types, cb, context); +} + +function onSync(types, cb, context) { + return on.call(this, 'sync', types, cb, context); +} + +function off(types) { + var type; + + if (!this._callbacks) { + return this; + } + + types = types.split(splitter); + + while (type = types.shift()) { + delete this._callbacks[type]; + } + + return this; +} + +function trigger(types) { + var type; + var callbacks; + var args; + var syncFlush; + var asyncFlush; + + if (!this._callbacks) { + return this; + } + + types = types.split(splitter); + args = [].slice.call(arguments, 1); + + while ((type = types.shift()) && (callbacks = this._callbacks[type])) { // eslint-disable-line + syncFlush = getFlush(callbacks.sync, this, [type].concat(args)); + asyncFlush = getFlush(callbacks.async, this, [type].concat(args)); + + if (syncFlush()) { + immediate(asyncFlush); + } + } + + return this; +} + +function getFlush(callbacks, context, args) { + return flush; + + function flush() { + var cancelled; + + for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { + // only cancel if the callback explicitly returns false + cancelled = callbacks[i].apply(context, args) === false; + } + + return !cancelled; + } +} + +function bindContext(fn, context) { + return fn.bind ? + fn.bind(context) : + function() { fn.apply(context, [].slice.call(arguments, 0)); }; +} diff --git a/js/autocomplete.js/src/autocomplete/html.js b/js/autocomplete.js/src/autocomplete/html.js new file mode 100644 index 0000000..528571f --- /dev/null +++ b/js/autocomplete.js/src/autocomplete/html.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + wrapper: '', + dropdown: '', + dataset: '
', + suggestions: '', + suggestion: '
' +}; diff --git a/js/autocomplete.js/src/autocomplete/input.js b/js/autocomplete.js/src/autocomplete/input.js new file mode 100644 index 0000000..6e70776 --- /dev/null +++ b/js/autocomplete.js/src/autocomplete/input.js @@ -0,0 +1,341 @@ +'use strict'; + +var specialKeyCodeMap; + +specialKeyCodeMap = { + 9: 'tab', + 27: 'esc', + 37: 'left', + 39: 'right', + 13: 'enter', + 38: 'up', + 40: 'down' +}; + +var _ = require('../common/utils.js'); +var DOM = require('../common/dom.js'); +var EventEmitter = require('./event_emitter.js'); + +// constructor +// ----------- + +function Input(o) { + var that = this; + var onBlur; + var onFocus; + var onKeydown; + var onInput; + + o = o || {}; + + if (!o.input) { + _.error('input is missing'); + } + + // bound functions + onBlur = _.bind(this._onBlur, this); + onFocus = _.bind(this._onFocus, this); + onKeydown = _.bind(this._onKeydown, this); + onInput = _.bind(this._onInput, this); + + this.$hint = DOM.element(o.hint); + this.$input = DOM.element(o.input) + .on('blur.aa', onBlur) + .on('focus.aa', onFocus) + .on('keydown.aa', onKeydown); + + // if no hint, noop all the hint related functions + if (this.$hint.length === 0) { + this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; + } + + // ie7 and ie8 don't support the input event + // ie9 doesn't fire the input event when characters are removed + // not sure if ie10 is compatible + if (!_.isMsie()) { + this.$input.on('input.aa', onInput); + } else { + this.$input.on('keydown.aa keypress.aa cut.aa paste.aa', function($e) { + // if a special key triggered this, ignore it + if (specialKeyCodeMap[$e.which || $e.keyCode]) { + return; + } + + // give the browser a chance to update the value of the input + // before checking to see if the query changed + _.defer(_.bind(that._onInput, that, $e)); + }); + } + + // the query defaults to whatever the value of the input is + // on initialization, it'll most likely be an empty string + this.query = this.$input.val(); + + // helps with calculating the width of the input's value + this.$overflowHelper = buildOverflowHelper(this.$input); +} + +// static methods +// -------------- + +Input.normalizeQuery = function(str) { + // strips leading whitespace and condenses all whitespace + return (str || '').replace(/^\s*/g, '').replace(/\s{2,}/g, ' '); +}; + +// instance methods +// ---------------- + +_.mixin(Input.prototype, EventEmitter, { + + // ### private + + _onBlur: function onBlur() { + this.resetInputValue(); + this.$input.removeAttr('aria-activedescendant'); + this.trigger('blurred'); + }, + + _onFocus: function onFocus() { + this.trigger('focused'); + }, + + _onKeydown: function onKeydown($e) { + // which is normalized and consistent (but not for ie) + var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; + + this._managePreventDefault(keyName, $e); + if (keyName && this._shouldTrigger(keyName, $e)) { + this.trigger(keyName + 'Keyed', $e); + } + }, + + _onInput: function onInput() { + this._checkInputValue(); + }, + + _managePreventDefault: function managePreventDefault(keyName, $e) { + var preventDefault; + var hintValue; + var inputValue; + + switch (keyName) { + case 'tab': + hintValue = this.getHint(); + inputValue = this.getInputValue(); + + preventDefault = hintValue && + hintValue !== inputValue && + !withModifier($e); + break; + + case 'up': + case 'down': + preventDefault = !withModifier($e); + break; + + default: + preventDefault = false; + } + + if (preventDefault) { + $e.preventDefault(); + } + }, + + _shouldTrigger: function shouldTrigger(keyName, $e) { + var trigger; + + switch (keyName) { + case 'tab': + trigger = !withModifier($e); + break; + + default: + trigger = true; + } + + return trigger; + }, + + _checkInputValue: function checkInputValue() { + var inputValue; + var areEquivalent; + var hasDifferentWhitespace; + + inputValue = this.getInputValue(); + areEquivalent = areQueriesEquivalent(inputValue, this.query); + hasDifferentWhitespace = areEquivalent && this.query ? + this.query.length !== inputValue.length : false; + + this.query = inputValue; + + if (!areEquivalent) { + this.trigger('queryChanged', this.query); + } else if (hasDifferentWhitespace) { + this.trigger('whitespaceChanged', this.query); + } + }, + + // ### public + + focus: function focus() { + this.$input.focus(); + }, + + blur: function blur() { + this.$input.blur(); + }, + + getQuery: function getQuery() { + return this.query; + }, + + setQuery: function setQuery(query) { + this.query = query; + }, + + getInputValue: function getInputValue() { + return this.$input.val(); + }, + + setInputValue: function setInputValue(value, silent) { + if (typeof value === 'undefined') { + value = this.query; + } + this.$input.val(value); + + // silent prevents any additional events from being triggered + if (silent) { + this.clearHint(); + } else { + this._checkInputValue(); + } + }, + + expand: function expand() { + this.$input.attr('aria-expanded', 'true'); + }, + + collapse: function collapse() { + this.$input.attr('aria-expanded', 'false'); + }, + + setActiveDescendant: function setActiveDescendant(activedescendantId) { + this.$input.attr('aria-activedescendant', activedescendantId); + }, + + removeActiveDescendant: function removeActiveDescendant() { + this.$input.removeAttr('aria-activedescendant'); + }, + + resetInputValue: function resetInputValue() { + this.setInputValue(this.query, true); + }, + + getHint: function getHint() { + return this.$hint.val(); + }, + + setHint: function setHint(value) { + this.$hint.val(value); + }, + + clearHint: function clearHint() { + this.setHint(''); + }, + + clearHintIfInvalid: function clearHintIfInvalid() { + var val; + var hint; + var valIsPrefixOfHint; + var isValid; + + val = this.getInputValue(); + hint = this.getHint(); + valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; + isValid = val !== '' && valIsPrefixOfHint && !this.hasOverflow(); + + if (!isValid) { + this.clearHint(); + } + }, + + getLanguageDirection: function getLanguageDirection() { + return (this.$input.css('direction') || 'ltr').toLowerCase(); + }, + + hasOverflow: function hasOverflow() { + // 2 is arbitrary, just picking a small number to handle edge cases + var constraint = this.$input.width() - 2; + + this.$overflowHelper.text(this.getInputValue()); + + return this.$overflowHelper.width() >= constraint; + }, + + isCursorAtEnd: function() { + var valueLength; + var selectionStart; + var range; + + valueLength = this.$input.val().length; + selectionStart = this.$input[0].selectionStart; + + if (_.isNumber(selectionStart)) { + return selectionStart === valueLength; + } else if (document.selection) { + // NOTE: this won't work unless the input has focus, the good news + // is this code should only get called when the input has focus + range = document.selection.createRange(); + range.moveStart('character', -valueLength); + + return valueLength === range.text.length; + } + + return true; + }, + + destroy: function destroy() { + this.$hint.off('.aa'); + this.$input.off('.aa'); + + this.$hint = this.$input = this.$overflowHelper = null; + } +}); + +// helper functions +// ---------------- + +function buildOverflowHelper($input) { + return DOM.element('') + .css({ + // position helper off-screen + position: 'absolute', + visibility: 'hidden', + // avoid line breaks and whitespace collapsing + whiteSpace: 'pre', + // use same font css as input to calculate accurate width + fontFamily: $input.css('font-family'), + fontSize: $input.css('font-size'), + fontStyle: $input.css('font-style'), + fontVariant: $input.css('font-variant'), + fontWeight: $input.css('font-weight'), + wordSpacing: $input.css('word-spacing'), + letterSpacing: $input.css('letter-spacing'), + textIndent: $input.css('text-indent'), + textRendering: $input.css('text-rendering'), + textTransform: $input.css('text-transform') + }) + .insertAfter($input); +} + +function areQueriesEquivalent(a, b) { + return Input.normalizeQuery(a) === Input.normalizeQuery(b); +} + +function withModifier($e) { + return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; +} + +module.exports = Input; diff --git a/js/autocomplete.js/src/autocomplete/typeahead.js b/js/autocomplete.js/src/autocomplete/typeahead.js new file mode 100644 index 0000000..73d256a --- /dev/null +++ b/js/autocomplete.js/src/autocomplete/typeahead.js @@ -0,0 +1,654 @@ +'use strict'; + +var attrsKey = 'aaAttrs'; + +var _ = require('../common/utils.js'); +var DOM = require('../common/dom.js'); +var EventBus = require('./event_bus.js'); +var Input = require('./input.js'); +var Dropdown = require('./dropdown.js'); +var html = require('./html.js'); +var css = require('./css.js'); + +// constructor +// ----------- + +// THOUGHT: what if datasets could dynamically be added/removed? +function Typeahead(o) { + var $menu; + var $hint; + + o = o || {}; + + if (!o.input) { + _.error('missing input'); + } + + this.isActivated = false; + this.debug = !!o.debug; + this.autoselect = !!o.autoselect; + this.autoselectOnBlur = !!o.autoselectOnBlur; + this.openOnFocus = !!o.openOnFocus; + this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; + this.autoWidth = (o.autoWidth === undefined) ? true : !!o.autoWidth; + this.clearOnSelected = !!o.clearOnSelected; + this.tabAutocomplete = (o.tabAutocomplete === undefined) ? true : !!o.tabAutocomplete; + + o.hint = !!o.hint; + + if (o.hint && o.appendTo) { + throw new Error('[autocomplete.js] hint and appendTo options can\'t be used at the same time'); + } + + this.css = o.css = _.mixin({}, css, o.appendTo ? css.appendTo : {}); + this.cssClasses = o.cssClasses = _.mixin({}, css.defaultClasses, o.cssClasses || {}); + this.cssClasses.prefix = + o.cssClasses.formattedPrefix = _.formatPrefix(this.cssClasses.prefix, this.cssClasses.noPrefix); + this.listboxId = o.listboxId = [this.cssClasses.root, 'listbox', _.getUniqueId()].join('-'); + + var domElts = buildDom(o); + + this.$node = domElts.wrapper; + var $input = this.$input = domElts.input; + $menu = domElts.menu; + $hint = domElts.hint; + + if (o.dropdownMenuContainer) { + DOM.element(o.dropdownMenuContainer) + .css('position', 'relative') // ensure the container has a relative position + .append($menu.css('top', '0')); // override the top: 100% + } + + // #705: if there's scrollable overflow, ie doesn't support + // blur cancellations when the scrollbar is clicked + // + // #351: preventDefault won't cancel blurs in ie <= 8 + $input.on('blur.aa', function($e) { + var active = document.activeElement; + if (_.isMsie() && ($menu[0] === active || $menu[0].contains(active))) { + $e.preventDefault(); + // stop immediate in order to prevent Input#_onBlur from + // getting exectued + $e.stopImmediatePropagation(); + _.defer(function() { $input.focus(); }); + } + }); + + // #351: prevents input blur due to clicks within dropdown menu + $menu.on('mousedown.aa', function($e) { $e.preventDefault(); }); + + this.eventBus = o.eventBus || new EventBus({el: $input}); + + this.dropdown = new Typeahead.Dropdown({ + appendTo: o.appendTo, + wrapper: this.$node, + menu: $menu, + datasets: o.datasets, + templates: o.templates, + cssClasses: o.cssClasses, + minLength: this.minLength + }) + .onSync('suggestionClicked', this._onSuggestionClicked, this) + .onSync('cursorMoved', this._onCursorMoved, this) + .onSync('cursorRemoved', this._onCursorRemoved, this) + .onSync('opened', this._onOpened, this) + .onSync('closed', this._onClosed, this) + .onSync('shown', this._onShown, this) + .onSync('empty', this._onEmpty, this) + .onSync('redrawn', this._onRedrawn, this) + .onAsync('datasetRendered', this._onDatasetRendered, this); + + this.input = new Typeahead.Input({input: $input, hint: $hint}) + .onSync('focused', this._onFocused, this) + .onSync('blurred', this._onBlurred, this) + .onSync('enterKeyed', this._onEnterKeyed, this) + .onSync('tabKeyed', this._onTabKeyed, this) + .onSync('escKeyed', this._onEscKeyed, this) + .onSync('upKeyed', this._onUpKeyed, this) + .onSync('downKeyed', this._onDownKeyed, this) + .onSync('leftKeyed', this._onLeftKeyed, this) + .onSync('rightKeyed', this._onRightKeyed, this) + .onSync('queryChanged', this._onQueryChanged, this) + .onSync('whitespaceChanged', this._onWhitespaceChanged, this); + + this._bindKeyboardShortcuts(o); + + this._setLanguageDirection(); +} + +// instance methods +// ---------------- + +_.mixin(Typeahead.prototype, { + // ### private + + _bindKeyboardShortcuts: function(options) { + if (!options.keyboardShortcuts) { + return; + } + var $input = this.$input; + var keyboardShortcuts = []; + _.each(options.keyboardShortcuts, function(key) { + if (typeof key === 'string') { + key = key.toUpperCase().charCodeAt(0); + } + keyboardShortcuts.push(key); + }); + DOM.element(document).keydown(function(event) { + var elt = (event.target || event.srcElement); + var tagName = elt.tagName; + if (elt.isContentEditable || tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA') { + // already in an input + return; + } + + var which = event.which || event.keyCode; + if (keyboardShortcuts.indexOf(which) === -1) { + // not the right shortcut + return; + } + + $input.focus(); + event.stopPropagation(); + event.preventDefault(); + }); + }, + + _onSuggestionClicked: function onSuggestionClicked(type, $el) { + var datum; + var context = {selectionMethod: 'click'}; + + if (datum = this.dropdown.getDatumForSuggestion($el)) { + this._select(datum, context); + } + }, + + _onCursorMoved: function onCursorMoved(event, updateInput) { + var datum = this.dropdown.getDatumForCursor(); + var currentCursorId = this.dropdown.getCurrentCursor().attr('id'); + this.input.setActiveDescendant(currentCursorId); + + if (datum) { + if (updateInput) { + this.input.setInputValue(datum.value, true); + } + + this.eventBus.trigger('cursorchanged', datum.raw, datum.datasetName); + } + }, + + _onCursorRemoved: function onCursorRemoved() { + this.input.resetInputValue(); + this._updateHint(); + this.eventBus.trigger('cursorremoved'); + }, + + _onDatasetRendered: function onDatasetRendered() { + this._updateHint(); + + this.eventBus.trigger('updated'); + }, + + _onOpened: function onOpened() { + this._updateHint(); + this.input.expand(); + + this.eventBus.trigger('opened'); + }, + + _onEmpty: function onEmpty() { + this.eventBus.trigger('empty'); + }, + + _onRedrawn: function onRedrawn() { + this.$node.css('top', 0 + 'px'); + this.$node.css('left', 0 + 'px'); + + var inputRect = this.$input[0].getBoundingClientRect(); + + if (this.autoWidth) { + this.$node.css('width', inputRect.width + 'px'); + } + + var wrapperRect = this.$node[0].getBoundingClientRect(); + + var top = inputRect.bottom - wrapperRect.top; + this.$node.css('top', top + 'px'); + var left = inputRect.left - wrapperRect.left; + this.$node.css('left', left + 'px'); + + this.eventBus.trigger('redrawn'); + }, + + _onShown: function onShown() { + this.eventBus.trigger('shown'); + if (this.autoselect) { + this.dropdown.cursorTopSuggestion(); + } + }, + + _onClosed: function onClosed() { + this.input.clearHint(); + this.input.removeActiveDescendant(); + this.input.collapse(); + + this.eventBus.trigger('closed'); + }, + + _onFocused: function onFocused() { + this.isActivated = true; + + if (this.openOnFocus) { + var query = this.input.getQuery(); + if (query.length >= this.minLength) { + this.dropdown.update(query); + } else { + this.dropdown.empty(); + } + + this.dropdown.open(); + } + }, + + _onBlurred: function onBlurred() { + var cursorDatum; + var topSuggestionDatum; + + cursorDatum = this.dropdown.getDatumForCursor(); + topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); + var context = {selectionMethod: 'blur'}; + + if (!this.debug) { + if (this.autoselectOnBlur && cursorDatum) { + this._select(cursorDatum, context); + } else if (this.autoselectOnBlur && topSuggestionDatum) { + this._select(topSuggestionDatum, context); + } else { + this.isActivated = false; + this.dropdown.empty(); + this.dropdown.close(); + } + } + }, + + _onEnterKeyed: function onEnterKeyed(type, $e) { + var cursorDatum; + var topSuggestionDatum; + + cursorDatum = this.dropdown.getDatumForCursor(); + topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); + var context = {selectionMethod: 'enterKey'}; + + if (cursorDatum) { + this._select(cursorDatum, context); + $e.preventDefault(); + } else if (this.autoselect && topSuggestionDatum) { + this._select(topSuggestionDatum, context); + $e.preventDefault(); + } + }, + + _onTabKeyed: function onTabKeyed(type, $e) { + if (!this.tabAutocomplete) { + // Closing the dropdown enables further tabbing + this.dropdown.close(); + return; + } + + var datum; + var context = {selectionMethod: 'tabKey'}; + + if (datum = this.dropdown.getDatumForCursor()) { + this._select(datum, context); + $e.preventDefault(); + } else { + this._autocomplete(true); + } + }, + + _onEscKeyed: function onEscKeyed() { + this.dropdown.close(); + this.input.resetInputValue(); + }, + + _onUpKeyed: function onUpKeyed() { + var query = this.input.getQuery(); + + if (this.dropdown.isEmpty && query.length >= this.minLength) { + this.dropdown.update(query); + } else { + this.dropdown.moveCursorUp(); + } + + this.dropdown.open(); + }, + + _onDownKeyed: function onDownKeyed() { + var query = this.input.getQuery(); + + if (this.dropdown.isEmpty && query.length >= this.minLength) { + this.dropdown.update(query); + } else { + this.dropdown.moveCursorDown(); + } + + this.dropdown.open(); + }, + + _onLeftKeyed: function onLeftKeyed() { + if (this.dir === 'rtl') { + this._autocomplete(); + } + }, + + _onRightKeyed: function onRightKeyed() { + if (this.dir === 'ltr') { + this._autocomplete(); + } + }, + + _onQueryChanged: function onQueryChanged(e, query) { + this.input.clearHintIfInvalid(); + + if (query.length >= this.minLength) { + this.dropdown.update(query); + } else { + this.dropdown.empty(); + } + + this.dropdown.open(); + this._setLanguageDirection(); + }, + + _onWhitespaceChanged: function onWhitespaceChanged() { + this._updateHint(); + this.dropdown.open(); + }, + + _setLanguageDirection: function setLanguageDirection() { + var dir = this.input.getLanguageDirection(); + + if (this.dir !== dir) { + this.dir = dir; + this.$node.css('direction', dir); + this.dropdown.setLanguageDirection(dir); + } + }, + + _updateHint: function updateHint() { + var datum; + var val; + var query; + var escapedQuery; + var frontMatchRegEx; + var match; + + datum = this.dropdown.getDatumForTopSuggestion(); + + if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { + val = this.input.getInputValue(); + query = Input.normalizeQuery(val); + escapedQuery = _.escapeRegExChars(query); + + // match input value, then capture trailing text + frontMatchRegEx = new RegExp('^(?:' + escapedQuery + ')(.+$)', 'i'); + match = frontMatchRegEx.exec(datum.value); + + // clear hint if there's no trailing text + if (match) { + this.input.setHint(val + match[1]); + } else { + this.input.clearHint(); + } + } else { + this.input.clearHint(); + } + }, + + _autocomplete: function autocomplete(laxCursor) { + var hint; + var query; + var isCursorAtEnd; + var datum; + + hint = this.input.getHint(); + query = this.input.getQuery(); + isCursorAtEnd = laxCursor || this.input.isCursorAtEnd(); + + if (hint && query !== hint && isCursorAtEnd) { + datum = this.dropdown.getDatumForTopSuggestion(); + if (datum) { + this.input.setInputValue(datum.value); + } + + this.eventBus.trigger('autocompleted', datum.raw, datum.datasetName); + } + }, + + _select: function select(datum, context) { + if (typeof datum.value !== 'undefined') { + this.input.setQuery(datum.value); + } + if (this.clearOnSelected) { + this.setVal(''); + } else { + this.input.setInputValue(datum.value, true); + } + + this._setLanguageDirection(); + + var event = this.eventBus.trigger('selected', datum.raw, datum.datasetName, context); + if (event.isDefaultPrevented() === false) { + this.dropdown.close(); + + // #118: allow click event to bubble up to the body before removing + // the suggestions otherwise we break event delegation + _.defer(_.bind(this.dropdown.empty, this.dropdown)); + } + }, + + // ### public + + open: function open() { + // if the menu is not activated yet, we need to update + // the underlying dropdown menu to trigger the search + // otherwise we're not gonna see anything + if (!this.isActivated) { + var query = this.input.getInputValue(); + if (query.length >= this.minLength) { + this.dropdown.update(query); + } else { + this.dropdown.empty(); + } + } + this.dropdown.open(); + }, + + close: function close() { + this.dropdown.close(); + }, + + setVal: function setVal(val) { + // expect val to be a string, so be safe, and coerce + val = _.toStr(val); + + if (this.isActivated) { + this.input.setInputValue(val); + } else { + this.input.setQuery(val); + this.input.setInputValue(val, true); + } + + this._setLanguageDirection(); + }, + + getVal: function getVal() { + return this.input.getQuery(); + }, + + destroy: function destroy() { + this.input.destroy(); + this.dropdown.destroy(); + + destroyDomStructure(this.$node, this.cssClasses); + + this.$node = null; + }, + + getWrapper: function getWrapper() { + return this.dropdown.$container[0]; + } +}); + +function buildDom(options) { + var $input; + var $wrapper; + var $dropdown; + var $hint; + + $input = DOM.element(options.input); + $wrapper = DOM + .element(html.wrapper.replace('%ROOT%', options.cssClasses.root)) + .css(options.css.wrapper); + + // override the display property with the table-cell value + // if the parent element is a table and the original input was a block + // -> https://github.com/algolia/autocomplete.js/issues/16 + if (!options.appendTo && $input.css('display') === 'block' && $input.parent().css('display') === 'table') { + $wrapper.css('display', 'table-cell'); + } + var dropdownHtml = html.dropdown. + replace('%PREFIX%', options.cssClasses.prefix). + replace('%DROPDOWN_MENU%', options.cssClasses.dropdownMenu); + $dropdown = DOM.element(dropdownHtml) + .css(options.css.dropdown) + .attr({ + role: 'listbox', + id: options.listboxId + }); + if (options.templates && options.templates.dropdownMenu) { + $dropdown.html(_.templatify(options.templates.dropdownMenu)()); + } + $hint = $input.clone().css(options.css.hint).css(getBackgroundStyles($input)); + + $hint + .val('') + .addClass(_.className(options.cssClasses.prefix, options.cssClasses.hint, true)) + .removeAttr('id name placeholder required') + .prop('readonly', true) + .attr({ + 'aria-hidden': 'true', + autocomplete: 'off', + spellcheck: 'false', + tabindex: -1 + }); + if ($hint.removeData) { + $hint.removeData(); + } + + // store the original values of the attrs that get modified + // so modifications can be reverted on destroy + $input.data(attrsKey, { + 'aria-autocomplete': $input.attr('aria-autocomplete'), + 'aria-expanded': $input.attr('aria-expanded'), + 'aria-owns': $input.attr('aria-owns'), + autocomplete: $input.attr('autocomplete'), + dir: $input.attr('dir'), + role: $input.attr('role'), + spellcheck: $input.attr('spellcheck'), + style: $input.attr('style'), + type: $input.attr('type') + }); + + $input + .addClass(_.className(options.cssClasses.prefix, options.cssClasses.input, true)) + .attr({ + autocomplete: 'off', + spellcheck: false, + + // Accessibility features + // Give the field a presentation of a "select". + // Combobox is the combined presentation of a single line textfield + // with a listbox popup. + // https://www.w3.org/WAI/PF/aria/roles#combobox + role: 'combobox', + // Let the screen reader know the field has an autocomplete + // feature to it. + 'aria-autocomplete': (options.datasets && + options.datasets[0] && options.datasets[0].displayKey ? 'both' : 'list'), + // Indicates whether the dropdown it controls is currently expanded or collapsed + 'aria-expanded': 'false', + 'aria-label': options.ariaLabel, + // Explicitly point to the listbox, + // which is a list of suggestions (aka options) + 'aria-owns': options.listboxId + }) + .css(options.hint ? options.css.input : options.css.inputWithNoHint); + + // ie7 does not like it when dir is set to auto + try { + if (!$input.attr('dir')) { + $input.attr('dir', 'auto'); + } + } catch (e) { + // ignore + } + + $wrapper = options.appendTo + ? $wrapper.appendTo(DOM.element(options.appendTo).eq(0)).eq(0) + : $input.wrap($wrapper).parent(); + + $wrapper + .prepend(options.hint ? $hint : null) + .append($dropdown); + + return { + wrapper: $wrapper, + input: $input, + hint: $hint, + menu: $dropdown + }; +} + +function getBackgroundStyles($el) { + return { + backgroundAttachment: $el.css('background-attachment'), + backgroundClip: $el.css('background-clip'), + backgroundColor: $el.css('background-color'), + backgroundImage: $el.css('background-image'), + backgroundOrigin: $el.css('background-origin'), + backgroundPosition: $el.css('background-position'), + backgroundRepeat: $el.css('background-repeat'), + backgroundSize: $el.css('background-size') + }; +} + +function destroyDomStructure($node, cssClasses) { + var $input = $node.find(_.className(cssClasses.prefix, cssClasses.input)); + + // need to remove attrs that weren't previously defined and + // revert attrs that originally had a value + _.each($input.data(attrsKey), function(val, key) { + if (val === undefined) { + $input.removeAttr(key); + } else { + $input.attr(key, val); + } + }); + + $input + .detach() + .removeClass(_.className(cssClasses.prefix, cssClasses.input, true)) + .insertAfter($node); + if ($input.removeData) { + $input.removeData(attrsKey); + } + + $node.remove(); +} + +Typeahead.Dropdown = Dropdown; +Typeahead.Input = Input; +Typeahead.sources = require('../sources/index.js'); + +module.exports = Typeahead; diff --git a/js/autocomplete.js/src/common/dom.js b/js/autocomplete.js/src/common/dom.js new file mode 100644 index 0000000..15684cb --- /dev/null +++ b/js/autocomplete.js/src/common/dom.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + element: null +}; diff --git a/js/autocomplete.js/src/common/parseAlgoliaClientVersion.js b/js/autocomplete.js/src/common/parseAlgoliaClientVersion.js new file mode 100644 index 0000000..077bdd4 --- /dev/null +++ b/js/autocomplete.js/src/common/parseAlgoliaClientVersion.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = function parseAlgoliaClientVersion(agent) { + var parsed = + // User agent for algoliasearch >= 3.33.0 + agent.match(/Algolia for JavaScript \((\d+\.)(\d+\.)(\d+)\)/) || + // User agent for algoliasearch < 3.33.0 + agent.match(/Algolia for vanilla JavaScript (\d+\.)(\d+\.)(\d+)/); + + if (parsed) { + return [parsed[1], parsed[2], parsed[3]]; + } + + return undefined; +}; diff --git a/js/autocomplete.js/src/common/utils.js b/js/autocomplete.js/src/common/utils.js new file mode 100644 index 0000000..6ea7ad4 --- /dev/null +++ b/js/autocomplete.js/src/common/utils.js @@ -0,0 +1,131 @@ +'use strict'; + +var DOM = require('./dom.js'); + +function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); +} + +module.exports = { + // those methods are implemented differently + // depending on which build it is, using + // $... or angular... or Zepto... or require(...) + isArray: null, + isFunction: null, + isObject: null, + bind: null, + each: null, + map: null, + mixin: null, + + isMsie: function(agentString) { + if (agentString === undefined) { agentString = navigator.userAgent; } + // from https://github.com/ded/bowser/blob/master/bowser.js + if ((/(msie|trident)/i).test(agentString)) { + var match = agentString.match(/(msie |rv:)(\d+(.\d+)?)/i); + if (match) { return match[2]; } + } + return false; + }, + + // http://stackoverflow.com/a/6969486 + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + }, + + isNumber: function(obj) { return typeof obj === 'number'; }, + + toStr: function toStr(s) { + return s === undefined || s === null ? '' : s + ''; + }, + + cloneDeep: function cloneDeep(obj) { + var clone = this.mixin({}, obj); + var self = this; + this.each(clone, function(value, key) { + if (value) { + if (self.isArray(value)) { + clone[key] = [].concat(value); + } else if (self.isObject(value)) { + clone[key] = self.cloneDeep(value); + } + } + }); + return clone; + }, + + error: function(msg) { + throw new Error(msg); + }, + + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + this.each(obj, function(val, key) { + if (result) { + result = test.call(null, val, key, obj) && result; + } + }); + return !!result; + }, + + any: function(obj, test) { + var found = false; + if (!obj) { + return found; + } + this.each(obj, function(val, key) { + if (test.call(null, val, key, obj)) { + found = true; + return false; + } + }); + return found; + }, + + getUniqueId: (function() { + var counter = 0; + return function() { return counter++; }; + })(), + + templatify: function templatify(obj) { + if (this.isFunction(obj)) { + return obj; + } + var $template = DOM.element(obj); + if ($template.prop('tagName') === 'SCRIPT') { + return function template() { return $template.text(); }; + } + return function template() { return String(obj); }; + }, + + defer: function(fn) { setTimeout(fn, 0); }, + + noop: function() {}, + + formatPrefix: function(prefix, noPrefix) { + return noPrefix ? '' : prefix + '-'; + }, + + className: function(prefix, clazz, skipDot) { + return (skipDot ? '' : '.') + prefix + clazz; + }, + + escapeHighlightedString: function(str, highlightPreTag, highlightPostTag) { + highlightPreTag = highlightPreTag || ''; + var pre = document.createElement('div'); + pre.appendChild(document.createTextNode(highlightPreTag)); + + highlightPostTag = highlightPostTag || ''; + var post = document.createElement('div'); + post.appendChild(document.createTextNode(highlightPostTag)); + + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML + .replace(RegExp(escapeRegExp(pre.innerHTML), 'g'), highlightPreTag) + .replace(RegExp(escapeRegExp(post.innerHTML), 'g'), highlightPostTag); + } +}; diff --git a/js/autocomplete.js/src/jquery/plugin.js b/js/autocomplete.js/src/jquery/plugin.js new file mode 100644 index 0000000..1194a35 --- /dev/null +++ b/js/autocomplete.js/src/jquery/plugin.js @@ -0,0 +1,163 @@ +'use strict'; + +// setup DOM element +var DOM = require('../common/dom.js'); +var $ = require('jquery'); +DOM.element = $; + +// setup utils functions +var _ = require('../common/utils.js'); +_.isArray = $.isArray; +_.isFunction = $.isFunction; +_.isObject = $.isPlainObject; +_.bind = $.proxy; +_.each = function(collection, cb) { + // stupid argument order for jQuery.each + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); + } +}; +_.map = $.map; +_.mixin = $.extend; +_.Event = $.Event; + +var Typeahead = require('../autocomplete/typeahead.js'); +var EventBus = require('../autocomplete/event_bus.js'); + +var old; +var typeaheadKey; +var methods; + +old = $.fn.autocomplete; + +typeaheadKey = 'aaAutocomplete'; + +methods = { + // supported signatures: + // function(o, dataset, dataset, ...) + // function(o, [dataset, dataset, ...]) + initialize: function initialize(o, datasets) { + datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); + + o = o || {}; + + return this.each(attach); + + function attach() { + var $input = $(this); + var eventBus = new EventBus({el: $input}); + var typeahead; + + typeahead = new Typeahead({ + input: $input, + eventBus: eventBus, + dropdownMenuContainer: o.dropdownMenuContainer, + hint: o.hint === undefined ? true : !!o.hint, + minLength: o.minLength, + autoselect: o.autoselect, + autoselectOnBlur: o.autoselectOnBlur, + tabAutocomplete: o.tabAutocomplete, + openOnFocus: o.openOnFocus, + templates: o.templates, + debug: o.debug, + clearOnSelected: o.clearOnSelected, + cssClasses: o.cssClasses, + datasets: datasets, + keyboardShortcuts: o.keyboardShortcuts, + appendTo: o.appendTo, + autoWidth: o.autoWidth + }); + + $input.data(typeaheadKey, typeahead); + } + }, + + open: function open() { + return this.each(openTypeahead); + + function openTypeahead() { + var $input = $(this); + var typeahead; + + if (typeahead = $input.data(typeaheadKey)) { + typeahead.open(); + } + } + }, + + close: function close() { + return this.each(closeTypeahead); + + function closeTypeahead() { + var $input = $(this); + var typeahead; + + if (typeahead = $input.data(typeaheadKey)) { + typeahead.close(); + } + } + }, + + val: function val(newVal) { + // mirror jQuery#val functionality: read operate on first match, + // write operates on all matches + return !arguments.length ? getVal(this.first()) : this.each(setVal); + + function setVal() { + var $input = $(this); + var typeahead; + + if (typeahead = $input.data(typeaheadKey)) { + typeahead.setVal(newVal); + } + } + + function getVal($input) { + var typeahead; + var query; + + if (typeahead = $input.data(typeaheadKey)) { + query = typeahead.getVal(); + } + + return query; + } + }, + + destroy: function destroy() { + return this.each(unattach); + + function unattach() { + var $input = $(this); + var typeahead; + + if (typeahead = $input.data(typeaheadKey)) { + typeahead.destroy(); + $input.removeData(typeaheadKey); + } + } + } +}; + +$.fn.autocomplete = function(method) { + var tts; + + // methods that should only act on intialized typeaheads + if (methods[method] && method !== 'initialize') { + // filter out non-typeahead inputs + tts = this.filter(function() { return !!$(this).data(typeaheadKey); }); + return methods[method].apply(tts, [].slice.call(arguments, 1)); + } + return methods.initialize.apply(this, arguments); +}; + +$.fn.autocomplete.noConflict = function noConflict() { + $.fn.autocomplete = old; + return this; +}; + +$.fn.autocomplete.sources = Typeahead.sources; +$.fn.autocomplete.escapeHighlightedString = _.escapeHighlightedString; + +module.exports = $.fn.autocomplete; diff --git a/js/autocomplete.js/src/sources/hits.js b/js/autocomplete.js/src/sources/hits.js new file mode 100644 index 0000000..cd6f9ea --- /dev/null +++ b/js/autocomplete.js/src/sources/hits.js @@ -0,0 +1,24 @@ +'use strict'; + +var _ = require('../common/utils.js'); +var version = require('../../version.js'); +var parseAlgoliaClientVersion = require('../common/parseAlgoliaClientVersion.js'); + +module.exports = function search(index, params) { + var algoliaVersion = parseAlgoliaClientVersion(index.as._ua); + if (algoliaVersion && algoliaVersion[0] >= 3 && algoliaVersion[1] > 20) { + params = params || {}; + params.additionalUA = 'autocomplete.js ' + version; + } + return sourceFn; + + function sourceFn(query, cb) { + index.search(query, params, function(error, content) { + if (error) { + _.error(error.message); + return; + } + cb(content.hits, content); + }); + } +}; diff --git a/js/autocomplete.js/src/sources/index.js b/js/autocomplete.js/src/sources/index.js new file mode 100644 index 0000000..4cea159 --- /dev/null +++ b/js/autocomplete.js/src/sources/index.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + hits: require('./hits.js'), + popularIn: require('./popularIn.js') +}; diff --git a/js/autocomplete.js/src/sources/popularIn.js b/js/autocomplete.js/src/sources/popularIn.js new file mode 100644 index 0000000..adce19c --- /dev/null +++ b/js/autocomplete.js/src/sources/popularIn.js @@ -0,0 +1,85 @@ +'use strict'; + +var _ = require('../common/utils.js'); +var version = require('../../version.js'); +var parseAlgoliaClientVersion = require('../common/parseAlgoliaClientVersion.js'); + +module.exports = function popularIn(index, params, details, options) { + var algoliaVersion = parseAlgoliaClientVersion(index.as._ua); + if (algoliaVersion && algoliaVersion[0] >= 3 && algoliaVersion[1] > 20) { + params = params || {}; + params.additionalUA = 'autocomplete.js ' + version; + } + if (!details.source) { + return _.error("Missing 'source' key"); + } + var source = _.isFunction(details.source) ? details.source : function(hit) { return hit[details.source]; }; + + if (!details.index) { + return _.error("Missing 'index' key"); + } + var detailsIndex = details.index; + + options = options || {}; + + return sourceFn; + + function sourceFn(query, cb) { + index.search(query, params, function(error, content) { + if (error) { + _.error(error.message); + return; + } + + if (content.hits.length > 0) { + var first = content.hits[0]; + + var detailsParams = _.mixin({hitsPerPage: 0}, details); + delete detailsParams.source; // not a query parameter + delete detailsParams.index; // not a query parameter + + var detailsAlgoliaVersion = parseAlgoliaClientVersion(detailsIndex.as._ua); + if (detailsAlgoliaVersion && detailsAlgoliaVersion[0] >= 3 && detailsAlgoliaVersion[1] > 20) { + params.additionalUA = 'autocomplete.js ' + version; + } + + detailsIndex.search(source(first), detailsParams, function(error2, content2) { + if (error2) { + _.error(error2.message); + return; + } + + var suggestions = []; + + // add the 'all department' entry before others + if (options.includeAll) { + var label = options.allTitle || 'All departments'; + suggestions.push(_.mixin({ + facet: {value: label, count: content2.nbHits} + }, _.cloneDeep(first))); + } + + // enrich the first hit iterating over the facets + _.each(content2.facets, function(values, facet) { + _.each(values, function(count, value) { + suggestions.push(_.mixin({ + facet: {facet: facet, value: value, count: count} + }, _.cloneDeep(first))); + }); + }); + + // append all other hits + for (var i = 1; i < content.hits.length; ++i) { + suggestions.push(content.hits[i]); + } + + cb(suggestions, content); + }); + + return; + } + + cb([]); + }); + } +}; diff --git a/js/autocomplete.js/src/standalone/index.js b/js/autocomplete.js/src/standalone/index.js new file mode 100644 index 0000000..9020f53 --- /dev/null +++ b/js/autocomplete.js/src/standalone/index.js @@ -0,0 +1,91 @@ +'use strict'; + +// this will inject Zepto in window, unfortunately no easy commonJS zepto build +var zepto = require('../../zepto.js'); + +// setup DOM element +var DOM = require('../common/dom.js'); +DOM.element = zepto; + +// setup utils functions +var _ = require('../common/utils.js'); +_.isArray = zepto.isArray; +_.isFunction = zepto.isFunction; +_.isObject = zepto.isPlainObject; +_.bind = zepto.proxy; +_.each = function(collection, cb) { + // stupid argument order for jQuery.each + zepto.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); + } +}; +_.map = zepto.map; +_.mixin = zepto.extend; +_.Event = zepto.Event; + +var typeaheadKey = 'aaAutocomplete'; +var Typeahead = require('../autocomplete/typeahead.js'); +var EventBus = require('../autocomplete/event_bus.js'); + +function autocomplete(selector, options, datasets, typeaheadObject) { + datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 2); + + var inputs = zepto(selector).each(function(i, input) { + var $input = zepto(input); + var eventBus = new EventBus({el: $input}); + var typeahead = typeaheadObject || new Typeahead({ + input: $input, + eventBus: eventBus, + dropdownMenuContainer: options.dropdownMenuContainer, + hint: options.hint === undefined ? true : !!options.hint, + minLength: options.minLength, + autoselect: options.autoselect, + autoselectOnBlur: options.autoselectOnBlur, + tabAutocomplete: options.tabAutocomplete, + openOnFocus: options.openOnFocus, + templates: options.templates, + debug: options.debug, + clearOnSelected: options.clearOnSelected, + cssClasses: options.cssClasses, + datasets: datasets, + keyboardShortcuts: options.keyboardShortcuts, + appendTo: options.appendTo, + autoWidth: options.autoWidth, + ariaLabel: options.ariaLabel || input.getAttribute('aria-label') + }); + $input.data(typeaheadKey, typeahead); + }); + + // expose all methods in the `autocomplete` attribute + inputs.autocomplete = {}; + _.each(['open', 'close', 'getVal', 'setVal', 'destroy', 'getWrapper'], function(method) { + inputs.autocomplete[method] = function() { + var methodArguments = arguments; + var result; + inputs.each(function(j, input) { + var typeahead = zepto(input).data(typeaheadKey); + result = typeahead[method].apply(typeahead, methodArguments); + }); + return result; + }; + }); + + return inputs; +} + +autocomplete.sources = Typeahead.sources; +autocomplete.escapeHighlightedString = _.escapeHighlightedString; + +var wasAutocompleteSet = 'autocomplete' in window; +var oldAutocomplete = window.autocomplete; +autocomplete.noConflict = function noConflict() { + if (wasAutocompleteSet) { + window.autocomplete = oldAutocomplete; + } else { + delete window.autocomplete; + } + return autocomplete; +}; + +module.exports = autocomplete; diff --git a/js/autocomplete.js/test/ci.sh b/js/autocomplete.js/test/ci.sh new file mode 100644 index 0000000..7a3e83e --- /dev/null +++ b/js/autocomplete.js/test/ci.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -e # exit when error, no verbose + +if [ "$TEST_SUITE" == "unit" ]; then + ./node_modules/karma/bin/karma start --single-run +elif [ "$TRAVIS_SECURE_ENV_VARS" == "true" -a "$TEST_SUITE" == "integration" ]; then + static -p 8080 & + sleep 3 && + ./node_modules/mocha/bin/mocha --harmony -R spec ./test/integration/test.js +elif [ "$TEST_SUITE" == "examples" ]; then + yarn docs:netlify +else + echo "Not running any tests" +fi diff --git a/js/autocomplete.js/test/fixtures.js b/js/autocomplete.js/test/fixtures.js new file mode 100644 index 0000000..c663dab --- /dev/null +++ b/js/autocomplete.js/test/fixtures.js @@ -0,0 +1,54 @@ +'use strict'; + +var fixtures = {}; + +fixtures.data = { + simple: [ + {value: 'big'}, + {value: 'bigger'}, + {value: 'biggest'}, + {value: 'small'}, + {value: 'smaller'}, + {value: 'smallest'} + ], + animals: [ + {value: 'dog'}, + {value: 'cat'}, + {value: 'moose'} + ] +}; + +fixtures.html = { + textInput: '', + angularTextInput: '', + input: '', + hint: '', + menu: '', + customMenu: [ + '' + ].join(''), + customMenuContainer: '
', + dataset: [ + '
', + '', + '

one

', + '

two

', + '

three

', + '
', + '
' + ].join('') +}; + +module.exports = fixtures; diff --git a/js/autocomplete.js/test/helpers/mocks.js b/js/autocomplete.js/test/helpers/mocks.js new file mode 100644 index 0000000..62930c6 --- /dev/null +++ b/js/autocomplete.js/test/helpers/mocks.js @@ -0,0 +1,47 @@ +'use strict'; + +var _ = require('../../src/common/utils.js'); +var $ = require('jquery'); +module.exports = function mock(Constructor) { + var constructorSpy; + + Mock.prototype = Constructor.prototype; + constructorSpy = jasmine.createSpy('mock constructor').and.callFake(Mock); + + // copy instance methods + for (var key in Constructor) { + if (typeof Constructor[key] === 'function') { + constructorSpy[key] = Constructor[key]; + } + } + + return constructorSpy; + + function Mock() { + var instance = _.mixin({}, Constructor.prototype); + + for (var key in instance) { + if (typeof instance[key] === 'function') { + spyOn(instance, key); + + // special case for some components + if (key === 'bind') { + instance[key].and.callFake(function() { return this; }); + } + } + } + + // have the event emitter methods call through + instance.onSync && instance.onSync.and.callThrough(); + instance.onAsync && instance.onAsync.and.callThrough(); + instance.off && instance.off.and.callThrough(); + instance.trigger && instance.trigger.and.callThrough(); + + // have some datasets methods call through + instance.getRoot && instance.getRoot.and.callFake(function() { return $(''); }); + + instance.constructor = Constructor; + + return instance; + } +}; diff --git a/js/autocomplete.js/test/helpers/waits_for.js b/js/autocomplete.js/test/helpers/waits_for.js new file mode 100644 index 0000000..668069b --- /dev/null +++ b/js/autocomplete.js/test/helpers/waits_for.js @@ -0,0 +1,24 @@ +'use strict'; + +var waitsForAndRuns = function(escapeFunction, runFunction, escapeTime) { + // check the escapeFunction every millisecond so as soon as it is met we can escape the function + var interval = setInterval(function() { + if (escapeFunction()) { + clearMe(); + runFunction(); + } + }, 1); + // in case we never reach the escapeFunction, we will time out + // at the escapeTime + var timeOut = setTimeout(function() { + clearMe(); + runFunction(); + }, escapeTime); + // clear the interval and the timeout + function clearMe() { + clearInterval(interval); + clearTimeout(timeOut); + } +}; + +module.exports = waitsForAndRuns; diff --git a/js/autocomplete.js/test/integration/test.html b/js/autocomplete.js/test/integration/test.html new file mode 100644 index 0000000..2737a75 --- /dev/null +++ b/js/autocomplete.js/test/integration/test.html @@ -0,0 +1,127 @@ + + + + + + + + + + + + +
+
+
+ + +
+
+
+ + + + diff --git a/js/autocomplete.js/test/integration/test.js b/js/autocomplete.js/test/integration/test.js new file mode 100644 index 0000000..edd2e3d --- /dev/null +++ b/js/autocomplete.js/test/integration/test.js @@ -0,0 +1,427 @@ +'use strict'; + +/* eslint-env jasmine */ + +var wd = require('yiewd'); +var colors = require('colors'); +var expect = require('chai').expect; +var f = require('util').format; +var env = process.env; + +var browser; +var caps; + +browser = (process.env.BROWSER || 'chrome').split(':'); + +caps = { + name: f('[%s] typeahead.js ui', browser.join(' , ')), + browserName: browser[0] +}; + +setIf(caps, 'version', browser[1]); +setIf(caps, 'platform', browser[2]); +setIf(caps, 'tunnel-identifier', env['TRAVIS_JOB_NUMBER']); +setIf(caps, 'build', env['TRAVIS_BUILD_NUMBER']); +setIf(caps, 'tags', env['CI'] ? ['CI'] : ['local']); + +function setIf(obj, key, val) { + val && (obj[key] = val); +} + +describe('jquery-typeahead.js', function() { + var driver; + var body; + var input; + var hint; + var dropdown; + var allPassed = true; + + this.timeout(300000); + + before(function(done) { + var host = 'ondemand.saucelabs.com'; + var port = 80; + var username; + var password; + + if (env['CI']) { + host = 'localhost'; + port = 4445; + username = env['SAUCE_USERNAME']; + password = env['SAUCE_ACCESS_KEY']; + } + + driver = wd.remote(host, port, username, password); + driver.configureHttp({ + timeout: 30000, + retries: 5, + retryDelay: 200 + }); + + driver.on('status', function(info) { + console.log(info.cyan); + }); + + driver.on('command', function(meth, path, data) { + console.log(' > ' + meth.yellow, path.grey, data || ''); + }); + + driver.run(function*() { + yield this.init(caps); + yield this.get((env['TEST_HOST'] || 'http://localhost:8080') + '/test/integration/test.html'); + + body = this.elementByTagName('body'); + input = yield this.elementById('states'); + hint = yield this.elementByClassName('aa-hint'); + dropdown = yield this.elementByClassName('aa-dropdown-menu'); + + done(); + }); + }); + + beforeEach(function(done) { + driver.run(function*() { + yield body.click(); + yield this.execute('$("#states").autocomplete("val", "")'); + done(); + }); + }); + + afterEach(function() { + allPassed = allPassed && (this.currentTest.state === 'passed'); + }); + + after(function(done) { + driver.run(function*() { + yield this.quit(); + yield driver.sauceJobStatus(allPassed); + done(); + }); + }); + + function testSuite () { + describe('on blur', function() { + it('should close dropdown', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + expect(yield dropdown.isDisplayed()).to.equal(true); + + yield body.click(); + expect(yield dropdown.isDisplayed()).to.equal(false); + + done(); + }); + }); + + it('should clear hint', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + expect(yield hint.getValue()).to.equal('michigan'); + + yield body.click(); + expect(yield hint.getValue()).to.equal(''); + + done(); + }); + }); + }); + + describe('on query change', function() { + it('should open dropdown if suggestions', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + + expect(yield dropdown.isDisplayed()).to.equal(true); + + done(); + }); + }); + + it('should position the dropdown correctly', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + + var inputPos = yield input.getLocation(); + var dropdownPos = yield dropdown.getLocation(); + + expect(inputPos.x >= dropdownPos.x - 2 && inputPos.x <= dropdownPos.x + 2).to.equal(true); + expect(dropdownPos.y > inputPos.y).to.equal(true); + + done(); + }); + }); + + it('should close dropdown if no suggestions', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('huh?'); + + expect(yield dropdown.isDisplayed()).to.equal(false); + + done(); + }); + }); + + it('should render suggestions if suggestions', function(done) { + driver.run(function*() { + var suggestions; + + yield input.click(); + yield input.type('mi'); + + suggestions = yield dropdown.elementsByClassName('aa-suggestion'); + + expect(suggestions).to.have.length('4'); + expect(yield suggestions[0].text()).to.equal('Michigan'); + expect(yield suggestions[1].text()).to.equal('Minnesota'); + expect(yield suggestions[2].text()).to.equal('Mississippi'); + expect(yield suggestions[3].text()).to.equal('Missouri'); + + done(); + }); + }); + + it('should show hint if top suggestion is a match', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + + expect(yield hint.getValue()).to.equal('michigan'); + + done(); + }); + }); + + it('should not show hint if top suggestion is not a match', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('ham'); + + expect(yield hint.getValue()).to.equal(''); + + done(); + }); + }); + + it('should not show hint if there is query overflow', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('this is a very long value so deal with it otherwise'); + + expect(yield hint.getValue()).to.equal(''); + + done(); + }); + }); + }); + + describe('on up arrow', function() { + it('should cycle through suggestions', function(done) { + driver.run(function*() { + var suggestions; + + yield input.click(); + yield input.type('mi'); + + suggestions = yield dropdown.elementsByClassName('aa-suggestion'); + + yield input.type(wd.SPECIAL_KEYS['Up arrow']); + expect(yield input.getValue()).to.equal('Missouri'); + expect(yield suggestions[3].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); + + yield input.type(wd.SPECIAL_KEYS['Up arrow']); + expect(yield input.getValue()).to.equal('Mississippi'); + expect(yield suggestions[2].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); + + yield input.type(wd.SPECIAL_KEYS['Up arrow']); + expect(yield input.getValue()).to.equal('Minnesota'); + expect(yield suggestions[1].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); + + yield input.type(wd.SPECIAL_KEYS['Up arrow']); + expect(yield input.getValue()).to.equal('Michigan'); + expect(yield suggestions[0].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); + + yield input.type(wd.SPECIAL_KEYS['Up arrow']); + expect(yield input.getValue()).to.equal('mi'); + expect(yield suggestions[0].getAttribute('class')).to.equal('aa-suggestion'); + expect(yield suggestions[1].getAttribute('class')).to.equal('aa-suggestion'); + expect(yield suggestions[2].getAttribute('class')).to.equal('aa-suggestion'); + expect(yield suggestions[3].getAttribute('class')).to.equal('aa-suggestion'); + + done(); + }); + }); + }); + + describe('on down arrow', function() { + it('should cycle through suggestions', function(done) { + driver.run(function*() { + var suggestions; + + yield input.click(); + yield input.type('mi'); + + suggestions = yield dropdown.elementsByClassName('aa-suggestion'); + + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + expect(yield input.getValue()).to.equal('Michigan'); + expect(yield suggestions[0].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); + + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + expect(yield input.getValue()).to.equal('Minnesota'); + expect(yield suggestions[1].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); + + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + expect(yield input.getValue()).to.equal('Mississippi'); + expect(yield suggestions[2].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); + + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + expect(yield input.getValue()).to.equal('Missouri'); + expect(yield suggestions[3].getAttribute('class')).to.equal('aa-suggestion aa-cursor'); + + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + expect(yield input.getValue()).to.equal('mi'); + expect(yield suggestions[0].getAttribute('class')).to.equal('aa-suggestion'); + expect(yield suggestions[1].getAttribute('class')).to.equal('aa-suggestion'); + expect(yield suggestions[2].getAttribute('class')).to.equal('aa-suggestion'); + expect(yield suggestions[3].getAttribute('class')).to.equal('aa-suggestion'); + + done(); + }); + }); + }); + + describe('on escape', function() { + it('should close dropdown', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + expect(yield dropdown.isDisplayed()).to.equal(true); + + yield input.type(wd.SPECIAL_KEYS['Escape']); + expect(yield dropdown.isDisplayed()).to.equal(false); + + done(); + }); + }); + + it('should clear hint', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + expect(yield hint.getValue()).to.equal('michigan'); + + yield input.type(wd.SPECIAL_KEYS['Escape']); + expect(yield hint.getValue()).to.equal(''); + + done(); + }); + }); + }); + + describe('on tab', function() { + it('should autocomplete if hint is present', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + + yield input.type(wd.SPECIAL_KEYS['Tab']); + expect(yield input.getValue()).to.equal('Michigan'); + + done(); + }); + }); + + it('should select if cursor is on suggestion', function(done) { + driver.run(function*() { + var suggestions; + + yield input.click(); + yield input.type('mi'); + + suggestions = yield dropdown.elementsByClassName('aa-suggestion'); + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + yield input.type(wd.SPECIAL_KEYS['Tab']); + + expect(yield dropdown.isDisplayed()).to.equal(false); + expect(yield input.getValue()).to.equal('Minnesota'); + + done(); + }); + }); + }); + + describe('on right arrow', function() { + it('should autocomplete if hint is present', function(done) { + driver.run(function*() { + yield input.click(); + yield input.type('mi'); + + yield input.type(wd.SPECIAL_KEYS['Right arrow']); + expect(yield input.getValue()).to.equal('Michigan'); + + done(); + }); + }); + }); + + describe('on suggestion click', function() { + it('should select suggestion', function(done) { + if (browser[0] === 'firefox') { + // crazy Firefox issue, skip it + done(); + return; + } + driver.run(function*() { + var suggestions; + + yield input.click(); + yield input.type('mi'); + + suggestions = yield dropdown.elementsByClassName('aa-suggestion'); + yield suggestions[1].click(); + + expect(yield dropdown.isDisplayed()).to.equal(false); + expect(yield input.getValue()).to.equal('Minnesota'); + + done(); + }); + }); + }); + + describe('on enter', function() { + it('should select if cursor is on suggestion', function(done) { + driver.run(function*() { + var suggestions; + + yield input.click(); + yield input.type('mi'); + + suggestions = yield dropdown.elementsByClassName('aa-suggestion'); + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + yield input.type(wd.SPECIAL_KEYS['Down arrow']); + yield input.type(wd.SPECIAL_KEYS['Return']); + + expect(yield dropdown.isDisplayed()).to.equal(false); + expect(yield input.getValue()).to.equal('Minnesota'); + + done(); + }); + }); + }); + } + + testSuite(); + describe('appendTo', function () { + before('all', function*() { + yield this.execute("buildAutocomplete({hint: false, appendTo: 'body'})"); + }); + + testSuite(); + }); +}); diff --git a/js/autocomplete.js/test/playground.css b/js/autocomplete.js/test/playground.css new file mode 100644 index 0000000..f894b72 --- /dev/null +++ b/js/autocomplete.js/test/playground.css @@ -0,0 +1,52 @@ +.my-custom-menu { + width: 400px; +} + +.aa-cursor { + background: #F6624E; + color: white; +} + +.autocomplete-wrapper { + display: block; + margin: 50px 0; +} + +.aa-dropdown-menu { + min-width: 100%; + border: 1px solid #ccc; + background-color: white; + padding: 4px; +} +.aa-suggestion { + padding: 5px 10px; +} +.aa-suggestion em{ + font-style: normal; + font-weight: bold; +} +.aa-info-results { + padding: 4px; + color: #aaa; + font-style: italic; + text-align: right; +} +.aa-category-title{ + display: block; + width: 100%; + padding: 8px 4px 4px; + border-bottom: solid 1px #eee; + font-size: .8em; + color: #F6624E; + font-weight: bold; + text-transform: uppercase; +} +.aa-dropdown-footer{ + padding: 10px 12px 6px 12px; + margin-top: 12px; + border-top: solid 1px #eee; + text-align: right; + color: #aaa; + font-size: 12px; + letter-spacing: 1px; +} diff --git a/js/autocomplete.js/test/playground.html b/js/autocomplete.js/test/playground.html new file mode 100644 index 0000000..0258974 --- /dev/null +++ b/js/autocomplete.js/test/playground.html @@ -0,0 +1,152 @@ + + + + + + + +
+
+
+
+
+

Simple auto-complete

+
+ + + Go +
+
+
+

Multi-sections auto-complete

+
+ + Go +
+
+
+

Simple auto-complete with debounce

+
+ + Go +
+
+
+
+
+
+ + + + + + + diff --git a/js/autocomplete.js/test/playground_angular.html b/js/autocomplete.js/test/playground_angular.html new file mode 100644 index 0000000..0577970 --- /dev/null +++ b/js/autocomplete.js/test/playground_angular.html @@ -0,0 +1,55 @@ + + + + + + + +
+
+
+
+
+

Simple auto-complete

+
+ + Go +
+
+
+
+
+
+ + + + + + + + diff --git a/js/autocomplete.js/test/playground_jquery.html b/js/autocomplete.js/test/playground_jquery.html new file mode 100644 index 0000000..d6ab3ad --- /dev/null +++ b/js/autocomplete.js/test/playground_jquery.html @@ -0,0 +1,207 @@ + + + + + + + +
+
+
+
+
+

Simple auto-complete

+
+ + Go +
+
+
+

Multi-sections auto-complete

+ +
+
+

Tabbed auto-complete

+
+ + Go +
+
+
+
+
+
+ + + + + + + + + + + + + diff --git a/js/autocomplete.js/test/test.bundle.js b/js/autocomplete.js/test/test.bundle.js new file mode 100644 index 0000000..d8786ba --- /dev/null +++ b/js/autocomplete.js/test/test.bundle.js @@ -0,0 +1,5 @@ +'use strict'; + +var context = require.context('.', true, /.+_spec\.js$/); +context.keys().forEach(context); +module.exports = context; diff --git a/js/autocomplete.js/test/unit/angular_spec.js b/js/autocomplete.js/test/unit/angular_spec.js new file mode 100644 index 0000000..7c578e1 --- /dev/null +++ b/js/autocomplete.js/test/unit/angular_spec.js @@ -0,0 +1,64 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +if (typeof Function.prototype.bind != 'function') { + Function.prototype.bind = function bind(obj) { + var args = Array.prototype.slice.call(arguments, 1), + self = this, + nop = function() { + }, + bound = function() { + return self.apply( + this instanceof nop ? this : (obj || {}), args.concat( + Array.prototype.slice.call(arguments) + ) + ); + }; + nop.prototype = this.prototype || {}; + bound.prototype = new nop(); + return bound; + }; +} + +describe('autocomplete directive', function() { + global.jQuery = require('jquery'); + var fixtures = require('../fixtures.js'); + + var angular = require('angular'); + require('../../src/angular/directive.js'); + require('angular-mocks'); + + var scope; + + beforeEach(angular.mock.module('algolia.autocomplete')); + + describe('with scope', function() { + beforeEach(angular.mock.inject(function($rootScope, $compile) { + scope = $rootScope.$new(); + scope.q = ''; + scope.getDatasets = function() { + return []; + }; + })); + + describe('when initialized', function() { + var form; + + beforeEach(function() { + inject(function($compile) { + form = $compile(fixtures.html.angularTextInput)(scope); + }); + scope.$digest(); + }); + + it('should have a parent', function() { + expect(form.parent().length).toEqual(1); + }); + }); + }); + + afterAll(function() { + global.jQuery = undefined; + }); +}); diff --git a/js/autocomplete.js/test/unit/dataset_spec.js b/js/autocomplete.js/test/unit/dataset_spec.js new file mode 100644 index 0000000..1bec373 --- /dev/null +++ b/js/autocomplete.js/test/unit/dataset_spec.js @@ -0,0 +1,545 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + + +describe('Dataset', function() { + require('../../src/common/dom.js').element = require('jquery'); + require('../../src/jquery/plugin.js'); + + var $ = require('jquery'); + require('jasmine-jquery'); + + var Dataset = require('../../src/autocomplete/dataset.js'); + + beforeEach(function() { + this.dataset = new Dataset({ + name: 'test', + source: this.source = jasmine.createSpy('source') + }); + }); + + it('should throw an error if source is missing', function() { + expect(noSource).toThrow(); + + function noSource() { + new Dataset(); + } + }); + + it('should throw an error if the name is not a valid class name', function() { + expect(fn).toThrow(); + + function fn() { + var d = new Dataset({name: 'a space', source: $.noop}); + } + }); + + describe('#getRoot', function() { + it('should return the root element', function() { + expect(this.dataset.getRoot()).toBeMatchedBy('div.aa-dataset-test'); + }); + }); + + describe('#update', function() { + it('should render suggestions', function() { + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + + expect(this.dataset.getRoot()).toContainText('one'); + expect(this.dataset.getRoot()).toContainText('two'); + expect(this.dataset.getRoot()).toContainText('three'); + }); + + it('should allow custom display functions', function() { + this.dataset = new Dataset({ + name: 'test', + display: function(o) { return o.display; }, + source: this.source = jasmine.createSpy('source') + }); + + this.source.and.callFake(fakeGetForDisplayFn); + this.dataset.update('woah'); + + expect(this.dataset.getRoot()).toContainText('4'); + expect(this.dataset.getRoot()).toContainText('5'); + expect(this.dataset.getRoot()).toContainText('6'); + }); + + it('should render empty when no suggestions are available', function() { + this.dataset = new Dataset({ + source: this.source, + templates: { + empty: '

empty

' + } + }); + + this.source.and.callFake(fakeGetWithSyncEmptyResults); + this.dataset.update('woah'); + + expect(this.dataset.getRoot()).toContainText('empty'); + }); + + it('should throw an error if suggestions is not an array', function() { + this.source.and.callFake(fakeGetWithSyncNonArrayResults); + expect(this.dataset.update.bind(this.dataset, 'woah')) + .toThrowError(TypeError, 'suggestions must be an array'); + }); + + it('should set the aa-without class when no suggestions are available', function() { + var $menu = $('
'); + this.dataset = new Dataset({ + $menu: $menu, + source: this.source, + templates: { + empty: '

empty

' + } + }); + + this.source.and.callFake(fakeGetWithSyncEmptyResults); + this.dataset.update('woah'); + + expect($menu).toHaveClass('aa-without-1'); + expect($menu).not.toHaveClass('aa-with-1'); + }); + + it('should set the aa-with class when suggestions are available', function() { + var $menu = $('
'); + this.dataset = new Dataset({ + $menu: $menu, + name: 'fake', + source: this.source, + templates: { + empty: '

empty

' + } + }); + + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + + expect($menu).not.toHaveClass('aa-without-fake'); + expect($menu).toHaveClass('aa-with-fake'); + }); + + it('should allow dataset name=0 and use the provided div', function() { + var $menu = $('
'); + this.dataset = new Dataset({ + $menu: $menu, + name: 0, + source: this.source + }); + expect(this.dataset.$el).toHaveClass('predefined'); + }); + + it('should render isEmpty with extra params', function() { + var spy = jasmine.createSpy('empty with extra params'); + this.dataset = new Dataset({ + source: this.source, + templates: { + empty: spy + } + }); + + this.source.and.callFake(fakeGetWithSyncEmptyResultsAndExtraParams); + this.dataset.update('woah'); + + expect(spy).toHaveBeenCalled(); + expect(spy.calls.argsFor(0).length).toEqual(4); + expect(spy.calls.argsFor(0)[0]).toEqual({query: 'woah', isEmpty: true}); + expect(spy.calls.argsFor(0)[1]).toEqual(42); + expect(spy.calls.argsFor(0)[2]).toEqual(true); + expect(spy.calls.argsFor(0)[3]).toEqual(false); + }); + + it('should render with extra params', function() { + var headerSpy = jasmine.createSpy('header with extra params'); + var footerSpy = jasmine.createSpy('footer with extra params'); + var suggestionSpy = jasmine.createSpy('suggestion with extra params'); + this.dataset = new Dataset({ + source: this.source, + templates: { + header: headerSpy, + footer: footerSpy, + suggestion: suggestionSpy + } + }); + + var suggestions; + fakeGetWithSyncResultsAndExtraParams('woah', function(all) { + suggestions = all; + }); + + this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); + this.dataset.update('woah'); + + expect(headerSpy).toHaveBeenCalled(); + expect(headerSpy.calls.argsFor(0).length).toEqual(4); + expect(headerSpy.calls.argsFor(0)[0]).toEqual({query: 'woah', isEmpty: false}); + expect(headerSpy.calls.argsFor(0)[1]).toEqual(42); + expect(headerSpy.calls.argsFor(0)[2]).toEqual(true); + expect(headerSpy.calls.argsFor(0)[3]).toEqual(false); + + expect(footerSpy).toHaveBeenCalled(); + expect(footerSpy.calls.argsFor(0).length).toEqual(4); + expect(footerSpy.calls.argsFor(0)[0]).toEqual({query: 'woah', isEmpty: false}); + expect(footerSpy.calls.argsFor(0)[1]).toEqual(42); + expect(footerSpy.calls.argsFor(0)[2]).toEqual(true); + expect(footerSpy.calls.argsFor(0)[3]).toEqual(false); + + expect(suggestionSpy).toHaveBeenCalled(); + for (var i = 0; i < 2; ++i) { + expect(suggestionSpy.calls.argsFor(i).length).toEqual(4); + expect(suggestionSpy.calls.argsFor(i)[0]).toEqual(suggestions[i]); + expect(suggestionSpy.calls.argsFor(i)[1]).toEqual(42); + expect(suggestionSpy.calls.argsFor(i)[2]).toEqual(true); + expect(suggestionSpy.calls.argsFor(i)[3]).toEqual(false); + } + }); + + it('should render header', function() { + this.dataset = new Dataset({ + source: this.source, + templates: { + header: '

header

' + } + }); + + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + + expect(this.dataset.getRoot()).toContainText('header'); + }); + + it('should render footer', function() { + this.dataset = new Dataset({ + source: this.source, + templates: { + footer: function(c) { return '

' + c.query + '

'; } + } + }); + + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + + expect(this.dataset.getRoot()).toContainText('woah'); + }); + + it('should not render header/footer if there is no content', function() { + this.dataset = new Dataset({ + source: this.source, + templates: { + header: '

header

', + footer: '

footer

' + } + }); + + this.source.and.callFake(fakeGetWithSyncEmptyResults); + this.dataset.update('woah'); + + expect(this.dataset.getRoot()).not.toContainText('header'); + expect(this.dataset.getRoot()).not.toContainText('footer'); + }); + + it('should not render stale suggestions', function(done) { + this.source.and.callFake(fakeGetWithAsyncResults); + this.dataset.update('woah'); + + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('nelly'); + + var that = this; + setTimeout(function() { + expect(that.dataset.getRoot()).toContainText('one'); + expect(that.dataset.getRoot()).toContainText('two'); + expect(that.dataset.getRoot()).toContainText('three'); + expect(that.dataset.getRoot()).not.toContainText('four'); + expect(that.dataset.getRoot()).not.toContainText('five'); + done(); + }, 100); + }); + + it('should not render suggestions if update was canceled', function(done) { + this.source.and.callFake(fakeGetWithAsyncResults); + this.dataset.update('woah'); + this.dataset.cancel(); + + var that = this; + setTimeout(function() { + expect(that.dataset.getRoot()).toBeEmpty(); + done(); + }, 100); + }); + + it('should trigger rendered after suggestions are rendered', function(done) { + var spy; + + this.dataset.onSync('rendered', spy = jasmine.createSpy()); + + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + + setTimeout(function() { + expect(spy.calls.count()).toBe(1); + done(); + }, 100); + }); + + it('should cache latest query, suggestions and extra render arguments', function() { + this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); + this.dataset.update('woah'); + + expect(this.dataset.cachedQuery).toEqual('woah'); + expect(this.dataset.cachedSuggestions).toEqual([ + {value: 'one', raw: {value: 'one'}}, + {value: 'two', raw: {value: 'two'}}, + {value: 'three', raw: {value: 'three'}} + ]); + expect(this.dataset.cachedRenderExtraArgs).toEqual([42, true, false]); + }); + + it('should retrieve cached results for subsequent identical queries', function() { + this.source.and.callFake(fakeGetWithSyncResults); + + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(1); + expect(this.dataset.getRoot()).toContainText('one'); + expect(this.dataset.getRoot()).toContainText('two'); + expect(this.dataset.getRoot()).toContainText('three'); + + this.dataset.clear(); + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(1); + expect(this.dataset.getRoot()).toContainText('one'); + expect(this.dataset.getRoot()).toContainText('two'); + expect(this.dataset.getRoot()).toContainText('three'); + }); + + it('should not retrieve cached results for subsequent identical queries if cache is disabled', function() { + this.dataset = new Dataset({ + name: 'test', + source: this.source = jasmine.createSpy('source'), + cache: false, + }); + + this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); + + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(1); + expect(this.dataset.getRoot()).toContainText('one'); + expect(this.dataset.getRoot()).toContainText('two'); + expect(this.dataset.getRoot()).toContainText('three'); + + this.dataset.clear(); + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(2); + expect(this.dataset.getRoot()).toContainText('one'); + expect(this.dataset.getRoot()).toContainText('two'); + expect(this.dataset.getRoot()).toContainText('three'); + }); + + it('should reuse render function extra params for subsequent identical queries', function() { + var spy = spyOn(this.dataset, '_render'); + this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); + + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(1); + expect(spy.calls.argsFor(0)).toEqual([ + 'woah', [ + {value: 'one', raw: {value: 'one'}}, + {value: 'two', raw: {value: 'two'}}, + {value: 'three', raw: {value: 'three'}} + ], 42, true, false]); + + this.dataset.clear(); + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(1); + expect(spy.calls.argsFor(1)).toEqual([ + 'woah', [ + {value: 'one', raw: {value: 'one'}}, + {value: 'two', raw: {value: 'two'}}, + {value: 'three', raw: {value: 'three'}} + ], 42, true, false]); + }); + + it('should not retrieved cached results for subsequent different queries', function() { + this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); + + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(1); + + this.dataset.clear(); + this.dataset.update('woah 2'); + expect(this.source.calls.count()).toBe(2); + }); + + it('should wait before calling the source if debounce is provided', function(done) { + var that = this; + + this.dataset = new Dataset({ + source: this.source, + debounce: 250 + }); + + this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); + + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(0); + + this.dataset.update('woah 2'); + expect(this.source.calls.count()).toBe(0); + + setTimeout(function() { + expect(that.source.calls.count()).toBe(1); + done(); + }, 500); + }); + + it('should not call the source if update was canceled', function(done) { + var that = this; + + this.dataset = new Dataset({ + source: this.source, + debounce: 250 + }); + + this.source.and.callFake(fakeGetWithSyncResultsAndExtraParams); + + this.dataset.update('woah'); + expect(this.source.calls.count()).toBe(0); + + this.dataset.update('woah 2'); + expect(this.source.calls.count()).toBe(0); + + this.dataset.clear(); + expect(this.source.calls.count()).toBe(0); + + setTimeout(function() { + expect(that.source.calls.count()).toBe(0); + done(); + }, 500); + }); + }); + + describe('#cacheSuggestions', function() { + it('should assign cachedQuery, cachedSuggestions and cachedRenderArgs properties', function() { + this.dataset.cacheSuggestions('woah', ['one', 'two'], 42); + expect(this.dataset.cachedQuery).toEqual('woah'); + expect(this.dataset.cachedSuggestions).toEqual(['one', 'two']); + expect(this.dataset.cachedRenderExtraArgs).toEqual(42); + }); + }); + + describe('#clearCachedSuggestions', function() { + it('should delete cachedQuery and cachedSuggestions properties', function() { + this.dataset.cachedQuery = 'one'; + this.dataset.cachedSuggestions = ['one', 'two']; + this.dataset.cachedRenderExtraArgs = 42; + + this.dataset.clearCachedSuggestions(); + + expect(this.dataset.cachedQuery).toBeUndefined(); + expect(this.dataset.cachedSuggestions).toBeUndefined(); + expect(this.dataset.cachedRenderExtraArgs).toBeUndefined(); + }); + }); + + describe('#clear', function() { + it('should clear suggestions', function() { + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + + this.dataset.clear(); + expect(this.dataset.getRoot()).toBeEmpty(); + }); + + it('should cancel pending updates', function() { + var spy = spyOn(this.dataset, 'cancel'); + + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + expect(this.dataset.canceled).toBe(false); + + this.dataset.clear(); + expect(spy).toHaveBeenCalled(); + }); + + it('should not throw if called right after destroy()', function() { + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + this.dataset.destroy(); + this.dataset.clear(); + expect(this.dataset.getRoot()).toBeNull(); + }); + }); + + describe('#isEmpty', function() { + it('should return true when empty', function() { + expect(this.dataset.isEmpty()).toBe(true); + }); + + it('should return false when not empty', function() { + this.source.and.callFake(fakeGetWithSyncResults); + this.dataset.update('woah'); + + expect(this.dataset.isEmpty()).toBe(false); + }); + }); + + describe('#destroy', function() { + it('should null out the reference to the dataset element', function() { + this.dataset.destroy(); + + expect(this.dataset.$el).toBeNull(); + }); + + it('should clear suggestion cache', function() { + var spy = spyOn(this.dataset, 'clearCachedSuggestions'); + this.dataset.destroy(); + expect(spy).toHaveBeenCalled(); + }); + }); + + // helper functions + // ---------------- + + function fakeGetWithSyncResults(query, cb) { + cb([ + {value: 'one', raw: {value: 'one'}}, + {value: 'two', raw: {value: 'two'}}, + {value: 'three', raw: {value: 'three'}} + ]); + } + + function fakeGetForDisplayFn(query, cb) { + cb([{display: '4'}, {display: '5'}, {display: '6'}]); + } + + function fakeGetWithSyncNonArrayResults(query, cb) { + cb({}); + } + + function fakeGetWithSyncEmptyResults(query, cb) { + cb(); + } + + function fakeGetWithSyncEmptyResultsAndExtraParams(query, cb) { + cb([], 42, true, false); + } + + function fakeGetWithSyncResultsAndExtraParams(query, cb) { + cb([ + {value: 'one', raw: {value: 'one'}}, + {value: 'two', raw: {value: 'two'}}, + {value: 'three', raw: {value: 'three'}} + ], 42, true, false); + } + + function fakeGetWithAsyncResults(query, cb) { + setTimeout(function() { + cb([ + {value: 'four', raw: {value: 'four'}}, + {value: 'five', raw: {value: 'five'}} + ]); + }, 0); + } +}); diff --git a/js/autocomplete.js/test/unit/dropdown_spec.js b/js/autocomplete.js/test/unit/dropdown_spec.js new file mode 100644 index 0000000..0933425 --- /dev/null +++ b/js/autocomplete.js/test/unit/dropdown_spec.js @@ -0,0 +1,494 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +describe('Dropdown', function() { + require('../../src/common/dom.js').element = require('jquery'); + require('../../src/jquery/plugin.js'); + + var $ = require('jquery'); + require('jasmine-jquery'); + var Dropdown = require('../../src/autocomplete/dropdown.js'); + var fixtures = require('../fixtures.js'); + var mocks = require('../helpers/mocks.js'); + + Dropdown.Dataset = mocks(Dropdown.Dataset); + + + beforeEach(function() { + var $fixture; + + setFixtures(fixtures.html.menu); + + $fixture = $('#jasmine-fixtures'); + this.$menu = $fixture.find('.aa-dropdown-menu'); + this.$menu.html(fixtures.html.dataset); + + this.view = new Dropdown({menu: this.$menu, datasets: [{}]}); + this.dataset = this.view.datasets[0]; + }); + + it('should throw an error if menu and/or datasets is missing', function() { + expect(noMenu).toThrow(); + expect(noDatasets).toThrow(); + + function noMenu() { + new Dropdown({menu: '.menu'}); + } + + function noDatasets() { + new Dropdown({datasets: true}); + } + }); + + describe('when click event is triggered on a suggestion', function() { + it('should trigger suggestionClicked', function() { + var spy; + + this.view.onSync('suggestionClicked', spy = jasmine.createSpy()); + + this.$menu.find('.aa-suggestion').first().click(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when instantiated with a custom header or footer', function() { + beforeEach(function() { + this.view.destroy(); + this.view = new Dropdown({ + menu: this.$menu, + datasets: [{}], + templates: { + header: function() { return '

Header

'; }, + footer: '' + } + }); + }); + + it('should include the header', function() { + expect(this.$menu.find('h2.header').length).toEqual(1); + }); + + it('should include the footer', function() { + expect(this.$menu.find('h2.footer').length).toEqual(1); + }); + }); + + describe('when instantiated with a custom empty template', function() { + beforeEach(function() { + this.view.destroy(); + this.view = new Dropdown({ + menu: this.$menu, + datasets: [{}], + templates: { + empty: '

this is empty

' + }, + minLength: 4 + }); + }); + + it('should include the empty', function() { + expect(this.$menu.find('.aa-empty').length).toEqual(1); + expect(this.$menu.find('.aa-empty').children().length).toEqual(0); + expect(this.$menu.find('.aa-empty').css('display')).toEqual('none'); + }); + + it('should not hide the dropdown if empty', function() { + this.view.datasets[0].isEmpty.and.returnValue(true); + + this.view.open(); + this.view._onRendered('rendered', 'a query'); + + expect(this.$menu.find('.aa-empty').length).toEqual(1); + expect(this.$menu.find('.aa-empty').children().length).toEqual(1); + expect(this.$menu.find('.aa-empty').find('h3.empty').length).toEqual(1); + expect(this.$menu.find('.aa-empty').css('display')).not.toEqual('none'); + }); + + it('should trigger empty', function() { + var spy; + + this.view.datasets[0].isEmpty.and.returnValue(true); + this.view.onSync('empty', spy = jasmine.createSpy()); + + this.view.open(); + this.view._onRendered('rendered', 'a query'); + + expect(spy).toHaveBeenCalled(); + }); + + it('should not trigger empty if minLength is no satisfied', function() { + var spy; + + this.view.datasets[0].isEmpty.and.returnValue(true); + this.view.onSync('empty', spy = jasmine.createSpy()); + + this.view.open(); + this.view._onRendered('rendered', 'te'); + + expect(spy).not.toHaveBeenCalled(); + expect(this.$menu.find('.aa-empty').css('display')).toEqual('none'); + }); + }); + + describe('when mouseenter is triggered on a suggestion', function() { + it('should remove pre-existing cursor', function(done) { + var $first; + var $last; + + $first = this.$menu.find('.aa-suggestion').first(); + $last = this.$menu.find('.aa-suggestion').last(); + + $first.addClass('aa-cursor'); + $last.mouseenter(); + + // see implementation on why we need this setTimeout + setTimeout(function() { + expect($first).not.toHaveClass('aa-cursor'); + expect($last).toHaveClass('aa-cursor'); + done(); + }, 0); + }); + + it('should set the cursor', function(done) { + var $suggestion; + + $suggestion = this.$menu.find('.aa-suggestion').first(); + $suggestion.mouseenter(); + + // see implementation on why we need this setTimeout + setTimeout(function() { + expect($suggestion).toHaveClass('aa-cursor'); + done(); + }, 0); + }); + + it('should trigger cursorMoved', function(done) { + var spy; + var $suggestion; + + this.view.onSync('cursorMoved', spy = jasmine.createSpy()); + + $suggestion = this.$menu.find('.aa-suggestion').first(); + $suggestion.mouseenter(); + + setTimeout(function() { + expect(spy).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('when mouseleave is triggered on a suggestion', function() { + it('should remove the cursor', function() { + var $suggestion; + + $suggestion = this.$menu.find('.aa-suggestion').first(); + $suggestion.mouseenter().mouseleave(); + + expect($suggestion).not.toHaveClass('aa-cursor'); + }); + }); + + describe('when rendered is triggered on a dataset', function() { + it('should hide the dropdown if empty', function() { + this.dataset.isEmpty.and.returnValue(true); + + this.view.open(); + this.view._show(); + this.dataset.trigger('rendered', 'the query'); + + expect(this.$menu).not.toBeVisible(); + }); + + it('should show the dropdown if not empty', function() { + this.dataset.isEmpty.and.returnValue(false); + + this.view.open(); + this.dataset.trigger('rendered', 'the query'); + + expect(this.$menu).toBeVisible(); + }); + + it('should trigger datasetRendered', function() { + var spy; + + this.view.onSync('datasetRendered', spy = jasmine.createSpy()); + this.dataset.trigger('rendered'); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('#open', function() { + it('should display the menu if not empty', function() { + this.view.isOpen = true; + this.view.close(); + expect(this.$menu).not.toBeVisible(); + + this.view.isEmpty = false; + this.view.open(); + + expect(this.$menu).toBeVisible(); + }); + + it('should not display the menu if empty', function() { + this.view.isOpen = true; + this.view.close(); + expect(this.$menu).not.toBeVisible(); + + this.view.isEmpty = true; + this.view.open(); + + expect(this.$menu).not.toBeVisible(); + }); + + it('should trigger opened', function() { + var spy; + + this.view.onSync('opened', spy = jasmine.createSpy()); + + this.view.close(); + this.view.open(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should trigger shown', function() { + var spy; + + this.view.onSync('shown', spy = jasmine.createSpy()); + + this.view.close(); + this.view.isEmpty = false; + this.view.open(); + + expect(spy).toHaveBeenCalled(); + }); + + + it('should trigger redrawn', function() { + var spy; + + this.view.onSync('redrawn', spy = jasmine.createSpy()); + + this.view.close(); + this.view.isEmpty = false; + this.view.appendTo = 'body'; + this.view.open(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('#close', function() { + it('should hide the menu', function() { + this.view.open(); + this.view.close(); + + expect(this.$menu).not.toBeVisible(); + }); + + it('should trigger closed', function() { + var spy; + + this.view.onSync('closed', spy = jasmine.createSpy()); + + this.view.open(); + this.view.close(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('#setLanguageDirection', function() { + it('should update css for given language direction', function() { + // TODO: eh, the toHaveCss matcher doesn't seem to work very well + /* + this.view.setLanguageDirection('rtl'); + expect(this.$menu).toHaveCss({ left: 'auto', right: '0px' }); + + this.view.setLanguageDirection('ltr'); + expect(this.$menu).toHaveCss({ left: '0px', right: 'auto' }); + */ + }); + }); + + describe('#moveCursorUp', function() { + beforeEach(function() { + this.view.open(); + }); + + it('should move the cursor up', function() { + var $first; + var $second; + + $first = this.view._getSuggestions().eq(0); + $second = this.view._getSuggestions().eq(1); + + this.view._setCursor($second); + this.view.moveCursorUp(); + expect(this.view._getCursor()).toContainText($first.text()); + }); + + it('should move cursor to bottom if cursor is not present', function() { + var $bottom; + + $bottom = this.view._getSuggestions().eq(-1); + + this.view.moveCursorUp(); + expect(this.view._getCursor()).toContainText($bottom.text()); + }); + + it('should remove cursor if already at top', function() { + var $first; + + $first = this.view._getSuggestions().eq(0); + + this.view._setCursor($first); + this.view.moveCursorUp(); + expect(this.view._getCursor().length).toBe(0); + }); + }); + + describe('#moveCursorDown', function() { + beforeEach(function() { + this.view.open(); + }); + + it('should move the cursor down', function() { + var $first; + var $second; + + $first = this.view._getSuggestions().eq(0); + $second = this.view._getSuggestions().eq(1); + + this.view._setCursor($first); + this.view.moveCursorDown(); + expect(this.view._getCursor()).toContainText($second.text()); + }); + + it('should move cursor to top if cursor is not present', function() { + var $first; + + $first = this.view._getSuggestions().eq(0); + + this.view.moveCursorDown(); + expect(this.view._getCursor()).toContainText($first.text()); + }); + + it('should remove cursor if already at bottom', function() { + var $bottom; + + $bottom = this.view._getSuggestions().eq(-1); + + this.view._setCursor($bottom); + this.view.moveCursorDown(); + expect(this.view._getCursor().length).toBe(0); + }); + }); + + describe('#getDatumForSuggestion', function() { + it('should extract the datum from the suggestion element', function() { + var $suggestion; + var datum; + + $suggestion = $('
').data({aaValue: 'one', aaDatum: JSON.stringify('two')}); + datum = this.view.getDatumForSuggestion($suggestion); + + expect(datum).toEqual({value: 'one', raw: 'two', datasetName: undefined}); + }); + + it('should return null if no element is given', function() { + expect(this.view.getDatumForSuggestion($('notreal'))).toBeNull(); + }); + }); + + describe('#getDatumForCursor', function() { + it('should return the datum for the cursor', function() { + var $first; + + $first = this.view._getSuggestions().eq(0); + $first.data({aaValue: 'one', aaDatum: JSON.stringify('two')}); + + this.view._setCursor($first); + expect(this.view.getDatumForCursor()) + .toEqual({value: 'one', raw: 'two', datasetName: undefined}); + }); + }); + + describe('#getDatumForTopSuggestion', function() { + it('should return the datum for top suggestion', function() { + var $first; + + $first = this.view._getSuggestions().eq(0); + $first.data({aaValue: 'one', aaDatum: JSON.stringify('two')}); + + expect(this.view.getDatumForTopSuggestion()) + .toEqual({value: 'one', raw: 'two', datasetName: undefined}); + }); + }); + + describe('#update', function() { + it('should invoke update on each dataset', function() { + this.view.update(); + expect(this.dataset.update).toHaveBeenCalled(); + }); + }); + + describe('#empty', function() { + it('should invoke clear on each dataset', function() { + this.view.empty(); + expect(this.dataset.clear).toHaveBeenCalled(); + }); + }); + + describe('#isVisible', function() { + it('should return true if open and not empty', function() { + this.view.isOpen = true; + this.view.isEmpty = false; + + expect(this.view.isVisible()).toBe(true); + + this.view.isOpen = false; + this.view.isEmpty = false; + + expect(this.view.isVisible()).toBe(false); + + this.view.isOpen = true; + this.view.isEmpty = true; + + expect(this.view.isVisible()).toBe(false); + + this.view.isOpen = false; + this.view.isEmpty = false; + + expect(this.view.isVisible()).toBe(false); + }); + }); + + describe('#destroy', function() { + it('should remove event handlers', function() { + var $menu = this.view.$menu; + + spyOn($menu, 'off'); + + this.view.destroy(); + + expect($menu.off).toHaveBeenCalledWith('.aa'); + }); + + it('should destroy its datasets', function() { + this.view.destroy(); + + expect(this.dataset.destroy).toHaveBeenCalled(); + }); + + it('should null out its reference to the menu element', function() { + this.view.destroy(); + + expect(this.view.$menu).toBeNull(); + }); + }); +}); diff --git a/js/autocomplete.js/test/unit/event_emitter_spec.js b/js/autocomplete.js/test/unit/event_emitter_spec.js new file mode 100644 index 0000000..bcaca98 --- /dev/null +++ b/js/autocomplete.js/test/unit/event_emitter_spec.js @@ -0,0 +1,134 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +describe('EventEmitter', function() { + require('../../src/common/dom.js').element = require('jquery'); + require('../../src/jquery/plugin.js'); + + var EventEmitter = require('../../src/autocomplete/event_emitter.js'); + var _ = require('../../src/common/utils.js'); + var waitsForAndRuns = require('../helpers/waits_for.js'); + + beforeEach(function() { + this.spy = jasmine.createSpy(); + this.target = _.mixin({}, EventEmitter); + }); + + it('methods should be chainable', function() { + expect(this.target.onSync()).toEqual(this.target); + expect(this.target.onAsync()).toEqual(this.target); + expect(this.target.off()).toEqual(this.target); + expect(this.target.trigger()).toEqual(this.target); + }); + + it('#on should take the context a callback should be called in', function(done) { + var context = {val: 3}; + var cbContext; + + this.target.onSync('xevent', setCbContext, context).trigger('xevent'); + + waitsForAndRuns(assertCbContext, done, 100); + + function setCbContext() { + cbContext = this; + } + + function assertCbContext() { + return cbContext === context; + } + }); + + it('#onAsync callbacks should be invoked asynchronously', function(done) { + this.target.onAsync('event', this.spy).trigger('event'); + + expect(this.spy.calls.count()).toBe(0); + waitsForAndRuns(assertCallCount(this.spy, 1), done, 100); + }); + + it('#onSync callbacks should be invoked synchronously', function() { + this.target.onSync('event', this.spy).trigger('event'); + + expect(this.spy.calls.count()).toBe(1); + }); + + it('#off should remove callbacks', function(done) { + this.target + .onSync('event1 event2', this.spy) + .onAsync('event1 event2', this.spy) + .off('event1 event2') + .trigger('event1 event2'); + + setTimeout(assertCallCount(this.spy, 0, done), 100); + }); + + it('methods should accept multiple event types', function(done) { + this.target + .onSync('event1 event2', this.spy) + .onAsync('event1 event2', this.spy) + .trigger('event1 event2'); + + expect(this.spy.calls.count()).toBe(2); + setTimeout(assertCallCount(this.spy, 4, done), 100); + }); + + it('the event type should be passed to the callback', function(done) { + this.target + .onSync('sync', this.spy) + .onAsync('async', this.spy) + .trigger('sync async'); + + var that = this; + waitsForAndRuns(assertArgs(this.spy, 0, ['sync']), function() { + waitsForAndRuns(assertArgs(that.spy, 1, ['async']), done, 100); + }, 100); + }); + + it('arbitrary args should be passed to the callback', function(done) { + this.target + .onSync('event', this.spy) + .onAsync('event', this.spy) + .trigger('event', 1, 2); + + var that = this; + waitsForAndRuns(assertArgs(this.spy, 0, ['event', 1, 2]), function() { + waitsForAndRuns(assertArgs(that.spy, 1, ['event', 1, 2]), done, 100); + }, 100); + }); + + it('callback execution should be cancellable', function(done) { + var cancelSpy = jasmine.createSpy().and.callFake(cancel); + + this.target + .onSync('one', cancelSpy) + .onSync('one', this.spy) + .onAsync('two', cancelSpy) + .onAsync('two', this.spy) + .onSync('three', cancelSpy) + .onAsync('three', this.spy) + .trigger('one two three'); + + var that = this; + setTimeout(assertCallCount(cancelSpy, 3, function() { + setTimeout(assertCallCount(that.spy, 0, done), 100); + }), 100); + + function cancel() { + return false; + } + }); + + function assertCallCount(spy, expected, done) { + return function() { + expect(spy.calls.count()).toBe(expected); + done && done(); + }; + } + + function assertArgs(spy, call, expected) { + return function() { + var actual = spy.calls.argsFor(call); + return expect(actual).toEqual(expected); + }; + } +}); diff --git a/js/autocomplete.js/test/unit/input_spec.js b/js/autocomplete.js/test/unit/input_spec.js new file mode 100644 index 0000000..e097e72 --- /dev/null +++ b/js/autocomplete.js/test/unit/input_spec.js @@ -0,0 +1,495 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +describe('Input', function() { + var $ = require('jquery'); + require('jasmine-jquery'); + require('../../src/common/dom.js').element = $; + require('../../src/jquery/plugin.js'); + var Input = require('../../src/autocomplete/input.js'); + var _ = require('../../src/common/utils.js'); + var fixtures = require('../fixtures.js'); + var waitsForAndRuns = require('../helpers/waits_for.js'); + + var KEYS; + + KEYS = { + enter: 13, + esc: 27, + tab: 9, + left: 37, + right: 39, + up: 38, + down: 40, + normal: 65 // "A" key + }; + + beforeEach(function() { + var $fixture; + + setFixtures(fixtures.html.input + fixtures.html.hint); + + $fixture = $('#jasmine-fixtures'); + this.$input = $fixture.find('.aa-input'); + this.$hint = $fixture.find('.aa-hint'); + + this.view = new Input({input: this.$input, hint: this.$hint}); + }); + + it('should throw an error if no hint and/or input is provided', function() { + expect(noInput).toThrow(); + + function noInput() { + new Input({hint: '.hint'}); + } + }); + + describe('when the blur DOM event is triggered', function() { + it('should reset the input value', function() { + this.view.setQuery('wine'); + this.view.setInputValue('cheese', true); + + this.$input.blur(); + + expect(this.$input.val()).toBe('wine'); + }); + + it('should trigger blurred', function() { + var spy; + + this.view.onSync('blurred', spy = jasmine.createSpy()); + this.$input.blur(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when the focus DOM event is triggered', function() { + it('should trigger focused', function() { + var spy; + + this.view.onSync('focused', spy = jasmine.createSpy()); + this.$input.focus(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when the keydown DOM event is triggered by tab', function() { + it('should trigger tabKeyed if no modifiers were pressed', function() { + var spy; + + this.view.onSync('tabKeyed', spy = jasmine.createSpy()); + simulateKeyEvent(this.$input, 'keydown', KEYS.tab); + + expect(spy).toHaveBeenCalled(); + }); + + it('should not trigger tabKeyed if modifiers were pressed', function() { + var spy; + + this.view.onSync('tabKeyed', spy = jasmine.createSpy()); + simulateKeyEvent(this.$input, 'keydown', KEYS.tab, true); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should prevent default behavior if there is a hint', function() { + var $e; + + this.view.setHint('good'); + this.view.setInputValue('goo'); + + $e = simulateKeyEvent(this.$input, 'keydown', KEYS.tab); + + expect($e.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('when the keydown DOM event is triggered by esc', function() { + it('should trigger escKeyed', function() { + var spy; + + this.view.onSync('escKeyed', spy = jasmine.createSpy()); + simulateKeyEvent(this.$input, 'keydown', KEYS.esc); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when the keydown DOM event is triggered by left', function() { + it('should trigger leftKeyed', function() { + var spy; + + this.view.onSync('leftKeyed', spy = jasmine.createSpy()); + simulateKeyEvent(this.$input, 'keydown', KEYS.left); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when the keydown DOM event is triggered by right', function() { + it('should trigger rightKeyed', function() { + var spy; + + this.view.onSync('rightKeyed', spy = jasmine.createSpy()); + simulateKeyEvent(this.$input, 'keydown', KEYS.right); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when the keydown DOM event is triggered by enter', function() { + it('should trigger enterKeyed', function() { + var spy; + + this.view.onSync('enterKeyed', spy = jasmine.createSpy()); + simulateKeyEvent(this.$input, 'keydown', KEYS.enter); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when the keydown DOM event is triggered by up', function() { + it('should trigger upKeyed', function() { + var spy; + + this.view.onSync('upKeyed', spy = jasmine.createSpy()); + simulateKeyEvent(this.$input, 'keydown', KEYS.up); + + expect(spy).toHaveBeenCalled(); + }); + + it('should prevent default if no modifers were pressed', function() { + var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.up); + + expect($e.preventDefault).toHaveBeenCalled(); + }); + + it('should not prevent default if modifers were pressed', function() { + var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.up, true); + + expect($e.preventDefault).not.toHaveBeenCalled(); + }); + }); + + describe('when the keydown DOM event is triggered by down', function() { + it('should trigger downKeyed', function() { + var spy; + + this.view.onSync('downKeyed', spy = jasmine.createSpy()); + simulateKeyEvent(this.$input, 'keydown', KEYS.down); + + expect(spy).toHaveBeenCalled(); + }); + + it('should prevent default if no modifers were pressed', function() { + var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.down); + + expect($e.preventDefault).toHaveBeenCalled(); + }); + + it('should not prevent default if modifers were pressed', function() { + var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.down, true); + + expect($e.preventDefault).not.toHaveBeenCalled(); + }); + }); + + // NOTE: have to treat these as async because the ie polyfill acts + // in a async manner + describe('when the input DOM event is triggered', function() { + it('should update query', function(done) { + this.view.setQuery('wine'); + this.view.setInputValue('cheese', true); + + simulateInputEvent(this.$input); + + var that = this; + waitsForAndRuns(function() { return that.view.getQuery() === 'cheese'; }, done, 100); + }); + + it('should trigger queryChanged if the query changed', function() { + var spy; + + this.view.setQuery('wine'); + this.view.setInputValue('cheese', true); + this.view.onSync('queryChanged', spy = jasmine.createSpy()); + + simulateInputEvent(this.$input); + + expect(spy).toHaveBeenCalled(); + }); + + it('should trigger whitespaceChagned if whitespace changed', function() { + var spy; + + this.view.setQuery('wine bar'); + this.view.setInputValue('wine bar', true); + this.view.onSync('whitespaceChanged', spy = jasmine.createSpy()); + + simulateInputEvent(this.$input); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('#focus', function() { + it('should focus the input', function() { + this.$input.blur(); + this.view.focus(); + + expect(this.$input).toBeFocused(); + }); + }); + + describe('#blur', function() { + it('should blur the input', function() { + this.$input.focus(); + this.view.blur(); + + expect(this.$input).not.toBeFocused(); + }); + }); + + describe('#getQuery/#setQuery', function() { + it('should act as getter/setter to the query property', function() { + this.view.setQuery('mouse'); + expect(this.view.getQuery()).toBe('mouse'); + }); + }); + + describe('#getInputValue', function() { + it('should act as getter to the input value', function() { + this.$input.val('cheese'); + expect(this.view.getInputValue()).toBe('cheese'); + }); + }); + + describe('#setInputValue', function() { + it('should act as setter to the input value', function() { + this.view.setInputValue('cheese'); + expect(this.view.getInputValue()).toBe('cheese'); + }); + + it('should not set the current query if null', function() { + this.view.setQuery('cheese'); + this.view.setInputValue(null); + expect(this.view.getInputValue()).toBe(''); + }); + + it('should set the current query if undefined', function() { + this.view.setQuery('cheese'); + this.view.setInputValue(undefined); + expect(this.view.getInputValue()).toBe('cheese'); + }); + + it('should trigger {query|whitespace}Changed when applicable', function() { + var spy1; + var spy2; + + this.view.onSync('queryChanged', spy1 = jasmine.createSpy()); + this.view.onSync('whitespaceChanged', spy2 = jasmine.createSpy()); + + this.view.setInputValue('cheese head'); + expect(spy1).toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + + this.view.setInputValue('cheese head'); + expect(spy1.calls.count()).toBe(1); + expect(spy2).toHaveBeenCalled(); + }); + }); + + describe('#setActiveDescendant', function() { + it('should set the aria-activedescendant attribute', function() { + this.view.setActiveDescendant('abc'); + expect(this.$input.attr('aria-activedescendant')).toBe('abc'); + }); + }); + + describe('#removeActiveDescendant', function() { + it('should remove the aria-activedescendant attribute', function() { + this.view.setActiveDescendant('foo'); + expect(this.$input.attr('aria-activedescendant')).toBe('foo'); + this.view.removeActiveDescendant('bar'); + expect(this.$input.attr('aria-activedescendant')).toBeUndefined(); + }); + }); + + describe('#getHint/#setHint', function() { + it('should act as getter/setter to value of hint', function() { + this.view.setHint('mountain'); + expect(this.view.getHint()).toBe('mountain'); + }); + }); + + describe('#resetInputValue', function() { + it('should reset input value to last query', function() { + this.view.setQuery('cheese'); + this.view.setInputValue('wine', true); + + this.view.resetInputValue(); + expect(this.view.getInputValue()).toBe('cheese'); + }); + }); + + describe('#clearHint', function() { + it('should set the hint value to the empty string', function() { + this.view.setHint('cheese'); + this.view.clearHint(); + + expect(this.view.getHint()).toBe(''); + }); + }); + + describe('#clearHintIfInvalid', function() { + it('should clear hint if input value is empty string', function() { + this.view.setInputValue('', true); + this.view.setHint('cheese'); + this.view.clearHintIfInvalid(); + + expect(this.view.getHint()).toBe(''); + }); + + it('should clear hint if input value is not prefix of input', function() { + this.view.setInputValue('milk', true); + this.view.setHint('cheese'); + this.view.clearHintIfInvalid(); + + expect(this.view.getHint()).toBe(''); + }); + + it('should clear hint if overflow exists', function() { + spyOn(this.view, 'hasOverflow').and.returnValue(true); + this.view.setInputValue('che', true); + this.view.setHint('cheese'); + this.view.clearHintIfInvalid(); + + expect(this.view.getHint()).toBe(''); + }); + + it('should not clear hint if input value is prefix of input', function() { + this.view.setInputValue('che', true); + this.view.setHint('cheese'); + this.view.clearHintIfInvalid(); + + expect(this.view.getHint()).toBe('cheese'); + }); + }); + + describe('#getLanguageDirection', function() { + it('should return the language direction of the input', function() { + this.$input.css('direction', 'ltr'); + expect(this.view.getLanguageDirection()).toBe('ltr'); + + this.$input.css('direction', 'rtl'); + expect(this.view.getLanguageDirection()).toBe('rtl'); + }); + }); + + describe('#hasOverflow', function() { + it('should return true if the input has overflow text', function() { + var longStr = new Array(1000).join('a'); + + this.view.setInputValue(longStr); + expect(this.view.hasOverflow()).toBe(true); + }); + + it('should return false if the input has no overflow text', function() { + var shortStr = 'aah'; + + this.view.setInputValue(shortStr); + expect(this.view.hasOverflow()).toBe(false); + }); + }); + + describe('#isCursorAtEnd', function() { + it('should return true if the text cursor is at the end', function() { + this.view.setInputValue('boo'); + + setCursorPosition(this.$input, 3); + expect(this.view.isCursorAtEnd()).toBe(true); + }); + + it('should return false if the text cursor is not at the end', function() { + this.view.setInputValue('boo'); + + setCursorPosition(this.$input, 1); + expect(this.view.isCursorAtEnd()).toBe(false); + }); + }); + + describe('#destroy', function() { + it('should remove event handlers', function() { + var $input; + var $hint; + + $hint = this.view.$hint; + $input = this.view.$input; + + spyOn($hint, 'off'); + spyOn($input, 'off'); + + this.view.destroy(); + + expect($hint.off).toHaveBeenCalledWith('.aa'); + expect($input.off).toHaveBeenCalledWith('.aa'); + }); + + it('should null out its reference to DOM elements', function() { + this.view.destroy(); + + expect(this.view.$hint).toBeNull(); + expect(this.view.$input).toBeNull(); + expect(this.view.$overflowHelper).toBeNull(); + }); + }); + + // helper functions + // ---------------- + + function simulateInputEvent($node) { + var $e; + var type; + + type = _.isMsie() ? 'keypress' : 'input'; + $e = $.Event(type); + + $node.trigger($e); + } + + function simulateKeyEvent($node, type, key, withModifier) { + var $e; + + $e = $.Event(type, { + keyCode: key, + altKey: !!withModifier, + ctrlKey: !!withModifier, + metaKey: !!withModifier, + shiftKey: !!withModifier + }); + + spyOn($e, 'preventDefault'); + $node.trigger($e); + + return $e; + } + + function setCursorPosition($input, pos) { + var input = $input[0]; + var range; + + if (input.setSelectionRange) { + input.focus(); + input.setSelectionRange(pos, pos); + } else if (input.createTextRange) { + range = input.createTextRange(); + range.collapse(true); + range.moveEnd('character', pos); + range.moveStart('character', pos); + range.select(); + } + } +}); diff --git a/js/autocomplete.js/test/unit/jquery_spec.js b/js/autocomplete.js/test/unit/jquery_spec.js new file mode 100644 index 0000000..8677314 --- /dev/null +++ b/js/autocomplete.js/test/unit/jquery_spec.js @@ -0,0 +1,41 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +describe('Typeahead', function() { + var $ = require('jquery'); + require('jasmine-jquery'); + + var fixtures = require('../fixtures.js'); + var $autocomplete = require('../../src/jquery/plugin.js'); + + describe('when instantiated from jquery', function() { + beforeEach(function() { + this.$fixture = $(setFixtures(fixtures.html.textInput)); + + this.view = this.$fixture.find('input').autocomplete({}, [{ + name: 'test', + source: function(q, cb) { + cb([{name: 'test'}]); + }, + templates: { + suggestion: function(sugg) { + return sugg.name; + } + } + }]).data('aaAutocomplete'); + }); + + it('should initialize', function() { + expect(this.$fixture.find('.aa-dropdown-menu').length).toEqual(1); + }); + + it('should open the dropdown', function() { + this.$fixture.find('input').val('test'); + expect(this.view.input.getInputValue()).toEqual('test'); + $autocomplete.call($('input'), 'val', 'test'); + $autocomplete.call($('input'), 'open'); + $autocomplete.call($('input'), 'close'); + }); + }); +}); diff --git a/js/autocomplete.js/test/unit/parseAlgoliaClientVersion_spec.js b/js/autocomplete.js/test/unit/parseAlgoliaClientVersion_spec.js new file mode 100644 index 0000000..f7e6aec --- /dev/null +++ b/js/autocomplete.js/test/unit/parseAlgoliaClientVersion_spec.js @@ -0,0 +1,27 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +describe('parseAlgoliaClientVersion', function() { + var parseAlgoliaClientVersion = require('../../src/common/parseAlgoliaClientVersion.js'); + + it('should return undefined for unknown user agents', function() { + expect(parseAlgoliaClientVersion('random user agent 1.2.3')).toEqual( + undefined + ); + }); + + it('should parse user agents with algoliasearch < 3.33.0 format', function() { + expect( + parseAlgoliaClientVersion('Algolia for vanilla JavaScript 3.1.0') + ).toEqual(['3.', '1.', '0']); + }); + + it('should parse user agents with algoliasearch >= 3.33.0 format', function() { + expect(parseAlgoliaClientVersion('Algolia for JavaScript (3.5.0)')).toEqual([ + '3.', + '5.', + '0' + ]); + }); +}); diff --git a/js/autocomplete.js/test/unit/popularIn_spec.js b/js/autocomplete.js/test/unit/popularIn_spec.js new file mode 100644 index 0000000..e3e9725 --- /dev/null +++ b/js/autocomplete.js/test/unit/popularIn_spec.js @@ -0,0 +1,133 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +describe('popularIn', function() { + require('../../src/common/dom.js').element = require('jquery'); + require('../../src/jquery/plugin.js'); + + var popularIn = require('../../src/sources/popularIn.js'); + + beforeEach(function() { + }); + + function build(options) { + var queries = { + as: { + _ua: 'javascript wrong agent', + }, + search: function(q, params, cb) { + cb(false, { + hits: [ + { value: 'q1' }, + { value: 'q2' }, + { value: 'q3' } + ] + }); + } + }; + var products = { + as: { + _ua: 'javascript wrong agent', + }, + search: function(q, params, cb) { + cb(false, { + facets: { + category: { + c1: 42, + c2: 21, + c3: 2 + } + } + }) + } + }; + var f = popularIn(queries, { hitsPerPage: 3 }, { + source: 'value', + index: products, + facets: 'category', + maxValuesPerFacet: 3 + }, options); + + var suggestions = []; + function cb(hits) { + suggestions = suggestions.concat(hits); + } + f('q', cb); + return suggestions; + } + + it('should query 2 indices and build the combinatory', function() { + var suggestions = build(); + expect(suggestions.length).toEqual(5); + expect(suggestions[0].value).toEqual('q1'); + expect(suggestions[0].facet.value).toEqual('c1'); + expect(suggestions[1].value).toEqual('q1'); + expect(suggestions[1].facet.value).toEqual('c2'); + expect(suggestions[2].value).toEqual('q1'); + expect(suggestions[2].facet.value).toEqual('c3'); + expect(suggestions[3].value).toEqual('q2'); + expect(suggestions[3].facet).toBe(undefined); + expect(suggestions[4].value).toEqual('q3'); + expect(suggestions[4].facet).toBe(undefined); + }); + + it('should include the all department entry', function() { + var suggestions = build({includeAll: true}); + expect(suggestions.length).toEqual(6); + expect(suggestions[0].value).toEqual('q1'); + expect(suggestions[0].facet.value).toEqual('All departments'); + expect(suggestions[1].value).toEqual('q1'); + expect(suggestions[1].facet.value).toEqual('c1'); + expect(suggestions[2].value).toEqual('q1'); + expect(suggestions[2].facet.value).toEqual('c2'); + expect(suggestions[3].value).toEqual('q1'); + expect(suggestions[3].facet.value).toEqual('c3'); + expect(suggestions[4].value).toEqual('q2'); + expect(suggestions[4].facet).toBe(undefined); + expect(suggestions[5].value).toEqual('q3'); + expect(suggestions[5].facet).toBe(undefined); + }); + + it('should include the all department entry with a custom title', function() { + var suggestions = build({includeAll: true, allTitle: 'ALL'}); + expect(suggestions.length).toEqual(6); + expect(suggestions[0].value).toEqual('q1'); + expect(suggestions[0].facet.value).toEqual('ALL'); + }); + + it('should not include the all department entry when no results', function() { + var queries = { + as: { + _ua: 'Algolia for vanilla JavaScript 4.3.6' + }, + search: function(q, params, cb) { + cb(false, { + hits: [] + }); + } + }; + var products = { + as: { + _ua: 'javascript wrong agent', + }, + search: function(q, params, cb) { + throw new Error('Never reached'); + } + }; + var f = popularIn(queries, { hitsPerPage: 3 }, { + source: 'value', + index: products + }, { + includeAll: true + }); + + var suggestions = []; + function cb(hits) { + suggestions = suggestions.concat(hits); + } + f('q', cb); + expect(suggestions.length).toEqual(0); + }); + +}); diff --git a/js/autocomplete.js/test/unit/standalone_spec.js b/js/autocomplete.js/test/unit/standalone_spec.js new file mode 100644 index 0000000..2356030 --- /dev/null +++ b/js/autocomplete.js/test/unit/standalone_spec.js @@ -0,0 +1,130 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +describe('Typeahead', function() { + + var fixtures = require('../fixtures.js'); + var autocomplete = require('../../src/standalone/index.js'); + + beforeEach(function() { + this.$fixture = setFixtures(fixtures.html.textInput); + + this.ac = autocomplete('input', {}, { + name: 'test', + source: function(q, cb) { + cb([{name: 'test'}]); + }, + templates: { + suggestion: function(sugg) { + return sugg.name; + } + } + }); + }); + + describe('when instantiated from standalone', function() { + + it('should initialize', function() { + expect(this.$fixture.find('.aa-dropdown-menu').length).toEqual(1); + }); + + it('has an .autocomplete property', function() { + expect(this.ac.autocomplete).toBeDefined(); + }); + + }); + + describe('when accessing autocomplete function', function() { + + it('should have an open, close, getVal, setVal and destroy methods', function() { + var methodsToAssert = ['open', 'close', 'getVal', 'setVal', 'destroy', 'getWrapper']; + + for (var i = 0; i < methodsToAssert.length; i++) { + expect(this.ac.autocomplete[methodsToAssert[i]]).toBeDefined(); + expect(typeof this.ac.autocomplete[methodsToAssert[i]]).toEqual('function'); + } + }); + + describe('when executing the methods', function() { + beforeEach(function() { + this.$fixture = setFixtures(fixtures.html.textInput); + + this.typeaheadSpy = { + input: { + $input: {} + }, + open: sinon.stub().returns('hello'), + close: sinon.stub().returns('hello'), + getVal: sinon.stub().returns('hello'), + setVal: sinon.stub().returns('hello'), + destroy: sinon.stub().returns('hello'), + getWrapper: sinon.stub().returns('hello') + }; + + this.ac = autocomplete('input', {}, { + name: 'test', + source: function(q, cb) { + cb([{name: 'test'}]); + }, + templates: { + suggestion: function(sugg) { + return sugg.name; + } + } + }, this.typeaheadSpy); + }); + + it('should proxy the method call on typeahead object', function() { + expect(this.ac.autocomplete.open()).toEqual('hello'); + expect(this.typeaheadSpy.open.calledOnce).toBe(true); + expect(this.ac.autocomplete.close()).toEqual('hello'); + expect(this.typeaheadSpy.close.calledOnce).toBe(true); + expect(this.ac.autocomplete.getVal()).toEqual('hello'); + expect(this.typeaheadSpy.getVal.calledOnce).toBe(true); + expect(this.ac.autocomplete.setVal('Hey')).toEqual('hello'); + expect(this.typeaheadSpy.setVal.withArgs('Hey').calledOnce).toBe(true); + expect(this.ac.autocomplete.destroy()).toEqual('hello'); + expect(this.typeaheadSpy.destroy.calledOnce).toBe(true); + expect(this.ac.autocomplete.getWrapper()).toEqual('hello'); + expect(this.typeaheadSpy.getWrapper.calledOnce).toBe(true); + }); + + }); + }); + +}); + +describe('noConflict()', function() { + it('should restore the previous value of autocomplete', function() { + window.autocomplete = 'test'; + + // Rerequire autocomplete + delete require.cache[require.resolve('../../src/standalone/index.js')]; + var autocomplete = require('../../src/standalone/index.js'); + + expect(window.autocomplete).toBe('test'); + window.autocomplete = autocomplete; + + var aa = autocomplete.noConflict(); + expect(aa).toBe(autocomplete); + expect(window.autocomplete).toBe('test'); + }); + + it('should delete window.autocomplete if it wasn\'t set', function() { + if ('autocomplete' in window) { + delete window.autocomplete; + } + + // Rerequire autocomplete + delete require.cache[require.resolve('../../src/standalone/index.js')]; + var autocomplete = require('../../src/standalone/index.js'); + + expect('autocomplete' in window).toBe(false); + window.autocomplete = autocomplete; + + var aa = autocomplete.noConflict(); + expect(aa).toBe(autocomplete); + expect('autocomplete' in window).toBe(false); + }); +}); diff --git a/js/autocomplete.js/test/unit/typeahead_spec.js b/js/autocomplete.js/test/unit/typeahead_spec.js new file mode 100644 index 0000000..82b056d --- /dev/null +++ b/js/autocomplete.js/test/unit/typeahead_spec.js @@ -0,0 +1,1039 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +describe('Typeahead', function() { + require('../../src/common/dom.js').element = require('jquery'); + require('../../src/jquery/plugin.js'); + + var $ = require('jquery'); + require('jasmine-jquery'); + var Typeahead = require('../../src/autocomplete/typeahead.js'); + var fixtures = require('../fixtures.js'); + + var mocks = require('../helpers/mocks.js'); + var waitsForAndRuns = require('../helpers/waits_for.js'); + + Typeahead.Dropdown = mocks(Typeahead.Dropdown); + Typeahead.Input = mocks(Typeahead.Input); + + var testDatum; + + beforeEach(function() { + var $fixture; + var $input; + + setFixtures(fixtures.html.textInput); + + $fixture = $('#jasmine-fixtures'); + this.$input = $fixture.find('input'); + + testDatum = fixtures.data.simple[0]; + + this.view = new Typeahead({ + input: this.$input, + hint: true, + datasets: {} + }); + + this.input = this.view.input; + this.dropdown = this.view.dropdown; + }); + + describe('appendTo', function() { + it('should throw if used with hint', function(done) { + expect(function() { + return new Typeahead({ + input: this.$input, + hint: true, + appendTo: 'body' + }); + }).toThrow(); + done(); + }); + + it('should be appended to the target of appendTo', function(done) { + var node = document.createElement('div'); + document.querySelector('body').appendChild(node); + + expect(node.children.length).toEqual(0); + + this.view.destroy(); + + this.view = new Typeahead({ + input: this.$input, + hint: false, + appendTo: node + }); + + expect(document.querySelectorAll('.algolia-autocomplete').length).toEqual(1); + expect(node.children.length).toEqual(1); + + this.view.destroy(); + + done(); + }); + }); + + describe('when dropdown triggers suggestionClicked', function() { + beforeEach(function() { + this.dropdown.getDatumForSuggestion.and.returnValue(testDatum); + }); + + it('should select the datum', function(done) { + var spy; + + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.dropdown.trigger('suggestionClicked'); + + expect(spy).toHaveBeenCalled(); + expect(this.input.setQuery).toHaveBeenCalledWith(testDatum.value); + expect(this.input.setInputValue) + .toHaveBeenCalledWith(testDatum.value, true); + + var that = this; + waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); + }); + + it('should pass the selection method as part of the context ', function(done) { + var spy; + + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.dropdown.trigger('suggestionClicked'); + + expect(spy).toHaveBeenCalledWith( + jasmine.any(Object), + undefined, + undefined, + { selectionMethod: 'click' } + ); + + var that = this; + waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); + }); + }); + + describe('when dropdown triggers suggestionClicked with undefined displayKey', function() { + beforeEach(function() { + this.dropdown.getDatumForSuggestion.and.returnValue({}); + }); + + it('should not set input to undefined', function(done) { + var spy; + + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.dropdown.trigger('suggestionClicked'); + + expect(spy).toHaveBeenCalled(); + expect(this.input.setQuery).not.toHaveBeenCalled(); + expect(this.input.setInputValue).toHaveBeenCalledWith(undefined, true); + + var that = this; + waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); + }); + }); + + describe('when dropdown triggers cursorMoved', function() { + beforeEach(function() { + this.dropdown.getDatumForCursor.and.returnValue(testDatum); + this.dropdown.getCurrentCursor.and.returnValue($('
')); + }); + + it('should update the input value', function() { + this.dropdown.trigger('cursorMoved', true); + + expect(this.input.setInputValue) + .toHaveBeenCalledWith(testDatum.value, true); + }); + + it('should update the active descendant', function() { + this.dropdown.trigger('cursorMoved', false); + + expect(this.input.setActiveDescendant) + .toHaveBeenCalledWith('option-id'); + }); + + it('should not update the input', function() { + this.dropdown.trigger('cursorMoved', false); + + expect(this.input.setInputValue) + .not.toHaveBeenCalled(); + }); + + it('should trigger cursorchanged', function() { + var spy; + + this.$input.on('autocomplete:cursorchanged', spy = jasmine.createSpy()); + + this.dropdown.trigger('cursorMoved'); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when dropdown triggers cursorRemoved', function() { + it('should reset the input value', function() { + this.dropdown.trigger('cursorRemoved'); + + expect(this.input.resetInputValue).toHaveBeenCalled(); + }); + + it('should update the hint', function() { + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + this.dropdown.isVisible.and.returnValue(true); + this.input.hasOverflow.and.returnValue(false); + this.input.getInputValue.and.returnValue(testDatum.value.slice(0, 2)); + + this.dropdown.trigger('cursorRemoved'); + + expect(this.input.setHint).toHaveBeenCalledWith(testDatum.value); + }); + }); + + describe('when dropdown triggers datasetRendered', function() { + it('should update the hint asynchronously', function(done) { + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + this.dropdown.isVisible.and.returnValue(true); + this.input.hasOverflow.and.returnValue(false); + this.input.getInputValue.and.returnValue(testDatum.value.slice(0, 2)); + + this.dropdown.trigger('datasetRendered'); + + // ensure it wasn't called synchronously + expect(this.input.setHint).not.toHaveBeenCalled(); + + var that = this; + waitsForAndRuns(function() { return !!that.input.setHint.calls.count(); }, function() { + expect(that.input.setHint).toHaveBeenCalledWith(testDatum.value); + done(); + }, 100); + }); + + it('should trigger autocomplete:updated', function(done) { + var spy; + this.$input.on('autocomplete:updated', spy = jasmine.createSpy()); + + this.dropdown.trigger('datasetRendered'); + + var that = this; + waitsForAndRuns(function() { return !!that.input.setHint.calls.count(); }, function() { + expect(spy).toHaveBeenCalled(); + done(); + }, 100); + }); + }); + + describe('when dropdown triggers opened', function() { + it('should update the hint', function() { + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + this.dropdown.isVisible.and.returnValue(true); + this.input.hasOverflow.and.returnValue(false); + this.input.getInputValue.and.returnValue(testDatum.value.slice(0, 2)); + + this.dropdown.trigger('opened'); + + expect(this.input.setHint).toHaveBeenCalledWith(testDatum.value); + }); + + it('should trigger autocomplete:opened', function() { + var spy; + + this.$input.on('autocomplete:opened', spy = jasmine.createSpy()); + + this.dropdown.trigger('opened'); + + expect(spy).toHaveBeenCalled(); + }); + + it('should trigger autocomplete:shown', function() { + var spy; + + this.$input.on('autocomplete:shown', spy = jasmine.createSpy()); + + this.dropdown.trigger('shown'); + + expect(spy).toHaveBeenCalled(); + }); + + it('should trigger autocomplete:redrawn', function() { + var spy; + + this.$input.on('autocomplete:redrawn', spy = jasmine.createSpy()); + + this.dropdown.trigger('redrawn'); + + expect(spy).toHaveBeenCalled(); + }); + + it('should set the input\'s aria-expanded to true', function() { + this.dropdown.trigger('opened'); + expect(this.input.expand).toHaveBeenCalled(); + }); + }); + + describe('when dropdown triggers closed', function() { + it('should clear the hint', function() { + this.dropdown.trigger('closed'); + + expect(this.input.clearHint).toHaveBeenCalled(); + }); + + it('should trigger autocomplete:closed', function() { + var spy; + + this.$input.on('autocomplete:closed', spy = jasmine.createSpy()); + + this.dropdown.trigger('closed'); + + expect(spy).toHaveBeenCalled(); + }); + + it('should set the input\'s aria-expanded to false', function() { + this.dropdown.trigger('closed'); + expect(this.input.collapse).toHaveBeenCalled(); + }); + }); + + describe('when input triggers focused', function() { + it('should activate the typeahead', function() { + this.input.trigger('focused'); + + expect(this.view.isActivated).toBe(true); + }); + + it('should not open the dropdown', function() { + this.input.trigger('focused'); + + expect(this.dropdown.open).not.toHaveBeenCalled(); + }); + }); + + describe('when input triggers blurred', function() { + it('should deactivate the typeahead', function() { + this.input.trigger('blurred'); + + expect(this.view.isActivated).toBe(false); + }); + + it('should empty the dropdown', function() { + this.input.trigger('blurred'); + + expect(this.dropdown.empty).toHaveBeenCalled(); + }); + + it('should close the dropdown', function() { + this.input.trigger('blurred'); + + expect(this.dropdown.close).toHaveBeenCalled(); + }); + + it('should select the suggestion if autoselectOnBlur is true', function() { + this.view.autoselectOnBlur = true; + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + + var spy; + + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.input.trigger('blurred'); + + expect(spy).toHaveBeenCalled(); + expect(this.input.setQuery).toHaveBeenCalledWith(testDatum.value); + expect(this.input.setInputValue) + .toHaveBeenCalledWith(testDatum.value, true); + }); + + it('should select the cursor suggestion if autoselectOnBlur is true', function() { + this.view.autoselectOnBlur = true; + this.dropdown.getDatumForTopSuggestion.and.returnValue(fixtures.data.simple[0]); + this.dropdown.getDatumForCursor.and.returnValue(fixtures.data.simple[1]); + + var spy; + + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.input.trigger('blurred'); + + expect(spy).toHaveBeenCalled(); + expect(this.input.setQuery).toHaveBeenCalledWith(fixtures.data.simple[1].value); + expect(this.input.setInputValue).toHaveBeenCalledWith(fixtures.data.simple[1].value, true); + }); + + it('should pass the selectionMethod as part of the context', function() { + this.view.autoselectOnBlur = true; + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + + var spy; + + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.input.trigger('blurred'); + + expect(spy).toHaveBeenCalledWith( + jasmine.any(Object), + undefined, + undefined, + { selectionMethod: 'blur' } + ); + }); + }); + + describe('when debug flag is set', function() { + beforeEach(function() { + this.view = new Typeahead({ + input: this.$input, + debug: true, + hint: true, + datasets: {} + }); + this.input = this.view.input; + }); + + describe('when input triggers blurred', function() { + it('should not empty the dropdown', function() { + this.input.trigger('blurred'); + + expect(this.dropdown.empty).not.toHaveBeenCalled(); + }); + + it('should not close the dropdown', function() { + this.input.trigger('blurred'); + + expect(this.dropdown.close).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when clearOnSelected flag is set to true', function() { + it('clears input when selected', function() { + var spy = jasmine.createSpy(); + var view = new Typeahead({ + input: this.$input, + clearOnSelected: true, + hint: true, + datasets: {} + }); + view.dropdown.getDatumForCursor.and.returnValue(testDatum); + + // select something, and clear + var $e = jasmine.createSpyObj('event', ['preventDefault']); + view.$input.on('autocomplete:selected', spy); + view.input.trigger('enterKeyed', $e); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({ + type: 'autocomplete:selected', + target: jasmine.any(Object), + delegateTarget: jasmine.any(Object), + currentTarget: jasmine.any(Object), + handleObj: jasmine.objectContaining({ + type: 'autocomplete:selected' + }) + }), undefined, undefined, jasmine.any(Object)); + expect(view.input.setQuery).toHaveBeenCalledWith(''); + expect(view.input.setInputValue).toHaveBeenCalledWith('', true); + }); + }); + + describe('when input triggers enterKeyed', function() { + beforeEach(function() { + this.dropdown.getDatumForCursor.and.returnValue(testDatum); + }); + + it('should select the datum', function(done) { + var $e; + var spy; + + $e = jasmine.createSpyObj('event', ['preventDefault']); + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.input.trigger('enterKeyed', $e); + + expect(spy).toHaveBeenCalled(); + expect(this.input.setQuery).toHaveBeenCalledWith(testDatum.value); + expect(this.input.setInputValue) + .toHaveBeenCalledWith(testDatum.value, true); + + var that = this; + waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); + }); + + it('should prevent the default behavior of the event', function() { + var $e; + + $e = jasmine.createSpyObj('event', ['preventDefault']); + this.input.trigger('enterKeyed', $e); + expect($e.preventDefault).toHaveBeenCalled(); + }); + + it('should pass the selection method as part of the context ', function(done) { + var $e; + var spy; + var anything = jasmine.any(Object); + + $e = jasmine.createSpyObj('event', ['preventDefault']); + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.input.trigger('enterKeyed', $e); + + expect(spy).toHaveBeenCalledWith( + anything, + undefined, + undefined, + {selectionMethod: 'enterKey'} + ); + + var that = this; + waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); + }); + }); + + describe('when input triggers tabKeyed', function() { + describe('when cursor is in use', function() { + beforeEach(function() { + this.dropdown.getDatumForCursor.and.returnValue(testDatum); + }); + + it('should select the datum', function(done) { + var $e; + var spy; + + $e = jasmine.createSpyObj('event', ['preventDefault']); + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.input.trigger('tabKeyed', $e); + + expect(spy).toHaveBeenCalled(); + expect(this.input.setQuery).toHaveBeenCalledWith(testDatum.value); + expect(this.input.setInputValue) + .toHaveBeenCalledWith(testDatum.value, true); + + var that = this; + waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); + }); + + it('should prevent the default behavior of the event', function() { + var $e; + + $e = jasmine.createSpyObj('event', ['preventDefault']); + this.input.trigger('tabKeyed', $e); + + expect($e.preventDefault).toHaveBeenCalled(); + }); + + it('should pass the selectionMethod as part of the context', function(done) { + var $e; + var spy; + + $e = jasmine.createSpyObj('event', ['preventDefault']); + this.$input.on('autocomplete:selected', spy = jasmine.createSpy()); + this.input.trigger('tabKeyed', $e); + + expect(spy).toHaveBeenCalledWith( + jasmine.any(Object), + undefined, + undefined, + {selectionMethod: 'tabKey'} + ); + + var that = this; + waitsForAndRuns(function() { return that.dropdown.close.calls.count(); }, done, 100); + }); + }); + + describe('when cursor is not in use', function() { + it('should autocomplete if tabAutocomplete is true', function() { + var spy; + + this.input.getQuery.and.returnValue('bi'); + this.input.getHint.and.returnValue(testDatum.value); + this.input.isCursorAtEnd.and.returnValue(true); + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + this.$input.on('autocomplete:autocompleted', spy = jasmine.createSpy()); + + this.input.trigger('tabKeyed'); + + expect(this.input.setInputValue).toHaveBeenCalledWith(testDatum.value); + expect(spy).toHaveBeenCalled(); + }); + + it('should not autocomplete if tabAutocomplete is false', function() { + this.view.tabAutocomplete = false; + var spy; + + this.input.getQuery.and.returnValue('bi'); + this.input.getHint.and.returnValue(testDatum.value); + this.input.isCursorAtEnd.and.returnValue(true); + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + this.$input.on('autocomplete:autocompleted', spy = jasmine.createSpy()); + + this.input.trigger('tabKeyed'); + + expect(this.input.setInputValue).not.toHaveBeenCalledWith(testDatum.value); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should close the dropdown if tabAutocomplete is false', function() { + this.view.tabAutocomplete = false; + + this.input.getQuery.and.returnValue('bi'); + this.input.getHint.and.returnValue(testDatum.value); + this.input.isCursorAtEnd.and.returnValue(true); + + this.input.trigger('tabKeyed'); + + expect(this.dropdown.close).toHaveBeenCalled(); + }); + }); + }); + + describe('when input triggers escKeyed', function() { + it('should close the dropdown', function() { + this.input.trigger('escKeyed'); + + expect(this.dropdown.close).toHaveBeenCalled(); + }); + + it('should reset the input value', function() { + this.input.trigger('escKeyed'); + + expect(this.input.resetInputValue).toHaveBeenCalled(); + }); + }); + + describe('when input triggers upKeyed', function() { + beforeEach(function() { + this.input.getQuery.and.returnValue('ghost'); + }); + + describe('when dropdown is empty and minLength is satisfied', function() { + beforeEach(function() { + this.dropdown.isEmpty = true; + this.view.minLength = 2; + this.input.trigger('upKeyed'); + }); + + it('should update dropdown', function() { + expect(this.dropdown.update).toHaveBeenCalledWith('ghost'); + }); + + it('should not move cursor up', function() { + expect(this.dropdown.moveCursorUp).not.toHaveBeenCalled(); + }); + }); + + describe('when dropdown is not empty', function() { + beforeEach(function() { + this.dropdown.isEmpty = false; + this.view.minLength = 2; + + this.input.trigger('upKeyed'); + }); + + it('should not update dropdown', function() { + expect(this.dropdown.update).not.toHaveBeenCalled(); + }); + + it('should move cursor up', function() { + expect(this.dropdown.moveCursorUp).toHaveBeenCalled(); + }); + }); + + describe('when minLength is not satisfied', function() { + beforeEach(function() { + this.dropdown.isEmpty = true; + this.view.minLength = 10; + + this.input.trigger('upKeyed'); + }); + + it('should not update dropdown', function() { + expect(this.dropdown.update).not.toHaveBeenCalled(); + }); + + it('should move cursor up', function() { + expect(this.dropdown.moveCursorUp).toHaveBeenCalled(); + }); + }); + + it('should open the dropdown', function() { + this.input.trigger('upKeyed'); + + expect(this.dropdown.open).toHaveBeenCalled(); + }); + }); + + describe('when input triggers downKeyed', function() { + beforeEach(function() { + this.input.getQuery.and.returnValue('ghost'); + }); + + describe('when dropdown is empty and minLength is satisfied', function() { + beforeEach(function() { + this.dropdown.isEmpty = true; + this.view.minLength = 2; + + this.input.trigger('downKeyed'); + }); + + it('should update dropdown', function() { + expect(this.dropdown.update).toHaveBeenCalledWith('ghost'); + }); + + it('should not move cursor down', function() { + expect(this.dropdown.moveCursorDown).not.toHaveBeenCalled(); + }); + }); + + describe('when dropdown is not empty', function() { + beforeEach(function() { + this.dropdown.isEmpty = false; + this.view.minLength = 2; + + this.input.trigger('downKeyed'); + }); + + it('should not update dropdown', function() { + expect(this.dropdown.update).not.toHaveBeenCalled(); + }); + + it('should move cursor down', function() { + expect(this.dropdown.moveCursorDown).toHaveBeenCalled(); + }); + }); + + describe('when minLength is not satisfied', function() { + beforeEach(function() { + this.dropdown.isEmpty = true; + this.view.minLength = 10; + + this.input.trigger('downKeyed'); + }); + + it('should not update dropdown', function() { + expect(this.dropdown.update).not.toHaveBeenCalled(); + }); + + it('should move cursor down', function() { + expect(this.dropdown.moveCursorDown).toHaveBeenCalled(); + }); + }); + + it('should open the dropdown', function() { + this.input.trigger('downKeyed'); + + expect(this.dropdown.open).toHaveBeenCalled(); + }); + }); + + describe('when dropdown is empty', function() { + it('should trigger autocomplete:empty', function() { + var spy; + + this.$input.on('autocomplete:empty', spy = jasmine.createSpy()); + + this.dropdown.trigger('empty'); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when input triggers leftKeyed', function() { + it('should autocomplete if language is rtl', function() { + var spy; + + this.view.dir = 'rtl'; + this.input.getQuery.and.returnValue('bi'); + this.input.getHint.and.returnValue(testDatum.value); + this.input.isCursorAtEnd.and.returnValue(true); + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + this.$input.on('autocomplete:autocompleted', spy = jasmine.createSpy()); + + this.input.trigger('leftKeyed'); + + expect(this.input.setInputValue).toHaveBeenCalledWith(testDatum.value); + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when input triggers rightKeyed', function() { + it('should autocomplete if language is ltr', function() { + var spy; + + this.view.dir = 'ltr'; + this.input.getQuery.and.returnValue('bi'); + this.input.getHint.and.returnValue(testDatum.value); + this.input.isCursorAtEnd.and.returnValue(true); + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + this.$input.on('autocomplete:autocompleted', spy = jasmine.createSpy()); + + this.input.trigger('rightKeyed'); + + expect(this.input.setInputValue).toHaveBeenCalledWith(testDatum.value); + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('when input triggers queryChanged', function() { + it('should clear the hint if it has become invalid', function() { + this.input.trigger('queryChanged', testDatum.value); + + expect(this.input.clearHintIfInvalid).toHaveBeenCalled(); + }); + + it('should empty dropdown if the query is empty', function() { + this.input.trigger('queryChanged', ''); + + expect(this.dropdown.empty).toHaveBeenCalled(); + }); + + it('should not empty dropdown if the query is non-empty', function() { + this.input.trigger('queryChanged', testDatum.value); + + expect(this.dropdown.empty).not.toHaveBeenCalled(); + }); + + it('should update dropdown', function() { + this.input.trigger('queryChanged', testDatum.value); + + expect(this.dropdown.update).toHaveBeenCalledWith(testDatum.value); + }); + + it('should open the dropdown', function() { + this.input.trigger('queryChanged', testDatum.value); + + expect(this.dropdown.open).toHaveBeenCalled(); + }); + + it('should set the language direction', function() { + this.input.getLanguageDirection.and.returnValue('rtl'); + + this.input.trigger('queryChanged', testDatum.value); + + expect(this.view.dir).toBe('rtl'); + expect(this.view.$node).toHaveCss({direction: 'rtl'}); + expect(this.dropdown.setLanguageDirection).toHaveBeenCalledWith('rtl'); + }); + }); + + describe('when input triggers whitespaceChanged', function() { + it('should update the hint', function() { + this.dropdown.getDatumForTopSuggestion.and.returnValue(testDatum); + this.dropdown.isVisible.and.returnValue(true); + this.input.hasOverflow.and.returnValue(false); + this.input.getInputValue.and.returnValue(testDatum.value.slice(0, 2)); + + this.input.trigger('whitespaceChanged'); + + expect(this.input.setHint).toHaveBeenCalledWith(testDatum.value); + }); + + it('should open the dropdown', function() { + this.input.trigger('whitespaceChanged'); + + expect(this.dropdown.open).toHaveBeenCalled(); + }); + }); + + describe('#open', function() { + it('should open the dropdown', function() { + this.input.getInputValue.and.returnValue(''); + this.view.open(); + + expect(this.dropdown.open).toHaveBeenCalled(); + }); + + it('should update & open the dropdown if there is a query', function() { + this.input.getInputValue.and.returnValue('test'); + this.view.open(); + + expect(this.dropdown.update).toHaveBeenCalled(); + expect(this.dropdown.open).toHaveBeenCalled(); + }); + }); + + describe('#close', function() { + it('should close the dropdown', function() { + this.view.close(); + + expect(this.dropdown.close).toHaveBeenCalled(); + }); + }); + + describe('#getVal', function() { + it('should return the current query', function() { + this.input.getQuery.and.returnValue('woah'); + this.view.close(); + + expect(this.view.getVal()).toBe('woah'); + }); + }); + + describe('#setVal', function() { + it('should update query', function() { + this.view.isActivated = true; + this.view.setVal('woah'); + + expect(this.input.setInputValue).toHaveBeenCalledWith('woah'); + }); + + it('should update query silently if not activated', function() { + this.view.setVal('woah'); + + expect(this.input.setQuery).toHaveBeenCalledWith('woah'); + expect(this.input.setInputValue).toHaveBeenCalledWith('woah', true); + }); + }); + + describe('#destroy', function() { + it('should destroy input', function() { + this.view.destroy(); + + expect(this.input.destroy).toHaveBeenCalled(); + }); + + it('should destroy dropdown', function() { + this.view.destroy(); + + expect(this.dropdown.destroy).toHaveBeenCalled(); + }); + + it('should null out its reference to the wrapper element', function() { + this.view.destroy(); + + expect(this.view.$node).toBeNull(); + }); + + it('should revert DOM changes', function() { + this.view.destroy(); + + // TODO: bad test + expect(this.$input).not.toHaveClass('aa-input'); + }); + }); + + describe('when instantiated with a custom menu template', function() { + beforeEach(function() { + appendSetFixtures(fixtures.html.customMenu); + + this.view.destroy(); + this.view = new Typeahead({ + input: this.$input, + templates: { + dropdownMenu: '#my-custom-menu-template' + }, + datasets: {} + }); + }); + + it('should include the template in the menu', function() { + var $fixture = $('#jasmine-fixtures'); + expect($fixture.find('.aa-dropdown-menu .my-custom-menu').length).toEqual(1); + }); + }); + + describe('when instantiated with a custom CSS classes', function() { + beforeEach(function() { + appendSetFixtures(fixtures.html.customMenu); + + this.view.destroy(); + this.view = new Typeahead({ + input: this.$input, + hint: true, + cssClasses: { + root: 'my-root', + prefix: 'pp', + dropdownMenu: 'my-menu', + input: 'my-bar', + hint: 'my-clue', + suggestions: 'list', + suggestion: 'item', + cursor: 'pointer', + dataset: 'resultset' + }, + datasets: {} + }); + }); + + it('should include the template in the menu', function() { + var $fixture = $('#jasmine-fixtures'); + expect($fixture.find('.my-root').length).toEqual(1); + expect($fixture.find('.my-root .pp-my-menu').length).toEqual(1); + expect($fixture.find('.my-root .pp-my-bar').length).toEqual(1); + expect($fixture.find('.my-root .pp-my-clue').length).toEqual(1); + }); + }); + + describe('when instantiated with a custom menu container', function() { + beforeEach(function() { + appendSetFixtures(fixtures.html.customMenuContainer); + + this.view.destroy(); + this.view = new Typeahead({ + input: this.$input, + dropdownMenuContainer: '#custom-menu-container', + datasets: {} + }); + }); + + it('should include the template in the menu', function() { + var $fixture = $('#custom-menu-container'); + expect($fixture.find('.aa-dropdown-menu').length).toEqual(1); + }); + }); + + describe('when openOnFocus is set', function() { + + beforeEach(function() { + appendSetFixtures(fixtures.html.customMenu); + + this.view.destroy(); + this.view = new Typeahead({ + input: this.$input, + openOnFocus: true, + minLength: 0, + datasets: {} + }); + + this.input = this.view.input; + }); + + it('should open the dropdown', function() { + this.input.getQuery.and.returnValue(''); + this.input.trigger('focused'); + expect(this.view.dropdown.open).toHaveBeenCalled(); + }); + }); + + describe('when set autoWidth option', function() { + it('should set default to true', function() { + this.dropdown.trigger('redrawn'); + expect(this.view.autoWidth).toBeTruthy(); + expect(/\d{3}px/.test(this.view.$node[0].style.width)).toBeTruthy(); + }); + + it('should not put width style when autoWidth is false', function() { + this.view.autoWidth = false; + this.dropdown.trigger('redrawn'); + expect(this.view.autoWidth).toBeFalsy(); + expect(this.view.$node[0].style.width).toBeFalsy(); + }); + }); + + describe('when aria-label is set', function() { + beforeEach(function() { + this.view.destroy(); + }); + + it('should set aria-label to the specified string', function() { + this.view = new Typeahead({ + input: this.$input, + ariaLabel: 'custom-aria-label' + }); + + expect(this.$input.attr('aria-label')).toBe('custom-aria-label'); + }); + + it('should not set an aria-label if no value is specified', function() { + this.view = new Typeahead({ + input: this.$input + }); + + expect(this.$input.attr('aria-label')).toBeUndefined(); + }); + }); +}); diff --git a/js/autocomplete.js/test/unit/utils_spec.js b/js/autocomplete.js/test/unit/utils_spec.js new file mode 100644 index 0000000..2c5cd36 --- /dev/null +++ b/js/autocomplete.js/test/unit/utils_spec.js @@ -0,0 +1,84 @@ +'use strict'; + +/* eslint-env mocha, jasmine */ + +var _ = require('../../src/common/utils.js'); + +describe('escapeHTML', function() { + it('should escape HTML but preserve the default tags', function() { + var test = '' + + 'OTHER CONTENTVALUE2OTHER CONTENT$'; + var actual = _.escapeHighlightedString(test); + expect(actual).toEqual('<img src=VALUE1 onerror=alert(1) />OTHER CONTENTVALUE2OTHER CONTENT$'); + }); + + it('should escape HTML but preserve the default tags when using custom tags', function() { + var test = '' + + 'OTHER CONTENTVALUE2OTHER CONTENT$'; + var actual = _.escapeHighlightedString(test, '', ''); + expect(actual).toEqual('<img src=VALUE1 onerror=alert(1) />OTHER CONTENTVALUE2OTHER CONTENT$'); + }); + + it('should report the isMsie state correctly', function() { + var actual = _.isMsie(); + expect(actual).toEqual(false); + }); + + it('should report the isMsie state correctly under a non-IE browser', function() { + var ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'; + var actual = _.isMsie(ua); + expect(actual).toEqual(false); + }); + + it('should report the isMsie state correctly under an IE browser', function() { + var ua = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko'; + var actual = _.isMsie(ua); + expect(actual).toEqual('11.0'); + }); + + it('should report the isMsie state correctly under a browser that includes Trident but is not IE', function() { + var ua = 'Mozilla/5.0 (iPad; CPU OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15G77 KurogoVersion/2.7.7 (Kurogo iOS Tablet) KurogoOSVersion/11.4.1 KurogoAppVersion/2.0.1 (com.telerik.TridentUniversity)'; + var actual = _.isMsie(ua); + expect(actual).toEqual(false); + }); +}); + +describe('every', function() { + // simulating an implementation of Array.prototype.each + _.each = function(obj, callback) { + for (var i = 0; i < obj.length; i++) { + callback(obj[i], i, _); + // note that we do not return here to break for loop, angular does not do this + } + }; + + it('_.every should return false when at least one result is false', function() { + expect( + _.every([ + {isEmpty: true}, + {isEmpty: false} + ], function(dataset) { + return dataset.isEmpty; + })).toEqual(false); + }); + + it('_.every should return false when each result is false', function() { + expect( + _.every([ + {isEmpty: false}, + {isEmpty: false} + ], function(dataset) { + return dataset.isEmpty; + })).toEqual(false); + }); + + it('_.every should return true when all results are true', function() { + expect( + _.every([ + {isEmpty: true}, + {isEmpty: true} + ], function(dataset) { + return dataset.isEmpty; + })).toEqual(true); + }); +}); diff --git a/js/autocomplete.js/version.js b/js/autocomplete.js/version.js new file mode 100644 index 0000000..09e98fb --- /dev/null +++ b/js/autocomplete.js/version.js @@ -0,0 +1 @@ +module.exports = "0.37.1"; diff --git a/js/autocomplete.js/zepto.js b/js/autocomplete.js/zepto.js new file mode 100644 index 0000000..390e5ca --- /dev/null +++ b/js/autocomplete.js/zepto.js @@ -0,0 +1,1319 @@ +/* istanbul ignore next */ +/* Zepto v1.2.0 - zepto event assets data - zeptojs.com/license */ +(function(global, factory) { + module.exports = factory(global); +}(/* this ##### UPDATED: here we want to use window/global instead of this which is the current file context ##### */ window, function(window) { + var Zepto = (function() { + var undefined, key, $, classList, emptyArray = [], concat = emptyArray.concat, filter = emptyArray.filter, slice = emptyArray.slice, + document = window.document, + elementDisplay = {}, classCache = {}, + cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1,'opacity': 1, 'z-index': 1, 'zoom': 1 }, + fragmentRE = /^\s*<(\w+|!)[^>]*>/, + singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rootNodeRE = /^(?:body|html)$/i, + capitalRE = /([A-Z])/g, + + // special attributes that should be get/set via method calls + methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'], + + adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ], + table = document.createElement('table'), + tableRow = document.createElement('tr'), + containers = { + 'tr': document.createElement('tbody'), + 'tbody': table, 'thead': table, 'tfoot': table, + 'td': tableRow, 'th': tableRow, + '*': document.createElement('div') + }, + readyRE = /complete|loaded|interactive/, + simpleSelectorRE = /^[\w-]*$/, + class2type = {}, + toString = class2type.toString, + zepto = {}, + camelize, uniq, + tempParent = document.createElement('div'), + propMap = { + 'tabindex': 'tabIndex', + 'readonly': 'readOnly', + 'for': 'htmlFor', + 'class': 'className', + 'maxlength': 'maxLength', + 'cellspacing': 'cellSpacing', + 'cellpadding': 'cellPadding', + 'rowspan': 'rowSpan', + 'colspan': 'colSpan', + 'usemap': 'useMap', + 'frameborder': 'frameBorder', + 'contenteditable': 'contentEditable' + }, + isArray = Array.isArray || + function(object){ return object instanceof Array } + + zepto.matches = function(element, selector) { + if (!selector || !element || element.nodeType !== 1) return false + var matchesSelector = element.matches || element.webkitMatchesSelector || + element.mozMatchesSelector || element.oMatchesSelector || + element.matchesSelector + if (matchesSelector) return matchesSelector.call(element, selector) + // fall back to performing a selector: + var match, parent = element.parentNode, temp = !parent + if (temp) (parent = tempParent).appendChild(element) + match = ~zepto.qsa(parent, selector).indexOf(element) + temp && tempParent.removeChild(element) + return match + } + + function type(obj) { + return obj == null ? String(obj) : + class2type[toString.call(obj)] || "object" + } + + function isFunction(value) { return type(value) == "function" } + function isWindow(obj) { return obj != null && obj == obj.window } + function isDocument(obj) { return obj != null && obj.nodeType == obj.DOCUMENT_NODE } + function isObject(obj) { return type(obj) == "object" } + function isPlainObject(obj) { + return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype + } + + function likeArray(obj) { + var length = !!obj && 'length' in obj && obj.length, + type = $.type(obj) + + return 'function' != type && !isWindow(obj) && ( + 'array' == type || length === 0 || + (typeof length == 'number' && length > 0 && (length - 1) in obj) + ) + } + + function compact(array) { return filter.call(array, function(item){ return item != null }) } + function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array } + camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) } + function dasherize(str) { + return str.replace(/::/g, '/') + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') + .replace(/([a-z\d])([A-Z])/g, '$1_$2') + .replace(/_/g, '-') + .toLowerCase() + } + uniq = function(array){ return filter.call(array, function(item, idx){ return array.indexOf(item) == idx }) } + + function classRE(name) { + return name in classCache ? + classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)')) + } + + function maybeAddPx(name, value) { + return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value + } + + function defaultDisplay(nodeName) { + var element, display + if (!elementDisplay[nodeName]) { + element = document.createElement(nodeName) + document.body.appendChild(element) + display = getComputedStyle(element, '').getPropertyValue("display") + element.parentNode.removeChild(element) + display == "none" && (display = "block") + elementDisplay[nodeName] = display + } + return elementDisplay[nodeName] + } + + function children(element) { + return 'children' in element ? + slice.call(element.children) : + $.map(element.childNodes, function(node){ if (node.nodeType == 1) return node }) + } + + function Z(dom, selector) { + var i, len = dom ? dom.length : 0 + for (i = 0; i < len; i++) this[i] = dom[i] + this.length = len + this.selector = selector || '' + } + + // `$.zepto.fragment` takes a html string and an optional tag name + // to generate DOM nodes from the given html string. + // The generated DOM nodes are returned as an array. + // This function can be overridden in plugins for example to make + // it compatible with browsers that don't support the DOM fully. + zepto.fragment = function(html, name, properties) { + var dom, nodes, container + + // A special case optimization for a single tag + if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1)) + + if (!dom) { + if (html.replace) html = html.replace(tagExpanderRE, "<$1>") + if (name === undefined) name = fragmentRE.test(html) && RegExp.$1 + if (!(name in containers)) name = '*' + + container = containers[name] + container.innerHTML = '' + html + dom = $.each(slice.call(container.childNodes), function(){ + container.removeChild(this) + }) + } + + if (isPlainObject(properties)) { + nodes = $(dom) + $.each(properties, function(key, value) { + if (methodAttributes.indexOf(key) > -1) nodes[key](value) + else nodes.attr(key, value) + }) + } + + return dom + } + + // `$.zepto.Z` swaps out the prototype of the given `dom` array + // of nodes with `$.fn` and thus supplying all the Zepto functions + // to the array. This method can be overridden in plugins. + zepto.Z = function(dom, selector) { + return new Z(dom, selector) + } + + // `$.zepto.isZ` should return `true` if the given object is a Zepto + // collection. This method can be overridden in plugins. + zepto.isZ = function(object) { + return object instanceof zepto.Z + } + + // `$.zepto.init` is Zepto's counterpart to jQuery's `$.fn.init` and + // takes a CSS selector and an optional context (and handles various + // special cases). + // This method can be overridden in plugins. + zepto.init = function(selector, context) { + var dom + // If nothing given, return an empty Zepto collection + if (!selector) return zepto.Z() + // Optimize for string selectors + else if (typeof selector == 'string') { + selector = selector.trim() + // If it's a html fragment, create nodes from it + // Note: In both Chrome 21 and Firefox 15, DOM error 12 + // is thrown if the fragment doesn't begin with < + if (selector[0] == '<' && fragmentRE.test(selector)) + dom = zepto.fragment(selector, RegExp.$1, context), selector = null + // If there's a context, create a collection on that context first, and select + // nodes from there + else if (context !== undefined) return $(context).find(selector) + // If it's a CSS selector, use it to select nodes. + else dom = zepto.qsa(document, selector) + } + // If a function is given, call it when the DOM is ready + else if (isFunction(selector)) return $(document).ready(selector) + // If a Zepto collection is given, just return it + else if (zepto.isZ(selector)) return selector + else { + // normalize array if an array of nodes is given + if (isArray(selector)) dom = compact(selector) + // Wrap DOM nodes. + else if (isObject(selector)) + dom = [selector], selector = null + // If it's a html fragment, create nodes from it + else if (fragmentRE.test(selector)) + dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null + // If there's a context, create a collection on that context first, and select + // nodes from there + else if (context !== undefined) return $(context).find(selector) + // And last but no least, if it's a CSS selector, use it to select nodes. + else dom = zepto.qsa(document, selector) + } + // create a new Zepto collection from the nodes found + return zepto.Z(dom, selector) + } + + // `$` will be the base `Zepto` object. When calling this + // function just call `$.zepto.init, which makes the implementation + // details of selecting nodes and creating Zepto collections + // patchable in plugins. + $ = function(selector, context){ + return zepto.init(selector, context) + } + + function extend(target, source, deep) { + for (key in source) + if (deep && (isPlainObject(source[key]) || isArray(source[key]))) { + if (isPlainObject(source[key]) && !isPlainObject(target[key])) + target[key] = {} + if (isArray(source[key]) && !isArray(target[key])) + target[key] = [] + extend(target[key], source[key], deep) + } + else if (source[key] !== undefined) target[key] = source[key] + } + + // Copy all but undefined properties from one or more + // objects to the `target` object. + $.extend = function(target){ + var deep, args = slice.call(arguments, 1) + if (typeof target == 'boolean') { + deep = target + target = args.shift() + } + args.forEach(function(arg){ extend(target, arg, deep) }) + return target + } + + // `$.zepto.qsa` is Zepto's CSS selector implementation which + // uses `document.querySelectorAll` and optimizes for some special cases, like `#id`. + // This method can be overridden in plugins. + zepto.qsa = function(element, selector){ + var found, + maybeID = selector[0] == '#', + maybeClass = !maybeID && selector[0] == '.', + nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked + isSimple = simpleSelectorRE.test(nameOnly) + return (element.getElementById && isSimple && maybeID) ? // Safari DocumentFragment doesn't have getElementById + ( (found = element.getElementById(nameOnly)) ? [found] : [] ) : + (element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] : + slice.call( + isSimple && !maybeID && element.getElementsByClassName ? // DocumentFragment doesn't have getElementsByClassName/TagName + maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class + element.getElementsByTagName(selector) : // Or a tag + element.querySelectorAll(selector) // Or it's not simple, and we need to query all + ) + } + + function filtered(nodes, selector) { + return selector == null ? $(nodes) : $(nodes).filter(selector) + } + + $.contains = document.documentElement.contains ? + function(parent, node) { + return parent !== node && parent.contains(node) + } : + function(parent, node) { + while (node && (node = node.parentNode)) + if (node === parent) return true + return false + } + + function funcArg(context, arg, idx, payload) { + return isFunction(arg) ? arg.call(context, idx, payload) : arg + } + + function setAttribute(node, name, value) { + value == null ? node.removeAttribute(name) : node.setAttribute(name, value) + } + + // access className property while respecting SVGAnimatedString + function className(node, value){ + var klass = node.className || '', + svg = klass && klass.baseVal !== undefined + + if (value === undefined) return svg ? klass.baseVal : klass + svg ? (klass.baseVal = value) : (node.className = value) + } + + // "true" => true + // "false" => false + // "null" => null + // "42" => 42 + // "42.5" => 42.5 + // "08" => "08" + // JSON => parse if valid + // String => self + function deserializeValue(value) { + try { + return value ? + value == "true" || + ( value == "false" ? false : + value == "null" ? null : + +value + "" == value ? +value : + /^[\[\{]/.test(value) ? $.parseJSON(value) : + value ) + : value + } catch(e) { + return value + } + } + + $.type = type + $.isFunction = isFunction + $.isWindow = isWindow + $.isArray = isArray + $.isPlainObject = isPlainObject + + $.isEmptyObject = function(obj) { + var name + for (name in obj) return false + return true + } + + $.isNumeric = function(val) { + var num = Number(val), type = typeof val + return val != null && type != 'boolean' && + (type != 'string' || val.length) && + !isNaN(num) && isFinite(num) || false + } + + $.inArray = function(elem, array, i){ + return emptyArray.indexOf.call(array, elem, i) + } + + $.camelCase = camelize + $.trim = function(str) { + return str == null ? "" : String.prototype.trim.call(str) + } + + // plugin compatibility + $.uuid = 0 + $.support = { } + $.expr = { } + $.noop = function() {} + + $.map = function(elements, callback){ + var value, values = [], i, key + if (likeArray(elements)) + for (i = 0; i < elements.length; i++) { + value = callback(elements[i], i) + if (value != null) values.push(value) + } + else + for (key in elements) { + value = callback(elements[key], key) + if (value != null) values.push(value) + } + return flatten(values) + } + + $.each = function(elements, callback){ + var i, key + if (likeArray(elements)) { + for (i = 0; i < elements.length; i++) + if (callback.call(elements[i], i, elements[i]) === false) return elements + } else { + for (key in elements) + if (callback.call(elements[key], key, elements[key]) === false) return elements + } + + return elements + } + + $.grep = function(elements, callback){ + return filter.call(elements, callback) + } + + if (window.JSON) $.parseJSON = JSON.parse + + // Populate the class2type map + $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase() + }) + + // Define methods that will be available on all + // Zepto collections + $.fn = { + constructor: zepto.Z, + length: 0, + + // Because a collection acts like an array + // copy over these useful array functions. + forEach: emptyArray.forEach, + reduce: emptyArray.reduce, + push: emptyArray.push, + sort: emptyArray.sort, + splice: emptyArray.splice, + indexOf: emptyArray.indexOf, + concat: function(){ + var i, value, args = [] + for (i = 0; i < arguments.length; i++) { + value = arguments[i] + args[i] = zepto.isZ(value) ? value.toArray() : value + } + return concat.apply(zepto.isZ(this) ? this.toArray() : this, args) + }, + + // `map` and `slice` in the jQuery API work differently + // from their array counterparts + map: function(fn){ + return $($.map(this, function(el, i){ return fn.call(el, i, el) })) + }, + slice: function(){ + return $(slice.apply(this, arguments)) + }, + + ready: function(callback){ + // need to check if document.body exists for IE as that browser reports + // document ready when it hasn't yet created the body element + if (readyRE.test(document.readyState) && document.body) callback($) + else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false) + return this + }, + get: function(idx){ + return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length] + }, + toArray: function(){ return this.get() }, + size: function(){ + return this.length + }, + remove: function(){ + return this.each(function(){ + if (this.parentNode != null) + this.parentNode.removeChild(this) + }) + }, + each: function(callback){ + emptyArray.every.call(this, function(el, idx){ + return callback.call(el, idx, el) !== false + }) + return this + }, + filter: function(selector){ + if (isFunction(selector)) return this.not(this.not(selector)) + return $(filter.call(this, function(element){ + return zepto.matches(element, selector) + })) + }, + add: function(selector,context){ + return $(uniq(this.concat($(selector,context)))) + }, + is: function(selector){ + return this.length > 0 && zepto.matches(this[0], selector) + }, + not: function(selector){ + var nodes=[] + if (isFunction(selector) && selector.call !== undefined) + this.each(function(idx){ + if (!selector.call(this,idx)) nodes.push(this) + }) + else { + var excludes = typeof selector == 'string' ? this.filter(selector) : + (likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector) + this.forEach(function(el){ + if (excludes.indexOf(el) < 0) nodes.push(el) + }) + } + return $(nodes) + }, + has: function(selector){ + return this.filter(function(){ + return isObject(selector) ? + $.contains(this, selector) : + $(this).find(selector).size() + }) + }, + eq: function(idx){ + return idx === -1 ? this.slice(idx) : this.slice(idx, + idx + 1) + }, + first: function(){ + var el = this[0] + return el && !isObject(el) ? el : $(el) + }, + last: function(){ + var el = this[this.length - 1] + return el && !isObject(el) ? el : $(el) + }, + find: function(selector){ + var result, $this = this + if (!selector) result = $() + else if (typeof selector == 'object') + result = $(selector).filter(function(){ + var node = this + return emptyArray.some.call($this, function(parent){ + return $.contains(parent, node) + }) + }) + else if (this.length == 1) result = $(zepto.qsa(this[0], selector)) + else result = this.map(function(){ return zepto.qsa(this, selector) }) + return result + }, + closest: function(selector, context){ + var nodes = [], collection = typeof selector == 'object' && $(selector) + this.each(function(_, node){ + while (node && !(collection ? collection.indexOf(node) >= 0 : zepto.matches(node, selector))) + node = node !== context && !isDocument(node) && node.parentNode + if (node && nodes.indexOf(node) < 0) nodes.push(node) + }) + return $(nodes) + }, + parents: function(selector){ + var ancestors = [], nodes = this + while (nodes.length > 0) + nodes = $.map(nodes, function(node){ + if ((node = node.parentNode) && !isDocument(node) && ancestors.indexOf(node) < 0) { + ancestors.push(node) + return node + } + }) + return filtered(ancestors, selector) + }, + parent: function(selector){ + return filtered(uniq(this.pluck('parentNode')), selector) + }, + children: function(selector){ + return filtered(this.map(function(){ return children(this) }), selector) + }, + contents: function() { + return this.map(function() { return this.contentDocument || slice.call(this.childNodes) }) + }, + siblings: function(selector){ + return filtered(this.map(function(i, el){ + return filter.call(children(el.parentNode), function(child){ return child!==el }) + }), selector) + }, + empty: function(){ + return this.each(function(){ this.innerHTML = '' }) + }, + // `pluck` is borrowed from Prototype.js + pluck: function(property){ + return $.map(this, function(el){ return el[property] }) + }, + show: function(){ + return this.each(function(){ + this.style.display == "none" && (this.style.display = '') + if (getComputedStyle(this, '').getPropertyValue("display") == "none") + this.style.display = defaultDisplay(this.nodeName) + }) + }, + replaceWith: function(newContent){ + return this.before(newContent).remove() + }, + wrap: function(structure){ + var func = isFunction(structure) + if (this[0] && !func) + var dom = $(structure).get(0), + clone = dom.parentNode || this.length > 1 + + return this.each(function(index){ + $(this).wrapAll( + func ? structure.call(this, index) : + clone ? dom.cloneNode(true) : dom + ) + }) + }, + wrapAll: function(structure){ + if (this[0]) { + $(this[0]).before(structure = $(structure)) + var children + // drill down to the inmost element + while ((children = structure.children()).length) structure = children.first() + $(structure).append(this) + } + return this + }, + wrapInner: function(structure){ + var func = isFunction(structure) + return this.each(function(index){ + var self = $(this), contents = self.contents(), + dom = func ? structure.call(this, index) : structure + contents.length ? contents.wrapAll(dom) : self.append(dom) + }) + }, + unwrap: function(){ + this.parent().each(function(){ + $(this).replaceWith($(this).children()) + }) + return this + }, + clone: function(){ + return this.map(function(){ return this.cloneNode(true) }) + }, + hide: function(){ + return this.css("display", "none") + }, + toggle: function(setting){ + return this.each(function(){ + var el = $(this) + ;(setting === undefined ? el.css("display") == "none" : setting) ? el.show() : el.hide() + }) + }, + prev: function(selector){ return $(this.pluck('previousElementSibling')).filter(selector || '*') }, + next: function(selector){ return $(this.pluck('nextElementSibling')).filter(selector || '*') }, + html: function(html){ + return 0 in arguments ? + this.each(function(idx){ + var originHtml = this.innerHTML + $(this).empty().append( funcArg(this, html, idx, originHtml) ) + }) : + (0 in this ? this[0].innerHTML : null) + }, + text: function(text){ + return 0 in arguments ? + this.each(function(idx){ + var newText = funcArg(this, text, idx, this.textContent) + this.textContent = newText == null ? '' : ''+newText + }) : + (0 in this ? this.pluck('textContent').join("") : null) + }, + attr: function(name, value){ + var result + return (typeof name == 'string' && !(1 in arguments)) ? + (0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined) : + this.each(function(idx){ + if (this.nodeType !== 1) return + if (isObject(name)) for (key in name) setAttribute(this, key, name[key]) + else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name))) + }) + }, + removeAttr: function(name){ + return this.each(function(){ this.nodeType === 1 && name.split(' ').forEach(function(attribute){ + setAttribute(this, attribute) + }, this)}) + }, + prop: function(name, value){ + name = propMap[name] || name + return (1 in arguments) ? + this.each(function(idx){ + this[name] = funcArg(this, value, idx, this[name]) + }) : + (this[0] && this[0][name]) + }, + removeProp: function(name){ + name = propMap[name] || name + return this.each(function(){ delete this[name] }) + }, + data: function(name, value){ + var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase() + + var data = (1 in arguments) ? + this.attr(attrName, value) : + this.attr(attrName) + + return data !== null ? deserializeValue(data) : undefined + }, + val: function(value){ + if (0 in arguments) { + if (value == null) value = "" + return this.each(function(idx){ + this.value = funcArg(this, value, idx, this.value) + }) + } else { + return this[0] && (this[0].multiple ? + $(this[0]).find('option').filter(function(){ return this.selected }).pluck('value') : + this[0].value) + } + }, + offset: function(coordinates){ + if (coordinates) return this.each(function(index){ + var $this = $(this), + coords = funcArg(this, coordinates, index, $this.offset()), + parentOffset = $this.offsetParent().offset(), + props = { + top: coords.top - parentOffset.top, + left: coords.left - parentOffset.left + } + + if ($this.css('position') == 'static') props['position'] = 'relative' + $this.css(props) + }) + if (!this.length) return null + if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0])) + return {top: 0, left: 0} + var obj = this[0].getBoundingClientRect() + return { + left: obj.left + window.pageXOffset, + top: obj.top + window.pageYOffset, + width: Math.round(obj.width), + height: Math.round(obj.height) + } + }, + css: function(property, value){ + if (arguments.length < 2) { + var element = this[0] + if (typeof property == 'string') { + if (!element) return + return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property) + } else if (isArray(property)) { + if (!element) return + var props = {} + var computedStyle = getComputedStyle(element, '') + $.each(property, function(_, prop){ + props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop)) + }) + return props + } + } + + var css = '' + if (type(property) == 'string') { + if (!value && value !== 0) + this.each(function(){ this.style.removeProperty(dasherize(property)) }) + else + css = dasherize(property) + ":" + maybeAddPx(property, value) + } else { + for (key in property) + if (!property[key] && property[key] !== 0) + this.each(function(){ this.style.removeProperty(dasherize(key)) }) + else + css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';' + } + + return this.each(function(){ this.style.cssText += ';' + css }) + }, + index: function(element){ + return element ? this.indexOf($(element)[0]) : this.parent().children().indexOf(this[0]) + }, + hasClass: function(name){ + if (!name) return false + return emptyArray.some.call(this, function(el){ + return this.test(className(el)) + }, classRE(name)) + }, + addClass: function(name){ + if (!name) return this + return this.each(function(idx){ + if (!('className' in this)) return + classList = [] + var cls = className(this), newName = funcArg(this, name, idx, cls) + newName.split(/\s+/g).forEach(function(klass){ + if (!$(this).hasClass(klass)) classList.push(klass) + }, this) + classList.length && className(this, cls + (cls ? " " : "") + classList.join(" ")) + }) + }, + removeClass: function(name){ + return this.each(function(idx){ + if (!('className' in this)) return + if (name === undefined) return className(this, '') + classList = className(this) + funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass){ + classList = classList.replace(classRE(klass), " ") + }) + className(this, classList.trim()) + }) + }, + toggleClass: function(name, when){ + if (!name) return this + return this.each(function(idx){ + var $this = $(this), names = funcArg(this, name, idx, className(this)) + names.split(/\s+/g).forEach(function(klass){ + (when === undefined ? !$this.hasClass(klass) : when) ? + $this.addClass(klass) : $this.removeClass(klass) + }) + }) + }, + scrollTop: function(value){ + if (!this.length) return + var hasScrollTop = 'scrollTop' in this[0] + if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset + return this.each(hasScrollTop ? + function(){ this.scrollTop = value } : + function(){ this.scrollTo(this.scrollX, value) }) + }, + scrollLeft: function(value){ + if (!this.length) return + var hasScrollLeft = 'scrollLeft' in this[0] + if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset + return this.each(hasScrollLeft ? + function(){ this.scrollLeft = value } : + function(){ this.scrollTo(value, this.scrollY) }) + }, + position: function() { + if (!this.length) return + + var elem = this[0], + // Get *real* offsetParent + offsetParent = this.offsetParent(), + // Get correct offsets + offset = this.offset(), + parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset() + + // Subtract element margins + // note: when an element has margin: auto the offsetLeft and marginLeft + // are the same in Safari causing offset.left to incorrectly be 0 + offset.top -= parseFloat( $(elem).css('margin-top') ) || 0 + offset.left -= parseFloat( $(elem).css('margin-left') ) || 0 + + // Add offsetParent borders + parentOffset.top += parseFloat( $(offsetParent[0]).css('border-top-width') ) || 0 + parentOffset.left += parseFloat( $(offsetParent[0]).css('border-left-width') ) || 0 + + // Subtract the two offsets + return { + top: offset.top - parentOffset.top, + left: offset.left - parentOffset.left + } + }, + offsetParent: function() { + return this.map(function(){ + var parent = this.offsetParent || document.body + while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static") + parent = parent.offsetParent + return parent + }) + } + } + + // for now + $.fn.detach = $.fn.remove + + // Generate the `width` and `height` functions + ;['width', 'height'].forEach(function(dimension){ + var dimensionProperty = + dimension.replace(/./, function(m){ return m[0].toUpperCase() }) + + $.fn[dimension] = function(value){ + var offset, el = this[0] + if (value === undefined) return isWindow(el) ? el['inner' + dimensionProperty] : + isDocument(el) ? el.documentElement['scroll' + dimensionProperty] : + (offset = this.offset()) && offset[dimension] + else return this.each(function(idx){ + el = $(this) + el.css(dimension, funcArg(this, value, idx, el[dimension]())) + }) + } + }) + + function traverseNode(node, fun) { + fun(node) + for (var i = 0, len = node.childNodes.length; i < len; i++) + traverseNode(node.childNodes[i], fun) + } + + // Generate the `after`, `prepend`, `before`, `append`, + // `insertAfter`, `insertBefore`, `appendTo`, and `prependTo` methods. + adjacencyOperators.forEach(function(operator, operatorIndex) { + var inside = operatorIndex % 2 //=> prepend, append + + $.fn[operator] = function(){ + // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings + var argType, nodes = $.map(arguments, function(arg) { + var arr = [] + argType = type(arg) + if (argType == "array") { + arg.forEach(function(el) { + if (el.nodeType !== undefined) return arr.push(el) + else if ($.zepto.isZ(el)) return arr = arr.concat(el.get()) + arr = arr.concat(zepto.fragment(el)) + }) + return arr + } + return argType == "object" || arg == null ? + arg : zepto.fragment(arg) + }), + parent, copyByClone = this.length > 1 + if (nodes.length < 1) return this + + return this.each(function(_, target){ + parent = inside ? target : target.parentNode + + // convert all methods to a "before" operation + target = operatorIndex == 0 ? target.nextSibling : + operatorIndex == 1 ? target.firstChild : + operatorIndex == 2 ? target : + null + + var parentInDocument = $.contains(document.documentElement, parent) + + nodes.forEach(function(node){ + if (copyByClone) node = node.cloneNode(true) + else if (!parent) return $(node).remove() + + parent.insertBefore(node, target) + if (parentInDocument) traverseNode(node, function(el){ + if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' && + (!el.type || el.type === 'text/javascript') && !el.src){ + var target = el.ownerDocument ? el.ownerDocument.defaultView : window + target['eval'].call(target, el.innerHTML) + } + }) + }) + }) + } + + // after => insertAfter + // prepend => prependTo + // before => insertBefore + // append => appendTo + $.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){ + $(html)[operator](this) + return this + } + }) + + zepto.Z.prototype = Z.prototype = $.fn + + // Export internal API functions in the `$.zepto` namespace + zepto.uniq = uniq + zepto.deserializeValue = deserializeValue + $.zepto = zepto + + return $ +})() + +;(function($){ + var _zid = 1, undefined, + slice = Array.prototype.slice, + isFunction = $.isFunction, + isString = function(obj){ return typeof obj == 'string' }, + handlers = {}, + specialEvents={}, + focusinSupported = 'onfocusin' in window, + focus = { focus: 'focusin', blur: 'focusout' }, + hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' } + + specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents' + + function zid(element) { + return element._zid || (element._zid = _zid++) + } + function findHandlers(element, event, fn, selector) { + event = parse(event) + if (event.ns) var matcher = matcherFor(event.ns) + return (handlers[zid(element)] || []).filter(function(handler) { + return handler + && (!event.e || handler.e == event.e) + && (!event.ns || matcher.test(handler.ns)) + && (!fn || zid(handler.fn) === zid(fn)) + && (!selector || handler.sel == selector) + }) + } + function parse(event) { + var parts = ('' + event).split('.') + return {e: parts[0], ns: parts.slice(1).sort().join(' ')} + } + function matcherFor(ns) { + return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)') + } + + function eventCapture(handler, captureSetting) { + return handler.del && + (!focusinSupported && (handler.e in focus)) || + !!captureSetting + } + + function realEvent(type) { + return hover[type] || (focusinSupported && focus[type]) || type + } + + function add(element, events, fn, data, selector, delegator, capture){ + var id = zid(element), set = (handlers[id] || (handlers[id] = [])) + events.split(/\s/).forEach(function(event){ + if (event == 'ready') return $(document).ready(fn) + var handler = parse(event) + handler.fn = fn + handler.sel = selector + // emulate mouseenter, mouseleave + if (handler.e in hover) fn = function(e){ + var related = e.relatedTarget + if (!related || (related !== this && !$.contains(this, related))) + return handler.fn.apply(this, arguments) + } + handler.del = delegator + var callback = delegator || fn + handler.proxy = function(e){ + e = compatible(e) + if (e.isImmediatePropagationStopped()) return + try { + var dataPropDescriptor = Object.getOwnPropertyDescriptor(e, 'data') + if (!dataPropDescriptor || dataPropDescriptor.writable) + e.data = data + } catch (e) {} // when using strict mode dataPropDescriptor will be undefined when e is InputEvent (even though data property exists). So we surround with try/catch + var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args)) + if (result === false) e.preventDefault(), e.stopPropagation() + return result + } + handler.i = set.length + set.push(handler) + if ('addEventListener' in element) + element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) + }) + } + function remove(element, events, fn, selector, capture){ + var id = zid(element) + ;(events || '').split(/\s/).forEach(function(event){ + findHandlers(element, event, fn, selector).forEach(function(handler){ + delete handlers[id][handler.i] + if ('removeEventListener' in element) + element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) + }) + }) + } + + $.event = { add: add, remove: remove } + + $.proxy = function(fn, context) { + var args = (2 in arguments) && slice.call(arguments, 2) + if (isFunction(fn)) { + var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) } + proxyFn._zid = zid(fn) + return proxyFn + } else if (isString(context)) { + if (args) { + args.unshift(fn[context], fn) + return $.proxy.apply(null, args) + } else { + return $.proxy(fn[context], fn) + } + } else { + throw new TypeError("expected function") + } + } + + $.fn.bind = function(event, data, callback){ + return this.on(event, data, callback) + } + $.fn.unbind = function(event, callback){ + return this.off(event, callback) + } + $.fn.one = function(event, selector, data, callback){ + return this.on(event, selector, data, callback, 1) + } + + var returnTrue = function(){return true}, + returnFalse = function(){return false}, + ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$|webkitMovement[XY]$)/, + eventMethods = { + preventDefault: 'isDefaultPrevented', + stopImmediatePropagation: 'isImmediatePropagationStopped', + stopPropagation: 'isPropagationStopped' + } + + function compatible(event, source) { + if (source || !event.isDefaultPrevented) { + source || (source = event) + + $.each(eventMethods, function(name, predicate) { + var sourceMethod = source[name] + event[name] = function(){ + this[predicate] = returnTrue + return sourceMethod && sourceMethod.apply(source, arguments) + } + event[predicate] = returnFalse + }) + + try { + event.timeStamp || (event.timeStamp = Date.now()) + } catch (ignored) { } + + if (source.defaultPrevented !== undefined ? source.defaultPrevented : + 'returnValue' in source ? source.returnValue === false : + source.getPreventDefault && source.getPreventDefault()) + event.isDefaultPrevented = returnTrue + } + return event + } + + function createProxy(event) { + var key, proxy = { originalEvent: event } + for (key in event) + if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key] + + return compatible(proxy, event) + } + + $.fn.delegate = function(selector, event, callback){ + return this.on(event, selector, callback) + } + $.fn.undelegate = function(selector, event, callback){ + return this.off(event, selector, callback) + } + + $.fn.live = function(event, callback){ + $(document.body).delegate(this.selector, event, callback) + return this + } + $.fn.die = function(event, callback){ + $(document.body).undelegate(this.selector, event, callback) + return this + } + + $.fn.on = function(event, selector, data, callback, one){ + var autoRemove, delegator, $this = this + if (event && !isString(event)) { + $.each(event, function(type, fn){ + $this.on(type, selector, data, fn, one) + }) + return $this + } + + if (!isString(selector) && !isFunction(callback) && callback !== false) + callback = data, data = selector, selector = undefined + if (callback === undefined || data === false) + callback = data, data = undefined + + if (callback === false) callback = returnFalse + + return $this.each(function(_, element){ + if (one) autoRemove = function(e){ + remove(element, e.type, callback) + return callback.apply(this, arguments) + } + + if (selector) delegator = function(e){ + var evt, match = $(e.target).closest(selector, element).get(0) + if (match && match !== element) { + evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) + return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1))) + } + } + + add(element, event, callback, data, selector, delegator || autoRemove) + }) + } + $.fn.off = function(event, selector, callback){ + var $this = this + if (event && !isString(event)) { + $.each(event, function(type, fn){ + $this.off(type, selector, fn) + }) + return $this + } + + if (!isString(selector) && !isFunction(callback) && callback !== false) + callback = selector, selector = undefined + + if (callback === false) callback = returnFalse + + return $this.each(function(){ + remove(this, event, callback, selector) + }) + } + + $.fn.trigger = function(event, args){ + event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event) + event._args = args + return this.each(function(){ + // handle focus(), blur() by calling them directly + if (event.type in focus && typeof this[event.type] == "function") this[event.type]() + // items in the collection might not be DOM elements + else if ('dispatchEvent' in this) this.dispatchEvent(event) + else $(this).triggerHandler(event, args) + }) + } + + // triggers event handlers on current element just as if an event occurred, + // doesn't trigger an actual event, doesn't bubble + $.fn.triggerHandler = function(event, args){ + var e, result + this.each(function(i, element){ + e = createProxy(isString(event) ? $.Event(event) : event) + e._args = args + e.target = element + $.each(findHandlers(element, event.type || event), function(i, handler){ + result = handler.proxy(e) + if (e.isImmediatePropagationStopped()) return false + }) + }) + return result + } + + // shortcut methods for `.bind(event, fn)` for each event type + ;('focusin focusout focus blur load resize scroll unload click dblclick '+ + 'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+ + 'change select keydown keypress keyup error').split(' ').forEach(function(event) { + $.fn[event] = function(callback) { + return (0 in arguments) ? + this.bind(event, callback) : + this.trigger(event) + } + }) + + $.Event = function(type, props) { + if (!isString(type)) props = type, type = props.type + var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true + if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]) + event.initEvent(type, bubbles, true) + return compatible(event) + } + +})(Zepto) + +;(function($){ + var cache = [], timeout + + $.fn.remove = function(){ + return this.each(function(){ + if(this.parentNode){ + if(this.tagName === 'IMG'){ + cache.push(this) + this.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' + if (timeout) clearTimeout(timeout) + timeout = setTimeout(function(){ cache = [] }, 60000) + } + this.parentNode.removeChild(this) + } + }) + } +})(Zepto) + +;(function($){ + var data = {}, dataAttr = $.fn.data, camelize = $.camelCase, + exp = $.expando = 'Zepto' + (+new Date()), emptyArray = [] + + // Get value from node: + // 1. first try key as given, + // 2. then try camelized key, + // 3. fall back to reading "data-*" attribute. + function getData(node, name) { + var id = node[exp], store = id && data[id] + if (name === undefined) return store || setData(node) + else { + if (store) { + if (name in store) return store[name] + var camelName = camelize(name) + if (camelName in store) return store[camelName] + } + return dataAttr.call($(node), name) + } + } + + // Store value under camelized key on node + function setData(node, name, value) { + var id = node[exp] || (node[exp] = ++$.uuid), + store = data[id] || (data[id] = attributeData(node)) + if (name !== undefined) store[camelize(name)] = value + return store + } + + // Read all "data-*" attributes from a node + function attributeData(node) { + var store = {} + $.each(node.attributes || emptyArray, function(i, attr){ + if (attr.name.indexOf('data-') == 0) + store[camelize(attr.name.replace('data-', ''))] = + $.zepto.deserializeValue(attr.value) + }) + return store + } + + $.fn.data = function(name, value) { + return value === undefined ? + // set multiple values via object + $.isPlainObject(name) ? + this.each(function(i, node){ + $.each(name, function(key, value){ setData(node, key, value) }) + }) : + // get value from first element + (0 in this ? getData(this[0], name) : undefined) : + // set value on all elements + this.each(function(){ setData(this, name, value) }) + } + + $.data = function(elem, name, value) { + return $(elem).data(name, value) + } + + $.hasData = function(elem) { + var id = elem[exp], store = id && data[id] + return store ? !$.isEmptyObject(store) : false + } + + $.fn.removeData = function(names) { + if (typeof names == 'string') names = names.split(/\s+/) + return this.each(function(){ + var id = this[exp], store = id && data[id] + if (store) $.each(names || store, function(key){ + delete store[names ? camelize(this) : key] + }) + }) + } + + // Generate extended `remove` and `empty` functions + ;['remove', 'empty'].forEach(function(methodName){ + var origFn = $.fn[methodName] + $.fn[methodName] = function() { + var elements = this.find('*') + if (methodName === 'remove') elements = elements.add(this) + elements.removeData() + return origFn.call(this) + } + }) +})(Zepto) + return Zepto +})) diff --git a/js/instantsearch.js/CHANGELOG.md b/js/instantsearch.js/CHANGELOG.md new file mode 100644 index 0000000..5989064 --- /dev/null +++ b/js/instantsearch.js/CHANGELOG.md @@ -0,0 +1,1460 @@ +
+## [1.12.1](https://github.com/algolia/instantsearch.js/compare/v1.11.15...v1.12.1) (2018-05-16) + + +### Bug Fixes + +* **doc:** Absolute URLs for meta images ([b453bb4](https://github.com/algolia/instantsearch.js/commit/b453bb4)) + + +### Features + +* **algolia:** update the algolia dependencies ([8c17361](https://github.com/algolia/instantsearch.js/commit/8c17361)) + + + + +# [1.12.0](https://github.com/algolia/instantsearch.js/compare/v1.11.15...v1.12.0) (2018-05-16) + + +### Bug Fixes + +* **doc:** Absolute URLs for meta images ([b453bb4](https://github.com/algolia/instantsearch.js/commit/b453bb4)) + + +### Features + +* **algolia:** update the algolia dependencies ([8c17361](https://github.com/algolia/instantsearch.js/commit/8c17361)) + + + + +## [1.11.15](https://github.com/algolia/instantsearch.js/compare/v1.11.14...v1.11.15) (2017-06-20) + + +### Bug Fixes + +* **numeric-refinement-list:** reset page on refine ([ee55ccb](https://github.com/algolia/instantsearch.js/commit/ee55ccb)) + + + + +## [1.11.14](https://github.com/algolia/instantsearch.js/compare/v1.11.13...v1.11.14) (2017-06-19) + + +### Bug Fixes + +* **powered-by:** update logo ([7e68b51](https://github.com/algolia/instantsearch.js/commit/7e68b51)), closes [#2126](https://github.com/algolia/instantsearch.js/issues/2126) + + + + +## [1.11.13](https://github.com/algolia/instantsearch.js/compare/v1.11.12...v1.11.13) (2017-06-07) + + +### Bug Fixes + +* **url-sync:** reverting back to using `change` event (#2183) ([07f4be0](https://github.com/algolia/instantsearch.js/commit/07f4be0)), closes [#2173](https://github.com/algolia/instantsearch.js/issues/2173) [#2171](https://github.com/algolia/instantsearch.js/issues/2171) + + + + +## [1.11.12](https://github.com/algolia/instantsearch.js/compare/v1.11.11...v1.11.12) (2017-05-30) + + +### Bug Fixes + +* **sffv:** when using a large limit, retain the search (#2163) ([3d95d4c](https://github.com/algolia/instantsearch.js/commit/3d95d4c)), closes [#2156](https://github.com/algolia/instantsearch.js/issues/2156) + + + + +## [1.11.10](https://github.com/algolia/instantsearch.js/compare/v1.11.9...v1.11.10) (2017-05-17) + + + + +## [1.11.9](https://github.com/algolia/instantsearch.js/compare/v1.11.8...v1.11.9) (2017-05-17) + + + + +## [1.11.8](https://github.com/algolia/instantsearch.js/compare/v1.11.7...v1.11.8) (2017-05-16) + + +### Bug Fixes + +* **url-sync:** set firstRender to be class attribute ([22dbaeb](https://github.com/algolia/instantsearch.js/commit/22dbaeb)) + + + + +## [1.11.7](https://github.com/algolia/instantsearch.js/compare/v1.11.6...v1.11.7) (2017-04-24) + + +### Bug Fixes + +* **sffv:** add class for disabled state at the form level (#2122) ([029fa5f](https://github.com/algolia/instantsearch.js/commit/029fa5f)) +* **sffv:** fixes typo (: was left) ([26d2845](https://github.com/algolia/instantsearch.js/commit/26d2845)) + + + + +## [1.11.6](https://github.com/algolia/instantsearch.js/compare/v1.11.5...v1.11.6) (2017-04-20) + + +### Bug Fixes + +* **CONTRIBUTING:** remove section about beta releases (#2109) ([5640131](https://github.com/algolia/instantsearch.js/commit/5640131)) +* **sffv:** disable sffv input when few facet values FIX #2111 ([1e33c10](https://github.com/algolia/instantsearch.js/commit/1e33c10)), closes [#2111](https://github.com/algolia/instantsearch.js/issues/2111) + + + + +## [1.11.5](https://github.com/algolia/instantsearch.js/compare/v1.11.4...v1.11.5) (2017-04-12) + + +### Bug Fixes + +* **url-sync:** sync url on search (#2108) ([7f33ffb](https://github.com/algolia/instantsearch.js/commit/7f33ffb)) + + + + +## [1.11.4](https://github.com/algolia/instantsearch.js/compare/v1.11.3...v1.11.4) (2017-03-29) + + +### Bug Fixes + +* **autoHideContainer:** dont prevent render with `shouldComponentUpdate` (#2076) ([b520400](https://github.com/algolia/instantsearch.js/commit/b520400)) +* **star-rating:** make max value inclusive ([f5fc41c](https://github.com/algolia/instantsearch.js/commit/f5fc41c)), closes [#2002](https://github.com/algolia/instantsearch.js/issues/2002) + + + + +## [1.11.3](https://github.com/algolia/instantsearch.js/compare/v1.11.2...v1.11.3) (2017-03-22) + + +### Bug Fixes + +* **Slider:** display disabled slider when `min === max` (#2041) ([511fdfd](https://github.com/algolia/instantsearch.js/commit/511fdfd)), closes [#2037](https://github.com/algolia/instantsearch.js/issues/2037) + + + + +## [1.11.2](https://github.com/algolia/instantsearch.js/compare/v1.11.1...v1.11.2) (2017-02-28) + + +### Bug Fixes + +* **searchBox:** avoid unwanted cursor jumps on hashchange (#2013) ([d0103db](https://github.com/algolia/instantsearch.js/commit/d0103db)), closes [#2012](https://github.com/algolia/instantsearch.js/issues/2012) + + + + +## [1.11.1](https://github.com/algolia/instantsearch.js/compare/v1.11.0...v1.11.1) (2017-02-14) + + +### Bug Fixes + +* **infinite-hits:** disable load more button when no more pages (#1973) ([745ed89](https://github.com/algolia/instantsearch.js/commit/745ed89)), closes [#1971](https://github.com/algolia/instantsearch.js/issues/1971) + + + + +# [1.11.0](https://github.com/algolia/instantsearch.js/compare/v1.10.5...v1.11.0) (2017-02-12) + + +### Features + +* **analytics-widget:** add a new parameter pushInitialSearch (#1963) ([d777997](https://github.com/algolia/instantsearch.js/commit/d777997)) +* **custom client:** allows to provide a custom JS client instance (#1948) ([cce4f2e](https://github.com/algolia/instantsearch.js/commit/cce4f2e)) +* **InfiniteHits:** add new widget ([2d77e4b](https://github.com/algolia/instantsearch.js/commit/2d77e4b)) + + + + +## [1.10.5](https://github.com/algolia/instantsearch.js/compare/v1.10.4...v1.10.5) (2017-02-06) + + +### Bug Fixes + +* **urlSync:** update url only after threshold (#1917) ([b0f0cf1](https://github.com/algolia/instantsearch.js/commit/b0f0cf1)), closes [#1856](https://github.com/algolia/instantsearch.js/issues/1856) + + + + +## [1.10.4](https://github.com/algolia/instantsearch.js/compare/v1.10.3...v1.10.4) (2017-01-25) + + + + +## [1.10.3](https://github.com/algolia/instantsearch.js/compare/v1.10.2...v1.10.3) (2016-12-26) + + +### Bug Fixes + +* **sffv-searchbox:** update classnames to avoid conflicts (#1781) ([f53e8fd](https://github.com/algolia/instantsearch.js/commit/f53e8fd)) + + + + +## [1.10.2](https://github.com/algolia/instantsearch.js/compare/v1.10.1...v1.10.2) (2016-12-23) + + +### Bug Fixes + +* **url:** clear timeout on pop ([41ad9af](https://github.com/algolia/instantsearch.js/commit/41ad9af)) + + + + +## [1.10.1](https://github.com/algolia/instantsearch.js/compare/v1.10.0...v1.10.1) (2016-12-23) + + +### Bug Fixes + +* **url:** default param ([7a18e1c](https://github.com/algolia/instantsearch.js/commit/7a18e1c)) + + +### Features + +* **url:** add a beta updateOnEveryKeystroke option (#1779) ([63f73fe](https://github.com/algolia/instantsearch.js/commit/63f73fe)) + + + + +# [1.10.0](https://github.com/algolia/instantsearch.js/compare/v1.9.0...v1.10.0) (2016-12-22) + + +### Features + +* **widget:** Search for facet values - refinement list (#1753) ([b9e20f3](https://github.com/algolia/instantsearch.js/commit/b9e20f3)) + + + + +# [1.9.0](https://github.com/algolia/instantsearch.js/compare/v1.8.16...v1.9.0) (2016-12-14) + + +### Bug Fixes + +* **currentRefinedValues:** unescape disjunctive facet refinement names (#1574) ([9ab65c4](https://github.com/algolia/instantsearch.js/commit/9ab65c4)), closes [#1569](https://github.com/algolia/instantsearch.js/issues/1569) +* **transformData:** default data is an object when not provided (#1570) ([8eeeeba](https://github.com/algolia/instantsearch.js/commit/8eeeeba)), closes [#1538](https://github.com/algolia/instantsearch.js/issues/1538) + + +### Features + +* **analytics:** new analytics widget to easily plug search to any analytics service ([09d8fda](https://github.com/algolia/instantsearch.js/commit/09d8fda)) +* **retry strategy:** new retry strategy ([afdcc3c](https://github.com/algolia/instantsearch.js/commit/afdcc3c)) + + + + +## [1.8.16](https://github.com/algolia/instantsearch.js/compare/v1.8.15...v1.8.16) (2016-11-16) + + + + +## [1.8.15](https://github.com/algolia/instantsearch.js/compare/v1.8.14...v1.8.15) (2016-11-16) + + +### Bug Fixes + +* **priceRanges:** avoid displaying solo ranges (#1544) ([ff396f0](https://github.com/algolia/instantsearch.js/commit/ff396f0)), closes [#1536](https://github.com/algolia/instantsearch.js/issues/1536) +* **priceRanges:** use formatNumber in defaultTemplate (#1559) ([557a501](https://github.com/algolia/instantsearch.js/commit/557a501)), closes [#1230](https://github.com/algolia/instantsearch.js/issues/1230) +* **toggle:** support negative numeric values for on/off (#1551) ([e4d88e0](https://github.com/algolia/instantsearch.js/commit/e4d88e0)), closes [#1537](https://github.com/algolia/instantsearch.js/issues/1537) +* **transformData:** always call transformData (#1555) ([49bfeca](https://github.com/algolia/instantsearch.js/commit/49bfeca)), closes [#1538](https://github.com/algolia/instantsearch.js/issues/1538) + + + + +## [1.8.14](https://github.com/algolia/instantsearch.js/compare/v1.8.13...v1.8.14) (2016-11-03) + + +### Bug Fixes + +* **slider:** avoid multi touch issues (#1501) ([0b8a242](https://github.com/algolia/instantsearch.js/commit/0b8a242)), closes [#1186](https://github.com/algolia/instantsearch.js/issues/1186) + + + + +## [1.8.13](https://github.com/algolia/instantsearch.js/compare/v1.8.12...v1.8.13) (2016-10-21) + + +### Bug Fixes + +* **searchbox:** poweredBy Algolia logo weren't visible in firefox ([39701f8](https://github.com/algolia/instantsearch.js/commit/39701f8)) + + + + +## [1.8.12](https://github.com/algolia/instantsearch.js/compare/v1.8.11...v1.8.12) (2016-10-19) + + +### Bug Fixes + +* **numericRefinementList:** classes on radio buttons (#1358) (#1432) ([fec6495](https://github.com/algolia/instantsearch.js/commit/fec6495)) + + + + +## [1.8.11](https://github.com/algolia/instantsearch.js/compare/v1.8.10...v1.8.11) (2016-10-07) + + +### Bug Fixes + +* **merge:** merge only plain object from searchParameters ([aab1c87](https://github.com/algolia/instantsearch.js/commit/aab1c87)) + + + + +## [1.8.10](https://github.com/algolia/instantsearch.js/compare/v1.8.9...v1.8.10) (2016-10-07) + + +### Bug Fixes + +* **lodash:** set lodash back to 4.15.0, fixes build, unknown issue for now ([ba4247e](https://github.com/algolia/instantsearch.js/commit/ba4247e)) + + + + +## [1.8.9](https://github.com/algolia/instantsearch.js/compare/v1.8.8...v1.8.9) (2016-10-07) + + +### Bug Fixes + +* **react:** avoid duplicating React ([59010f6](https://github.com/algolia/instantsearch.js/commit/59010f6)), closes [#1386](https://github.com/algolia/instantsearch.js/issues/1386) + + + + +## [1.8.8](https://github.com/algolia/instantsearch.js/compare/v1.8.6...v1.8.8) (2016-09-14) + + +### Bug Fixes + +* **numericSelector:** do not change state on init (#1280) ([cf27db3](https://github.com/algolia/instantsearch.js/commit/cf27db3)), closes [#1253](https://github.com/algolia/instantsearch.js/issues/1253) +* **Slider:** default precision to 2 (#1279) ([552b9ea](https://github.com/algolia/instantsearch.js/commit/552b9ea)) + + + + +## [1.8.6](https://github.com/algolia/instantsearch.js/compare/v1.8.5...v1.8.6) (2016-09-12) + + + + +## [1.8.5](https://github.com/algolia/instantsearch.js/compare/v1.8.4...v1.8.5) (2016-09-06) + + +### Bug Fixes + +* **deps:** upgrade all deps 2016-09-05 (#1261) ([408d597](https://github.com/algolia/instantsearch.js/commit/408d597)) +* **rangeSlider:** round pips numbers when step is integer (#1255) ([b993033](https://github.com/algolia/instantsearch.js/commit/b993033)), closes [#1254](https://github.com/algolia/instantsearch.js/issues/1254) + + + + +## [1.8.4](https://github.com/algolia/instantsearch.js/compare/v1.8.3...v1.8.4) (2016-08-29) + + +### Bug Fixes + +* **bundle:** switch back to React by default, create a preact build (#1228) ([4845868](https://github.com/algolia/instantsearch.js/commit/4845868)) + + + + +## [1.8.3](https://github.com/algolia/instantsearch.js/compare/v1.8.2...v1.8.3) (2016-08-29) + + +### Bug Fixes + +* **numericSelector:** if no currentValue found, use the first option ([ef56dfa](https://github.com/algolia/instantsearch.js/commit/ef56dfa)) +* **poweredBy:** fixed Algolia logo version (#1223) ([aab3fc3](https://github.com/algolia/instantsearch.js/commit/aab3fc3)), closes [#1223](https://github.com/algolia/instantsearch.js/issues/1223) [#1222](https://github.com/algolia/instantsearch.js/issues/1222) +* **Selector:** render a controlled component ([e9f6ff7](https://github.com/algolia/instantsearch.js/commit/e9f6ff7)) + + +### Performance Improvements + +* **filesize:** use preact in production build (#1224) ([5bb38f2](https://github.com/algolia/instantsearch.js/commit/5bb38f2)), closes [#1030](https://github.com/algolia/instantsearch.js/issues/1030) + + + + +## [1.8.2](https://github.com/algolia/instantsearch.js/compare/v1.8.1...v1.8.2) (2016-08-25) + + +### Bug Fixes + +* **lodash:** use lodash v4, reduce build size ([216d1e0](https://github.com/algolia/instantsearch.js/commit/216d1e0)) + + + + +## [1.8.1](https://github.com/algolia/instantsearch.js/compare/v1.8.0...v1.8.1) (2016-08-24) + + +### Bug Fixes + +* **searchBox:** handle BFCache browsers (#1212) ([7deb9c3](https://github.com/algolia/instantsearch.js/commit/7deb9c3)) +* **toggle:** make autoHide check facetValue.count (#1213) ([86872eb](https://github.com/algolia/instantsearch.js/commit/86872eb)) + + + + +# [1.8.0](https://github.com/algolia/instantsearch.js/compare/v1.7.1...v1.8.0) (2016-08-18) + + +### Bug Fixes + +* **documentation:** Change instantsearch.widgets.stats typo data.processingTimMS to data.processingTimeMS ([034703e](https://github.com/algolia/instantsearch.js/commit/034703e)) +* **documentation:** Change responsiveNavigation.js & header.html to fix #1090 ([bf3a808](https://github.com/algolia/instantsearch.js/commit/bf3a808)), closes [#1090](https://github.com/algolia/instantsearch.js/issues/1090) +* **nouislider:** fix the slider for nouislider 8.5.1 ([af8f56b](https://github.com/algolia/instantsearch.js/commit/af8f56b)) + + +### Features + +* **clearAll:** Add optional excludeAttributes to list protected filters ([fe6d19c](https://github.com/algolia/instantsearch.js/commit/fe6d19c)) + + + + +## [1.7.1](https://github.com/algolia/instantsearch.js/compare/v1.7.0...v1.7.1) (2016-07-28) + + +### Bug Fixes + +* **toggle:** add backward compatibility for previous toggle implem (#1154) ([a1973a0](https://github.com/algolia/instantsearch.js/commit/a1973a0)) + + + + +# [1.7.0](https://github.com/algolia/instantsearch.js/compare/v1.6.4...v1.7.0) (2016-07-26) + + +### Bug Fixes + +* **searchParameters:** avoid mutating provided objects (#1148) ([0ea3bef](https://github.com/algolia/instantsearch.js/commit/0ea3bef)), closes [#1130](https://github.com/algolia/instantsearch.js/issues/1130) + + +### Features + +* **toggle:** Provide a better default widget (#1146) ([d54107e](https://github.com/algolia/instantsearch.js/commit/d54107e)), closes [#1096](https://github.com/algolia/instantsearch.js/issues/1096) [#919](https://github.com/algolia/instantsearch.js/issues/919) + + + + +## [1.6.4](https://github.com/algolia/instantsearch.js/compare/v1.6.3...v1.6.4) (2016-07-12) + + + + +## [1.6.3](https://github.com/algolia/instantsearch.js/compare/v1.6.2...v1.6.3) (2016-07-11) + + +### Bug Fixes + +* **Hits:** always render hits ([2e7bf8a](https://github.com/algolia/instantsearch.js/commit/2e7bf8a)), closes [#1100](https://github.com/algolia/instantsearch.js/issues/1100) + + + + +## [1.6.2](https://github.com/algolia/instantsearch.js/compare/v1.6.1...v1.6.2) (2016-07-11) + + +### Bug Fixes + +* **paginationLink:** it's aria-label not ariaLabel (#1125) ([70a190c](https://github.com/algolia/instantsearch.js/commit/70a190c)) +* **pricesRange:** fill the form according to the current refinement (#1126) ([12ebde7](https://github.com/algolia/instantsearch.js/commit/12ebde7)), closes [#1009](https://github.com/algolia/instantsearch.js/issues/1009) +* **rangeSlider:** handles now support stacking (#1129) ([ad394d3](https://github.com/algolia/instantsearch.js/commit/ad394d3)) +* **rangeSlider:** use stats min/max when only user min or max is provided (#1124) ([4348463](https://github.com/algolia/instantsearch.js/commit/4348463)), closes [#1004](https://github.com/algolia/instantsearch.js/issues/1004) +* **searchBox:** force cursor position to be at the end of the query (#1123) ([8a27769](https://github.com/algolia/instantsearch.js/commit/8a27769)), closes [#946](https://github.com/algolia/instantsearch.js/issues/946) +* **searchBox:** IE8, IE9 needs to listen for setQuery ([97c166a](https://github.com/algolia/instantsearch.js/commit/97c166a)) +* **searchBox:** update helper query on every keystroke (#1127) ([997c0c2](https://github.com/algolia/instantsearch.js/commit/997c0c2)), closes [#1015](https://github.com/algolia/instantsearch.js/issues/1015) +* **urlSync:** urls should be safe by default (#1104) ([db833c6](https://github.com/algolia/instantsearch.js/commit/db833c6)), closes [#982](https://github.com/algolia/instantsearch.js/issues/982) + + + + +## [1.6.1](https://github.com/algolia/instantsearch.js/compare/v1.6.0...v1.6.1) (2016-06-20) + + +### Bug Fixes + +* **meteorjs:** lite build must point to the browser lite (#1097) ([265ace3](https://github.com/algolia/instantsearch.js/commit/265ace3)) +* **toggle:** read numerical facet results stats for toggle count (#1098) ([1feb539](https://github.com/algolia/instantsearch.js/commit/1feb539)), closes [#1096](https://github.com/algolia/instantsearch.js/issues/1096) +* **website:** footer wording ([8355460](https://github.com/algolia/instantsearch.js/commit/8355460)) + + + + +# [1.6.0](https://github.com/algolia/instantsearch.js/compare/v1.5.2...v1.6.0) (2016-06-13) + + +### Bug Fixes + +* **hits:** rename __position to hitIndex ([d051a54](https://github.com/algolia/instantsearch.js/commit/d051a54)) +* **refinementList/header:** rename count to refinedFacetCount ([89ad602](https://github.com/algolia/instantsearch.js/commit/89ad602)) + +### Features + +* **header:** Pass count of current refined filters in header ([d9e8582](https://github.com/algolia/instantsearch.js/commit/d9e8582)), closes [#1013](https://github.com/algolia/instantsearch.js/issues/1013) [#1041](https://github.com/algolia/instantsearch.js/issues/1041) +* **hits:** Add a `__position` attribute to data passed to items ([43ce1c7](https://github.com/algolia/instantsearch.js/commit/43ce1c7)), closes [#903](https://github.com/algolia/instantsearch.js/issues/903) + + + + +## [1.5.2](https://github.com/algolia/instantsearch.js/compare/v1.5.1...v1.5.2) (2016-06-10) + + +### Bug Fixes + +* **lite:** use lite algoliasearch build (js client) ([219fa9f](https://github.com/algolia/instantsearch.js/commit/219fa9f)), closes [#1024](https://github.com/algolia/instantsearch.js/issues/1024) +* **poweredBy:** Let users define their own poweredBy template ([f1a96d8](https://github.com/algolia/instantsearch.js/commit/f1a96d8)) + + + + +## [1.5.1](https://github.com/algolia/instantsearch.js/compare/v1.5.0...v1.5.1) (2016-05-17) + + +### Bug Fixes + +* **numericRefinementList:** Correctly apply active class ([7cca9a4](https://github.com/algolia/instantsearch.js/commit/7cca9a4)), closes [#1010](https://github.com/algolia/instantsearch.js/issues/1010) + + + + +# [1.5.0](https://github.com/algolia/instantsearch.js/compare/v1.4.5...v1.5.0) (2016-04-29) + + +### Bug Fixes + +* **base href:** always create absolute URLS in widgets ([ae6dbf6](https://github.com/algolia/instantsearch.js/commit/ae6dbf6)), closes [#970](https://github.com/algolia/instantsearch.js/issues/970) +* **IE11:** classList do not supports .add(class, class) ([ab10347](https://github.com/algolia/instantsearch.js/commit/ab10347)), closes [#989](https://github.com/algolia/instantsearch.js/issues/989) +* **lifecycle:** save configuration done in widget.init ([07d1fea](https://github.com/algolia/instantsearch.js/commit/07d1fea)) +* **RefinementList:** use attributeNameKey when calling createURL ([253ec28](https://github.com/algolia/instantsearch.js/commit/253ec28)) +* **rootpath:** remember rootpath option on 'back' button ([01ecdaa](https://github.com/algolia/instantsearch.js/commit/01ecdaa)) +* **searchBox:** do not trigger a search when input value is the same ([81c2e80](https://github.com/algolia/instantsearch.js/commit/81c2e80)) +* **urlSync:** only start watching for changes at first render ([4a672ae](https://github.com/algolia/instantsearch.js/commit/4a672ae)) + +### Features + +* **urlSync:** allow overriding replaceState(state)/pushState(state) ([989856c](https://github.com/algolia/instantsearch.js/commit/989856c)) + + + + +## [1.4.5](https://github.com/algolia/instantsearch.js/compare/v1.4.4...v1.4.5) (2016-04-18) + + +### Bug Fixes + +* **showMore:** hide "show less" when nothing to hide ([5ac2bb6](https://github.com/algolia/instantsearch.js/commit/5ac2bb6)) + + + + +## [1.4.4](https://github.com/algolia/instantsearch.js/compare/v1.4.3...v1.4.4) (2016-04-15) + + +### Bug Fixes + +* **pagination:** Disabled pagination link can no longer be clicked ([88b567f](https://github.com/algolia/instantsearch.js/commit/88b567f)), closes [#974](https://github.com/algolia/instantsearch.js/issues/974) +* **showMore:** hide showMore when no more facet values to show ([cc31b1a](https://github.com/algolia/instantsearch.js/commit/cc31b1a)) + + + + +## [1.4.3](https://github.com/algolia/instantsearch.js/compare/v1.4.2...v1.4.3) (2016-04-01) + + +### Bug Fixes + +* **rangeSlider:** step accepts a float value ([6ecc925](https://github.com/algolia/instantsearch.js/commit/6ecc925)) + + + + +## [1.4.2](https://github.com/algolia/instantsearch.js/compare/v1.4.1...v1.4.2) (2016-03-24) + + +### Performance Improvements + +* **refinementList:** Stop creating URL for hidden refinements. ([2cdd17d](https://github.com/algolia/instantsearch.js/commit/2cdd17d)) + + + + +## [1.4.1](https://github.com/algolia/instantsearch.js/compare/v1.4.0...v1.4.1) (2016-03-22) + + +### Bug Fixes + +* **searchBox:** do not update the input when focused ([61cf9be](https://github.com/algolia/instantsearch.js/commit/61cf9be)), closes [#944](https://github.com/algolia/instantsearch.js/issues/944) + + + + +# [1.4.0](https://github.com/algolia/instantsearch.js/compare/v1.3.3...v1.4.0) (2016-03-16) + + +### Bug Fixes + +* **url:** allow hierarchical facets in trackedParameters ([36b4011](https://github.com/algolia/instantsearch.js/commit/36b4011)) + +### Features + +* **url-sync:** use the new mapping option ([f869885](https://github.com/algolia/instantsearch.js/commit/f869885)), closes [#838](https://github.com/algolia/instantsearch.js/issues/838) + + + + +## [1.3.3](https://github.com/algolia/instantsearch.js/compare/v1.3.2...v1.3.3) (2016-03-07) + + +### Bug Fixes + +* **headerFooter:** make collapsible click handler work ([add0d50](https://github.com/algolia/instantsearch.js/commit/add0d50)) + +### Performance Improvements + +* **linters:** Greatly improve the `npm run lint` task speed ([1ba53b0](https://github.com/algolia/instantsearch.js/commit/1ba53b0)) + + + + +## [1.3.2](https://github.com/algolia/instantsearch.js/compare/v1.3.1...v1.3.2) (2016-03-07) + + +### Bug Fixes + +* **Template:** stop leaking `data="[object Object]"` attributes in production builds ([7ec0431](https://github.com/algolia/instantsearch.js/commit/7ec0431)), closes [#899](https://github.com/algolia/instantsearch.js/issues/899) + +### Features + +* **validate-pr:** Allow `docs()` commits to be merged in master ([0abc689](https://github.com/algolia/instantsearch.js/commit/0abc689)) + + + + +## [1.3.1](https://github.com/algolia/instantsearch.js/compare/v1.3.0...v1.3.1) (2016-03-07) + + +### Bug Fixes + +* **collapsible:** stop duplicating collapsible styling ([7362901](https://github.com/algolia/instantsearch.js/commit/7362901)) +* **lodash:** stop leaking lodash in the global scope ([91f71dc](https://github.com/algolia/instantsearch.js/commit/91f71dc)), closes [#900](https://github.com/algolia/instantsearch.js/issues/900) + + + + +# [1.3.0](https://github.com/algolia/instantsearch.js/compare/v1.2.5...v1.3.0) (2016-03-04) + + +### Bug Fixes + +* **browser support:** make IE lte 10 work by fixing Object.getPrototypeOf ([bbb264b](https://github.com/algolia/instantsearch.js/commit/bbb264b)) +* **menu,refinementList:** sort by count AND name to avoid reorders on refine ([02fe7bf](https://github.com/algolia/instantsearch.js/commit/02fe7bf)), closes [#65](https://github.com/algolia/instantsearch.js/issues/65) +* **priceRanges:** pass the bound refine to the form ([ce2b956](https://github.com/algolia/instantsearch.js/commit/ce2b956)) +* **searchBox:** handle external updates of the query ([6a0af14](https://github.com/algolia/instantsearch.js/commit/6a0af14)), closes [#803](https://github.com/algolia/instantsearch.js/issues/803) +* **searchBox:** stop setting the query twice ([91270b2](https://github.com/algolia/instantsearch.js/commit/91270b2)) +* **searchBox:** stop updating query at eachkeystroke with searchOnEnterKeyPressOnly ([28dc4d2](https://github.com/algolia/instantsearch.js/commit/28dc4d2)), closes [#875](https://github.com/algolia/instantsearch.js/issues/875) +* **Slider:** do not render Slider when range.min === range.max ([f20274e](https://github.com/algolia/instantsearch.js/commit/f20274e)) +* **Template:** now render() when templateKey changes ([8906224](https://github.com/algolia/instantsearch.js/commit/8906224)) +* **toggle:** pass isRefined to toggleRefinement ([8ac494e](https://github.com/algolia/instantsearch.js/commit/8ac494e)) +* **url-sync:** always decode incoming query string ([bea38e3](https://github.com/algolia/instantsearch.js/commit/bea38e3)), closes [#848](https://github.com/algolia/instantsearch.js/issues/848) +* **url-sync:** handle href pages ([e58aadc](https://github.com/algolia/instantsearch.js/commit/e58aadc)), closes [#790](https://github.com/algolia/instantsearch.js/issues/790) + +### Features + +* **collapsable widgets:** add collapsable and collapsed option ([c4df7c5](https://github.com/algolia/instantsearch.js/commit/c4df7c5)) +* **instantsearch:** allow overriding the helper.search function ([9a930e7](https://github.com/algolia/instantsearch.js/commit/9a930e7)) +* **rangeSlider:** allow passing min and max values ([409295c](https://github.com/algolia/instantsearch.js/commit/409295c)), closes [#858](https://github.com/algolia/instantsearch.js/issues/858) +* **searchBox:** allow to pass a queryHook ([5786a64](https://github.com/algolia/instantsearch.js/commit/5786a64)) +* **Template:** allow template functions to return a React element ([748077d](https://github.com/algolia/instantsearch.js/commit/748077d)) +* **Template:** allow template functions to return a React element ([0f9296d](https://github.com/algolia/instantsearch.js/commit/0f9296d)) + +### Performance Improvements + +* **autoHideContainer:** stop re-creating React components ([8c89862](https://github.com/algolia/instantsearch.js/commit/8c89862)) +* **formatting numbers:** stop using a default locale, use the system one ([b056554](https://github.com/algolia/instantsearch.js/commit/b056554)) +* **nouislider:** upgrade nouislider, shaves some more ms ([fefbe65](https://github.com/algolia/instantsearch.js/commit/fefbe65)) +* **React:** use babel `optimisation` option for React ([95f940c](https://github.com/algolia/instantsearch.js/commit/95f940c)) +* **React, widgets:** implement shouldComponentUpdate, reduce bind ([5efaac1](https://github.com/algolia/instantsearch.js/commit/5efaac1)) + + + + +## [1.2.5](https://github.com/algolia/instantsearch.js/compare/v1.2.4...v1.2.5) (2016-03-02) + + +### Bug Fixes + +* **hierarchicalMenu:** configure maxValuesPerFacet using the limit option ([4868717](https://github.com/algolia/instantsearch.js/commit/4868717)), closes [#66](https://github.com/algolia/instantsearch.js/issues/66) + + + + +## [1.2.4](https://github.com/algolia/instantsearch.js/compare/v1.2.3...v1.2.4) (2016-02-29) + +Upgraded the helper to 2.9.0 to support undocumented parameters from the API. + + + +## [1.2.3](https://github.com/algolia/instantsearch.js/compare/v1.2.2...v1.2.3) (2016-02-18) + + +### Bug Fixes + +* **currentRefinedValues:** clear numeric refinements using original value ([9a0ad45](https://github.com/algolia/instantsearch.js/commit/9a0ad45)), closes [#844](https://github.com/algolia/instantsearch.js/issues/844) + + + + +## [1.2.2](https://github.com/algolia/instantsearch.js/compare/v1.2.1...v1.2.2) (2016-02-03) + + +### Features + +* **menu:** add showMore option ([e7e7677](https://github.com/algolia/instantsearch.js/commit/e7e7677)), closes [#815](https://github.com/algolia/instantsearch.js/issues/815) + + + + +## [1.2.1](https://github.com/algolia/instantsearch.js/compare/v1.2.0...v1.2.1) (2016-02-02) + + +### Bug Fixes + +* **showmore:** now showMore in doc and also show-more BEM ([a020439](https://github.com/algolia/instantsearch.js/commit/a020439)) + + + + +# [1.2.0](https://github.com/algolia/instantsearch.js/compare/v1.1.3...v1.2.0) (2016-02-02) + + +### Bug Fixes + +* **all:** typos ([fa8ba09](https://github.com/algolia/instantsearch.js/commit/fa8ba09)) +* **currentRefinedValues:** allow array of strings for cssClasses.* ([55b3a3f](https://github.com/algolia/instantsearch.js/commit/55b3a3f)) +* **docs:** fixed bad link to scss in custom themes section ([823a859](https://github.com/algolia/instantsearch.js/commit/823a859)) +* **getRefinements:** a name should be a string ([7efd1fd](https://github.com/algolia/instantsearch.js/commit/7efd1fd)) +* **getRefinements:** hierarchical facets ([fe0fc5d](https://github.com/algolia/instantsearch.js/commit/fe0fc5d)) +* **index:** Use module.exports instead of export on index ([81e7eee](https://github.com/algolia/instantsearch.js/commit/81e7eee)) +* **pagination:** remove default value of maxPages. Fixes #761 ([607fe9a](https://github.com/algolia/instantsearch.js/commit/607fe9a)), closes [#761](https://github.com/algolia/instantsearch.js/issues/761) +* **prepareTemplates:** uses templates with keys that are not in defaults ([c4bf8ec](https://github.com/algolia/instantsearch.js/commit/c4bf8ec)) +* **rangeSlider:** prevent slider from extending farther than the last pip ([6e534f5](https://github.com/algolia/instantsearch.js/commit/6e534f5)) +* **search-box:** update value when state changes from the outside ([4550f99](https://github.com/algolia/instantsearch.js/commit/4550f99)) +* **url-sync:** adds indexName in the helper configuration ([e50bafd](https://github.com/algolia/instantsearch.js/commit/e50bafd)) +* **url-sync:** Makes url sync more reliable ([3157abc](https://github.com/algolia/instantsearch.js/commit/3157abc)), closes [#730](https://github.com/algolia/instantsearch.js/issues/730) [#729](https://github.com/algolia/instantsearch.js/issues/729) + +### Features + +* **currentRefinedValues:** new widget ([6c926d0](https://github.com/algolia/instantsearch.js/commit/6c926d0)), closes [#404](https://github.com/algolia/instantsearch.js/issues/404) +* **hits:** adds allItems template as an alternative to item ([1f3f889](https://github.com/algolia/instantsearch.js/commit/1f3f889)) +* **poweredBy:** automatically add utm link to poweredBy ([05d1425](https://github.com/algolia/instantsearch.js/commit/05d1425)), closes [#711](https://github.com/algolia/instantsearch.js/issues/711) +* **priceRanges:** add currency option ([f41484a](https://github.com/algolia/instantsearch.js/commit/f41484a)) +* **refinementlist:** lets configure showmore feature ([3b8688a](https://github.com/algolia/instantsearch.js/commit/3b8688a)) +* **Template:** accepts any parameters and forwards them ([5170f53](https://github.com/algolia/instantsearch.js/commit/5170f53)) + + + + +## [1.1.3](https://github.com/algolia/instantsearch.js/compare/v1.1.2...v1.1.3) (2016-01-12) + + +### Bug Fixes + +* **searchBox:** fixes cssClasses option ([660ee2f](https://github.com/algolia/instantsearch.js/commit/660ee2f)), closes [#775](https://github.com/algolia/instantsearch.js/issues/775) + + + + +## [1.1.2](https://github.com/algolia/instantsearch.js/compare/v1.1.1...v1.1.2) (2016-01-08) + + + + + +## [1.1.1](https://github.com/algolia/instantsearch.js/compare/v1.1.0...v1.1.1) (2016-01-07) + + +### Bug Fixes + +* **style:** keyframes ([40eb0a5](https://github.com/algolia/instantsearch.js/commit/40eb0a5)) +* **url-sync:** adds indexName in the helper configuration ([c2c0bc7](https://github.com/algolia/instantsearch.js/commit/c2c0bc7)) + +### Features + +* **clearRefinements:** Added two utils methods ([49564e1](https://github.com/algolia/instantsearch.js/commit/49564e1)) + + + + +# [1.1.0](https://github.com/algolia/instantsearch.js/compare/v1.0.0...v1.1.0) (2015-11-26) + + +### Bug Fixes + +* **pagination:** fix #668 edge case ([d8f1196](https://github.com/algolia/instantsearch.js/commit/d8f1196)), closes [#668](https://github.com/algolia/instantsearch.js/issues/668) +* **priceRanges:** Remove round from first range ([bf82395](https://github.com/algolia/instantsearch.js/commit/bf82395)) +* **slider:** hide the slider when stats.min=stats.max ([42e4b64](https://github.com/algolia/instantsearch.js/commit/42e4b64)) +* **starRating:** Retrieve the correct count and use numericRefinement ([f00ce38](https://github.com/algolia/instantsearch.js/commit/f00ce38)), closes [#615](https://github.com/algolia/instantsearch.js/issues/615) + +### Features + +* **hierarchical:** expose rootPath and showParentLevel ([6e9bb7c](https://github.com/algolia/instantsearch.js/commit/6e9bb7c)) + + + + +# [1.0.0](https://github.com/algolia/instantsearch.js/compare/v0.14.9...v1.0.0) (2015-11-18) + + + + + +## [0.14.9](https://github.com/algolia/instantsearch.js/compare/v0.14.8...v0.14.9) (2015-11-18) + + + + + +## [0.14.8](https://github.com/algolia/instantsearch.js/compare/v0.14.7...v0.14.8) (2015-11-18) + + + + + +## [0.14.7](https://github.com/algolia/instantsearch.js/compare/v0.14.6...v0.14.7) (2015-11-18) + + + + + +## [0.14.6](https://github.com/algolia/instantsearch.js/compare/v0.14.5...v0.14.6) (2015-11-17) + + + + + +## [0.14.5](https://github.com/algolia/instantsearch.js/compare/v0.14.4...v0.14.5) (2015-11-17) + + + + + +## [0.14.4](https://github.com/algolia/instantsearch.js/compare/v0.14.3...v0.14.4) (2015-11-17) + + +### Bug Fixes + +* **doc:** Expand input on documentation page ([6814a14](https://github.com/algolia/instantsearch.js/commit/6814a14)) + + + + +## [0.14.3](https://github.com/algolia/instantsearch.js/compare/v0.14.2...v0.14.3) (2015-11-17) + + +### Bug Fixes + +* **examples:** media logo ([64f850e](https://github.com/algolia/instantsearch.js/commit/64f850e)) +* **website:** demos link to https ([b69c0f5](https://github.com/algolia/instantsearch.js/commit/b69c0f5)) + + + + +## [0.14.2](https://github.com/algolia/instantsearch.js/compare/v0.14.1...v0.14.2) (2015-11-17) + + +### Bug Fixes + +* **numericSelector:** pass currentValue as the refined value, not the full obj ([9286b4b](https://github.com/algolia/instantsearch.js/commit/9286b4b)) +* **website:** search icon ([623f071](https://github.com/algolia/instantsearch.js/commit/623f071)) + + + + +## [0.14.1](https://github.com/algolia/instantsearch.js/compare/v0.14.0...v0.14.1) (2015-11-16) + + +### Bug Fixes + +* **docs:** minor CSS fixes ([94fa868](https://github.com/algolia/instantsearch.js/commit/94fa868)), closes [#573](https://github.com/algolia/instantsearch.js/issues/573) + + + + +# [0.14.0](https://github.com/algolia/instantsearch.js/compare/v0.13.0...v0.14.0) (2015-11-13) + + +### Bug Fixes + +* **hierarchicalMenu:** handle limit option ([968cf58](https://github.com/algolia/instantsearch.js/commit/968cf58)), closes [#585](https://github.com/algolia/instantsearch.js/issues/585) [#235](https://github.com/algolia/instantsearch.js/issues/235) +* **numeric-selector:** makes init comply with the new API ([068e8d3](https://github.com/algolia/instantsearch.js/commit/068e8d3)) + +### Features + +* **core:** sends a custom User Agent ([2561154](https://github.com/algolia/instantsearch.js/commit/2561154)) +* **lifecycle:** makes init API consistent with the rest ([e7ed81f](https://github.com/algolia/instantsearch.js/commit/e7ed81f)) + +### BREAKING CHANGES + +* all widgets using "facetName" are now using "attributeName" + + +# [0.13.0](https://github.com/algolia/instantsearch.js/compare/v0.12.3...v0.13.0) (2015-11-12) + + +### Features + +* **clearAll:** New widget ([9e61a14](https://github.com/algolia/instantsearch.js/commit/9e61a14)) + + + + +## [0.12.3](https://github.com/algolia/instantsearch.js/compare/v0.12.2...v0.12.3) (2015-11-12) + + + + + +## [0.12.2](https://github.com/algolia/instantsearch.js/compare/v0.12.1...v0.12.2) (2015-11-12) + + +### Bug Fixes + +* **layout:** missing div (did we lost that fix?) ([9a515e4](https://github.com/algolia/instantsearch.js/commit/9a515e4)) + + + + +## [0.12.1](https://github.com/algolia/instantsearch.js/compare/v0.12.0...v0.12.1) (2015-11-12) + + +### Bug Fixes + +* **counts:** missing formatNumber calls ([65e5ba0](https://github.com/algolia/instantsearch.js/commit/65e5ba0)), closes [#560](https://github.com/algolia/instantsearch.js/issues/560) +* **doc:** ensure selector is not conflicting ([6528f2c](https://github.com/algolia/instantsearch.js/commit/6528f2c)), closes [#505](https://github.com/algolia/instantsearch.js/issues/505) +* **docs:** improved label/input hover debug ([58573db](https://github.com/algolia/instantsearch.js/commit/58573db)), closes [#503](https://github.com/algolia/instantsearch.js/issues/503) +* **examples/airbnb:** Use default theme from CDN ([f379c0a](https://github.com/algolia/instantsearch.js/commit/f379c0a)), closes [#522](https://github.com/algolia/instantsearch.js/issues/522) +* **examples/youtube:** use the default theme ([cf9a4b6](https://github.com/algolia/instantsearch.js/commit/cf9a4b6)) +* **rangeSlider:** fixed tooltip CSS & outdated default theme. ([c4be2ef](https://github.com/algolia/instantsearch.js/commit/c4be2ef)) + + + + +# [0.12.0](https://github.com/algolia/instantsearch.js/compare/v0.11.1...v0.12.0) (2015-11-10) + + +### Bug Fixes + +* **pagination:** Fix double BEM classes on elements ([2ede317](https://github.com/algolia/instantsearch.js/commit/2ede317)), closes [#500](https://github.com/algolia/instantsearch.js/issues/500) +* **price-ranges:** fix usage + add test ([89601d7](https://github.com/algolia/instantsearch.js/commit/89601d7)) +* **range-slider:** check usage + display (fixes #395) ([301643a](https://github.com/algolia/instantsearch.js/commit/301643a)), closes [#395](https://github.com/algolia/instantsearch.js/issues/395) +* **rangeSlider:** error when no result ([70e8554](https://github.com/algolia/instantsearch.js/commit/70e8554)) +* **theme:** Revert default spacing into pagination ([d755fd5](https://github.com/algolia/instantsearch.js/commit/d755fd5)) + + +### BREAKING CHANGES + +* pagination: Removes all `__disabled`, `__first`, `__last`, +`__next`, `__previous`, `__active` and `__page` classes added on the links in the +pagination. It only ads them to the parent `li`. Links instead now +have a `.ais-pagination--link` class + +Previously, the same CSS classes where added to both the `item` (`li`) and the +link inside it. I've split them in `--item` and `--link`. + +I've also made the various active/first/disabled/etc modifiers as +actual `__modifier` classes. + +I've updated the tests, the CSS skeleton, the examples and the docs +accordingly. + + + + +## [0.11.1](https://github.com/algolia/instantsearch.js/compare/v0.11.0...v0.11.1) (2015-11-10) + + + + + +# [0.11.0](https://github.com/algolia/instantsearch.js/compare/v0.10.0...v0.11.0) (2015-11-06) + +### Bug Fixes + +* **bem:** Make scss mixins actually follow BEM ([fcfb408](https://github.com/algolia/instantsearch.js/commit/fcfb408)) +* **doc:** bolder font for the navigation ([64f6d56](https://github.com/algolia/instantsearch.js/commit/64f6d56)) +* **InstantSearch:** throw error when init and render are not defined. Fixes #499 ([2830cd3](https://github.com/algolia/instantsearch.js/commit/2830cd3)), closes [#499](https://github.com/algolia/instantsearch.js/issues/499) +* **live-doc:** adds a start at a responsive display ([c83967e](https://github.com/algolia/instantsearch.js/commit/c83967e)) +* **live-doc:** adds navigation menu for smaller screens ([a6bb71e](https://github.com/algolia/instantsearch.js/commit/a6bb71e)) +* **live-doc:** fixes flow for texts ([3855071](https://github.com/algolia/instantsearch.js/commit/3855071)) +* **live-doc:** Momentum scroll for iPhone ([60a36ff](https://github.com/algolia/instantsearch.js/commit/60a36ff)) +* **live-doc:** uses only h4 and fixes style of h4 (mobile) ([0fdd2d0](https://github.com/algolia/instantsearch.js/commit/0fdd2d0)) +* **middle-click:** Allow middle click on links ([a7601c0](https://github.com/algolia/instantsearch.js/commit/a7601c0)) +* **range-slider:** Use lodash find instead of Array.prototype.find ([056153c](https://github.com/algolia/instantsearch.js/commit/056153c)) +* **searchBox:** handling pasting event with contextual menu. ([a172458](https://github.com/algolia/instantsearch.js/commit/a172458)), closes [#467](https://github.com/algolia/instantsearch.js/issues/467) +* **website:** defered doc scripts ([0c1324f](https://github.com/algolia/instantsearch.js/commit/0c1324f)) +* **website:** doc layout responsive ([a4dc894](https://github.com/algolia/instantsearch.js/commit/a4dc894)) +* **website:** fixed space overlay color animation ([200b8a7](https://github.com/algolia/instantsearch.js/commit/200b8a7)) +* **website:** Fixes & responsive stuff for doc ([7a8f920](https://github.com/algolia/instantsearch.js/commit/7a8f920)) +* **website:** footer markup ([95364a1](https://github.com/algolia/instantsearch.js/commit/95364a1)) +* **website:** home.js lint ([b70e06e](https://github.com/algolia/instantsearch.js/commit/b70e06e)) +* **website:** icon-theme didn't like svgo (to fix) ([38d84af](https://github.com/algolia/instantsearch.js/commit/38d84af)) +* **website:** image alt ([30cca29](https://github.com/algolia/instantsearch.js/commit/30cca29)) +* **website:** jsdelivr for every scripts ([06591d4](https://github.com/algolia/instantsearch.js/commit/06591d4)) +* **website:** Nav Icon + logo ([c1f419c](https://github.com/algolia/instantsearch.js/commit/c1f419c)) +* **website:** only load what's needed in bootstrap ([4843474](https://github.com/algolia/instantsearch.js/commit/4843474)) +* **website:** removed animation debug ([01ac079](https://github.com/algolia/instantsearch.js/commit/01ac079)) +* **website:** space bg fadeIn ([5e09844](https://github.com/algolia/instantsearch.js/commit/5e09844)) +* **website:** unclosed content block ([d42dc3e](https://github.com/algolia/instantsearch.js/commit/d42dc3e)) + +### Features + +* **hierarchicalMenu:** Adding indentation with default theme ([34885d2](https://github.com/algolia/instantsearch.js/commit/34885d2)) + + +### BREAKING CHANGES + +* hierarchicalMenu: Hierarchical menu levels 1 and 2 now have +a margin-left added in the default theme. + + + +# [0.10.0](https://github.com/algolia/instantsearch.js/compare/v0.9.0...v0.10.0) (2015-11-06) + + +### Bug Fixes + +* **api:** rename hideContainerWhenNoResults to autoHideContainer ([3f64bef](https://github.com/algolia/instantsearch.js/commit/3f64bef)), closes [#407](https://github.com/algolia/instantsearch.js/issues/407) +* **doc:** ensure the documentation content doesn't overflow ([1e28a4e](https://github.com/algolia/instantsearch.js/commit/1e28a4e)), closes [#444](https://github.com/algolia/instantsearch.js/issues/444) +* **hitsPerPageSelector:** Be more tolerant in options ([e14a344](https://github.com/algolia/instantsearch.js/commit/e14a344)) +* **numeric widgets:** synchronizes rounded value between widgets ([b314160](https://github.com/algolia/instantsearch.js/commit/b314160)) +* **numeric-refinement:** Replace Array.find with lodash find/includes ([b3e815c](https://github.com/algolia/instantsearch.js/commit/b3e815c)) +* **price-ranges:** makes it uses same operator as the slider ([ad6f5c2](https://github.com/algolia/instantsearch.js/commit/ad6f5c2)) +* **range-slider:** fixes bound definition ([e15c9b7](https://github.com/algolia/instantsearch.js/commit/e15c9b7)) +* **selector:** makes component as uncontrolled component ([1dda12a](https://github.com/algolia/instantsearch.js/commit/1dda12a)) +* **slider:** fixed `pip` propTypes constraint ([c77b7f4](https://github.com/algolia/instantsearch.js/commit/c77b7f4)) +* **website:** fix images path ([a3f62eb](https://github.com/algolia/instantsearch.js/commit/a3f62eb)) + +### Features + +* **searchBox:** ability to be non-instant ([b3ef871](https://github.com/algolia/instantsearch.js/commit/b3ef871)), closes [#458](https://github.com/algolia/instantsearch.js/issues/458) +* **toggle:** Allow custom on/off values ([9b6c2bf](https://github.com/algolia/instantsearch.js/commit/9b6c2bf)), closes [#409](https://github.com/algolia/instantsearch.js/issues/409) + +### Performance Improvements + +* **hitsPerPageSelector:** Use the correct lodash function ([be9aea7](https://github.com/algolia/instantsearch.js/commit/be9aea7)) + + +### BREAKING CHANGES + +* api: use autoHideContainer instead of +hideContainerWhenNoResults + + + + +# [0.9.0](https://github.com/algolia/instantsearch.js/compare/v0.8.2...v0.9.0) (2015-11-04) + + +### Features + +* **numericRefinementList:** create numericRefinementList widget using refinementList component ([a29e9c7](https://github.com/algolia/instantsearch.js/commit/a29e9c7)) + + + + +## [0.8.2](https://github.com/algolia/instantsearch.js/compare/v0.8.1...v0.8.2) (2015-11-04) + + +### Bug Fixes + +* **doc:** All wigdets in docs are not anymore linked together #fix #446 ([4361320](https://github.com/algolia/instantsearch.js/commit/4361320)), closes [#446](https://github.com/algolia/instantsearch.js/issues/446) +* **hitsPerPageSelector:** Issue when state did not have a `hitsPerPage` ([dc9371c](https://github.com/algolia/instantsearch.js/commit/dc9371c)) + + + + +## [0.8.1](https://github.com/algolia/instantsearch.js/compare/v0.8.0...v0.8.1) (2015-11-04) + + +### Bug Fixes + +* **hierarchicalMenu:** handle cases where no results after a search ([0a1d0ac](https://github.com/algolia/instantsearch.js/commit/0a1d0ac)), closes [#385](https://github.com/algolia/instantsearch.js/issues/385) + +### Features + +* **build:** allow building React based custom widgets ([cfbbfe4](https://github.com/algolia/instantsearch.js/commit/cfbbfe4)), closes [#373](https://github.com/algolia/instantsearch.js/issues/373) + + + + +# [0.8.0](https://github.com/algolia/instantsearch.js/compare/v0.7.0...v0.8.0) (2015-11-03) + + +### Bug Fixes + +* **cssClasses:** Fixed duplication of classNames ([e193f45](https://github.com/algolia/instantsearch.js/commit/e193f45)), closes [#388](https://github.com/algolia/instantsearch.js/issues/388) +* **doc:** add doctype were missing ([86a18aa](https://github.com/algolia/instantsearch.js/commit/86a18aa)) +* **doc:** new color scheme ([deccc17](https://github.com/algolia/instantsearch.js/commit/deccc17)) +* **doc:** only show a scrollbar when needed ([f2d955b](https://github.com/algolia/instantsearch.js/commit/f2d955b)) +* **hierarchical:** setPage 0 when toggling ([a976539](https://github.com/algolia/instantsearch.js/commit/a976539)), closes [#371](https://github.com/algolia/instantsearch.js/issues/371) +* **jsdoc:** use babel-node ([453dc21](https://github.com/algolia/instantsearch.js/commit/453dc21)) +* **live-doc:** generates missing ul ([b43e6e2](https://github.com/algolia/instantsearch.js/commit/b43e6e2)) +* **live-doc:** move scrollbars, removes useless ones ([548ae5f](https://github.com/algolia/instantsearch.js/commit/548ae5f)) +* **live-doc:** moves octocat link to top. Removes stackOverflow ([8ff6a79](https://github.com/algolia/instantsearch.js/commit/8ff6a79)) +* **live-doc:** Moves version in the main content ([27731c3](https://github.com/algolia/instantsearch.js/commit/27731c3)) +* **live-reload:** integrates the links into the menu flow ([c118051](https://github.com/algolia/instantsearch.js/commit/c118051)) +* **numerical widgets:** s/facetName/attributeName ([f209f5d](https://github.com/algolia/instantsearch.js/commit/f209f5d)), closes [#431](https://github.com/algolia/instantsearch.js/issues/431) +* **refinementList:** ensure the key reflects the underlying state ([b048f0b](https://github.com/algolia/instantsearch.js/commit/b048f0b)), closes [#398](https://github.com/algolia/instantsearch.js/issues/398) + +### Features + +* **examples:** try examples instead of themes ([bedffce](https://github.com/algolia/instantsearch.js/commit/bedffce)) +* **headerFooter:** Only add markup if a template is defined ([7a2d22d](https://github.com/algolia/instantsearch.js/commit/7a2d22d)), closes [#370](https://github.com/algolia/instantsearch.js/issues/370) +* **priceRanges:** Add BEM classes and tests ([ad58d7a](https://github.com/algolia/instantsearch.js/commit/ad58d7a)), closes [#387](https://github.com/algolia/instantsearch.js/issues/387) + + +### BREAKING CHANGES + +* numerical widgets: the priceRanges and rangeSlider widgets are now using `attributeName` instead of `facetName`. +* priceRanges: `ais-price-ranges--range` are now named +`ais-price-ranges--item` and are wrapped in +a `ais-price-ranges--list`. + +I've moved the bottom form into it's own PriceRangesForm component, +along with its own tests. I've fixed a minor typo where the component +was internally named PriceRange (without the final __s__). + +I factorize some logic form the render in individual methods and +manage to individually test them. This was not an easy task. I had to +mock the default `render` (so it does nothing) before instantiating +the component. Then, I was able to call each inner method +individually. This requires to stub prototype methods in beforeEach, +then restore them in afterEach. I've added a few helper methods, this +can surely be simplified again but this gives nice granularity in +testing. + +I've renamed the `range` items to `item` and wrapped them in a `list`. +I've also added classes to all elements we add (`label`, `separator`, +etc). I've removed the empty `span`s. +* headerFooter: The `
` and `
` markup is only added when +a `templates.{header,footer}` is passed. + + + + +# [0.7.0](https://github.com/algolia/instantsearch.js/compare/v0.6.5...v0.7.0) (2015-10-28) + + +### Features + +* **searchBox:** Add `wrapInput` option ([b327dbc](https://github.com/algolia/instantsearch.js/commit/b327dbc)) +* **urls:** ability to create an URL from a set of params ([9ca8369](https://github.com/algolia/instantsearch.js/commit/9ca8369)), closes [#372](https://github.com/algolia/instantsearch.js/issues/372) + + +### BREAKING CHANGES + +* urls: the instantsearch.createURL method is now taking a +simple JS object and not a SearchParameter instance anymore. +* searchBox: The `input` used by the search-box widget is now +wrapped in a `