├── src ├── Client │ ├── .openapi-generator │ │ ├── VERSION │ │ └── FILES │ ├── openapitools.json │ ├── lib │ │ ├── Lib │ │ │ ├── GuzzleHttp │ │ │ │ ├── Exception │ │ │ │ │ ├── TooManyRedirectsException.php │ │ │ │ │ ├── TransferException.php │ │ │ │ │ ├── InvalidArgumentException.php │ │ │ │ │ ├── ClientException.php │ │ │ │ │ ├── ServerException.php │ │ │ │ │ ├── GuzzleException.php │ │ │ │ │ ├── BadResponseException.php │ │ │ │ │ └── ConnectException.php │ │ │ │ ├── functions_include.php │ │ │ │ ├── Promise │ │ │ │ │ ├── CancellationException.php │ │ │ │ │ ├── PromisorInterface.php │ │ │ │ │ ├── AggregateException.php │ │ │ │ │ ├── TaskQueueInterface.php │ │ │ │ │ ├── Is.php │ │ │ │ │ ├── RejectionException.php │ │ │ │ │ ├── Create.php │ │ │ │ │ ├── TaskQueue.php │ │ │ │ │ ├── FulfilledPromise.php │ │ │ │ │ ├── RejectedPromise.php │ │ │ │ │ ├── Each.php │ │ │ │ │ ├── PromiseInterface.php │ │ │ │ │ └── Coroutine.php │ │ │ │ ├── Psr7 │ │ │ │ │ ├── Exception │ │ │ │ │ │ └── MalformedUriException.php │ │ │ │ │ ├── NoSeekStream.php │ │ │ │ │ ├── Rfc7230.php │ │ │ │ │ ├── LazyOpenStream.php │ │ │ │ │ ├── UriComparator.php │ │ │ │ │ ├── DroppingStream.php │ │ │ │ │ ├── InflateStream.php │ │ │ │ │ ├── HttpFactory.php │ │ │ │ │ ├── BufferStream.php │ │ │ │ │ ├── StreamDecoratorTrait.php │ │ │ │ │ ├── Query.php │ │ │ │ │ ├── Header.php │ │ │ │ │ ├── Request.php │ │ │ │ │ ├── StreamWrapper.php │ │ │ │ │ ├── LimitStream.php │ │ │ │ │ ├── CachingStream.php │ │ │ │ │ ├── FnStream.php │ │ │ │ │ └── PumpStream.php │ │ │ │ ├── BodySummarizerInterface.php │ │ │ │ ├── MessageFormatterInterface.php │ │ │ │ ├── Handler │ │ │ │ │ ├── CurlFactoryInterface.php │ │ │ │ │ ├── HeaderProcessor.php │ │ │ │ │ ├── CurlHandler.php │ │ │ │ │ ├── Proxy.php │ │ │ │ │ └── EasyHandle.php │ │ │ │ ├── BodySummarizer.php │ │ │ │ ├── Cookie │ │ │ │ │ ├── SessionCookieJar.php │ │ │ │ │ ├── CookieJarInterface.php │ │ │ │ │ └── FileCookieJar.php │ │ │ │ ├── ClientInterface.php │ │ │ │ ├── PrepareBodyMiddleware.php │ │ │ │ ├── TransferStats.php │ │ │ │ ├── RetryMiddleware.php │ │ │ │ └── Pool.php │ │ │ └── Psr │ │ │ │ └── Http │ │ │ │ ├── Client │ │ │ │ ├── ClientExceptionInterface.php │ │ │ │ ├── ClientInterface.php │ │ │ │ ├── RequestExceptionInterface.php │ │ │ │ └── NetworkExceptionInterface.php │ │ │ │ └── Message │ │ │ │ ├── UriFactoryInterface.php │ │ │ │ ├── RequestFactoryInterface.php │ │ │ │ ├── ResponseFactoryInterface.php │ │ │ │ ├── ServerRequestFactoryInterface.php │ │ │ │ ├── UploadedFileFactoryInterface.php │ │ │ │ ├── StreamFactoryInterface.php │ │ │ │ ├── ResponseInterface.php │ │ │ │ └── UploadedFileInterface.php │ │ ├── Model │ │ │ └── ModelInterface.php │ │ └── ApiException.php │ ├── .travis.yml │ ├── .gitignore │ ├── .php-cs-fixer.dist.php │ ├── .openapi-generator-ignore │ └── git_push.sh ├── Cron.php ├── ClientFactory.php ├── Uninstall.php ├── Setup.php ├── Admin │ ├── Messages.php │ ├── Filters.php │ ├── Provisioning │ │ ├── Integrations │ │ │ ├── EDD.php │ │ │ └── WooCommerce.php │ │ └── Integrations.php │ ├── Actions.php │ └── Settings │ │ └── Hooks.php ├── Integrations.php ├── Plugin.php ├── Integrations │ └── FormSubmit.php └── Filters.php ├── codecov.yml ├── assets └── src │ ├── css │ └── admin │ │ ├── main.min.css │ │ └── main.css │ └── js │ ├── integrations │ ├── form-submit.min.js │ ├── woocommerce.min.js │ ├── woocommerce.js │ └── form-submit.js │ └── affiliate-links.js ├── openapitools.json ├── postcss.config.js ├── uninstall.php ├── plausible-analytics.php ├── license.txt ├── tailwind.config.js ├── phpunit-with-integration.xml └── mu-plugin └── plausible-proxy-speed-module.php /src/Client/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 7.0.1 -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off -------------------------------------------------------------------------------- /assets/src/css/admin/main.min.css: -------------------------------------------------------------------------------- 1 | @tailwind base;@tailwind components; -------------------------------------------------------------------------------- /assets/src/css/admin/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | div[id$="_content"] .toggle-container { 6 | @apply mt-6; 7 | } 8 | -------------------------------------------------------------------------------- /openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "7.0.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // postcss.config.js 2 | module.exports = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | ...( process.env.NODE_ENV === 'production' ? { cssnano: {} } : {} ), 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/Client/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "7.3.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Exception/TooManyRedirectsException.php: -------------------------------------------------------------------------------- 1 | run(); 17 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/BodySummarizerInterface.php: -------------------------------------------------------------------------------- 1 | {let plausible_track_form_submit={forms:document.querySelectorAll("form"),init:function(){this.bindEvents()},bindEvents:function(){let self=this;this.forms.forEach(form=>{form.addEventListener("submit",e=>{if(e.target.checkValidity()){self.trackSubmission()}})})},trackSubmission:function(){plausible(plausible_analytics_i18n.form_completions,{props:{path:document.location.pathname}})}};plausible_track_form_submit.init()}); -------------------------------------------------------------------------------- /src/Client/.gitignore: -------------------------------------------------------------------------------- 1 | # ref: https://github.com/github/gitignore/blob/master/Composer.gitignore 2 | 3 | composer.phar 4 | /vendor/ 5 | 6 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control 7 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 8 | # composer.lock 9 | 10 | # php-cs-fixer cache 11 | .php_cs.cache 12 | .php-cs-fixer.cache 13 | 14 | # PHPUnit cache 15 | .phpunit.result.cache 16 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/AggregateException.php: -------------------------------------------------------------------------------- 1 | {let[resource,config]=args;if(config===undefined||config.body===undefined){return originalFetch(resource,config)}let data;try{data=JSON.parse(config.body)}catch(e){return originalFetch(resource,config)}if(data===null||data.requests===undefined||!data.requests instanceof Array){return originalFetch(resource,config)}data.requests.forEach(function(request){if(!request.path.includes("cart/add-item")){return}request.body._wp_http_referer=window.location.href});config.body=JSON.stringify(data);return originalFetch(resource,config)}; -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/TaskQueueInterface.php: -------------------------------------------------------------------------------- 1 | @,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; 22 | public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; 23 | } 24 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Handler/CurlFactoryInterface.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 9 | ->exclude('vendor') 10 | ->exclude('test') 11 | ->exclude('tests') 12 | ; 13 | 14 | $config = new PhpCsFixer\Config(); 15 | return $config->setRules([ 16 | '@PSR12' => true, 17 | 'phpdoc_order' => true, 18 | 'array_syntax' => [ 'syntax' => 'short' ], 19 | 'strict_comparison' => true, 20 | 'strict_param' => true, 21 | 'no_trailing_whitespace' => false, 22 | 'no_trailing_whitespace_in_comment' => false, 23 | 'braces' => false, 24 | 'single_blank_line_at_eof' => false, 25 | 'blank_line_after_namespace' => false, 26 | 'no_leading_import_slash' => false, 27 | ]) 28 | ->setFinder($finder) 29 | ; 30 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/BodySummarizer.php: -------------------------------------------------------------------------------- 1 | truncateAt = $truncateAt; 17 | } 18 | 19 | /** 20 | * Returns a summarized message body. 21 | */ 22 | public function summarize(MessageInterface $message): ?string 23 | { 24 | return $this->truncateAt === null 25 | ? \Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Psr7\Message::bodySummary($message) 26 | : \Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Psr7\Message::bodySummary($message, $this->truncateAt); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/src/js/integrations/woocommerce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plausible Analytics 3 | * 4 | * WooCommerce integration JS. 5 | */ 6 | const {fetch: originalFetch} = window; 7 | 8 | window.fetch = (...args) => { 9 | let [resource, config] = args; 10 | 11 | if (config === undefined || config.body === undefined) { 12 | return originalFetch(resource, config); 13 | } 14 | 15 | let data; 16 | 17 | try { 18 | data = JSON.parse(config.body); 19 | } catch (e) { 20 | return originalFetch(resource, config); 21 | } 22 | 23 | if (data === null || data.requests === undefined || !data.requests instanceof Array) { 24 | return originalFetch(resource, config); 25 | } 26 | 27 | data.requests.forEach(function (request) { 28 | if (!request.path.includes('cart/add-item')) { 29 | return; 30 | } 31 | 32 | request.body._wp_http_referer = window.location.href; 33 | }); 34 | 35 | config.body = JSON.stringify(data); 36 | 37 | return originalFetch(resource, config); 38 | }; 39 | -------------------------------------------------------------------------------- /assets/src/js/integrations/form-submit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plausible Analytics 3 | * 4 | * Track Form Submissions JS 5 | */ 6 | document.addEventListener('DOMContentLoaded', () => { 7 | let plausible_track_form_submit = { 8 | forms: document.querySelectorAll('form'), 9 | 10 | /** 11 | * Initialization. 12 | */ 13 | init: function () { 14 | this.bindEvents(); 15 | }, 16 | 17 | /** 18 | * Bind Events. 19 | */ 20 | bindEvents: function () { 21 | let self = this; 22 | 23 | this.forms.forEach((form) => { 24 | form.addEventListener('submit', (e) => { 25 | if (e.target.checkValidity()) { 26 | self.trackSubmission(); 27 | } 28 | }) 29 | }) 30 | }, 31 | 32 | /** 33 | * Send a custom event to Plausible. 34 | */ 35 | trackSubmission: function () { 36 | plausible(plausible_analytics_i18n.form_completions, {'props': {'path': document.location.pathname}}); 37 | } 38 | }; 39 | 40 | plausible_track_form_submit.init(); 41 | }); 42 | -------------------------------------------------------------------------------- /plausible-analytics.php: -------------------------------------------------------------------------------- 1 | register(); 31 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 Plausible Analytics 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/Psr/Http/Message/ServerRequestFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getState() === PromiseInterface::PENDING; 15 | } 16 | 17 | /** 18 | * Returns true if a promise is fulfilled or rejected. 19 | */ 20 | public static function settled(PromiseInterface $promise): bool 21 | { 22 | return $promise->getState() !== PromiseInterface::PENDING; 23 | } 24 | 25 | /** 26 | * Returns true if a promise is fulfilled. 27 | */ 28 | public static function fulfilled(PromiseInterface $promise): bool 29 | { 30 | return $promise->getState() === PromiseInterface::FULFILLED; 31 | } 32 | 33 | /** 34 | * Returns true if a promise is rejected. 35 | */ 36 | public static function rejected(PromiseInterface $promise): bool 37 | { 38 | return $promise->getState() === PromiseInterface::REJECTED; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | const plugin = require('tailwindcss/plugin'); 3 | 4 | module.exports = { 5 | content: ["./src/**/*.php", "./assets/src/js/admin/*.js"], 6 | theme: { 7 | extend: {}, 8 | }, 9 | darkMode: 'class', 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: '1rem', 14 | }, 15 | extend: { 16 | colors: { 17 | yellow: colors.amber, // We started using `yellow` in v2 but it was renamed to `amber` in v3 https://tailwindcss.com/docs/upgrade-guide#removed-color-aliases 18 | gray: colors.slate, 19 | 'gray-950': 'rgb(13, 18, 30)', 20 | 'gray-850': 'rgb(26, 32, 44)', 21 | 'gray-825': 'rgb(37, 47, 63)', 22 | }, 23 | spacing: { 24 | 44: '11rem', 25 | }, 26 | width: { 27 | content: 'fit-content', 28 | }, 29 | opacity: { 30 | 15: '0.15', 31 | }, 32 | zIndex: { 33 | 9: 9, 34 | }, 35 | maxWidth: { 36 | '2xs': '15rem', 37 | '3xs': '12rem', 38 | }, 39 | transitionProperty: { 40 | padding: 'padding', 41 | }, 42 | }, 43 | }, 44 | plugins: [ 45 | require('@tailwindcss/forms'), 46 | ], 47 | corePlugins: { 48 | preflight: false, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Exception/BadResponseException.php: -------------------------------------------------------------------------------- 1 | init(); 32 | } 33 | 34 | /** 35 | * Run 36 | * 37 | * @return void 38 | * @throws InvalidArgument 39 | * @throws Exception 40 | */ 41 | private function init() { 42 | $this->download(); 43 | } 44 | 45 | /** 46 | * Download the plausible.js file if the Proxy is enabled and downloads it to the uploads directory with an alias. 47 | * 48 | * @return bool 49 | * @throws InvalidArgument 50 | * @throws Exception 51 | */ 52 | private function download() { 53 | if ( ! Helpers::proxy_enabled() ) { 54 | return false; 55 | } 56 | 57 | $remote = Helpers::get_js_url(); 58 | $local = Helpers::get_js_path(); 59 | 60 | return Helpers::download_file( $remote, $local ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Handler/HeaderProcessor.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 35 | $this->mode = $mode; 36 | 37 | // unsetting the property forces the first access to go through 38 | // __get(). 39 | unset($this->stream); 40 | } 41 | 42 | /** 43 | * Creates the underlying stream lazily when required. 44 | */ 45 | protected function createStream(): StreamInterface 46 | { 47 | return Utils::streamFor(Utils::tryFopen($this->filename, $this->mode)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpunit-with-integration.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | tests 21 | 22 | 23 | 25 | 26 | src 27 | 28 | 29 | src/Client 30 | src/Admin/Settings 31 | src/Actions.php 32 | src/Cron.php 33 | src/Uninstall.php 34 | src/Admin/Actions.php 35 | src/Admin/Filters.php 36 | src/Admin/Messages.php 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/UriComparator.php: -------------------------------------------------------------------------------- 1 | getHost(), $modified->getHost()) !== 0) { 23 | return true; 24 | } 25 | 26 | if ($original->getScheme() !== $modified->getScheme()) { 27 | return true; 28 | } 29 | 30 | if (self::computePort($original) !== self::computePort($modified)) { 31 | return true; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | private static function computePort(UriInterface $uri): int 38 | { 39 | $port = $uri->getPort(); 40 | 41 | if (null !== $port) { 42 | return $port; 43 | } 44 | 45 | return 'https' === $uri->getScheme() ? 443 : 80; 46 | } 47 | 48 | private function __construct() 49 | { 50 | // cannot be instantiated 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/DroppingStream.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 30 | $this->maxLength = $maxLength; 31 | } 32 | 33 | public function write($string): int 34 | { 35 | $diff = $this->maxLength - $this->stream->getSize(); 36 | 37 | // Begin returning 0 when the underlying stream is too large. 38 | if ($diff <= 0) { 39 | return 0; 40 | } 41 | 42 | // Write the stream or a subset of the stream if needed. 43 | if (strlen($string) < $diff) { 44 | return $this->stream->write($string); 45 | } 46 | 47 | return $this->stream->write(substr($string, 0, $diff)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/RejectionException.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 24 | 25 | $message = 'The promise was rejected'; 26 | 27 | if ($description) { 28 | $message .= ' with reason: '.$description; 29 | } elseif (is_string($reason) 30 | || (is_object($reason) && method_exists($reason, '__toString')) 31 | ) { 32 | $message .= ' with reason: '.$this->reason; 33 | } elseif ($reason instanceof \JsonSerializable) { 34 | $message .= ' with reason: '.json_encode($this->reason, JSON_PRETTY_PRINT); 35 | } 36 | 37 | parent::__construct($message); 38 | } 39 | 40 | /** 41 | * Returns the rejection reason. 42 | * 43 | * @return mixed 44 | */ 45 | public function getReason() 46 | { 47 | return $this->reason; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/InflateStream.php: -------------------------------------------------------------------------------- 1 | 15 + 32]); 35 | $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Handler/CurlHandler.php: -------------------------------------------------------------------------------- 1 | factory = $options['handle_factory'] 34 | ?? new CurlFactory(3); 35 | } 36 | 37 | public function __invoke(RequestInterface $request, array $options): PromiseInterface 38 | { 39 | if (isset($options['delay'])) { 40 | \usleep($options['delay'] * 1000); 41 | } 42 | 43 | $easy = $this->factory->create($request, $options); 44 | \curl_exec($easy->handle); 45 | $easy->errno = \curl_errno($easy->handle); 46 | 47 | return CurlFactory::finish($this, $easy, $this->factory); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ClientFactory.php: -------------------------------------------------------------------------------- 1 | token = $token; 20 | } 21 | 22 | /** 23 | * Loads the Client class if all conditions are met. 24 | * 25 | * @return false|Client 26 | */ 27 | public function build() { 28 | /** 29 | * cURL or allow_url_fopen ini setting is required for GuzzleHttp to function properly. 30 | */ 31 | if ( ! extension_loaded( 'curl' ) && ! ini_get( 'allow_url_fopen' ) ) { 32 | add_action( 'init', [ $this, 'add_curl_error' ] ); // @codeCoverageIgnore 33 | 34 | return false; // @codeCoverageIgnore 35 | } 36 | 37 | if ( ! $this->token ) { 38 | $this->token = Helpers::get_settings()[ 'api_token' ]; 39 | } 40 | 41 | if ( ! $this->token ) { 42 | return false; 43 | } 44 | 45 | return new Client( $this->token ); 46 | } 47 | 48 | /** 49 | * Show an error on the settings screen if cURL isn't enabled on this machine. 50 | * 51 | * @return void 52 | * 53 | * @codeCoverageIgnore 54 | */ 55 | public function add_curl_error() { 56 | Messages::set_error( 57 | __( 58 | 'cURL is not enabled on this server, which means API provisioning will not work. Please contact your hosting provider to enable the cURL module or allow_url_fopen.', 59 | 'plausible-analytics' 60 | ) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/Psr/Http/Message/StreamFactoryInterface.php: -------------------------------------------------------------------------------- 1 | request = $request; 33 | $this->handlerContext = $handlerContext; 34 | } 35 | 36 | /** 37 | * Get the request that caused the exception 38 | */ 39 | public function getRequest(): RequestInterface 40 | { 41 | return $this->request; 42 | } 43 | 44 | /** 45 | * Get contextual information about the error from the underlying handler. 46 | * 47 | * The contents of this array will vary depending on which handler you are 48 | * using. It may also be just an empty array. Relying on this data will 49 | * couple you to a specific handler, but can give more debug information 50 | * when needed. 51 | */ 52 | public function getHandlerContext(): array 53 | { 54 | return $this->handlerContext; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Uninstall.php: -------------------------------------------------------------------------------- 1 | delete_options(); 29 | $this->delete_transients(); 30 | $this->delete_proxy_speed_module(); 31 | } 32 | 33 | /** 34 | * Delete options. 35 | * 36 | * @return void 37 | */ 38 | private function delete_options() { 39 | delete_option( 'plausible_analytics_settings' ); 40 | delete_option( 'plausible_analytics_version' ); 41 | delete_option( 'plausible_analytics_proxy_resources' ); 42 | delete_option( 'plausible_analytics_created_mu_plugins_dir' ); 43 | delete_option( 'plausible_analytics_proxy_speed_module_installed' ); 44 | } 45 | 46 | /** 47 | * Delete transients. 48 | * 49 | * @return void 50 | */ 51 | private function delete_transients() { 52 | delete_transient( 'plausible_analytics_notice_dismissed' ); 53 | delete_transient( 'plausible_analytics_notice' ); 54 | } 55 | 56 | /** 57 | * Deletes the Proxy Speed Module from the mu-plugins directory. 58 | * 59 | * @return void 60 | */ 61 | private function delete_proxy_speed_module() { 62 | $file_path = WP_CONTENT_DIR . '/mu-plugins/plausible-proxy-speed-module.php'; 63 | 64 | if ( file_exists( $file_path ) ) { 65 | unlink( $file_path ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Setup.php: -------------------------------------------------------------------------------- 1 | $message ], $expiration ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Integrations.php: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { %s });'; 16 | 17 | /** 18 | * Build class. 19 | */ 20 | public function __construct() { 21 | $this->init(); 22 | } 23 | 24 | /** 25 | * Run available integrations. 26 | * @return void 27 | */ 28 | private function init() { 29 | // WooCommerce 30 | if ( self::is_wc_active() && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ) { 31 | new Integrations\WooCommerce(); 32 | } 33 | 34 | // Easy Digital Downloads 35 | if ( self::is_edd_active() && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ) { 36 | new Integrations\EDD(); 37 | } 38 | 39 | // Form Plugins 40 | if ( Helpers::is_enhanced_measurement_enabled( 'form-completions' ) ) { 41 | new Integrations\FormSubmit(); 42 | } 43 | } 44 | 45 | /** 46 | * Checks if WooCommerce is installed and activated. 47 | * @return bool 48 | */ 49 | public static function is_wc_active() { 50 | return apply_filters( 'plausible_analytics_integrations_woocommerce', function_exists( 'WC' ) ); 51 | } 52 | 53 | /** 54 | * Checks if Easy Digital Downloads is installed and activated. 55 | * @return bool 56 | */ 57 | public static function is_edd_active() { 58 | return apply_filters( 'plausible_analytics_integrations_edd', function_exists( 'EDD' ) ); 59 | } 60 | 61 | /** 62 | * Checks if EDD Recurring is installed and activated. 63 | * @return mixed|null 64 | */ 65 | public static function is_edd_recurring_active() { 66 | return apply_filters( 'plausible_analytics_integrations_edd_recurring', function_exists( 'EDD_Recurring' ) ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Client/git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=$(git remote) 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | -------------------------------------------------------------------------------- /src/Client/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .php-cs-fixer.dist.php 3 | .travis.yml 4 | README.md 5 | git_push.sh 6 | lib/Api/DefaultApi.php 7 | lib/ApiException.php 8 | lib/Configuration.php 9 | lib/HeaderSelector.php 10 | lib/Model/Capabilities.php 11 | lib/Model/CapabilitiesFeatures.php 12 | lib/Model/CustomProp.php 13 | lib/Model/CustomPropCustomProp.php 14 | lib/Model/CustomPropDisableRequest.php 15 | lib/Model/CustomPropDisableRequestBulkDisable.php 16 | lib/Model/CustomPropEnableRequest.php 17 | lib/Model/CustomPropEnableRequestBulkEnable.php 18 | lib/Model/CustomPropListResponse.php 19 | lib/Model/Error.php 20 | lib/Model/Funnel.php 21 | lib/Model/FunnelCreateRequest.php 22 | lib/Model/FunnelCreateRequestFunnel.php 23 | lib/Model/FunnelCreateRequestFunnelStepsInner.php 24 | lib/Model/FunnelFunnel.php 25 | lib/Model/FunnelListResponse.php 26 | lib/Model/FunnelListResponseMeta.php 27 | lib/Model/Goal.php 28 | lib/Model/GoalCreateRequest.php 29 | lib/Model/GoalCreateRequestBulkGetOrCreate.php 30 | lib/Model/GoalCreateRequestCustomEvent.php 31 | lib/Model/GoalCreateRequestCustomEventGoal.php 32 | lib/Model/GoalCreateRequestPageview.php 33 | lib/Model/GoalCreateRequestPageviewGoal.php 34 | lib/Model/GoalCreateRequestRevenue.php 35 | lib/Model/GoalCreateRequestRevenueGoal.php 36 | lib/Model/GoalCustomEvent.php 37 | lib/Model/GoalCustomEventAllOfGoal.php 38 | lib/Model/GoalDeleteBulkRequest.php 39 | lib/Model/GoalListResponse.php 40 | lib/Model/GoalPageview.php 41 | lib/Model/GoalPageviewAllOfGoal.php 42 | lib/Model/GoalRevenue.php 43 | lib/Model/GoalRevenueAllOfGoal.php 44 | lib/Model/GoalType.php 45 | lib/Model/Link.php 46 | lib/Model/ModelInterface.php 47 | lib/Model/NotFoundError.php 48 | lib/Model/PaginationMetadata.php 49 | lib/Model/PaginationMetadataLinks.php 50 | lib/Model/PaymentRequiredError.php 51 | lib/Model/SharedLink.php 52 | lib/Model/SharedLinkCreateRequest.php 53 | lib/Model/SharedLinkCreateRequestSharedLink.php 54 | lib/Model/SharedLinkListResponse.php 55 | lib/Model/SharedLinkSharedLink.php 56 | lib/Model/UnauthorizedError.php 57 | lib/Model/UnprocessableEntityError.php 58 | lib/ObjectSerializer.php 59 | phpunit.xml.dist 60 | -------------------------------------------------------------------------------- /src/Admin/Filters.php: -------------------------------------------------------------------------------- 1 | base, 'plausible-analytics' ) ) { 35 | return sprintf( 36 | /* translators: %s: Link to 5 star rating */ __( 37 | 'If you like Plausible Analytics please leave us a %s rating. It takes a minute and helps a lot. Thanks in advance!', 38 | 'plausible-analytics' 39 | ), 40 | '★★★★★' 43 | ); 44 | } 45 | 46 | return $footer_text; 47 | } 48 | 49 | /** 50 | * Plugin page action links. 51 | * 52 | * @since 1.0.0 53 | * 54 | * @param array $actions An array of plugin action links. 55 | * 56 | * @return array 57 | */ 58 | public function add_plugin_action_links( $actions ) { 59 | $new_actions = [ 60 | 'settings' => sprintf( 61 | '%2$s', 62 | admin_url( 'admin.php?page=plausible_analytics' ), 63 | esc_html__( 'Settings', 'plausible-analytics' ) 64 | ), 65 | 'support' => sprintf( 66 | '%2$s', 67 | esc_url_raw( 'https://wordpress.org/support/plugin/plausible-analytics/' ), 68 | esc_html__( 'Support', 'plausible-analytics' ) 69 | ), 70 | ]; 71 | 72 | return array_merge( $new_actions, $actions ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Cookie/SessionCookieJar.php: -------------------------------------------------------------------------------- 1 | sessionKey = $sessionKey; 32 | $this->storeSessionCookies = $storeSessionCookies; 33 | $this->load(); 34 | } 35 | 36 | /** 37 | * Saves cookies to session when shutting down 38 | */ 39 | public function __destruct() 40 | { 41 | $this->save(); 42 | } 43 | 44 | /** 45 | * Save cookies to the client session 46 | */ 47 | public function save(): void 48 | { 49 | $json = []; 50 | /** @var SetCookie $cookie */ 51 | foreach ($this as $cookie) { 52 | if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { 53 | $json[] = $cookie->toArray(); 54 | } 55 | } 56 | 57 | $_SESSION[$this->sessionKey] = \json_encode($json); 58 | } 59 | 60 | /** 61 | * Load the contents of the client session into the data array 62 | */ 63 | protected function load(): void 64 | { 65 | if (!isset($_SESSION[$this->sessionKey])) { 66 | return; 67 | } 68 | $data = \json_decode($_SESSION[$this->sessionKey], true); 69 | if (\is_array($data)) { 70 | foreach ($data as $cookie) { 71 | $this->setCookie(new SetCookie($cookie)); 72 | } 73 | } elseif (\strlen($data)) { 74 | throw new \RuntimeException('Invalid cookie data'); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/Create.php: -------------------------------------------------------------------------------- 1 | then([$promise, 'resolve'], [$promise, 'reject']); 26 | 27 | return $promise; 28 | } 29 | 30 | return new FulfilledPromise($value); 31 | } 32 | 33 | /** 34 | * Creates a rejected promise for a reason if the reason is not a promise. 35 | * If the provided reason is a promise, then it is returned as-is. 36 | * 37 | * @param mixed $reason Promise or reason. 38 | */ 39 | public static function rejectionFor($reason): PromiseInterface 40 | { 41 | if ($reason instanceof PromiseInterface) { 42 | return $reason; 43 | } 44 | 45 | return new RejectedPromise($reason); 46 | } 47 | 48 | /** 49 | * Create an exception for a rejected promise value. 50 | * 51 | * @param mixed $reason 52 | */ 53 | public static function exceptionFor($reason): \Throwable 54 | { 55 | if ($reason instanceof \Throwable) { 56 | return $reason; 57 | } 58 | 59 | return new RejectionException($reason); 60 | } 61 | 62 | /** 63 | * Returns an iterator for the given value. 64 | * 65 | * @param mixed $value 66 | */ 67 | public static function iterFor($value): \Iterator 68 | { 69 | if ($value instanceof \Iterator) { 70 | return $value; 71 | } 72 | 73 | if (is_array($value)) { 74 | return new \ArrayIterator($value); 75 | } 76 | 77 | return new \ArrayIterator([$value]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/TaskQueue.php: -------------------------------------------------------------------------------- 1 | run(); 15 | * 16 | * @final 17 | */ 18 | class TaskQueue implements TaskQueueInterface 19 | { 20 | private $enableShutdown = true; 21 | private $queue = []; 22 | 23 | public function __construct(bool $withShutdown = true) 24 | { 25 | if ($withShutdown) { 26 | register_shutdown_function(function (): void { 27 | if ($this->enableShutdown) { 28 | // Only run the tasks if an E_ERROR didn't occur. 29 | $err = error_get_last(); 30 | if (!$err || ($err['type'] ^ E_ERROR)) { 31 | $this->run(); 32 | } 33 | } 34 | }); 35 | } 36 | } 37 | 38 | public function isEmpty(): bool 39 | { 40 | return !$this->queue; 41 | } 42 | 43 | public function add(callable $task): void 44 | { 45 | $this->queue[] = $task; 46 | } 47 | 48 | public function run(): void 49 | { 50 | while ($task = array_shift($this->queue)) { 51 | /** @var callable $task */ 52 | $task(); 53 | } 54 | } 55 | 56 | /** 57 | * The task queue will be run and exhausted by default when the process 58 | * exits IFF the exit is not the result of a PHP E_ERROR error. 59 | * 60 | * You can disable running the automatic shutdown of the queue by calling 61 | * this function. If you disable the task queue shutdown process, then you 62 | * MUST either run the task queue (as a result of running your event loop 63 | * or manually using the run() method) or wait on each outstanding promise. 64 | * 65 | * Note: This shutdown will occur before any destructors are triggered. 66 | */ 67 | public function disableShutdown(): void 68 | { 69 | $this->enableShutdown = false; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/FulfilledPromise.php: -------------------------------------------------------------------------------- 1 | value = $value; 31 | } 32 | 33 | public function then( 34 | ?callable $onFulfilled = null, 35 | ?callable $onRejected = null 36 | ): PromiseInterface { 37 | // Return itself if there is no onFulfilled function. 38 | if (!$onFulfilled) { 39 | return $this; 40 | } 41 | 42 | $queue = Utils::queue(); 43 | $p = new Promise([$queue, 'run']); 44 | $value = $this->value; 45 | $queue->add(static function () use ($p, $value, $onFulfilled): void { 46 | if (Is::pending($p)) { 47 | try { 48 | $p->resolve($onFulfilled($value)); 49 | } catch (\Throwable $e) { 50 | $p->reject($e); 51 | } 52 | } 53 | }); 54 | 55 | return $p; 56 | } 57 | 58 | public function otherwise(callable $onRejected): PromiseInterface 59 | { 60 | return $this->then(null, $onRejected); 61 | } 62 | 63 | public function wait(bool $unwrap = true) 64 | { 65 | return $unwrap ? $this->value : null; 66 | } 67 | 68 | public function getState(): string 69 | { 70 | return self::FULFILLED; 71 | } 72 | 73 | public function resolve($value): void 74 | { 75 | if ($value !== $this->value) { 76 | throw new \LogicException('Cannot resolve a fulfilled promise'); 77 | } 78 | } 79 | 80 | public function reject($reason): void 81 | { 82 | throw new \LogicException('Cannot reject a fulfilled promise'); 83 | } 84 | 85 | public function cancel(): void 86 | { 87 | // pass 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /mu-plugin/plausible-proxy-speed-module.php: -------------------------------------------------------------------------------- 1 | request_uri = $this->get_request_uri(); 35 | $this->is_proxy_request = $this->is_proxy_request(); 36 | 37 | $this->init(); 38 | } 39 | 40 | /** 41 | * Helper method to retrieve Request URI. Checks several globals. 42 | * 43 | * @return mixed 44 | */ 45 | private function get_request_uri() { 46 | return $_SERVER[ 'REQUEST_URI' ]; 47 | } 48 | 49 | /** 50 | * Check if current request is a proxy request. 51 | * 52 | * @return bool 53 | */ 54 | private function is_proxy_request() { 55 | $namespace = get_option( 'plausible_analytics_proxy_resources' )[ 'namespace' ] ?? ''; 56 | 57 | if ( ! $namespace ) { 58 | return false; 59 | } 60 | 61 | return strpos( $this->request_uri, $namespace ) !== false; 62 | } 63 | 64 | /** 65 | * Add filters and actions. 66 | * 67 | * @return void 68 | */ 69 | private function init() { 70 | add_filter( 'option_active_plugins', [ $this, 'filter_active_plugins' ] ); 71 | } 72 | 73 | /** 74 | * Filter the list of active plugins for custom endpoint requests. 75 | * 76 | * @param array $active_plugins The list of active plugins. 77 | * 78 | * @return array The filtered list of active plugins. 79 | */ 80 | public function filter_active_plugins( $active_plugins ) { 81 | if ( ! $this->is_proxy_request || ! is_array( $active_plugins ) ) { 82 | return $active_plugins; 83 | } 84 | 85 | $allowed_plugin_files = [ 'plausible-analytics.php' ]; 86 | $filtered_plugins = []; 87 | 88 | foreach ( $active_plugins as $plugin ) { 89 | foreach ( $allowed_plugin_files as $allowed_plugin_file ) { 90 | if ( strpos( $plugin, $allowed_plugin_file ) !== false ) { 91 | $filtered_plugins[] = $plugin; 92 | break; 93 | } 94 | } 95 | } 96 | 97 | return $filtered_plugins; 98 | } 99 | } 100 | 101 | new PlausibleProxySpeed(); 102 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | setup(); 20 | 21 | // Register services used throughout the plugin. (WP Rocket runs at priority 10) 22 | add_action( 'plugins_loaded', [ $this, 'register_services' ], 9 ); 23 | 24 | // Load text domain. 25 | add_action( 'init', [ $this, 'load_plugin_textdomain' ], 1000 ); 26 | } 27 | 28 | /** 29 | * Register plugin (de)activation hooks and cron job. 30 | * 31 | * @return void 32 | */ 33 | public function setup() { 34 | new Setup(); 35 | } 36 | 37 | /** 38 | * Registers the individual services of the plugin. 39 | * 40 | * @since 1.0.0 41 | * @access public 42 | * @return void 43 | */ 44 | public function register_services() { 45 | 46 | if ( is_admin() ) { 47 | add_action( 'init', [ $this, 'load_settings' ] ); 48 | add_action( 'init', [ $this, 'load_provisioning' ] ); 49 | 50 | new Admin\Upgrades(); 51 | new Admin\Filters(); 52 | new Admin\Actions(); 53 | new Admin\Module(); 54 | } 55 | 56 | add_action( 'init', [ $this, 'load_integrations' ] ); 57 | new Actions(); 58 | new Ajax(); 59 | new Compatibility(); 60 | new Filters(); 61 | new Proxy(); 62 | } 63 | 64 | /** 65 | * Load @see Admin\Settings\Page() 66 | * 67 | * @return void 68 | * 69 | * @codeCoverageIgnore 70 | */ 71 | public function load_settings() { 72 | new Admin\Settings\Page(); 73 | } 74 | 75 | /** 76 | * Load @see Admin\Provisioning() 77 | * 78 | * @return void 79 | * 80 | * @codeCoverageIgnore 81 | */ 82 | public function load_provisioning() { 83 | new Admin\Provisioning(); 84 | new Admin\Provisioning\Integrations(); 85 | } 86 | 87 | /** 88 | * Load @see Integrations() 89 | * 90 | * @return void 91 | * 92 | * @codeCoverageIgnore 93 | */ 94 | public function load_integrations() { 95 | new Integrations(); 96 | } 97 | 98 | /** 99 | * Loads the plugin's translated strings. 100 | * 101 | * @since 1.0.0 102 | * @access public 103 | * @return void 104 | * 105 | * @codeCoverageIgnore 106 | */ 107 | public function load_plugin_textdomain() { 108 | load_plugin_textdomain( 109 | 'plausible-analytics', 110 | false, 111 | dirname( plugin_basename( PLAUSIBLE_ANALYTICS_PLUGIN_FILE ) ) . '/languages/' 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/RejectedPromise.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 31 | } 32 | 33 | public function then( 34 | ?callable $onFulfilled = null, 35 | ?callable $onRejected = null 36 | ): PromiseInterface { 37 | // If there's no onRejected callback then just return self. 38 | if (!$onRejected) { 39 | return $this; 40 | } 41 | 42 | $queue = Utils::queue(); 43 | $reason = $this->reason; 44 | $p = new Promise([$queue, 'run']); 45 | $queue->add(static function () use ($p, $reason, $onRejected): void { 46 | if (Is::pending($p)) { 47 | try { 48 | // Return a resolved promise if onRejected does not throw. 49 | $p->resolve($onRejected($reason)); 50 | } catch (\Throwable $e) { 51 | // onRejected threw, so return a rejected promise. 52 | $p->reject($e); 53 | } 54 | } 55 | }); 56 | 57 | return $p; 58 | } 59 | 60 | public function otherwise(callable $onRejected): PromiseInterface 61 | { 62 | return $this->then(null, $onRejected); 63 | } 64 | 65 | public function wait(bool $unwrap = true) 66 | { 67 | if ($unwrap) { 68 | throw Create::exceptionFor($this->reason); 69 | } 70 | 71 | return null; 72 | } 73 | 74 | public function getState(): string 75 | { 76 | return self::REJECTED; 77 | } 78 | 79 | public function resolve($value): void 80 | { 81 | throw new \LogicException('Cannot resolve a rejected promise'); 82 | } 83 | 84 | public function reject($reason): void 85 | { 86 | if ($reason !== $this->reason) { 87 | throw new \LogicException('Cannot reject a rejected promise'); 88 | } 89 | } 90 | 91 | public function cancel(): void 92 | { 93 | // pass 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Admin/Provisioning/Integrations/EDD.php: -------------------------------------------------------------------------------- 1 | integrations = $integrations; 26 | 27 | $this->init(); 28 | } 29 | 30 | /** 31 | * Action and filter hooks. 32 | * 33 | * @return void 34 | */ 35 | private function init() { 36 | add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_create_edd_funnel' ], 10, 2 ); 37 | add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_delete_edd_goals' ], 11, 2 ); 38 | } 39 | 40 | /** 41 | * Creates an EDD purchase funnel if enhanced measurement is enabled and EDD is active. 42 | * 43 | * @param array $old_settings The previous settings before the update. 44 | * @param array $settings The updated settings array. 45 | * 46 | * @return void 47 | * 48 | * @codeCoverageIgnore Because it interacts with the Plugins API 49 | */ 50 | public function maybe_create_edd_funnel( $old_settings, $settings ) { 51 | if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_edd_active() ) { 52 | return; // @codeCoverageIgnore 53 | } 54 | 55 | $edd = new Integrations\EDD( false ); 56 | 57 | $this->integrations->create_integration_funnel( $edd->event_goals, __( 'EDD Purchase Funnel', 'plausible-analytics' ) ); 58 | } 59 | 60 | /** 61 | * * Delete all custom EDD event goals if Revenue setting is disabled. The funnel is deleted when the minimum 62 | * * required no. of goals is no longer met. 63 | * 64 | * @param array $old_settings The previous settings before the update. 65 | * @param array $settings The current updated settings. 66 | * 67 | * @return void 68 | * 69 | * @codeCoverageIgnore Because it interacts with the Plugins API. 70 | */ 71 | public function maybe_delete_edd_goals( $old_settings, $settings ) { 72 | $enhanced_measurements = array_filter( $settings[ 'enhanced_measurements' ] ); 73 | 74 | if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) ) { 75 | return; 76 | } 77 | 78 | $edd_integration = new Integrations\EDD( false ); 79 | 80 | $this->integrations->delete_integration_goals( $edd_integration ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Client/lib/Model/ModelInterface.php: -------------------------------------------------------------------------------- 1 | true ] 52 | ); 53 | 54 | wp_localize_script( 'plausible-admin', 'plausible_analytics_i18n', [ 'connected' => __( 'Connected', 'plausible-analytics' ) ] ); 55 | 56 | wp_enqueue_script( 'plausible-admin' ); 57 | 58 | wp_add_inline_script( 'plausible-admin', 'var plausible_analytics_hosted_domain = "' . Helpers::get_hosted_domain_url() . '";' ); 59 | } 60 | 61 | /** 62 | * Redirect to Configuration Wizard on first boot. 63 | * 64 | * @return void 65 | */ 66 | public function maybe_redirect_to_wizard() { 67 | // Make sure it only runs when requested by (an admin in) a browser. 68 | if ( wp_doing_ajax() || wp_doing_cron() || ! current_user_can( 'manage_options' ) ) { 69 | return; 70 | } 71 | 72 | // If we're already on the Settings page, there's no need to redirect. 73 | if ( array_key_exists( 'page', $_GET ) && $_GET[ 'page' ] === 'plausible_analytics' ) { 74 | return; 75 | } 76 | 77 | // Self-hosters should never be redirected to the settings screen, because the wizard isn't shown to them. 78 | $wizard_done = get_option( 'plausible_analytics_wizard_done', false ) || ! empty( Helpers::get_settings()[ 'self_hosted_domain' ] ); 79 | 80 | if ( ! $wizard_done ) { 81 | $url = admin_url( 'options-general.php?page=plausible_analytics#welcome_slide' ); 82 | 83 | wp_redirect( $url ); 84 | 85 | exit; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/Psr/Http/Message/ResponseInterface.php: -------------------------------------------------------------------------------- 1 | integrations = $integrations; 26 | 27 | $this->init(); 28 | } 29 | 30 | /** 31 | * Action & filters hooks. 32 | * 33 | * @return void 34 | */ 35 | private function init() { 36 | add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_create_woocommerce_funnel' ], 10, 2 ); 37 | add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_delete_woocommerce_goals' ], 11, 2 ); 38 | } 39 | 40 | /** 41 | * Checks whether the WooCommerce funnel should be created based on the provided settings 42 | * and creates the funnel if the conditions are met. 43 | * 44 | * @param array $old_settings The previous settings before the update. 45 | * @param array $settings The updated settings to check for enhanced measurement and WooCommerce integration. 46 | * 47 | * @return void 48 | * 49 | * @codeCoverageIgnore Because it interacts with the Plugins API. 50 | */ 51 | public function maybe_create_woocommerce_funnel( $old_settings, $settings ) { 52 | if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_wc_active() ) { 53 | return; // @codeCoverageIgnore 54 | } 55 | 56 | $woocommerce = new Integrations\WooCommerce( false ); 57 | 58 | $this->integrations->create_integration_funnel( $woocommerce->event_goals, __( 'Woo Purchase Funnel', 'plausible-analytics' ) ); 59 | } 60 | 61 | /** 62 | * Delete all custom WooCommerce event goals if Revenue setting is disabled. The funnel is deleted when the minimum 63 | * required no. of goals is no longer met. 64 | * 65 | * @param $old_settings 66 | * @param $settings 67 | * 68 | * @return void 69 | * 70 | * @codeCoverageIgnore Because we don't want to test if the API is working. 71 | */ 72 | public function maybe_delete_woocommerce_goals( $old_settings, $settings ) { 73 | $enhanced_measurements = array_filter( $settings[ 'enhanced_measurements' ] ); 74 | 75 | // Setting is enabled, no need to continue. 76 | if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) || ! Integrations::is_wc_active() ) { 77 | return; 78 | } 79 | 80 | $woo_integration = new Integrations\WooCommerce( false ); 81 | 82 | $this->integrations->delete_integration_goals( $woo_integration ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Handler/Proxy.php: -------------------------------------------------------------------------------- 1 | $onFulfilled, 33 | 'rejected' => $onRejected, 34 | ]))->promise(); 35 | } 36 | 37 | /** 38 | * Like of, but only allows a certain number of outstanding promises at any 39 | * given time. 40 | * 41 | * $concurrency may be an integer or a function that accepts the number of 42 | * pending promises and returns a numeric concurrency limit value to allow 43 | * for dynamic a concurrency size. 44 | * 45 | * @param mixed $iterable 46 | * @param int|callable $concurrency 47 | * @param callable $onFulfilled 48 | * @param callable $onRejected 49 | */ 50 | public static function ofLimit( 51 | $iterable, 52 | $concurrency, 53 | ?callable $onFulfilled = null, 54 | ?callable $onRejected = null 55 | ): PromiseInterface { 56 | return (new EachPromise($iterable, [ 57 | 'fulfilled' => $onFulfilled, 58 | 'rejected' => $onRejected, 59 | 'concurrency' => $concurrency, 60 | ]))->promise(); 61 | } 62 | 63 | /** 64 | * Like limit, but ensures that no promise in the given $iterable argument 65 | * is rejected. If any promise is rejected, then the aggregate promise is 66 | * rejected with the encountered rejection. 67 | * 68 | * @param mixed $iterable 69 | * @param int|callable $concurrency 70 | * @param callable $onFulfilled 71 | */ 72 | public static function ofLimitAll( 73 | $iterable, 74 | $concurrency, 75 | ?callable $onFulfilled = null 76 | ): PromiseInterface { 77 | return self::ofLimit( 78 | $iterable, 79 | $concurrency, 80 | $onFulfilled, 81 | function ($reason, $idx, PromiseInterface $aggregate): void { 82 | $aggregate->reject($reason); 83 | } 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/PromiseInterface.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | interface CookieJarInterface extends \Countable, \IteratorAggregate 21 | { 22 | /** 23 | * Create a request with added cookie headers. 24 | * 25 | * If no matching cookies are found in the cookie jar, then no Cookie 26 | * header is added to the request and the same request is returned. 27 | * 28 | * @param RequestInterface $request Request object to modify. 29 | * 30 | * @return RequestInterface returns the modified request. 31 | */ 32 | public function withCookieHeader(RequestInterface $request): RequestInterface; 33 | 34 | /** 35 | * Extract cookies from an HTTP response and store them in the CookieJar. 36 | * 37 | * @param RequestInterface $request Request that was sent 38 | * @param ResponseInterface $response Response that was received 39 | */ 40 | public function extractCookies(RequestInterface $request, ResponseInterface $response): void; 41 | 42 | /** 43 | * Sets a cookie in the cookie jar. 44 | * 45 | * @param SetCookie $cookie Cookie to set. 46 | * 47 | * @return bool Returns true on success or false on failure 48 | */ 49 | public function setCookie(SetCookie $cookie): bool; 50 | 51 | /** 52 | * Remove cookies currently held in the cookie jar. 53 | * 54 | * Invoking this method without arguments will empty the whole cookie jar. 55 | * If given a $domain argument only cookies belonging to that domain will 56 | * be removed. If given a $domain and $path argument, cookies belonging to 57 | * the specified path within that domain are removed. If given all three 58 | * arguments, then the cookie with the specified name, path and domain is 59 | * removed. 60 | * 61 | * @param string|null $domain Clears cookies matching a domain 62 | * @param string|null $path Clears cookies matching a domain and path 63 | * @param string|null $name Clears cookies matching a domain, path, and name 64 | */ 65 | public function clear( ?string $domain = null, ?string $path = null, ?string $name = null ): void; 66 | 67 | /** 68 | * Discard all sessions cookies. 69 | * 70 | * Removes cookies that don't have an expire field or a have a discard 71 | * field set to true. To be called when the user agent shuts down according 72 | * to RFC 2965. 73 | */ 74 | public function clearSessionCookies(): void; 75 | 76 | /** 77 | * Converts the cookie jar to an array. 78 | */ 79 | public function toArray(): array; 80 | } 81 | -------------------------------------------------------------------------------- /assets/src/js/affiliate-links.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cloaked (Affiliate) Links tracking JS 3 | * 4 | * @since 2.4.0 5 | */ 6 | 7 | const plausibleLinksTracking = { 8 | middleMouseButton: 1, 9 | 10 | /** 11 | * Intialize. 12 | */ 13 | init: () => { 14 | plausibleLinksTracking.bindEvents(); 15 | }, 16 | 17 | /** 18 | * Bind Events. 19 | */ 20 | bindEvents: () => { 21 | document.addEventListener('click', plausibleLinksTracking.handleLinkClick); 22 | document.addEventListener('auxclick', plausibleLinksTracking.handleLinkClick); 23 | }, 24 | 25 | /** 26 | * Handle Link Clicks. 27 | * 28 | * @param e 29 | */ 30 | handleLinkClick: (e) => { 31 | if (e.type === 'auxclick' && e.button !== plausibleLinksTracking.middleMouseButton) { 32 | return; 33 | } 34 | 35 | var link = plausibleLinksTracking.getLinkEl(e.target); 36 | 37 | if (link && plausibleLinksTracking.shouldTrackLink(link)) { 38 | var eventName = 'Cloaked Link: Click'; 39 | var eventProps = {url: link.href}; 40 | 41 | return plausibleLinksTracking.sendLinkClickEvent(e, link, eventName, eventProps); 42 | } 43 | }, 44 | 45 | /** 46 | * Retrieves a link element from an event target. 47 | * 48 | * @param link 49 | * 50 | * @returns {{href}|*} 51 | */ 52 | getLinkEl: (link) => { 53 | while (link && (typeof link.tagName === 'undefined' || link.tagName.toLowerCase() !== 'a' || !link.href)) { 54 | link = link.parentNode; 55 | } 56 | 57 | return link; 58 | }, 59 | 60 | /** 61 | * Should we track this link? 62 | * 63 | * @param link 64 | * @returns {boolean} 65 | */ 66 | shouldTrackLink: (link) => { 67 | let affiliateLinks = plausibleAffiliateLinks; 68 | 69 | let foundMatch = affiliateLinks.filter((affiliateLink) => { 70 | return link.href.match(affiliateLink); 71 | }); 72 | 73 | return foundMatch.length > 0; 74 | }, 75 | 76 | /** 77 | * Sends the click event to the Plausible API. 78 | * 79 | * @param event 80 | * @param link 81 | * @param eventName 82 | * @param eventProps 83 | */ 84 | sendLinkClickEvent: (event, link, eventName, eventProps) => { 85 | var followedLink = false; 86 | 87 | function followLink() { 88 | if (!followedLink) { 89 | followedLink = true; 90 | window.location = link.href; 91 | } 92 | } 93 | 94 | if (plausibleLinksTracking.shouldFollowLink(event, link)) { 95 | plausible(eventName, {props: eventProps, callback: followLink}); 96 | setTimeout(followLink, 5000); 97 | event.preventDefault(); 98 | } else { 99 | plausible(eventName, {props: eventProps}); 100 | } 101 | }, 102 | 103 | /** 104 | * 105 | * @param event 106 | * @param link 107 | * @returns {*|boolean} 108 | */ 109 | shouldFollowLink: (event, link) => { 110 | // If default has been prevented by an external script, Plausible should not intercept navigation. 111 | if (event.defaultPrevented) { 112 | return false; 113 | } 114 | 115 | var targetsCurrentWindow = !link.target || link.target.match(/^_(self|parent|top)$/i); 116 | var isRegularClick = !(event.ctrlKey || event.metaKey || event.shiftKey) && event.type === 'click'; 117 | 118 | return targetsCurrentWindow && isRegularClick; 119 | } 120 | } 121 | 122 | plausibleLinksTracking.init(); 123 | -------------------------------------------------------------------------------- /src/Client/lib/ApiException.php: -------------------------------------------------------------------------------- 1 | responseHeaders = $responseHeaders; 73 | $this->responseBody = $responseBody; 74 | } 75 | 76 | /** 77 | * Gets the HTTP response header 78 | * 79 | * @return string[]|null HTTP response header 80 | */ 81 | public function getResponseHeaders() { 82 | return $this->responseHeaders; 83 | } 84 | 85 | /** 86 | * Gets the HTTP body of the server response either as Json or string 87 | * 88 | * @return \stdClass|string|null HTTP body of the server response either as \stdClass or string 89 | */ 90 | public function getResponseBody() { 91 | return $this->responseBody; 92 | } 93 | 94 | /** 95 | * Gets the deserialized response object (during deserialization) 96 | * 97 | * @return mixed the deserialized response object 98 | */ 99 | public function getResponseObject() { 100 | return $this->responseObject; 101 | } 102 | 103 | /** 104 | * Sets the deserialized response object (during deserialization) 105 | * 106 | * @param mixed $obj Deserialized response object 107 | * 108 | * @return void 109 | */ 110 | public function setResponseObject( $obj ) { 111 | $this->responseObject = $obj; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Cookie/FileCookieJar.php: -------------------------------------------------------------------------------- 1 | filename = $cookieFile; 35 | $this->storeSessionCookies = $storeSessionCookies; 36 | 37 | if (\file_exists($cookieFile)) { 38 | $this->load($cookieFile); 39 | } 40 | } 41 | 42 | /** 43 | * Saves the file when shutting down 44 | */ 45 | public function __destruct() 46 | { 47 | $this->save($this->filename); 48 | } 49 | 50 | /** 51 | * Saves the cookies to a file. 52 | * 53 | * @param string $filename File to save 54 | * 55 | * @throws \RuntimeException if the file cannot be found or created 56 | */ 57 | public function save(string $filename): void 58 | { 59 | $json = []; 60 | /** @var SetCookie $cookie */ 61 | foreach ($this as $cookie) { 62 | if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { 63 | $json[] = $cookie->toArray(); 64 | } 65 | } 66 | 67 | $jsonStr = Utils::jsonEncode($json); 68 | if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) { 69 | throw new \RuntimeException("Unable to save file {$filename}"); 70 | } 71 | } 72 | 73 | /** 74 | * Load cookies from a JSON formatted file. 75 | * 76 | * Old cookies are kept unless overwritten by newly loaded ones. 77 | * 78 | * @param string $filename Cookie file to load. 79 | * 80 | * @throws \RuntimeException if the file cannot be loaded. 81 | */ 82 | public function load(string $filename): void 83 | { 84 | $json = \file_get_contents($filename); 85 | if (false === $json) { 86 | throw new \RuntimeException("Unable to load file {$filename}"); 87 | } 88 | if ($json === '') { 89 | return; 90 | } 91 | 92 | $data = Utils::jsonDecode($json, true); 93 | if (\is_array($data)) { 94 | foreach ($data as $cookie) { 95 | $this->setCookie(new SetCookie($cookie)); 96 | } 97 | } elseif (\is_scalar($data) && !empty($data)) { 98 | throw new \RuntimeException("Invalid cookie file: {$filename}"); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Admin/Provisioning/Integrations.php: -------------------------------------------------------------------------------- 1 | provisioning = new Provisioning(); 26 | 27 | $this->init(); 28 | } 29 | 30 | /** 31 | * Action & filter hooks. 32 | * 33 | * We use Dependency Injection to prevent circular dependency. 34 | * 35 | * @return void 36 | * @codeCoverageIgnore This is merely a wrapper to load classes. No need to test. 37 | */ 38 | private function init() { 39 | new Integrations\WooCommerce( $this ); 40 | new Integrations\EDD( $this ); 41 | } 42 | 43 | /** 44 | * @param array $event_goals 45 | * @param string $funnel_name 46 | * 47 | * @return void 48 | * @codeCoverageIgnore We don't want to test the API. 49 | */ 50 | public function create_integration_funnel( $event_goals, $funnel_name ) { 51 | $goals = []; 52 | 53 | foreach ( $event_goals as $event_key => $event_goal ) { 54 | // Don't add this goal to the funnel. Create it separately instead. 55 | if ( $event_key === 'remove-from-cart' ) { 56 | $this->provisioning->create_goals( [ $this->provisioning->create_goal_request( $event_goal ) ] ); 57 | 58 | continue; 59 | } 60 | 61 | if ( $event_key === 'purchase' ) { 62 | if ( \Plausible\Analytics\WP\Integrations::is_edd_active() ) { 63 | $currency = edd_get_currency(); 64 | } else { 65 | $currency = get_woocommerce_currency(); 66 | } 67 | 68 | $goals[] = $this->provisioning->create_goal_request( $event_goal, 'Revenue', $currency ); 69 | 70 | continue; 71 | } 72 | 73 | if ( $event_key === 'view-product' ) { 74 | $path = preg_replace( '/^.*?\//', '', $event_goal ); 75 | $goals[] = $this->provisioning->create_goal_request( $event_goal, 'Pageview', null, '/' . $path ); 76 | 77 | continue; 78 | } 79 | 80 | $goals[] = $this->provisioning->create_goal_request( $event_goal ); 81 | } 82 | 83 | $this->provisioning->create_funnel( $funnel_name, $goals ); 84 | } 85 | 86 | /** 87 | * Deletes the integration-specific goals using the stored goal IDs. 88 | * 89 | * @param object $integration The integration object containing event goals to be deleted. 90 | * 91 | * @return void 92 | * @codeCoverageIgnore Because we don't want to test the API. 93 | */ 94 | public function delete_integration_goals( $integration ) { 95 | $goals = get_option( 'plausible_analytics_enhanced_measurements_goal_ids', [] ); 96 | 97 | foreach ( $goals as $id => $name ) { 98 | $key = $this->provisioning->array_search_contains( $name, $integration->event_goals ); 99 | 100 | if ( $key ) { 101 | $this->provisioning->client->delete_goal( $id ); 102 | 103 | unset( $goals[ $id ] ); 104 | } 105 | } 106 | 107 | // Refresh the stored IDs in the DB. 108 | update_option( 'plausible_analytics_enhanced_measurements_goal_ids', $goals ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/ClientInterface.php: -------------------------------------------------------------------------------- 1 | headers); 72 | 73 | $normalizedKeys = Utils::normalizeHeaderKeys($headers); 74 | 75 | if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) { 76 | $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; 77 | unset($headers[$normalizedKeys['content-encoding']]); 78 | if (isset($normalizedKeys['content-length'])) { 79 | $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; 80 | 81 | $bodyLength = (int) $this->sink->getSize(); 82 | if ($bodyLength) { 83 | $headers[$normalizedKeys['content-length']] = $bodyLength; 84 | } else { 85 | unset($headers[$normalizedKeys['content-length']]); 86 | } 87 | } 88 | } 89 | 90 | // Attach a response to the easy handle with the parsed headers. 91 | $this->response = new Response( 92 | $status, 93 | $headers, 94 | $this->sink, 95 | $ver, 96 | $reason 97 | ); 98 | } 99 | 100 | /** 101 | * @param string $name 102 | * 103 | * @return void 104 | * 105 | * @throws \BadMethodCallException 106 | */ 107 | public function __get($name) 108 | { 109 | $msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: '.$name; 110 | throw new \BadMethodCallException($msg); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/PrepareBodyMiddleware.php: -------------------------------------------------------------------------------- 1 | nextHandler = $nextHandler; 27 | } 28 | 29 | public function __invoke(RequestInterface $request, array $options): PromiseInterface 30 | { 31 | $fn = $this->nextHandler; 32 | 33 | // Don't do anything if the request has no body. 34 | if ($request->getBody()->getSize() === 0) { 35 | return $fn($request, $options); 36 | } 37 | 38 | $modify = []; 39 | 40 | // Add a default content-type if possible. 41 | if (!$request->hasHeader('Content-Type')) { 42 | if ($uri = $request->getBody()->getMetadata('uri')) { 43 | if (is_string($uri) && $type = Psr7\MimeType::fromFilename($uri)) { 44 | $modify['set_headers']['Content-Type'] = $type; 45 | } 46 | } 47 | } 48 | 49 | // Add a default content-length or transfer-encoding header. 50 | if (!$request->hasHeader('Content-Length') 51 | && !$request->hasHeader('Transfer-Encoding') 52 | ) { 53 | $size = $request->getBody()->getSize(); 54 | if ($size !== null) { 55 | $modify['set_headers']['Content-Length'] = $size; 56 | } else { 57 | $modify['set_headers']['Transfer-Encoding'] = 'chunked'; 58 | } 59 | } 60 | 61 | // Add the expect header if needed. 62 | $this->addExpectHeader($request, $options, $modify); 63 | 64 | return $fn(Psr7\Utils::modifyRequest($request, $modify), $options); 65 | } 66 | 67 | /** 68 | * Add expect header 69 | */ 70 | private function addExpectHeader(RequestInterface $request, array $options, array &$modify): void 71 | { 72 | // Determine if the Expect header should be used 73 | if ($request->hasHeader('Expect')) { 74 | return; 75 | } 76 | 77 | $expect = $options['expect'] ?? null; 78 | 79 | // Return if disabled or if you're not using HTTP/1.1 or HTTP/2.0 80 | if ($expect === false || $request->getProtocolVersion() < 1.1) { 81 | return; 82 | } 83 | 84 | // The expect header is unconditionally enabled 85 | if ($expect === true) { 86 | $modify['set_headers']['Expect'] = '100-Continue'; 87 | 88 | return; 89 | } 90 | 91 | // By default, send the expect header when the payload is > 1mb 92 | if ($expect === null) { 93 | $expect = 1048576; 94 | } 95 | 96 | // Always add if the body cannot be rewound, the size cannot be 97 | // determined, or the size is greater than the cutoff threshold 98 | $body = $request->getBody(); 99 | $size = $body->getSize(); 100 | 101 | if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { 102 | $modify['set_headers']['Expect'] = '100-Continue'; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/HttpFactory.php: -------------------------------------------------------------------------------- 1 | getSize(); 37 | } 38 | 39 | return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); 40 | } 41 | 42 | public function createStream(string $content = ''): StreamInterface 43 | { 44 | return Utils::streamFor($content); 45 | } 46 | 47 | public function createStreamFromFile(string $file, string $mode = 'r'): StreamInterface 48 | { 49 | try { 50 | $resource = Utils::tryFopen($file, $mode); 51 | } catch (\RuntimeException $e) { 52 | if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { 53 | throw new \InvalidArgumentException(sprintf('Invalid file opening mode "%s"', $mode), 0, $e); 54 | } 55 | 56 | throw $e; 57 | } 58 | 59 | return Utils::streamFor($resource); 60 | } 61 | 62 | public function createStreamFromResource($resource): StreamInterface 63 | { 64 | return Utils::streamFor($resource); 65 | } 66 | 67 | public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface 68 | { 69 | if (empty($method)) { 70 | if (!empty($serverParams['REQUEST_METHOD'])) { 71 | $method = $serverParams['REQUEST_METHOD']; 72 | } else { 73 | throw new \InvalidArgumentException('Cannot determine HTTP method'); 74 | } 75 | } 76 | 77 | return new ServerRequest($method, $uri, [], null, '1.1', $serverParams); 78 | } 79 | 80 | public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface 81 | { 82 | return new Response($code, [], null, '1.1', $reasonPhrase); 83 | } 84 | 85 | public function createRequest(string $method, $uri): RequestInterface 86 | { 87 | return new Request($method, $uri); 88 | } 89 | 90 | public function createUri(string $uri = ''): UriInterface 91 | { 92 | return new Uri($uri); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Integrations/FormSubmit.php: -------------------------------------------------------------------------------- 1 | init(); 21 | } 22 | 23 | /** 24 | * Init 25 | * 26 | * @return void 27 | * 28 | * @codeCoverageIgnore 29 | */ 30 | private function init() { 31 | /** 32 | * Adds required JS and classes. 33 | */ 34 | add_action( 'wp_enqueue_scripts', [ $this, 'add_js' ], 1 ); 35 | /** 36 | * Contact Form 7 doesn't respect JS checkValidity() function, so this is a custom compatibility fix. 37 | */ 38 | add_filter( 'wpcf7_validate', [ $this, 'maybe_track_submission' ], 10, 2 ); 39 | /** 40 | * Gravity Forms contains its own form submission handler, so this is a custom compatibility fix. 41 | */ 42 | add_action( 'gform_after_submission', [ $this, 'track_gravity_forms_submission' ], 10 ); 43 | } 44 | 45 | /** 46 | * Enqueues the required JavaScript for form submissions integration. 47 | * @return void 48 | * 49 | * @codeCoverageIgnore because there's nothing to test here. 50 | */ 51 | public function add_js() { 52 | wp_register_script( 53 | 'plausible-form-submit-integration', 54 | PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-form-submit-integration.js', 55 | [ 'plausible-analytics' ], 56 | filemtime( PLAUSIBLE_ANALYTICS_PLUGIN_DIR . 'assets/dist/js/plausible-form-submit-integration.js' ) 57 | ); 58 | 59 | wp_localize_script( 60 | 'plausible-form-submit-integration', 61 | 'plausible_analytics_i18n', 62 | [ 'form_completions' => __( 'WP Form Completions', 'plausible-analytics' ), ] 63 | ); 64 | 65 | wp_enqueue_script( 'plausible-form-submit-integration' ); 66 | } 67 | 68 | /** 69 | * Tracks the form submission if CF7 says it's valid. 70 | * 71 | * @filter wpcf7_validate 72 | * 73 | * @param \WPCF7_Validation $result Form submission result object containing validation results. 74 | * @param array $tags Array of tags associated with the form fields. 75 | * 76 | * @return \WPCF7_Validation 77 | * 78 | * @codeCoverageIgnore because we can't test XHR requests here. 79 | */ 80 | public function maybe_track_submission( $result, $tags ) { 81 | $invalid_fields = $result->get_invalid_fields(); 82 | 83 | if ( empty( $invalid_fields ) ) { 84 | $post = get_post( $_POST[ '_wpcf7_container_post' ] ); 85 | $uri = '/' . $post->post_name . '/'; 86 | 87 | $this->track_submission( $uri ); 88 | } 89 | 90 | return $result; 91 | } 92 | 93 | /** 94 | * Track submission using the Proxy. 95 | * 96 | * @param $uri 97 | * 98 | * @return void 99 | * 100 | * @codeCoverageIgnore because we can't test XHR requests here. 101 | */ 102 | private function track_submission( $uri ) { 103 | $proxy = new Proxy( false ); 104 | 105 | $proxy->do_request( 106 | __( 'WP Form Completions', 'plausible-analytics' ), 107 | null, 108 | null, 109 | [ 'path' => $uri ] 110 | ); 111 | } 112 | 113 | /** 114 | * Compatibility fix for Gravity Forms. 115 | * 116 | * @action gform_after_submission 117 | * 118 | * @param $form 119 | * @param $entry 120 | * 121 | * @return void 122 | * 123 | * @codeCoverageIgnore because we can't test XHR requests here. 124 | */ 125 | public function track_gravity_forms_submission( $form ) { 126 | $uri = str_replace( home_url(), '', $form[ 'source_url' ] ) ?? ''; 127 | 128 | if ( empty( $uri ) ) { 129 | return; 130 | } 131 | 132 | $this->track_submission( $uri ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/TransferStats.php: -------------------------------------------------------------------------------- 1 | request = $request; 55 | $this->response = $response; 56 | $this->transferTime = $transferTime; 57 | $this->handlerErrorData = $handlerErrorData; 58 | $this->handlerStats = $handlerStats; 59 | } 60 | 61 | public function getRequest(): RequestInterface 62 | { 63 | return $this->request; 64 | } 65 | 66 | /** 67 | * Returns the response that was received (if any). 68 | */ 69 | public function getResponse(): ?ResponseInterface 70 | { 71 | return $this->response; 72 | } 73 | 74 | /** 75 | * Returns true if a response was received. 76 | */ 77 | public function hasResponse(): bool 78 | { 79 | return $this->response !== null; 80 | } 81 | 82 | /** 83 | * Gets handler specific error data. 84 | * 85 | * This might be an exception, a integer representing an error code, or 86 | * anything else. Relying on this value assumes that you know what handler 87 | * you are using. 88 | * 89 | * @return mixed 90 | */ 91 | public function getHandlerErrorData() 92 | { 93 | return $this->handlerErrorData; 94 | } 95 | 96 | /** 97 | * Get the effective URI the request was sent to. 98 | */ 99 | public function getEffectiveUri(): UriInterface 100 | { 101 | return $this->request->getUri(); 102 | } 103 | 104 | /** 105 | * Get the estimated time the request was being transferred by the handler. 106 | * 107 | * @return float|null Time in seconds. 108 | */ 109 | public function getTransferTime(): ?float 110 | { 111 | return $this->transferTime; 112 | } 113 | 114 | /** 115 | * Gets an array of all of the handler specific transfer data. 116 | */ 117 | public function getHandlerStats(): array 118 | { 119 | return $this->handlerStats; 120 | } 121 | 122 | /** 123 | * Get a specific handler statistic from the handler by name. 124 | * 125 | * @param string $stat Handler specific transfer stat to retrieve. 126 | * 127 | * @return mixed|null 128 | */ 129 | public function getHandlerStat(string $stat) 130 | { 131 | return $this->handlerStats[$stat] ?? null; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/BufferStream.php: -------------------------------------------------------------------------------- 1 | hwm = $hwm; 35 | } 36 | 37 | public function __toString(): string 38 | { 39 | return $this->getContents(); 40 | } 41 | 42 | public function getContents(): string 43 | { 44 | $buffer = $this->buffer; 45 | $this->buffer = ''; 46 | 47 | return $buffer; 48 | } 49 | 50 | public function close(): void 51 | { 52 | $this->buffer = ''; 53 | } 54 | 55 | public function detach() 56 | { 57 | $this->close(); 58 | 59 | return null; 60 | } 61 | 62 | public function getSize(): ?int 63 | { 64 | return strlen($this->buffer); 65 | } 66 | 67 | public function isReadable(): bool 68 | { 69 | return true; 70 | } 71 | 72 | public function isWritable(): bool 73 | { 74 | return true; 75 | } 76 | 77 | public function isSeekable(): bool 78 | { 79 | return false; 80 | } 81 | 82 | public function rewind(): void 83 | { 84 | $this->seek(0); 85 | } 86 | 87 | public function seek($offset, $whence = SEEK_SET): void 88 | { 89 | throw new \RuntimeException('Cannot seek a BufferStream'); 90 | } 91 | 92 | public function eof(): bool 93 | { 94 | return strlen($this->buffer) === 0; 95 | } 96 | 97 | public function tell(): int 98 | { 99 | throw new \RuntimeException('Cannot determine the position of a BufferStream'); 100 | } 101 | 102 | /** 103 | * Reads data from the buffer. 104 | */ 105 | public function read($length): string 106 | { 107 | $currentLength = strlen($this->buffer); 108 | 109 | if ($length >= $currentLength) { 110 | // No need to slice the buffer because we don't have enough data. 111 | $result = $this->buffer; 112 | $this->buffer = ''; 113 | } else { 114 | // Slice up the result to provide a subset of the buffer. 115 | $result = substr($this->buffer, 0, $length); 116 | $this->buffer = substr($this->buffer, $length); 117 | } 118 | 119 | return $result; 120 | } 121 | 122 | /** 123 | * Writes data to the buffer. 124 | */ 125 | public function write($string): int 126 | { 127 | $this->buffer .= $string; 128 | 129 | if (strlen($this->buffer) >= $this->hwm) { 130 | return 0; 131 | } 132 | 133 | return strlen($string); 134 | } 135 | 136 | /** 137 | * @return mixed 138 | */ 139 | public function getMetadata($key = null) 140 | { 141 | if ($key === 'hwm') { 142 | return $this->hwm; 143 | } 144 | 145 | return $key ? null : []; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/StreamDecoratorTrait.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 22 | } 23 | 24 | /** 25 | * Magic method used to create a new stream if streams are not added in 26 | * the constructor of a decorator (e.g., LazyOpenStream). 27 | * 28 | * @return StreamInterface 29 | */ 30 | public function __get(string $name) 31 | { 32 | if ($name === 'stream') { 33 | $this->stream = $this->createStream(); 34 | 35 | return $this->stream; 36 | } 37 | 38 | throw new \UnexpectedValueException("$name not found on class"); 39 | } 40 | 41 | public function __toString(): string 42 | { 43 | try { 44 | if ($this->isSeekable()) { 45 | $this->seek(0); 46 | } 47 | 48 | return $this->getContents(); 49 | } catch (\Throwable $e) { 50 | if (\PHP_VERSION_ID >= 70400) { 51 | throw $e; 52 | } 53 | trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); 54 | 55 | return ''; 56 | } 57 | } 58 | 59 | public function getContents(): string 60 | { 61 | return Utils::copyToString($this); 62 | } 63 | 64 | /** 65 | * Allow decorators to implement custom methods 66 | * 67 | * @return mixed 68 | */ 69 | public function __call(string $method, array $args) 70 | { 71 | /** @var callable $callable */ 72 | $callable = [$this->stream, $method]; 73 | $result = call_user_func_array($callable, $args); 74 | 75 | // Always return the wrapped object if the result is a return $this 76 | return $result === $this->stream ? $this : $result; 77 | } 78 | 79 | public function close(): void 80 | { 81 | $this->stream->close(); 82 | } 83 | 84 | /** 85 | * @return mixed 86 | */ 87 | public function getMetadata($key = null) 88 | { 89 | return $this->stream->getMetadata($key); 90 | } 91 | 92 | public function detach() 93 | { 94 | return $this->stream->detach(); 95 | } 96 | 97 | public function getSize(): ?int 98 | { 99 | return $this->stream->getSize(); 100 | } 101 | 102 | public function eof(): bool 103 | { 104 | return $this->stream->eof(); 105 | } 106 | 107 | public function tell(): int 108 | { 109 | return $this->stream->tell(); 110 | } 111 | 112 | public function isReadable(): bool 113 | { 114 | return $this->stream->isReadable(); 115 | } 116 | 117 | public function isWritable(): bool 118 | { 119 | return $this->stream->isWritable(); 120 | } 121 | 122 | public function isSeekable(): bool 123 | { 124 | return $this->stream->isSeekable(); 125 | } 126 | 127 | public function rewind(): void 128 | { 129 | $this->seek(0); 130 | } 131 | 132 | public function seek($offset, $whence = SEEK_SET): void 133 | { 134 | $this->stream->seek($offset, $whence); 135 | } 136 | 137 | public function read($length): string 138 | { 139 | return $this->stream->read($length); 140 | } 141 | 142 | public function write($string): int 143 | { 144 | return $this->stream->write($string); 145 | } 146 | 147 | /** 148 | * Implement in subclasses to dynamically create streams when requested. 149 | * 150 | * @throws \BadMethodCallException 151 | */ 152 | protected function createStream(): StreamInterface 153 | { 154 | throw new \BadMethodCallException('Not implemented'); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/Query.php: -------------------------------------------------------------------------------- 1 | '1', 'foo[b]' => '2'])`. 16 | * 17 | * @param string $str Query string to parse 18 | * @param int|bool $urlEncoding How the query string is encoded 19 | */ 20 | public static function parse(string $str, $urlEncoding = true): array 21 | { 22 | $result = []; 23 | 24 | if ($str === '') { 25 | return $result; 26 | } 27 | 28 | if ($urlEncoding === true) { 29 | $decoder = function ($value) { 30 | return rawurldecode(str_replace('+', ' ', (string) $value)); 31 | }; 32 | } elseif ($urlEncoding === PHP_QUERY_RFC3986) { 33 | $decoder = 'rawurldecode'; 34 | } elseif ($urlEncoding === PHP_QUERY_RFC1738) { 35 | $decoder = 'urldecode'; 36 | } else { 37 | $decoder = function ($str) { 38 | return $str; 39 | }; 40 | } 41 | 42 | foreach (explode('&', $str) as $kvp) { 43 | $parts = explode('=', $kvp, 2); 44 | $key = $decoder($parts[0]); 45 | $value = isset($parts[1]) ? $decoder($parts[1]) : null; 46 | if (!array_key_exists($key, $result)) { 47 | $result[$key] = $value; 48 | } else { 49 | if (!is_array($result[$key])) { 50 | $result[$key] = [$result[$key]]; 51 | } 52 | $result[$key][] = $value; 53 | } 54 | } 55 | 56 | return $result; 57 | } 58 | 59 | /** 60 | * Build a query string from an array of key value pairs. 61 | * 62 | * This function can use the return value of `parse()` to build a query 63 | * string. This function does not modify the provided keys when an array is 64 | * encountered (like `http_build_query()` would). 65 | * 66 | * @param array $params Query string parameters. 67 | * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 68 | * to encode using RFC3986, or PHP_QUERY_RFC1738 69 | * to encode using RFC1738. 70 | */ 71 | public static function build(array $params, $encoding = PHP_QUERY_RFC3986): string 72 | { 73 | if (!$params) { 74 | return ''; 75 | } 76 | 77 | if ($encoding === false) { 78 | $encoder = function (string $str): string { 79 | return $str; 80 | }; 81 | } elseif ($encoding === PHP_QUERY_RFC3986) { 82 | $encoder = 'rawurlencode'; 83 | } elseif ($encoding === PHP_QUERY_RFC1738) { 84 | $encoder = 'urlencode'; 85 | } else { 86 | throw new \InvalidArgumentException('Invalid type'); 87 | } 88 | 89 | $qs = ''; 90 | foreach ($params as $k => $v) { 91 | $k = $encoder((string) $k); 92 | if (!is_array($v)) { 93 | $qs .= $k; 94 | $v = is_bool($v) ? (int) $v : $v; 95 | if ($v !== null) { 96 | $qs .= '='.$encoder((string) $v); 97 | } 98 | $qs .= '&'; 99 | } else { 100 | foreach ($v as $vv) { 101 | $qs .= $k; 102 | $vv = is_bool($vv) ? (int) $vv : $vv; 103 | if ($vv !== null) { 104 | $qs .= '='.$encoder((string) $vv); 105 | } 106 | $qs .= '&'; 107 | } 108 | } 109 | } 110 | 111 | return $qs ? (string) substr($qs, 0, -1) : ''; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/RetryMiddleware.php: -------------------------------------------------------------------------------- 1 | decider = $decider; 46 | $this->nextHandler = $nextHandler; 47 | $this->delay = $delay ?: __CLASS__.'::exponentialDelay'; 48 | } 49 | 50 | /** 51 | * Default exponential backoff delay function. 52 | * 53 | * @return int milliseconds. 54 | */ 55 | public static function exponentialDelay(int $retries): int 56 | { 57 | return (int) 2 ** ($retries - 1) * 1000; 58 | } 59 | 60 | public function __invoke(RequestInterface $request, array $options): PromiseInterface 61 | { 62 | if (!isset($options['retries'])) { 63 | $options['retries'] = 0; 64 | } 65 | 66 | $fn = $this->nextHandler; 67 | 68 | return $fn($request, $options) 69 | ->then( 70 | $this->onFulfilled($request, $options), 71 | $this->onRejected($request, $options) 72 | ); 73 | } 74 | 75 | /** 76 | * Execute fulfilled closure 77 | */ 78 | private function onFulfilled(RequestInterface $request, array $options): callable 79 | { 80 | return function ($value) use ($request, $options) { 81 | if (!($this->decider)( 82 | $options['retries'], 83 | $request, 84 | $value, 85 | null 86 | )) { 87 | return $value; 88 | } 89 | 90 | return $this->doRetry($request, $options, $value); 91 | }; 92 | } 93 | 94 | /** 95 | * Execute rejected closure 96 | */ 97 | private function onRejected(RequestInterface $req, array $options): callable 98 | { 99 | return function ($reason) use ($req, $options) { 100 | if (!($this->decider)( 101 | $options['retries'], 102 | $req, 103 | null, 104 | $reason 105 | )) { 106 | return P\Create::rejectionFor($reason); 107 | } 108 | 109 | return $this->doRetry($req, $options); 110 | }; 111 | } 112 | 113 | private function doRetry( RequestInterface $request, array $options, ?ResponseInterface $response = null ): PromiseInterface 114 | { 115 | $options['delay'] = ($this->delay)(++$options['retries'], $response, $request); 116 | 117 | return $this($request, $options); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/Header.php: -------------------------------------------------------------------------------- 1 | ]+>|[^=]+/', $kvp, $matches)) { 27 | $m = $matches[0]; 28 | if (isset($m[1])) { 29 | $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); 30 | } else { 31 | $part[] = trim($m[0], $trimmed); 32 | } 33 | } 34 | } 35 | if ($part) { 36 | $params[] = $part; 37 | } 38 | } 39 | } 40 | 41 | return $params; 42 | } 43 | 44 | /** 45 | * Converts an array of header values that may contain comma separated 46 | * headers into an array of headers with no comma separated values. 47 | * 48 | * @param string|array $header Header to normalize. 49 | * 50 | * @deprecated Use self::splitList() instead. 51 | */ 52 | public static function normalize($header): array 53 | { 54 | $result = []; 55 | foreach ((array) $header as $value) { 56 | foreach (self::splitList($value) as $parsed) { 57 | $result[] = $parsed; 58 | } 59 | } 60 | 61 | return $result; 62 | } 63 | 64 | /** 65 | * Splits a HTTP header defined to contain a comma-separated list into 66 | * each individual value. Empty values will be removed. 67 | * 68 | * Example headers include 'accept', 'cache-control' and 'if-none-match'. 69 | * 70 | * This method must not be used to parse headers that are not defined as 71 | * a list, such as 'user-agent' or 'set-cookie'. 72 | * 73 | * @param string|string[] $values Header value as returned by MessageInterface::getHeader() 74 | * 75 | * @return string[] 76 | */ 77 | public static function splitList($values): array 78 | { 79 | if (!\is_array($values)) { 80 | $values = [$values]; 81 | } 82 | 83 | $result = []; 84 | foreach ($values as $value) { 85 | if (!\is_string($value)) { 86 | throw new \TypeError('$header must either be a string or an array containing strings.'); 87 | } 88 | 89 | $v = ''; 90 | $isQuoted = false; 91 | $isEscaped = false; 92 | for ($i = 0, $max = \strlen($value); $i < $max; ++$i) { 93 | if ($isEscaped) { 94 | $v .= $value[$i]; 95 | $isEscaped = false; 96 | 97 | continue; 98 | } 99 | 100 | if (!$isQuoted && $value[$i] === ',') { 101 | $v = \trim($v); 102 | if ($v !== '') { 103 | $result[] = $v; 104 | } 105 | 106 | $v = ''; 107 | continue; 108 | } 109 | 110 | if ($isQuoted && $value[$i] === '\\') { 111 | $isEscaped = true; 112 | $v .= $value[$i]; 113 | 114 | continue; 115 | } 116 | if ($value[$i] === '"') { 117 | $isQuoted = !$isQuoted; 118 | $v .= $value[$i]; 119 | 120 | continue; 121 | } 122 | 123 | $v .= $value[$i]; 124 | } 125 | 126 | $v = \trim($v); 127 | if ($v !== '') { 128 | $result[] = $v; 129 | } 130 | } 131 | 132 | return $result; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/Request.php: -------------------------------------------------------------------------------- 1 | $headers Request headers 32 | * @param string|resource|StreamInterface|null $body Request body 33 | * @param string $version Protocol version 34 | */ 35 | public function __construct( 36 | string $method, 37 | $uri, 38 | array $headers = [], 39 | $body = null, 40 | string $version = '1.1' 41 | ) { 42 | $this->assertMethod($method); 43 | if (!($uri instanceof UriInterface)) { 44 | $uri = new Uri($uri); 45 | } 46 | 47 | $this->method = strtoupper($method); 48 | $this->uri = $uri; 49 | $this->setHeaders($headers); 50 | $this->protocol = $version; 51 | 52 | if (!isset($this->headerNames['host'])) { 53 | $this->updateHostFromUri(); 54 | } 55 | 56 | if ($body !== '' && $body !== null) { 57 | $this->stream = Utils::streamFor($body); 58 | } 59 | } 60 | 61 | public function getRequestTarget(): string 62 | { 63 | if ($this->requestTarget !== null) { 64 | return $this->requestTarget; 65 | } 66 | 67 | $target = $this->uri->getPath(); 68 | if ($target === '') { 69 | $target = '/'; 70 | } 71 | if ($this->uri->getQuery() != '') { 72 | $target .= '?'.$this->uri->getQuery(); 73 | } 74 | 75 | return $target; 76 | } 77 | 78 | public function withRequestTarget($requestTarget): RequestInterface 79 | { 80 | if (preg_match('#\s#', $requestTarget)) { 81 | throw new InvalidArgumentException( 82 | 'Invalid request target provided; cannot contain whitespace' 83 | ); 84 | } 85 | 86 | $new = clone $this; 87 | $new->requestTarget = $requestTarget; 88 | 89 | return $new; 90 | } 91 | 92 | public function getMethod(): string 93 | { 94 | return $this->method; 95 | } 96 | 97 | public function withMethod($method): RequestInterface 98 | { 99 | $this->assertMethod($method); 100 | $new = clone $this; 101 | $new->method = strtoupper($method); 102 | 103 | return $new; 104 | } 105 | 106 | public function getUri(): UriInterface 107 | { 108 | return $this->uri; 109 | } 110 | 111 | public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface 112 | { 113 | if ($uri === $this->uri) { 114 | return $this; 115 | } 116 | 117 | $new = clone $this; 118 | $new->uri = $uri; 119 | 120 | if (!$preserveHost || !isset($this->headerNames['host'])) { 121 | $new->updateHostFromUri(); 122 | } 123 | 124 | return $new; 125 | } 126 | 127 | private function updateHostFromUri(): void 128 | { 129 | $host = $this->uri->getHost(); 130 | 131 | if ($host == '') { 132 | return; 133 | } 134 | 135 | if (($port = $this->uri->getPort()) !== null) { 136 | $host .= ':'.$port; 137 | } 138 | 139 | if (isset($this->headerNames['host'])) { 140 | $header = $this->headerNames['host']; 141 | } else { 142 | $header = 'Host'; 143 | $this->headerNames['host'] = 'Host'; 144 | } 145 | // Ensure Host is the first header. 146 | // See: http://tools.ietf.org/html/rfc7230#section-5.4 147 | $this->headers = [$header => [$host]] + $this->headers; 148 | } 149 | 150 | /** 151 | * @param mixed $method 152 | */ 153 | private function assertMethod($method): void 154 | { 155 | if (!is_string($method) || $method === '') { 156 | throw new InvalidArgumentException('Method must be a non-empty string.'); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Filters.php: -------------------------------------------------------------------------------- 1 | post_author; 91 | 92 | if ( $author ) { 93 | $author_name = get_the_author_meta( 'display_name', $author ); 94 | 95 | $params .= " event-author='$author_name'"; 96 | } 97 | 98 | // Add support for post category and tags along with custom taxonomies. 99 | $taxonomies = get_object_taxonomies( $post->post_type ); 100 | 101 | // Loop through existing taxonomies. 102 | foreach ( $taxonomies as $taxonomy ) { 103 | $terms = get_the_terms( $post->ID, $taxonomy ); 104 | 105 | // Skip the iteration, if `$terms` is not array. 106 | if ( ! is_array( $terms ) ) { 107 | continue; // @codeCoverageIgnore; 108 | } 109 | 110 | // Loop through the terms. 111 | foreach ( $terms as $term ) { 112 | if ( $term instanceof WP_Term ) { 113 | $params .= " event-{$taxonomy}=\"{$term->name}\""; 114 | } 115 | } 116 | } 117 | 118 | return $params; 119 | } 120 | 121 | /** 122 | * Adds custom parameter User Logged In if Custom Properties is enabled. 123 | * 124 | * @since v2.4.0 125 | * 126 | * @param $params 127 | * 128 | * @return mixed|string 129 | */ 130 | public function maybe_track_logged_in_users( $params ) { 131 | $settings = Helpers::get_settings(); 132 | 133 | if ( ! is_array( $settings[ 'enhanced_measurements' ] ) || ! in_array( 'pageview-props', $settings[ 'enhanced_measurements' ] ) ) { 134 | return $params; // @codeCoverageIgnore 135 | } 136 | 137 | $logged_in = _x( 'no', __( 'Value when user is not logged in.', 'plausible-analytics' ), 'plausible-analytics' ); 138 | 139 | if ( is_user_logged_in() ) { 140 | $user = wp_get_current_user(); 141 | $roles = (array) $user->roles; 142 | 143 | if ( ! empty( $roles ) ) { 144 | $logged_in = $roles[ 0 ]; 145 | } 146 | } 147 | 148 | $params .= " event-user_logged_in='$logged_in'"; 149 | 150 | return $params; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/StreamWrapper.php: -------------------------------------------------------------------------------- 1 | isReadable()) { 39 | $mode = $stream->isWritable() ? 'r+' : 'r'; 40 | } elseif ($stream->isWritable()) { 41 | $mode = 'w'; 42 | } else { 43 | throw new \InvalidArgumentException('The stream must be readable, ' 44 | .'writable, or both.'); 45 | } 46 | 47 | return fopen('guzzle://stream', $mode, false, self::createStreamContext($stream)); 48 | } 49 | 50 | /** 51 | * Creates a stream context that can be used to open a stream as a php stream resource. 52 | * 53 | * @return resource 54 | */ 55 | public static function createStreamContext(StreamInterface $stream) 56 | { 57 | return stream_context_create([ 58 | 'guzzle' => ['stream' => $stream], 59 | ]); 60 | } 61 | 62 | /** 63 | * Registers the stream wrapper if needed 64 | */ 65 | public static function register(): void 66 | { 67 | if (!in_array('guzzle', stream_get_wrappers())) { 68 | stream_wrapper_register('guzzle', __CLASS__); 69 | } 70 | } 71 | 72 | public function stream_open( string $path, string $mode, int $options, ?string &$opened_path = null ): bool 73 | { 74 | $options = stream_context_get_options($this->context); 75 | 76 | if (!isset($options['guzzle']['stream'])) { 77 | return false; 78 | } 79 | 80 | $this->mode = $mode; 81 | $this->stream = $options['guzzle']['stream']; 82 | 83 | return true; 84 | } 85 | 86 | public function stream_read(int $count): string 87 | { 88 | return $this->stream->read($count); 89 | } 90 | 91 | public function stream_write(string $data): int 92 | { 93 | return $this->stream->write($data); 94 | } 95 | 96 | public function stream_tell(): int 97 | { 98 | return $this->stream->tell(); 99 | } 100 | 101 | public function stream_eof(): bool 102 | { 103 | return $this->stream->eof(); 104 | } 105 | 106 | public function stream_seek(int $offset, int $whence): bool 107 | { 108 | $this->stream->seek($offset, $whence); 109 | 110 | return true; 111 | } 112 | 113 | /** 114 | * @return resource|false 115 | */ 116 | public function stream_cast(int $cast_as) 117 | { 118 | $stream = clone $this->stream; 119 | $resource = $stream->detach(); 120 | 121 | return $resource ?? false; 122 | } 123 | 124 | /** 125 | * @return array 126 | */ 127 | public function stream_stat(): array 128 | { 129 | static $modeMap = [ 130 | 'r' => 33060, 131 | 'rb' => 33060, 132 | 'r+' => 33206, 133 | 'w' => 33188, 134 | 'wb' => 33188, 135 | ]; 136 | 137 | return [ 138 | 'dev' => 0, 139 | 'ino' => 0, 140 | 'mode' => $modeMap[$this->mode], 141 | 'nlink' => 0, 142 | 'uid' => 0, 143 | 'gid' => 0, 144 | 'rdev' => 0, 145 | 'size' => $this->stream->getSize() ?: 0, 146 | 'atime' => 0, 147 | 'mtime' => 0, 148 | 'ctime' => 0, 149 | 'blksize' => 0, 150 | 'blocks' => 0, 151 | ]; 152 | } 153 | 154 | /** 155 | * @return array 156 | */ 157 | public function url_stat(string $path, int $flags): array 158 | { 159 | return [ 160 | 'dev' => 0, 161 | 'ino' => 0, 162 | 'mode' => 0, 163 | 'nlink' => 0, 164 | 'uid' => 0, 165 | 'gid' => 0, 166 | 'rdev' => 0, 167 | 'size' => 0, 168 | 'atime' => 0, 169 | 'mtime' => 0, 170 | 'ctime' => 0, 171 | 'blksize' => 0, 172 | 'blocks' => 0, 173 | ]; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Promise/Coroutine.php: -------------------------------------------------------------------------------- 1 | then(function ($v) { echo $v; }); 39 | * 40 | * @param callable $generatorFn Generator function to wrap into a promise. 41 | * 42 | * @return Promise 43 | * 44 | * @see https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration 45 | */ 46 | final class Coroutine implements PromiseInterface 47 | { 48 | /** 49 | * @var PromiseInterface|null 50 | */ 51 | private $currentPromise; 52 | 53 | /** 54 | * @var Generator 55 | */ 56 | private $generator; 57 | 58 | /** 59 | * @var Promise 60 | */ 61 | private $result; 62 | 63 | public function __construct(callable $generatorFn) 64 | { 65 | $this->generator = $generatorFn(); 66 | $this->result = new Promise(function (): void { 67 | while (isset($this->currentPromise)) { 68 | $this->currentPromise->wait(); 69 | } 70 | }); 71 | try { 72 | $this->nextCoroutine($this->generator->current()); 73 | } catch (Throwable $throwable) { 74 | $this->result->reject($throwable); 75 | } 76 | } 77 | 78 | /** 79 | * Create a new coroutine. 80 | */ 81 | public static function of(callable $generatorFn): self 82 | { 83 | return new self($generatorFn); 84 | } 85 | 86 | public function then( 87 | ?callable $onFulfilled = null, 88 | ?callable $onRejected = null 89 | ): PromiseInterface { 90 | return $this->result->then($onFulfilled, $onRejected); 91 | } 92 | 93 | public function otherwise(callable $onRejected): PromiseInterface 94 | { 95 | return $this->result->otherwise($onRejected); 96 | } 97 | 98 | public function wait(bool $unwrap = true) 99 | { 100 | return $this->result->wait($unwrap); 101 | } 102 | 103 | public function getState(): string 104 | { 105 | return $this->result->getState(); 106 | } 107 | 108 | public function resolve($value): void 109 | { 110 | $this->result->resolve($value); 111 | } 112 | 113 | public function reject($reason): void 114 | { 115 | $this->result->reject($reason); 116 | } 117 | 118 | public function cancel(): void 119 | { 120 | $this->currentPromise->cancel(); 121 | $this->result->cancel(); 122 | } 123 | 124 | private function nextCoroutine($yielded): void 125 | { 126 | $this->currentPromise = Create::promiseFor($yielded) 127 | ->then([$this, '_handleSuccess'], [$this, '_handleFailure']); 128 | } 129 | 130 | /** 131 | * @internal 132 | */ 133 | public function _handleSuccess($value): void 134 | { 135 | unset($this->currentPromise); 136 | try { 137 | $next = $this->generator->send($value); 138 | if ($this->generator->valid()) { 139 | $this->nextCoroutine($next); 140 | } else { 141 | $this->result->resolve($value); 142 | } 143 | } catch (Throwable $throwable) { 144 | $this->result->reject($throwable); 145 | } 146 | } 147 | 148 | /** 149 | * @internal 150 | */ 151 | public function _handleFailure($reason): void 152 | { 153 | unset($this->currentPromise); 154 | try { 155 | $nextYield = $this->generator->throw(Create::exceptionFor($reason)); 156 | // The throw was caught, so keep iterating on the coroutine 157 | $this->nextCoroutine($nextYield); 158 | } catch (Throwable $throwable) { 159 | $this->result->reject($throwable); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/LimitStream.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 38 | $this->setLimit($limit); 39 | $this->setOffset($offset); 40 | } 41 | 42 | public function eof(): bool 43 | { 44 | // Always return true if the underlying stream is EOF 45 | if ($this->stream->eof()) { 46 | return true; 47 | } 48 | 49 | // No limit and the underlying stream is not at EOF 50 | if ($this->limit === -1) { 51 | return false; 52 | } 53 | 54 | return $this->stream->tell() >= $this->offset + $this->limit; 55 | } 56 | 57 | /** 58 | * Returns the size of the limited subset of data 59 | */ 60 | public function getSize(): ?int 61 | { 62 | if (null === ($length = $this->stream->getSize())) { 63 | return null; 64 | } elseif ($this->limit === -1) { 65 | return $length - $this->offset; 66 | } else { 67 | return min($this->limit, $length - $this->offset); 68 | } 69 | } 70 | 71 | /** 72 | * Allow for a bounded seek on the read limited stream 73 | */ 74 | public function seek($offset, $whence = SEEK_SET): void 75 | { 76 | if ($whence !== SEEK_SET || $offset < 0) { 77 | throw new \RuntimeException(sprintf( 78 | 'Cannot seek to offset %s with whence %s', 79 | $offset, 80 | $whence 81 | )); 82 | } 83 | 84 | $offset += $this->offset; 85 | 86 | if ($this->limit !== -1) { 87 | if ($offset > $this->offset + $this->limit) { 88 | $offset = $this->offset + $this->limit; 89 | } 90 | } 91 | 92 | $this->stream->seek($offset); 93 | } 94 | 95 | /** 96 | * Give a relative tell() 97 | */ 98 | public function tell(): int 99 | { 100 | return $this->stream->tell() - $this->offset; 101 | } 102 | 103 | /** 104 | * Set the offset to start limiting from 105 | * 106 | * @param int $offset Offset to seek to and begin byte limiting from 107 | * 108 | * @throws \RuntimeException if the stream cannot be seeked. 109 | */ 110 | public function setOffset(int $offset): void 111 | { 112 | $current = $this->stream->tell(); 113 | 114 | if ($current !== $offset) { 115 | // If the stream cannot seek to the offset position, then read to it 116 | if ($this->stream->isSeekable()) { 117 | $this->stream->seek($offset); 118 | } elseif ($current > $offset) { 119 | throw new \RuntimeException("Could not seek to stream offset $offset"); 120 | } else { 121 | $this->stream->read($offset - $current); 122 | } 123 | } 124 | 125 | $this->offset = $offset; 126 | } 127 | 128 | /** 129 | * Set the limit of bytes that the decorator allows to be read from the 130 | * stream. 131 | * 132 | * @param int $limit Number of bytes to allow to be read from the stream. 133 | * Use -1 for no limit. 134 | */ 135 | public function setLimit(int $limit): void 136 | { 137 | $this->limit = $limit; 138 | } 139 | 140 | public function read($length): string 141 | { 142 | if ($this->limit === -1) { 143 | return $this->stream->read($length); 144 | } 145 | 146 | // Check if the current position is less than the total allowed 147 | // bytes + original offset 148 | $remaining = ($this->offset + $this->limit) - $this->stream->tell(); 149 | if ($remaining > 0) { 150 | // Only return the amount of requested data, ensuring that the byte 151 | // limit is not exceeded 152 | return $this->stream->read(min($remaining, $length)); 153 | } 154 | 155 | return ''; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Admin/Settings/Hooks.php: -------------------------------------------------------------------------------- 1 | init_hooks(); 26 | } 27 | } 28 | 29 | /** 30 | * Init action hooks. 31 | * 32 | * @return void 33 | */ 34 | private function init_hooks() { 35 | add_filter( 'plausible_analytics_toggle_option_success_message', [ $this, 'maybe_modify_success_message' ], 10, 3 ); 36 | add_action( 'plausible_analytics_settings_api_token_missing', [ $this, 'missing_api_token_warning' ] ); 37 | add_action( 'plausible_analytics_settings_enable_analytics_dashboard_notice', [ $this, 'enable_analytics_dashboard_notice' ] ); 38 | add_action( 'plausible_analytics_settings_option_disabled_by_missing_api_token', [ $this, 'option_disabled_by_missing_api_token' ] ); 39 | add_action( 'plausible_analytics_settings_option_disabled_by_proxy', [ $this, 'option_disabled_by_proxy' ] ); 40 | add_action( 'plausible_analytics_settings_option_not_available_in_ce', [ $this, 'option_na_in_ce' ] ); 41 | add_action( 'plausible_analytics_settings_proxy_warning', [ $this, 'proxy_warning' ] ); 42 | } 43 | 44 | /** 45 | * Modifies "Enable proxy enabled" to "Proxy enabled", etc. 46 | * 47 | * @param $message 48 | * @param $option_name 49 | * @param $status 50 | * 51 | * @return string 52 | */ 53 | public function maybe_modify_success_message( $message, $option_name, $status ) { 54 | if ( $option_name !== 'proxy_enabled' ) { 55 | return $message; 56 | } 57 | 58 | if ( ! $status ) { 59 | return __( 'Proxy disabled.', 'plausible-analytics' ); 60 | } 61 | 62 | return __( 'Proxy enabled.', 'plausible-analytics' ); 63 | } 64 | 65 | /** 66 | * Renders the warning for the Enable Proxy option. 67 | * 68 | * @since 1.3.0 69 | * @output HTML 70 | */ 71 | public function proxy_warning() { 72 | if ( ! empty( Helpers::get_settings()[ 'self_hosted_domain' ] ) ) { 73 | $this->option_na_in_ce(); 74 | } else { 75 | echo sprintf( 76 | wp_kses( 77 | __( 78 | 'After enabling this option, please check your Plausible dashboard to make sure stats are being recorded. Are stats not being recorded? Do reach out to us. We\'re here to help!', 79 | 'plausible-analytics' 80 | ), 81 | 'post' 82 | ), 83 | 'https://plausible.io/contact' 84 | ); 85 | } 86 | } 87 | 88 | /** 89 | * Show notice when Plugin Token notice is disabled. 90 | * 91 | * @output HTML 92 | */ 93 | public function option_na_in_ce() { 94 | echo wp_kses( 95 | __( 96 | 'This feature is not available in Plausible Community Edition.', 97 | 'plausible-analytics' 98 | ), 99 | 'post' 100 | ); 101 | } 102 | 103 | /** 104 | * Renders the analytics dashboard link if the option is enabled. 105 | * 106 | * @since 2.0.0 107 | * @output HTML 108 | */ 109 | public function enable_analytics_dashboard_notice() { 110 | if ( ! empty( Helpers::get_settings()[ 'enable_analytics_dashboard' ] ) ) { 111 | echo sprintf( 112 | wp_kses( 113 | __( 114 | 'Your analytics dashboard is available here.', 115 | 'plausible-analytics' 116 | ), 117 | 'post' 118 | ), 119 | admin_url( 'index.php?page=plausible_analytics_statistics' ) 120 | ); 121 | } 122 | } 123 | 124 | /** 125 | * Renders the Self-hosted warning if the Proxy is enabled. 126 | * 127 | * @since 1.3.3 128 | * @output HTML 129 | */ 130 | public function option_disabled_by_proxy() { 131 | if ( Helpers::proxy_enabled() ) { 132 | echo wp_kses( 133 | __( 134 | 'This option is disabled, because the Proxy setting is enabled under Settings.', 135 | 'plausible-analytics' 136 | ), 137 | 'post' 138 | ); 139 | } 140 | } 141 | 142 | /** 143 | * Display missing Plugin Token warning. 144 | * 145 | * @output HTML 146 | */ 147 | public function missing_api_token_warning() { 148 | echo sprintf( 149 | wp_kses( 150 | __( 151 | 'Please create a Plugin Token and insert it into the Plugin Token field above.', 152 | 'plausible-analytics' 153 | ), 154 | 'post' 155 | ) 156 | ); 157 | } 158 | 159 | /** 160 | * Display option disabled by missing Plugin Token warning. 161 | * 162 | * @output HTML 163 | */ 164 | public function option_disabled_by_missing_api_token() { 165 | echo wp_kses( 166 | __( 167 | 'Please create a Plugin Token and insert it into the Plugin Token field above to enable this option.', 168 | 'plausible-analytics' 169 | ), 170 | 'post' 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/Psr/Http/Message/UploadedFileInterface.php: -------------------------------------------------------------------------------- 1 | remoteStream = $stream; 39 | $this->stream = $target ?: new Stream(Utils::tryFopen('php://temp', 'r+')); 40 | } 41 | 42 | public function getSize(): ?int 43 | { 44 | $remoteSize = $this->remoteStream->getSize(); 45 | 46 | if (null === $remoteSize) { 47 | return null; 48 | } 49 | 50 | return max($this->stream->getSize(), $remoteSize); 51 | } 52 | 53 | public function rewind(): void 54 | { 55 | $this->seek(0); 56 | } 57 | 58 | public function seek($offset, $whence = SEEK_SET): void 59 | { 60 | if ($whence === SEEK_SET) { 61 | $byte = $offset; 62 | } elseif ($whence === SEEK_CUR) { 63 | $byte = $offset + $this->tell(); 64 | } elseif ($whence === SEEK_END) { 65 | $size = $this->remoteStream->getSize(); 66 | if ($size === null) { 67 | $size = $this->cacheEntireStream(); 68 | } 69 | $byte = $size + $offset; 70 | } else { 71 | throw new \InvalidArgumentException('Invalid whence'); 72 | } 73 | 74 | $diff = $byte - $this->stream->getSize(); 75 | 76 | if ($diff > 0) { 77 | // Read the remoteStream until we have read in at least the amount 78 | // of bytes requested, or we reach the end of the file. 79 | while ($diff > 0 && !$this->remoteStream->eof()) { 80 | $this->read($diff); 81 | $diff = $byte - $this->stream->getSize(); 82 | } 83 | } else { 84 | // We can just do a normal seek since we've already seen this byte. 85 | $this->stream->seek($byte); 86 | } 87 | } 88 | 89 | public function read($length): string 90 | { 91 | // Perform a regular read on any previously read data from the buffer 92 | $data = $this->stream->read($length); 93 | $remaining = $length - strlen($data); 94 | 95 | // More data was requested so read from the remote stream 96 | if ($remaining) { 97 | // If data was written to the buffer in a position that would have 98 | // been filled from the remote stream, then we must skip bytes on 99 | // the remote stream to emulate overwriting bytes from that 100 | // position. This mimics the behavior of other PHP stream wrappers. 101 | $remoteData = $this->remoteStream->read( 102 | $remaining + $this->skipReadBytes 103 | ); 104 | 105 | if ($this->skipReadBytes) { 106 | $len = strlen($remoteData); 107 | $remoteData = substr($remoteData, $this->skipReadBytes); 108 | $this->skipReadBytes = max(0, $this->skipReadBytes - $len); 109 | } 110 | 111 | $data .= $remoteData; 112 | $this->stream->write($remoteData); 113 | } 114 | 115 | return $data; 116 | } 117 | 118 | public function write($string): int 119 | { 120 | // When appending to the end of the currently read stream, you'll want 121 | // to skip bytes from being read from the remote stream to emulate 122 | // other stream wrappers. Basically replacing bytes of data of a fixed 123 | // length. 124 | $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); 125 | if ($overflow > 0) { 126 | $this->skipReadBytes += $overflow; 127 | } 128 | 129 | return $this->stream->write($string); 130 | } 131 | 132 | public function eof(): bool 133 | { 134 | return $this->stream->eof() && $this->remoteStream->eof(); 135 | } 136 | 137 | /** 138 | * Close both the remote stream and buffer stream 139 | */ 140 | public function close(): void 141 | { 142 | $this->remoteStream->close(); 143 | $this->stream->close(); 144 | } 145 | 146 | private function cacheEntireStream(): int 147 | { 148 | $target = new FnStream(['write' => 'strlen']); 149 | Utils::copyToStream($this, $target); 150 | 151 | return $this->tell(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/FnStream.php: -------------------------------------------------------------------------------- 1 | */ 25 | private $methods; 26 | 27 | /** 28 | * @param array $methods Hash of method name to a callable. 29 | */ 30 | public function __construct(array $methods) 31 | { 32 | $this->methods = $methods; 33 | 34 | // Create the functions on the class 35 | foreach ($methods as $name => $fn) { 36 | $this->{'_fn_'.$name} = $fn; 37 | } 38 | } 39 | 40 | /** 41 | * Lazily determine which methods are not implemented. 42 | * 43 | * @throws \BadMethodCallException 44 | */ 45 | public function __get(string $name): void 46 | { 47 | throw new \BadMethodCallException(str_replace('_fn_', '', $name) 48 | .'() is not implemented in the FnStream'); 49 | } 50 | 51 | /** 52 | * The close method is called on the underlying stream only if possible. 53 | */ 54 | public function __destruct() 55 | { 56 | if (isset($this->_fn_close)) { 57 | call_user_func($this->_fn_close); 58 | } 59 | } 60 | 61 | /** 62 | * An unserialize would allow the __destruct to run when the unserialized value goes out of scope. 63 | * 64 | * @throws \LogicException 65 | */ 66 | public function __wakeup(): void 67 | { 68 | throw new \LogicException('FnStream should never be unserialized'); 69 | } 70 | 71 | /** 72 | * Adds custom functionality to an underlying stream by intercepting 73 | * specific method calls. 74 | * 75 | * @param StreamInterface $stream Stream to decorate 76 | * @param array $methods Hash of method name to a closure 77 | * 78 | * @return FnStream 79 | */ 80 | public static function decorate(StreamInterface $stream, array $methods) 81 | { 82 | // If any of the required methods were not provided, then simply 83 | // proxy to the decorated stream. 84 | foreach (array_diff(self::SLOTS, array_keys($methods)) as $diff) { 85 | /** @var callable $callable */ 86 | $callable = [$stream, $diff]; 87 | $methods[$diff] = $callable; 88 | } 89 | 90 | return new self($methods); 91 | } 92 | 93 | public function __toString(): string 94 | { 95 | try { 96 | return call_user_func($this->_fn___toString); 97 | } catch (\Throwable $e) { 98 | if (\PHP_VERSION_ID >= 70400) { 99 | throw $e; 100 | } 101 | trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); 102 | 103 | return ''; 104 | } 105 | } 106 | 107 | public function close(): void 108 | { 109 | call_user_func($this->_fn_close); 110 | } 111 | 112 | public function detach() 113 | { 114 | return call_user_func($this->_fn_detach); 115 | } 116 | 117 | public function getSize(): ?int 118 | { 119 | return call_user_func($this->_fn_getSize); 120 | } 121 | 122 | public function tell(): int 123 | { 124 | return call_user_func($this->_fn_tell); 125 | } 126 | 127 | public function eof(): bool 128 | { 129 | return call_user_func($this->_fn_eof); 130 | } 131 | 132 | public function isSeekable(): bool 133 | { 134 | return call_user_func($this->_fn_isSeekable); 135 | } 136 | 137 | public function rewind(): void 138 | { 139 | call_user_func($this->_fn_rewind); 140 | } 141 | 142 | public function seek($offset, $whence = SEEK_SET): void 143 | { 144 | call_user_func($this->_fn_seek, $offset, $whence); 145 | } 146 | 147 | public function isWritable(): bool 148 | { 149 | return call_user_func($this->_fn_isWritable); 150 | } 151 | 152 | public function write($string): int 153 | { 154 | return call_user_func($this->_fn_write, $string); 155 | } 156 | 157 | public function isReadable(): bool 158 | { 159 | return call_user_func($this->_fn_isReadable); 160 | } 161 | 162 | public function read($length): string 163 | { 164 | return call_user_func($this->_fn_read, $length); 165 | } 166 | 167 | public function getContents(): string 168 | { 169 | return call_user_func($this->_fn_getContents); 170 | } 171 | 172 | /** 173 | * @return mixed 174 | */ 175 | public function getMetadata($key = null) 176 | { 177 | return call_user_func($this->_fn_getMetadata, $key); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Pool.php: -------------------------------------------------------------------------------- 1 | $rfn) { 57 | if ($rfn instanceof RequestInterface) { 58 | yield $key => $client->sendAsync($rfn, $opts); 59 | } elseif (\is_callable($rfn)) { 60 | yield $key => $rfn($opts); 61 | } else { 62 | throw new \InvalidArgumentException('Each value yielded by the iterator must be a Psr7\Http\Message\RequestInterface or a callable that returns a promise that fulfills with a Psr7\Message\Http\ResponseInterface object.'); 63 | } 64 | } 65 | }; 66 | 67 | $this->each = new EachPromise($requests(), $config); 68 | } 69 | 70 | /** 71 | * Get promise 72 | */ 73 | public function promise(): PromiseInterface 74 | { 75 | return $this->each->promise(); 76 | } 77 | 78 | /** 79 | * Sends multiple requests concurrently and returns an array of responses 80 | * and exceptions that uses the same ordering as the provided requests. 81 | * 82 | * IMPORTANT: This method keeps every request and response in memory, and 83 | * as such, is NOT recommended when sending a large number or an 84 | * indeterminate number of requests concurrently. 85 | * 86 | * @param ClientInterface $client Client used to send the requests 87 | * @param array|\Iterator $requests Requests to send concurrently. 88 | * @param array $options Passes through the options available in 89 | * {@see \Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Pool::__construct} 90 | * 91 | * @return array Returns an array containing the response or an exception 92 | * in the same order that the requests were sent. 93 | * 94 | * @throws \InvalidArgumentException if the event format is incorrect. 95 | */ 96 | public static function batch(ClientInterface $client, $requests, array $options = []): array 97 | { 98 | $res = []; 99 | self::cmpCallback($options, 'fulfilled', $res); 100 | self::cmpCallback($options, 'rejected', $res); 101 | $pool = new static($client, $requests, $options); 102 | $pool->promise()->wait(); 103 | \ksort($res); 104 | 105 | return $res; 106 | } 107 | 108 | /** 109 | * Execute callback(s) 110 | */ 111 | private static function cmpCallback(array &$options, string $name, array &$results): void 112 | { 113 | if (!isset($options[$name])) { 114 | $options[$name] = static function ($v, $k) use (&$results) { 115 | $results[$k] = $v; 116 | }; 117 | } else { 118 | $currentFn = $options[$name]; 119 | $options[$name] = static function ($v, $k) use (&$results, $currentFn) { 120 | $currentFn($v, $k); 121 | $results[$k] = $v; 122 | }; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Client/lib/Lib/GuzzleHttp/Psr7/PumpStream.php: -------------------------------------------------------------------------------- 1 | source = $source; 49 | $this->size = $options['size'] ?? null; 50 | $this->metadata = $options['metadata'] ?? []; 51 | $this->buffer = new BufferStream(); 52 | } 53 | 54 | public function __toString(): string 55 | { 56 | try { 57 | return Utils::copyToString($this); 58 | } catch (\Throwable $e) { 59 | if (\PHP_VERSION_ID >= 70400) { 60 | throw $e; 61 | } 62 | trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); 63 | 64 | return ''; 65 | } 66 | } 67 | 68 | public function close(): void 69 | { 70 | $this->detach(); 71 | } 72 | 73 | public function detach() 74 | { 75 | $this->tellPos = 0; 76 | $this->source = null; 77 | 78 | return null; 79 | } 80 | 81 | public function getSize(): ?int 82 | { 83 | return $this->size; 84 | } 85 | 86 | public function tell(): int 87 | { 88 | return $this->tellPos; 89 | } 90 | 91 | public function eof(): bool 92 | { 93 | return $this->source === null; 94 | } 95 | 96 | public function isSeekable(): bool 97 | { 98 | return false; 99 | } 100 | 101 | public function rewind(): void 102 | { 103 | $this->seek(0); 104 | } 105 | 106 | public function seek($offset, $whence = SEEK_SET): void 107 | { 108 | throw new \RuntimeException('Cannot seek a PumpStream'); 109 | } 110 | 111 | public function isWritable(): bool 112 | { 113 | return false; 114 | } 115 | 116 | public function write($string): int 117 | { 118 | throw new \RuntimeException('Cannot write to a PumpStream'); 119 | } 120 | 121 | public function isReadable(): bool 122 | { 123 | return true; 124 | } 125 | 126 | public function read($length): string 127 | { 128 | $data = $this->buffer->read($length); 129 | $readLen = strlen($data); 130 | $this->tellPos += $readLen; 131 | $remaining = $length - $readLen; 132 | 133 | if ($remaining) { 134 | $this->pump($remaining); 135 | $data .= $this->buffer->read($remaining); 136 | $this->tellPos += strlen($data) - $readLen; 137 | } 138 | 139 | return $data; 140 | } 141 | 142 | public function getContents(): string 143 | { 144 | $result = ''; 145 | while (!$this->eof()) { 146 | $result .= $this->read(1000000); 147 | } 148 | 149 | return $result; 150 | } 151 | 152 | /** 153 | * @return mixed 154 | */ 155 | public function getMetadata($key = null) 156 | { 157 | if (!$key) { 158 | return $this->metadata; 159 | } 160 | 161 | return $this->metadata[$key] ?? null; 162 | } 163 | 164 | private function pump(int $length): void 165 | { 166 | if ($this->source) { 167 | do { 168 | $data = call_user_func($this->source, $length); 169 | if ($data === false || $data === null) { 170 | $this->source = null; 171 | 172 | return; 173 | } 174 | $this->buffer->write($data); 175 | $length -= strlen($data); 176 | } while ($length > 0); 177 | } 178 | } 179 | } 180 | --------------------------------------------------------------------------------