├── 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 |
--------------------------------------------------------------------------------