diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..961d530 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + push: + branches: [ master, develop ] + pull_request: + branches: [ "*" ] + +jobs: + test: + name: "PHPUnit: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" + continue-on-error: ${{ matrix.experimental }} + + strategy: + matrix: + include: + - mw: 'REL1_39' + php: 8.1 + experimental: false + - mw: 'REL1_40' + php: 8.1 + experimental: true + - mw: 'master' + php: 8.1 + experimental: true + + runs-on: ubuntu-latest + + defaults: + run: + working-directory: mediawiki + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, intl + tools: composer + + - name: Cache MediaWiki + id: cache-mediawiki + uses: actions/cache@v3 + with: + path: | + mediawiki + !mediawiki/extensions/ + !mediawiki/vendor/ + key: mw_${{ matrix.mw }}-php${{ matrix.php }}-v20 + + - name: Cache Composer cache + uses: actions/cache@v3 + with: + path: ~/.composer/cache + key: composer-php${{ matrix.php }} + + - uses: actions/checkout@v3 + with: + path: EarlyCopy + + - name: Install MediaWiki + if: steps.cache-mediawiki.outputs.cache-hit != 'true' + working-directory: ~ + run: bash EarlyCopy/.github/workflows/installWiki.sh ${{ matrix.mw }} + + - uses: actions/checkout@v3 + with: + path: mediawiki/extensions/Plausible + + - name: Composer update + run: composer update + + - name: Run PHPUnit + run: composer phpunit:entrypoint -- --group Plausible \ No newline at end of file diff --git a/.github/workflows/installWiki.sh b/.github/workflows/installWiki.sh new file mode 100644 index 0000000..6a5f065 --- /dev/null +++ b/.github/workflows/installWiki.sh @@ -0,0 +1,36 @@ +#! /bin/bash + +MW_BRANCH=$1 +EXTENSION_NAME=$2 + +wget https://github.com/wikimedia/mediawiki/archive/$MW_BRANCH.tar.gz -nv + +tar -zxf $MW_BRANCH.tar.gz +mv mediawiki-$MW_BRANCH mediawiki + +cd mediawiki + +composer install +php maintenance/install.php --dbtype sqlite --dbuser root --dbname mw --dbpath $(pwd) --pass AdminPassword WikiName AdminUser + +# echo 'error_reporting(E_ALL| E_STRICT);' >> LocalSettings.php +# echo 'ini_set("display_errors", 1);' >> LocalSettings.php +echo '$wgShowExceptionDetails = true;' >> LocalSettings.php +echo '$wgShowDBErrorBacktrace = true;' >> LocalSettings.php +echo '$wgDevelopmentWarnings = true;' >> LocalSettings.php + +echo 'wfLoadExtension( "Plausible" );' >> LocalSettings.php + +cat <> composer.local.json +{ + "require": { + + }, + "extra": { + "merge-plugin": { + "merge-dev": true, + "include": [] + } + } +} +EOT \ No newline at end of file diff --git a/.github/workflows/lint-check.yml b/.github/workflows/lint-check.yml new file mode 100644 index 0000000..7a04f06 --- /dev/null +++ b/.github/workflows/lint-check.yml @@ -0,0 +1,38 @@ +name: Lint Tests +on: + push: + branches: [ master, develop, feature/** ] + pull_request: + branches: [ master ] +jobs: + lint-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Installing PHP + uses: shivammathur/setup-php@master + with: + php-version: '8.0' + - name: Get Composer Cache Directory 2 + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + id: actions-cache + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - name: Cache PHP dependencies + uses: actions/cache@v3 + id: vendor-cache + with: + path: vendor + key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }} + - name: Composer install + if: steps.vendor-cache.outputs.cache-hit != 'true' + run: composer install --no-ansi --no-interaction --no-scripts --no-suggest --prefer-dist + - name: Run Test + run: composer run test diff --git a/README.md b/README.md index e922ba2..3da728e 100644 --- a/README.md +++ b/README.md @@ -17,55 +17,75 @@ $wgPlausibleApikey = ''; // Only necessary when using Extension:PageViewInfo * Done – Navigate to Special:Version on your wiki to verify that the extension is successfully installed. ## Configuration -| Key | Description | Example | Default | -|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|---------| -| $wgPlausibleDomain | Plausible Domain. **Required** | https://plausible.io | null | -| $wgPlausibleDomainKey | Domain Key set on the plausible website. **Required** | plausible.io | null | -| $wgPlausibleHonorDNT | Honor the Do Not Track header and disable tracking. | false | true | -| $wgPlausibleTrackOutboundLinks | Enable Tracking of outbound link clicks. | true | false | -| $wgPlausibleTrackFileDownloads | Enable Tracking of link clicks that lead to files, sending a `File Download` event. See [the official docs](https://plausible.io/docs/file-downloads-tracking). | true | false | -| $wgPlausibleTrackFileDownloadExtensions | List of additional file extensions to track. See [the official docs](https://plausible.io/docs/file-downloads-tracking#which-file-types-are-tracked). | ['js', 'py'] | [] | -| $wgPlausibleTrackLoggedIn | Enable Tracking for logged in users. | true | false | -| $wgPlausibleEnableCustomEvents | Enable to add the global `window.plausible` function needed for custom event tracking. | true | false | -| $wgPlausibleIgnoredTitles | List of page titles that should not be tracked. [Examples](https://github.com/plausible/docs/blob/master/docs/excluding-pages.md#common-use-cases-and-examples). | ['/Page1', '/Special:*', ] | [] | -| $wgPlausibleEnableOptOutTag | Enables or disables the `` tag that allows users to opt-out from being tracked. | false | true | -| $wgPlausibleApiKey | Auth Bearer key for integration with [Extension:PageViewInfo](https://www.mediawiki.org/wiki/Extension:PageViewInfo) | | | +| Key | Description | Example | Default | +|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|---------| +| $wgPlausibleDomain | Plausible Domain. **Required** | https://plausible.io | null | +| $wgPlausibleDomainKey | Domain Key set on the plausible website. **Required** | plausible.io | null | +| $wgPlausibleApiKey | Auth Bearer key for integration with [Extension:PageViewInfo](https://www.mediawiki.org/wiki/Extension:PageViewInfo) | | | +| $wgPlausibleHonorDNT | Honor the Do Not Track header and disable tracking. | false | true | +| $wgPlausibleTrackOutboundLinks | Enable Tracking of outbound link clicks. | true | false | +| $wgPlausibleTrackFileDownloads | Enable Tracking of link clicks that lead to files, sending a `File Download` event. See [the official docs](https://plausible.io/docs/file-downloads-tracking). | true | false | +| $wgPlausibleTrackFileDownloadExtensions | List of additional file extensions to track. See [the official docs](https://plausible.io/docs/file-downloads-tracking#which-file-types-are-tracked). | ['js', 'py'] | [] | +| $wgPlausibleTrackLoggedIn | Enable Tracking for logged in users. | true | false | +| $wgPlausibleEnableTaggedEvents | Enable click tracking via css classes. See [the official docs](https://plausible.io/docs/custom-event-goals#2-add-a-css-class-name-to-the-element-you-want-to-track-on-your-site). | true | false | +| $wgPlausibleIgnoredTitles | List of page titles that should not be tracked. [Examples](https://github.com/plausible/docs/blob/master/docs/excluding-pages.md#common-use-cases-and-examples). | ['/Page1', '/Special:*', ] | [] | +| $wgPlausibleEnableOptOutTag | Enables or disables the `` tag that allows users to opt-out from being tracked. | false | true | ### Included tracking scripts The following tracking modules can be activated by setting the provided configuration key in `LocalSettings.php` to true. -| Key | Description | EventName | -|-------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------| -| $wgPlausibleTrack404 | Sends a `404` event for unknown titles. | `404` | -| $wgPlausibleTrackSearchInput | Send inputs to `#searchInput` to plausible as a custom event named `SearchInput`. | `SearchInput` | -| $wgPlausibleTrackEditButtonClicks | Track clicks to `#ca-edit a` as a custom event named `EditButtonClick`. | `EditButtonClick` | -| $wgPlausibleTrackNavplateClicks | Track clicks to links inside `.navplate` elements. | `Navplate: Click` | -| $wgPlausibleTrackInfoboxClicks | Track clicks to links inside `.mw-capiunto-infobox` elements. | `Infobox: Click` | -| $wgPlausibleTrackCitizenSearchLinks | Only for [Skin:Citizen](https://github.com/StarCitizenTools/mediawiki-skins-Citizen). Track clicks to search result links found in `#typeahead-suggestions`. Event is named `CitizenSearchLinkClick`. | `CitizenSearchLinkClick` | -| $wgPlausibleTrackCitizenMenuLinks | Only for [Skin:Citizen](https://github.com/StarCitizenTools/mediawiki-skins-Citizen). Track clicks to links in the sidebar menu. Event is named `CitizenMenuLinkClick`. | `CitizenMenuLinkClick` | +| Key | Description | Event Name | +|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------| +| $wgPlausibleTrack404 | Sends a `404` event for unknown titles. | `404` | +| $wgPlausibleTrackSearchInput | Send inputs to `#searchInput` to plausible as a custom event named `Search: Input`. | `Search: Input` | +| $wgPlausibleTrackEditButtonClicks | Track clicks to `#ca-edit a` as a custom event named `Edit Button: Click`. | `Edit Button: Click` | +| $wgPlausibleTrackNavplateClicks | Track clicks to links inside `.navplate` elements. | `Navplate: Click` | +| $wgPlausibleTrackInfoboxClicks | Track clicks to links inside `.mw-capiunto-infobox` and `.infobox` elements. | `Infobox: Click` | +| $wgPlausibleTrackCitizenSearchLinks | Only for [Skin:Citizen](https://github.com/StarCitizenTools/mediawiki-skins-Citizen). Track clicks to search result links found in `#typeahead-suggestions`. Event is named `Citizen: Search Link Click`. | `Citizen: Search Link Click` | +| $wgPlausibleTrackCitizenMenuLinks | Only for [Skin:Citizen](https://github.com/StarCitizenTools/mediawiki-skins-Citizen). Track clicks to links in the sidebar menu. Event is named `Citizen: Menu Link Click`. | `Citizen: Menu Link Click` | ### Server Side Tracking Some events can be sent serverside without having to rely on the included plausible client script. The following custom events can be activated: ```php +# Default Configuration $wgPlausibleServerSideTracking = [ + // Event Name: pageview 'pageview' => false, + // Event Name: 404 'page404' => false, - 'pageedit' => true, - 'pagedelete' => true, - 'pageundelete' => true, - 'pagemove' => true, - 'userregister' => true, - 'userlogin' => true, - 'userlogout' => true, - 'fileupload' => true, - 'filedelete' => true, - 'fileundelete' => true, + // Event Name: Page: Edit + 'pageedit' => true, // Page has been successfully edited + // Event Name: Page: Delete + 'pagedelete' => true, // Page has been deleted + // Event Name: Page: Undelete + 'pageundelete' => true, // Page has been undeleted + // Event Name: Page: Move + 'pagemove' => true, // Page was moved + // Event Name: User: Register + 'userregister' => false, // A new user registered + // Event Name: User: Login + 'userlogin' => false, // A user logged in + // Event Name: User: Logout + 'userlogout' => false, // A user logged out + // Event Name: File: Upload + 'fileupload' => true, // A file was uploaded + // Event Name: File: Delete + 'filedelete' => true, // A file was deleted + // Event Name: File: Undelete + 'fileundelete' => true, // A file was undeleted + // Event Name: Search: Not found + 'searchnotfound' => true, // A searched term was not found / has no title on the wiki + // Event Name: Search: Found + 'searchfound' => true, // A searched term was found / has a corresponding title on the wiki ]; ``` +### Event / Goal Names +This extension chooses the following convention for naming events / goals: `Subject: Event/Action`. + ## Tracking Custom Events https://github.com/plausible/docs/blob/master/docs/custom-event-goals.md @@ -80,10 +100,31 @@ if (typeof window.plausible === 'undefined') { } document.querySelector('#ca-edit a').addEventListener('click', function (event) { - plausible('Editbtn Clicked'); + plausible('Edit Button: Click'); }); ``` +### Via css classes +With setting `$wgPlausibleEnableTaggedEvents = true;` click to elements can be tracked by setting css classes. +From [the official docs](https://plausible.io/docs/custom-event-goals): +> You can also add class names directly in HTML +> If you can edit the raw HTML code of the element you want to track, you can also add the classes directly in HTML. For example: +> +> `````` +> `````` +> `````` +> `````` +> +> Or if your element already has a class attribute, just separate the new ones with a space: +> +> `````` +> `````` +> +> `````` +> `````` + +> When you send custom events to Plausible, they won't show up in your dashboard automatically. You'll have to configure the goal for the conversion numbers to show up. + ## Ignoring Pages https://github.com/plausible/docs/blob/master/docs/excluding-pages.md#common-use-cases-and-examples diff --git a/composer.json b/composer.json index 65a44a0..c4c959d 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "octfx/plausible", - "version": "1.3.1", + "version": "1.4.0", "type": "mediawiki-extension", "description": "Integrates plausible analytics", "homepage": "https://www.mediawiki.org/wiki/Extension:Plausible", @@ -13,7 +13,7 @@ } ], "require": { - "php": ">=7.2", + "php": ">=8.0", "ext-json": "*", "composer/installers": ">=1.0.1" }, diff --git a/extension.json b/extension.json index cce5f1b..f501819 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "Plausible", - "version": "1.3.1", + "version": "1.4.0", "author": [ "[https://www.mediawiki.org/wiki/User:Octfx Octfx]" ], @@ -11,7 +11,7 @@ "requires": { "MediaWiki": ">= 1.39.0", "platform": { - "php": ">=7.3.19" + "php": ">=8.0" } }, "config": { @@ -43,8 +43,8 @@ "description": "Enable Tracking for logged in users", "value": false }, - "PlausibleEnableCustomEvents": { - "description": "Set to true to add the window.plausible function. Needed for custom event goals etc.", + "PlausibleEnableTaggedEvents": { + "description": "Enable tracking clicks to tagged elements via css classes", "value": false }, "PlausibleIgnoredTitles": { @@ -68,7 +68,7 @@ "value": false }, "PlausibleTrackInfoboxClicks": { - "description": "Tracks clicks to links inside .mw-capiunto-infobox elements", + "description": "Tracks clicks to links inside .mw-capiunto-infobox and .infobox elements", "value": false }, "PlausibleTrackCitizenSearchLinks": { @@ -96,15 +96,16 @@ "pagedelete": true, "pageundelete": true, "pagemove": true, - "userregister": true, - "userlogin": true, - "userlogout": true, + "userregister": false, + "userlogin": false, + "userlogout": false, "fileupload": true, "filedelete": true, "fileundelete": true, "searchnotfound": true, "searchfound": true - } + }, + "merge_strategy": "array_plus" } }, "ConfigRegistry": { @@ -160,17 +161,17 @@ }, "Hooks": { "BeforePageDisplay": "PageHooks", - "PageSaveCompleteHook": "PageHooks", - "ArticleDeleteAfterSuccessHook": "PageHooks", + "PageSaveComplete": "PageHooks", + "ArticleDeleteAfterSuccess": "PageHooks", "ParserFirstCallInit": "ParserHooks", "MediaWikiServices": "MediaWikiServices", - "LocalUserCreatedHook": "UserHooks", - "UserLogoutCompleteHook": "UserHooks", - "UserLoginCompleteHook": "UserHooks", - "UploadCompleteHook": "FileHooks", - "FileDeleteCompleteHook": "FileHooks", - "SpecialSearchNogomatchHook": "SearchHooks", - "SpecialSearchGoResultHook": "SearchHooks", + "LocalUserCreated": "UserHooks", + "UserLogoutComplete": "UserHooks", + "UserLoginComplete": "UserHooks", + "UploadComplete": "FileHooks", + "FileDeleteComplete": "FileHooks", + "SpecialSearchNogomatch": "SearchHooks", + "SpecialSearchGoResult": "SearchHooks", "ScribuntoExternalLibraries": "MediaWiki\\Extension\\Plausible\\Hooks\\ScribuntoHooks::onScribuntoExternalLibraries" }, "ResourceModules": { diff --git a/includes/Hooks/FileHooks.php b/includes/Hooks/FileHooks.php index 6a71f24..4e83119 100644 --- a/includes/Hooks/FileHooks.php +++ b/includes/Hooks/FileHooks.php @@ -15,6 +15,10 @@ class FileHooks implements UploadCompleteHook, FileDeleteCompleteHook, FileUndel private array $config; private JobQueueGroup $jobs; + /** + * @param Config $config + * @param JobQueueGroup $group + */ public function __construct( Config $config, JobQueueGroup $group ) { $this->config = $config->get( 'PlausibleServerSideTracking' ); $this->jobs = $group; @@ -23,33 +27,35 @@ public function __construct( Config $config, JobQueueGroup $group ) { /** * @inheritDoc */ - public function onFileDeleteComplete( $file, $oldimage, $article, $user, $reason ) { + public function onFileDeleteComplete( $file, $oldimage, $article, $user, $reason ): void { if ( !$this->config['filedelete'] ) { return; } - $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), 'filedelete' ) ); + $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), 'File: Delete' ) ); } /** * @inheritDoc */ - public function onUploadComplete( $uploadBase ) { + public function onUploadComplete( $uploadBase ): void { if ( !$this->config['fileupload'] ) { return; } - $this->jobs->push( PlausibleEventJob::newFromRequest( RequestContext::getMain()->getRequest(), 'fileupload' ) ); + $this->jobs->push( + PlausibleEventJob::newFromRequest( RequestContext::getMain()->getRequest(), 'File: Upload' ) + ); } /** * @inheritDoc */ - public function onFileUndeleteComplete( $title, $fileVersions, $user, $reason ) { + public function onFileUndeleteComplete( $title, $fileVersions, $user, $reason ): void { if ( !$this->config['fileundelete'] ) { return; } - $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), 'fileundelete' ) ); + $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), 'File: Undelete' ) ); } } diff --git a/includes/Hooks/MediaWikiServices.php b/includes/Hooks/MediaWikiServices.php index 6f6c806..fa47de5 100644 --- a/includes/Hooks/MediaWikiServices.php +++ b/includes/Hooks/MediaWikiServices.php @@ -14,8 +14,9 @@ class MediaWikiServices implements MediaWikiServicesHook { /** * @inheritDoc */ - public function onMediaWikiServices( $services ) { - if ( !ExtensionRegistry::getInstance()->isLoaded( 'PageViewInfo' ) ) { + public function onMediaWikiServices( $services ): void { + if ( !ExtensionRegistry::getInstance()->isLoaded( 'PageViewInfo' ) || + empty( $services->getMainConfig()->get( 'PlausibleApiKey' ) ) ) { return; } diff --git a/includes/Hooks/PageHooks.php b/includes/Hooks/PageHooks.php index 0924898..7d15f36 100644 --- a/includes/Hooks/PageHooks.php +++ b/includes/Hooks/PageHooks.php @@ -37,11 +37,21 @@ /** * Hooks to run relating the page */ -class PageHooks implements BeforePageDisplayHook, PageSaveCompleteHook, ArticleDeleteAfterSuccessHook, ArticleUndeleteHook, PageMoveCompleteHook { +class PageHooks implements + BeforePageDisplayHook, + PageSaveCompleteHook, + ArticleDeleteAfterSuccessHook, + ArticleUndeleteHook, + PageMoveCompleteHook +{ private array $config; private JobQueueGroup $jobs; + /** + * @param Config $config + * @param JobQueueGroup $group + */ public function __construct( Config $config, JobQueueGroup $group ) { $this->config = $config->get( 'PlausibleServerSideTracking' ); $this->jobs = $group; @@ -76,44 +86,53 @@ public function onBeforePageDisplay( $out, $skin ): void { /** * @inheritDoc */ - public function onArticleDeleteAfterSuccess( $title, $outputPage ) { + public function onArticleDeleteAfterSuccess( $title, $outputPage ): void { if ( !$this->config['pagedelete'] ) { return; } - $this->jobs->push( PlausibleEventJob::newFromRequest( $outputPage->getRequest(), 'pagedelete' ) ); + $this->jobs->push( PlausibleEventJob::newFromRequest( $outputPage->getRequest(), 'Page: Delete' ) ); } /** * @inheritDoc */ - public function onPageSaveComplete( $wikiPage, $user, $summary, $flags, $revisionRecord, $editResult ) { + public function onPageSaveComplete( $wikiPage, $user, $summary, $flags, $revisionRecord, $editResult ): void { if ( !$this->config['pageedit'] || $editResult->isNullEdit() ) { return; } - $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), 'pageedit' ) ); + $this->jobs->push( PlausibleEventJob::newFromRequest( + $user->getRequest(), + 'Page: Edit', + [ + 'title' => $wikiPage->getTitle()->getText(), + 'user' => $user->isRegistered() ? $user->getName() : null, + ] + ) ); } /** * @inheritDoc */ - public function onArticleUndelete( $title, $create, $comment, $oldPageId, $restoredPages ) { + public function onArticleUndelete( $title, $create, $comment, $oldPageId, $restoredPages ): void { if ( !$this->config['pageundelete'] ) { return; } - $this->jobs->push( PlausibleEventJob::newFromRequest( RequestContext::getMain()->getRequest(), 'pageedit' ) ); + $this->jobs->push( + PlausibleEventJob::newFromRequest( RequestContext::getMain()->getRequest(), 'Page: Undelete' ) + ); } /** * @inheritDoc */ - public function onPageMoveComplete( $old, $new, $user, $pageid, $redirid, $reason, $revision ) { + public function onPageMoveComplete( $old, $new, $user, $pageid, $redirid, $reason, $revision ): void { if ( !$this->config['pagemove'] ) { return; } - $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), 'pagemove' ) ); + $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), 'Page: Move' ) ); } } diff --git a/includes/Hooks/ParserHooks.php b/includes/Hooks/ParserHooks.php index f8310eb..e261d4a 100644 --- a/includes/Hooks/ParserHooks.php +++ b/includes/Hooks/ParserHooks.php @@ -12,7 +12,7 @@ class ParserHooks implements ParserFirstCallInitHook { /** * @inheritDoc */ - public function onParserFirstCallInit( $parser ) { + public function onParserFirstCallInit( $parser ): void { $config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'Plausible' ); if ( $config->get( 'PlausibleEnableOptOutTag' ) === false ) { return; diff --git a/includes/Hooks/ScribuntoHooks.php b/includes/Hooks/ScribuntoHooks.php index ee6e8c8..adb1190 100644 --- a/includes/Hooks/ScribuntoHooks.php +++ b/includes/Hooks/ScribuntoHooks.php @@ -30,7 +30,7 @@ class ScribuntoHooks { * Register Lua Library * * @param string $engine - * @param array $extraLibraries + * @param array &$extraLibraries * @return bool */ public static function onScribuntoExternalLibraries( string $engine, array &$extraLibraries ): bool { diff --git a/includes/Hooks/SearchHooks.php b/includes/Hooks/SearchHooks.php index c3c6d80..87e5204 100644 --- a/includes/Hooks/SearchHooks.php +++ b/includes/Hooks/SearchHooks.php @@ -14,6 +14,10 @@ class SearchHooks implements SpecialSearchNogomatchHook, SpecialSearchGoResultHo private array $config; private JobQueueGroup $jobs; + /** + * @param Config $config + * @param JobQueueGroup $group + */ public function __construct( Config $config, JobQueueGroup $group ) { $this->config = $config->get( 'PlausibleServerSideTracking' ); $this->jobs = $group; @@ -22,16 +26,16 @@ public function __construct( Config $config, JobQueueGroup $group ) { /** * @inheritDoc */ - public function onSpecialSearchNogomatch( &$title ) { + public function onSpecialSearchNogomatch( &$title ): void { if ( !$this->config['searchnotfound'] ) { return; } $this->jobs->push( PlausibleEventJob::newFromRequest( RequestContext::getMain()->getRequest(), - 'searchnotfound', + 'Search: Not Found', [ - 'title' => $title->getText(), + 'term' => $title->getText(), ] ) ); } @@ -39,14 +43,14 @@ public function onSpecialSearchNogomatch( &$title ) { /** * @inheritDoc */ - public function onSpecialSearchGoResult( $term, $title, &$url ) { + public function onSpecialSearchGoResult( $term, $title, &$url ): void { if ( !$this->config['searchfound'] ) { return; } $this->jobs->push( PlausibleEventJob::newFromRequest( RequestContext::getMain()->getRequest(), - 'searchfound', + 'Search: Found', [ 'term' => $term, 'title' => $title->getText(), diff --git a/includes/Hooks/UserHooks.php b/includes/Hooks/UserHooks.php index 61b3e19..5051f0e 100644 --- a/includes/Hooks/UserHooks.php +++ b/includes/Hooks/UserHooks.php @@ -14,6 +14,10 @@ class UserHooks implements LocalUserCreatedHook, UserLogoutCompleteHook, UserLog private array $config; private JobQueueGroup $jobs; + /** + * @param Config $config + * @param JobQueueGroup $group + */ public function __construct( Config $config, JobQueueGroup $group ) { $this->config = $config->get( 'PlausibleServerSideTracking' ); $this->jobs = $group; @@ -22,16 +26,17 @@ public function __construct( Config $config, JobQueueGroup $group ) { /** * @inheritDoc */ - public function onLocalUserCreated( $user, $autocreated ) { + public function onLocalUserCreated( $user, $autocreated ): void { if ( !$this->config['userregister'] ) { return; } $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), - 'userregister', + 'User: Register', [ 'user' => $user->isRegistered() ? $user->getName() : null, + 'autocreated' => $autocreated, ] ) ); } @@ -39,14 +44,14 @@ public function onLocalUserCreated( $user, $autocreated ) { /** * @inheritDoc */ - public function onUserLoginComplete( $user, &$inject_html, $direct ) { - if ( !$this->config['userlogin'] ) { + public function onUserLoginComplete( $user, &$inject_html, $direct ): void { + if ( !$this->config['userlogin'] || !$direct ) { return; } $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), - 'userlogin', + 'User: Login', [ 'user' => $user->isRegistered() ? $user->getName() : null, ] @@ -56,14 +61,14 @@ public function onUserLoginComplete( $user, &$inject_html, $direct ) { /** * @inheritDoc */ - public function onUserLogoutComplete( $user, &$inject_html, $oldName ) { + public function onUserLogoutComplete( $user, &$inject_html, $oldName ): void { if ( !$this->config['userlogout'] ) { return; } $this->jobs->push( PlausibleEventJob::newFromRequest( $user->getRequest(), - 'userlogout', + 'User: Logout', [ 'user' => $user->isRegistered() ? $user->getName() : null, ] diff --git a/includes/OptOut.php b/includes/OptOut.php index 886d8f6..853a0bc 100644 --- a/includes/OptOut.php +++ b/includes/OptOut.php @@ -10,9 +10,10 @@ class OptOut { /** * * + * @param string|null $input + * @param array $args Arguments * @param Parser $parser The active Parser instance * @param PPFrame $frame Frame - * @param array $args Arguments * * @return string The button */ diff --git a/includes/Plausible.php b/includes/Plausible.php index e27b60e..88ca66c 100644 --- a/includes/Plausible.php +++ b/includes/Plausible.php @@ -23,7 +23,6 @@ use Config; use ConfigException; -use MediaWiki\MediaWikiServices; use OutputPage; class Plausible { @@ -47,21 +46,17 @@ class Plausible { */ private $domainKey; - /** - * True if window.plausible was added - * - * @var bool - */ - private $windowFnAdded = false; - /** * @var Config */ private $config; + /** + * @param OutputPage $out + */ public function __construct( OutputPage $out ) { $this->out = $out; - $this->config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'Plausible' ); + $this->config = $out->getConfig(); $this->plausibleDomain = $this->getConfigValue( 'PlausibleDomain' ); $this->domainKey = $this->getConfigValue( 'PlausibleDomainKey' ); } @@ -75,16 +70,7 @@ public function addScript(): void { } $script = $this->buildScript(); - // Script needs to be placed in - if ( strpos( $script, 'plausible.js' ) === false ) { - $this->out->addHeadItem( 'plausible', $script ); - } else { - $this->out->addScript( $script ); - } - - if ( $this->getConfigValue( 'PlausibleEnableCustomEvents', false ) === true ) { - $this->addWindowDotPlausible(); - } + $this->out->addHeadItem( 'plausible', $script ); } /** @@ -106,8 +92,6 @@ public function addModules(): void { 'PlausibleTrackCitizenMenuLinks' => 'ext.plausible.scripts.citizen.track-menu-links', ]; - $anythingAdded = false; - foreach ( $availableModules as $config => $module ) { if ( $this->getConfigValue( $config, false ) === false ) { continue; @@ -119,33 +103,9 @@ public function addModules(): void { } $this->out->addModules( $module ); - $anythingAdded = true; - } - - if ( $anythingAdded ) { - $this->addWindowDotPlausible(); } } - /** - * Adds the global window.plausible function - */ - private function addWindowDotPlausible(): void { - if ( $this->windowFnAdded === true ) { - return; - } - - $nonce = $this->out->getCSP()->getNonce(); - - $this->out->addScript( - sprintf( - '', - $nonce !== false ? $nonce : '' - ) - ); - $this->windowFnAdded = true; - } - /** * Builds the complete script * @@ -168,7 +128,8 @@ private function buildScript(): string { * @return string */ private function buildScriptPath(): string { - $name = 'plausible'; + $name = 'script'; + $name = sprintf( '%s.pageview-props', $name ); if ( $this->getConfigValue( 'PlausibleTrackOutboundLinks', false ) === true ) { $name = sprintf( '%s.outbound-links', $name ); @@ -182,6 +143,10 @@ private function buildScriptPath(): string { $name = sprintf( '%s.file-downloads', $name ); } + if ( $this->getConfigValue( 'PlausibleEnableTaggedEvents', false ) === true ) { + $name = sprintf( '%s.tagged-events', $name ); + } + return sprintf( '%s/js/%s.js', rtrim( $this->plausibleDomain, '/' ), diff --git a/includes/PlausibleEventJob.php b/includes/PlausibleEventJob.php index 209d65a..97801b3 100644 --- a/includes/PlausibleEventJob.php +++ b/includes/PlausibleEventJob.php @@ -1,42 +1,62 @@ params['url'] ) || !is_string( $this->params['agent'] ) ) { + if ( empty( $this->params['url'] ) || empty( $this->params['agent'] ) ) { return false; } $config = MediaWikiServices::getInstance()->getMainConfig(); - $request = MediaWikiServices::getInstance()->getHttpRequestFactory()->create( - sprintf( '%s/api/event', $config->get( 'PlausibleDomain' ) ), - [ - 'userAgent' => $this->params['agent'], - 'postData' => [ - 'domain' => $config->get( 'PlausibleDomainKey' ), - 'name' => 'pageview', - 'url' => $this->params['url'], - 'props' => $this->params['props'] ?? [], - ], - ] - ); + try { + if ( !$config->get( 'PlausibleTrackLoggedIn' ) && ( $this->params['isAnon'] ?? true ) === false ) { + return true; + } + + $this->params['props']['isAnon'] = $this->params['isAnon']; + + $request = MediaWikiServices::getInstance()->getHttpRequestFactory()->create( + sprintf( '%s/api/event', $config->get( 'PlausibleDomain' ) ), + [ + 'method' => 'POST', + 'userAgent' => $this->params['agent'], + 'postData' => json_encode( [ + 'domain' => $config->get( 'PlausibleDomainKey' ), + 'name' => $this->params['event'], + 'url' => $this->params['url'], + 'props' => $this->params['props'] ?? [], + ], JSON_THROW_ON_ERROR ), + ] + ); + } catch ( JsonException | ConfigException $e ) { + return false; + } $request->setHeader( 'Content-Type', 'application/json' ); $request->setHeader( 'X-Forwarded-For', $this->params['ip'] ); @@ -56,12 +76,19 @@ public function run(): bool { */ public static function newFromRequest( WebRequest $request, string $event = 'pageview', array $props = [] ): Job { try { + $url = $request->getFullRequestURL(); + if ( isset( $props['title'] ) ) { + $url = ( Title::newFromText( $props['title'] ) )->getFullURL(); + unset( $props['title'] ); + } + return new self( [ 'event' => $event, 'ip' => $request->getIP(), - 'url' => $request->getRequestURL(), + 'url' => $url, 'agent' => $request->getHeader( 'User-Agent' ), 'props' => $props, + 'isAnon' => $request->getSession()->getUser()->isAnon(), ] ); } catch ( Exception $e ) { return new NullJob( [ 'removeDuplicates' => true ] ); diff --git a/includes/PlausibleLua.php b/includes/PlausibleLua.php index 191ca6a..415af1a 100644 --- a/includes/PlausibleLua.php +++ b/includes/PlausibleLua.php @@ -26,6 +26,9 @@ use MediaWiki\Title\Title; use Scribunto_LuaLibraryBase; +/** + * phpcs:disable MediaWiki.Commenting.FunctionComment.ExtraParamComment + */ class PlausibleLua extends Scribunto_LuaLibraryBase { /** @@ -45,7 +48,7 @@ public function register() { /** * The top pages in the last day (or specified days) for this site * - * @param $days int optional Number of days to calculate the top pages over + * @param int $days optional Number of days to calculate the top pages over * * @return array|array[] */ @@ -85,7 +88,7 @@ public function getTopPages(): array { /** * Number of views for whole site * - * @param $days int optional Number of days for returning the views + * @param int $days optional Number of days for returning the views * * @return array */ @@ -107,8 +110,9 @@ public function getSiteData(): array { /** * Number of views for the specified titles * - * @param $titles string|string[] The titles to work on - * @param $days int optional Number of days to calculate the views over + * MediaWiki.Commenting.FunctionComment.ExtraParamComment + * @param string|string[] $titles The titles to work on + * @param int $days optional Number of days to calculate the views over * * @return array */ diff --git a/includes/PlausiblePageViewService.php b/includes/PlausiblePageViewService.php index 6c288c2..bd8f90c 100644 --- a/includes/PlausiblePageViewService.php +++ b/includes/PlausiblePageViewService.php @@ -175,6 +175,9 @@ public function getTopPages( $metric = self::METRIC_VIEW ) { /** * This is getTopPages with a configurable day range + * @param int $days + * @param string $metric One of the METRIC_* constants. + * @return StatusValue */ public function getTopPagesDays( $days = 1, $metric = self::METRIC_VIEW ) { if ( !in_array( $metric, [ self::METRIC_VIEW, self::METRIC_UNIQUE ] ) ) { diff --git a/resources/ext.plausible.scripts.citizen.track-menu-links/track-menu-links.js b/resources/ext.plausible.scripts.citizen.track-menu-links/track-menu-links.js index a057e31..e6e0867 100644 --- a/resources/ext.plausible.scripts.citizen.track-menu-links/track-menu-links.js +++ b/resources/ext.plausible.scripts.citizen.track-menu-links/track-menu-links.js @@ -1,6 +1,7 @@ // Menu Link click Tracking ( function () { - var eventName = 'CitizenMenuLinkClick'; + var eventName = 'Citizen: Menu Link Click', + isAnon = mw.user?.tokens?.values?.watchToken === null || mw.user?.tokens?.values?.watchToken === '+\\'; if ( typeof window.plausible === 'undefined' || window.plausible.length === 0 ) { return; @@ -15,7 +16,8 @@ window.plausible( eventName, { props: { entry: event.target.innerText, - path: event.target.href + title: event.target.href, + isAnon, }, callback: function () { window.location = event.target.href; diff --git a/resources/ext.plausible.scripts.citizen.track-search-links/track-search-links.js b/resources/ext.plausible.scripts.citizen.track-search-links/track-search-links.js index 8884aa0..00dcdc4 100644 --- a/resources/ext.plausible.scripts.citizen.track-search-links/track-search-links.js +++ b/resources/ext.plausible.scripts.citizen.track-search-links/track-search-links.js @@ -1,8 +1,9 @@ // Search Link click Tracking ( function () { - var eventName = 'CitizenSearchLinkClick', + var eventName = 'Citizen: Search Link Click', suggestions = document.getElementById( 'searchform' ), search = document.getElementById( 'searchInput' ), + isAnon = mw.user?.tokens?.values?.watchToken === null || mw.user?.tokens?.values?.watchToken === '+\\', callback = function ( event ) { var currentEl, href = null, @@ -38,8 +39,9 @@ window.plausible( eventName, { props: { - query: searchValue, - path: url.pathname + term: searchValue, + title: url.pathname, + isAnon, }, callback: function () { window.location = href; diff --git a/resources/ext.plausible.scripts.track-404/track-404.js b/resources/ext.plausible.scripts.track-404/track-404.js index 9d6c7ac..019954a 100644 --- a/resources/ext.plausible.scripts.track-404/track-404.js +++ b/resources/ext.plausible.scripts.track-404/track-404.js @@ -1,6 +1,7 @@ // 404 page Tracking ( function () { - var eventName = '404'; + var eventName = '404', + isAnon = mw.user?.tokens?.values?.watchToken === null || mw.user?.tokens?.values?.watchToken === '+\\'; if ( typeof window.plausible === 'undefined' || window.plausible.length === 0 || typeof mw.config === 'undefined' ) { return; @@ -9,7 +10,8 @@ if ( mw.config.get( 'is404', false ) === true ) { window.plausible( eventName, { props: { - path: document.location.pathname + title: document.location.pathname, + isAnon, } } ); } diff --git a/resources/ext.plausible.scripts.track-edit-btn/track-edit-btn.js b/resources/ext.plausible.scripts.track-edit-btn/track-edit-btn.js index 7617123..8c8310a 100644 --- a/resources/ext.plausible.scripts.track-edit-btn/track-edit-btn.js +++ b/resources/ext.plausible.scripts.track-edit-btn/track-edit-btn.js @@ -5,10 +5,12 @@ } var registerEvent = function() { - var eventName = 'EditButtonClick'; + var eventName = 'Edit Button: Click', + isAnon = mw.user?.tokens?.values?.watchToken === null || mw.user?.tokens?.values?.watchToken === '+\\'; + window.plausible( eventName, { props: { - path: document.location.pathname + isAnon, } } ); }; diff --git a/resources/ext.plausible.scripts.track-infobox-clicks/track-infobox-clicks.js b/resources/ext.plausible.scripts.track-infobox-clicks/track-infobox-clicks.js index 214e807..93d5fd8 100644 --- a/resources/ext.plausible.scripts.track-infobox-clicks/track-infobox-clicks.js +++ b/resources/ext.plausible.scripts.track-infobox-clicks/track-infobox-clicks.js @@ -1,15 +1,20 @@ // Infobox Link Click Tracking ( function () { var eventName = 'Infobox: Click', - infoboxes = document.querySelectorAll( '.mw-capiunto-infobox' ); + infoboxes = [ + ...Array.from(document.querySelectorAll( '.mw-capiunto-infobox' )), + ...Array.from(document.querySelectorAll( '.infobox' )), + ], + isAnon = mw.user?.tokens?.values?.watchToken === null || mw.user?.tokens?.values?.watchToken === '+\\'; - if ( typeof window.plausible === 'undefined' || window.plausible.length === 0 || infoboxes === null ) { + + if ( typeof window.plausible === 'undefined' || window.plausible.length === 0 || infoboxes.length === null ) { return; } infoboxes.forEach(infobox => { infobox.querySelectorAll('a:not(.new)').forEach(link => { - link.addEventListener('click', function (event) { + const callback = function (event) { if (link.getAttribute('href') === null) { return; } @@ -19,7 +24,8 @@ eventName, { props: { - link: 'Infobox Image' + title: 'Infobox Image', + isAnon, } } ); @@ -30,7 +36,8 @@ eventName, { props: { - link: link.textContent + title: link.innerText, + isAnon, }, callback: function () { window.location = link.getAttribute('href'); @@ -38,7 +45,10 @@ } ); } - }); + }; + + link.removeEventListener('click', callback); + link.addEventListener('click', callback); }); }); }() ); diff --git a/resources/ext.plausible.scripts.track-navplate-clicks/track-navplate-clicks.js b/resources/ext.plausible.scripts.track-navplate-clicks/track-navplate-clicks.js index e78e322..6847479 100644 --- a/resources/ext.plausible.scripts.track-navplate-clicks/track-navplate-clicks.js +++ b/resources/ext.plausible.scripts.track-navplate-clicks/track-navplate-clicks.js @@ -1,7 +1,8 @@ // Navplate Link Click Tracking ( function () { var eventName = 'Navplate: Click', - navplates = document.querySelectorAll( '.navplate' ); + navplates = document.querySelectorAll( '.navplate' ), + isAnon = mw.user?.tokens?.values?.watchToken === null || mw.user?.tokens?.values?.watchToken === '+\\'; if ( typeof window.plausible === 'undefined' || window.plausible.length === 0 || navplates === null ) { return; @@ -20,7 +21,8 @@ eventName, { props: { - link: link.textContent + title: link.innerText, + isAnon, }, callback: function () { window.location = link.getAttribute('href'); diff --git a/resources/ext.plausible.scripts.track-search/track-search.js b/resources/ext.plausible.scripts.track-search/track-search.js index 1682c4b..0cfc6da 100644 --- a/resources/ext.plausible.scripts.track-search/track-search.js +++ b/resources/ext.plausible.scripts.track-search/track-search.js @@ -1,7 +1,8 @@ // Search Input Tracking ( function () { - var eventName = 'SearchInput', + var eventName = 'Search: Input', search = document.getElementById( 'searchInput' ), + isAnon = mw.user?.tokens?.values?.watchToken === null || mw.user?.tokens?.values?.watchToken === '+\\', sendAfter = 1500, // ms minLength = 3, timeoutId; @@ -18,8 +19,9 @@ window.plausible( eventName, { props: { - query: event.target.value, - path: document.location.pathname + term: event.target.value, + title: document.location.pathname, + isAnon, } } ); diff --git a/resources/ext.plausible.scripts.track-special-search/track-special-search.js b/resources/ext.plausible.scripts.track-special-search/track-special-search.js index 0b777c6..8d7d180 100644 --- a/resources/ext.plausible.scripts.track-special-search/track-special-search.js +++ b/resources/ext.plausible.scripts.track-special-search/track-special-search.js @@ -1,7 +1,8 @@ // Special Search Input Tracking ( function () { - var eventName = 'SpecialSearchInput', + var eventName = 'Special Search: Input', search = document.querySelector( 'body.mw-special-Search input[type="search"]' ), + isAnon = mw.user?.tokens?.values?.watchToken === null || mw.user?.tokens?.values?.watchToken === '+\\', sendAfter = 1500, // ms minLength = 3, timeoutId; @@ -18,8 +19,9 @@ window.plausible( eventName, { props: { - query: event.target.value, - path: document.location.pathname + term: event.target.value, + title: document.location.pathname, + isAnon, } } ); diff --git a/tests/phpunit/Hooks/FileHooksTest.php b/tests/phpunit/Hooks/FileHooksTest.php new file mode 100644 index 0000000..cc40d08 --- /dev/null +++ b/tests/phpunit/Hooks/FileHooksTest.php @@ -0,0 +1,189 @@ +overrideConfigValues( [ + 'PlausibleTrackOutboundLinks' => false, + 'PlausibleTrackFileDownloads' => false, + 'PlausibleTrackLoggedIn' => false, + 'PlausibleEnableTaggedEvents' => false, + ] ); + + $this->mockQueue = $this->getMockBuilder( JobQueueGroup::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'push' ] ) + ->getMock(); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\FileHooks + * + * @return void + * @throws Exception + */ + public function testConstructor() { + $hooks = new FileHooks( + $this->getServiceContainer()->getMainConfig(), + $this->getServiceContainer()->getJobQueueGroup() + ); + + $this->assertInstanceOf( FileHooks::class, $hooks ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\FileHooks::onUploadComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testUploadComplete() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new FileHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onUploadComplete( new UploadFromFile() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\FileHooks::onFileDeleteComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testDeleteComplete() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new FileHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $file = new LocalFile( + Title::newFromText( 'Foo.jpg', NS_FILE ), + $this->getServiceContainer()->getRepoGroup()->getLocalRepo() + ); + + $hooks->onFileDeleteComplete( $file, null, null, User::createNew( 'Foo' ), '' ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\FileHooks::onFileUndeleteComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testUndeleteComplete() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new FileHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onFileUndeleteComplete( Title::newFromText( 'Foo.jpg', NS_FILE ), [], User::createNew( 'Foo2' ), '' ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\FileHooks::onUploadComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testUploadCompleteDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'fileupload' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new FileHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onUploadComplete( new UploadFromFile() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\FileHooks::onFileDeleteComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testDeleteCompleteDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'filedelete' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new FileHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $file = new LocalFile( + Title::newFromText( 'Foo.jpg', NS_FILE ), + $this->getServiceContainer()->getRepoGroup()->getLocalRepo() + ); + + $hooks->onFileDeleteComplete( $file, null, null, User::createNew( 'Foo' ), '' ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\FileHooks::onFileUndeleteComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testUndeleteCompleteDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'fileundelete' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new FileHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onFileUndeleteComplete( Title::newFromText( 'Foo.jpg', NS_FILE ), [], User::createNew( 'Foo2' ), '' ); + } +} diff --git a/tests/phpunit/Hooks/PageHooksTest.php b/tests/phpunit/Hooks/PageHooksTest.php new file mode 100644 index 0000000..736bcbe --- /dev/null +++ b/tests/phpunit/Hooks/PageHooksTest.php @@ -0,0 +1,348 @@ +overrideConfigValues( [ + 'PlausibleTrackOutboundLinks' => false, + 'PlausibleTrackFileDownloads' => false, + 'PlausibleTrackLoggedIn' => false, + 'PlausibleEnableTaggedEvents' => false, + ] ); + + $this->mockQueue = $this->getMockBuilder( JobQueueGroup::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'push' ] ) + ->getMock(); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks + * + * @return void + * @throws Exception + */ + public function testConstructor() { + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->getServiceContainer()->getJobQueueGroup() + ); + + $this->assertInstanceOf( PageHooks::class, $hooks ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onBeforePageDisplay + * @return void + * @throws Exception + */ + public function testOnBeforePageDisplay() { + $this->overrideConfigValues( [ + 'PlausibleDomain' => 'foo', + 'PlausibleDomainKey' => 'foo', + 'PlausibleTrackLoggedIn' => true, + 'PlausibleHonorDNT' => false, + ] ); + + $out = new OutputPage( RequestContext::getMain() ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onBeforePageDisplay( $out, null ); + + $this->assertArrayHasKey( 'plausible', $out->getHeadItemsArray() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onBeforePageDisplay + * @return void + * @throws Exception + */ + public function testOnBeforePageDisplayAllModules() { + $this->overrideConfigValues( [ + 'PlausibleDomain' => 'foo', + 'PlausibleDomainKey' => 'foo', + 'PlausibleTrackLoggedIn' => true, + 'PlausibleHonorDNT' => false, + 'PlausibleTrack404' => true, + 'PlausibleTrackSearchInput' => true, + 'PlausibleTrackEditButtonClicks' => true, + 'PlausibleTrackNavplateClicks' => true, + 'PlausibleTrackInfoboxClicks' => true, + 'PlausibleTrackCitizenSearchLinks' => true, + 'PlausibleTrackCitizenMenuLinks' => true, + ] ); + + $out = new OutputPage( RequestContext::getMain() ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onBeforePageDisplay( $out, null ); + + $this->assertContains( 'ext.plausible.scripts.track-404', $out->getModules() ); + $this->assertContains( 'ext.plausible.scripts.track-search', $out->getModules() ); + $this->assertContains( 'ext.plausible.scripts.track-edit-btn', $out->getModules() ); + $this->assertContains( 'ext.plausible.scripts.track-navplate-clicks', $out->getModules() ); + $this->assertContains( 'ext.plausible.scripts.track-infobox-clicks', $out->getModules() ); + $this->assertContains( 'ext.plausible.scripts.citizen.track-search-links', $out->getModules() ); + $this->assertContains( 'ext.plausible.scripts.citizen.track-menu-links', $out->getModules() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onArticleDeleteAfterSuccess + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testArticleDeleteAfterSuccess() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onArticleDeleteAfterSuccess( + Title::newFromText( 'Foo' ), + new OutputPage( RequestContext::getMain() ) + ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onPageSaveComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testPageSaveComplete() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onPageSaveComplete( + $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( Title::newFromText( 'Foo' ) ), + User::createNew( 'PageSaveComplete' ), + '', + 0, + $this->getMockBuilder( RevisionRecord::class )->disableOriginalConstructor()->getMock(), + // phpcs:ignore Generic.Files.LineLength.TooLong + new EditResult( true, false, null, null, null, false, false, [] ) + ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onArticleUndelete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testArticleUndelete() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onArticleUndelete( + Title::newFromText( 'ArticleUndelete' ), + true, + null, + 0, + [], + ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onPageMoveComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testPageMoveComplete() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onPageMoveComplete( + Title::newFromText( 'Foo' ), + Title::newFromText( 'Bar' ), + User::createNew( 'PageMoveComplete' ), + 0, + 0, + '', + $this->getMockBuilder( RevisionRecord::class )->disableOriginalConstructor()->getMock() + ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onArticleDeleteAfterSuccess + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testArticleDeleteAfterSuccessDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'pageedit' => false, + 'pagedelete' => false, + 'pageundelete' => false, + 'pagemove' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onArticleDeleteAfterSuccess( + Title::newFromText( 'Foo' ), + new OutputPage( RequestContext::getMain() ) + ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onPageSaveComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testPageSaveCompleteDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'pageedit' => false, + 'pagedelete' => false, + 'pageundelete' => false, + 'pagemove' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onPageSaveComplete( + $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( Title::newFromText( 'Foo' ) ), + User::createNew( 'PageSaveCompleteDisabled' ), + '', + 0, + $this->getMockBuilder( RevisionRecord::class )->disableOriginalConstructor()->getMock(), + // phpcs:ignore Generic.Files.LineLength.TooLong + new EditResult( true, false, null, null, null, false, false, [] ) + ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onArticleUndelete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testArticleUndeleteDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'pageedit' => false, + 'pagedelete' => false, + 'pageundelete' => false, + 'pagemove' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onArticleUndelete( + Title::newFromText( 'Foo' ), + true, + null, + 0, + [], + ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\PageHooks::onPageMoveComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testPageMoveCompleteDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'pageedit' => false, + 'pagedelete' => false, + 'pageundelete' => false, + 'pagemove' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new PageHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $hooks->onPageMoveComplete( + Title::newFromText( 'Foo' ), + Title::newFromText( 'Bar' ), + User::createNew( 'PageMoveCompleteDisabled' ), + 0, + 0, + '', + $this->getMockBuilder( RevisionRecord::class )->disableOriginalConstructor()->getMock() + ); + } +} diff --git a/tests/phpunit/Hooks/ParserHooksTest.php b/tests/phpunit/Hooks/ParserHooksTest.php new file mode 100644 index 0000000..8c0d251 --- /dev/null +++ b/tests/phpunit/Hooks/ParserHooksTest.php @@ -0,0 +1,66 @@ +assertInstanceOf( ParserHooks::class, $hooks ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\ParserHooks::onParserFirstCallInit + * + * @return void + * @throws Exception + */ + public function testAddOptOut() { + $this->overrideConfigValues( [ + 'PlausibleEnableOptOutTag' => true, + ] ); + + $hooks = new ParserHooks(); + $parser = $this->getServiceContainer()->getParser(); + + $hooks->onParserFirstCallInit( $parser ); + + $this->assertContains( 'plausible-opt-out', $parser->getTags() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\ParserHooks::onParserFirstCallInit + * + * @return void + * @throws Exception + */ + public function testNotAddOptOut() { + $this->overrideConfigValues( [ + 'PlausibleEnableOptOutTag' => false, + ] ); + + $hooks = new ParserHooks(); + $parser = $this->getServiceContainer()->getParser(); + + $hooks->onParserFirstCallInit( $parser ); + + $this->assertNotContains( 'plausible-opt-out', $parser->getTags() ); + } + +} diff --git a/tests/phpunit/Hooks/SearchHooksTest.php b/tests/phpunit/Hooks/SearchHooksTest.php new file mode 100644 index 0000000..e31856b --- /dev/null +++ b/tests/phpunit/Hooks/SearchHooksTest.php @@ -0,0 +1,141 @@ +overrideConfigValues( [ + 'PlausibleTrackOutboundLinks' => false, + 'PlausibleTrackFileDownloads' => false, + 'PlausibleTrackLoggedIn' => false, + 'PlausibleEnableTaggedEvents' => false, + ] ); + + $this->mockQueue = $this->getMockBuilder( JobQueueGroup::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'push' ] ) + ->getMock(); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\SearchHooks + * + * @return void + * @throws Exception + */ + public function testConstructor() { + $hooks = new SearchHooks( + $this->getServiceContainer()->getMainConfig(), + $this->getServiceContainer()->getJobQueueGroup() + ); + + $this->assertInstanceOf( SearchHooks::class, $hooks ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\SearchHooks::onSpecialSearchGoResult + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testSearchHooksFound() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new SearchHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $empty = null; + + $hooks->onSpecialSearchGoResult( 'Foo', Title::newFromText( 'Foo' ), $empty ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\SearchHooks::onSpecialSearchGoResult + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testSearchHooksFoundDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'searchfound' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new SearchHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $empty = null; + + $hooks->onSpecialSearchGoResult( 'Foo', Title::newFromText( 'Foo' ), $empty ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\SearchHooks::onSpecialSearchNogomatch + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testSearchHooksNotFound() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new SearchHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $title = Title::newFromText( 'Foo' ); + + $hooks->onSpecialSearchNogomatch( $title ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\SearchHooks::onSpecialSearchNogomatch + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testSearchHooksNotFoundDisabled() { + $this->overrideConfigValues( [ + 'PlausibleServerSideTracking' => [ + 'searchnotfound' => false, + ], + ] ); + + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new SearchHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $title = Title::newFromText( 'Foo' ); + + $hooks->onSpecialSearchNogomatch( $title ); + } +} diff --git a/tests/phpunit/Hooks/UserHooksTest.php b/tests/phpunit/Hooks/UserHooksTest.php new file mode 100644 index 0000000..441e0f9 --- /dev/null +++ b/tests/phpunit/Hooks/UserHooksTest.php @@ -0,0 +1,141 @@ +overrideConfigValues( [ + 'PlausibleTrackOutboundLinks' => false, + 'PlausibleTrackFileDownloads' => false, + 'PlausibleTrackLoggedIn' => false, + 'PlausibleEnableTaggedEvents' => false, + 'PlausibleServerSideTracking' => [ + 'userregister' => true, + 'userlogin' => true, + 'userlogout' => true, + ] + ] ); + + $this->mockQueue = $this->getMockBuilder( JobQueueGroup::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'push' ] ) + ->getMock(); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\UserHooks + * + * @return void + * @throws Exception + */ + public function testConstructor() { + $hooks = new UserHooks( + $this->getServiceContainer()->getMainConfig(), + $this->getServiceContainer()->getJobQueueGroup() + ); + + $this->assertInstanceOf( UserHooks::class, $hooks ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\UserHooks::onLocalUserCreated + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testOnLocalUserCreated() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new UserHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $user = $this->getServiceContainer()->getUserFactory()->newFromName( 'OnLocalUserCreated' ); + + $hooks->onLocalUserCreated( $user, false ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\UserHooks::onUserLoginComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testOnUserLoginComplete() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new UserHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $user = $this->getServiceContainer()->getUserFactory()->newFromName( 'OnUserLoginComplete' ); + + $html = ''; + + $hooks->onUserLoginComplete( $user, $html, true ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\UserHooks::onUserLoginComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testOnUserLoginCompleteNotDirect() { + $this->mockQueue->expects( $this->never() )->method( 'push' ); + + $hooks = new UserHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $user = $this->getServiceContainer()->getUserFactory()->newFromName( 'OnUserLoginComplete' ); + + $html = ''; + + $hooks->onUserLoginComplete( $user, $html, false ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Hooks\UserHooks::onUserLogoutComplete + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * + * @return void + * @throws Exception + */ + public function testOnUserLogoutComplete() { + $this->mockQueue->expects( $this->once() )->method( 'push' ); + + $hooks = new UserHooks( + $this->getServiceContainer()->getMainConfig(), + $this->mockQueue + ); + + $user = $this->getServiceContainer()->getUserFactory()->newFromName( 'OnUserLogoutComplete' ); + + $html = ''; + + $hooks->onUserLogoutComplete( $user, $html, '' ); + } + +} diff --git a/tests/phpunit/PlausibleEventJobTest.php b/tests/phpunit/PlausibleEventJobTest.php new file mode 100644 index 0000000..f03ee8c --- /dev/null +++ b/tests/phpunit/PlausibleEventJobTest.php @@ -0,0 +1,224 @@ +assertInstanceOf( PlausibleEventJob::class, $job ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * @return void + * @throws MWException + */ + public function testCreateFromRequest() { + $session = SessionManager::singleton()->getEmptySession(); + $session->setUser( User::createNew( 'FromRequestUser' ) ); + + $title = Title::newFromText( 'Foo' ); + + $request = new FauxRequest( [], false, $session, 'https' ); + $request->setRequestURL( $title->getLinkURL() ); + + $job = PlausibleEventJob::newFromRequest( $request ); + + $this->assertInstanceOf( PlausibleEventJob::class, $job ); + $this->assertStringEndsWith( '/Foo', $job->params['url'] ); + $this->assertFalse( $job->params['isAnon'] ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * @return void + */ + public function testCreateFromRequestNullJob() { + $job = PlausibleEventJob::newFromRequest( new FauxRequest() ); + + $this->assertInstanceOf( NullJob::class, $job ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * @return void + * @throws MWException + */ + public function testNotRunMissingAgent() { + $session = SessionManager::singleton()->getEmptySession(); + $session->setUser( User::createNew( 'NotRunMissingAgent' ) ); + + $title = Title::newFromText( 'Foo' ); + + $request = new FauxRequest( [], false, $session, 'https' ); + $request->setRequestURL( $title->getLinkURL() ); + + $job = PlausibleEventJob::newFromRequest( $request ); + + $this->assertInstanceOf( PlausibleEventJob::class, $job ); + $this->assertStringEndsWith( '/Foo', $job->params['url'] ); + $this->assertFalse( $job->params['isAnon'] ); + + $this->assertFalse( $job->run() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::run + * @return void + * @throws MWException + * @throws Exception + */ + public function testRun() { + $this->overrideConfigValues( [ + 'PlausibleTrackLoggedIn' => true, + ] ); + + $session = SessionManager::singleton()->getEmptySession(); + $session->setUser( User::createNew( 'Run' ) ); + + $title = Title::newFromText( 'Foo' ); + + $request = new FauxRequest( [], false, $session, 'https' ); + $request->setRequestURL( $title->getLinkURL() ); + $request->setHeader( 'User-Agent', 'Foo-Agent' ); + + $fac = $this->getMockBuilder( HttpRequestFactory::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'create' ] ) + ->getMock(); + + $this->getServiceContainer()->redefineService( 'HttpRequestFactory', function () use ( $fac ) { + return $fac; + } ); + + $req = $this->getMockBuilder( MWHttpRequest::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'setHeader', 'execute' ] ) + ->getMock(); + + $req->expects( $this->once() )->method( 'execute' )->willReturn( Status::newGood() ); + $fac->expects( $this->once() )->method( 'create' )->willReturn( $req ); + + $job = PlausibleEventJob::newFromRequest( $request ); + + $this->assertInstanceOf( PlausibleEventJob::class, $job ); + $this->assertStringEndsWith( '/Foo', $job->params['url'] ); + $this->assertFalse( $job->params['isAnon'] ); + $this->assertTrue( $job->run() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::run + * @return void + * @throws MWException + * @throws Exception + */ + public function testRunAnon() { + $this->overrideConfigValues( [ + 'PlausibleTrackLoggedIn' => false, + ] ); + + $session = SessionManager::singleton()->getEmptySession(); + $session->setUser( User::newFromSession() ); + + $title = Title::newFromText( 'Foo' ); + + $request = new FauxRequest( [], false, $session, 'https' ); + $request->setRequestURL( $title->getLinkURL() ); + $request->setHeader( 'User-Agent', 'Foo-Agent' ); + + $fac = $this->getMockBuilder( HttpRequestFactory::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'create' ] ) + ->getMock(); + + $this->getServiceContainer()->redefineService( 'HttpRequestFactory', function () use ( $fac ) { + return $fac; + } ); + + $req = $this->getMockBuilder( MWHttpRequest::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'setHeader', 'execute' ] ) + ->getMock(); + + $req->expects( $this->once() )->method( 'execute' )->willReturn( Status::newGood() ); + $fac->expects( $this->once() )->method( 'create' )->willReturn( $req ); + + $job = PlausibleEventJob::newFromRequest( $request ); + + $this->assertInstanceOf( PlausibleEventJob::class, $job ); + $this->assertStringEndsWith( '/Foo', $job->params['url'] ); + $this->assertTrue( $job->params['isAnon'] ); + $this->assertTrue( $job->run() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::newFromRequest + * @covers \MediaWiki\Extension\Plausible\PlausibleEventJob::run + * @return void + * @throws MWException + * @throws Exception + */ + public function testNotRunLoggedInUser() { + $this->overrideConfigValues( [ + 'PlausibleTrackLoggedIn' => false, + ] ); + + $session = SessionManager::singleton()->getEmptySession(); + $session->setUser( User::createNew( 'NotRunLoggedInUser' ) ); + + $title = Title::newFromText( 'Foo' ); + + $request = new FauxRequest( [], false, $session, 'https' ); + $request->setRequestURL( $title->getLinkURL() ); + $request->setHeader( 'User-Agent', 'Foo-Agent' ); + + $fac = $this->getMockBuilder( HttpRequestFactory::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'create' ] ) + ->getMock(); + + $this->getServiceContainer()->redefineService( 'HttpRequestFactory', function () use ( $fac ) { + return $fac; + } ); + + $req = $this->getMockBuilder( MWHttpRequest::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'setHeader', 'execute' ] ) + ->getMock(); + + $req->expects( $this->never() )->method( 'execute' ); + $fac->expects( $this->never() )->method( 'create' ); + + $job = PlausibleEventJob::newFromRequest( $request ); + + $this->assertInstanceOf( PlausibleEventJob::class, $job ); + $this->assertStringEndsWith( '/Foo', $job->params['url'] ); + + $this->assertFalse( $job->params['isAnon'] ); + $this->assertTrue( $job->run() ); + } +} diff --git a/tests/phpunit/PlausibleTest.php b/tests/phpunit/PlausibleTest.php new file mode 100644 index 0000000..63bf9d0 --- /dev/null +++ b/tests/phpunit/PlausibleTest.php @@ -0,0 +1,165 @@ +overrideConfigValues( [ + 'PlausibleTrackOutboundLinks' => false, + 'PlausibleTrackFileDownloads' => false, + 'PlausibleTrackLoggedIn' => false, + 'PlausibleEnableTaggedEvents' => false, + ] ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Plausible + * @return void + */ + public function testConstructor() { + $plausible = new Plausible( new OutputPage( RequestContext::getMain() ) ); + $this->assertInstanceOf( Plausible::class, $plausible ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Plausible::addScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptAttribs + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptPath + * @covers \MediaWiki\Extension\Plausible\Plausible::addModules + * @covers \MediaWiki\Extension\Plausible\Plausible::canAdd + * @return void + */ + public function testScriptGeneration() { + $this->overrideConfigValues( [ + 'PlausibleDomain' => 'localhost', + 'PlausibleDomainKey' => 'localwiki', + ] ); + + $page = new OutputPage( RequestContext::getMain() ); + $plausible = new Plausible( $page ); + $plausible->addModules(); + $plausible->addScript(); + + $this->assertArrayHasKey( 'plausible', $page->getHeadItemsArray() ); + $this->assertStringContainsString( 'script.pageview-props.js', $page->getHeadItemsArray()['plausible'] ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Plausible::addScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptAttribs + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptPath + * @covers \MediaWiki\Extension\Plausible\Plausible::addModules + * @covers \MediaWiki\Extension\Plausible\Plausible::canAdd + * @return void + */ + public function testNoScriptGeneration() { + $this->overrideConfigValues( [ + 'PlausibleDomain' => null, + 'PlausibleDomainKey' => null, + ] ); + + $this->expectWarning(); + + $page = new OutputPage( RequestContext::getMain() ); + $plausible = new Plausible( $page ); + $plausible->addModules(); + $plausible->addScript(); + + $this->assertArrayNotHasKey( 'plausible', $page->getHeadItemsArray() ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Plausible::addScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptAttribs + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptPath + * @covers \MediaWiki\Extension\Plausible\Plausible::addModules + * @covers \MediaWiki\Extension\Plausible\Plausible::canAdd + * @return void + */ + public function testCustomScriptGeneration() { + $this->overrideConfigValues( [ + 'PlausibleDomain' => 'localhost', + 'PlausibleDomainKey' => 'localwiki', + 'PlausibleTrackFileDownloads' => true, + ] ); + + $page = new OutputPage( RequestContext::getMain() ); + $plausible = new Plausible( $page ); + $plausible->addModules(); + $plausible->addScript(); + + $this->assertArrayHasKey( 'plausible', $page->getHeadItemsArray() ); + $script = $page->getHeadItemsArray()['plausible']; + + $this->assertStringContainsString( 'script.pageview-props', $script ); + $this->assertStringContainsString( 'file-downloads', $script ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Plausible::addScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptAttribs + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptPath + * @covers \MediaWiki\Extension\Plausible\Plausible::addModules + * @covers \MediaWiki\Extension\Plausible\Plausible::canAdd + * @return void + */ + public function testIgnoredTitles() { + $this->overrideConfigValues( [ + 'PlausibleDomain' => 'localhost', + 'PlausibleDomainKey' => 'localwiki', + 'PlausibleIgnoredTitles' => [ 'Main Page', 'Foo' ], + ] ); + + $page = new OutputPage( RequestContext::getMain() ); + $plausible = new Plausible( $page ); + $plausible->addModules(); + $plausible->addScript(); + + $this->assertArrayHasKey( 'plausible', $page->getHeadItemsArray() ); + $script = $page->getHeadItemsArray()['plausible']; + + $this->assertStringContainsString( 'exclusions', $script ); + $this->assertStringContainsString( 'data-exclude="Main Page, Foo"', $script ); + } + + /** + * @covers \MediaWiki\Extension\Plausible\Plausible::addScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScript + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptAttribs + * @covers \MediaWiki\Extension\Plausible\Plausible::buildScriptPath + * @covers \MediaWiki\Extension\Plausible\Plausible::addModules + * @covers \MediaWiki\Extension\Plausible\Plausible::canAdd + * @return void + */ + public function testAddModule() { + $this->overrideConfigValues( [ + 'PlausibleDomain' => 'localhost', + 'PlausibleDomainKey' => 'localwiki', + 'PlausibleTrack404' => true, + ] ); + + $page = new OutputPage( RequestContext::getMain() ); + $plausible = new Plausible( $page ); + $plausible->addModules(); + $plausible->addScript(); + + $this->assertArrayHasKey( 'plausible', $page->getHeadItemsArray() ); + + $this->assertContainsEquals( 'ext.plausible.scripts.track-404', $page->getModules() ); + } +}