diff --git a/.eslintrc b/.eslintrc index ae03ea59a..8ec3391b1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,30 +1,46 @@ { - "env": { "browser": true }, + "env": { + "browser": true + }, "extends": [ "plugin:@wordpress/eslint-plugin/recommended-with-formatting", "plugin:jest/recommended", - "plugin:@typescript-eslint/recommended" + "plugin:@typescript-eslint/recommended" ], "plugins": [ "@typescript-eslint" ], "settings": { - "jest": { "version": "latest" }, - "import/resolver": { + "jest": { + "version": "latest" + }, + "import/resolver": { "node": { - "extensions": [ ".js", ".jsx", ".ts", ".tsx", ".json" ] + "extensions": [ + ".js", + ".jsx", + ".ts", + ".tsx", + ".json" + ] } - } + } }, - "ignorePatterns": [ "webpack.config.js" ], + "ignorePatterns": [ + "webpack.config.js" + ], "rules": { - "@wordpress/i18n-text-domain": [ "error", { "allowedTextDomain": [ "wp-parsely" ] } ], - "@typescript-eslint/ban-ts-comment": "off", + "@wordpress/i18n-text-domain": [ + "error", + { + "allowedTextDomain": [ + "wp-parsely" + ] + } + ], + "@typescript-eslint/ban-ts-comment": "off", + // Enabling TS rule and disabling Base rule as it can report incorrect errors. "no-shadow": "off", "@typescript-eslint/no-shadow": ["error"] - }, - "globals": { - // Page variable for Puppeteer tests - "page": true } } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6ea85496c..118120aad 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,8 @@ # See https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners -# These owners will be the default owners for everything in the repo. # This team will be requested for review when someone opens a pull request. * @Parsely/wp-parsely + +# Don't assign owners for the following files: +package.json +package-lock.json diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 807219bb0..72a6575d9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,9 +1,12 @@ --- name: Bug report about: Create a report to help us improve - --- + + ## Describe the bug diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 8ffb70296..87662a455 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,9 +1,12 @@ --- name: Feature request about: Suggest an idea for this project - --- + + ## Is your feature request related to a problem? diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e9340b574..1b9c08d18 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,15 +1,18 @@ - + ## Description - + -## Motivation and Context - - +## Motivation and context + + -## How Has This Been Tested? - - - +## How has this been tested? + + + ## Screenshots (if appropriate) + diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index dc7cd15df..6246e2648 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3 - name: Use desired version of NodeJS - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: 16 cache: npm diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 0bb02b70d..f9f99f908 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ matrix.node-version }} cache: npm diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index c68c6a885..6ef0e8fb3 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -15,6 +15,6 @@ jobs: steps: - name: Create a new release draft - uses: release-drafter/release-drafter@v5.21.1 + uses: release-drafter/release-drafter@v5.23.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.husky/pre-commit b/.husky/pre-commit index 812e3ba17..12c96504e 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,3 +3,4 @@ npm run lint composer cs +vendor/bin/phpstan analyse --memory-limit=-1G diff --git a/.wordpress-org/screenshot-4.png b/.wordpress-org/screenshot-4.png index 94432afb4..5ca71aed2 100644 Binary files a/.wordpress-org/screenshot-4.png and b/.wordpress-org/screenshot-4.png differ diff --git a/.wordpress-org/screenshot-5.png b/.wordpress-org/screenshot-5.png index 79f00d70c..e406b183b 100644 Binary files a/.wordpress-org/screenshot-5.png and b/.wordpress-org/screenshot-5.png differ diff --git a/.wordpress-org/screenshot-6.png b/.wordpress-org/screenshot-6.png index 5188a0629..7c2f72aff 100644 Binary files a/.wordpress-org/screenshot-6.png and b/.wordpress-org/screenshot-6.png differ diff --git a/.wordpress-org/screenshot-7.png b/.wordpress-org/screenshot-7.png new file mode 100644 index 000000000..79f00d70c Binary files /dev/null and b/.wordpress-org/screenshot-7.png differ diff --git a/.wordpress-org/screenshot-8.png b/.wordpress-org/screenshot-8.png new file mode 100644 index 000000000..5188a0629 Binary files /dev/null and b/.wordpress-org/screenshot-8.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ebee49f..982a6e047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.7.0](https://github.com/Parsely/wp-parsely/compare/3.6.2...3.7.0) - 2023-02-27 + +### Added + +- Add filters to configure user capability on remote APIs ([#1417](https://github.com/Parsely/wp-parsely/pull/1417)) +- Content Helper: Add edit post icon and make linking behavior consistent across features ([#1346](https://github.com/Parsely/wp-parsely/pull/1346)) +- Content Helper: Add WordPress Dashboard Widget ([#1305](https://github.com/Parsely/wp-parsely/pull/1305)) +- Content Helper: Add Parse.ly Stats List Column ([#1271](https://github.com/Parsely/wp-parsely/pull/1271)) +- Add TypeScript support to all remaining JavaScript files ([#1239](https://github.com/Parsely/wp-parsely/pull/1239)) + +### Changed + +- Show/hide Parse.ly widget and stats column based on user capabilities ([#1407](https://github.com/Parsely/wp-parsely/pull/1407)) +- Content Helper: Update naming ([#1380](https://github.com/Parsely/wp-parsely/pull/1380)) +- UI: Minor wording tweak for Content Helper error ([#1304](https://github.com/Parsely/wp-parsely/pull/1304)) +- UI: Fix grammar in Content Helper error ([#1303](https://github.com/Parsely/wp-parsely/pull/1303)) +- UI: Fix typo in Disable JavaScript option ([#1302](https://github.com/Parsely/wp-parsely/pull/1302)) +- Refactor Content Helper for better structure ([#1288](https://github.com/Parsely/wp-parsely/pull/1288)) +- Centralize dashboard URL generation in a single function ([#1287](https://github.com/Parsely/wp-parsely/pull/1287)) +- Improve Remote APIs naming ([#1272](https://github.com/Parsely/wp-parsely/pull/1272)) +- Rename API Key to Site ID to improve consistency ([#1244](https://github.com/Parsely/wp-parsely/pull/1244)) + +### Fixed + +- Fix referral distribution in Performance Details panel ([#1381](https://github.com/Parsely/wp-parsely/pull/1381)) +- Fix Undefined warnings on Performance Details panel ([#1378](https://github.com/Parsely/wp-parsely/pull/1378)) +- Content Helper: Fix top referrers percentage displaying as "NaN" ([#1374](https://github.com/Parsely/wp-parsely/pull/1374)) +- Fix PHP 8 incompatibilities ([#1362](https://github.com/Parsely/wp-parsely/pull/1362)) +- Fix PHP Notice on Settings page: "Undefined index: title" ([#1342](https://github.com/Parsely/wp-parsely/pull/1342)) +- Content Helper: Make error hint display for all Forbidden (403) errors ([#1336](https://github.com/Parsely/wp-parsely/pull/1336)) +- Fix PHPStan Errors ([#1252](https://github.com/Parsely/wp-parsely/pull/1252)) +- Fix SonarCloud warnings ([#1246](https://github.com/Parsely/wp-parsely/pull/1246)) + +### Dependency Updates + +- The list of all dependency updates for this release is available [here](https://github.com/Parsely/wp-parsely/pulls?q=is%3Apr+is%3Amerged+milestone%3A3.7.0+label%3A%22Component%3A+Dependencies%22). + ## [3.6.2](https://github.com/Parsely/wp-parsely/compare/3.6.1...3.6.2) - 2023-02-13 ### Fixed @@ -13,6 +50,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve checks on proxy endpoints - Fix referral distribution in Performance Details panel ([#1382](https://github.com/Parsely/wp-parsely/pull/1382)) +## [3.5.3](https://github.com/Parsely/wp-parsely/compare/3.5.2...3.5.3) - 2023-02-13 + +### Fixed + +- Improve checks on proxy endpoints + +## [3.4.3](https://github.com/Parsely/wp-parsely/compare/3.4.2...3.4.3) - 2023-02-13 + +### Fixed + +- Improve checks on proxy endpoints + ## [3.6.1](https://github.com/Parsely/wp-parsely/compare/3.6.0...3.6.1) - 2022-12-20 ### Fixed diff --git a/README.md b/README.md index bde154ed8..03596fcd9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Parse.ly -Stable tag: 3.6.2 +Stable tag: 3.7.0 Requires at least: 5.0 Tested up to: 6.1 Requires PHP: 7.1 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: analytics, content marketing, parse.ly, parsely, parsley -Contributors: parsely, hbbtstar, jblz, mikeyarce, GaryJ, parsely_mike, pauargelaguet, acicovic, mehmoodak +Contributors: parsely, hbbtstar, jblz, mikeyarce, GaryJ, parsely_mike, acicovic, mehmoodak The Parse.ly plugin facilitates real-time and historical analytics to your content through a platform designed and built for digital publishing. @@ -25,22 +25,35 @@ Feedback, suggestions, questions or concerns? Open a new [GitHub issue](https:// ### Features -Some notable features of the wp-parsely plugin are: - -- Automatically inserts the Parse.ly metadata and JavaScript in all published pages and posts (supports Custom Post Types). -- [Supports Google Tag Manager, AMP, Facebook Instant Articles, Google Web Stories and Cloudflare](https://docs.parse.ly/plugin-common-questions/#h-is-wp-parsely-compatible-with-amp-facebook-instant-articles-or-google-web-stories). -- Offers the `wpParselyOnLoad` and `wpParselyOnReady` JavaScript hooks that allow advanced integrations requiring JavaScript, such as [Dynamic Tracking](https://docs.parse.ly/plugin-dynamic-tracking/). -- [Supports WordPress Network (Multisite) setups](https://docs.parse.ly/plugin-common-questions/#h-is-wp-parsely-compatible-with-wordpress-network-multisite). -- [Supports decoupled (headless) setups](https://docs.parse.ly/plugin-decoupled-headless-support/). -- Provides a [Recommendations Block](https://docs.parse.ly/recommendations-block/) that shows a list of links related to the currently viewed page. Useful for showcasing related content to visitors. -- Provides a [Content Helper](https://docs.parse.ly/plugin-content-helper/) in the WordPress Editor sidebar that displays the following panels: - - **Performance Details**: Shows performance metrics about the post/page currently being edited. - - **Related Top-Performing Posts**: Provides a list of the website’s most successful posts, similar to the post/page currently being edited. -- Provides a settings page to customize your integration. Some of the options include: - - Output metadata as [JSON-LD](https://docs.parse.ly/metadata-jsonld/) or [repeated meta tags](https://docs.parse.ly/metatags/). - - Choose whether logged-in users should be tracked. - - Define how to track every Post Type (as Post, Non-Post or no tracking). -- Offers a wide range of hooks to customize the plugin's functionality even further. +The wp-parsely plugin is packed with features that allow for a seamless integration process, and brings the power of the Parse.ly dashboard into WordPress. + +#### Automated integration + +The plugin automatically inserts the Parse.ly metadata and JavaScript in all published pages and posts (Custom Post Types are supported). It also provides a settings page to customize your integration, with options including: +- Output Parse.ly metadata as [JSON-LD](https://docs.parse.ly/metadata-jsonld/) or [repeated meta tags](https://docs.parse.ly/metatags/). +- Choose whether logged-in users should be tracked. +- Define how to track every Post Type (as Post, Non-Post or no tracking). + +#### The Parse.ly Content Helper + +The [Content Helper](https://docs.parse.ly/plugin-content-helper/) is a set of content insight tools including: +- The [Parse.ly Dashboard Widget](https://docs.parse.ly/plugin-content-helper/#h-dashboard) - Displays the site's top posts in the last 7 days in the WordPress Dashboard. +- The [Parse.ly Stats Column](https://docs.parse.ly/plugin-content-helper/#h-posts) - Displays published post performance for the last 7 days in Post Lists. +- The [Parse.ly Editor Sidebar](https://docs.parse.ly/plugin-content-helper/#h-editor) - This sidebar is integrated into the WordPress Editor and offers insights about the content currently being edited such as: + - [Performance Details](https://docs.parse.ly/plugin-content-helper/#h-performance-details) - Displays performance metrics about the content currently being edited. + - [Related Top Posts](https://docs.parse.ly/plugin-content-helper/#h-related-top-posts) - Displays a list of the website’s most successful posts, similar to the post/page currently being edited. + +#### The Parse.ly Recommendations Block + +The plugin includes a [Recommendations Block](https://docs.parse.ly/recommendations-block/) that displays a list of posts related to the currently viewed post/page. The Block is useful for showcasing related content to visitors, and it can also be used in Full Site Editing mode or as a [Block-based Widget](https://wordpress.org/documentation/article/block-based-widgets-editor/). + +#### Advanced integrations support + +While the plugin works out of the box for basic integrations, it offers a host of features that easily allow for advanced integration scenarios: +- Support for [Google Tag Manager, AMP, Facebook Instant Articles, Google Web Stories and Cloudflare](https://docs.parse.ly/plugin-common-questions/#h-is-wp-parsely-compatible-with-amp-facebook-instant-articles-or-google-web-stories) is included. +- The plugin exposes the `wpParselyOnLoad` and `wpParselyOnReady` JavaScript hooks that allow for advanced integrations requiring JavaScript, such as [Dynamic Tracking](https://docs.parse.ly/plugin-dynamic-tracking/). +- Support for WordPress [network/multisite](https://docs.parse.ly/plugin-common-questions/#h-is-wp-parsely-compatible-with-wordpress-network-multisite) and [decoupled/headless](https://docs.parse.ly/plugin-decoupled-headless-support/) (GraphQL and WP Rest API) setups is included. +- Last but not least, a wide range of hooks is available in order to customize the plugin's functionality even further. ### Documentation and resources @@ -96,15 +109,19 @@ Please visit the [changelog](https://github.com/parsely/wp-parsely/blob/trunk/CH ## Screenshots -1. Parse.ly plugin main settings for easy setup. For the plugin to start working, only the Site ID is needed. - ![The main settings screen of the wp-parsely plugin](.wordpress-org/screenshot-1.png) -2. Parse.ly plugin settings that require you to submit a website recrawl request whenever you update them. - ![The main settings screen of the wp-parsely plugin](.wordpress-org/screenshot-2.png) -3. Parse.ly plugin advanced settings. To be used only if instructed by Parse.ly staff. - ![The main settings screen of the wp-parsely plugin](.wordpress-org/screenshot-3.png) -4. The Content Helper, featuring the "Performance Details" and "Related Top-Performing Posts" panels. - ![The settings for the Parse.ly Recommended Widget](.wordpress-org/screenshot-4.png) -5. The Recommendations Block. Showcases links to content on your site as provided by the Parse.ly /related API. - ![The settings for the Parse.ly Recommended Widget](.wordpress-org/screenshot-5.png) -6. A view of the Parse.ly Dashboard Overview. Parse.ly offers analytics that empowers you to better understand how your content is performing. - ![The Parsely Dashboard Overview](.wordpress-org/screenshot-6.png) +1. Parse.ly plugin basic settings for easy setup. For the plugin to start working, only the Site ID is needed. + ![Parse.ly Plugin - Basic Settings](.wordpress-org/screenshot-1.png) +2. Parse.ly plugin settings that require a website recrawl whenever they are updated. + ![Parse.ly Plugin - Requires Recrawl Settings](.wordpress-org/screenshot-2.png) +3. Parse.ly plugin advanced settings, to be used only if instructed by Parse.ly staff. + ![Parse.ly Plugin - Advanced Settings](.wordpress-org/screenshot-3.png) +4. The Parse.ly Dashboard Widget, showing the website's top posts in the last 7 days. + ![Parse.ly Dashboard Widget](.wordpress-org/screenshot-4.png) +5. The Parse.ly Stats Column (on the right), showing information about content that is being tracked as Posts. + ![Parse.ly List Column](.wordpress-org/screenshot-5.png) +6. The Parse.ly Editor Sidebar, featuring the Performance Details and Related Top Posts panels. + ![Parse.ly Editor Sidebar](.wordpress-org/screenshot-6.png) +7. The Recommendations Block, showcasing links to related content on your site. + ![Parse.ly Recommendations Block](.wordpress-org/screenshot-7.png) +8. A view of the Parse.ly Dashboard Overview. Parse.ly offers analytics that empower you to better understand how your content is performing. + ![Parse.ly Dashboard Overview](.wordpress-org/screenshot-8.png) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..5dcf94247 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +> Although we strive to create the most secure products possible, we are not perfect. If you happen to find a security vulnerability in one of our services, we would appreciate letting us know and allowing us to respond before disclosing the issue publicly. We take security seriously, and we will try to review and reply to every legitimate security report personally within 24 hours. +> +> – [Automattic](https://automattic.com/security/) + +## Supported Versions + +We fully support one minor release behind the latest (i.e. if 3.6 is the latest release, we will support 3.5). + +Depending on the severity of the vulnerability, we may port and release fixes for older, unsupported versions as well. + +## Reporting a Vulnerability + +For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit security issuess via the HackerOne portal: https://hackerone.com/automattic diff --git a/bin/uvn.sh b/bin/uvn.sh new file mode 100755 index 000000000..7cdcb6418 --- /dev/null +++ b/bin/uvn.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Script to update version numbers (uvn) in the code. It will create a new +# branch and commit the changes. After inspecting the results, it can be pushed +# to GitHub as a PR. +# +# Usage: Specify the new version as an argument. (e.g. bin/uvn.sh 3.7.0) +# Note: This has only been tested with macOS sed. + +git checkout -b update/wp-parsely-version-to-$1 + +sed -i '' "s/Stable tag: .* $/Stable tag: $1 /" README.md +sed -i '' "s/\"version\": \".*\"/\"version\": \"$1\"/" package.json +sed -i '' "s/export const PLUGIN_VERSION = '.*'/export const PLUGIN_VERSION = '$1'/" tests/e2e/utils.ts +sed -i '' "s/ \* Version: .*$/ \* Version: $1/" wp-parsely.php +sed -i '' "s/const PARSELY_VERSION = '.*'/const PARSELY_VERSION = '$1'/" wp-parsely.php + +npm install # Update version numbers in package.lock.json. + +git add -A && git commit -m "Update wp-parsely version to $1" diff --git a/build/admin-parsely-stats.asset.php b/build/admin-parsely-stats.asset.php new file mode 100644 index 000000000..eb26d4757 --- /dev/null +++ b/build/admin-parsely-stats.asset.php @@ -0,0 +1 @@ + array(), 'version' => '073b1605887b0f95d4fb'); diff --git a/build/admin-parsely-stats.css b/build/admin-parsely-stats.css new file mode 100644 index 000000000..f253a4177 --- /dev/null +++ b/build/admin-parsely-stats.css @@ -0,0 +1 @@ +.column-parsely-stats{width:200px}@media only screen and (max-width:991px){.column-parsely-stats{width:150px}}.column-parsely-stats .parsely-post-stats{color:#959da5;line-height:18px;min-height:54px}.column-parsely-stats .parsely-post-stats-placeholder{letter-spacing:2px}.column-parsely-stats .parsely-post-page-views{color:#000} diff --git a/build/admin-parsely-stats.js b/build/admin-parsely-stats.js new file mode 100644 index 000000000..535ac4140 --- /dev/null +++ b/build/admin-parsely-stats.js @@ -0,0 +1 @@ +!function(){"use strict";!function(){function s(){return document.querySelectorAll(".parsely-post-stats")}document.addEventListener("DOMContentLoaded",(function(){!function(){if(null===(n=s())||void 0===n||n.forEach((function(s){s.innerHTML="—"})),window.wpParselyPostsStatsResponse){var n,e,t,r,a=JSON.parse(window.wpParselyPostsStatsResponse);if(null==a?void 0:a.error)return e=a.error,void(null!==(r=document.querySelector(".wp-header-end"))&&(r.innerHTML+=(void 0===(t=e.htmlMessage)&&(t=""),'
'.concat(t,"
"))));(null==a?void 0:a.data)&&function(n){var e;n&&(null===(e=s())||void 0===e||e.forEach((function(s){var e=s.getAttribute("data-stats-key");if(null!==e&&void 0!==n[e]){var t=n[e];s.innerHTML="",t.page_views&&(s.innerHTML+=''.concat(t.page_views,"
")),t.visitors&&(s.innerHTML+=''.concat(t.visitors,"
")),t.avg_time&&(s.innerHTML+=''.concat(t.avg_time,"
"))}})))}(a.data)}}()}))}()}(); \ No newline at end of file diff --git a/build/admin-settings.asset.php b/build/admin-settings.asset.php index 3eaca6c85..fdcd69ccc 100644 --- a/build/admin-settings.asset.php +++ b/build/admin-settings.asset.php @@ -1 +1 @@ - array(), 'version' => 'b9bff574d1250ea8ca88'); + array(), 'version' => 'd7e63a5d750fd6858259'); diff --git a/build/admin-settings.js b/build/admin-settings.js index f548bb4da..19577edb0 100644 --- a/build/admin-settings.js +++ b/build/admin-settings.js @@ -1 +1 @@ -document.querySelector(".media-single-image button.browse").addEventListener("click",(function(){const e=this.dataset.option,t=wp.media({multiple:!1,library:{type:"image"}});t.on("select",(function(){const i=t.state().get("selection").first().toJSON().url,n="#media-single-image-"+e+" input.file-path";document.querySelector(n).value=i})),t.open()})); \ No newline at end of file +!function(){"use strict";var e;null===(e=document.querySelector(".media-single-image button.browse"))||void 0===e||e.addEventListener("click",(function(e){var t=e.target.dataset.option,i=window.wp.media({multiple:!1,library:{type:"image"}});i.on("select",(function(){var e=i.state().get("selection").first().toJSON().url,n="#media-single-image-"+t+" input.file-path",a=document.querySelector(n);a&&(a.value=e)})),i.open()}))}(); \ No newline at end of file diff --git a/build/blocks/recommendations/edit.asset.php b/build/blocks/recommendations/edit.asset.php index 9ebe93668..bb4436554 100644 --- a/build/blocks/recommendations/edit.asset.php +++ b/build/blocks/recommendations/edit.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '687105d10db0d8940eb9'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '8dfa7621a1f8d9e50570'); diff --git a/build/blocks/recommendations/edit.js b/build/blocks/recommendations/edit.js index 75c62572a..8cfab92e5 100644 --- a/build/blocks/recommendations/edit.js +++ b/build/blocks/recommendations/edit.js @@ -1 +1 @@ -!function(){"use strict";var e,r={766:function(e,r,n){var t=n(893),a=window.wp.i18n,o=window.wp.blocks,i=window.wp.blockEditor,l=window.wp.apiFetch,s=n.n(l),c=window.wp.compose,u=window.wp.element,p=window.wp.url,d="RECOMMENDATIONS_BLOCK_ERROR",f="RECOMMENDATIONS_BLOCK_RECOMMENDATIONS";function w(){return w=Object.assign||function(e){for(var r=1;r{switch(r.type){case d:return{...e,isLoaded:!0,error:r.error,recommendations:void 0};case"RECOMMENDATIONS_BLOCK_LOADED":return{...e,isLoaded:!0};case f:{const{recommendations:n}=r;if(!Array.isArray(n))return{...e,recommendations:void 0};const t=n.map((e=>{let{title:r,url:n,image_url:t,thumb_url_medium:a}=e;return{title:r,url:n,image_url:t,thumb_url_medium:a}}));return{...e,isLoaded:!0,error:void 0,recommendations:t}}default:return{...e}}},v=()=>(0,u.useContext)(y);var b=e=>{var r,n;const t={isLoaded:!1,recommendations:void 0,uuid:null===(r=window.PARSELY)||void 0===r||null===(n=r.config)||void 0===n?void 0:n.uuid,clientId:e.clientId},[a,o]=(0,u.useReducer)(m,t);return(0,u.createElement)(y.Provider,w({value:{state:a,dispatch:o}},e))},g=function(){return g=Object.assign||function(e){for(var r,n=1,t=arguments.length;n0&&a[a.length-1])||6!==l[0]&&2!==l[0])){i=0;continue}if(3===l[0]&&(!a||l[1]>a[0]&&l[1]=o)&&Object.keys(t.O).every((function(e){return t.O[e](n[s])}))?n.splice(s--,1):(l=!1,o0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,a,o]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},function(){var e={878:0,570:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var a,o,i=n[0],l=n[1],s=n[2],c=0;if(i.some((function(r){return 0!==e[r]}))){for(a in l)t.o(l,a)&&(t.m[a]=l[a]);if(s)var u=s(t)}for(r&&r(n);c0&&a[a.length-1])||6!==i[0]&&2!==i[0])){l=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=o)&&Object.keys(t.O).every((function(e){return t.O[e](n[s])}))?n.splice(s--,1):(i=!1,o0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,a,o]},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,{a:r}),r},t.d=function(e,r){for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},function(){var e={878:0,570:0};t.O.j=function(r){return 0===e[r]};var r=function(r,n){var a,o,l=n[0],i=n[1],s=n[2],c=0;if(l.some((function(r){return 0!==e[r]}))){for(a in i)t.o(i,a)&&(t.m[a]=i[a]);if(s)var u=s(t)}for(r&&r(n);c array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '76a903436f9d320c5418'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'a9b6083e743164b93926'); diff --git a/build/blocks/recommendations/view.js b/build/blocks/recommendations/view.js index 58e7c8bbd..315b7f395 100644 --- a/build/blocks/recommendations/view.js +++ b/build/blocks/recommendations/view.js @@ -1 +1 @@ -!function(){"use strict";var e={418:function(e){var r=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,t=Object.prototype.propertyIsEnumerable;function o(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var r={},n=0;n<10;n++)r["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(r).map((function(e){return r[e]})).join(""))return!1;var t={};return"abcdefghijklmnopqrst".split("").forEach((function(e){t[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},t)).join("")}catch(e){return!1}}()?Object.assign:function(e,a){for(var i,s,l=o(e),c=1;c{switch(r.type){case p:return{...e,isLoaded:!0,error:r.error,recommendations:void 0};case"RECOMMENDATIONS_BLOCK_LOADED":return{...e,isLoaded:!0};case d:{const{recommendations:n}=r;if(!Array.isArray(n))return{...e,recommendations:void 0};const t=n.map((e=>{let{title:r,url:n,image_url:t,thumb_url_medium:o}=e;return{title:r,url:n,image_url:t,thumb_url_medium:o}}));return{...e,isLoaded:!0,error:void 0,recommendations:t}}default:return{...e}}},h=()=>(0,t.useContext)(m);var b=e=>{var r,n;const o={isLoaded:!1,recommendations:void 0,uuid:null===(r=window.PARSELY)||void 0===r||null===(n=r.config)||void 0===n?void 0:n.uuid,clientId:e.clientId},[a,i]=(0,t.useReducer)(y,o);return(0,t.createElement)(m.Provider,f({value:{state:a,dispatch:i}},e))},v=function(){return v=Object.assign||function(e){for(var r,n=1,t=arguments.length;n0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1] array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-edit-post', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => '9191b48f317cc940ffdb'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-edit-post', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => '2a4869c75e9583652c32'); diff --git a/build/content-helper.css b/build/content-helper.css index 5faa27817..7f37ee2c7 100644 --- a/build/content-helper.css +++ b/build/content-helper.css @@ -1 +1 @@ -.wp-parsely-content-helper{--base-font:"source-sans-pro",arial,sans-serif;--numeric-font:"ff-din-round-web",sans-serif;--gray-400:#d7dbdf;--gray-600:#586069;--gray-700:#444d56;--blue-500:#44a8e5;--green-500:#7bc01b;--ref-direct:205,13%,52%;--ref-internal:161,91%,41%;--ref-social:210,72%,41%;--ref-search:42,100%,50%;--ref-other:3,76%,58%;--base-text-2:var(--gray-600);--base-3:var(--gray-400);--border:var(--gray-400);--data:var(--green-500);--control:var(--blue-500);--font-size--large:1rem;--font-size--extra-large:1.2rem;--sidebar-black:#1e1e1e}.parsely-spinner-wrapper{display:flex;justify-content:center;margin-top:2.5rem}.parsely-spinner-wrapper svg{height:22px;width:22px}.parsely-top-posts{list-style-type:none;margin:1.375rem 0 0}.wp-parsely-content-helper p.parsely-error-hint,.wp-parsely-content-helper p.parsely-top-posts-descr{color:#444d56}.wp-parsely-content-helper p.parsely-top-posts-descr{margin-top:.9375rem}.parsely-top-post{border-top:1px solid #edeeef;margin-bottom:.3125rem;padding:.625rem 0}.parsely-top-post-title a{line-height:16px;text-decoration:none}.parsely-top-post-stats-link{color:#000;font-size:.875rem;margin-right:.4375rem}.parsely-top-post-stats-link:hover{color:#2596db}.parsely-top-post-link{display:inline-block;height:16px;position:relative;width:16px}.parsely-top-post-link svg{fill:#8d98a1;position:absolute;top:2px}.wp-parsely-content-helper p.parsely-top-post-info{align-items:center;display:flex;justify-content:space-between;margin:.3125rem 0 0}.wp-parsely-content-helper p.parsely-top-post-info>span{color:#586069;display:flex;margin-bottom:0}.wp-parsely-content-helper p.parsely-top-post-info>span:not(:first-child){margin-left:.3125rem}.parsely-top-post-views svg{fill:#586069;margin-right:.1875rem;position:relative;top:2px}.parsely-top-post-link:hover svg{fill:#2596db}.parsely-contact-us{margin-top:.9375rem!important}div.wp-parsely-content-helper div.current-post-details-panel div.section{font-family:var(--base-font);margin-top:1.8rem}div.wp-parsely-content-helper div.current-post-details-panel div.section table{border-collapse:collapse;width:100%}div.wp-parsely-content-helper div.current-post-details-panel div.section table th{font-weight:400;text-align:left}div.wp-parsely-content-helper div.current-post-details-panel div.section div.section-title{color:var(--base-text-2);margin-bottom:.5rem}div.wp-parsely-content-helper div.current-post-details-panel div.section.period{margin-top:.8rem}div.wp-parsely-content-helper div.current-post-details-panel div.section.period span{color:var(--base-text-2)}div.wp-parsely-content-helper div.current-post-details-panel div.section.general-performance table tbody tr{font-family:var(--numeric-font);font-size:var(--font-size--extra-large);font-weight:500}div.wp-parsely-content-helper div.current-post-details-panel div.section.general-performance table tfoot tr{color:var(--gray-700);height:1.4rem;vertical-align:bottom}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types div.multi-percentage-bar{--radius:2px;display:flex;height:.5rem}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill:first-child{border-radius:var(--radius) 0 0 var(--radius)}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill:last-child{border-radius:0 var(--radius) var(--radius) 0}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.direct{background-color:hsl(var(--ref-direct))}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.internal{background-color:hsl(var(--ref-internal))}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.search{background-color:hsl(var(--ref-search))}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.social{background-color:hsl(var(--ref-social))}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.other{background-color:hsl(var(--ref-other))}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types table{margin-top:.5rem}div.wp-parsely-content-helper div.current-post-details-panel div.section.referrer-types table tbody tr{font-family:var(--numeric-font);font-size:var(--font-size--large);height:1.4rem;vertical-align:bottom}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers table thead tr{color:var(--base-text-2);height:1.6rem;vertical-align:top}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers table thead tr th:last-child{text-align:right}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers table tbody tr{border:1px solid var(--border);border-left:0;border-right:0;height:2rem}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers table tbody tr th:first-child{--width:8rem;max-width:var(--width);min-width:var(--width);overflow:hidden;padding-right:1rem;text-overflow:ellipsis;white-space:nowrap}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers table tbody tr td:nth-child(2){width:100%}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers table tbody tr td:last-child{padding-left:1rem;text-align:right}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers table tbody div.percentage-bar{--radius:4px;background-color:var(--base-3);border-radius:var(--radius);display:flex;height:.4rem;margin:0;overflow:hidden}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers table tbody div.percentage-bar:after{background-color:var(--data);border-radius:var(--radius);content:"";height:100%;width:var(--bar-fill)}div.wp-parsely-content-helper div.current-post-details-panel div.section.top-referrers div:last-child{color:var(--base-text-2);margin-top:.6rem}div.wp-parsely-content-helper div.current-post-details-panel div.section.actions{display:inline-flex;justify-content:space-between;width:100%}div.wp-parsely-content-helper div.current-post-details-panel div.section.actions a.components-button{border-radius:4px;text-transform:uppercase}div.wp-parsely-content-helper div.current-post-details-panel div.section.actions a.components-button.is-secondary{box-shadow:inset 0 0 0 1px var(--border);color:var(--sidebar-black)}div.wp-parsely-content-helper div.current-post-details-panel div.section.actions a.components-button.is-primary{background-color:var(--control)} +#wp-parsely-dashboard-widget,.wp-parsely-content-helper{--base-font:"source-sans-pro",arial,sans-serif;--numeric-font:"ff-din-round-web",sans-serif;--gray-300:#edeeef;--gray-400:#d7dbdf;--gray-500:#959da5;--gray-600:#586069;--gray-700:#444d56;--gray-900:#24292e;--blue-500:#44a8e5;--blue-550:#2596db;--green-500:#7bc01b;--ref-direct:205,13%,52%;--ref-internal:161,91%,41%;--ref-social:210,72%,41%;--ref-search:42,100%,50%;--ref-other:3,76%,58%;--base-text:var(--gray-900);--base-text-2:var(--gray-600);--base-3:var(--gray-400);--border:var(--gray-400);--data:var(--green-500);--control:var(--blue-500);--font-size--large:1rem;--font-size--extra-large:1.2rem;--black:#000;--sidebar-black:#1e1e1e}.wp-parsely-content-helper .parsely-spinner-wrapper{display:flex;justify-content:center;margin:2.5rem 0}.wp-parsely-content-helper .parsely-spinner-wrapper svg{height:22px;width:22px}.wp-parsely-content-helper .parsely-contact-us{margin-top:.9375rem!important}.wp-parsely-content-helper p.parsely-error-hint{color:var(--gray-700)}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-posts{list-style-type:none;margin:1.375rem 0 0}.wp-parsely-content-helper .parsely-top-posts-wrapper p.parsely-top-posts-descr{color:var(--gray-700);margin-top:.9375rem}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post{border-top:1px solid var(--gray-300);margin-bottom:.3125rem;padding:.625rem 0}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-title a{line-height:16px;text-decoration:none}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-stats-link{color:var(--black);font-size:.875rem;margin-right:.4375rem}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-stats-link:hover{color:var(--blue-550)}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-views svg{fill:var(--gray-600);margin-right:.1875rem;position:relative;top:2px}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-edit-link,.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-view-link{display:inline-block;height:16px;margin-right:.1875rem;position:relative;width:16px}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-edit-link svg,.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-view-link svg{fill:#8d98a1;position:absolute;top:2px}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-edit-link:hover svg,.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-view-link:hover svg{fill:var(--blue-550)}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-info{align-items:center;display:flex;justify-content:space-between;margin:.3125rem 0 0}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-info>span{color:var(--gray-600);display:flex;margin-bottom:0}.wp-parsely-content-helper .parsely-top-posts-wrapper .parsely-top-post-info>span:not(:first-child){margin-left:.3125rem}.wp-parsely-content-helper .performance-details-panel div.section{font-family:var(--base-font);margin-top:1.8rem}.wp-parsely-content-helper .performance-details-panel div.section table{border-collapse:collapse;width:100%}.wp-parsely-content-helper .performance-details-panel div.section table th{font-weight:400;text-align:left}.wp-parsely-content-helper .performance-details-panel div.section div.section-title{color:var(--base-text-2);margin-bottom:.5rem}.wp-parsely-content-helper .performance-details-panel div.section.period{margin-top:.8rem}.wp-parsely-content-helper .performance-details-panel div.section.period span{color:var(--base-text-2)}.wp-parsely-content-helper .performance-details-panel div.section.general-performance table tbody tr{font-family:var(--numeric-font);font-size:var(--font-size--extra-large);font-weight:500}.wp-parsely-content-helper .performance-details-panel div.section.general-performance table tfoot tr{color:var(--gray-700);height:1.4rem;vertical-align:bottom}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types div.multi-percentage-bar{--radius:2px;display:flex;height:.5rem}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill:first-child{border-radius:var(--radius) 0 0 var(--radius)}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill:last-child{border-radius:0 var(--radius) var(--radius) 0}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.direct{background-color:hsl(var(--ref-direct))}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.internal{background-color:hsl(var(--ref-internal))}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.search{background-color:hsl(var(--ref-search))}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.social{background-color:hsl(var(--ref-social))}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types div.multi-percentage-bar .bar-fill.other{background-color:hsl(var(--ref-other))}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types table{margin-top:.5rem}.wp-parsely-content-helper .performance-details-panel div.section.referrer-types table tbody tr{font-family:var(--numeric-font);font-size:var(--font-size--large);height:1.4rem;vertical-align:bottom}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers table thead tr{color:var(--base-text-2);height:1.6rem;vertical-align:top}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers table thead tr th:last-child{text-align:right}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers table tbody tr{border:1px solid var(--border);border-left:0;border-right:0;height:2rem}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers table tbody tr th:first-child{--width:8rem;max-width:var(--width);min-width:var(--width);overflow:hidden;padding-right:1rem;text-overflow:ellipsis;white-space:nowrap}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers table tbody tr td:nth-child(2){width:100%}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers table tbody tr td:last-child{padding-left:1rem;text-align:right}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers table tbody div.percentage-bar{--radius:4px;background-color:var(--base-3);border-radius:var(--radius);display:flex;height:.4rem;margin:0;overflow:hidden}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers table tbody div.percentage-bar:after{background-color:var(--data);border-radius:var(--radius);content:"";height:100%;width:var(--bar-fill)}.wp-parsely-content-helper .performance-details-panel div.section.top-referrers div:last-child{color:var(--base-text-2);margin-top:.6rem}.wp-parsely-content-helper .performance-details-panel div.section.actions{display:inline-flex;justify-content:space-between;width:100%}.wp-parsely-content-helper .performance-details-panel div.section.actions a.components-button{border-radius:4px;text-transform:uppercase}.wp-parsely-content-helper .performance-details-panel div.section.actions a.components-button.is-secondary{box-shadow:inset 0 0 0 1px var(--border);color:var(--sidebar-black)}.wp-parsely-content-helper .performance-details-panel div.section.actions a.components-button.is-primary{background-color:var(--control)} diff --git a/build/content-helper.js b/build/content-helper.js index 42e82fa2e..75df4db41 100644 --- a/build/content-helper.js +++ b/build/content-helper.js @@ -1,20 +1,20 @@ -!function(){"use strict";var e={418:function(e){var t=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable;function s(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},r=0;r<10;r++)t["_"+String.fromCharCode(r)]=r;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var n={};return"abcdefghijklmnopqrst".split("").forEach((function(e){n[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},n)).join("")}catch(e){return!1}}()?Object.assign:function(e,a){for(var o,i,c=s(e),l=1;l0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]1?[2,Promise.reject(new y((0,s.sprintf)( +(0,s.__)("The post %s has 0 views, or the Parse.ly API returned no data.","wp-parsely"),e),t.ParselyApiReturnedNoData,""))]:r.data.length>1?[2,Promise.reject(new y((0,s.sprintf)( /* translators: URL of the published post */ -(0,s.__)("Multiple results were returned for the post %s by the Parse.ly API.","wp-parsely"),e),t.ParselyApiReturnedTooManyResults))]:[2,r.data[0]]}}))}))},e.prototype.fetchReferrerDataFromWpEndpoint=function(e,r){return v(this,void 0,void 0,(function(){var n,s;return b(this,(function(a){switch(a.label){case 0:return a.trys.push([0,2,,3]),[4,d()({path:(0,u.addQueryArgs)("/wp-parsely/v1/referrers/post/detail",{url:e,period_start:this.dataPeriodStart,period_end:this.dataPeriodEnd,total_views:r})})];case 1:return n=a.sent(),[3,3];case 2:return s=a.sent(),[2,Promise.reject(new y(s.message,s.code))];case 3:return(null==n?void 0:n.error)?[2,Promise.reject(new y(n.error.message,t.ParselyApiResponseContainsError))]:[2,n.data]}}))}))},e.prototype.setDataPeriod=function(e){this.dataPeriodDays=e,this.dataPeriodEnd=this.convertDateToString(new Date)+"T23:59",this.dataPeriodStart=this.removeDaysFromDate(this.dataPeriodEnd,this.dataPeriodDays-1)+"T00:00"},e.prototype.removeDaysFromDate=function(e,t){var r=new Date(e);return r.setDate(r.getDate()-t),this.convertDateToString(r)},e.prototype.convertDateToString=function(e){return e.toISOString().substring(0,10)},e}(),g=function(){return g=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]=l){var p=t;(s=n/l)%1>1/i&&(p=s>10?1:2),p=parseFloat(s.toFixed(2))===parseFloat(s.toFixed(0))?0:p,a=s.toFixed(p),o=c}i=l})),a+r+o}var x=function(){return x=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]=l){var u=t;(s=n/l)%1>1/i&&(u=s>10?1:2),u=parseFloat(s.toFixed(2))===parseFloat(s.toFixed(0))?0:u,a=s.toFixed(u),o=c}i=l})),a+r+o}var D=function(){var e=this,t=(0,c.useState)(!0),r=t[0],s=t[1],o=(0,c.useState)(null),i=o[0],l=o[1],u=(0,c.useState)(null),p=u[0],d=u[1],f=new m;return(0,c.useEffect)((function(){var t=function(r){return j(e,void 0,void 0,(function(){var e=this;return _(this,(function(n){return f.getCurrentPostDetails().then((function(e){d(e),s(!1)})).catch((function(n){return j(e,void 0,void 0,(function(){return _(this,(function(e){switch(e.label){case 0:return r>0?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return e.sent(),[4,t(r-1)];case 2:return e.sent(),[3,4];case 3:l(n),s(!1),e.label=4;case 4:return[2]}}))}))})),[2]}))}))};s(!0),t(3)}),[]),i?i.ProcessedMessage():r?(0,n.jsx)(a.Spinner,{}):(0,n.jsx)(x,{data:p})},k=function(e,t,r,n){return new(r||(r=Promise))((function(s,a){function o(e){try{c(n.next(e))}catch(e){a(e)}}function i(e){try{c(n.throw(e))}catch(e){a(e)}}function c(e){var t;e.done?s(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(o,i)}c((n=n.apply(e,t||[])).next())}))},A=function(e,t){var r,n,s,a,o={label:0,sent:function(){if(1&s[0])throw s[1];return s[1]},trys:[],ops:[]};return a={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function i(i){return function(c){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(s=2&i[0]?n.return:i[0]?n.throw||((s=n.return)&&s.call(n),0):n.next)&&!(s=s.call(n,i[1])).done)return s;switch(n=0,s&&(i=[2&i[0],s.value]),i[0]){case 0:case 1:s=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((s=(s=o.trys).length>0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]0?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return e.sent(),[4,t(r-1)];case 2:return e.sent(),[3,4];case 3:l(n),s(!1),e.label=4;case 4:return[2]}}))}))})),[2]}))}))};s(!0),t(3)}),[]),i?i.ProcessedMessage():r?(0,n.jsx)("div",x({className:"parsely-spinner-wrapper","data-testid":"parsely-spinner-wrapper"},{children:(0,n.jsx)(a.Spinner,{})})):(0,n.jsx)(S,{data:u})},D=function(e,t,r,n){return new(r||(r=Promise))((function(s,a){function o(e){try{c(n.next(e))}catch(e){a(e)}}function i(e){try{c(n.throw(e))}catch(e){a(e)}}function c(e){var t;e.done?s(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(o,i)}c((n=n.apply(e,t||[])).next())}))},R=function(e,t){var r,n,s,a,o={label:0,sent:function(){if(1&s[0])throw s[1];return s[1]},trys:[],ops:[]};return a={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function i(i){return function(c){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(s=2&i[0]?n.return:i[0]?n.throw||((s=n.return)&&s.call(n),0):n.next)&&!(s=s.call(n,i[1])).done)return s;switch(n=0,s&&(i=[2&i[0],s.value]),i[0]){case 0:case 1:s=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((s=(s=o.trys).length>0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]0?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return e.sent(),[4,t(r-1)];case 2:return e.sent(),[3,4];case 3:s(!1),l(n),e.label=4;case 4:return[2]}}))}))})),[2]}))}))};return s(!0),t(3),function(){s(!1),y([]),d(""),l(null)}}),[]),i)return i.ProcessedMessage("parsely-top-posts-descr");var w=(0,n.jsx)("ol",$({className:"parsely-top-posts"},{children:h.map((function(e){return(0,n.jsx)(H,{post:e},e.id)}))}));return r?(0,n.jsx)("div",$({className:"parsely-spinner-wrapper","data-testid":"parsely-spinner-wrapper"},{children:(0,n.jsx)(a.Spinner,{})})):(0,n.jsxs)("div",$({className:"parsely-top-posts-wrapper"},{children:[(0,n.jsx)("p",$({className:"parsely-top-posts-descr","data-testid":"parsely-top-posts-descr"},{children:p})),w]}))},G=function(){return G=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]0?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return e.sent(),[4,t(r-1)];case 2:return e.sent(),[3,4];case 3:s(!1),l(n),e.label=4;case 4:return[2]}}))}))})),[2]}))}))};return s(!0),t(3),function(){s(!1),y([]),d(""),l(void 0)}}),[]),i)return i.ProcessedMessage("parsely-top-posts-descr");var v=(0,n.jsx)("ol",G({className:"parsely-top-posts"},{children:f.map((function(e){return(0,n.jsx)(q,{post:e},e.id)}))}));return r?(0,n.jsx)("div",G({className:"parsely-spinner-wrapper","data-testid":"parsely-spinner-wrapper"},{children:(0,n.jsx)(a.Spinner,{})})):(0,n.jsxs)("div",G({className:"parsely-top-posts-wrapper"},{children:[(0,n.jsx)("p",G({className:"parsely-top-posts-descr","data-testid":"parsely-top-posts-descr"},{children:u})),v]}))},Z=function(){return Z=Object.assign||function(e){for(var t,r=1,n=arguments.length;r array('react', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'fa8ad5a549362faff65a'); diff --git a/build/content-helper/dashboard-widget.css b/build/content-helper/dashboard-widget.css new file mode 100644 index 000000000..257e07ed8 --- /dev/null +++ b/build/content-helper/dashboard-widget.css @@ -0,0 +1 @@ +#wp-parsely-dashboard-widget,.wp-parsely-content-helper{--base-font:"source-sans-pro",arial,sans-serif;--numeric-font:"ff-din-round-web",sans-serif;--gray-300:#edeeef;--gray-400:#d7dbdf;--gray-500:#959da5;--gray-600:#586069;--gray-700:#444d56;--gray-900:#24292e;--blue-500:#44a8e5;--blue-550:#2596db;--green-500:#7bc01b;--ref-direct:205,13%,52%;--ref-internal:161,91%,41%;--ref-social:210,72%,41%;--ref-search:42,100%,50%;--ref-other:3,76%,58%;--base-text:var(--gray-900);--base-text-2:var(--gray-600);--base-3:var(--gray-400);--border:var(--gray-400);--data:var(--green-500);--control:var(--blue-500);--font-size--large:1rem;--font-size--extra-large:1.2rem;--black:#000;--sidebar-black:#1e1e1e}#wp-parsely-dashboard-widget .parsely-spinner-wrapper{display:flex;justify-content:center;margin:6.4375rem 0}#wp-parsely-dashboard-widget .parsely-spinner-wrapper svg{height:22px;width:22px}#wp-parsely-dashboard-widget .parsely-contact-us{margin-top:.9375rem!important}#wp-parsely-dashboard-widget p.parsely-error-hint{color:var(--gray-700)}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper{color:var(--base-text);font-family:var(--base-font)}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .page-views-title{margin-bottom:.25rem;text-align:right;width:100%}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-content{display:flex}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-content:before{content:counter(item) "";counter-increment:item;padding-right:.5rem}@media only screen and (max-width:380px){#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-content:before{content:"";padding-right:0}}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-posts{counter-reset:item;list-style:none;margin:0}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post{margin-bottom:1rem}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-thumbnail{height:46px;width:46px}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-thumbnail img{height:100%;width:100%}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-data{border-top:1px solid var(--gray-300);flex-grow:1;margin-left:.5rem;padding-top:.25rem}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-title{color:var(--base-text);font-size:.875rem;margin-right:.4375rem}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper a.parsely-top-post-title:hover{color:var(--blue-550)}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-icon-link{position:relative;top:.25rem}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-icon-link svg{fill:#8d98a1;margin-right:.1875rem}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-icon-link svg:hover{fill:var(--blue-550)}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-metadata{margin:.25rem 0 0}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-metadata>span{color:var(--gray-500)}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-metadata>span:not(:first-child){margin-left:.75rem}#wp-parsely-dashboard-widget .parsely-top-posts-wrapper .parsely-top-post-views{float:right;font-family:var(--numeric-font);font-size:1.125rem;padding-left:.625rem} diff --git a/build/content-helper/dashboard-widget.js b/build/content-helper/dashboard-widget.js new file mode 100644 index 000000000..cd325e113 --- /dev/null +++ b/build/content-helper/dashboard-widget.js @@ -0,0 +1 @@ +!function(){"use strict";var e={418:function(e){var t=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable;function s(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},r=0;r<10;r++)t["_"+String.fromCharCode(r)]=r;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var n={};return"abcdefghijklmnopqrst".split("").forEach((function(e){n[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},n)).join("")}catch(e){return!1}}()?Object.assign:function(e,a){for(var o,i,c=s(e),l=1;l0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]=l){var p=t;(s=n/l)%1>1/i&&(p=s>10?1:2),p=parseFloat(s.toFixed(2))===parseFloat(s.toFixed(0))?0:p,a=s.toFixed(p),o=c}i=l})),a+r+o}var _=function(){return _=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&s[s.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!s||i[1]>s[0]&&i[1]0?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return e.sent(),[4,t(r-1)];case 2:return e.sent(),[3,4];case 3:i(!1),p(n),e.label=4;case 4:return[2]}}))}))})),[2]}))}))};return i(!0),t(3),function(){i(!1),f([]),p(void 0)}}),[]),l)return l.ProcessedMessage("parsely-top-posts-descr");var w=(0,n.jsx)("ol",C({className:"parsely-top-posts"},{children:d.map((function(e){return(0,n.jsx)(N,{post:e},e.id)}))}));return r?(0,n.jsx)("div",C({className:"parsely-spinner-wrapper"},{children:(0,n.jsx)(o.Spinner,{})})):(0,n.jsxs)("div",C({className:"parsely-top-posts-wrapper"},{children:[(0,n.jsx)("div",C({className:"page-views-title"},{children:(0,a.__)("Page Views","wp-parsely")})),w]}))};window.addEventListener("load",(function(){(0,s.render)((0,n.jsx)(T,{}),document.querySelector("#wp-parsely-dashboard-widget > .inside"))}),!1)}()}(); \ No newline at end of file diff --git a/build/loader.asset.php b/build/loader.asset.php index 09a8fe419..6ef99464c 100644 --- a/build/loader.asset.php +++ b/build/loader.asset.php @@ -1 +1 @@ - array('wp-hooks'), 'version' => '11d36be1108ae9f257cd'); + array('wp-hooks'), 'version' => '1d54726e91ce976b3e82'); diff --git a/build/loader.js b/build/loader.js index 2320b0666..753c5d95e 100644 --- a/build/loader.js +++ b/build/loader.js @@ -1 +1 @@ -!function(){var o={};o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(o){if("object"==typeof window)return window}}(),function(){"use strict";var o=window.wp.hooks;window.wpParselyHooks=(0,o.createHooks)(),function(){const o=()=>window.wpParselyHooks.doAction("wpParselyOnLoad"),n=()=>window.wpParselyHooks.doAction("wpParselyOnReady");if("object"==typeof window.PARSELY){if("function"!=typeof window.PARSELY.onload)window.PARSELY.onload=o;else{const n=window.PARSELY.onload;window.PARSELY.onload=function(){n&&n(),o()}}if("function"!=typeof window.PARSELY.onReady)window.PARSELY.onReady=n;else{const o=window.PARSELY.onReady;window.PARSELY.onReady=function(){o&&o(),n()}}}else window.PARSELY={onload:o,onReady:n};!0===window.wpParselyDisableAutotrack&&(window.PARSELY.autotrack=!1)}()}(),void 0!==window.wpParselyApiKey&&window.wpParselyHooks.addAction("wpParselyOnLoad","wpParsely",(async function(){var n,i;const e=null===(n=o.g.PARSELY)||void 0===n||null===(i=n.config)||void 0===i?void 0:i.parsely_site_uuid;if(!window.wpParselyApiKey||!e)return;const w=`https://api.parsely.com/v2/profile?apikey=${encodeURIComponent(window.wpParselyApiKey)}&uuid=${encodeURIComponent(e)}&url=${encodeURIComponent(window.location.href)}`;return fetch(w)}))}(); \ No newline at end of file +!function(){"use strict";var n,o,e,t;n=window.wp.hooks,window.wpParselyHooks=(0,n.createHooks)(),function(){var n=function(){var n;return null===(n=window.wpParselyHooks)||void 0===n?void 0:n.doAction("wpParselyOnLoad")},o=function(){var n;return null===(n=window.wpParselyHooks)||void 0===n?void 0:n.doAction("wpParselyOnReady")};if("object"==typeof window.PARSELY){if("function"!=typeof window.PARSELY.onload)window.PARSELY.onload=n;else{var e=window.PARSELY.onload;window.PARSELY.onload=function(){e&&e(),n()}}if("function"!=typeof window.PARSELY.onReady)window.PARSELY.onReady=o;else{var t=window.PARSELY.onReady;window.PARSELY.onReady=function(){t&&t(),o()}}}else window.PARSELY={onload:n,onReady:o};!0===window.wpParselyDisableAutotrack&&(window.PARSELY.autotrack=!1)}(),e=function(n,o,e,t){return new(e||(e=Promise))((function(i,r){function a(n){try{c(t.next(n))}catch(n){r(n)}}function l(n){try{c(t.throw(n))}catch(n){r(n)}}function c(n){var o;n.done?i(n.value):(o=n.value,o instanceof e?o:new e((function(n){n(o)}))).then(a,l)}c((t=t.apply(n,o||[])).next())}))},t=function(n,o){var e,t,i,r,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return r={next:l(0),throw:l(1),return:l(2)},"function"==typeof Symbol&&(r[Symbol.iterator]=function(){return this}),r;function l(l){return function(c){return function(l){if(e)throw new TypeError("Generator is already executing.");for(;r&&(r=0,l[0]&&(a=0)),a;)try{if(e=1,t&&(i=2&l[0]?t.return:l[0]?t.throw||((i=t.return)&&i.call(t),0):t.next)&&!(i=i.call(t,l[1])).done)return i;switch(t=0,i&&(l=[2&l[0],i.value]),l[0]){case 0:case 1:i=l;break;case 4:return a.label++,{value:l[1],done:!1};case 5:a.label++,t=l[1],l=[0];continue;case 7:l=a.ops.pop(),a.trys.pop();continue;default:if(!((i=(i=a.trys).length>0&&i[i.length-1])||6!==l[0]&&2!==l[0])){a=0;continue}if(3===l[0]&&(!i||l[1]>i[0]&&l[1]{const e=document.querySelectorAll(".parsely-recommended-widget"),t=Array.from(e).map(a).reduce(((e,t)=>(e[t.url]||(e[t.url]=[]),e[t.url].push(t),e)),{});Object.entries(t).forEach((e=>{let[t,r]=e;fetch(t).then((e=>e.json())).then((e=>{r.forEach((t=>{!function(e,t){let{outerDiv:r,displayAuthor:n,displayDirection:i,imgDisplay:o,widgetId:d}=t;"none"!==o&&r.classList.add("display-thumbnail"),i&&r.classList.add("list-"+i);const a=document.createElement("ul");a.className="parsely-recommended-widget",r.appendChild(a);for(const[t,r]of Object.entries(e.data)){const e=document.createElement("li");e.className="parsely-recommended-widget-entry",e.setAttribute("id","parsely-recommended-widget-item"+t);const i=document.createElement("div");i.className="parsely-text-wrapper";const c=document.createElement("img");"parsely_thumb"===o?c.setAttribute("src",r.thumb_url_medium):"original"===o&&c.setAttribute("src",r.image_url),e.appendChild(c);const u=`?itm_campaign=${d}`,s="&itmMedium=site_widget",l="&itmSource=parsely_recommended_widget",p="&itm_content=widget_item-"+t,m=r.url+u+s+l+p,f=document.createElement("div");f.className="parsely-recommended-widget-title";const g=document.createElement("a");if(g.setAttribute("href",m),g.textContent=r.title,f.appendChild(g),i.appendChild(f),n){const e=document.createElement("div");e.className="parsely-recommended-widget-author",e.textContent=r.author,i.appendChild(e)}e.appendChild(i),a.appendChild(e)}r.appendChild(a),r.closest(".widget.Recommended_Widget").classList.remove("parsely-recommended-widget-hidden")}(e,t)}))}))}))}))}()}(); \ No newline at end of file +!function(){"use strict";var e={n:function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,{a:r}),r},d:function(t,r){for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}};!function(){var t=window.wp.domReady,r=e.n(t);function n(e){for(var t=1;t=16", @@ -2076,17 +2078,17 @@ "dev": true }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.20.1.tgz", - "integrity": "sha512-oeJK41dcdqkvdZy/HctKklJNkt/jh+av3PZARrZEl+fs/8HaHeeYoAvEwOV0u5I6bArTF17JEsTZMY359e/nfQ==", + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz", + "integrity": "sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg==", "dev": true, "dependencies": { - "comment-parser": "1.3.0", + "comment-parser": "1.3.1", "esquery": "^1.4.0", - "jsdoc-type-pratt-parser": "~2.2.3" + "jsdoc-type-pratt-parser": "~3.1.0" }, "engines": { - "node": "^12 || ^14 || ^16 || ^17" + "node": "^14 || ^16 || ^17 || ^18 || ^19" } }, "node_modules/@eslint/eslintrc": { @@ -3186,9 +3188,9 @@ } }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "dev": true }, "node_modules/@sideway/pinpoint": { @@ -3971,6 +3973,22 @@ "pretty-format": "^27.0.0" } }, + "node_modules/@types/jest-environment-puppeteer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-5.0.3.tgz", + "integrity": "sha512-vWGfeb+0TOPZy7+VscKURWzE5lzYjclSWLxtjVpDAYcjUv8arAS1av06xK3mpgeNCDVx7XvavD8Elq1a4w9wIA==", + "dev": true, + "dependencies": { + "@jest/types": ">=24 <=27", + "@types/puppeteer": "^5.4.0", + "jest-environment-node": ">=24 <=27" + } + }, + "node_modules/@types/js-cookie": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz", + "integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==" + }, "node_modules/@types/json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz", @@ -4058,6 +4076,15 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", "dev": true }, + "node_modules/@types/puppeteer": { + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.7.tgz", + "integrity": "sha512-JdGWZZYL0vKapXF4oQTC5hLVNfOgdPrqeZ1BiQnGk5cB7HeE91EWUiTdVSdQPobRN8rIcdffjiOgCYJ/S8QrnQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -4458,15 +4485,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.1.tgz", - "integrity": "sha512-cOizjPlKEh0bXdFrBLTrI/J6B/QMlhwE9auOov53tgB+qMukH6/h8YAK/qw+QJGct/PTbdh2lytGyipxCcEtAw==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.53.0.tgz", + "integrity": "sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.45.1", - "@typescript-eslint/type-utils": "5.45.1", - "@typescript-eslint/utils": "5.45.1", + "@typescript-eslint/scope-manager": "5.53.0", + "@typescript-eslint/type-utils": "5.53.0", + "@typescript-eslint/utils": "5.53.0", "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", @@ -4491,13 +4519,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.45.1.tgz", - "integrity": "sha512-D6fCileR6Iai7E35Eb4Kp+k0iW7F1wxXYrOhX/3dywsOJpJAQ20Fwgcf+P/TDtvQ7zcsWsrJaglaQWDhOMsspQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz", + "integrity": "sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/visitor-keys": "5.45.1" + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4508,9 +4536,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.1.tgz", - "integrity": "sha512-HEW3U0E5dLjUT+nk7b4lLbOherS1U4ap+b9pfu2oGsW3oPu7genRaY9dDv3nMczC1rbnRY2W/D7SN05wYoGImg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", + "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4521,12 +4549,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.1.tgz", - "integrity": "sha512-cy9ln+6rmthYWjH9fmx+5FU/JDpjQb586++x2FZlveq7GdGuLLW9a2Jcst2TGekH82bXpfmRNSwP9tyEs6RjvQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", + "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/types": "5.53.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -4596,165 +4624,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.16.0.tgz", - "integrity": "sha512-bitZtqO13XX64/UOQKoDbVg2H4VHzbHnWWlTRc7ofq7SuQyPCwEycF1Zmn5ZAMTJZ3p5uMS7xJGUdOtZK7LrNw==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "5.16.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.16.0.tgz", - "integrity": "sha512-P+Yab2Hovg8NekLIR/mOElCDPyGgFZKhGoZA901Yax6WR6HVeGLbsqJkZ+Cvk5nts/dAlFKm8PfL43UZnWdpIQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.16.0", - "@typescript-eslint/visitor-keys": "5.16.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.16.0.tgz", - "integrity": "sha512-oUorOwLj/3/3p/HFwrp6m/J2VfbLC8gjW5X3awpQJ/bSG+YRGFS4dpsvtQ8T2VNveV+LflQHjlLvB6v0R87z4g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.16.0.tgz", - "integrity": "sha512-SE4VfbLWUZl9MR+ngLSARptUv2E8brY0luCdgmUevU6arZRY/KxYoLI/3V/yxaURR8tLRN7bmZtJdgmzLHI6pQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.16.0", - "@typescript-eslint/visitor-keys": "5.16.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/utils": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.16.0.tgz", - "integrity": "sha512-iYej2ER6AwmejLWMWzJIHy3nPJeGDuCqf8Jnb+jAQVoPpmWzwQOfa9hWVB8GIQE5gsCv/rfN4T+AYb/V06WseQ==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.16.0", - "@typescript-eslint/types": "5.16.0", - "@typescript-eslint/typescript-estree": "5.16.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.16.0.tgz", - "integrity": "sha512-jqxO8msp5vZDhikTwq9ubyMHqZ67UIvawohr4qF3KhlpL7gzSjOd+8471H3nh5LyABkaI85laEKKU8SnGUK5/g==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.16.0", - "eslint-visitor-keys": "^3.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/parser": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.15.0.tgz", @@ -4800,13 +4669,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.1.tgz", - "integrity": "sha512-aosxFa+0CoYgYEl3aptLe1svP910DJq68nwEJzyQcrtRhC4BN0tJAvZGAe+D0tzjJmFXe+h4leSsiZhwBa2vrA==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.53.0.tgz", + "integrity": "sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.45.1", - "@typescript-eslint/utils": "5.45.1", + "@typescript-eslint/typescript-estree": "5.53.0", + "@typescript-eslint/utils": "5.53.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -4827,9 +4696,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.1.tgz", - "integrity": "sha512-HEW3U0E5dLjUT+nk7b4lLbOherS1U4ap+b9pfu2oGsW3oPu7genRaY9dDv3nMczC1rbnRY2W/D7SN05wYoGImg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", + "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4840,13 +4709,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.1.tgz", - "integrity": "sha512-76NZpmpCzWVrrb0XmYEpbwOz/FENBi+5W7ipVXAsG3OoFrQKJMiaqsBMbvGRyLtPotGqUfcY7Ur8j0dksDJDng==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz", + "integrity": "sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/visitor-keys": "5.45.1", + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4867,12 +4736,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.1.tgz", - "integrity": "sha512-cy9ln+6rmthYWjH9fmx+5FU/JDpjQb586++x2FZlveq7GdGuLLW9a2Jcst2TGekH82bXpfmRNSwP9tyEs6RjvQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", + "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/types": "5.53.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -5016,16 +4885,16 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.1.tgz", - "integrity": "sha512-rlbC5VZz68+yjAzQBc4I7KDYVzWG2X/OrqoZrMahYq3u8FFtmQYc+9rovo/7wlJH5kugJ+jQXV5pJMnofGmPRw==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.53.0.tgz", + "integrity": "sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.45.1", - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/typescript-estree": "5.45.1", + "@typescript-eslint/scope-manager": "5.53.0", + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/typescript-estree": "5.53.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -5042,13 +4911,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.45.1.tgz", - "integrity": "sha512-D6fCileR6Iai7E35Eb4Kp+k0iW7F1wxXYrOhX/3dywsOJpJAQ20Fwgcf+P/TDtvQ7zcsWsrJaglaQWDhOMsspQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz", + "integrity": "sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/visitor-keys": "5.45.1" + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5059,9 +4928,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.1.tgz", - "integrity": "sha512-HEW3U0E5dLjUT+nk7b4lLbOherS1U4ap+b9pfu2oGsW3oPu7genRaY9dDv3nMczC1rbnRY2W/D7SN05wYoGImg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", + "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5072,13 +4941,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.1.tgz", - "integrity": "sha512-76NZpmpCzWVrrb0XmYEpbwOz/FENBi+5W7ipVXAsG3OoFrQKJMiaqsBMbvGRyLtPotGqUfcY7Ur8j0dksDJDng==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz", + "integrity": "sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/visitor-keys": "5.45.1", + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -5099,12 +4968,12 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.1.tgz", - "integrity": "sha512-cy9ln+6rmthYWjH9fmx+5FU/JDpjQb586++x2FZlveq7GdGuLLW9a2Jcst2TGekH82bXpfmRNSwP9tyEs6RjvQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", + "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/types": "5.53.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -5415,14 +5284,14 @@ } }, "node_modules/@wordpress/api-fetch": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.19.0.tgz", - "integrity": "sha512-nidem0S47aulcXzIjy5oQrC/nKrVtSkEEE0nmHQAp/bx2ZYBu7UwByiTfbI3bxLKRPhtdgLBkQfyA7eUlegGPQ==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.24.0.tgz", + "integrity": "sha512-XxvjND2hh9ooC+Iv7xMqfNubxSok3F+EWDk/yewNRZYKrIHEYXlCnxXImndlI0hQdzWsNKawGvwWa/gk8GoPIg==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.22.0", - "@wordpress/url": "^3.23.0" + "@wordpress/i18n": "^4.27.0", + "@wordpress/url": "^3.28.0" }, "engines": { "node": ">=12" @@ -5441,9 +5310,9 @@ } }, "node_modules/@wordpress/babel-plugin-import-jsx-pragma": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-4.5.0.tgz", - "integrity": "sha512-/5TZUHgy4fh7L1aQJPQ8dKjaWBio41uiR4Y9aGH0oeg6pXdQlEOtbAXQtTAHeGyE1vJMYJUVHdRWdLocpGQWgA==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-4.10.0.tgz", + "integrity": "sha512-JEhn9v6rZJ4RVNC1g6W2KR4qd/VPwJ8hIoRIZnL0XxpCD7LTGiVm14rQPqlKZARqWlRknYn1Zt1pIr4XlVIPVg==", "dev": true, "engines": { "node": ">=14" @@ -5453,9 +5322,9 @@ } }, "node_modules/@wordpress/babel-preset-default": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-7.6.0.tgz", - "integrity": "sha512-DVwF85jvgGR6ExSIOsJKBj2v5vWO2AsrKUs5Vg9vTG7YdkR23JENBOiORYPgYWIdbK7JmSIXEPyfL7HpdvNp9A==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-7.11.0.tgz", + "integrity": "sha512-nFfGYmgdptqOnOwQqJYOOhqEECznhTT5oq3DaO/nw+LXczhIPWfBnVj5GWNIp+8mnwKDZcI2Ns5Vf64WSzA7Fw==", "dev": true, "dependencies": { "@babel/core": "^7.16.0", @@ -5464,10 +5333,10 @@ "@babel/preset-env": "^7.16.0", "@babel/preset-typescript": "^7.16.0", "@babel/runtime": "^7.16.0", - "@wordpress/babel-plugin-import-jsx-pragma": "^4.5.0", - "@wordpress/browserslist-config": "^5.5.0", - "@wordpress/element": "^4.20.0", - "@wordpress/warning": "^2.22.0", + "@wordpress/babel-plugin-import-jsx-pragma": "^4.10.0", + "@wordpress/browserslist-config": "^5.10.0", + "@wordpress/element": "^5.4.0", + "@wordpress/warning": "^2.27.0", "browserslist": "^4.17.6", "core-js": "^3.19.1" }, @@ -5475,6 +5344,88 @@ "node": ">=14" } }, + "node_modules/@wordpress/babel-preset-default/node_modules/@types/react": { + "version": "18.0.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", + "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@wordpress/babel-preset-default/node_modules/@types/react-dom": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", + "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@wordpress/babel-preset-default/node_modules/@wordpress/element": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.4.0.tgz", + "integrity": "sha512-vOlLdqzmkJCNWXDSp+1bwdT721mkAnMIfFom5SQnAhNOb59Y4CJRNQ37Oh1P19kMEYTnYMi/1R0DtOShlId+iA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.16.0", + "@types/react": "^18.0.21", + "@types/react-dom": "^18.0.6", + "@wordpress/escape-html": "^2.27.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@wordpress/babel-preset-default/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wordpress/babel-preset-default/node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wordpress/babel-preset-default/node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@wordpress/babel-preset-default/node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/@wordpress/base-styles": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-4.13.0.tgz", @@ -5660,9 +5611,9 @@ } }, "node_modules/@wordpress/browserslist-config": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.5.0.tgz", - "integrity": "sha512-M3BhWQ+R8AK2g861hOtLTp/WnjcXPddiNzoMHR5zQOhcuSmgfP4dNJIJUWIVRPhqpES2EGcc30F9MIEyV27WiQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.10.0.tgz", + "integrity": "sha512-NYqAGHJno4/AqikS6pok4BuudUBZR/pd3fhSzQUVaCFgK2C5qzauaGU9C7J6sRJ1NDchJu05Ubu7gRkA8dIASA==", "dev": true, "engines": { "node": ">=14" @@ -5946,15 +5897,15 @@ } }, "node_modules/@wordpress/e2e-test-utils": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-8.6.0.tgz", - "integrity": "sha512-ojauttVboG2jc8OftFoG09SLPqhGGPGQY22Gzzn270jdB+ZJFP3mRv2E79/nd0F8b9qvYbiyhE57Dv+Tby9W+g==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-9.4.0.tgz", + "integrity": "sha512-9PMTPtyevCurd8yfT+6V2yhdp/2a6tC4sUmjCj3dl8QFsbBgCMZZ/CDDfdi8SgtS0CC81EaQHkGCiiPa4RWs+g==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/api-fetch": "^6.19.0", - "@wordpress/keycodes": "^3.22.0", - "@wordpress/url": "^3.23.0", + "@wordpress/api-fetch": "^6.24.0", + "@wordpress/keycodes": "^3.27.0", + "@wordpress/url": "^3.28.0", "change-case": "^4.1.2", "form-data": "^4.0.0", "node-fetch": "^2.6.0" @@ -6092,9 +6043,9 @@ } }, "node_modules/@wordpress/env": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-5.7.0.tgz", - "integrity": "sha512-9H5ZUhqRzdjghQgVMpxZDpx/W0Tf74D7mExFEQPGZdfoJUWNiHpgfbRK70IGKJk2kGit+9b90zuMIIA6PDahNw==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-5.12.0.tgz", + "integrity": "sha512-skDX2spqGOhtjSZNIZdYEGQLGmrX6agQW1E1YB5Ev9tEPzOWy5mh5Ub4MVen1sADrwjE7I5P+QkcSxMjU1UxpA==", "dev": true, "dependencies": { "chalk": "^4.0.0", @@ -6185,9 +6136,9 @@ } }, "node_modules/@wordpress/escape-html": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.22.0.tgz", - "integrity": "sha512-GUo6VLugIZxen1rdYuotvz6Vqa+5fNtVelNjXLwDqRu0iY2RXeoTux9V5bZWXPnGb54ryqfYmR4gH6F8xZhWzQ==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.27.0.tgz", + "integrity": "sha512-XXmqdY6AOpzegQeKCqAkaqfHdgcyLdXRE2E5iP67YSVuz/ccLP3Xm4YU/IRVBBKWK6Zzb5/dGwefGGN0r37fEw==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0" @@ -6197,21 +6148,21 @@ } }, "node_modules/@wordpress/eslint-plugin": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-13.6.0.tgz", - "integrity": "sha512-GIW4AHb0IC9VA6y8IRGQpADxpvdG+K0an/ZpzlYmBudql7YuLlUmp9rLpBlSvHf9iGX81OUYl9B63XEItPzGEw==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-13.10.0.tgz", + "integrity": "sha512-FW3JryRMeUpdhbBi6n4bKPHoYUqwSZI/7jjmvObiUlr8uJfXRFRXfgYOCP8BiVjMyGDBpiMs95Fyf1QbQ79Img==", "dev": true, "dependencies": { "@babel/eslint-parser": "^7.16.0", "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", - "@wordpress/babel-preset-default": "^7.6.0", - "@wordpress/prettier-config": "^2.5.0", + "@wordpress/babel-preset-default": "^7.10.0", + "@wordpress/prettier-config": "^2.9.0", "cosmiconfig": "^7.0.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jest": "^25.2.3", - "eslint-plugin-jsdoc": "^37.0.3", + "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-jsdoc": "^39.6.9", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^3.3.0", "eslint-plugin-react": "^7.27.0", @@ -6238,30 +6189,6 @@ } } }, - "node_modules/@wordpress/eslint-plugin/node_modules/eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/experimental-utils": "^5.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, "node_modules/@wordpress/eslint-plugin/node_modules/globals": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", @@ -6290,9 +6217,9 @@ } }, "node_modules/@wordpress/hooks": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.22.0.tgz", - "integrity": "sha512-0pjpXzUDiiIlQGRcOCHO5N73eto367KrevFhTPn8NSK8rhNqL7XaA3YJRIBemViwsk1GaPUzheg9E3UmIL0W4g==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.27.0.tgz", + "integrity": "sha512-izhRvOJzc/VFsu59KC+et1/35GL0Op7I60RZj2lkTnEz1vGvtClY3okCbOtGN0Adc8ewbTf4kB6qgKMsLtW0Dg==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0" @@ -6314,13 +6241,13 @@ } }, "node_modules/@wordpress/i18n": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.22.0.tgz", - "integrity": "sha512-b1nQJhrBilDj3oJql9k9dzlPEJ5vWd36Q0ri0znLBOJUOq2J0jgKwgtC84dun77kBb9Upfi4NZNiBI8OuSbiuA==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.27.0.tgz", + "integrity": "sha512-mb4xN7aYh+e9QHWxwg21RqcIHROowWD7XlC62KlpwZmhIKj92C0az6HBH5a2b9VhvrsLL3xw1hWMzfNWPT62bg==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.22.0", + "@wordpress/hooks": "^3.27.0", "gettext-parser": "^1.3.1", "memize": "^1.1.0", "sprintf-js": "^1.1.1", @@ -6442,15 +6369,14 @@ } }, "node_modules/@wordpress/keycodes": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.22.0.tgz", - "integrity": "sha512-nWEVm1hJdcDh5EJ6IEO4chqsZxDCt5qYyaUPjzFDtEM65abcMnbE7rBT36WP17slSJlPN8Y8HldajERwvKXR6Q==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.27.0.tgz", + "integrity": "sha512-iGqJ1DS7dS95zOqsDWSCg1cqp8V5HhnylSofQyAMgY7xZRlo5tHxyrR/bP+w1aPs2N4rz0akMLv4YJsx2+V/nA==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.22.0", - "change-case": "^4.1.2", - "lodash": "^4.17.21" + "@wordpress/i18n": "^4.27.0", + "change-case": "^4.1.2" }, "engines": { "node": ">=12" @@ -6560,9 +6486,9 @@ } }, "node_modules/@wordpress/prettier-config": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-2.5.0.tgz", - "integrity": "sha512-YI7CwUScwFW3N6PCH6IH2tvnfgkhAYEnYDOJ30JG2P0E3vXG6lnvrdVpQyKlEsGqUDy3FBcTPIP/m3/SHpp6Iw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-2.9.0.tgz", + "integrity": "sha512-Y6Huuwr0XzVAREsALqQ+Il2SI5da0uTiysNd6Rq4hFPvjolsiFKCZYdniow6VpTXm5iVMGdKQIOoC3awSyTAXA==", "dev": true, "engines": { "node": ">=14" @@ -6916,9 +6842,9 @@ } }, "node_modules/@wordpress/url": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.23.0.tgz", - "integrity": "sha512-JBNrzSUg7+b4cpJQjDVTHAw8x77EcdLWOAxLlKqI37Pd2EHUZXWnlVU5EqbNLLhXVJ+/6QMzS3QqNILhjIiqdw==", + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.28.0.tgz", + "integrity": "sha512-DZ4eLOXEPI2IYg2BxcEQ0qar8qb7B06KdRbV/0DacZ1jTsBfxB19Md73jeo4h3+ws3Iu+eWjg8c+zGwnRmzyHw==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", @@ -6946,9 +6872,9 @@ } }, "node_modules/@wordpress/warning": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.22.0.tgz", - "integrity": "sha512-BMM4GqiJNIZzh5SIK17EOyilp08mQ+DoKfxL+pl/lpA68jxYUTcKJf0atNbxXKPZHDCWGnQzFwr+huNYOtp4CQ==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.27.0.tgz", + "integrity": "sha512-s5JIGBNGTnYVsNN0zxCRxbi2Gs+q+tqSZNAznHQWkCeANaB22LeUQw7KL13T0ekFL6y1h2jNP9tWSU5/mnMTCg==", "dev": true, "engines": { "node": ">=12" @@ -7554,9 +7480,9 @@ } }, "node_modules/babel-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -8554,9 +8480,9 @@ } }, "node_modules/comment-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.0.tgz", - "integrity": "sha512-hRpmWIKgzd81vn0ydoWoyPoALEOnF4wt8yKD35Ib1D6XC2siLiYaiqfGkYrunuKdsXGwpBpHU3+9r+RVw2NZfA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", "dev": true, "engines": { "node": ">= 12.0.0" @@ -10433,9 +10359,9 @@ "dev": true }, "node_modules/eslint-plugin-jest": { - "version": "27.1.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.1.6.tgz", - "integrity": "sha512-XA7RFLSrlQF9IGtAmhddkUkBuICCTuryfOTfCSWcZHiHb69OilIH05oozH2XA6CEOtztnOd0vgXyvxZodkxGjg==", + "version": "27.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", + "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -10457,27 +10383,43 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "37.9.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-37.9.7.tgz", - "integrity": "sha512-8alON8yYcStY94o0HycU2zkLKQdcS+qhhOUNQpfONHHwvI99afbmfpYuPqf6PbLz5pLZldG3Te5I0RbAiTN42g==", + "version": "39.7.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.7.5.tgz", + "integrity": "sha512-6L90P0AnZcE4ra7nocolp9vTjgVr2wEZ7jPnEA/X30XAoQPk+wvnaq61n164Tf7Fg4QPpJtRSCPpApOsfWDdNA==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.20.1", - "comment-parser": "1.3.0", - "debug": "^4.3.3", + "@es-joy/jsdoccomment": "~0.36.1", + "comment-parser": "1.3.1", + "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.4.0", - "regextras": "^0.8.0", - "semver": "^7.3.5", + "semver": "^7.3.8", "spdx-expression-parse": "^3.0.1" }, "engines": { - "node": "^12 || ^14 || ^16 || ^17" + "node": "^14 || ^16 || ^17 || ^18 || ^19" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, + "node_modules/eslint-plugin-jsdoc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/eslint-plugin-jsdoc/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -10491,9 +10433,9 @@ } }, "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -12072,6 +12014,12 @@ "node": ">=0.10.0" } }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -12281,9 +12229,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "node_modules/http-deceiver": { @@ -12414,9 +12362,9 @@ } }, "node_modules/husky": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz", - "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", "dev": true, "bin": { "husky": "lib/bin.js" @@ -15101,9 +15049,9 @@ } }, "node_modules/jsdoc-type-pratt-parser": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", - "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz", + "integrity": "sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw==", "dev": true, "engines": { "node": ">=12.0.0" @@ -15218,9 +15166,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -17981,9 +17929,9 @@ } }, "node_modules/prettier": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz", - "integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -18699,15 +18647,6 @@ "node": ">=4" } }, - "node_modules/regextras": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", - "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", - "dev": true, - "engines": { - "node": ">=0.1.14" - } - }, "node_modules/regjsgen": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", @@ -19574,9 +19513,9 @@ "dev": true }, "node_modules/simple-git": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.15.1.tgz", - "integrity": "sha512-73MVa5984t/JP4JcQt0oZlKGr42ROYWC3BcUZfuHtT3IHKPspIvL0cZBnvPXF7LL3S/qVeVHVdYYmJ3LOTw4Rg==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz", + "integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==", "dev": true, "dependencies": { "@kwsites/file-exists": "^1.1.1", @@ -20994,9 +20933,9 @@ } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -21109,9 +21048,9 @@ } }, "node_modules/typescript": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", - "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -23764,14 +23703,14 @@ "dev": true }, "@es-joy/jsdoccomment": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.20.1.tgz", - "integrity": "sha512-oeJK41dcdqkvdZy/HctKklJNkt/jh+av3PZARrZEl+fs/8HaHeeYoAvEwOV0u5I6bArTF17JEsTZMY359e/nfQ==", + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz", + "integrity": "sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg==", "dev": true, "requires": { - "comment-parser": "1.3.0", + "comment-parser": "1.3.1", "esquery": "^1.4.0", - "jsdoc-type-pratt-parser": "~2.2.3" + "jsdoc-type-pratt-parser": "~3.1.0" } }, "@eslint/eslintrc": { @@ -24602,9 +24541,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "dev": true }, "@sideway/pinpoint": { @@ -25193,6 +25132,22 @@ "pretty-format": "^27.0.0" } }, + "@types/jest-environment-puppeteer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-5.0.3.tgz", + "integrity": "sha512-vWGfeb+0TOPZy7+VscKURWzE5lzYjclSWLxtjVpDAYcjUv8arAS1av06xK3mpgeNCDVx7XvavD8Elq1a4w9wIA==", + "dev": true, + "requires": { + "@jest/types": ">=24 <=27", + "@types/puppeteer": "^5.4.0", + "jest-environment-node": ">=24 <=27" + } + }, + "@types/js-cookie": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz", + "integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==" + }, "@types/json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz", @@ -25280,6 +25235,15 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", "dev": true }, + "@types/puppeteer": { + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.7.tgz", + "integrity": "sha512-JdGWZZYL0vKapXF4oQTC5hLVNfOgdPrqeZ1BiQnGk5cB7HeE91EWUiTdVSdQPobRN8rIcdffjiOgCYJ/S8QrnQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -25675,15 +25639,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.1.tgz", - "integrity": "sha512-cOizjPlKEh0bXdFrBLTrI/J6B/QMlhwE9auOov53tgB+qMukH6/h8YAK/qw+QJGct/PTbdh2lytGyipxCcEtAw==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.53.0.tgz", + "integrity": "sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.45.1", - "@typescript-eslint/type-utils": "5.45.1", - "@typescript-eslint/utils": "5.45.1", + "@typescript-eslint/scope-manager": "5.53.0", + "@typescript-eslint/type-utils": "5.53.0", + "@typescript-eslint/utils": "5.53.0", "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", @@ -25692,28 +25657,28 @@ }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.45.1.tgz", - "integrity": "sha512-D6fCileR6Iai7E35Eb4Kp+k0iW7F1wxXYrOhX/3dywsOJpJAQ20Fwgcf+P/TDtvQ7zcsWsrJaglaQWDhOMsspQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz", + "integrity": "sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/visitor-keys": "5.45.1" + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0" } }, "@typescript-eslint/types": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.1.tgz", - "integrity": "sha512-HEW3U0E5dLjUT+nk7b4lLbOherS1U4ap+b9pfu2oGsW3oPu7genRaY9dDv3nMczC1rbnRY2W/D7SN05wYoGImg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", + "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", "dev": true }, "@typescript-eslint/visitor-keys": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.1.tgz", - "integrity": "sha512-cy9ln+6rmthYWjH9fmx+5FU/JDpjQb586++x2FZlveq7GdGuLLW9a2Jcst2TGekH82bXpfmRNSwP9tyEs6RjvQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", + "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/types": "5.53.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -25758,102 +25723,6 @@ } } }, - "@typescript-eslint/experimental-utils": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.16.0.tgz", - "integrity": "sha512-bitZtqO13XX64/UOQKoDbVg2H4VHzbHnWWlTRc7ofq7SuQyPCwEycF1Zmn5ZAMTJZ3p5uMS7xJGUdOtZK7LrNw==", - "dev": true, - "requires": { - "@typescript-eslint/utils": "5.16.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.16.0.tgz", - "integrity": "sha512-P+Yab2Hovg8NekLIR/mOElCDPyGgFZKhGoZA901Yax6WR6HVeGLbsqJkZ+Cvk5nts/dAlFKm8PfL43UZnWdpIQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.16.0", - "@typescript-eslint/visitor-keys": "5.16.0" - } - }, - "@typescript-eslint/types": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.16.0.tgz", - "integrity": "sha512-oUorOwLj/3/3p/HFwrp6m/J2VfbLC8gjW5X3awpQJ/bSG+YRGFS4dpsvtQ8T2VNveV+LflQHjlLvB6v0R87z4g==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.16.0.tgz", - "integrity": "sha512-SE4VfbLWUZl9MR+ngLSARptUv2E8brY0luCdgmUevU6arZRY/KxYoLI/3V/yxaURR8tLRN7bmZtJdgmzLHI6pQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.16.0", - "@typescript-eslint/visitor-keys": "5.16.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.16.0.tgz", - "integrity": "sha512-iYej2ER6AwmejLWMWzJIHy3nPJeGDuCqf8Jnb+jAQVoPpmWzwQOfa9hWVB8GIQE5gsCv/rfN4T+AYb/V06WseQ==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.16.0", - "@typescript-eslint/types": "5.16.0", - "@typescript-eslint/typescript-estree": "5.16.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.16.0.tgz", - "integrity": "sha512-jqxO8msp5vZDhikTwq9ubyMHqZ67UIvawohr4qF3KhlpL7gzSjOd+8471H3nh5LyABkaI85laEKKU8SnGUK5/g==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.16.0", - "eslint-visitor-keys": "^3.0.0" - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, "@typescript-eslint/parser": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.15.0.tgz", @@ -25877,31 +25746,31 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.1.tgz", - "integrity": "sha512-aosxFa+0CoYgYEl3aptLe1svP910DJq68nwEJzyQcrtRhC4BN0tJAvZGAe+D0tzjJmFXe+h4leSsiZhwBa2vrA==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.53.0.tgz", + "integrity": "sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.45.1", - "@typescript-eslint/utils": "5.45.1", + "@typescript-eslint/typescript-estree": "5.53.0", + "@typescript-eslint/utils": "5.53.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "dependencies": { "@typescript-eslint/types": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.1.tgz", - "integrity": "sha512-HEW3U0E5dLjUT+nk7b4lLbOherS1U4ap+b9pfu2oGsW3oPu7genRaY9dDv3nMczC1rbnRY2W/D7SN05wYoGImg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", + "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.1.tgz", - "integrity": "sha512-76NZpmpCzWVrrb0XmYEpbwOz/FENBi+5W7ipVXAsG3OoFrQKJMiaqsBMbvGRyLtPotGqUfcY7Ur8j0dksDJDng==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz", + "integrity": "sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/visitor-keys": "5.45.1", + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -25910,12 +25779,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.1.tgz", - "integrity": "sha512-cy9ln+6rmthYWjH9fmx+5FU/JDpjQb586++x2FZlveq7GdGuLLW9a2Jcst2TGekH82bXpfmRNSwP9tyEs6RjvQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", + "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/types": "5.53.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -26008,45 +25877,45 @@ } }, "@typescript-eslint/utils": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.1.tgz", - "integrity": "sha512-rlbC5VZz68+yjAzQBc4I7KDYVzWG2X/OrqoZrMahYq3u8FFtmQYc+9rovo/7wlJH5kugJ+jQXV5pJMnofGmPRw==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.53.0.tgz", + "integrity": "sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.45.1", - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/typescript-estree": "5.45.1", + "@typescript-eslint/scope-manager": "5.53.0", + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/typescript-estree": "5.53.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.45.1.tgz", - "integrity": "sha512-D6fCileR6Iai7E35Eb4Kp+k0iW7F1wxXYrOhX/3dywsOJpJAQ20Fwgcf+P/TDtvQ7zcsWsrJaglaQWDhOMsspQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz", + "integrity": "sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/visitor-keys": "5.45.1" + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0" } }, "@typescript-eslint/types": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.1.tgz", - "integrity": "sha512-HEW3U0E5dLjUT+nk7b4lLbOherS1U4ap+b9pfu2oGsW3oPu7genRaY9dDv3nMczC1rbnRY2W/D7SN05wYoGImg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", + "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.1.tgz", - "integrity": "sha512-76NZpmpCzWVrrb0XmYEpbwOz/FENBi+5W7ipVXAsG3OoFrQKJMiaqsBMbvGRyLtPotGqUfcY7Ur8j0dksDJDng==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz", + "integrity": "sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.45.1", - "@typescript-eslint/visitor-keys": "5.45.1", + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -26055,12 +25924,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.45.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.1.tgz", - "integrity": "sha512-cy9ln+6rmthYWjH9fmx+5FU/JDpjQb586++x2FZlveq7GdGuLLW9a2Jcst2TGekH82bXpfmRNSwP9tyEs6RjvQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", + "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.45.1", + "@typescript-eslint/types": "5.53.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -26319,14 +26188,14 @@ } }, "@wordpress/api-fetch": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.19.0.tgz", - "integrity": "sha512-nidem0S47aulcXzIjy5oQrC/nKrVtSkEEE0nmHQAp/bx2ZYBu7UwByiTfbI3bxLKRPhtdgLBkQfyA7eUlegGPQ==", + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.24.0.tgz", + "integrity": "sha512-XxvjND2hh9ooC+Iv7xMqfNubxSok3F+EWDk/yewNRZYKrIHEYXlCnxXImndlI0hQdzWsNKawGvwWa/gk8GoPIg==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.22.0", - "@wordpress/url": "^3.23.0" + "@wordpress/i18n": "^4.27.0", + "@wordpress/url": "^3.28.0" } }, "@wordpress/autop": { @@ -26339,16 +26208,16 @@ } }, "@wordpress/babel-plugin-import-jsx-pragma": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-4.5.0.tgz", - "integrity": "sha512-/5TZUHgy4fh7L1aQJPQ8dKjaWBio41uiR4Y9aGH0oeg6pXdQlEOtbAXQtTAHeGyE1vJMYJUVHdRWdLocpGQWgA==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-4.10.0.tgz", + "integrity": "sha512-JEhn9v6rZJ4RVNC1g6W2KR4qd/VPwJ8hIoRIZnL0XxpCD7LTGiVm14rQPqlKZARqWlRknYn1Zt1pIr4XlVIPVg==", "dev": true, "requires": {} }, "@wordpress/babel-preset-default": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-7.6.0.tgz", - "integrity": "sha512-DVwF85jvgGR6ExSIOsJKBj2v5vWO2AsrKUs5Vg9vTG7YdkR23JENBOiORYPgYWIdbK7JmSIXEPyfL7HpdvNp9A==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-7.11.0.tgz", + "integrity": "sha512-nFfGYmgdptqOnOwQqJYOOhqEECznhTT5oq3DaO/nw+LXczhIPWfBnVj5GWNIp+8mnwKDZcI2Ns5Vf64WSzA7Fw==", "dev": true, "requires": { "@babel/core": "^7.16.0", @@ -26357,12 +26226,84 @@ "@babel/preset-env": "^7.16.0", "@babel/preset-typescript": "^7.16.0", "@babel/runtime": "^7.16.0", - "@wordpress/babel-plugin-import-jsx-pragma": "^4.5.0", - "@wordpress/browserslist-config": "^5.5.0", - "@wordpress/element": "^4.20.0", - "@wordpress/warning": "^2.22.0", + "@wordpress/babel-plugin-import-jsx-pragma": "^4.10.0", + "@wordpress/browserslist-config": "^5.10.0", + "@wordpress/element": "^5.4.0", + "@wordpress/warning": "^2.27.0", "browserslist": "^4.17.6", "core-js": "^3.19.1" + }, + "dependencies": { + "@types/react": { + "version": "18.0.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", + "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", + "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@wordpress/element": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.4.0.tgz", + "integrity": "sha512-vOlLdqzmkJCNWXDSp+1bwdT721mkAnMIfFom5SQnAhNOb59Y4CJRNQ37Oh1P19kMEYTnYMi/1R0DtOShlId+iA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.16.0", + "@types/react": "^18.0.21", + "@types/react-dom": "^18.0.6", + "@wordpress/escape-html": "^2.27.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + } } }, "@wordpress/base-styles": { @@ -26523,9 +26464,9 @@ } }, "@wordpress/browserslist-config": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.5.0.tgz", - "integrity": "sha512-M3BhWQ+R8AK2g861hOtLTp/WnjcXPddiNzoMHR5zQOhcuSmgfP4dNJIJUWIVRPhqpES2EGcc30F9MIEyV27WiQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.10.0.tgz", + "integrity": "sha512-NYqAGHJno4/AqikS6pok4BuudUBZR/pd3fhSzQUVaCFgK2C5qzauaGU9C7J6sRJ1NDchJu05Ubu7gRkA8dIASA==", "dev": true }, "@wordpress/components": { @@ -26729,15 +26670,15 @@ } }, "@wordpress/e2e-test-utils": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-8.6.0.tgz", - "integrity": "sha512-ojauttVboG2jc8OftFoG09SLPqhGGPGQY22Gzzn270jdB+ZJFP3mRv2E79/nd0F8b9qvYbiyhE57Dv+Tby9W+g==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-9.4.0.tgz", + "integrity": "sha512-9PMTPtyevCurd8yfT+6V2yhdp/2a6tC4sUmjCj3dl8QFsbBgCMZZ/CDDfdi8SgtS0CC81EaQHkGCiiPa4RWs+g==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/api-fetch": "^6.19.0", - "@wordpress/keycodes": "^3.22.0", - "@wordpress/url": "^3.23.0", + "@wordpress/api-fetch": "^6.24.0", + "@wordpress/keycodes": "^3.27.0", + "@wordpress/url": "^3.28.0", "change-case": "^4.1.2", "form-data": "^4.0.0", "node-fetch": "^2.6.0" @@ -26850,9 +26791,9 @@ } }, "@wordpress/env": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-5.7.0.tgz", - "integrity": "sha512-9H5ZUhqRzdjghQgVMpxZDpx/W0Tf74D7mExFEQPGZdfoJUWNiHpgfbRK70IGKJk2kGit+9b90zuMIIA6PDahNw==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-5.12.0.tgz", + "integrity": "sha512-skDX2spqGOhtjSZNIZdYEGQLGmrX6agQW1E1YB5Ev9tEPzOWy5mh5Ub4MVen1sADrwjE7I5P+QkcSxMjU1UxpA==", "dev": true, "requires": { "chalk": "^4.0.0", @@ -26921,30 +26862,30 @@ } }, "@wordpress/escape-html": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.22.0.tgz", - "integrity": "sha512-GUo6VLugIZxen1rdYuotvz6Vqa+5fNtVelNjXLwDqRu0iY2RXeoTux9V5bZWXPnGb54ryqfYmR4gH6F8xZhWzQ==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.27.0.tgz", + "integrity": "sha512-XXmqdY6AOpzegQeKCqAkaqfHdgcyLdXRE2E5iP67YSVuz/ccLP3Xm4YU/IRVBBKWK6Zzb5/dGwefGGN0r37fEw==", "dev": true, "requires": { "@babel/runtime": "^7.16.0" } }, "@wordpress/eslint-plugin": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-13.6.0.tgz", - "integrity": "sha512-GIW4AHb0IC9VA6y8IRGQpADxpvdG+K0an/ZpzlYmBudql7YuLlUmp9rLpBlSvHf9iGX81OUYl9B63XEItPzGEw==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-13.10.0.tgz", + "integrity": "sha512-FW3JryRMeUpdhbBi6n4bKPHoYUqwSZI/7jjmvObiUlr8uJfXRFRXfgYOCP8BiVjMyGDBpiMs95Fyf1QbQ79Img==", "dev": true, "requires": { "@babel/eslint-parser": "^7.16.0", "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", - "@wordpress/babel-preset-default": "^7.6.0", - "@wordpress/prettier-config": "^2.5.0", + "@wordpress/babel-preset-default": "^7.10.0", + "@wordpress/prettier-config": "^2.9.0", "cosmiconfig": "^7.0.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jest": "^25.2.3", - "eslint-plugin-jsdoc": "^37.0.3", + "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-jsdoc": "^39.6.9", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^3.3.0", "eslint-plugin-react": "^7.27.0", @@ -26953,15 +26894,6 @@ "requireindex": "^1.2.0" }, "dependencies": { - "eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "^5.0.0" - } - }, "globals": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", @@ -26980,9 +26912,9 @@ } }, "@wordpress/hooks": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.22.0.tgz", - "integrity": "sha512-0pjpXzUDiiIlQGRcOCHO5N73eto367KrevFhTPn8NSK8rhNqL7XaA3YJRIBemViwsk1GaPUzheg9E3UmIL0W4g==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.27.0.tgz", + "integrity": "sha512-izhRvOJzc/VFsu59KC+et1/35GL0Op7I60RZj2lkTnEz1vGvtClY3okCbOtGN0Adc8ewbTf4kB6qgKMsLtW0Dg==", "dev": true, "requires": { "@babel/runtime": "^7.16.0" @@ -26998,13 +26930,13 @@ } }, "@wordpress/i18n": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.22.0.tgz", - "integrity": "sha512-b1nQJhrBilDj3oJql9k9dzlPEJ5vWd36Q0ri0znLBOJUOq2J0jgKwgtC84dun77kBb9Upfi4NZNiBI8OuSbiuA==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.27.0.tgz", + "integrity": "sha512-mb4xN7aYh+e9QHWxwg21RqcIHROowWD7XlC62KlpwZmhIKj92C0az6HBH5a2b9VhvrsLL3xw1hWMzfNWPT62bg==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.22.0", + "@wordpress/hooks": "^3.27.0", "gettext-parser": "^1.3.1", "memize": "^1.1.0", "sprintf-js": "^1.1.1", @@ -27086,15 +27018,14 @@ } }, "@wordpress/keycodes": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.22.0.tgz", - "integrity": "sha512-nWEVm1hJdcDh5EJ6IEO4chqsZxDCt5qYyaUPjzFDtEM65abcMnbE7rBT36WP17slSJlPN8Y8HldajERwvKXR6Q==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.27.0.tgz", + "integrity": "sha512-iGqJ1DS7dS95zOqsDWSCg1cqp8V5HhnylSofQyAMgY7xZRlo5tHxyrR/bP+w1aPs2N4rz0akMLv4YJsx2+V/nA==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.22.0", - "change-case": "^4.1.2", - "lodash": "^4.17.21" + "@wordpress/i18n": "^4.27.0", + "change-case": "^4.1.2" } }, "@wordpress/media-utils": { @@ -27168,9 +27099,9 @@ } }, "@wordpress/prettier-config": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-2.5.0.tgz", - "integrity": "sha512-YI7CwUScwFW3N6PCH6IH2tvnfgkhAYEnYDOJ30JG2P0E3vXG6lnvrdVpQyKlEsGqUDy3FBcTPIP/m3/SHpp6Iw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-2.9.0.tgz", + "integrity": "sha512-Y6Huuwr0XzVAREsALqQ+Il2SI5da0uTiysNd6Rq4hFPvjolsiFKCZYdniow6VpTXm5iVMGdKQIOoC3awSyTAXA==", "dev": true, "requires": {} }, @@ -27431,9 +27362,9 @@ } }, "@wordpress/url": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.23.0.tgz", - "integrity": "sha512-JBNrzSUg7+b4cpJQjDVTHAw8x77EcdLWOAxLlKqI37Pd2EHUZXWnlVU5EqbNLLhXVJ+/6QMzS3QqNILhjIiqdw==", + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.28.0.tgz", + "integrity": "sha512-DZ4eLOXEPI2IYg2BxcEQ0qar8qb7B06KdRbV/0DacZ1jTsBfxB19Md73jeo4h3+ws3Iu+eWjg8c+zGwnRmzyHw==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", @@ -27452,9 +27383,9 @@ } }, "@wordpress/warning": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.22.0.tgz", - "integrity": "sha512-BMM4GqiJNIZzh5SIK17EOyilp08mQ+DoKfxL+pl/lpA68jxYUTcKJf0atNbxXKPZHDCWGnQzFwr+huNYOtp4CQ==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.27.0.tgz", + "integrity": "sha512-s5JIGBNGTnYVsNN0zxCRxbi2Gs+q+tqSZNAznHQWkCeANaB22LeUQw7KL13T0ekFL6y1h2jNP9tWSU5/mnMTCg==", "dev": true }, "@wordpress/wordcount": { @@ -27898,9 +27829,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" @@ -28676,9 +28607,9 @@ "dev": true }, "comment-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.0.tgz", - "integrity": "sha512-hRpmWIKgzd81vn0ydoWoyPoALEOnF4wt8yKD35Ib1D6XC2siLiYaiqfGkYrunuKdsXGwpBpHU3+9r+RVw2NZfA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", "dev": true }, "common-path-prefix": { @@ -30255,30 +30186,38 @@ } }, "eslint-plugin-jest": { - "version": "27.1.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.1.6.tgz", - "integrity": "sha512-XA7RFLSrlQF9IGtAmhddkUkBuICCTuryfOTfCSWcZHiHb69OilIH05oozH2XA6CEOtztnOd0vgXyvxZodkxGjg==", + "version": "27.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", + "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", "dev": true, "requires": { "@typescript-eslint/utils": "^5.10.0" } }, "eslint-plugin-jsdoc": { - "version": "37.9.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-37.9.7.tgz", - "integrity": "sha512-8alON8yYcStY94o0HycU2zkLKQdcS+qhhOUNQpfONHHwvI99afbmfpYuPqf6PbLz5pLZldG3Te5I0RbAiTN42g==", + "version": "39.7.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.7.5.tgz", + "integrity": "sha512-6L90P0AnZcE4ra7nocolp9vTjgVr2wEZ7jPnEA/X30XAoQPk+wvnaq61n164Tf7Fg4QPpJtRSCPpApOsfWDdNA==", "dev": true, "requires": { - "@es-joy/jsdoccomment": "~0.20.1", - "comment-parser": "1.3.0", - "debug": "^4.3.3", + "@es-joy/jsdoccomment": "~0.36.1", + "comment-parser": "1.3.1", + "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.4.0", - "regextras": "^0.8.0", - "semver": "^7.3.5", + "semver": "^7.3.8", "spdx-expression-parse": "^3.0.1" }, "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -30289,9 +30228,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -31349,6 +31288,12 @@ "integrity": "sha1-DH4heVWeXOfY1x9EI6+TcQCyJIw=", "dev": true }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -31515,9 +31460,9 @@ "dev": true }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "http-deceiver": { @@ -31615,9 +31560,9 @@ "dev": true }, "husky": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz", - "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", "dev": true }, "iconv-lite": { @@ -33590,9 +33535,9 @@ } }, "jsdoc-type-pratt-parser": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", - "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz", + "integrity": "sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw==", "dev": true }, "jsdom": { @@ -33686,9 +33631,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonc-parser": { @@ -35707,9 +35652,9 @@ "dev": true }, "prettier": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz", - "integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", "dev": true }, "prettier-linter-helpers": { @@ -36246,12 +36191,6 @@ "unicode-match-property-value-ecmascript": "^2.0.0" } }, - "regextras": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", - "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", - "dev": true - }, "regjsgen": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", @@ -36937,9 +36876,9 @@ "dev": true }, "simple-git": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.15.1.tgz", - "integrity": "sha512-73MVa5984t/JP4JcQt0oZlKGr42ROYWC3BcUZfuHtT3IHKPspIvL0cZBnvPXF7LL3S/qVeVHVdYYmJ3LOTw4Rg==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz", + "integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==", "dev": true, "requires": { "@kwsites/file-exists": "^1.1.1", @@ -38054,9 +37993,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" @@ -38146,9 +38085,9 @@ } }, "typescript": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", - "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true }, "uc.micro": { diff --git a/package.json b/package.json index 9d9f57893..2eb38f316 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wp-parsely", - "version": "3.6.2", + "version": "3.7.0", "private": true, "description": "The Parse.ly plugin facilitates real-time and historical analytics to your content through a platform designed and built for digital publishing.", "author": "parsely, hbbtstar, jblz, mikeyarce, GaryJ, parsely_mike, pauarge", @@ -37,42 +37,44 @@ "defaults" ], "dependencies": { + "@types/js-cookie": "^3.0.3", "@wordpress/dom-ready": "^3.9.0", "js-cookie": "^3.0.1" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", + "@types/jest-environment-puppeteer": "^5.0.3", "@types/wordpress__block-editor": "^7.0.0", "@types/wordpress__components": "^19.10.4", "@types/wordpress__core-data": "^2.4.5", "@types/wordpress__edit-post": "^4.0.1", "@types/wordpress__plugins": "^3.0.0", - "@typescript-eslint/eslint-plugin": "^5.45.1", - "@wordpress/api-fetch": "^6.11.0", - "@wordpress/babel-preset-default": "^7.5.0", + "@typescript-eslint/eslint-plugin": "^5.53.0", + "@wordpress/api-fetch": "^6.21.0", + "@wordpress/babel-preset-default": "^7.11.0", "@wordpress/block-editor": "^10.5.0", "@wordpress/blocks": "^11.21.0", "@wordpress/components": "^22.1.0", "@wordpress/compose": "^5.20.0", "@wordpress/core-data": "^5.5.0", "@wordpress/data": "^7.5.0", - "@wordpress/e2e-test-utils": "^8.6.0", + "@wordpress/e2e-test-utils": "^9.4.0", "@wordpress/edit-post": "^6.19.0", "@wordpress/element": "^4.7.0", - "@wordpress/env": "^5.7.0", - "@wordpress/eslint-plugin": "^13.6.0", + "@wordpress/env": "^5.12.0", + "@wordpress/eslint-plugin": "^13.10.0", "@wordpress/hooks": "^3.16.0", "@wordpress/i18n": "^4.5.0", "@wordpress/plugins": "^4.20.0", "@wordpress/scripts": "^24.6.0", - "@wordpress/url": "^3.23.0", + "@wordpress/url": "^3.26.0", "concurrently": "^7.6.0", - "eslint-plugin-jest": "^27.1.6", - "husky": "^8.0.2", - "prettier": "^2.8.0", + "eslint-plugin-jest": "^27.2.1", + "husky": "^8.0.3", + "prettier": "^2.8.4", "ts-loader": "^9.4.2", - "typescript": "^4.9.3" + "typescript": "^4.9.5" }, "scripts": { "build": "wp-scripts build", diff --git a/phpstan.neon b/phpstan.neon index ef43dd140..5d4709782 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,9 +3,10 @@ parameters: paths: - src/ - views/ + - tests/ - wp-parsely.php - uninstall.php - inferPrivatePropertyTypeFromConstructor: true - ignoreErrors: - # Uses func_get_args() - - '#^Function apply_filters(_ref_array)? invoked with [34567] parameters, 2 required\.$#' + scanFiles: + - vendor/axepress/wp-graphql-stubs/wp-graphql-stubs.php + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - vendor/php-stubs/wordpress-tests-stubs/wordpress-tests-stubs.php diff --git a/src/@types/assets/index.d.ts b/src/@types/assets/svg.d.ts similarity index 100% rename from src/@types/assets/index.d.ts rename to src/@types/assets/svg.d.ts diff --git a/src/@types/assets/window.d.ts b/src/@types/assets/window.d.ts new file mode 100644 index 000000000..cb3a4fbae --- /dev/null +++ b/src/@types/assets/window.d.ts @@ -0,0 +1,30 @@ +/** + * External dependencies. + */ +import type{ _Hooks } from '@wordpress/hooks/build-types/createHooks'; + +export {}; + +declare global { + interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + wp: any; + + /** + * Parsely Options + */ + PARSELY?: { + config?: { + uuid: string, + parsely_site_uuid: string, + }, + autotrack?: boolean, + onload?: () => unknown, + onReady?: () => unknown, + }, + wpParselySiteId: string, + wpParselyDisableAutotrack?: boolean; + wpParselyHooks?: _Hooks; + wpParselyPostsStatsResponse: string; + } +} diff --git a/src/@types/assets/wp-e2e-test-utils.d.ts b/src/@types/assets/wp-e2e-test-utils.d.ts new file mode 100644 index 000000000..c9b620132 --- /dev/null +++ b/src/@types/assets/wp-e2e-test-utils.d.ts @@ -0,0 +1 @@ +declare module '@wordpress/e2e-test-utils'; diff --git a/src/Endpoints/class-analytics-post-detail-api-proxy.php b/src/Endpoints/class-analytics-post-detail-api-proxy.php index 1cf8e3d06..b483d1bdd 100644 --- a/src/Endpoints/class-analytics-post-detail-api-proxy.php +++ b/src/Endpoints/class-analytics-post-detail-api-proxy.php @@ -10,8 +10,10 @@ namespace Parsely\Endpoints; +use Parsely\Parsely; use stdClass; use WP_REST_Request; +use WP_Error; /** * Configures the `/stats/post/detail` REST API endpoint. @@ -22,15 +24,15 @@ final class Analytics_Post_Detail_API_Proxy extends Base_API_Proxy { * Registers the endpoint's WP REST route. */ public function run(): void { - $this->register_endpoint( '/stats/post/detail', 'publish_posts' ); + $this->register_endpoint( '/stats/post/detail' ); } /** * Cached "proxy" to the Parse.ly `/analytics/post/detail` API endpoint. * * @param WP_REST_Request $request The request object. - * @return stdClass|WPError stdClass containing the data or a WP_Error - * object on failure. + * + * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. */ public function get_items( WP_REST_Request $request ) { return $this->get_data( $request ); @@ -39,17 +41,17 @@ public function get_items( WP_REST_Request $request ) { /** * Generates the final data from the passed response. * - * @param array $response The response received by the proxy. + * @param array $response The response received by the proxy. * @return array The generated data. */ - protected function generate_data( array $response ): array { - $stats_base_url = trailingslashit( 'https://dash.parsely.com/' . $this->parsely->get_api_key() ) . 'find'; + protected function generate_data( $response ): array { + $site_id = $this->parsely->get_site_id(); - $result = array_map( - static function( stdClass $item ) use ( $stats_base_url ) { + return array_map( + static function( stdClass $item ) use ( $site_id ) { return (object) array( 'avgEngaged' => self::get_duration( (float) $item->avg_engaged ), - 'statsUrl' => $stats_base_url . '?url=' . rawurlencode( $item->url ), + 'dashUrl' => Parsely::get_dash_url( $site_id, $item->url ), 'url' => $item->url, 'views' => number_format_i18n( $item->metrics->views ), 'visitors' => number_format_i18n( $item->metrics->visitors ), @@ -57,8 +59,6 @@ static function( stdClass $item ) use ( $stats_base_url ) { }, $response ); - - return $result; } /** diff --git a/src/Endpoints/class-analytics-posts-api-proxy.php b/src/Endpoints/class-analytics-posts-api-proxy.php index 61bc6e3d4..2b09be143 100644 --- a/src/Endpoints/class-analytics-posts-api-proxy.php +++ b/src/Endpoints/class-analytics-posts-api-proxy.php @@ -12,8 +12,11 @@ use stdClass; use WP_REST_Request; +use WP_Error; use Parsely\Parsely; +use function Parsely\Utils\get_date_format; + /** * Configures the `/stats/posts` REST API endpoint. */ @@ -23,15 +26,15 @@ final class Analytics_Posts_API_Proxy extends Base_API_Proxy { * Registers the endpoint's WP REST route. */ public function run(): void { - $this->register_endpoint( '/stats/posts', 'publish_posts' ); + $this->register_endpoint( '/stats/posts' ); } /** * Cached "proxy" to the Parse.ly `/analytics/posts` API endpoint. * * @param WP_REST_Request $request The request object. - * @return stdClass|WPError stdClass containing the data or a WP_Error - * object on failure. + * + * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. */ public function get_items( WP_REST_Request $request ) { return $this->get_data( $request ); @@ -40,28 +43,30 @@ public function get_items( WP_REST_Request $request ) { /** * Generates the final data from the passed response. * - * @param array $response The response received by the proxy. + * @param array $response The response received by the proxy. * @return array The generated data. */ - protected function generate_data( array $response ): array { - $date_format = get_option( 'date_format' ); - $stats_base_url = trailingslashit( Parsely::DASHBOARD_BASE_URL . '/' . $this->parsely->get_api_key() ) . 'find'; + protected function generate_data( $response ): array { + $date_format = get_date_format(); + $site_id = $this->parsely->get_site_id(); - $result = array_map( - static function( stdClass $item ) use ( $date_format, $stats_base_url ) { + return array_map( + static function( stdClass $item ) use ( $date_format, $site_id ) { return (object) array( - 'author' => $item->author, - 'date' => wp_date( $date_format, strtotime( $item->pub_date ) ), - 'id' => $item->url, - 'statsUrl' => $stats_base_url . '?url=' . rawurlencode( $item->url ), - 'title' => $item->title, - 'url' => $item->url, - 'views' => $item->metrics->views, + 'author' => $item->author, + 'dashUrl' => Parsely::get_dash_url( $site_id, $item->url ), + 'date' => wp_date( $date_format, strtotime( $item->pub_date ) ), + // Unique ID (can be replaced by Parse.ly API ID if it becomes available). + 'id' => $item->url, + // WordPress Post ID (0 if the post cannot be found, might not be unique). + 'postId' => url_to_postid( $item->url ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid + 'thumbUrlMedium' => $item->thumb_url_medium, + 'title' => $item->title, + 'url' => $item->url, + 'views' => $item->metrics->views, ); }, $response ); - - return $result; } } diff --git a/src/Endpoints/class-base-api-proxy.php b/src/Endpoints/class-base-api-proxy.php index 88b10531f..878906ab5 100644 --- a/src/Endpoints/class-base-api-proxy.php +++ b/src/Endpoints/class-base-api-proxy.php @@ -11,12 +11,14 @@ namespace Parsely\Endpoints; use Parsely\Parsely; -use Parsely\RemoteAPI\Proxy; +use Parsely\RemoteAPI\Remote_API_Interface; use stdClass; use WP_Error; use WP_REST_Request; use WP_REST_Server; +use function Parsely\Utils\convert_endpoint_to_filter_key; + /** * Configures a REST API endpoint for use. */ @@ -28,21 +30,12 @@ abstract class Base_API_Proxy { */ protected $parsely; - /** - * Capability of the user based on which we should allow access to endpoint. - * - * `null` should be used for all public endpoints. - * - * @var string|null - */ - protected $user_capability; - /** * Proxy object which does the actual calls to the Parse.ly API. * - * @var Proxy + * @var Remote_API_Interface */ - private $proxy; + private $api; /** * Registers the endpoint's WP REST route. @@ -52,17 +45,17 @@ abstract public function run(): void; /** * Generates the final data from the passed response. * - * @param array $response The response received by the proxy. + * @param array $response The response received by the proxy. * @return array The generated data. */ - abstract protected function generate_data( array $response ): array; + abstract protected function generate_data( $response ): array; /** * Cached "proxy" to the Parse.ly API endpoint. * * @param WP_REST_Request $request The request object. - * @return stdClass|WPError stdClass containing the data or a WP_Error - * object on failure. + * + * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. */ abstract public function get_items( WP_REST_Request $request ); @@ -72,43 +65,27 @@ abstract public function get_items( WP_REST_Request $request ); * @return bool */ public function permission_callback(): bool { - // This endpoint does not require any capability checks. - if ( is_null( $this->user_capability ) ) { - return true; - } - - // The user has the required capability to access this endpoint. - if ( current_user_can( $this->user_capability ) ) { - return true; - } - - return false; + return $this->api->is_user_allowed_to_make_api_call(); } /** * Constructor. * - * @param Parsely $parsely Instance of Parsely class. - * @param Proxy $proxy Proxy object which does the actual calls to the - * Parse.ly API. + * @param Parsely $parsely Instance of Parsely class. + * @param Remote_API_Interface $api API object which does the actual calls to the Parse.ly API. */ - public function __construct( Parsely $parsely, Proxy $proxy ) { + public function __construct( Parsely $parsely, Remote_API_Interface $api ) { $this->parsely = $parsely; - $this->proxy = $proxy; + $this->api = $api; } /** * Registers the endpoint's WP REST route. * - * @param string $endpoint The endpoint's route (e.g. /stats/posts). - * @param string|null $user_capability Capability of the user based on which we should allow access to endpoint. - * @param bool $show_in_index Show endpoint in /wp-json view if TRUE. + * @param string $endpoint The endpoint's route (e.g. /stats/posts). */ - protected function register_endpoint( string $endpoint, ?string $user_capability, $show_in_index = false ): void { - $this->user_capability = $user_capability; - - $filter_key = trim( str_replace( '/', '_', $endpoint ), '_' ); - if ( ! apply_filters( 'wp_parsely_enable_' . $filter_key . '_api_proxy', true ) ) { + protected function register_endpoint( string $endpoint ): void { + if ( ! apply_filters( 'wp_parsely_enable_' . convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) { return; } @@ -132,7 +109,7 @@ protected function register_endpoint( string $endpoint, ?string $user_capability 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'permission_callback' ), 'args' => $get_items_args, - 'show_in_index' => $show_in_index, + 'show_in_index' => $this->permission_callback(), ), ); @@ -147,14 +124,14 @@ protected function register_endpoint( string $endpoint, ?string $user_capability * required. * @param string $param_item The param element to use to * get the items. - * @return stdClass|WPError stdClass containing the data or a WP_Error - * object on failure. + * + * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. */ protected function get_data( WP_REST_Request $request, bool $require_api_secret = true, string $param_item = null ) { - if ( false === $this->parsely->api_key_is_set() ) { + if ( false === $this->parsely->site_id_is_set() ) { return new WP_Error( 'parsely_site_id_not_set', - __( 'A Parse.ly API Key must be set in site options to use this endpoint', 'wp-parsely' ), + __( 'A Parse.ly Site ID must be set in site options to use this endpoint', 'wp-parsely' ), array( 'status' => 403 ) ); } @@ -174,12 +151,14 @@ protected function get_data( WP_REST_Request $request, bool $require_api_secret } // A proxy with caching behavior is used here. - $response = $this->proxy->get_items( $params ); + $response = $this->api->get_items( $params ); // @phpstan-ignore-line. if ( is_wp_error( $response ) ) { return $response; } - return (object) array( 'data' => $this->generate_data( $response ) ); + return (object) array( + 'data' => $this->generate_data( $response ), // @phpstan-ignore-line. + ); } } diff --git a/src/Endpoints/class-graphql-metadata.php b/src/Endpoints/class-graphql-metadata.php index 893402341..0531beac0 100644 --- a/src/Endpoints/class-graphql-metadata.php +++ b/src/Endpoints/class-graphql-metadata.php @@ -37,7 +37,7 @@ public function run(): void { * * @param bool $enabled True if enabled, false if not. */ - if ( apply_filters( 'wp_parsely_enable_graphql_support', true ) && $this->parsely->api_key_is_set() ) { + if ( apply_filters( 'wp_parsely_enable_graphql_support', true ) && $this->parsely->site_id_is_set() ) { add_action( 'graphql_register_types', array( $this, 'register_meta' ) ); } } @@ -131,8 +131,7 @@ private function register_fields(): void { return null; } - $options = $this->parsely->get_options(); - $object_types = array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) ); + $object_types = $this->parsely->get_all_track_types(); $current_object_type = get_post_type( $post ); return array( diff --git a/src/Endpoints/class-referrers-post-detail-api-proxy.php b/src/Endpoints/class-referrers-post-detail-api-proxy.php index bf11c0c8c..bb140521e 100644 --- a/src/Endpoints/class-referrers-post-detail-api-proxy.php +++ b/src/Endpoints/class-referrers-post-detail-api-proxy.php @@ -12,6 +12,9 @@ use stdClass; use WP_REST_Request; +use WP_Error; + +use function Parsely\Utils\convert_to_positive_integer; /** * Configures the `/referrers/post/detail` REST API endpoint. @@ -32,7 +35,7 @@ final class Referrers_Post_Detail_API_Proxy extends Base_API_Proxy { * @since 3.6.0 */ public function run(): void { - $this->register_endpoint( '/referrers/post/detail', 'publish_posts' ); + $this->register_endpoint( '/referrers/post/detail' ); } /** @@ -41,11 +44,13 @@ public function run(): void { * @since 3.6.0 * * @param WP_REST_Request $request The request object. - * @return stdClass|WPError stdClass containing the data or a WP_Error - * object on failure. + * + * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. */ public function get_items( WP_REST_Request $request ) { - $this->total_views = absint( $request->get_param( 'total_views' ) ); + $this->total_views = convert_to_positive_integer( + strval( $request->get_param( 'total_views' ) ) + ); $request->offsetUnset( 'total_views' ); // Remove param from request. return $this->get_data( $request ); } @@ -55,20 +60,21 @@ public function get_items( WP_REST_Request $request ) { * * @since 3.6.0 * - * @param array $response The response received by the proxy. + * @param array $response The response received by the proxy. + * * @return array The generated data. */ - protected function generate_data( array $response ): array { + protected function generate_data( $response ): array { $referrers_types = $this->generate_referrer_types_data( $response ); - $direct_views = absint( preg_replace( '/\D/', '', $referrers_types->direct->views ) ); + $direct_views = convert_to_positive_integer( + $referrers_types->direct->views + ); $referrers_top = $this->generate_referrers_data( 5, $response, $direct_views ); - $result = array( + return array( 'top' => $referrers_top, 'types' => $referrers_types, ); - - return $result; } /** @@ -137,6 +143,7 @@ private function generate_referrer_types_data( array $response ): stdClass { } // Set percentage values and format numbers. + // @phpstan-ignore-next-line. foreach ( $result as $key => $value ) { // Set and format percentage values. $result->{ $key }->viewsPercentage = $this->get_i18n_percentage( @@ -193,7 +200,7 @@ private function generate_referrers_data( } // If applicable, add the direct views. - if ( $direct_views >= $referrer_views ) { + if ( isset( $referrer_views ) && $direct_views >= $referrer_views ) { $temp_views['direct'] = $direct_views; $totals += $direct_views; arsort( $temp_views ); @@ -210,6 +217,7 @@ private function generate_referrers_data( $result->totals = (object) array( 'views' => $totals ); // Set percentages values and format numbers. + // @phpstan-ignore-next-line. foreach ( $result as $key => $value ) { // Percentage against all referrer views, even those not included // in the dataset due to the $limit argument. diff --git a/src/Endpoints/class-related-api-proxy.php b/src/Endpoints/class-related-api-proxy.php index 9833d4b96..d4603c025 100644 --- a/src/Endpoints/class-related-api-proxy.php +++ b/src/Endpoints/class-related-api-proxy.php @@ -12,6 +12,7 @@ use stdClass; use WP_REST_Request; +use WP_Error; /** * Configures the `/related` REST API endpoint. @@ -22,15 +23,15 @@ final class Related_API_Proxy extends Base_API_Proxy { * Registers the endpoint's WP REST route. */ public function run(): void { - $this->register_endpoint( '/related', null, true ); + $this->register_endpoint( '/related' ); } /** * Cached "proxy" to the Parse.ly `/related` API endpoint. * * @param WP_REST_Request $request The request object. - * @return stdClass|WPError stdClass containing the data or a WP_Error - * object on failure. + * + * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. */ public function get_items( WP_REST_Request $request ) { return $this->get_data( $request, false, 'query' ); @@ -39,11 +40,11 @@ public function get_items( WP_REST_Request $request ) { /** * Generates the final data from the passed response. * - * @param array $response The response received by the proxy. + * @param array $response The response received by the proxy. * @return array The generated data. */ - protected function generate_data( array $response ): array { - $result = array_map( + protected function generate_data( $response ): array { + return array_map( static function( stdClass $item ) { return (object) array( 'image_url' => $item->image_url, @@ -54,7 +55,5 @@ static function( stdClass $item ) { }, $response ); - - return $result; } } diff --git a/src/Endpoints/class-rest-metadata.php b/src/Endpoints/class-rest-metadata.php index 056692cb7..5c276e815 100644 --- a/src/Endpoints/class-rest-metadata.php +++ b/src/Endpoints/class-rest-metadata.php @@ -35,7 +35,7 @@ public function run(): void { * * @param bool $enabled True if enabled, false if not. */ - if ( apply_filters( 'wp_parsely_enable_rest_api_support', true ) && $this->parsely->api_key_is_set() ) { + if ( apply_filters( 'wp_parsely_enable_rest_api_support', true ) && $this->parsely->site_id_is_set() ) { $this->register_meta(); } } @@ -46,8 +46,7 @@ public function run(): void { * @since 3.1.0 */ public function register_meta(): void { - $options = $this->parsely->get_options(); - $object_types = array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) ); + $object_types = $this->parsely->get_all_track_types(); /** * Filters the list of object types that the Parse.ly REST API is hooked into. @@ -67,13 +66,19 @@ public function register_meta(): void { * Function to get hooked into the `get_callback` property of the `parsely` * REST API field. It generates the `parsely` object in the REST API. * - * @param array $object The WordPress object to extract to render the metadata for, - * usually a post or a page. + * @param array $object The WordPress object to extract to render the metadata for, + * usually a post or a page. + * * @return array The `parsely` object to be rendered in the REST API. Contains a * version number describing the response and the `meta` object * containing the actual metadata. */ public function get_callback( array $object ): array { + /** + * Variable. + * + * @var int + */ $post_id = $object['ID'] ?? $object['id'] ?? 0; $post = WP_Post::get_instance( $post_id ); $options = $this->parsely->get_options(); diff --git a/src/Integrations/class-amp.php b/src/Integrations/class-amp.php index 1c5e378a1..8923066a7 100644 --- a/src/Integrations/class-amp.php +++ b/src/Integrations/class-amp.php @@ -10,12 +10,38 @@ namespace Parsely\Integrations; -use Parsely\Parsely; - /** * Integrates Parse.ly tracking with the AMP plugin. * * @since 2.6.0 Moved from Parsely class to this file. + * + * @phpstan-type Amp_Analytics array{ + * parsely: Parsely_Amp_Analytics, + * } + * + * @phpstan-type Amp_Native_Analytics array{ + * parsely: Parsely_Amp_Native_Analytics, + * } + * + * @phpstan-type Parsely_Amp_Analytics array{ + * type: string, + * attributes: array, + * config_data: Parsely_Amp_Config + * } + * + * @phpstan-type Parsely_Amp_Native_Analytics array{ + * type: string, + * attributes: array, + * config: string + * } + * + * @phpstan-type Parsely_Amp_Config array{ + * vars: Parsely_Amp_Config_Vars, + * } + * + * @phpstan-type Parsely_Amp_Config_Vars array{ + * apikey: string, + * } */ class Amp extends Integration { /** @@ -55,7 +81,7 @@ public function is_amp_request(): bool { public function can_handle_amp_request(): bool { $options = self::$parsely->get_options(); - return $this->is_amp_request() && is_array( $options ) && ! $options['disable_amp']; + return $this->is_amp_request() && ! $options['disable_amp']; } /** @@ -75,8 +101,8 @@ public function add_actions(): void { * * @since 2.6.0 * - * @param array|null $analytics The analytics registry. - * @return array The analytics registry. + * @param array|null $analytics The analytics registry. + * @return array The analytics registry. */ public function register_parsely_for_amp_analytics( ?array $analytics ): array { if ( null === $analytics ) { @@ -102,8 +128,9 @@ public function register_parsely_for_amp_analytics( ?array $analytics ): array { * * @since 2.6.0 * - * @param array|null $analytics The analytics registry. - * @return array The analytics registry. + * @param array|null $analytics The analytics registry. + * + * @return Amp_Analytics|array The analytics registry. */ public function register_parsely_for_amp_native_analytics( ?array $analytics ): array { if ( null === $analytics ) { @@ -112,7 +139,7 @@ public function register_parsely_for_amp_native_analytics( ?array $analytics ): $options = self::$parsely->get_options(); - if ( isset( $options['disable_amp'] ) && true === $options['disable_amp'] ) { + if ( true === $options['disable_amp'] ) { return $analytics; } @@ -154,18 +181,17 @@ public static function construct_amp_json(): string { * consists of the site's Site ID if that's defined, or an empty array * otherwise. * + * @link https://docs.parse.ly/google-amp/ * @since 3.2.0 * * @return array> */ public static function construct_amp_config(): array { - $options = self::$parsely->get_options(); - - if ( isset( $options['apikey'] ) && is_string( $options['apikey'] ) && '' !== $options['apikey'] ) { + if ( self::$parsely->site_id_is_set() ) { return array( 'vars' => array( // This field will be rendered in a JS context. - 'apikey' => esc_js( $options['apikey'] ), + 'apikey' => esc_js( self::$parsely->get_site_id() ), ), ); } diff --git a/src/Integrations/class-facebook-instant-articles.php b/src/Integrations/class-facebook-instant-articles.php index 5e7c6e547..89c71e98c 100644 --- a/src/Integrations/class-facebook-instant-articles.php +++ b/src/Integrations/class-facebook-instant-articles.php @@ -16,6 +16,15 @@ * Integrates Parse.ly tracking with the Facebook Instant Articles plugin. * * @since 2.6.0 Moved from Parsely class to this file. + * + * @phpstan-type FB_Instant_Articles_Registry array{ + * 'parsely-analytics-for-wordpress'?: FB_Parsely_Registry, + * } + * + * @phpstan-type FB_Parsely_Registry array{ + * name: string, + * payload: string, + * } */ final class Facebook_Instant_Articles extends Integration { private const REGISTRY_IDENTIFIER = 'parsely-analytics-for-wordpress'; @@ -38,17 +47,17 @@ public function integrate(): void { * * @since 2.6.0 * - * @param array $registry The registry info for Facebook Instant Articles. + * @param FB_Instant_Articles_Registry $registry The registry info for Facebook Instant Articles. */ public function insert_parsely_tracking( &$registry ): void { $parsely = new Parsely(); - if ( $parsely->api_key_is_missing() ) { + if ( $parsely->site_id_is_missing() ) { return; } $registry[ self::REGISTRY_IDENTIFIER ] = array( 'name' => self::REGISTRY_DISPLAY_NAME, - 'payload' => $this->get_embed_code( $parsely->get_api_key() ), + 'payload' => $this->get_embed_code( $parsely->get_site_id() ), ); } @@ -57,10 +66,10 @@ public function insert_parsely_tracking( &$registry ): void { * * @since 2.6.0 * - * @param string $api_key API key. + * @param string $site_id Site ID. * @return string Embedded code. */ - private function get_embed_code( string $api_key ): string { + private function get_embed_code( string $site_id ): string { return ' - '; + '; } } diff --git a/src/Integrations/class-google-web-stories.php b/src/Integrations/class-google-web-stories.php index 8860337b2..4d1809f5c 100644 --- a/src/Integrations/class-google-web-stories.php +++ b/src/Integrations/class-google-web-stories.php @@ -43,7 +43,7 @@ public function render_amp_analytics_tracker(): void { '; } else { // Assume `meta_type` is `repeated_metas`. - $parsely_post_type = $this->parsely->convert_jsonld_to_parsely_type( $metadata['@type'] ); + $parsely_post_type = $this->parsely->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ); if ( isset( $metadata['keywords'] ) && is_array( $metadata['keywords'] ) ) { $metadata['keywords'] = implode( ',', $metadata['keywords'] ); } $parsely_metas = array( - 'title' => $metadata['headline'] ?? null, + 'title' => $metadata['headline'], 'link' => $metadata['url'] ?? null, 'type' => $parsely_post_type, 'image-url' => $metadata['thumbnailUrl'] ?? null, diff --git a/src/UI/class-network-admin-sites-list.php b/src/UI/class-network-admin-sites-list.php index 533024357..7f0845567 100644 --- a/src/UI/class-network-admin-sites-list.php +++ b/src/UI/class-network-admin-sites-list.php @@ -11,7 +11,6 @@ namespace Parsely\UI; use Parsely\Parsely; -use WP_Site; /** * Renders the additions to the WordPress Multisite Network Admin Sites List @@ -20,7 +19,13 @@ * @since 3.2.0 */ final class Network_Admin_Sites_List { - const COLUMN_NAME = 'parsely-api-key'; + const COLUMN_NAME = 'parsely-site-id'; + /** + * Instance of Parsely class. + * + * @var Parsely + */ + private $parsely; /** * Constructor. @@ -39,8 +44,8 @@ public function __construct( Parsely $parsely ) { */ public function run(): void { add_filter( 'manage_sites_action_links', array( __CLASS__, 'add_action_link' ), 10, 2 ); - add_filter( 'wpmu_blogs_columns', array( __CLASS__, 'add_api_key_column' ) ); - add_action( 'manage_sites_custom_column', array( $this, 'populate_api_key_column' ), 10, 2 ); + add_filter( 'wpmu_blogs_columns', array( __CLASS__, 'add_site_id_column' ) ); + add_action( 'manage_sites_custom_column', array( $this, 'populate_site_id_column' ), 10, 2 ); } /** @@ -49,10 +54,11 @@ public function run(): void { * * @since 3.2.0 * - * @param array $actions The list of actions meant to be displayed for the current site's - * context in the row actions. - * @param int $_blog_id The blog ID for the current context. - * @return array The list of actions including ours. + * @param array $actions The list of actions meant to be displayed for the current site's + * context in the row actions. + * @param int $_blog_id The blog ID for the current context. + * + * @return array The list of actions including ours. */ public static function add_action_link( array $actions, int $_blog_id ): array { if ( ! current_user_can( Parsely::CAPABILITY ) ) { @@ -78,12 +84,13 @@ public static function add_action_link( array $actions, int $_blog_id ): array { * @return string ARIA label content including the blogname. */ private static function generate_aria_label_for_blog_id( int $_blog_id ): string { - $site = get_blog_details( $_blog_id ); + $site = get_blog_details( $_blog_id ); + $blogname = false === $site ? '' : $site->blogname; return sprintf( /* translators: blog name or blog id if empty */ __( 'Go to Parse.ly stats for "%s"', 'wp-parsely' ), - empty( $site->blogname ) ? $_blog_id : $site->blogname + '' === $blogname ? $_blog_id : $blogname ); } @@ -93,11 +100,11 @@ private static function generate_aria_label_for_blog_id( int $_blog_id ): string * * @since 3.2.0 * - * @param array $sites_columns The list of columns meant to be displayed in the sites list table. - * @return array The list of columns to display in the network admin table including ours. + * @param array $sites_columns The list of columns meant to be displayed in the sites list table. + * @return array The list of columns to display in the network admin table including ours. */ - public static function add_api_key_column( array $sites_columns ): array { - $sites_columns[ self::COLUMN_NAME ] = __( 'Parse.ly API Key', 'wp-parsely' ); + public static function add_site_id_column( array $sites_columns ): array { + $sites_columns[ self::COLUMN_NAME ] = __( 'Parse.ly Site ID', 'wp-parsely' ); return $sites_columns; } @@ -110,20 +117,20 @@ public static function add_api_key_column( array $sites_columns ): array { * @param string $column_name The column name for the current context. * @param int $_blog_id The blog ID for the current context. */ - public function populate_api_key_column( string $column_name, int $_blog_id ): void { + public function populate_site_id_column( string $column_name, int $_blog_id ): void { if ( self::COLUMN_NAME !== $column_name ) { return; } // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.switch_to_blog_switch_to_blog switch_to_blog( $_blog_id ); - $apikey = $this->parsely->get_api_key(); + $site_id = $this->parsely->get_site_id(); restore_current_blog(); - if ( strlen( $apikey ) > 0 ) { - echo esc_html( $apikey ); + if ( strlen( $site_id ) > 0 ) { + echo esc_html( $site_id ); } else { - echo '' . esc_html__( 'Parse.ly API key is missing', 'wp-parsely' ) . ''; + echo '' . esc_html__( 'Parse.ly Site ID is missing', 'wp-parsely' ) . ''; } } } diff --git a/src/UI/class-plugins-actions.php b/src/UI/class-plugins-actions.php index 989b58d64..6e98d77a7 100644 --- a/src/UI/class-plugins-actions.php +++ b/src/UI/class-plugins-actions.php @@ -31,10 +31,10 @@ public function run(): void { /** * Adds a 'Settings' action link to the Plugins screen in WP admin. * - * @param array $actions An array of plugin action links. By default, this can include 'activate', - * 'deactivate', and 'delete'. With Multisite active this can also include - * 'network_active' and 'network_only' items. - * @return array + * @param array $actions An array of plugin action links. By default, this can include 'activate', + * 'deactivate', and 'delete'. With Multisite active this can also include + * 'network_active' and 'network_only' items. + * @return array */ public function add_plugin_meta_links( array $actions ): array { $link_pattern = '%s'; diff --git a/src/UI/class-recommended-widget.php b/src/UI/class-recommended-widget.php index 1c28571f7..512b44c91 100644 --- a/src/UI/class-recommended-widget.php +++ b/src/UI/class-recommended-widget.php @@ -18,6 +18,18 @@ /** * Provides a widget with Parse.ly recommended articles. + * + * @phpstan-type Widget_Settings array{ + * title: string, + * return_limit: int, + * display_direction: string, + * published_within: int, + * sort: string, + * boost: string, + * personalize_results: bool, + * img_src: string, + * display_author: bool, + * } */ final class Recommended_Widget extends WP_Widget { /** @@ -27,6 +39,23 @@ final class Recommended_Widget extends WP_Widget { */ private $parsely; + /** + * Default values of widget settings + * + * @var Widget_Settings + */ + private static $default_widget_settings = array( + 'title' => '', + 'return_limit' => 5, + 'display_direction' => 'vertical', + 'published_within' => 0, + 'sort' => 'score', + 'boost' => 'views', + 'personalize_results' => false, + 'img_src' => 'parsely_thumb', + 'display_author' => false, + ); + /** * Constructor. * @@ -54,7 +83,7 @@ public function __construct( Parsely $parsely ) { * * @since 2.5.0 * - * @param string $api_key Publisher Site ID (API key). + * @param string $site_id Publisher Site ID. * @param int|null $published_within Publication filter start date; see https://www.parse.ly/help/api/time for * formatting details. No restriction by default. * @param string|null $sort What to sort the results by. There are currently 2 valid options: `score`, @@ -65,11 +94,11 @@ public function __construct( Parsely $parsely ) { * @param int $return_limit Number of records to retrieve; defaults to "10". * @return string API URL. */ - private function get_api_url( string $api_key, ?int $published_within, ?string $sort, ?string $boost, int $return_limit ): string { - $related_api_endpoint = 'https://api.parsely.com/v2/related'; + private function get_api_url( string $site_id, ?int $published_within, ?string $sort, ?string $boost, int $return_limit ): string { + $related_api_endpoint = Parsely::PUBLIC_API_BASE_URL . '/related'; $query_args = array( - 'apikey' => $api_key, + 'apikey' => $site_id, 'sort' => $sort, 'limit' => $return_limit, ); @@ -88,14 +117,15 @@ private function get_api_url( string $api_key, ?int $published_within, ?string $ /** * This is the widget function. * - * @param array $args Widget Arguments. - * @param array $instance Values saved to the db. + * @param array $args Widget Arguments. + * @param array $widget_settings Values saved to the db. */ - public function widget( $args, $instance ): void { - if ( ! $this->api_key_and_secret_are_populated() ) { + public function widget( $args, $widget_settings ): void /* @phpstan-ignore-line */ { + if ( ! $this->site_id_and_secret_are_populated() ) { return; } + $instance = $this->get_widget_settings( $widget_settings ); $removed_title_esc = remove_filter( 'widget_title', 'esc_html' ); /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */ @@ -109,36 +139,35 @@ public function widget( $args, $instance ): void { echo wp_kses_post( $title_html ); // Set up the variables. - $options = $this->parsely->get_options(); $api_url = $this->get_api_url( - $options['apikey'], + $this->parsely->get_site_id(), $instance['published_within'], $instance['sort'], $instance['boost'], - (int) $instance['return_limit'] + $instance['return_limit'] ); - $recommended_widget_script_asset = require plugin_dir_path( PARSELY_FILE ) . 'build/recommended-widget.asset.php'; - ?> $current_settings Values saved to the db. */ - public function form( $instance ): void { - if ( ! $this->api_key_and_secret_are_populated() ) { + public function form( $current_settings ): string { + if ( ! $this->site_id_and_secret_are_populated() ) { $settings_page_url = add_query_arg( 'page', 'parsely', get_admin_url() . 'options-general.php' ); $message = sprintf( @@ -172,19 +201,21 @@ public function form( $instance ): void { echo '

', wp_kses_post( $message ), '

'; - return; + return ''; } + $instance = $this->get_widget_settings( $current_settings ); + // editable fields: title. - $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; - $return_limit = ! empty( $instance['return_limit'] ) ? (int) $instance['return_limit'] : 5; - $display_direction = ! empty( $instance['display_direction'] ) ? $instance['display_direction'] : 'vertical'; - $published_within = ! empty( $instance['published_within'] ) ? $instance['published_within'] : 0; - $sort = ! empty( $instance['sort'] ) ? $instance['sort'] : 'score'; - $boost = ! empty( $instance['boost'] ) ? $instance['boost'] : 'views'; - $personalize_results = ! empty( $instance['personalize_results'] ) ? $instance['personalize_results'] : false; - $img_src = ! empty( $instance['img_src'] ) ? $instance['img_src'] : 'parsely_thumb'; - $display_author = ! empty( $instance['display_author'] ) ? $instance['display_author'] : false; + $title = $instance['title']; + $return_limit = $instance['return_limit']; + $display_direction = $instance['display_direction']; + $published_within = $instance['published_within']; + $sort = $instance['sort']; + $boost = $instance['boost']; + $personalize_results = $instance['personalize_results']; + $img_src = $instance['img_src']; + $display_author = $instance['display_author']; $instance['return_limit'] = $return_limit; $instance['display_direction'] = $display_direction; @@ -210,7 +241,7 @@ class="tiny-text" aria-labelledby="get_field_id( 'pu

- +

@@ -259,26 +290,30 @@ class="tiny-text" aria-labelledby="get_field_id( 'pu

parsely->get_options(); - - // No options are saved, so API key is not available. - if ( ! is_array( $options ) ) { - return false; - } - - // Parse.ly Site ID settings field is not populated. - if ( ! array_key_exists( 'apikey', $options ) || '' === $options['apikey'] ) { - return false; - } - - // Parse.ly API Secret settings field is not populated. - if ( ! array_key_exists( 'api_secret', $options ) || '' === $options['api_secret'] ) { - return false; - } + private function site_id_and_secret_are_populated(): bool { + return $this->parsely->site_id_is_set() && $this->parsely->api_secret_is_set(); + } - return true; + /** + * Returns all widget settings by assigning defaults if a setting isn't present + * + * @since 3.7.0 + * + * @param array $settings Widget Options. + * + * @return Widget_Settings + */ + public function get_widget_settings( array $settings ) { + /** + * Variable. + * + * @var Widget_Settings + */ + $widget_settings = $settings; + + return array_merge( self::$default_widget_settings, $widget_settings ); } } diff --git a/src/UI/class-row-actions.php b/src/UI/class-row-actions.php index e2c56c1df..f571dbc0d 100644 --- a/src/UI/class-row-actions.php +++ b/src/UI/class-row-actions.php @@ -77,7 +77,7 @@ public function row_actions_add_parsely_link( array $actions, WP_Post $post ): a return $actions; } - $url = Dashboard_Link::generate_url( $post, $this->parsely->get_api_key(), 'wp-admin-posts-list', 'wp-admin' ); + $url = Dashboard_Link::generate_url( $post, $this->parsely->get_site_id(), 'wp-admin-posts-list', 'wp-admin' ); if ( '' !== $url ) { $actions['find_in_parsely'] = $this->generate_link_to_parsely( $post, $url ); } diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php index 1a9b8bf6c..03f33dd02 100644 --- a/src/UI/class-settings-page.php +++ b/src/UI/class-settings-page.php @@ -19,6 +19,20 @@ * Renders the wp-admin Parse.ly plugin settings page. * * @since 3.0.0 + * + * @phpstan-import-type Parsely_Options from Parsely + * + * @phpstan-type Setting_Arguments array{ + * option_key: string, + * label_for: string, + * title?: string, + * help_text?: string, + * yes_text?: string, + * filter?: string, + * optional_args?: array, + * select_options?: array, + * radio_options?: array, + * } */ final class Settings_Page { /** @@ -84,22 +98,22 @@ public function enqueue_settings_assets( string $hook_suffix ): void { add_filter( 'media_library_months_with_files', '__return_empty_array' ); wp_enqueue_media(); - $admin_settings_asset = require plugin_dir_path( PARSELY_FILE ) . 'build/admin-settings.asset.php'; + $admin_settings_asset = require_once plugin_dir_path( PARSELY_FILE ) . 'build/admin-settings.asset.php'; $built_assets_url = plugin_dir_url( PARSELY_FILE ) . '/build/'; wp_enqueue_script( 'parsely-admin-settings', $built_assets_url . 'admin-settings.js', - $admin_settings_asset['dependencies'], - $admin_settings_asset['version'], + $admin_settings_asset['dependencies'] ?? null, + $admin_settings_asset['version'] ?? Parsely::VERSION, true ); wp_enqueue_style( 'parsely-admin-settings', $built_assets_url . 'admin-settings.css', - $admin_settings_asset['dependencies'], - $admin_settings_asset['version'] + $admin_settings_asset['dependencies'] ?? null, + $admin_settings_asset['version'] ?? Parsely::VERSION ); } } @@ -182,9 +196,9 @@ public function add_screen_options(): void { * @param string $screen_settings Screen settings. * @param WP_Screen $screen WP_Screen object. * - * @return string The filtered screen settings. + * @return string|false The filtered screen settings. */ - public function screen_settings( string $screen_settings, WP_Screen $screen ): string { + public function screen_settings( string $screen_settings, WP_Screen $screen ) { if ( $this->hook_suffix !== $screen->base ) { return $screen_settings; } @@ -194,6 +208,11 @@ public function screen_settings( string $screen_settings, WP_Screen $screen ): s return $screen_settings; } + /** + * Variable. + * + * @var array + */ $user_meta = get_user_meta( get_current_user_id(), $this->screen_options_name, true ); ob_start(); @@ -262,7 +281,7 @@ public function display_settings(): void { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-parsely' ) ); } - include plugin_dir_path( PARSELY_FILE ) . 'views/parsely-settings.php'; + include_once plugin_dir_path( PARSELY_FILE ) . 'views/parsely-settings.php'; } /** @@ -358,6 +377,7 @@ private function initialize_basic_section(): void { // Metadata Format. $field_id = 'meta_type'; $field_args = array( + 'title' => __( 'Metadata Format', 'wp-parsely' ), 'option_key' => $field_id, 'help_text' => __( 'Choose the metadata format for our crawlers to access. Most publishers are fine with JSON-LD, but if you prefer to use our proprietary metadata format then you can do so here.', 'wp-parsely' ), 'radio_options' => array( @@ -427,7 +447,7 @@ private function initialize_basic_section(): void { 'option_key' => 'disable_javascript', 'radio_options' => array( 'true' => __( 'Yes, disable JavaScript tracking. I want to use a separate system for tracking instead of the Parse.ly plugin.', 'wp-parsely' ), - 'false' => __( 'No, do not disable JavaScript tracking. I want to the Parse.ly plugin to load the tracker.', 'wp-parsely' ), + 'false' => __( 'No, do not disable JavaScript tracking. I want the Parse.ly plugin to load the tracker.', 'wp-parsely' ), ), 'help_text' => __( 'WARNING: We highly recommend choosing "No." Disabling the JavaScript tracker will also disable the "Personalize Results" section of the recommendation widget.', 'wp-parsely' ), 'filter' => 'wp_parsely_load_js_tracker', @@ -468,7 +488,7 @@ function (): void { printf( /* translators: Mailto link */ esc_html__( 'Once you have changed a value and and saved, please contact %s to request a recrawl.', 'wp-parsely' ), - wp_kses_post( 'support@parsely.com' ) + wp_kses_post( 'support@parsely.com' ) ); }, Parsely::MENU_SLUG @@ -659,9 +679,9 @@ private function initialize_advanced_section(): void { * * @since 3.4.0 * - * @param array $args The arguments for the form field. May contain 'filter'. + * @param Setting_Arguments $args The arguments for the form field. May contain 'filter'. */ - private function print_filter_text( array $args ): void { + private function print_filter_text( $args ): void { if ( isset( $args['filter'] ) && has_filter( $args['filter'] ) ) { echo '

'; echo '' . esc_html( $args['filter'] ) . '' . esc_html__( 'filter hook is in use!', 'wp-parsely' ) . ' '; @@ -675,20 +695,25 @@ private function print_filter_text( array $args ): void { * * @since 3.1.0 * - * @param array $args The arguments for the form field. May contain 'help_text'. + * @param Setting_Arguments $args The arguments for the form field. May contain 'help_text'. */ - private function print_description_text( array $args ): void { + private function print_description_text( $args ): void { echo isset( $args['help_text'] ) ? '

' . wp_kses_post( $args['help_text'] ) . '

' : ''; } /** * Prints out an input text tag. * - * @param array $args The arguments for text tag. + * @param Setting_Arguments $args The arguments for text tag. */ - public function print_text_tag( array $args ): void { - $options = $this->parsely->get_options(); - $name = $args['option_key']; + public function print_text_tag( $args ): void { + $options = $this->parsely->get_options(); + $name = $args['option_key']; + /** + * Variable. + * + * @var string + */ $value = $options[ $name ] ?? ''; $optional_args = $args['optional_args'] ?? array(); $id = esc_attr( $name ); @@ -697,9 +722,11 @@ public function print_text_tag( array $args ): void { $accepted_args = array( 'placeholder', 'required' ); echo sprintf( " $val ) { if ( \in_array( $key, $accepted_args, true ) ) { echo ' ' . esc_attr( $key ) . '="' . esc_attr( $val ) . '"'; @@ -713,18 +740,18 @@ public function print_text_tag( array $args ): void { /** * Prints a checkbox tag in the settings page. * - * @param array $args Arguments to print to checkbox tag. + * @param Setting_Arguments $args Arguments to print to checkbox tag. */ - public function print_checkbox_tag( array $args ): void { + public function print_checkbox_tag( $args ): void { $options = $this->parsely->get_options(); $name = $args['option_key']; $value = $options[ $name ]; $id = esc_attr( $name ); $name = Parsely::OPTIONS_KEY . "[$id]"; - $yes_text = $args['yes_text']; + $yes_text = $args['yes_text'] ?? ''; echo sprintf( "parsely->get_options(); $name = $args['option_key']; - $select_options = $args['select_options']; + $select_options = $args['select_options'] ?? array(); $selected = $options[ $name ] ?? null; $id = esc_attr( $name ); $name = Parsely::OPTIONS_KEY . "[$id]"; echo sprintf( "parsely->get_options()[ $key ]; $input_name = Parsely::OPTIONS_KEY . "[$key]"; $button_text = __( 'Browse', 'wp-parsely' ); ?>
- +
@@ -831,17 +866,23 @@ public function print_media_single_image( array $args ): void { * * @since 3.2.0 * - * @param array $args The arguments used in the output HTML elements. + * @param Setting_Arguments $args The arguments used in the output HTML elements. */ - public function print_track_post_types_table( array $args ): void { + public function print_track_post_types_table( $args ): void { $option_key = esc_attr( $args['option_key'] ); + $title = $args['title'] ?? ''; + /** + * Variable. + * + * @var array + */ $post_types = get_post_types( array( 'public' => true ) ); $values = $this->get_tracking_values_for_display(); ?>
- + - + @@ -892,7 +933,7 @@ public function print_track_post_types_table( array $args ): void { * * @since 3.2.0 * - * @return array Key-value pairs with post type and their 'track as' value. + * @return array Key-value pairs with post type and their 'track as' value. */ public function get_tracking_values_for_display(): array { $options = $this->parsely->get_options(); @@ -900,11 +941,10 @@ public function get_tracking_values_for_display(): array { $result = array(); foreach ( $types as $type ) { - $array_key = "track_{$type}_types"; - if ( array_key_exists( $array_key, $options ) ) { - foreach ( $options[ $array_key ] as $post_type ) { - $result[ $post_type ] = $type; - } + $array_value = $options[ "track_{$type}_types" ]; + + foreach ( $array_value as $post_type ) { + $result[ $post_type ] = $type; } } @@ -914,41 +954,44 @@ public function get_tracking_values_for_display(): array { /** * Validates the options provided by the user. * - * @param array $input Options from the settings page. - * @return array List of validated input settings. + * @param Parsely_Options $input Options from the settings page. + * + * @return Parsely_Options List of validated input settings. */ - public function validate_options( array $input ): array { + public function validate_options( $input ) { $options = $this->parsely->get_options(); - if ( empty( $input['apikey'] ) ) { + if ( '' === $input['apikey'] ) { add_settings_error( Parsely::OPTIONS_KEY, 'apikey', __( 'Please specify the Site ID', 'wp-parsely' ) ); } else { - $api_key = $this->sanitize_api_key( $input['apikey'] ); - if ( false === $this->validate_api_key( $api_key ) ) { + $site_id = $this->sanitize_site_id( $input['apikey'] ); + if ( false === $this->validate_site_id( $site_id ) ) { add_settings_error( Parsely::OPTIONS_KEY, 'apikey', __( 'Your Parse.ly Site ID looks incorrect, it should look like "example.com".', 'wp-parsely' ) ); } else { - $input['apikey'] = $api_key; + $input['apikey'] = $site_id; } } $input['api_secret'] = sanitize_text_field( $input['api_secret'] ); - if ( ! empty( $input['metadata_secret'] ) ) { + if ( '' !== $input['metadata_secret'] ) { if ( strlen( $input['metadata_secret'] ) !== 10 ) { add_settings_error( Parsely::OPTIONS_KEY, 'metadata_secret', __( 'Metadata secret is incorrect. Please contact Parse.ly support!', 'wp-parsely' ) ); - } elseif ( isset( $input['parsely_wipe_metadata_cache'] ) && 'true' === $input['parsely_wipe_metadata_cache'] ) { + } elseif ( + isset( $input['parsely_wipe_metadata_cache'] ) && 'true' === $input['parsely_wipe_metadata_cache'] // @phpstan-ignore-line + ) { delete_post_meta_by_key( 'parsely_metadata_last_updated' ); wp_schedule_event( time() + 100, 'everytenminutes', 'parsely_bulk_metas_update' ); @@ -956,7 +999,7 @@ public function validate_options( array $input ): array { } } - if ( empty( $input['logo'] ) ) { + if ( '' === $input['logo'] ) { $input['logo'] = self::get_logo_default(); } @@ -1121,7 +1164,7 @@ public function validate_options( array $input ): array { } /** - * Validates the passed API key. + * Validates the passed Site ID. * * Accepts a www prefix and up to 3 periods. * @@ -1133,25 +1176,25 @@ public function validate_options( array $input ): array { * * @since 3.3.0 * - * @param string $api_key The API key to be validated. + * @param string $site_id The Site ID to be validated. * @return bool */ - private function validate_api_key( string $api_key ): bool { + private function validate_site_id( string $site_id ): bool { $key_format = '/^((\w+)\.)?(([\w-]+)?)(\.[\w-]+){1,2}$/'; - return 1 === preg_match( $key_format, $api_key ); + return 1 === preg_match( $key_format, $site_id ); } /** - * Sanitizes the passed API key. + * Sanitizes the passed Site ID. * * @since 3.3.0 * - * @param string $api_key The API key to be sanitized. + * @param string $site_id The Site ID to be sanitized. * @return string */ - private function sanitize_api_key( string $api_key ): string { - return strtolower( sanitize_text_field( $api_key ) ); + private function sanitize_site_id( string $site_id ): string { + return strtolower( sanitize_text_field( $site_id ) ); } /** @@ -1162,9 +1205,9 @@ private function sanitize_api_key( string $api_key ): string { * * @since 3.2.0 * - * @param array $input Array passed to validate_options() function. + * @param Parsely_Options $input Array passed to validate_options() function. */ - private function validate_options_post_type_tracking( array &$input ): void { + private function validate_options_post_type_tracking( &$input ): void { $options = $this->parsely->get_options(); $posts = 'track_post_types'; $pages = 'track_page_types'; @@ -1206,10 +1249,15 @@ private function validate_options_post_type_tracking( array &$input ): void { * @return string */ private static function get_logo_default(): string { + /** + * Variable. + * + * @var int + */ $custom_logo_id = get_theme_mod( 'custom_logo' ); - if ( $custom_logo_id ) { + if ( (bool) $custom_logo_id ) { $logo_attrs = wp_get_attachment_image_src( $custom_logo_id, 'full' ); - if ( $logo_attrs ) { + if ( isset( $logo_attrs[0] ) ) { return $logo_attrs[0]; } } @@ -1222,8 +1270,8 @@ private static function get_logo_default(): string { /** * Sanitizes all elements in an option array. * - * @param array $array Array of options to be sanitized. - * @return array + * @param array $array Array of options to be sanitized. + * @return array */ private static function sanitize_option_array( array $array ): array { $new_array = $array; diff --git a/src/UI/class-site-health.php b/src/UI/class-site-health.php index d3364c6eb..b7c144b87 100644 --- a/src/UI/class-site-health.php +++ b/src/UI/class-site-health.php @@ -18,6 +18,17 @@ * Provides debug information about the plugin. * * @since 3.4.0 + * + * @phpstan-type Site_Health_Info array{ + * parsely?: Parsely_Health_Info + * } + * + * @phpstan-type Parsely_Health_Info array{ + * label: string, + * description: string, + * show_count: bool, + * fields: array, + * } */ final class Site_Health { /** @@ -42,7 +53,7 @@ public function __construct( Parsely $parsely ) { * @since 3.4.0 */ public function run(): void { - add_filter( 'site_status_tests', array( $this, 'check_api_key' ) ); + add_filter( 'site_status_tests', array( $this, 'check_site_id' ) ); add_filter( 'debug_information', array( $this, 'options_debug_info' ) ); } @@ -53,9 +64,9 @@ public function run(): void { * * @param array $tests An associative array of direct and asynchronous tests. * - * @return array + * @return array */ - public function check_api_key( array $tests ): array { + public function check_site_id( array $tests ): array { $test = function() { $result = array( 'label' => __( 'The Site ID is correctly set up', 'wp-parsely' ), @@ -71,7 +82,7 @@ public function check_api_key( array $tests ): array { 'test' => 'loopback_requests', ); - if ( $this->parsely->api_key_is_missing() ) { + if ( $this->parsely->site_id_is_missing() ) { $result['status'] = 'critical'; $result['label'] = __( 'You need to provide the Site ID', 'wp-parsely' ); $result['actions'] = __( 'The site ID can be set in the Parse.ly Settings Page.', 'wp-parsely' ); @@ -80,7 +91,13 @@ public function check_api_key( array $tests ): array { return $result; }; - $tests['direct']['parsely'] = array( + /** + * Variable. + * + * @var array + */ + $direct = $tests['direct']; + $direct['parsely'] = array( 'label' => __( 'Parse.ly Site ID', 'wp-parsely' ), 'test' => $test, ); @@ -93,11 +110,11 @@ public function check_api_key( array $tests ): array { * * @since 3.4.0 * - * @param array $args The debug information to be added to the core information page. + * @param Site_Health_Info $args The debug information to be added to the core information page. * - * @return array + * @return Site_Health_Info */ - public function options_debug_info( array $args ): array { + public function options_debug_info( $args ) { $options = $this->parsely->get_options(); $args['parsely'] = array( diff --git a/src/Utils/utils.php b/src/Utils/utils.php new file mode 100644 index 000000000..4c0bc90bf --- /dev/null +++ b/src/Utils/utils.php @@ -0,0 +1,247 @@ +format( $number ); + + if ( false === $formatted_number ) { + return ''; + } + + return $formatted_number; +} + +/** + * Gets time in formatted form. + * + * Example: + * - Input `1000` (seconds) and Output `16:40` which represents "16 minutes, 40 seconds” + * + * @since 3.7.0 + * + * @param int|float $seconds Time in seconds that we have to format. + * + * @return string + */ +function get_formatted_time( $seconds ): string { + $time_formatter = new NumberFormatter( 'en_US', NumberFormatter::DURATION ); + $formatted_time = $time_formatter->format( $seconds ); + + if ( false === $formatted_time ) { + return ''; + } + + return $formatted_time; +} + +/** + * Converts to associate array. + * + * @since 3.7.0 + * + * @param mixed $obj Input object. + * + * @return array|WP_Error + */ +function convert_to_associative_array( $obj ) { + $encoded = wp_json_encode( $obj ); + + if ( false === $encoded ) { + return new WP_Error( 'parsely_encoding_failed', __( 'Unable to encode API response for associative array', 'wp-parsely' ) ); + } + + /** + * Variable. + * + * @var array + */ + return json_decode( $encoded, true ); +} + +/** + * Converts a string to a positive integer, removing any non-numeric + * characters. + * + * @param string $string The string to be converted to an integer. + * @return int The integer resulting from the conversion. + */ +function convert_to_positive_integer( string $string ): int { + return (int) preg_replace( '/\D/', '', $string ); +} + +/** + * Converts endpoint to filter key by replacing `/` with `_`. + * + * @param string $endpoint Route of the endpoint. + * + * @since 3.7.0 + * + * @return string + */ +function convert_endpoint_to_filter_key( string $endpoint ): string { + return trim( str_replace( '/', '_', $endpoint ), '_' ); +} diff --git a/src/blocks/content-helper/class-content-helper.php b/src/blocks/content-helper/class-content-helper.php index 24c5afae2..e5f10d97c 100644 --- a/src/blocks/content-helper/class-content-helper.php +++ b/src/blocks/content-helper/class-content-helper.php @@ -1,6 +1,6 @@ span { - color: $gray-600; - margin-bottom: 0; - display: flex; + .parsely-top-post-view-link, + .parsely-top-post-edit-link { + display: inline-block; + width: 16px; + height: 16px; + position: relative; + margin-right: to_rem(3px); + + svg { + position: absolute; + top: 2px; + fill: #8d98a1; + } - &:not(:first-child) { - margin-left: to_rem(5px); + &:hover svg { + fill: var(--blue-550); } } -} -.parsely-top-post-views svg { - position: relative; - top: 2px; - margin-right: to_rem(3px); - fill: $gray-600; -} + .parsely-top-post-info { + display: flex; + margin: to_rem(5px) 0 0; + justify-content: space-between; + align-items: center; -.parsely-top-post-link:hover svg { - fill: $blue-550; -} + >span { + color: var(--gray-600); + margin-bottom: 0; + display: flex; -.parsely-contact-us { - margin-top: to_rem(15px) !important; + &:not(:first-child) { + margin-left: to_rem(5px); + } + } + } } -div.wp-parsely-content-helper { +.wp-parsely-content-helper .performance-details-panel { - div.current-post-details-panel { + // Generic styles for all sections. + div.section { + font-family: var(--base-font); + margin-top: 1.8rem; - // Generic styles for all sections. - div.section { - font-family: var(--base-font); - margin-top: 1.8rem; - - table { - border-collapse: collapse; - width: 100%; + table { + border-collapse: collapse; + width: 100%; - th { - font-weight: 400; - text-align: left; - } + th { + font-weight: 400; + text-align: left; } + } - // Generic styles for section titles. - div.section-title { - color: var(--base-text-2); - margin-bottom: 0.5rem; - } + // Generic styles for section titles. + div.section-title { + color: var(--base-text-2); + margin-bottom: 0.5rem; } + } - // Data Period section (Last x days). - div.section.period { - margin-top: 0.8rem; + // Data Period section (Last x days). + div.section.period { + margin-top: 0.8rem; - span { - color: var(--base-text-2); - } + span { + color: var(--base-text-2); } + } - // General Performance section (Views, Visitors, Time). - div.section.general-performance { + // General Performance section (Views, Visitors, Time). + div.section.general-performance { - table { - // Metrics. - tbody tr { - font-family: var(--numeric-font); - font-size: var(--font-size--extra-large); - font-weight: 500; - } + table { - // Titles. - tfoot tr { - color: var(--gray-700); - height: 1.4rem; - vertical-align: bottom; - } + // Metrics. + tbody tr { + font-family: var(--numeric-font); + font-size: var(--font-size--extra-large); + font-weight: 500; + } + + // Titles. + tfoot tr { + color: var(--gray-700); + height: 1.4rem; + vertical-align: bottom; } } + } - // Referrer Types section. - div.section.referrer-types { + // Referrer Types section. + div.section.referrer-types { - // Multi-percentage bar. - div.multi-percentage-bar { - --radius: 2px; - display: flex; - height: 0.5rem; + // Multi-percentage bar. + div.multi-percentage-bar { + --radius: 2px; + display: flex; + height: 0.5rem; - .bar-fill { + .bar-fill { - // Border radiuses for first and last bar-fills. - &:first-child { - border-radius: var(--radius) 0 0 var(--radius); - } + // Border radiuses for first and last bar-fills. + &:first-child { + border-radius: var(--radius) 0 0 var(--radius); + } - &:last-child { - border-radius: 0 var(--radius) var(--radius) 0; - } + &:last-child { + border-radius: 0 var(--radius) var(--radius) 0; + } - // Bar-fill colors by referrer type. - &.direct { - background-color: hsl(var(--ref-direct)); - } + // Bar-fill colors by referrer type. + &.direct { + background-color: hsl(var(--ref-direct)); + } - &.internal { - background-color: hsl(var(--ref-internal)); - } + &.internal { + background-color: hsl(var(--ref-internal)); + } - &.search { - background-color: hsl(var(--ref-search)); - } + &.search { + background-color: hsl(var(--ref-search)); + } - &.social { - background-color: hsl(var(--ref-social)); - } + &.social { + background-color: hsl(var(--ref-social)); + } - &.other { - background-color: hsl(var(--ref-other)); - } + &.other { + background-color: hsl(var(--ref-other)); } } + } - // Table showing referrer types and metrics. - table { - margin-top: 0.5rem; + // Table showing referrer types and metrics. + table { + margin-top: 0.5rem; - // Metrics. - tbody tr { - font-family: var(--numeric-font); - font-size: var(--font-size--large); - height: 1.4rem; - vertical-align: bottom; - } + // Metrics. + tbody tr { + font-family: var(--numeric-font); + font-size: var(--font-size--large); + height: 1.4rem; + vertical-align: bottom; } } + } + + // Top Referrers section. + div.section.top-referrers { - // Top Referrers section. - div.section.top-referrers { + table { - table { + // Titles (Top Referrers, Page Views). + thead tr { + color: var(--base-text-2); + height: 1.6rem; + vertical-align: top; - // Titles (Top Referrers, Page Views). - thead tr { - color: var(--base-text-2); - height: 1.6rem; - vertical-align: top; + th:last-child { + text-align: right; + } + } - th:last-child { - text-align: right; + // Table rows. + tbody { + + tr { + border: 1px solid var(--border); + border-left: 0; + border-right: 0; + height: 2rem; + + // Referrer name column. + th:first-child { + --width: 8rem; + // Use min and max width for text truncation to work. + max-width: var(--width); + min-width: var(--width); + overflow: hidden; + padding-right: 1rem; + text-overflow: ellipsis; + white-space: nowrap; } - } - // Table rows. - tbody { - - tr { - border: 1px solid var(--border); - border-left: 0; - border-right: 0; - height: 2rem; - - // Referrer name column. - th:first-child { - --width: 8rem; - // Use min and max width for text truncation to work. - max-width: var(--width); - min-width: var(--width); - overflow: hidden; - padding-right: 1rem; - text-overflow: ellipsis; - white-space: nowrap; - } - - // Percentage bar column. - td:nth-child(2) { - width: 100%; - } - - // Views column. - td:last-child { - padding-left: 1rem; - text-align: right; - } + // Percentage bar column. + td:nth-child(2) { + width: 100%; } - // Percentage bar. - div.percentage-bar { - // Bar background. - --radius: 4px; - background-color: var(--base-3); - border-radius: var(--radius); - display: flex; - height: 0.4rem; - margin: 0; - overflow: hidden; + // Views column. + td:last-child { + padding-left: 1rem; + text-align: right; + } + } - // Bar fill. - &::after { - background-color: var(--data); - border-radius: var(--radius); - content: ""; - height: 100%; - width: var(--bar-fill); - } + // Percentage bar. + div.percentage-bar { + // Bar background. + --radius: 4px; + background-color: var(--base-3); + border-radius: var(--radius); + display: flex; + height: 0.4rem; + margin: 0; + overflow: hidden; + + // Bar fill. + &::after { + background-color: var(--data); + border-radius: var(--radius); + content: ""; + height: 100%; + width: var(--bar-fill); } } } + } - // Percentage text below table. - div:last-child { - color: var(--base-text-2); - margin-top: 0.6rem; - } + // Percentage text below table. + div:last-child { + color: var(--base-text-2); + margin-top: 0.6rem; } + } - // Actions section (Visit Post, View in Parse.ly buttons). - div.section.actions { - display: inline-flex; - justify-content: space-between; - width: 100%; + // Actions section (Visit Post, View in Parse.ly buttons). + div.section.actions { + display: inline-flex; + justify-content: space-between; + width: 100%; - a.components-button { - border-radius: 4px; - text-transform: uppercase; + a.components-button { + border-radius: 4px; + text-transform: uppercase; - // Visit Post. - &.is-secondary { - box-shadow: inset 0 0 0 1px var(--border); - color: var(--sidebar-black); - } + // Visit Post. + &.is-secondary { + box-shadow: inset 0 0 0 1px var(--border); + color: var(--sidebar-black); + } - // View in Parse.ly. - &.is-primary { - background-color: var(--control); - } + // View in Parse.ly. + &.is-primary { + background-color: var(--control); } } } diff --git a/src/blocks/content-helper/content-helper.tsx b/src/blocks/content-helper/content-helper.tsx index 6ee8169fc..2cd15751b 100644 --- a/src/blocks/content-helper/content-helper.tsx +++ b/src/blocks/content-helper/content-helper.tsx @@ -9,21 +9,21 @@ import { registerPlugin } from '@wordpress/plugins'; /** * Internal dependencies */ -import CurrentPostDetails from './current-post-details/component'; -import RelatedTopPostList from './components/related-top-post-list'; +import PerformanceDetails from './performance-details/component'; +import RelatedTopPostList from './related-top-posts/component-list'; import LeafIcon from '../shared/components/leaf-icon'; const BLOCK_PLUGIN_ID = 'wp-parsely-block-editor-sidebar'; const renderSidebar = () => ( - } name="wp-parsely-content-helper" className="wp-parsely-content-helper" title={ __( 'Parse.ly Content Helper', 'wp-parsely' ) }> + } name="wp-parsely-content-helper" className="wp-parsely-content-helper" title={ __( 'Parse.ly Editor Sidebar', 'wp-parsely' ) }> - + - + diff --git a/src/blocks/content-helper/icons/edit-icon.tsx b/src/blocks/content-helper/icons/edit-icon.tsx new file mode 100644 index 000000000..9349df00b --- /dev/null +++ b/src/blocks/content-helper/icons/edit-icon.tsx @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/components'; + +export const EditIcon = () => ( + +); + +export default EditIcon; diff --git a/src/blocks/content-helper/icons/published-link-icon.tsx b/src/blocks/content-helper/icons/open-link-icon.tsx similarity index 94% rename from src/blocks/content-helper/icons/published-link-icon.tsx rename to src/blocks/content-helper/icons/open-link-icon.tsx index 9679b4605..c3d8c5823 100644 --- a/src/blocks/content-helper/icons/published-link-icon.tsx +++ b/src/blocks/content-helper/icons/open-link-icon.tsx @@ -3,7 +3,7 @@ */ import { SVG, Path } from '@wordpress/components'; -export const ViewsIcon = () => ( +export const OpenLinkIcon = () => ( ); -export default ViewsIcon; +export default OpenLinkIcon; diff --git a/src/blocks/content-helper/current-post-details/component.tsx b/src/blocks/content-helper/performance-details/component.tsx similarity index 65% rename from src/blocks/content-helper/current-post-details/component.tsx rename to src/blocks/content-helper/performance-details/component.tsx index 3f4a1cb2b..10a2aa10d 100644 --- a/src/blocks/content-helper/current-post-details/component.tsx +++ b/src/blocks/content-helper/performance-details/component.tsx @@ -8,9 +8,10 @@ import { useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ -import CurrentPostDetailsProvider from './provider'; -import { PostPerformanceData } from './post-performance-data'; +import PerformanceDetailsProvider from './provider'; +import { PerformanceData } from './model'; import { ContentHelperError } from '../content-helper-error'; +import { formatToImpreciseNumber } from '../../shared/functions'; // Number of attempts to fetch the data before displaying an error. const FETCH_RETRIES = 3; @@ -18,22 +19,22 @@ const FETCH_RETRIES = 3; /** * Specifies the form of component props. */ -interface PostDetailsSectionProps { - data: PostPerformanceData; +interface PerformanceSectionProps { + data: PerformanceData; } /** * Outputs the current post's details or shows an error message on failure. */ -function CurrentPostDetails() { +function PerformanceDetails() { const [ loading, setLoading ] = useState( true ); - const [ error, setError ] = useState( null ); - const [ postDetailsData, setPostDetails ] = useState( null ); - const provider = new CurrentPostDetailsProvider(); + const [ error, setError ] = useState(); + const [ postDetailsData, setPostDetails ] = useState(); + const provider = new PerformanceDetailsProvider(); useEffect( () => { const fetchPosts = async ( retries: number ) => { - provider.getCurrentPostDetails() + provider.getPerformanceDetails() .then( ( result ) => { setPostDetails( result ); setLoading( false ); @@ -59,19 +60,25 @@ function CurrentPostDetails() { return ( loading - ? - : + ? ( +
+ +
+ ) + : ( + + ) ); } /** * Outputs all the "Current Post Details" sections. * - * @param {PostDetailsSectionProps} props The props needed to populate the sections. + * @param {PerformanceSectionProps} props The props needed to populate the sections. */ -function CurrentPostDetailsSections( props: PostDetailsSectionProps ) { +function PerformanceDetailsSections( props: PerformanceSectionProps ) { return ( -
+
@@ -85,9 +92,9 @@ function CurrentPostDetailsSections( props: PostDetailsSectionProps ) { * Outputs the "Period" section, which denotes the period for which data is * shown. * - * @param {PostDetailsSectionProps} props The props needed to populate the section. + * @param {PerformanceSectionProps} props The props needed to populate the section. */ -function DataPeriodSection( props: PostDetailsSectionProps ) { +function DataPeriodSection( props: PerformanceSectionProps ) { const period = props.data.period; // Get the date (in short format) on which the period starts. @@ -115,9 +122,9 @@ function DataPeriodSection( props: PostDetailsSectionProps ) { /** * Outputs the "General Performance" (Views, Visitors, Time) section. * - * @param {PostDetailsSectionProps} props The props needed to populate the section. + * @param {PerformanceSectionProps} props The props needed to populate the section. */ -function GeneralPerformanceSection( props: PostDetailsSectionProps ) { +function GeneralPerformanceSection( props: PerformanceSectionProps ) { const data = props.data; return ( @@ -125,8 +132,8 @@ function GeneralPerformanceSection( props: PostDetailsSectionProps ) {
- - + + @@ -145,9 +152,9 @@ function GeneralPerformanceSection( props: PostDetailsSectionProps ) { /** * Outputs the "Referrer Types" section. * - * @param {PostDetailsSectionProps} props The props needed to populate the section. + * @param {PerformanceSectionProps} props The props needed to populate the section. */ -function ReferrerTypesSection( props: PostDetailsSectionProps ) { +function ReferrerTypesSection( props: PerformanceSectionProps ) { const data = props.data; // Remove unneeded totals to simplify upcoming map() calls. @@ -198,7 +205,7 @@ function ReferrerTypesSection( props: PostDetailsSectionProps ) { { Object.entries( data.referrers.types ).map( ( [ key, value ] ) => { - return ; + return ; } ) } @@ -210,9 +217,9 @@ function ReferrerTypesSection( props: PostDetailsSectionProps ) { /** * Outputs the "Top Referrers" section. * - * @param {PostDetailsSectionProps} props The props needed to populate the section. + * @param {PerformanceSectionProps} props The props needed to populate the section. */ -function TopReferrersSection( props: PostDetailsSectionProps ) { +function TopReferrersSection( props: PerformanceSectionProps ) { const data = props.data; let totalViewsPercentage = 0; @@ -249,7 +256,7 @@ function TopReferrersSection( props: PostDetailsSectionProps ) { style={ { '--bar-fill': value.viewsPercentage + '%' } as React.CSSProperties }> - + ); } ) @@ -272,9 +279,9 @@ function TopReferrersSection( props: PostDetailsSectionProps ) { /** * Outputs the "Actions" section. * - * @param {PostDetailsSectionProps} props The props needed to populate the section. + * @param {PerformanceSectionProps} props The props needed to populate the section. */ -function ActionsSection( props: PostDetailsSectionProps ) { +function ActionsSection( props: PerformanceSectionProps ) { const data = props.data; const ariaOpensNewTab = { __( '(opens in new tab)', 'wp-parsely' ) } @@ -287,67 +294,11 @@ function ActionsSection( props: PostDetailsSectionProps ) { { __( 'Visit Post', 'wp-parsely' ) }{ ariaOpensNewTab } ); } -/** - * Implements the "Imprecise Number" functionality of the Parse.ly dashboard. - * - * Note: This function is not made to process float numbers. - * - * @param {string} value The number to process. It can be formatted. - * @param {number} fractionDigits The number of desired fraction digits. - * @param {string} glue A string to put between the number and unit. - * @return {string} The number formatted as an imprecise number. - */ -function impreciseNumber( value: string, fractionDigits = 1, glue = '' ): string { - const number = parseInt( value.replace( /\D/g, '' ), 10 ); - - if ( number < 1000 ) { - return value; - } else if ( number < 10000 ) { - fractionDigits = 1; - } - - const unitNames = { - 1000: 'k', - '1,000,000': 'M', - '1,000,000,000': 'B', - '1,000,000,000,000': 'T', - '1,000,000,000,000,000': 'Q', - }; - let currentNumber = number; - let currentNumberAsString = number.toString(); - let unit = ''; - let previousNumber = 0; - - Object.entries( unitNames ).forEach( ( [ thousands, suffix ] ) => { - const thousandsInt = parseInt( thousands.replace( /\D/g, '' ), 10 ); - - if ( number >= thousandsInt ) { - currentNumber = number / thousandsInt; - let precision = fractionDigits; - - // For over 10 units, we reduce the precision to 1 fraction digit. - if ( currentNumber % 1 > 1 / previousNumber ) { - precision = currentNumber > 10 ? 1 : 2; - } - - // Precision override, where we want to show 2 fraction digits. - const zeroes = parseFloat( currentNumber.toFixed( 2 ) ) === parseFloat( currentNumber.toFixed( 0 ) ); - precision = zeroes ? 0 : precision; - currentNumberAsString = currentNumber.toFixed( precision ); - unit = suffix; - } - - previousNumber = thousandsInt; - } ); - - return currentNumberAsString + glue + unit; -} - -export default CurrentPostDetails; +export default PerformanceDetails; diff --git a/src/blocks/content-helper/current-post-details/post-performance-data.ts b/src/blocks/content-helper/performance-details/model.ts similarity index 70% rename from src/blocks/content-helper/current-post-details/post-performance-data.ts rename to src/blocks/content-helper/performance-details/model.ts index 80974e704..5dcd26adb 100644 --- a/src/blocks/content-helper/current-post-details/post-performance-data.ts +++ b/src/blocks/content-helper/performance-details/model.ts @@ -1,4 +1,4 @@ -export interface PostPerformanceData { +export interface PerformanceData { author: string; avgEngaged: string; date: string; @@ -8,15 +8,15 @@ export interface PostPerformanceData { end: string; days: number; }; - referrers: PostPerformanceReferrerData; - statsUrl: string; + referrers: PerformanceReferrerData; + dashUrl: string; title: string; url: string; views: string; visitors: string; } -export interface PostPerformanceReferrerData { +export interface PerformanceReferrerData { top: { views: string; viewsPercentage: string; diff --git a/src/blocks/content-helper/current-post-details/provider.ts b/src/blocks/content-helper/performance-details/provider.ts similarity index 68% rename from src/blocks/content-helper/current-post-details/provider.ts rename to src/blocks/content-helper/performance-details/provider.ts index d60132d6f..cdadc59a9 100644 --- a/src/blocks/content-helper/current-post-details/provider.ts +++ b/src/blocks/content-helper/performance-details/provider.ts @@ -14,9 +14,13 @@ import { ContentHelperErrorCode, } from '../content-helper-error'; import { - PostPerformanceData, - PostPerformanceReferrerData, -} from './post-performance-data'; + PerformanceData, + PerformanceReferrerData, +} from './model'; +import { + convertDateToString, + removeDaysFromDate, +} from '../../shared/utils/date'; /** * Specifies the form of the response returned by the `/stats/post/detail` @@ -24,7 +28,7 @@ import { */ interface AnalyticsApiResponse { error?: Error; - data: PostPerformanceData[]; + data: PerformanceData[]; } /** @@ -33,13 +37,13 @@ import { */ interface ReferrersApiResponse { error?: Error; - data: PostPerformanceReferrerData; + data: PerformanceReferrerData; } /** * Provides current post details data for use in other components. */ -class CurrentPostDetailsProvider { +class PerformanceDetailsProvider { private dataPeriodDays: number; private dataPeriodStart: string; private dataPeriodEnd: string; @@ -48,17 +52,19 @@ class CurrentPostDetailsProvider { * Constructor. */ constructor() { - // Return data for the last 7 days (today included). - this.setDataPeriod( 7 ); + // Set period for the last 7 days (today included). + this.dataPeriodDays = 7; + this.dataPeriodEnd = convertDateToString( new Date() ) + 'T23:59'; + this.dataPeriodStart = removeDaysFromDate( this.dataPeriodEnd, this.dataPeriodDays - 1 ) + 'T00:00'; } /** * Returns details about the post that is currently being edited within the * WordPress Block Editor. * - * @return {Promise} The current post's details. + * @return {Promise} The current post's details. */ - public async getCurrentPostDetails(): Promise { + public async getPerformanceDetails(): Promise { const editor = select( 'core/editor' ); // We cannot show data for non-published posts. @@ -92,9 +98,9 @@ class CurrentPostDetailsProvider { * API. * * @param {string} postUrl - * @return {Promise } The current post's details. + * @return {Promise } The current post's details. */ - private async fetchPerformanceDataFromWpEndpoint( postUrl: string ): Promise { + private async fetchPerformanceDataFromWpEndpoint( postUrl: string ): Promise { let response; try { @@ -106,7 +112,7 @@ class CurrentPostDetailsProvider { period_end: this.dataPeriodEnd, } ), } ); - } catch ( wpError ) { + } catch ( wpError: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any return Promise.reject( new ContentHelperError( wpError.message, wpError.code ) ); @@ -124,7 +130,7 @@ class CurrentPostDetailsProvider { return Promise.reject( new ContentHelperError( sprintf( /* translators: URL of the published post */ - __( 'The post %s has 0 views or no data was returned for it by the Parse.ly API.', + __( 'The post %s has 0 views, or the Parse.ly API returned no data.', 'wp-parsely' ), postUrl ), ContentHelperErrorCode.ParselyApiReturnedNoData, '' ) ); @@ -149,11 +155,11 @@ class CurrentPostDetailsProvider { * * @param {string} postUrl The post's URL. * @param {string} totalViews Total post views (including direct views). - * @return {Promise} The post's referrer data. + * @return {Promise} The post's referrer data. */ private async fetchReferrerDataFromWpEndpoint( postUrl: string, totalViews: string - ): Promise { + ): Promise { let response; // Query WordPress API endpoint. @@ -166,7 +172,7 @@ class CurrentPostDetailsProvider { total_views: totalViews, // Needed to calculate direct views. } ), } ); - } catch ( wpError ) { + } catch ( wpError: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any return Promise.reject( new ContentHelperError( wpError.message, wpError.code ) ); @@ -181,42 +187,6 @@ class CurrentPostDetailsProvider { return response.data; } - - /** - * Sets the period for which to fetch the data. - * - * @param {number} days Number of last days to get the data for. - */ - private setDataPeriod( days: number ) { - this.dataPeriodDays = days; - this.dataPeriodEnd = this.convertDateToString( new Date() ) + 'T23:59'; - this.dataPeriodStart = this.removeDaysFromDate( this.dataPeriodEnd, this.dataPeriodDays - 1 ) + 'T00:00'; - } - - /** - * Removes the given number of days from a "YYYY-MM-DD" string, and returns - * the result in the same format. - * - * @param {string} date The date in "YYYY-MM-DD" format. - * @param {number} days The number of days to remove from the date. - * @return {string} The resulting date in "YYYY-MM-DD" format. - */ - private removeDaysFromDate( date: string, days: number ): string { - const pastDate = new Date( date ); - pastDate.setDate( pastDate.getDate() - days ); - - return this.convertDateToString( pastDate ); - } - - /** - * Converts a date to a string in "YYYY-MM-DD" format. - * - * @param {Date} date The date to format. - * @return {string} The date in "YYYY-MM-DD" format. - */ - private convertDateToString( date: Date ): string { - return date.toISOString().substring( 0, 10 ); - } } -export default CurrentPostDetailsProvider; +export default PerformanceDetailsProvider; diff --git a/src/blocks/content-helper/components/related-top-post-list-item.tsx b/src/blocks/content-helper/related-top-posts/component-list-item.tsx similarity index 54% rename from src/blocks/content-helper/components/related-top-post-list-item.tsx rename to src/blocks/content-helper/related-top-posts/component-list-item.tsx index d9b930623..f81c249d4 100644 --- a/src/blocks/content-helper/components/related-top-post-list-item.tsx +++ b/src/blocks/content-helper/related-top-posts/component-list-item.tsx @@ -6,9 +6,11 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { RelatedTopPostData } from '../models/related-top-post-data'; +import { RelatedTopPostData } from './model'; import ViewsIcon from '../icons/views-icon'; -import PublishedLinkIcon from '../icons/published-link-icon'; +import OpenLinkIcon from '../icons/open-link-icon'; +import EditIcon from '../icons/edit-icon'; +import { getPostEditUrl } from '../../shared/utils/post'; interface RelatedTopPostListItemProps { post: RelatedTopPostData; @@ -18,12 +20,20 @@ function RelatedTopPostListItem( { post }: RelatedTopPostListItemProps ): JSX.El return (
  • - + { post.title } - - + + + + + { + 0 !== post.postId && + + + + }

    Date { post.date } diff --git a/src/blocks/content-helper/components/related-top-post-list.tsx b/src/blocks/content-helper/related-top-posts/component-list.tsx similarity index 84% rename from src/blocks/content-helper/components/related-top-post-list.tsx rename to src/blocks/content-helper/related-top-posts/component-list.tsx index 2bd126d26..5a7915049 100644 --- a/src/blocks/content-helper/components/related-top-post-list.tsx +++ b/src/blocks/content-helper/related-top-posts/component-list.tsx @@ -7,11 +7,11 @@ import { useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ -import ContentHelperProvider from '../content-helper-provider'; -import RelatedTopPostListItem from './related-top-post-list-item'; +import RelatedTopPostsProvider from './provider'; +import RelatedTopPostListItem from './component-list-item'; +import { RelatedTopPostData } from './model'; import { ContentHelperError } from '../content-helper-error'; import { getDateInUserLang, SHORT_DATE_FORMAT } from '../../shared/utils/date'; -import { RelatedTopPostData } from '../models/related-top-post-data'; const FETCH_RETRIES = 3; @@ -20,13 +20,13 @@ const FETCH_RETRIES = 3; */ function RelatedTopPostList() { const [ loading, setLoading ] = useState( true ); - const [ error, setError ] = useState( null ); - const [ message, setMessage ] = useState( null ); + const [ error, setError ] = useState(); + const [ message, setMessage ] = useState(); const [ posts, setPosts ] = useState( [] ); useEffect( () => { const fetchPosts = async ( retries: number ) => { - ContentHelperProvider.getRelatedTopPosts() + RelatedTopPostsProvider.getRelatedTopPosts() .then( ( result ): void => { const mappedPosts: RelatedTopPostData[] = result.posts.map( ( post: RelatedTopPostData ): RelatedTopPostData => ( @@ -59,7 +59,7 @@ function RelatedTopPostList() { setLoading( false ); setPosts( [] ); setMessage( '' ); - setError( null ); + setError( undefined ); }; }, [] ); diff --git a/src/blocks/content-helper/models/related-top-post-data.ts b/src/blocks/content-helper/related-top-posts/model.ts similarity index 78% rename from src/blocks/content-helper/models/related-top-post-data.ts rename to src/blocks/content-helper/related-top-posts/model.ts index a68f978fb..4b1c41ee7 100644 --- a/src/blocks/content-helper/models/related-top-post-data.ts +++ b/src/blocks/content-helper/related-top-posts/model.ts @@ -2,7 +2,8 @@ export interface RelatedTopPostData { author: string; date: string; id: number; - statsUrl: string; + postId: number; + dashUrl: string; title: string; url: string; views: number; diff --git a/src/blocks/content-helper/content-helper-provider.ts b/src/blocks/content-helper/related-top-posts/provider.ts similarity index 86% rename from src/blocks/content-helper/content-helper-provider.ts rename to src/blocks/content-helper/related-top-posts/provider.ts index 4ccd99914..56c4e545c 100644 --- a/src/blocks/content-helper/content-helper-provider.ts +++ b/src/blocks/content-helper/related-top-posts/provider.ts @@ -14,8 +14,8 @@ import apiFetch from '@wordpress/api-fetch'; import { ContentHelperError, ContentHelperErrorCode, -} from './content-helper-error'; -import { RelatedTopPostData } from './models/related-top-post-data'; +} from '../content-helper-error'; +import { RelatedTopPostData } from './model'; /** * The form of the query that gets posted to the analytics/posts WordPress REST @@ -40,7 +40,7 @@ interface RelatedTopPostsApiResponse { /** * The form of the result returned by the getRelatedTopPosts() function. */ -interface GetRelatedTopPostsResult { +export interface GetRelatedTopPostsResult { message: string; posts: RelatedTopPostData[]; } @@ -48,10 +48,10 @@ interface GetRelatedTopPostsResult { export const RELATED_POSTS_DEFAULT_LIMIT = 5; export const RELATED_POSTS_DEFAULT_TIME_RANGE = 3; // In days. -class ContentHelperProvider { +class RelatedTopPostsProvider { /** - * Returns related top-performing posts to the one that is currently being - * edited within the WordPress Block Editor. + * Returns related top posts to the one that is currently being edited + * within the WordPress Block Editor. * * The 'related' status is determined by the current post's Author, Category * or tag. @@ -81,7 +81,7 @@ class ContentHelperProvider { return Promise.reject( contentHelperError ); } - // Fetch results from API and set the Content Helper's message. + // Fetch results from API and set the message. let data; try { data = await this.fetchRelatedTopPostsFromWpEndpoint( apiQuery ); @@ -90,16 +90,16 @@ class ContentHelperProvider { } /* translators: %s: message such as "in category Foo", %d: number of days */ - let message = sprintf( __( 'Top-performing posts %1$s in last %2$d days.', 'wp-parsely' ), apiQuery.message, RELATED_POSTS_DEFAULT_TIME_RANGE ); + let message = sprintf( __( 'Top posts %1$s in last %2$d days.', 'wp-parsely' ), apiQuery.message, RELATED_POSTS_DEFAULT_TIME_RANGE ); if ( data.length === 0 ) { - message = `${ __( 'The Parse.ly API did not return any results for top-performing posts', 'wp-parsely' ) } ${ apiQuery.message }.`; + message = `${ __( 'The Parse.ly API did not return any results for related top posts', 'wp-parsely' ) } ${ apiQuery.message }.`; } return { message, posts: data }; } /** - * Fetches the related top-performing posts data from the WordPress REST API. + * Fetches the related top posts data from the WordPress REST API. * * @param {RelatedTopPostsApiQuery} query * @return {Promise>} Array of fetched posts. @@ -109,9 +109,9 @@ class ContentHelperProvider { try { response = await apiFetch( { - path: addQueryArgs( '/wp-parsely/v1/stats/posts', query.query ), + path: addQueryArgs( '/wp-parsely/v1/stats/posts', query.query as object ), } ) as RelatedTopPostsApiResponse; - } catch ( wpError ) { + } catch ( wpError: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any return Promise.reject( new ContentHelperError( wpError.message, wpError.code ) ); @@ -129,7 +129,7 @@ class ContentHelperProvider { /** * Builds the query object used in the API for performing the related - * top-performing posts request. + * top posts request. * * @param {User} author The post's author. * @param {Taxonomy} category The post's category. @@ -174,4 +174,4 @@ class ContentHelperProvider { } } -export default ContentHelperProvider; +export default RelatedTopPostsProvider; diff --git a/src/blocks/recommendations/actions.ts b/src/blocks/recommendations/actions.ts index 7ae618f91..15cc7294f 100644 --- a/src/blocks/recommendations/actions.ts +++ b/src/blocks/recommendations/actions.ts @@ -1,4 +1,4 @@ -import { RECOMMENDATIONS_BLOCK_ERROR, RECOMMENDATIONS_BLOCK_LOADED, RECOMMENDATIONS_BLOCK_RECOMMENDATIONS } from './constants'; +import { RecommendationsAction } from './constants'; import { Recommendation } from './models/Recommendation'; interface SetErrorPayload { @@ -10,15 +10,15 @@ interface SetRecommendationsPayload { } export const setError = ( { error }: SetErrorPayload ) => ( { - type: RECOMMENDATIONS_BLOCK_ERROR, + type: RecommendationsAction.Error, error, } ); export const setRecommendations = ( { recommendations }: SetRecommendationsPayload ) => ( { - type: RECOMMENDATIONS_BLOCK_RECOMMENDATIONS, + type: RecommendationsAction.Recommendations, recommendations, } ); export const setLoaded = () => ( { - type: RECOMMENDATIONS_BLOCK_LOADED, + type: RecommendationsAction.Loaded, } ); diff --git a/src/blocks/recommendations/class-recommendations-block.php b/src/blocks/recommendations/class-recommendations-block.php index 3e9029804..972b21e21 100644 --- a/src/blocks/recommendations/class-recommendations-block.php +++ b/src/blocks/recommendations/class-recommendations-block.php @@ -62,10 +62,10 @@ public static function register_block(): void { * * @since 3.2.0 * - * @param array $attributes The user-controlled settings for this block. - * @return string + * @param array $attributes The user-controlled settings for this block. + * @return string|false */ - public static function render_callback( array $attributes ): string { + public static function render_callback( array $attributes ) { /** * In block.json we define a `viewScript` that is mean to only be loaded * on the front end. We need to manually enqueue this script here. @@ -75,7 +75,7 @@ public static function render_callback( array $attributes ): string { wp_enqueue_script( 'wp-parsely-recommendations-view-script' ); ob_start(); ?> -

    ->
    + > { +const ParselyRecommendationsFetcher = ( { boost, limit, sort, isEditMode } : ParselyRecommendationsFetcherProps ): JSX.Element | null => { const { dispatch } = useRecommendationsStore(); const query = { @@ -58,7 +58,7 @@ const ParselyRecommendationsFetcher = ( { boost, limit, sort, isEditMode } : Par } if ( error ) { - dispatch( setError( { error } ) ); + dispatch( setError( { error: error as string } ) ); return; } diff --git a/src/blocks/recommendations/components/parsely-recommendations-inspector-controls.tsx b/src/blocks/recommendations/components/parsely-recommendations-inspector-controls.tsx index 3a7857fed..65d492fc8 100644 --- a/src/blocks/recommendations/components/parsely-recommendations-inspector-controls.tsx +++ b/src/blocks/recommendations/components/parsely-recommendations-inspector-controls.tsx @@ -12,6 +12,7 @@ import { TextControl, ToggleControl, } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -26,14 +27,20 @@ interface ParselyRecommendationsInspectorControlsProps { const ParselyRecommendationsInspectorControls = ( { attributes: { boost, imagestyle, limit, openlinksinnewtab, showimages, sort, title }, setAttributes, -} : ParselyRecommendationsInspectorControlsProps ) => ( - +} : ParselyRecommendationsInspectorControlsProps ) => { + function setImageStyle( value: string ): void { + setAttributes( { + imagestyle: value === 'original' ? 'original' : 'thumbnail', + } ); + } + + return setAttributes( { title: newval } ) } + onChange={ useCallback( ( value: string ): void => setAttributes( { title: value } ), [ title ] ) } /> @@ -41,7 +48,7 @@ const ParselyRecommendationsInspectorControls = ( { label={ __( 'Maximum Results', 'wp-parsely' ) } min={ 1 } max={ 25 } - onChange={ ( newval ) => setAttributes( { limit: newval } ) } + onChange={ useCallback( ( value: number ): void => setAttributes( { limit: value } ), [ limit ] ) } value={ limit } /> @@ -49,7 +56,7 @@ const ParselyRecommendationsInspectorControls = ( { setAttributes( { openlinksinnewtab: ! openlinksinnewtab } ) } + onChange={ useCallback( (): void => setAttributes( { openlinksinnewtab: ! openlinksinnewtab } ), [ openlinksinnewtab ] ) } /> @@ -61,7 +68,7 @@ const ParselyRecommendationsInspectorControls = ( { : __( 'Not showing images', 'wp-parsely' ) } checked={ showimages } - onChange={ () => setAttributes( { showimages: ! showimages } ) } + onChange={ useCallback( (): void => setAttributes( { showimages: ! showimages } ), [ showimages ] ) } /> { showimages && ( @@ -73,11 +80,7 @@ const ParselyRecommendationsInspectorControls = ( { { label: __( 'Original image', 'wp-parsely' ), value: 'original' }, { label: __( 'Thumbnail from Parse.ly', 'wp-parsely' ), value: 'thumbnail' }, ] } - onChange={ ( newval ) => - setAttributes( { - imagestyle: newval === 'original' ? 'original' : 'thumbnail', - } ) - } + onChange={ setImageStyle } /> ) } @@ -95,7 +98,7 @@ const ParselyRecommendationsInspectorControls = ( { value: 'pub_date', }, ] } - onChange={ ( newval ) => setAttributes( { sort: newval } ) } + onChange={ useCallback( ( value: string ): void => setAttributes( { sort: value } ), [ sort ] ) } /> @@ -180,11 +183,11 @@ const ParselyRecommendationsInspectorControls = ( { value: 'pi_referrals', }, ] } - onChange={ ( newval ) => setAttributes( { boost: newval } ) } + onChange={ useCallback( ( value: string ): void => setAttributes( { boost: value } ), [ boost ] ) } /> - -); + ; +}; export default ParselyRecommendationsInspectorControls; diff --git a/src/blocks/recommendations/components/parsely-recommendations-list.tsx b/src/blocks/recommendations/components/parsely-recommendations-list.tsx index 902a6d689..1495ac373 100644 --- a/src/blocks/recommendations/components/parsely-recommendations-list.tsx +++ b/src/blocks/recommendations/components/parsely-recommendations-list.tsx @@ -18,11 +18,11 @@ interface ParselyRecommendationsListProps { const ParselyRecommendationsList = ( { imagestyle, recommendations, showimages, openlinksinnewtab }: ParselyRecommendationsListProps ) => (
      - { recommendations.map( ( recommendation, index ) => ( + { recommendations.map( ( recommendation ) => ( { - switch ( action.type ) { - case RECOMMENDATIONS_BLOCK_ERROR: - return { ...state, isLoaded: true, error: action.error, recommendations: undefined }; - case RECOMMENDATIONS_BLOCK_LOADED: - return { ...state, isLoaded: true }; - case RECOMMENDATIONS_BLOCK_RECOMMENDATIONS: { - const { recommendations } = action; - if ( ! Array.isArray( recommendations ) ) { - return { ...state, recommendations: undefined }; - } - const validRecommendations = recommendations.map( - // eslint-disable-next-line camelcase - ( { title, url, image_url, thumb_url_medium } ) => ( { - title, - url, - image_url, // eslint-disable-line camelcase - thumb_url_medium, // eslint-disable-line camelcase - } ) - ); - return { ...state, isLoaded: true, error: undefined, recommendations: validRecommendations }; - } - default: - return { ...state }; - } -}; - -const RecommendationsStore = ( props ) => { - const defaultState = { - isLoaded: false, - recommendations: undefined, - uuid: window.PARSELY?.config?.uuid, - clientId: props.clientId, - }; - - const [ state, dispatch ] = useReducer( reducer, defaultState ); - return ; -}; - -export const useRecommendationsStore = () => useContext( RecommendationsContext ); - -export default RecommendationsStore; diff --git a/src/blocks/recommendations/recommendations-store.tsx b/src/blocks/recommendations/recommendations-store.tsx new file mode 100644 index 000000000..77a1ece2f --- /dev/null +++ b/src/blocks/recommendations/recommendations-store.tsx @@ -0,0 +1,71 @@ +/** + * External dependencies + */ +import { createContext, useContext, useReducer } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { RecommendationsAction } from './constants'; +import { Recommendation } from './models/Recommendation'; + +interface RecommendationState { + isLoaded: boolean; + recommendations: Recommendation[]; + uuid: string | null; + clientId: string | null; + error: Error | null; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const RecommendationsContext = createContext( {} as any ); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const reducer = ( state: RecommendationState, action: any ): RecommendationState => { + switch ( action.type ) { + case RecommendationsAction.Error: + return { ...state, isLoaded: true, error: action.error, recommendations: [] }; + case RecommendationsAction.Loaded: + return { ...state, isLoaded: true }; + case RecommendationsAction.Recommendations: { + const { recommendations } = action; + if ( ! Array.isArray( recommendations ) ) { + return { ...state, recommendations: [] }; + } + const validRecommendations = recommendations.map( + // eslint-disable-next-line camelcase + ( { title, url, image_url, thumb_url_medium } ) => ( { + title, + url, + image_url, // eslint-disable-line camelcase + thumb_url_medium, // eslint-disable-line camelcase + } ) + ); + return { ...state, isLoaded: true, error: null, recommendations: validRecommendations }; + } + default: + return { ...state }; + } +}; + +interface RecommendationStore { + clientId?: string; + children: React.ReactNode; +} + +const RecommendationsStore = ( props: RecommendationStore ) => { + const defaultState: RecommendationState = { + isLoaded: false, + recommendations: [], + uuid: window.PARSELY?.config?.uuid || null, + clientId: props?.clientId || null, + error: null, + }; + + const [ state, dispatch ] = useReducer( reducer, defaultState ); + return ; +}; + +export const useRecommendationsStore = () => useContext( RecommendationsContext ); + +export default RecommendationsStore; diff --git a/src/blocks/recommendations/view.tsx b/src/blocks/recommendations/view.tsx index 1a1c9fe18..b7dd86263 100644 --- a/src/blocks/recommendations/view.tsx +++ b/src/blocks/recommendations/view.tsx @@ -12,11 +12,11 @@ import RecommendationsStore from './recommendations-store'; domReady( () => { const blocks = document.querySelectorAll( '.wp-block-wp-parsely-recommendations' ); - blocks.forEach( ( block, i ) => + blocks.forEach( ( block ) => render( { /* @ts-ignore */ } - + , block ) diff --git a/src/blocks/shared/functions.ts b/src/blocks/shared/functions.ts new file mode 100644 index 000000000..7915e640d --- /dev/null +++ b/src/blocks/shared/functions.ts @@ -0,0 +1,55 @@ +/** + * Implements the "Imprecise Number" functionality of the Parse.ly dashboard. + * + * Note: This function is not made to process float numbers. + * + * @param {string} value The number to process. It can be formatted. + * @param {number} fractionDigits The number of desired fraction digits. + * @param {string} glue A string to put between the number and unit. + * @return {string} The number formatted as an imprecise number. + */ +export function formatToImpreciseNumber( value: string, fractionDigits = 1, glue = '' ): string { + const number = parseInt( value.replace( /\D/g, '' ), 10 ); + + if ( number < 1000 ) { + return value; + } else if ( number < 10000 ) { + fractionDigits = 1; + } + + const unitNames: {[key:string]: string} = { + 1000: 'k', + '1,000,000': 'M', + '1,000,000,000': 'B', + '1,000,000,000,000': 'T', + '1,000,000,000,000,000': 'Q', + }; + let currentNumber = number; + let currentNumberAsString = number.toString(); + let unit = ''; + let previousNumber = 0; + + Object.entries( unitNames ).forEach( ( [ thousands, suffix ] ) => { + const thousandsInt = parseInt( thousands.replace( /\D/g, '' ), 10 ); + + if ( number >= thousandsInt ) { + currentNumber = number / thousandsInt; + let precision = fractionDigits; + + // For over 10 units, we reduce the precision to 1 fraction digit. + if ( currentNumber % 1 > 1 / previousNumber ) { + precision = currentNumber > 10 ? 1 : 2; + } + + // Precision override, where we want to show 2 fraction digits. + const zeroes = parseFloat( currentNumber.toFixed( 2 ) ) === parseFloat( currentNumber.toFixed( 0 ) ); + precision = zeroes ? 0 : precision; + currentNumberAsString = currentNumber.toFixed( precision ); + unit = suffix; + } + + previousNumber = thousandsInt; + } ); + + return currentNumberAsString + glue + unit; +} diff --git a/src/blocks/shared/utils/constants.ts b/src/blocks/shared/utils/constants.ts index 4a2f67938..54966a6a5 100644 --- a/src/blocks/shared/utils/constants.ts +++ b/src/blocks/shared/utils/constants.ts @@ -1 +1,2 @@ export const DASHBOARD_BASE_URL = 'https://dash.parsely.com'; +export const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2'; diff --git a/src/blocks/shared/utils/date.ts b/src/blocks/shared/utils/date.ts index 4e57e0ddb..d0e614e3d 100644 --- a/src/blocks/shared/utils/date.ts +++ b/src/blocks/shared/utils/date.ts @@ -1,4 +1,5 @@ export const SHORT_DATE_FORMAT: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }; +export const SHORT_DATE_FORMAT_WITHOUT_YEAR: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' }; export function getDateInUserLang( date: Date, options: Intl.DateTimeFormatOptions ): string { return Intl.DateTimeFormat( @@ -6,3 +7,48 @@ export function getDateInUserLang( date: Date, options: Intl.DateTimeFormatOptio options ).format( date ); } + +/** + * Returns the passed date in short format or in short format without year (if + * the passed date is within the current year), respecting the user's language. + * + * @param {Date} date The date to be formatted. + * @return {string} The resulting date in its final format. + */ +export function getSmartShortDate( date: Date ): string { + let dateFormat = SHORT_DATE_FORMAT; + + if ( date.getUTCFullYear() === new Date().getUTCFullYear() ) { + dateFormat = SHORT_DATE_FORMAT_WITHOUT_YEAR; + } + + return Intl.DateTimeFormat( + document.documentElement.lang || 'en', + dateFormat + ).format( date ); +} + +/** + * Removes the given number of days from a "YYYY-MM-DD" string, and returns + * the result in the same format. + * + * @param {string} date The date in "YYYY-MM-DD" format. + * @param {number} days The number of days to remove from the date. + * @return {string} The resulting date in "YYYY-MM-DD" format. + */ +export function removeDaysFromDate( date: string, days: number ): string { + const pastDate = new Date( date ); + pastDate.setDate( pastDate.getDate() - days ); + + return convertDateToString( pastDate ); +} + +/** + * Converts a date to a string in "YYYY-MM-DD" format. + * + * @param {Date} date The date to format. + * @return {string} The date in "YYYY-MM-DD" format. + */ +export function convertDateToString( date: Date ): string { + return date.toISOString().substring( 0, 10 ); +} diff --git a/src/blocks/shared/utils/post.ts b/src/blocks/shared/utils/post.ts new file mode 100644 index 000000000..1e6d0f2fa --- /dev/null +++ b/src/blocks/shared/utils/post.ts @@ -0,0 +1,10 @@ +/** + * Gets edit url of the post. + * + * @param {number} postId ID of the post. + * + * @return {string} Edit url of the post. + */ +export function getPostEditUrl( postId: number ): string { + return `/wp-admin/post.php?post=${ postId }&action=edit`; +} diff --git a/src/blocks/shared/variables.scss b/src/blocks/shared/variables.scss deleted file mode 100644 index 35cc6fd42..000000000 --- a/src/blocks/shared/variables.scss +++ /dev/null @@ -1,10 +0,0 @@ -$html-font-size: 16px; - -// Colors -$black: #000; -// gray -$gray-300: #edeeef; -$gray-600: #586069; -$gray-700: #444d56; -// blue -$blue-550: #2596db; diff --git a/src/class-dashboard-link.php b/src/class-dashboard-link.php index b7034beb2..6b9572f8d 100644 --- a/src/class-dashboard-link.php +++ b/src/class-dashboard-link.php @@ -27,12 +27,17 @@ class Dashboard_Link { * @since 3.1.0 Moved to class-dashboard-link.php. Added source parameter. * * @param WP_Post $post Which post object or ID to check. - * @param string $apikey API key or empty string. + * @param string $site_id Site ID or empty string. * @param string $campaign Campaign name for the `utm_campaign` URL parameter. * @param string $source Source name for the `utm_source` URL parameter. * @return string */ - public static function generate_url( WP_Post $post, string $apikey, string $campaign, string $source ): string { + public static function generate_url( WP_Post $post, string $site_id, string $campaign, string $source ): string { + /** + * Internal variable. + * + * @var string|false + */ $permalink = get_permalink( $post ); if ( ! is_string( $permalink ) ) { return ''; @@ -45,9 +50,7 @@ public static function generate_url( WP_Post $post, string $apikey, string $camp 'utm_medium' => 'wp-parsely', ); - $base_url = trailingslashit( Parsely::DASHBOARD_BASE_URL . "/{$apikey}" ) . 'find'; - - return add_query_arg( $query_args, $base_url ); + return add_query_arg( $query_args, Parsely::get_dash_url( $site_id ) ); } /** @@ -61,6 +64,6 @@ public static function generate_url( WP_Post $post, string $apikey, string $camp * @return bool True if the link can be shown, false otherwise. */ public static function can_show_link( WP_Post $post, Parsely $parsely ): bool { - return Parsely::post_has_trackable_status( $post ) && is_post_type_viewable( $post->post_type ) && ! $parsely->api_key_is_missing(); + return Parsely::post_has_trackable_status( $post ) && is_post_type_viewable( $post->post_type ) && ! $parsely->site_id_is_missing(); } } diff --git a/src/class-metadata.php b/src/class-metadata.php index 07d0acd39..f9e3f559d 100644 --- a/src/class-metadata.php +++ b/src/class-metadata.php @@ -21,11 +21,48 @@ use Parsely\Metadata\Tag_Builder; use WP_Post; +use function Parsely\Utils\get_page_for_posts; +use function Parsely\Utils\get_page_on_front; + /** * Generates and inserts metadata readable by the Parse.ly Crawler. * * @since 1.0.0 * @since 3.3.0 Logic extracted from Parsely\Parsely class to separate file/class. + * + * @phpstan-type Metadata_Attributes array{ + * '@id'?: string, + * '@type'?: string, + * headline?: string, + * url?: string, + * image?: Metadata_Image, + * thumbnailUrl?: string, + * articleSection?: string, + * creator?: string[], + * author?: Metadata_Author[], + * publisher?: Metadata_Publisher, + * keywords?: string[], + * dateCreated?: string, + * datePublished?: string, + * dateModified?: string, + * custom_metadata?: string, + * } + * + * @phpstan-type Metadata_Image array{ + * '@type': 'ImageObject', + * url: string, + * } + * + * @phpstan-type Metadata_Author array{ + * '@type': 'Person', + * name: string, + * } + * + * @phpstan-type Metadata_Publisher array{ + * '@type': 'Organization', + * name: string, + * logo: string, + * } */ class Metadata { /** @@ -49,9 +86,9 @@ public function __construct( Parsely $parsely ) { * * @param WP_Post $post object. * - * @return array + * @return Metadata_Attributes */ - public function construct_metadata( WP_Post $post ): array { + public function construct_metadata( WP_Post $post ) { $options = $this->parsely->get_options(); $queried_object_id = get_queried_object_id(); @@ -61,12 +98,12 @@ public function construct_metadata( WP_Post $post ): array { } else { $builder = new Paginated_Front_Page_Builder( $this->parsely ); } - } elseif ( 'page' === get_option( 'show_on_front' ) && ! get_option( 'page_on_front' ) ) { + } elseif ( 'page' === get_option( 'show_on_front' ) && ! get_page_on_front() ) { $builder = new Front_Page_Builder( $this->parsely ); } elseif ( is_home() && ( - ! ( 'page' === get_option( 'show_on_front' ) && ! get_option( 'page_on_front' ) ) || - $queried_object_id && (int) get_option( 'page_for_posts' ) === $queried_object_id + ! ( 'page' === get_option( 'show_on_front' ) && ! get_page_on_front() ) || + get_page_for_posts() === $queried_object_id ) ) { $builder = new Page_For_Posts_Builder( $this->parsely ); @@ -93,6 +130,8 @@ public function construct_metadata( WP_Post $post ): array { /** * Filters the structured metadata. * + * @var mixed + * * @param array $parsely_page Existing structured metadata for a page. * @param WP_Post $post Post object. * @param array $options The Parse.ly options. diff --git a/src/class-parsely.php b/src/class-parsely.php index 46d16bfc1..e377fbfa8 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -18,21 +18,47 @@ * * @since 1.0.0 * @since 2.5.0 Moved from plugin root file to this file. + * + * @phpstan-type Parsely_Options array{ + * apikey: string, + * content_id_prefix: string, + * api_secret: string, + * use_top_level_cats: bool, + * custom_taxonomy_section: string, + * cats_as_tags: bool, + * track_authenticated_users: bool, + * lowercase_tags: bool, + * force_https_canonicals: bool, + * track_post_types: string[], + * track_page_types: string[], + * track_post_types_as?: array, + * disable_javascript: bool, + * disable_amp: bool, + * meta_type: string, + * logo: string, + * metadata_secret: string, + * parsely_wipe_metadata_cache: bool, + * disable_autotrack: bool, + * plugin_version: string, + * } + * + * @phpstan-import-type Metadata_Attributes from Metadata */ class Parsely { /** * Declare our constants */ - public const VERSION = PARSELY_VERSION; - public const MENU_SLUG = 'parsely'; // Defines the page param passed to options-general.php. - public const OPTIONS_KEY = 'parsely'; // Defines the key used to store options in the WP database. - public const CAPABILITY = 'manage_options'; // The capability required for the user to administer settings. - public const DASHBOARD_BASE_URL = 'https://dash.parsely.com'; + public const VERSION = PARSELY_VERSION; + public const MENU_SLUG = 'parsely'; // Defines the page param passed to options-general.php. + public const OPTIONS_KEY = 'parsely'; // Defines the key used to store options in the WP database. + public const CAPABILITY = 'manage_options'; // The capability required for the user to administer settings. + public const DASHBOARD_BASE_URL = 'https://dash.parsely.com'; + public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2'; /** * Declare some class properties * - * @var array $option_defaults The defaults we need for the class. + * @var Parsely_Options $option_defaults The defaults we need for the class. */ private $option_defaults = array( 'apikey' => '', @@ -53,6 +79,7 @@ class Parsely { 'metadata_secret' => '', 'parsely_wipe_metadata_cache' => false, 'disable_autotrack' => false, + 'plugin_version' => '', ); /** @@ -90,6 +117,21 @@ class Parsely { 'Movie', ); + /** + * Declare all supported types (both post and non-post types). + * + * @since 3.7.0 + * @var string[] + */ + private static $all_supported_types; + + /** + * Constructor. + */ + public function __construct() { + self::$all_supported_types = array_merge( self::SUPPORTED_JSONLD_POST_TYPES, self::SUPPORTED_JSONLD_NON_POST_TYPES ); + } + /** * Registers action and filter hook callbacks, and immediately upgrades * options if needed. @@ -97,10 +139,16 @@ class Parsely { public function run(): void { // Run upgrade options if they exist for the version currently defined. $options = $this->get_options(); - if ( empty( $options['plugin_version'] ) || self::VERSION !== $options['plugin_version'] ) { + if ( self::VERSION !== $options['plugin_version'] ) { $method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION ); if ( method_exists( $this, $method ) ) { - call_user_func_array( array( $this, $method ), array( $options ) ); + /** + * Variable. + * + * @var callable + */ + $callable = array( $this, $method ); + call_user_func_array( $callable, array( $options ) ); } // Update our version info. $options['plugin_version'] = self::VERSION; @@ -116,8 +164,9 @@ public function run(): void { /** * Adds 10 minute cron interval. * - * @param array $schedules WP schedules array. - * @return array + * @param array $schedules WP schedules array. + * + * @return array */ public function wpparsely_add_cron_interval( array $schedules ): array { $schedules['everytenminutes'] = array( @@ -136,8 +185,8 @@ public function wpparsely_add_cron_interval( array $schedules ): array { * @return string */ public function get_tracker_url(): string { - if ( $this->api_key_is_set() ) { - $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_api_key() . '/p.js'; + if ( $this->site_id_is_set() ) { + $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js'; return esc_url( $tracker_url ); } return ''; @@ -237,9 +286,10 @@ public static function post_has_trackable_status( $post ): bool { * * @param array $parsely_options parsely_options array. * @param WP_Post $post object. - * @return array + * + * @return Metadata_Attributes */ - public function construct_parsely_metadata( array $parsely_options, WP_Post $post ): array { + public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) { _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' ); $metadata = new Metadata( $this ); return $metadata->construct_metadata( $post ); @@ -252,7 +302,7 @@ public function construct_parsely_metadata( array $parsely_options, WP_Post $pos */ public function update_metadata_endpoint( int $post_id ): void { $parsely_options = $this->get_options(); - if ( $this->api_key_is_missing() || empty( $parsely_options['metadata_secret'] ) ) { + if ( $this->site_id_is_missing() || '' === $parsely_options['metadata_secret'] ) { return; } @@ -264,17 +314,17 @@ public function update_metadata_endpoint( int $post_id ): void { $metadata = ( new Metadata( $this ) )->construct_metadata( $post ); $endpoint_metadata = array( - 'canonical_url' => $metadata['url'], - 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ), - 'title' => $metadata['headline'], - 'image_url' => $metadata['image']['url'], - 'pub_date_tmsp' => $metadata['datePublished'], - 'section' => $metadata['articleSection'], - 'authors' => $metadata['creator'], - 'tags' => $metadata['keywords'], + 'canonical_url' => $metadata['url'] ?? '', + 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ), + 'title' => $metadata['headline'] ?? '', + 'image_url' => isset( $metadata['image']['url'] ) ? $metadata['image']['url'] : '', + 'pub_date_tmsp' => $metadata['datePublished'] ?? '', + 'section' => $metadata['articleSection'] ?? '', + 'authors' => $metadata['creator'] ?? '', + 'tags' => $metadata['keywords'] ?? '', ); - $parsely_api_endpoint = 'https://api.parsely.com/v2/metadata/posts'; + $parsely_api_endpoint = self::PUBLIC_API_BASE_URL . '/metadata/posts'; $parsely_metadata_secret = $parsely_options['metadata_secret']; $headers = array( 'Content-Type' => 'application/json', @@ -282,7 +332,7 @@ public function update_metadata_endpoint( int $post_id ): void { $body = wp_json_encode( array( 'secret' => $parsely_metadata_secret, - 'apikey' => $parsely_options['apikey'], + 'apikey' => $this->get_site_id(), 'metadata' => $endpoint_metadata, ) ); @@ -308,8 +358,7 @@ public function update_metadata_endpoint( int $post_id ): void { */ public function bulk_update_posts(): void { global $wpdb; - $parsely_options = $this->get_options(); - $allowed_types = array_merge( $parsely_options['track_post_types'], $parsely_options['track_page_types'] ); + $allowed_types = $this->get_all_track_types(); $allowed_types_string = implode( ', ', array_map( @@ -319,7 +368,13 @@ function( $v ) { $allowed_types ) ); - $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' ); + + /** + * Variable. + * + * @var int[]|false + */ + $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' ); if ( false === $ids ) { $ids = array(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery @@ -350,9 +405,14 @@ function( $v ) { * As soon as actual options are saved, they override the defaults. This * prevents us from having to do a lot of isset() checking on variables. * - * @return array + * @return Parsely_Options */ - public function get_options(): array { + public function get_options() { + /** + * Variable. + * + * @var Parsely_Options|null + */ $options = get_option( self::OPTIONS_KEY, $this->option_defaults ); if ( ! is_array( $options ) ) { @@ -374,6 +434,27 @@ public static function get_settings_url( int $_blog_id = null ): string { return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG ); } + /** + * Returns the URL of the Parse.ly dashboard for a specific page. If a page + * is not specified, the home dashboard URL for the specified Site ID is + * returned. + * + * @since 3.7.0 + * + * @param string $site_id The Site ID for which to get the URL. + * @param string $page_url Optional. The page for which to get the URL. + * @return string The complete dashboard URL. + */ + public static function get_dash_url( string $site_id, string $page_url = '' ): string { + $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find'; + + if ( '' !== $page_url ) { + $result .= '?url=' . rawurlencode( $page_url ); + } + + return $result; + } + /** * Checks to see if the current user is a member of the current blog. * @@ -406,44 +487,43 @@ public function convert_jsonld_to_parsely_type( string $type ): string { } /** - * Determines if an API key is saved in the options. + * Determines if a Site ID is saved in the options. * * @since 2.6.0 + * @since 3.7.0 renamed from api_key_is_set * - * @return bool True is API key is set, false if it is missing. + * @return bool True is Site ID is set, false if it is missing. */ - public function api_key_is_set(): bool { + public function site_id_is_set(): bool { $options = $this->get_options(); - return ( - isset( $options['apikey'] ) && - is_string( $options['apikey'] ) && - '' !== $options['apikey'] - ); + return '' !== $options['apikey']; } /** - * Determines if an API key is not saved in the options. + * Determines if a Site ID is not saved in the options. * * @since 2.6.0 + * @since 3.7.0 renamed from api_key_is_missing * - * @return bool True if API key is missing, false if it is set. + * @return bool True if Site ID is missing, false if it is set. */ - public function api_key_is_missing(): bool { - return ! $this->api_key_is_set(); + public function site_id_is_missing(): bool { + return ! $this->site_id_is_set(); } /** - * Gets the API key if set. + * Gets the Site ID if set. * * @since 2.6.0 + * @since 3.7.0 renamed from get_site_id * - * @return string API key if set, or empty string if not. + * @return string Site ID if set, or empty string if not. */ - public function get_api_key(): string { + public function get_site_id(): string { $options = $this->get_options(); - return $this->api_key_is_set() ? $options['apikey'] : ''; + return $this->site_id_is_set() ? $options['apikey'] : ''; } /** @@ -456,11 +536,7 @@ public function get_api_key(): string { public function api_secret_is_set(): bool { $options = $this->get_options(); - return ( - isset( $options['api_secret'] ) && - is_string( $options['api_secret'] ) && - '' !== $options['api_secret'] - ); + return '' !== $options['api_secret']; } /** @@ -475,4 +551,28 @@ public function get_api_secret(): string { return $this->api_secret_is_set() ? $options['api_secret'] : ''; } + + /** + * Returns all supported post and non-post types. + * + * @since 3.7.0 + * + * @return string[] all supported types + */ + public function get_all_supported_types(): array { + return self::$all_supported_types; + } + + /** + * Gets all tracked post types. + * + * @since 3.7.0 + * + * @return array + */ + public function get_all_track_types(): array { + $options = $this->get_options(); + + return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) ); + } } diff --git a/src/class-scripts.php b/src/class-scripts.php index b34ca406c..3eb5b497f 100644 --- a/src/class-scripts.php +++ b/src/class-scripts.php @@ -40,14 +40,14 @@ public function __construct( Parsely $parsely ) { */ public function run(): void { $parsely_options = $this->parsely->get_options(); - if ( $this->parsely->api_key_is_set() && true !== $parsely_options['disable_javascript'] ) { + if ( $this->parsely->site_id_is_set() && true !== $parsely_options['disable_javascript'] ) { add_action( 'init', array( $this, 'register_scripts' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_js_tracker' ) ); } } /** - * Registers scripts, if there's an API key value saved. + * Registers scripts, if there's a Site ID value saved. * * @since 2.5.0 * @since 3.0.0 Rename from register_js @@ -61,12 +61,12 @@ public function register_scripts(): void { true ); - $loader_asset = require plugin_dir_path( PARSELY_FILE ) . 'build/loader.asset.php'; + $loader_asset = require_once plugin_dir_path( PARSELY_FILE ) . 'build/loader.asset.php'; wp_register_script( 'wp-parsely-loader', plugin_dir_url( PARSELY_FILE ) . 'build/loader.js', - $loader_asset['dependencies'], - $loader_asset['version'], + $loader_asset['dependencies'] ?? null, + $loader_asset['version'] ?? Parsely::VERSION, true ); } @@ -116,14 +116,14 @@ public function enqueue_js_tracker(): void { wp_enqueue_script( 'wp-parsely-loader' ); wp_enqueue_script( 'wp-parsely-tracker' ); - // If we don't have an API secret, there's no need to set the API key. - // Setting the API key triggers the UUID Profile Call function. - if ( isset( $parsely_options['api_secret'] ) && is_string( $parsely_options['api_secret'] ) && '' !== $parsely_options['api_secret'] ) { - $js_api_key = "window.wpParselyApiKey = '" . esc_js( $this->parsely->get_api_key() ) . "';"; - wp_add_inline_script( 'wp-parsely-loader', $js_api_key, 'before' ); + // If we don't have an API secret, there's no need to set the Site ID. + // Setting the Site ID triggers the UUID Profile Call function. + if ( $this->parsely->api_secret_is_set() ) { + $js_site_id = "window.wpParselySiteId = '" . esc_js( $this->parsely->get_site_id() ) . "';"; + wp_add_inline_script( 'wp-parsely-loader', $js_site_id, 'before' ); } - if ( isset( $parsely_options['disable_autotrack'] ) && true === $parsely_options['disable_autotrack'] ) { + if ( true === $parsely_options['disable_autotrack'] ) { $disable_autotrack = 'window.wpParselyDisableAutotrack = true;'; wp_add_inline_script( 'wp-parsely-loader', $disable_autotrack, 'before' ); } @@ -140,7 +140,6 @@ public function enqueue_js_tracker(): void { * @return string Amended `script` tag. */ public function script_loader_tag( string $tag, string $handle, string $src ): string { - $parsely_options = $this->parsely->get_options(); if ( \in_array( $handle, array( @@ -167,11 +166,14 @@ public function script_loader_tag( string $tag, string $handle, string $src ): s if ( null !== $tag && 'wp-parsely-tracker' === $handle ) { $tag = preg_replace( '/ id=(["\'])wp-parsely-tracker-js\1/', ' id="parsely-cfg"', $tag ); - $tag = str_replace( - ' src=', - ' data-parsely-site="' . esc_attr( $parsely_options['apikey'] ) . '" src=', - $tag - ); + + if ( null !== $tag ) { + $tag = str_replace( + ' src=', + ' data-parsely-site="' . esc_attr( $this->parsely->get_site_id() ) . '" src=', + $tag + ); + } } return $tag ?? ''; diff --git a/src/content-helper/dashboard-widget/class-dashboard-widget.php b/src/content-helper/dashboard-widget/class-dashboard-widget.php new file mode 100644 index 000000000..541f2ab33 --- /dev/null +++ b/src/content-helper/dashboard-widget/class-dashboard-widget.php @@ -0,0 +1,85 @@ +is_user_allowed_to_make_api_call() ) { + return; + } + + add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + } + + /** + * Adds the Widget and its contents to the WordPress Dashboard. + * + * @since 3.7.0 + */ + public function add_dashboard_widget(): void { + wp_add_dashboard_widget( + 'wp-parsely-dashboard-widget', + __( 'Parse.ly Top Posts (Last 7 Days)', 'wp-parsely' ), + '__return_empty_string' // Content will be populated by JavaScript. + ); + } + + /** + * Enqueues the Dashboard Widget's assets. + * + * @since 3.7.0 + * + * @param string $hook_suffix The current admin page. + */ + public function enqueue_assets( $hook_suffix ): void { + if ( 'index.php' === $hook_suffix ) { + $asset_php = require_once plugin_dir_path( PARSELY_FILE ) . 'build/content-helper/dashboard-widget.asset.php'; + $built_assets_url = plugin_dir_url( PARSELY_FILE ) . 'build/content-helper/'; + + wp_enqueue_script( + 'wp-parsely-dashboard-widget', + $built_assets_url . 'dashboard-widget.js', + $asset_php['dependencies'] ?? null, + $asset_php['version'] ?? Parsely::VERSION, + true + ); + + wp_enqueue_style( + 'wp-parsely-dashboard-widget', + $built_assets_url . 'dashboard-widget.css', + array(), + $asset_php['version'] ?? Parsely::VERSION + ); + } + } + +} diff --git a/src/content-helper/dashboard-widget/dashboard-widget.scss b/src/content-helper/dashboard-widget/dashboard-widget.scss new file mode 100644 index 000000000..aff7614c4 --- /dev/null +++ b/src/content-helper/dashboard-widget/dashboard-widget.scss @@ -0,0 +1,125 @@ +@import "../../css/shared/variables"; +@import "../../css/shared/functions"; + +#wp-parsely-dashboard-widget { + + .parsely-spinner-wrapper { + display: flex; + justify-content: center; + margin: to_rem(103px) 0; + + svg { + height: 22px; + width: 22px; + } + } + + .parsely-contact-us { + margin-top: to_rem(15px) !important; + } + + p.parsely-error-hint { + color: var(--gray-700); + } +} + +#wp-parsely-dashboard-widget .parsely-top-posts-wrapper { + font-family: var(--base-font); + color: var(--base-text); + + .page-views-title { + margin-bottom: to_rem(4px); + text-align: right; + width: 100%; + } + + .parsely-top-post-content { + display: flex; + + // Number at left of thumbnails. + &::before { + content: counter(item) ""; + counter-increment: item; + padding-right: to_rem(8px); + } + + @media only screen and (max-width: 380px) { + + &::before { + content: ""; + padding-right: 0; + } + } + } + + .parsely-top-posts { + counter-reset: item; // Needed to increment post numbers. + list-style: none; + margin: 0; + } + + .parsely-top-post { + margin-bottom: to_rem(16px); + } + + .parsely-top-post-thumbnail { + height: 46px; + width: 46px; + + img { + height: 100%; + width: 100%; + } + } + + .parsely-top-post-data { + border-top: 1px solid var(--gray-300); + flex-grow: 1; // Take all remaining width. + margin-left: to_rem(8px); + padding-top: to_rem(4px); + } + + // This element can be a link or div. + .parsely-top-post-title { + color: var(--base-text); + font-size: to_rem(14px); + margin-right: to_rem(7px); + } + + a.parsely-top-post-title:hover { + color: var(--blue-550); + } + + .parsely-top-post-icon-link { + position: relative; + top: to_rem(4px); + + svg { + fill: #8d98a1; + margin-right: to_rem(3px); + + &:hover { + fill: var(--blue-550); + } + } + } + + .parsely-top-post-metadata { + margin: to_rem(4px) 0 0; + + >span { + color: var(--gray-500); + + &:not(:first-child) { + margin-left: to_rem(12px); + } + } + } + + .parsely-top-post-views { + float: right; + font-family: var(--numeric-font); + font-size: to_rem(18px); + padding-left: to_rem(10px); + } +} diff --git a/src/content-helper/dashboard-widget/dashboard-widget.tsx b/src/content-helper/dashboard-widget/dashboard-widget.tsx new file mode 100644 index 000000000..efc3dcf92 --- /dev/null +++ b/src/content-helper/dashboard-widget/dashboard-widget.tsx @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import { render } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import TopPostList from './top-posts/component-list'; + +window.addEventListener( + 'load', + function() { + render( + , + document.querySelector( '#wp-parsely-dashboard-widget > .inside' ) + ); + }, + false +); diff --git a/src/content-helper/dashboard-widget/provider.ts b/src/content-helper/dashboard-widget/provider.ts new file mode 100644 index 000000000..e597d005d --- /dev/null +++ b/src/content-helper/dashboard-widget/provider.ts @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { + ContentHelperError, + ContentHelperErrorCode, +} from '../../blocks/content-helper/content-helper-error'; +import { TopPostData } from './top-posts/model'; +import { + convertDateToString, + removeDaysFromDate, +} from '../../blocks/shared/utils/date'; + +/** + * The form of the response returned by the /stats/posts WordPress REST API + * endpoint. + */ +interface TopPostsApiResponse { + error?: Error; + data?: TopPostData[]; +} + +export const TOP_POSTS_DEFAULT_LIMIT = 3; +export const TOP_POSTS_DEFAULT_TIME_RANGE = 7; // In days. + +class DashboardWidgetProvider { + private dataPeriodStart: string; + private dataPeriodEnd: string; + + /** + * Constructor. + */ + constructor() { + this.dataPeriodEnd = convertDateToString( new Date() ) + 'T23:59'; + this.dataPeriodStart = removeDaysFromDate( + this.dataPeriodEnd, + TOP_POSTS_DEFAULT_TIME_RANGE - 1 + ) + 'T00:00'; + } + + /** + * Returns the site's top posts. + * + * @return {Promise>} Object containing message and posts. + */ + public async getTopPosts(): Promise { + let data: TopPostData[] = []; + + try { + data = await this.fetchTopPostsFromWpEndpoint(); + } catch ( contentHelperError ) { + return Promise.reject( contentHelperError ); + } + + if ( 0 === data.length ) { + return Promise.reject( new ContentHelperError( + __( 'No Top Posts data is available.', 'wp-parsely' ), + ContentHelperErrorCode.ParselyApiReturnedNoData, + '' + ) ); + } + + return data; + } + + /** + * Fetches the site's top posts data from the WordPress REST API. + * + * @return {Promise>} Array of fetched posts. + */ + private async fetchTopPostsFromWpEndpoint(): Promise { + let response; + + try { + response = await apiFetch( { + path: addQueryArgs( '/wp-parsely/v1/stats/posts', { + limit: TOP_POSTS_DEFAULT_LIMIT, + period_start: this.dataPeriodStart, + period_end: this.dataPeriodEnd, + } ), + } ) as TopPostsApiResponse; + } catch ( wpError: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any + return Promise.reject( new ContentHelperError( + wpError.message, wpError.code + ) ); + } + + if ( response?.error ) { + return Promise.reject( new ContentHelperError( + response.error.message, + ContentHelperErrorCode.ParselyApiResponseContainsError + ) ); + } + + return response?.data || []; + } +} + +export default DashboardWidgetProvider; diff --git a/src/content-helper/dashboard-widget/top-posts/component-list-item.tsx b/src/content-helper/dashboard-widget/top-posts/component-list-item.tsx new file mode 100644 index 000000000..a22e2c4bf --- /dev/null +++ b/src/content-helper/dashboard-widget/top-posts/component-list-item.tsx @@ -0,0 +1,124 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { TopPostData } from './model'; +import { formatToImpreciseNumber } from '../../../blocks/shared/functions'; +import OpenLinkIcon from '../../../blocks/content-helper/icons/open-link-icon'; +import { getSmartShortDate } from '../../../blocks/shared/utils/date'; +import EditIcon from '../../../blocks/content-helper/icons/edit-icon'; +import { getPostEditUrl } from '../../../blocks/shared/utils/post'; + +interface TopPostListItemProps { + post: TopPostData; +} + +/** + * Returns a single list item depicting a post. + * + * @param {TopPostData} post The Post to be shown. + */ +function TopPostListItem( { post }: TopPostListItemProps ): JSX.Element { + return ( +
    • +
      + + { getPostThumbnailElement( { post } ) } + +
      + + + + { __( 'Number of Views', 'wp-parsely' ) } + + { formatToImpreciseNumber( post.views.toString() ) } + + + { getPostTitleElement( { post } ) } + + + + { __( 'View Post (opens in new tab)', 'wp-parsely' ) } + + + + + { + 0 !== post.postId && + + + { __( 'Edit Post (opens in new tab)', 'wp-parsely' ) } + + + + } + +
      + + + { __( 'Date', 'wp-parsely' ) } + + { getSmartShortDate( new Date( post.date ) ) } + + + + { __( 'Author', 'wp-parsely' ) } + + { post.author } + +
      + +
      + +
      +
    • + ); +} + +/** + * Returns the Post thumbnail with its div container. Returns an empty div if + * the post has no thumbnail. + * + * @param {TopPostData} post The Post from which to get the data. + */ +function getPostThumbnailElement( { post }: TopPostListItemProps ): JSX.Element { + if ( post.thumbUrlMedium ) { + return ( +
      + { __( 'Thumbnail', 'wp-parsely' ) } + { +
      + ); + } + + return ( +
      + { + __( 'Post thumbnail not available', 'wp-parsely' ) + } +
      + ); +} + +/** + * Returns the Post title as a link (for editing the Post) or a div if the Post + * has no valid ID. + * + * @param {TopPostData} post The Post from which to get the data. + */ +function getPostTitleElement( { post }: TopPostListItemProps ): JSX.Element { + return ( + + + { __( 'View in Parse.ly (opens in new tab)', 'wp-parsely' ) } + + { post.title } + + ); +} + +export default TopPostListItem; diff --git a/src/content-helper/dashboard-widget/top-posts/component-list.tsx b/src/content-helper/dashboard-widget/top-posts/component-list.tsx new file mode 100644 index 000000000..9bb17e8b2 --- /dev/null +++ b/src/content-helper/dashboard-widget/top-posts/component-list.tsx @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Spinner } from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import DashboardWidgetProvider from '../provider'; +import TopPostListItem from './component-list-item'; +import { TopPostData } from './model'; +import { ContentHelperError } from '../../../blocks/content-helper/content-helper-error'; +import { getDateInUserLang, SHORT_DATE_FORMAT } from '../../../blocks/shared/utils/date'; + +const FETCH_RETRIES = 3; + +/** + * List of the top posts. + */ +function TopPostList() { + const [ loading, setLoading ] = useState( true ); + const [ error, setError ] = useState(); + const [ posts, setPosts ] = useState( [] ); + const provider = new DashboardWidgetProvider(); + + useEffect( () => { + const fetchPosts = async ( retries: number ) => { + provider.getTopPosts() + .then( ( result ): void => { + const mappedPosts: TopPostData[] = result.map( + ( post: TopPostData ): TopPostData => ( + { + ...post, + date: getDateInUserLang( new Date( post.date ), SHORT_DATE_FORMAT ), + } + ) + ); + + setPosts( mappedPosts ); + setLoading( false ); + } ) + .catch( async ( err ) => { + if ( retries > 0 ) { + await new Promise( ( r ) => setTimeout( r, 500 ) ); + await fetchPosts( retries - 1 ); + } else { + setLoading( false ); + setError( err ); + } + } ); + }; + + setLoading( true ); + fetchPosts( FETCH_RETRIES ); + + return (): void => { + setLoading( false ); + setPosts( [] ); + setError( undefined ); + }; + }, [] ); + + // Show error message. + if ( error ) { + return error.ProcessedMessage( 'parsely-top-posts-descr' ); + } + + // Show top posts list. + const postList: JSX.Element = ( +
        + { posts.map( ( post: TopPostData ): JSX.Element => ) } +
      + ); + + return ( + loading + ? ( +
      + +
      + ) + : ( +
      +
      { __( 'Page Views', 'wp-parsely' ) }
      + { postList } +
      + ) + ); +} + +export default TopPostList; diff --git a/src/content-helper/dashboard-widget/top-posts/model.ts b/src/content-helper/dashboard-widget/top-posts/model.ts new file mode 100644 index 000000000..b8bda7184 --- /dev/null +++ b/src/content-helper/dashboard-widget/top-posts/model.ts @@ -0,0 +1,11 @@ +export interface TopPostData { + author: string; + dashUrl: string; + date: string; + id: number; + postId: number; + thumbUrlMedium: string; + title: string; + url: string; + views: number; +} diff --git a/src/css/admin-parsely-stats.scss b/src/css/admin-parsely-stats.scss new file mode 100644 index 000000000..2fb0480f8 --- /dev/null +++ b/src/css/admin-parsely-stats.scss @@ -0,0 +1,21 @@ +.column-parsely-stats { + width: 200px; + + @media only screen and (max-width: 991px) { + width: 150px; + } + + .parsely-post-stats { + color: #959da5; + min-height: 54px; + line-height: 18px; + } + + .parsely-post-stats-placeholder { + letter-spacing: 2px; + } + + .parsely-post-page-views { + color: #000; + } +} diff --git a/src/css/admin-settings.css b/src/css/admin-settings.scss similarity index 100% rename from src/css/admin-settings.css rename to src/css/admin-settings.scss diff --git a/src/css/recommended-widget.css b/src/css/recommended-widget.scss similarity index 100% rename from src/css/recommended-widget.css rename to src/css/recommended-widget.scss diff --git a/src/blocks/shared/functions.scss b/src/css/shared/functions.scss similarity index 100% rename from src/blocks/shared/functions.scss rename to src/css/shared/functions.scss diff --git a/src/blocks/content-helper/variables.scss b/src/css/shared/variables.scss similarity index 78% rename from src/blocks/content-helper/variables.scss rename to src/css/shared/variables.scss index 51dbf79c9..250ced092 100644 --- a/src/blocks/content-helper/variables.scss +++ b/src/css/shared/variables.scss @@ -1,20 +1,27 @@ +/** SASS variables **/ +$html-font-size: 16px; // Used in functions.scss. + /** * This is a subset of the CSS variables defined in the Parse.ly dashboard. It * is sourced from 1.44/src/styles/base.scss and defines some additional * variables in the end of the file. */ - -.wp-parsely-content-helper { +.wp-parsely-content-helper, +#wp-parsely-dashboard-widget { /** Layout section - base.scss. **/ --base-font: "source-sans-pro", arial, sans-serif; --numeric-font: "ff-din-round-web", sans-serif; /** Category colors section - base scss. **/ + --gray-300: #edeeef; --gray-400: #d7dbdf; + --gray-500: #959da5; --gray-600: #586069; --gray-700: #444d56; + --gray-900: #24292e; --blue-500: #44a8e5; + --blue-550: #2596db; --green-500: #7bc01b; // ref-* variables to be used as HSL colors. --ref-direct: 205, 13%, 52%; @@ -24,6 +31,7 @@ --ref-other: 3, 76%, 58%; /** Theme colors section - base.scss. **/ + --base-text: var(--gray-900); --base-text-2: var(--gray-600); --base-3: var(--gray-400); --border: var(--gray-400); @@ -33,5 +41,6 @@ /** Additional variables. **/ --font-size--large: 1rem; --font-size--extra-large: 1.2rem; + --black: #000; --sidebar-black: #1e1e1e; } diff --git a/src/js/admin-parsely-stats.ts b/src/js/admin-parsely-stats.ts new file mode 100644 index 000000000..e5a5905d4 --- /dev/null +++ b/src/js/admin-parsely-stats.ts @@ -0,0 +1,115 @@ +import { ParselyAPIError, ParselyAPIErrorInfo } from './common.interface'; + +export interface ParselyPostsStatsResponse extends ParselyAPIError { + data: ParselyStatsMap | null; +} + +interface ParselyStats { + page_views?: string; + visitors?: string; + avg_time?: string; +} + +interface ParselyStatsMap { + [key: string]: ParselyStats; +} + +document.addEventListener( 'DOMContentLoaded', (): void => { + showParselyPostsStatsResponse(); +} ); + +/** + * Shows Parse.ly Post Stats or Error depending on response. + */ +export function showParselyPostsStatsResponse(): void { + updateParselyStatsPlaceholder(); + + if ( ! window.wpParselyPostsStatsResponse ) { + return; + } + + const response: ParselyPostsStatsResponse = JSON.parse( window.wpParselyPostsStatsResponse ); + + if ( response?.error ) { + showParselyStatsError( response.error ); + return; + } + + if ( response?.data ) { + showParselyStats( response.data ); + } +} + +/** + * Replaces Parse.ly Stats placeholder from default to differentiate while the API request + * is in progress or completed. + */ +function updateParselyStatsPlaceholder(): void { + getAllPostStatsElements()?.forEach( ( statsElement: Element ): void => { + statsElement.innerHTML = '—'; + } ); +} + +/** + * Shows Parse.ly Stats on available posts. + * + * @param {ParselyStatsMap} parselyStatsMap Object contains unique keys and Parse.ly Stats for posts. + */ +function showParselyStats( parselyStatsMap: ParselyStatsMap ): void { + if ( ! parselyStatsMap ) { + return; + } + + getAllPostStatsElements()?.forEach( ( statsElement: Element ): void => { + const statsKey = statsElement.getAttribute( 'data-stats-key' ); + + if ( statsKey === null || parselyStatsMap[ statsKey ] === undefined ) { + return; + } + + const stats: ParselyStats = parselyStatsMap[ statsKey ]; + statsElement.innerHTML = ''; + + if ( stats.page_views ) { + statsElement.innerHTML += `${ stats.page_views }
      `; + } + + if ( stats.visitors ) { + statsElement.innerHTML += `${ stats.visitors }
      `; + } + + if ( stats.avg_time ) { + statsElement.innerHTML += `${ stats.avg_time }
      `; + } + } ); +} + +/** + * Shows Parse.ly Stats error as WP Admin Error Notice. + * + * @param {ParselyAPIErrorInfo} parselyStatsError Object which contians info about error. + */ +function showParselyStatsError( parselyStatsError: ParselyAPIErrorInfo ): void { + const headerEndElement = document.querySelector( '.wp-header-end' ); // WP has this element before admin notices. + if ( headerEndElement === null ) { + return; + } + + headerEndElement.innerHTML += getWPAdminError( parselyStatsError.htmlMessage ); +} + +/** + * Gets all elements inside which we will show Parse.ly Stats. + */ +function getAllPostStatsElements(): NodeListOf { + return document.querySelectorAll( '.parsely-post-stats' ); +} + +/** + * Gets HTML for showing error message as WP Admin Error Notice. + * + * @param {string} htmlMessage Message to show inside notice. + */ +function getWPAdminError( htmlMessage = '' ): string { + return `
      ${ htmlMessage }
      `; +} diff --git a/src/js/admin-settings.js b/src/js/admin-settings.js deleted file mode 100644 index 359b798bf..000000000 --- a/src/js/admin-settings.js +++ /dev/null @@ -1,20 +0,0 @@ -document.querySelector( '.media-single-image button.browse' ).addEventListener( 'click', selectImage ); - -function selectImage() { - const optionName = this.dataset.option; - - const imageFrame = wp.media( { - multiple: false, - library: { - type: 'image', - }, - } ); - - imageFrame.on( 'select', function() { - const url = imageFrame.state().get( 'selection' ).first().toJSON().url; - const inputSelector = '#media-single-image-' + optionName + ' input.file-path'; - document.querySelector( inputSelector ).value = url; - } ); - - imageFrame.open(); -} diff --git a/src/js/admin-settings.ts b/src/js/admin-settings.ts new file mode 100644 index 000000000..a702f6c16 --- /dev/null +++ b/src/js/admin-settings.ts @@ -0,0 +1,24 @@ +document.querySelector( '.media-single-image button.browse' )?.addEventListener( 'click', selectImage ); + +function selectImage( event: Event ) { + const optionName = ( event.target as HTMLButtonElement ).dataset.option; + + const imageFrame = window.wp.media( { + multiple: false, + library: { + type: 'image', + }, + } ); + + imageFrame.on( 'select', function() { + const url = imageFrame.state().get( 'selection' ).first().toJSON().url; + const inputSelector: string = '#media-single-image-' + optionName + ' input.file-path'; + + const inputElement: HTMLInputElement | null = document.querySelector( inputSelector ); + if ( inputElement ) { + inputElement.value = url; + } + } ); + + imageFrame.open(); +} diff --git a/src/js/common.interface.ts b/src/js/common.interface.ts new file mode 100644 index 000000000..d9af186d5 --- /dev/null +++ b/src/js/common.interface.ts @@ -0,0 +1,9 @@ +export interface ParselyAPIError { + error: ParselyAPIErrorInfo | null; +} + +export interface ParselyAPIErrorInfo { + code: number; + message: string; + htmlMessage: string; +} diff --git a/src/js/lib/loader.js b/src/js/lib/loader.ts similarity index 90% rename from src/js/lib/loader.js rename to src/js/lib/loader.ts index 95051d83e..cf5659547 100644 --- a/src/js/lib/loader.js +++ b/src/js/lib/loader.ts @@ -8,14 +8,14 @@ export function wpParselyInitCustom() { * All functions enqueued on that hook will be executed on that event according to their priorities. Those * functions should not expect any parameters and shouldn't return any. */ - const customOnLoad = () => window.wpParselyHooks.doAction( 'wpParselyOnLoad' ); + const customOnLoad = () => window.wpParselyHooks?.doAction( 'wpParselyOnLoad' ); /** * The `wpParselyOnReady` hook gets called with the `onReady` event of the `window.PARSELY` object. * All functions enqueued on that hook will be executed on that event according to their priorities. Those * functions should not expect any parameters and shouldn't return any. */ - const customOnReady = () => window.wpParselyHooks.doAction( 'wpParselyOnReady' ); + const customOnReady = () => window.wpParselyHooks?.doAction( 'wpParselyOnReady' ); // Construct window.PARSELY object. if ( typeof window.PARSELY === 'object' ) { diff --git a/src/js/lib/personalization.js b/src/js/lib/personalization.ts similarity index 100% rename from src/js/lib/personalization.js rename to src/js/lib/personalization.ts diff --git a/src/js/lib/uuid-profile-call.js b/src/js/lib/uuid-profile-call.js deleted file mode 100644 index 434967e58..000000000 --- a/src/js/lib/uuid-profile-call.js +++ /dev/null @@ -1,18 +0,0 @@ -// Only enqueuing the action if the site has a defined API key. -if ( typeof window.wpParselyApiKey !== 'undefined' ) { - window.wpParselyHooks.addAction( 'wpParselyOnLoad', 'wpParsely', uuidProfileCall ); -} - -async function uuidProfileCall() { - const uuid = global.PARSELY?.config?.parsely_site_uuid; - - if ( ! ( window.wpParselyApiKey && uuid ) ) { - return; - } - - const url = `https://api.parsely.com/v2/profile?apikey=${ encodeURIComponent( - window.wpParselyApiKey - ) }&uuid=${ encodeURIComponent( uuid ) }&url=${ encodeURIComponent( window.location.href ) }`; - - return fetch( url ); -} diff --git a/src/js/lib/uuid-profile-call.ts b/src/js/lib/uuid-profile-call.ts new file mode 100644 index 000000000..a1c6eadaf --- /dev/null +++ b/src/js/lib/uuid-profile-call.ts @@ -0,0 +1,20 @@ +import { PUBLIC_API_BASE_URL } from '../../blocks/shared/utils/constants'; + +// Only enqueuing the action if the site has a defined Site ID. +if ( typeof window.wpParselySiteId !== 'undefined' ) { + window.wpParselyHooks?.addAction( 'wpParselyOnLoad', 'wpParsely', uuidProfileCall ); +} + +async function uuidProfileCall() { + const uuid = window.PARSELY?.config?.parsely_site_uuid; + + if ( ! ( window.wpParselySiteId && uuid ) ) { + return; + } + + const url = `${ PUBLIC_API_BASE_URL }/profile?apikey=${ encodeURIComponent( + window.wpParselySiteId + ) }&uuid=${ encodeURIComponent( uuid ) }&url=${ encodeURIComponent( window.location.href ) }`; + + return fetch( url ); +} diff --git a/src/js/widgets/recommended.js b/src/js/widgets/recommended.ts similarity index 75% rename from src/js/widgets/recommended.js rename to src/js/widgets/recommended.ts index 593d4d5a7..552cba25f 100644 --- a/src/js/widgets/recommended.js +++ b/src/js/widgets/recommended.ts @@ -8,7 +8,34 @@ import domReady from '@wordpress/dom-ready'; */ import { getUuidFromVisitorCookie } from '../lib/personalization'; -function constructUrl( apiUrl, permalink, personalized ) { +interface WidgetData { + data: { + [key: string]: WidgetRecommendation; + }; +} + +interface WidgetRecommendation { + title: string; + url: string; + author: string; + image_url: string; + thumb_url_medium: string; +} + +interface WidgetOptions { + url: string; + outerDiv: Element; + displayAuthor: boolean; + displayDirection: string | null; + imgDisplay: string | null; + widgetId: string | null; +} + +interface WidgetOptionsGroup { + [key: string]: WidgetOptions[]; +} + +function constructUrl( apiUrl: string, permalink: string, personalized: boolean ): string { if ( personalized ) { const uuid = getUuidFromVisitorCookie(); if ( uuid ) { @@ -19,9 +46,9 @@ function constructUrl( apiUrl, permalink, personalized ) { return `${ apiUrl }&url=${ encodeURIComponent( permalink ) }`; } -function constructWidget( widget ) { - const apiUrl = widget.getAttribute( 'data-parsely-widget-api-url' ); - const permalink = widget.getAttribute( 'data-parsely-widget-permalink' ); +function constructWidget( widget: Element ): WidgetOptions { + const apiUrl = widget.getAttribute( 'data-parsely-widget-api-url' ) || ''; + const permalink = widget.getAttribute( 'data-parsely-widget-permalink' ) || ''; const personalized = widget.getAttribute( 'data-parsely-widget-personalized' ) === 'true'; const url = constructUrl( apiUrl, permalink, personalized ); @@ -35,13 +62,13 @@ function constructWidget( widget ) { }; } -function renderWidget( data, { +function renderWidget( data: WidgetData, { outerDiv, displayAuthor, displayDirection, imgDisplay, widgetId, -} ) { +}: WidgetOptions ) { if ( imgDisplay !== 'none' ) { outerDiv.classList.add( 'display-thumbnail' ); } @@ -99,14 +126,14 @@ function renderWidget( data, { } outerDiv.appendChild( outerList ); - outerDiv.closest( '.widget.Recommended_Widget' ).classList.remove( 'parsely-recommended-widget-hidden' ); + outerDiv.closest( '.widget.Recommended_Widget' )?.classList.remove( 'parsely-recommended-widget-hidden' ); } domReady( () => { const widgetDOMElements = document.querySelectorAll( '.parsely-recommended-widget' ); - const widgetObjects = Array.from( widgetDOMElements ).map( constructWidget ); + const widgetObjects = Array.from( widgetDOMElements ).map( ( widget: Element ) => constructWidget( widget ) ); - const widgetsGroupedByUrl = widgetObjects.reduce( ( acc, curr ) => { + const widgetsGroupedByUrl: WidgetOptionsGroup = widgetObjects.reduce( ( acc: WidgetOptionsGroup, curr: WidgetOptions ): object => { if ( ! acc[ curr.url ] ) { acc[ curr.url ] = []; } @@ -118,7 +145,7 @@ domReady( () => { fetch( url ) .then( ( response ) => response.json() ) .then( ( data ) => { - widgets.forEach( ( widget ) => { + widgets.forEach( ( widget: WidgetOptions ) => { renderWidget( data, widget ); } ); } ); diff --git a/tests/Integration/Blocks/ContentHelperTest.php b/tests/Integration/Blocks/ContentHelperTest.php index 816b06773..0f76589a6 100644 --- a/tests/Integration/Blocks/ContentHelperTest.php +++ b/tests/Integration/Blocks/ContentHelperTest.php @@ -1,6 +1,6 @@ run(); self::assertTrue( wp_script_is( self::BLOCK_NAME ) ); self::assertTrue( wp_style_is( self::BLOCK_NAME ) ); diff --git a/tests/Integration/DashboardLinkTest.php b/tests/Integration/DashboardLinkTest.php index 71d6e1327..ceaa069bd 100644 --- a/tests/Integration/DashboardLinkTest.php +++ b/tests/Integration/DashboardLinkTest.php @@ -39,11 +39,11 @@ public function set_up(): void { */ public function test_generate_parsely_post_url(): void { $post_id = self::factory()->post->create(); - $post = get_post( $post_id ); - $apikey = 'demo-api-key'; + $post = $this->get_post( $post_id ); + $site_id = 'demo-site-id'; - $expected = PARSELY::DASHBOARD_BASE_URL . '/demo-api-key/find?url=http%3A%2F%2Fexample.org%2F%3Fp%3D' . $post_id . '&utm_campaign=wp-admin-posts-list&utm_source=wp-admin&utm_medium=wp-parsely'; - $actual = Dashboard_Link::generate_url( $post, $apikey, 'wp-admin-posts-list', 'wp-admin' ); + $expected = PARSELY::DASHBOARD_BASE_URL . '/demo-site-id/find?url=http%3A%2F%2Fexample.org%2F%3Fp%3D' . $post_id . '&utm_campaign=wp-admin-posts-list&utm_source=wp-admin&utm_medium=wp-parsely'; + $actual = Dashboard_Link::generate_url( $post, $site_id, 'wp-admin-posts-list', 'wp-admin' ); self::assertSame( $expected, $actual ); } @@ -60,11 +60,11 @@ public function test_generate_invalid_post_url(): void { add_filter( 'post_link', '__return_false' ); $post_id = self::factory()->post->create(); - $post = get_post( $post_id ); - $apikey = 'demo-api-key'; + $post = $this->get_post( $post_id ); + $site_id = 'demo-site-id'; $expected = ''; - $actual = Dashboard_Link::generate_url( $post, $apikey, 'wp-admin-posts-list', 'wp-admin' ); + $actual = Dashboard_Link::generate_url( $post, $site_id, 'wp-admin-posts-list', 'wp-admin' ); self::assertSame( $expected, $actual ); } @@ -76,8 +76,8 @@ public function test_generate_invalid_post_url(): void { * @since 3.1.0 Moved to `DashboardLinkTest.php` * * @covers \Parsely\Dashboard_Link::can_show_link - * @uses \Parsely\Parsely::api_key_is_set - * @uses \Parsely\Parsely::api_key_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::site_id_is_missing * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::post_has_trackable_status * @uses \Parsely\Parsely::update_metadata_endpoint @@ -99,8 +99,8 @@ public function test_can_correctly_determine_if_Parsely_link_can_be_shown(): voi * @since 3.1.0 Moved to `DashboardLinkTest.php` * * @covers \Parsely\Dashboard_Link::can_show_link - * @uses \Parsely\Parsely::api_key_is_set - * @uses \Parsely\Parsely::api_key_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::site_id_is_missing * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::post_has_trackable_status * @uses \Parsely\Parsely::update_metadata_endpoint @@ -120,8 +120,8 @@ public function test_can_correctly_determine_if_Parsely_link_can_be_shown_when_p * @since 3.1.0 Moved to `DashboardLinkTest.php` * * @covers \Parsely\Dashboard_Link::can_show_link - * @uses \Parsely\Parsely::api_key_is_set - * @uses \Parsely\Parsely::api_key_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::site_id_is_missing * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::post_has_trackable_status * @uses \Parsely\Parsely::update_metadata_endpoint @@ -142,14 +142,14 @@ public function test_can_correctly_determine_if_Parsely_link_can_be_shown_when_p * @since 3.1.0 Moved to `DashboardLinkTest.php` * * @covers \Parsely\Dashboard_Link::can_show_link - * @uses \Parsely\Parsely::api_key_is_set - * @uses \Parsely\Parsely::api_key_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::site_id_is_missing * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::post_has_trackable_status * @uses \Parsely\Parsely::update_metadata_endpoint * @group ui */ - public function test_can_correctly_determine_if_Parsely_link_can_be_shown_when_api_key_is_set_or_missing(): void { + public function test_can_correctly_determine_if_Parsely_link_can_be_shown_when_site_id_is_set_or_missing(): void { $published_post = self::factory()->post->create_and_get(); // Site ID is not set. diff --git a/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php b/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php index c37b0caab..6935713e6 100644 --- a/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php +++ b/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php @@ -4,8 +4,6 @@ * * @package Parsely\Tests * @since 3.5.0 - * - * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found */ declare(strict_types=1); @@ -15,9 +13,12 @@ use Parsely\Endpoints\Analytics_Posts_API_Proxy; use Parsely\Endpoints\Base_API_Proxy; use Parsely\Parsely; -use Parsely\RemoteAPI\Analytics_Posts_Proxy; +use Parsely\RemoteAPI\Analytics_Posts_API; +use WP_Error; use WP_REST_Request; +use function Parsely\Utils\get_date_format; + /** * Integration Tests for the Analytics Posts API Proxy Endpoint. */ @@ -39,7 +40,7 @@ public static function initialize(): void { public function get_endpoint(): Base_API_Proxy { return new Analytics_Posts_API_Proxy( new Parsely(), - new Analytics_Posts_Proxy( new Parsely() ) + new Analytics_Posts_API( new Parsely() ) ); } @@ -49,10 +50,10 @@ public function get_endpoint(): Base_API_Proxy { * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::run * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\RemoteAPI\Base_Proxy::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct */ public function test_register_routes_by_default(): void { - parent::test_register_routes_by_default(); + parent::run_test_register_routes_by_default(); } /** @@ -62,14 +63,15 @@ public function test_register_routes_by_default(): void { * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::run * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\RemoteAPI\Base_Proxy::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct */ - public function test_do_not_register_route_when_proxy_is_disabled(): void { - parent::test_do_not_register_route_when_proxy_is_disabled(); + public function test_verify_that_route_is_not_registered_when_proxy_is_disabled(): void { + parent::run_test_do_not_register_route_when_proxy_is_disabled(); } /** - * Verifies forbidden error when current user doesn't have proper capabilities. + * Verifies forbidden error when current user doesn't have proper + * capabilities. * * @covers \Parsely\Endpoints\Base_API_Proxy::permission_callback * @@ -77,33 +79,115 @@ public function test_do_not_register_route_when_proxy_is_disabled(): void { * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::register_endpoint */ public function test_access_of_analytics_posts_endpoint_is_forbidden(): void { - $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', self::$route ) ); - $error = $response->as_error(); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', self::$route ) + ); + /** + * Variable. + * + * @var WP_Error + */ + $error = $response->as_error(); self::assertSame( 401, $response->get_status() ); self::assertSame( 'rest_forbidden', $error->get_error_code() ); - self::assertSame( 'Sorry, you are not allowed to do that.', $error->get_error_message() ); + self::assertSame( + 'Sorry, you are not allowed to do that.', + $error->get_error_message() + ); } /** * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an - * error and does not perform a remote call when the apikey is not populated + * error and does not perform a remote call when the Site ID is not populated * in site options. * * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::permission_callback * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options - * @uses \Parsely\RemoteAPI\Base_Proxy::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct * @uses \Parsely\Endpoints\Base_API_Proxy::get_data * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint */ - public function test_get_items_fails_without_apikey_set() { + public function test_get_items_fails_when_site_id_is_not_set(): void { $this->set_admin_user(); - parent::test_get_items_fails_without_apikey_set(); + parent::run_test_get_items_fails_without_site_id_set(); + } + + /** + * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an + * error and does not perform a remote call when the API Secret is not + * populated in site options. + * + * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items + * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct + * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::permission_callback + * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\Endpoints\Base_API_Proxy::get_data + * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint + */ + public function test_get_items_fails_when_api_secret_is_not_set(): void { + $this->set_admin_user(); + parent::run_test_get_items_fails_without_api_secret_set(); + } + + /** + * Verifies default user capability filter. + * + * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::permission_callback + * + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call + */ + public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { + $this->login_as_contributor(); + add_filter( + 'wp_parsely_user_capability_for_all_private_apis', + function () { + return 'edit_posts'; + } + ); + + $proxy_api = new Analytics_Posts_API_Proxy( + new Parsely(), + new Analytics_Posts_API( new Parsely() ) + ); + + self::assertTrue( $proxy_api->permission_callback() ); + } + + /** + * Verifies endpoint specific user capability filter. + * + * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::permission_callback + * + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call + */ + public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { + $this->login_as_contributor(); + add_filter( + 'wp_parsely_user_capability_for_analytics_posts_api', + function () { + return 'edit_posts'; + } + ); + + $proxy_api = new Analytics_Posts_API_Proxy( + new Parsely(), + new Analytics_Posts_API( new Parsely() ) + ); + + self::assertTrue( $proxy_api->permission_callback() ); } /** @@ -117,30 +201,51 @@ public function test_get_items_fails_without_apikey_set() { * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run * @uses \Parsely\Endpoints\Base_API_Proxy::get_data * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_api_key + * @uses \Parsely\Parsely::get_site_id * @uses \Parsely\Parsely::get_api_secret * @uses \Parsely\Parsely::get_options - * @uses \Parsely\RemoteAPI\Base_Proxy::__construct - * @uses \Parsely\RemoteAPI\Base_Proxy::get_api_url - * @uses \Parsely\RemoteAPI\Base_Proxy::get_items + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::get_api_url + * @uses \Parsely\RemoteAPI\Remote_API_Base::get_items */ - public function test_get_items() { - TestCase::set_options( array( 'apikey' => 'example.com' ) ); - TestCase::set_options( array( 'api_secret' => 'test' ) ); + public function test_get_items(): void { $this->set_admin_user(); + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test', + ) + ); $dispatched = 0; - $date_format = get_option( 'date_format' ); + $date_format = get_date_format(); add_filter( 'pre_http_request', function () use ( &$dispatched ) { $dispatched++; return array( - 'body' => '{"data":[{"_hits": 142, "author": "Aakash Shah", "authors": ["Aakash Shah"], "full_content_word_count": 3624, "image_url": "https://blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png?w=150&h=150&crop=1", "link": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api", "metadata": "", "metrics": {"views": 142}, "pub_date": "2020-04-06T13:30:58", "section": "Analytics That Matter", "tags": ["animalz", "parsely_smart:entity:Bounce rate", "parsely_smart:entity:Customer analytics", "parsely_smart:entity:Digital marketing", "parsely_smart:entity:Google Analytics", "parsely_smart:entity:Marketing strategy", "parsely_smart:entity:Multivariate testing in marketing", "parsely_smart:entity:Open source", "parsely_smart:entity:Pageview", "parsely_smart:entity:Search engine optimization", "parsely_smart:entity:Social media", "parsely_smart:entity:Social media analytics", "parsely_smart:entity:Usability", "parsely_smart:entity:User experience design", "parsely_smart:entity:Web analytics", "parsely_smart:entity:Web traffic", "parsely_smart:entity:Website", "parsely_smart:entity:World Wide Web", "parsely_smart:iab:Business", "parsely_smart:iab:Graphics", "parsely_smart:iab:Software", "parsely_smart:iab:Technology"], "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1", "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need", "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api"}, {"_hits": 40, "author": "Stephanie Schwartz and Andrew Butler", "authors": ["Stephanie Schwartz and Andrew Butler"], "full_content_word_count": 1785, "image_url": "https://blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg?w=150&h=150&crop=1", "link": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api", "metadata": "", "metrics": {"views": 40}, "pub_date": "2021-04-30T20:30:24", "section": "Analytics That Matter", "tags": ["parsely_smart:entity:Analytics", "parsely_smart:entity:Best practice", "parsely_smart:entity:Hashtag", "parsely_smart:entity:Metadata", "parsely_smart:entity:Search engine", "parsely_smart:entity:Search engine optimization", "parsely_smart:entity:Tag (metadata)", "parsely_smart:iab:Business", "parsely_smart:iab:Science", "parsely_smart:iab:Software", "parsely_smart:iab:Technology"], "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1", "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy", "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api"}]}', + 'body' => '{"data":[ + { + "author": "Aakash Shah", + "metrics": {"views": 142}, + "pub_date": "2020-04-06T13:30:58", + "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1", + "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need", + "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api" + }, + { + "author": "Stephanie Schwartz and Andrew Butler", + "metrics": {"views": 40}, + "pub_date": "2021-04-30T20:30:24", + "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1", + "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy", + "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api" + } + ]}', ); } ); @@ -153,22 +258,26 @@ function () use ( &$dispatched ) { (object) array( 'data' => array( (object) array( - 'author' => 'Aakash Shah', - 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ), - 'id' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api', - 'statsUrl' => PARSELY::DASHBOARD_BASE_URL . '/blog.parsely.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F%3Fitm_source%3Dparsely-api', - 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need', - 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api', - 'views' => 142, + 'author' => 'Aakash Shah', + 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ), + 'id' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api', + 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F%3Fitm_source%3Dparsely-api', + 'thumbUrlMedium' => 'https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1', + 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need', + 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api', + 'views' => 142, + 'postId' => 0, ), (object) array( - 'author' => 'Stephanie Schwartz and Andrew Butler', - 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ), - 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api', - 'statsUrl' => PARSELY::DASHBOARD_BASE_URL . '/blog.parsely.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F%3Fitm_source%3Dparsely-api', - 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy', - 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api', - 'views' => 40, + 'author' => 'Stephanie Schwartz and Andrew Butler', + 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ), + 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api', + 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F%3Fitm_source%3Dparsely-api', + 'thumbUrlMedium' => 'https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1', + 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy', + 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api', + 'views' => 40, + 'postId' => 0, ), ), ), diff --git a/tests/Integration/Endpoints/GraphQLMetadataTest.php b/tests/Integration/Endpoints/GraphQLMetadataTest.php index 1cc26ff4e..1d53747f9 100644 --- a/tests/Integration/Endpoints/GraphQLMetadataTest.php +++ b/tests/Integration/Endpoints/GraphQLMetadataTest.php @@ -49,7 +49,7 @@ public function set_up(): void { * * @covers \Parsely\Endpoints\GraphQL_Metadata::run * @uses \Parsely\Endpoints\Metadata_Endpoint::__construct - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options */ public function test_graphql_enqueued(): void { @@ -76,16 +76,16 @@ public function test_graphql_enqueued_filter(): void { } /** - * Verifies that GraphQL types are not registered if there's no API key. + * Verifies that GraphQL types are not registered if there's no Site ID. * * @since 3.2.0 * * @covers \Parsely\Endpoints\GraphQL_Metadata::run * @uses \Parsely\Endpoints\Metadata_Endpoint::__construct - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options */ - public function test_graphql_enqueued_no_api_key(): void { + public function test_graphql_enqueued_no_site_id(): void { self::$graphql->run(); self::assertFalse( has_filter( 'graphql_register_types', array( self::$graphql, 'register_meta' ) ) ); } diff --git a/tests/Integration/Endpoints/ReferrersPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/ReferrersPostDetailProxyEndpointTest.php new file mode 100644 index 000000000..b78337b4b --- /dev/null +++ b/tests/Integration/Endpoints/ReferrersPostDetailProxyEndpointTest.php @@ -0,0 +1,331 @@ +set_admin_user(); + parent::run_test_get_items_fails_without_site_id_set(); + } + + /** + * Verifies that calling `GET /wp-parsely/v1/referrers/post/detail` returns + * an error and does not perform a remote call when the Site ID is not + * populated in site options. + * + * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items + * @uses \Parsely\Endpoints\Base_API_Proxy::get_data + * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint + * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct + * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::permission_callback + * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + */ + public function test_get_items_fails_when_api_secret_is_not_set(): void { + $this->set_admin_user(); + parent::run_test_get_items_fails_without_api_secret_set(); + } + + /** + * Verifies default user capability filter. + * + * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::permission_callback + * + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call + */ + public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { + $this->login_as_contributor(); + add_filter( + 'wp_parsely_user_capability_for_all_private_apis', + function () { + return 'edit_posts'; + } + ); + + $proxy_api = new Referrers_Post_Detail_API_Proxy( + new Parsely(), + new Referrers_Post_Detail_API( new Parsely() ) + ); + + self::assertTrue( $proxy_api->permission_callback() ); + } + + /** + * Verifies endpoint specific user capability filter. + * + * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::permission_callback + * + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call + */ + public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { + $this->login_as_contributor(); + add_filter( + 'wp_parsely_user_capability_for_referrers_post_detail_api', + function () { + return 'edit_posts'; + } + ); + + $proxy_api = new Referrers_Post_Detail_API_Proxy( + new Parsely(), + new Referrers_Post_Detail_API( new Parsely() ) + ); + + self::assertTrue( $proxy_api->permission_callback() ); + } + + /** + * Verifies that calls to `GET /wp-parsely/v1/referrers/post/detail` return + * results in the expected format. + * + * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items + * @uses \Parsely\Endpoints\Base_API_Proxy::get_data + * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint + * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct + * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::generate_data + * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::permission_callback + * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::get_api_url + * @uses \Parsely\RemoteAPI\Remote_API_Base::get_items + */ + public function test_get_items(): void { + $this->set_admin_user(); + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test', + ) + ); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ) { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "metrics": {"referrers_views": 1500}, + "name": "google", + "type": "search" + }, + { + "metrics": {"referrers_views": 100}, + "name": "blog.parse.ly", + "type": "internal" + }, + { + "metrics": {"referrers_views": 50}, + "name": "bing", + "type": "search" + }, + { + "metrics": {"referrers_views": 30}, + "name": "facebook.com", + "type": "social" + }, + { + "metrics": {"referrers_views": 10}, + "name": "okt.to", + "type": "other" + }, + { + "metrics": {"referrers_views": 10}, + "name": "yandex", + "type": "search" + }, + { + "metrics": {"referrers_views": 10}, + "name": "parse.ly", + "type": "internal" + }, + { + "metrics": {"referrers_views": 10}, + "name": "yahoo!", + "type": "search" + }, + { + "metrics": {"referrers_views": 5}, + "name": "site1.com", + "type": "other" + }, + { + "metrics": {"referrers_views": 5}, + "name": "link.site2.com", + "type": "other" + } + ]}', + ); + } + ); + + $expected_top = (object) array( + 'direct' => (object) array( + 'views' => '770', + 'viewsPercentage' => '30.80', + 'datasetViewsPercentage' => '31.43', + ), + 'google' => (object) array( + 'views' => '1,500', + 'viewsPercentage' => '60.00', + 'datasetViewsPercentage' => '61.22', + ), + 'blog.parse.ly' => (object) array( + 'views' => '100', + 'viewsPercentage' => '4.00', + 'datasetViewsPercentage' => '4.08', + ), + 'bing' => (object) array( + 'views' => '50', + 'viewsPercentage' => '2.00', + 'datasetViewsPercentage' => '2.04', + ), + 'facebook.com' => (object) array( + 'views' => '30', + 'viewsPercentage' => '1.20', + 'datasetViewsPercentage' => '1.22', + ), + 'totals' => (object) array( + 'views' => '2,450', + 'viewsPercentage' => '98.00', + 'datasetViewsPercentage' => '100.00', + ), + ); + + $expected_types = (object) array( + 'social' => (object) array( + 'views' => '30', + 'viewsPercentage' => '1.20', + ), + 'search' => (object) array( + 'views' => '1,570', + 'viewsPercentage' => '62.80', + ), + 'other' => (object) array( + 'views' => '20', + 'viewsPercentage' => '0.80', + ), + 'internal' => (object) array( + 'views' => '110', + 'viewsPercentage' => '4.40', + ), + 'direct' => (object) array( + 'views' => '770', + 'viewsPercentage' => '30.80', + ), + 'totals' => (object) array( + 'views' => '2,500', + 'viewsPercentage' => '100.00', + ), + ); + + $request = new WP_REST_Request( 'GET', self::$route ); + $request->set_param( 'total_views', '2,500' ); + + $response = rest_get_server()->dispatch( $request ); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + (object) array( + 'data' => array( + 'top' => $expected_top, + 'types' => $expected_types, + ), + ), + $response->get_data() + ); + } +} diff --git a/tests/Integration/Endpoints/RelatedProxyEndpointTest.php b/tests/Integration/Endpoints/RelatedProxyEndpointTest.php index b81303d8c..09cc116ac 100644 --- a/tests/Integration/Endpoints/RelatedProxyEndpointTest.php +++ b/tests/Integration/Endpoints/RelatedProxyEndpointTest.php @@ -3,8 +3,6 @@ * Integration Tests: Related API Proxy Endpoint * * @package Parsely\Tests - * - * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found */ declare(strict_types=1); @@ -14,7 +12,7 @@ use Parsely\Endpoints\Base_API_Proxy; use Parsely\Endpoints\Related_API_Proxy; use Parsely\Parsely; -use Parsely\RemoteAPI\Related_Proxy; +use Parsely\RemoteAPI\Related_API; use WP_REST_Request; /** @@ -38,7 +36,7 @@ public static function initialize(): void { public function get_endpoint(): Base_API_Proxy { return new Related_API_Proxy( new Parsely(), - new Related_Proxy( new Parsely() ) + new Related_API( new Parsely() ) ); } @@ -48,10 +46,10 @@ public function get_endpoint(): Base_API_Proxy { * @covers \Parsely\Endpoints\Related_API_Proxy::run * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint * @uses \Parsely\Endpoints\Related_API_Proxy::__construct - * @uses \Parsely\RemoteAPI\Base_Proxy::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct */ public function test_register_routes_by_default(): void { - parent::test_register_routes_by_default(); + parent::run_test_register_routes_by_default(); } /** @@ -61,15 +59,15 @@ public function test_register_routes_by_default(): void { * @covers \Parsely\Endpoints\Related_API_Proxy::run * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint * @uses \Parsely\Endpoints\Related_API_Proxy::__construct - * @uses \Parsely\RemoteAPI\Base_Proxy::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct */ - public function test_do_not_register_route_when_proxy_is_disabled(): void { - parent::test_do_not_register_route_when_proxy_is_disabled(); + public function test_verify_that_route_is_not_registered_when_proxy_is_disabled(): void { + parent::run_test_do_not_register_route_when_proxy_is_disabled(); } /** * Verifies that calling `GET /wp-parsely/v1/related` returns an error and - * does not perform a remote call when the apikey is not populated + * does not perform a remote call when the Site ID is not populated * in site options. * * @covers \Parsely\Endpoints\Related_API_Proxy::get_items @@ -78,13 +76,13 @@ public function test_do_not_register_route_when_proxy_is_disabled(): void { * @uses \Parsely\Endpoints\Related_API_Proxy::__construct * @uses \Parsely\Endpoints\Related_API_Proxy::permission_callback * @uses \Parsely\Endpoints\Related_API_Proxy::run - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options - * @uses \Parsely\RemoteAPI\Base_Proxy::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct */ - public function test_get_items_fails_without_apikey_set() { - parent::test_get_items_fails_without_apikey_set(); + public function test_get_items_fails_when_site_id_is_not_set(): void { + parent::run_test_get_items_fails_without_site_id_set(); } /** @@ -98,16 +96,16 @@ public function test_get_items_fails_without_apikey_set() { * @uses \Parsely\Endpoints\Related_API_Proxy::generate_data * @uses \Parsely\Endpoints\Related_API_Proxy::permission_callback * @uses \Parsely\Endpoints\Related_API_Proxy::run - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_api_key + * @uses \Parsely\Parsely::get_site_id * @uses \Parsely\Parsely::get_options - * @uses \Parsely\RemoteAPI\Base_Proxy::__construct - * @uses \Parsely\RemoteAPI\Base_Proxy::get_api_url - * @uses \Parsely\RemoteAPI\Base_Proxy::get_items + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::get_api_url + * @uses \Parsely\RemoteAPI\Remote_API_Base::get_items */ - public function test_get_items() { + public function test_get_items(): void { TestCase::set_options( array( 'apikey' => 'example.com' ) ); $dispatched = 0; @@ -117,7 +115,20 @@ public function test_get_items() { function () use ( &$dispatched ) { $dispatched++; return array( - 'body' => '{"data":[{"image_url":"https:\/\/example.com\/img.png","thumb_url_medium":"https:\/\/example.com\/thumb.png","title":"something","url":"https:\/\/example.com"},{"image_url":"https:\/\/example.com\/img2.png","thumb_url_medium":"https:\/\/example.com\/thumb2.png","title":"something2","url":"https:\/\/example.com\/2"}]}', + 'body' => '{"data":[ + { + "image_url":"https:\/\/example.com\/img.png", + "thumb_url_medium":"https:\/\/example.com\/thumb.png", + "title":"something", + "url":"https:\/\/example.com" + }, + { + "image_url":"https:\/\/example.com\/img2.png", + "thumb_url_medium":"https:\/\/example.com\/thumb2.png", + "title":"something2", + "url":"https:\/\/example.com\/2" + } + ]}', ); } ); diff --git a/tests/Integration/Endpoints/RestMetadataTest.php b/tests/Integration/Endpoints/RestMetadataTest.php index 6785cbe64..d360eec0a 100644 --- a/tests/Integration/Endpoints/RestMetadataTest.php +++ b/tests/Integration/Endpoints/RestMetadataTest.php @@ -50,7 +50,7 @@ public function set_up(): void { * @covers \Parsely\Endpoints\Rest_Metadata::run * @uses \Parsely\Endpoints\Rest_Metadata::register_meta * @uses \Parsely\Endpoints\Metadata_Endpoint::__construct - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options */ public function test_register_enqueued_rest_init(): void { @@ -85,14 +85,14 @@ public function test_register_enqueued_rest_init_filter(): void { /** * Verifies that the logic has not been enqueued when the `run` method is - * called with no API key. + * called with no Site ID. * * @covers \Parsely\Endpoints\Rest_Metadata::run * @uses \Parsely\Endpoints\Metadata_Endpoint::__construct - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options */ - public function test_register_enqueued_rest_init_no_api_key(): void { + public function test_register_enqueued_rest_init_no_site_id(): void { global $wp_rest_additional_fields; self::$rest->run(); @@ -105,7 +105,7 @@ public function test_register_enqueued_rest_init_no_api_key(): void { * * @covers \Parsely\Endpoints\Rest_Metadata::register_meta * @uses \Parsely\Endpoints\Metadata_Endpoint::__construct - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options */ public function test_register_meta_registers_fields(): void { @@ -182,9 +182,9 @@ function() { * @uses \Parsely\Metadata\Post_Builder::get_coauthor_names * @uses \Parsely\Metadata\Post_Builder::get_metadata * @uses \Parsely\Metadata\Post_Builder::get_tags - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set - * @uses \Parsely\Parsely::get_api_key + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::get_site_id * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::get_tracker_url * @uses \Parsely\Parsely::post_has_trackable_status @@ -196,13 +196,13 @@ public function test_get_callback(): void { $post_id = self::factory()->post->create(); // Go to current post to update WP_Query with correct data. - $this->go_to( get_permalink( $post_id ) ); + $this->go_to( $this->get_permalink( $post_id ) ); - $meta_object = self::$rest->get_callback( get_post( $post_id, 'ARRAY_A' ) ); + $meta_object = self::$rest->get_callback( $this->get_post_in_array( $post_id ) ); $metadata = new Metadata( self::$parsely ); $expected = array( 'version' => '1.1.0', - 'meta' => $metadata->construct_metadata( get_post( $post_id ) ), + 'meta' => $metadata->construct_metadata( $this->get_post( $post_id ) ), 'rendered' => self::$rest->get_rendered_meta( 'json_ld' ), 'tracker_url' => 'https://cdn.parsely.com/keys/testkey/p.js', ); @@ -241,9 +241,9 @@ public function test_get_callback(): void { * @uses \Parsely\Metadata\Post_Builder::get_coauthor_names * @uses \Parsely\Metadata\Post_Builder::get_metadata * @uses \Parsely\Metadata\Post_Builder::get_tags - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set - * @uses \Parsely\Parsely::get_api_key + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::get_site_id * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::get_tracker_url * @uses \Parsely\Parsely::post_has_trackable_status @@ -253,11 +253,11 @@ public function test_get_callback_with_filter(): void { self::set_options( array( 'apikey' => 'testkey' ) ); $post_id = self::factory()->post->create(); - $meta_object = self::$rest->get_callback( get_post( $post_id, 'ARRAY_A' ) ); + $meta_object = self::$rest->get_callback( $this->get_post_in_array( $post_id ) ); $metadata = new Metadata( self::$parsely ); $expected = array( 'version' => '1.1.0', - 'meta' => $metadata->construct_metadata( get_post( $post_id ) ), + 'meta' => $metadata->construct_metadata( $this->get_post( $post_id ) ), 'tracker_url' => 'https://cdn.parsely.com/keys/testkey/p.js', ); @@ -295,8 +295,8 @@ public function test_get_callback_with_filter(): void { * @uses \Parsely\Metadata\Post_Builder::get_coauthor_names * @uses \Parsely\Metadata\Post_Builder::get_metadata * @uses \Parsely\Metadata\Post_Builder::get_tags - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::post_has_trackable_status * @uses \Parsely\UI\Metadata_Renderer::__construct @@ -308,13 +308,13 @@ public function test_get_callback_with_url_filter(): void { $post_id = self::factory()->post->create(); // Go to current post to update WP_Query with correct data. - $this->go_to( get_permalink( $post_id ) ); + $this->go_to( $this->get_permalink( $post_id ) ); - $meta_object = self::$rest->get_callback( get_post( $post_id, 'ARRAY_A' ) ); + $meta_object = self::$rest->get_callback( $this->get_post_in_array( $post_id ) ); $metadata = new Metadata( self::$parsely ); $expected = array( 'version' => '1.1.0', - 'meta' => $metadata->construct_metadata( get_post( $post_id ) ), + 'meta' => $metadata->construct_metadata( $this->get_post( $post_id ) ), 'rendered' => self::$rest->get_rendered_meta( 'json_ld' ), ); @@ -328,8 +328,8 @@ public function test_get_callback_with_url_filter(): void { * @covers \Parsely\Endpoints\Rest_Metadata::get_callback * @uses \Parsely\Endpoints\Metadata_Endpoint::__construct * @uses \Parsely\Endpoints\Metadata_Endpoint::get_rendered_meta - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::get_tracker_url * @uses \Parsely\UI\Metadata_Renderer::__construct @@ -378,8 +378,8 @@ public function test_get_callback_with_non_existent_post(): void { * @uses \Parsely\Metadata\Post_Builder::get_coauthor_names * @uses \Parsely\Metadata\Post_Builder::get_metadata * @uses \Parsely\Metadata\Post_Builder::get_tags - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::post_has_trackable_status * @uses \Parsely\UI\Metadata_Renderer::__construct @@ -396,11 +396,11 @@ public function test_get_rendered_meta_json_ld(): void { ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $post = get_post( $post_id ); - $date = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) ); + $post = $this->get_post( $post_id ); + $date = gmdate( 'Y-m-d\TH:i:s\Z', $this->get_post_time_in_int( 'U', true, $post ) ); // Go to current post to update WP_Query with correct data. - $this->go_to( get_permalink( $post_id ) ); + $this->go_to( $this->get_permalink( $post_id ) ); $meta_string = self::$rest->get_rendered_meta( 'json_ld' ); $expected = ''; @@ -438,8 +438,8 @@ public function test_get_rendered_meta_json_ld(): void { * @uses \Parsely\Metadata\Post_Builder::get_coauthor_names * @uses \Parsely\Metadata\Post_Builder::get_metadata * @uses \Parsely\Metadata\Post_Builder::get_tags - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::post_has_trackable_status * @uses \Parsely\Parsely::convert_jsonld_to_parsely_type @@ -459,11 +459,11 @@ public function test_get_rendered_repeated_metas(): void { ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $post = get_post( $post_id ); - $date = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) ); + $post = $this->get_post( $post_id ); + $date = gmdate( 'Y-m-d\TH:i:s\Z', $this->get_post_time_in_int( 'U', true, $post ) ); // Go to current post to update WP_Query with correct data. - $this->go_to( get_permalink( $post_id ) ); + $this->go_to( $this->get_permalink( $post_id ) ); $meta_string = self::$rest->get_rendered_meta( 'repeated_metas' ); $expected = ' @@ -481,6 +481,8 @@ public function test_get_rendered_repeated_metas(): void { * * @param string $post_type Post type. * @param array $wp_rest_additional_fields Global variable. + * + * @phpstan-ignore-next-line */ private function assertParselyRestFieldIsConstructedCorrectly( string $post_type, array $wp_rest_additional_fields ): void { self::assertArrayHasKey( $post_type, $wp_rest_additional_fields ); diff --git a/tests/Integration/Endpoints/StatsPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/StatsPostDetailProxyEndpointTest.php new file mode 100644 index 000000000..ac761c7d2 --- /dev/null +++ b/tests/Integration/Endpoints/StatsPostDetailProxyEndpointTest.php @@ -0,0 +1,231 @@ +set_admin_user(); + parent::run_test_get_items_fails_without_site_id_set(); + } + + /** + * Verifies that calling `GET /wp-parsely/v1/analytics/post/detail` returns + * an error and does not perform a remote call when the Site ID is not + * populated in site options. + * + * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items + * @uses \Parsely\Endpoints\Base_API_Proxy::get_data + * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint + * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct + * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::permission_callback + * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + */ + public function test_get_items_fails_when_api_secret_is_not_set(): void { + $this->set_admin_user(); + parent::run_test_get_items_fails_without_api_secret_set(); + } + + /** + * Verifies default user capability filter. + * + * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::permission_callback + * + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call + */ + public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { + $this->login_as_contributor(); + add_filter( + 'wp_parsely_user_capability_for_all_private_apis', + function () { + return 'edit_posts'; + } + ); + + $proxy_api = new Analytics_Post_Detail_API_Proxy( + new Parsely(), + new Analytics_Post_Detail_API( new Parsely() ) + ); + + self::assertTrue( $proxy_api->permission_callback() ); + } + + /** + * Verifies endpoint specific user capability filter. + * + * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::permission_callback + * + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call + */ + public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { + $this->login_as_contributor(); + add_filter( + 'wp_parsely_user_capability_for_analytics_post_detail_api', + function () { + return 'edit_posts'; + } + ); + + $proxy_api = new Analytics_Post_Detail_API_Proxy( + new Parsely(), + new Analytics_Post_Detail_API( new Parsely() ) + ); + + self::assertTrue( $proxy_api->permission_callback() ); + } + + /** + * Verifies that calls to `GET /wp-parsely/v1/analytics/post/detail` return + * results in the expected format. + * + * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items + * @uses \Parsely\Endpoints\Base_API_Proxy::get_data + * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint + * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct + * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::generate_data + * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::permission_callback + * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @uses \Parsely\RemoteAPI\Remote_API_Base::get_api_url + * @uses \Parsely\RemoteAPI\Remote_API_Base::get_items + */ + public function test_get_items(): void { + $this->set_admin_user(); + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test', + ) + ); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ) { + $dispatched++; + return array( + 'body' => ' + {"data":[{ + "avg_engaged": 1.911, + "metrics": { + "views": 2158, + "visitors": 1537 + }, + "url": "https://example.com" + }]} + ', + ); + } + ); + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/wp-parsely/v1/stats/post/detail' ) ); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + (object) array( + 'data' => array( + (object) array( + 'avgEngaged' => ' 1:55', + 'dashUrl' => Parsely::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fexample.com', + 'url' => 'https://example.com', + 'views' => '2,158', + 'visitors' => '1,537', + ), + ), + ), + $response->get_data() + ); + } +} diff --git a/tests/Integration/Integrations/AmpTest.php b/tests/Integration/Integrations/AmpTest.php index 7ff1c5992..f695b65da 100644 --- a/tests/Integration/Integrations/AmpTest.php +++ b/tests/Integration/Integrations/AmpTest.php @@ -15,6 +15,9 @@ /** * Integration Tests for the AMP Integration. + * + * @phpstan-import-type Amp_Analytics from Amp + * @phpstan-import-type Amp_Native_Analytics from Amp */ final class AmpTest extends TestCase { /** @@ -122,16 +125,21 @@ public function test_can_register_Parsely_for_AMP_analytics(): void { $amp = new Amp( self::$parsely ); $analytics = array(); - // If apikey is empty, $analytics are returned. + // If Site ID is empty, $analytics are returned. self::assertSame( $analytics, $amp->register_parsely_for_amp_analytics( $analytics ) ); // Now set the key and test for changes. - self::set_options( array( 'apikey' => 'my-api-key.com' ) ); + self::set_options( array( 'apikey' => 'my-site-id.com' ) ); + /** + * Variable. + * + * @var Amp_Analytics + */ $output = $amp->register_parsely_for_amp_analytics( $analytics ); self::assertSame( 'parsely', $output['parsely']['type'] ); - self::assertSame( 'my-api-key.com', $output['parsely']['config_data']['vars']['apikey'] ); + self::assertSame( 'my-site-id.com', $output['parsely']['config_data']['vars']['apikey'] ); } /** @@ -149,7 +157,7 @@ public function test_can_register_Parsely_for_AMP_native_analytics(): void { $amp = new Amp( self::$parsely ); $analytics = array(); - // If apikey is empty, $analytics are returned. + // If Site ID is empty, $analytics are returned. self::assertSame( $analytics, $amp->register_parsely_for_amp_native_analytics( $analytics ) ); // Check with AMP marked as disabled. @@ -157,16 +165,21 @@ public function test_can_register_Parsely_for_AMP_native_analytics(): void { self::assertSame( $analytics, $amp->register_parsely_for_amp_native_analytics( $analytics ) ); - // Now enable AMP, and set the API key and test for changes. + // Now enable AMP, and set the Site ID and test for changes. self::set_options( array( 'disable_amp' => false, - 'apikey' => 'my-api-key.com', + 'apikey' => 'my-site-id.com', ) ); + /** + * Variable. + * + * @var Amp_Native_Analytics + */ $output = $amp->register_parsely_for_amp_native_analytics( $analytics ); self::assertSame( 'parsely', $output['parsely']['type'] ); - self::assertStringContainsString( 'my-api-key.com', $output['parsely']['config'] ); + self::assertStringContainsString( 'my-site-id.com', $output['parsely']['config'] ); } } diff --git a/tests/Integration/Integrations/FacebookInstantArticlesTest.php b/tests/Integration/Integrations/FacebookInstantArticlesTest.php index e41a9e972..9d1394238 100644 --- a/tests/Integration/Integrations/FacebookInstantArticlesTest.php +++ b/tests/Integration/Integrations/FacebookInstantArticlesTest.php @@ -16,6 +16,9 @@ /** * Integration Tests for the Facebook Instant Articles Integration. + * + * @phpstan-import-type FB_Instant_Articles_Registry from Facebook_Instant_Articles + * @phpstan-import-type FB_Parsely_Registry from Facebook_Instant_Articles */ final class FacebookInstantArticlesTest extends TestCase { /** @@ -48,8 +51,15 @@ public function set_up(): void { self::$fbia = new Facebook_Instant_Articles( new Parsely() ); $reflect = new ReflectionClass( self::$fbia ); - self::$registry_identifier = $reflect->getReflectionConstant( 'REGISTRY_IDENTIFIER' )->getValue(); - self::$registry_display_name = $reflect->getReflectionConstant( 'REGISTRY_DISPLAY_NAME' )->getValue(); + $registry_identifier = $reflect->getReflectionConstant( 'REGISTRY_IDENTIFIER' ); + if ( false !== $registry_identifier ) { + self::$registry_identifier = $registry_identifier->getValue(); // @phpstan-ignore-line + } + + $registry_display_name = $reflect->getReflectionConstant( 'REGISTRY_DISPLAY_NAME' ); + if ( false !== $registry_display_name ) { + self::$registry_display_name = $registry_display_name->getValue(); // @phpstan-ignore-line + } } /** @@ -81,19 +91,22 @@ public function test_integration_only_runs_when_FBIA_plugin_is_active(): void { } /** - * Verifies that the integration is active only if an API key is set. + * Verifies that the integration is active only if a Site ID is set. * * @covers \Parsely\Integrations\Facebook_Instant_Articles::insert_parsely_tracking * @covers \Parsely\Integrations\Facebook_Instant_Articles::get_embed_code - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set - * @uses \Parsely\Parsely::get_api_key + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Parsely::get_site_id * @uses \Parsely\Parsely::get_options * @group fbia */ public function test_parsely_is_added_to_FBIA_registry(): void { - // We use our own registry here, but the integration with the FBIA - // plugin provides its own. + /** + * We use our own registry here, but the integration with the FBIA plugin provides its own. + * + * @var FB_Instant_Articles_Registry + */ $registry = array(); // Site ID is not set. @@ -101,25 +114,32 @@ public function test_parsely_is_added_to_FBIA_registry(): void { self::assertArrayNotHasKey( self::$registry_identifier, $registry ); // Site ID is set. - $fake_api_key = 'my-api-key.com'; - self::set_options( array( 'apikey' => $fake_api_key ) ); + $fake_site_id = 'my-site-id.com'; + self::set_options( array( 'apikey' => $fake_site_id ) ); self::$fbia->insert_parsely_tracking( $registry ); - self::assert_parsely_added_to_registry( $registry, $fake_api_key ); + self::assert_parsely_added_to_registry( $registry, $fake_site_id ); } /** * Verifies that the registry array has the integration identifier as a key, * and that the display name and payload are correct. * - * @param array $registry Representation of Facebook Instant Articles registry. - * @param string $api_key API key. + * @param FB_Instant_Articles_Registry $registry Representation of Facebook Instant Articles registry. + * @param string $site_id Site ID. */ - public static function assert_parsely_added_to_registry( array $registry, string $api_key ): void { + public static function assert_parsely_added_to_registry( $registry, string $site_id ): void { self::assertArrayHasKey( self::$registry_identifier, $registry ); - self::assertSame( self::$registry_display_name, $registry[ self::$registry_identifier ]['name'] ); - // Payload should contain a script tag and the API key. - self::assertStringContainsString( '", $output ); + self::assertStringContainsString( "", $output ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript self::assertStringContainsString( "", $output ); } - - /** - * Asserts that a passed script is not registered. - * - * @param string $handle Script handle to test. - */ - private function assert_is_script_not_registered( string $handle ): void { - $this->assert_script_statuses( $handle, array(), array( 'registered' ) ); - } - - /** - * Asserts that a passed script is registered. - * - * @param string $handle Script handle to test. - */ - private function assert_is_script_registered( string $handle ): void { - $this->assert_script_statuses( $handle, array( 'registered' ) ); - } - - /** - * Asserts that a passed script is not enqueued. - * - * @param string $handle Script handle to test. - */ - private function assert_is_script_not_enqueued( string $handle ): void { - $this->assert_script_statuses( $handle, array(), array( 'enqueued' ) ); - } - - /** - * Asserts that a passed script is enqueued. - * - * @param string $handle Script handle to test. - */ - private function assert_is_script_enqueued( string $handle ): void { - $this->assert_script_statuses( $handle, array( 'enqueued' ) ); - } - - /** - * Asserts multiple enqueuing statuses for a script. - * - * @param string $handle Script handle to test. - * @param array $assert_true Optional. Statuses that should assert to true. Accepts 'enqueued', - * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array. - * @param array $assert_false Optional. Statuses that should assert to false. Accepts 'enqueued', - * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array. - * - * @throws RiskyTestError If no assertions ($assert_true, $assert_false) get passed to the function. - */ - public function assert_script_statuses( string $handle, array $assert_true = array(), array $assert_false = array() ): void { - if ( 0 === count( $assert_true ) + count( $assert_false ) ) { - throw new RiskyTestError( 'Function assert_script_statuses() has been used without any arguments' ); - } - - foreach ( $assert_true as $status ) { - self::assertTrue( - wp_script_is( $handle, $status ), - "Unexpected script status: $handle status should be '$status'" - ); - } - - foreach ( $assert_false as $status ) { - self::assertFalse( - wp_script_is( $handle, $status ), - "Unexpected script status: $handle status should NOT be '$status'" - ); - } - } } diff --git a/tests/Integration/TestCase.php b/tests/Integration/TestCase.php index 02722686b..a56396761 100644 --- a/tests/Integration/TestCase.php +++ b/tests/Integration/TestCase.php @@ -9,10 +9,20 @@ namespace Parsely\Tests\Integration; +use DateInterval; +use DateTime; +use DateTimeZone; +use ReflectionClass; +use ReflectionProperty; +use ReflectionMethod; use Parsely\Parsely; -use WP_Error; +use PHPUnit\Framework\RiskyTestError; +use WP_Post; +use WP_Term; use Yoast\WPTestUtils\WPIntegration\TestCase as WPIntegrationTestCase; +use const Parsely\Utils\WP_DATE_TIME_FORMAT; + /** * Abstract base class for all test case implementations. */ @@ -52,13 +62,13 @@ abstract class TestCase extends WPIntegrationTestCase { 'metadata_secret' => '', 'parsely_wipe_metadata_cache' => false, 'disable_autotrack' => false, + 'plugin_version' => '', ); /** * Updates Parse.ly options with a merge of default and custom values. * - * @param array $custom_options Associative array of option keys and values - * to be saved. + * @param array $custom_options Associative array of option keys and values to be saved. */ public static function set_options( array $custom_options = array() ): void { update_option( Parsely::OPTIONS_KEY, array_merge( self::DEFAULT_OPTIONS, $custom_options ) ); @@ -70,7 +80,7 @@ public static function set_options( array $custom_options = array() ): void { * @param string $post_type Optional. The post's type. Default is 'post'. * @param string $post_status Optional. The post's status. Default is 'publish'. * - * @return array An array of WP_Post fields. + * @return array An array of WP_Post fields. */ public function create_test_post_array( string $post_type = 'post', string $post_status = 'publish' ): array { return array( @@ -86,8 +96,8 @@ public function create_test_post_array( string $post_type = 'post', string $post * Creates a test category. * * @param string $name Category name. - * @return array|WP_Error Array containing the term_id and term_taxonomy_id, - * WP_Error otherwise. + * + * @return int */ public function create_test_category( string $name ) { return self::factory()->category->create( @@ -104,11 +114,17 @@ public function create_test_category( string $name ) { * Creates a test user. * * @param string $user_login The user's login username. - * @return int|WP_Error The newly created user's ID or a WP_Error object - * if the user could not be created. + * @param string $user_role The user's role. Default is subscriber. + * + * @return int The newly created user's ID. */ - public function create_test_user( string $user_login ) { - return self::factory()->user->create( array( 'user_login' => $user_login ) ); + public function create_test_user( string $user_login, string $user_role = 'subscriber' ) { + return self::factory()->user->create( + array( + 'user_login' => $user_login, + 'role' => $user_role, + ) + ); } /** @@ -117,7 +133,8 @@ public function create_test_user( string $user_login ) { * @param string $domain Site second-level domain without a .com TLD e.g. 'example' will * result in a new subsite of 'http://example.com'. * @param int $user_id User ID for the site administrator. - * @return int|WP_Error The site ID on success, WP_Error object on failure. + * + * @return int */ public function create_test_blog( string $domain, int $user_id ) { return self::factory()->blog->create( @@ -133,8 +150,8 @@ public function create_test_blog( string $domain, int $user_id ) { * * @param string $taxonomy_key Taxonomy key, must not exceed 32 characters. * @param string $term_name The term name to add. - * @return array|WP_Error An array containing the term_id and term_taxonomy_id, - * WP_Error otherwise. + * + * @return int */ public function create_test_taxonomy( string $taxonomy_key, string $term_name ) { register_taxonomy( @@ -163,9 +180,251 @@ public function create_test_taxonomy( string $taxonomy_key, string $term_name ) */ public function create_test_post( string $post_status = 'publish' ): int { $post_data = $this->create_test_post_array( 'post', $post_status ); - $post_id = self::factory()->post->create( $post_data ); - return $post_id; + return self::factory()->post->create( $post_data ); + } + + /** + * Creates test posts in sequence. + * + * @param int $num_of_posts Optional. Number of posts we need to create. + * @param string $post_type Optional. Type of the posts. + * @param string $post_status Optional. Status of the posts. + * + * @return WP_Post[] + */ + public function create_and_get_test_posts( int $num_of_posts = 1, $post_type = 'post', $post_status = 'publish' ) { + $post_ids = $this->create_posts_and_get_ids( $num_of_posts, $post_type, $post_status ); + + return $this->get_test_posts( $post_ids ); + } + + /** + * Creates test posts in sequence. + * + * @param int $num_of_posts Optional. Number of posts we need to create. + * @param string $post_type Optional. Type of the posts. + * @param string $post_status Optional. Status of the posts. + * + * @return int[] + */ + private function create_posts_and_get_ids( int $num_of_posts = 1, $post_type = 'post', $post_status = 'publish' ) { + /** + * Variable. + * + * @var int[] + */ + $post_ids = array(); + + /** + * Variable. + * + * @var DateTime + */ + $date = new DateTime( '2009-12-31', new DateTimeZone( 'America/New_York' ) ); // Date with timezone to replicate real world scenarios. + + /** + * Variable. + * + * @var DateInterval + */ + $one_day_interval = date_interval_create_from_date_string( '1 days' ); + + for ( $i = 1; $i <= $num_of_posts; $i++ ) { + /** + * Variable. + * + * @var DateTime + */ + $post_date = date_add( $date, $one_day_interval ); // Like sequence increment by 1 day. + $post_id = self::factory()->post->create( + array( + 'post_type' => $post_type, + 'post_status' => $post_status, + 'post_title' => "Title $i-($post_status)", + 'post_author' => $i, + 'post_content' => "Content $i", + 'post_date' => $post_date->format( WP_DATE_TIME_FORMAT ), + 'post_date_gmt' => gmdate( WP_DATE_TIME_FORMAT, $post_date->getTimestamp() ), + ) + ); + + array_push( $post_ids, $post_id ); + } + + return $post_ids; + } + + /** + * Wrapper around get_post function which must return WP_Post. + * + * This function ensures strict typing in our codebase. + * + * @param int $post_id Optional. Defaults to global $post. + * + * @return WP_Post + */ + public function get_post( $post_id = null ) { + if ( null === $post_id ) { + global $post; + $post_obj = $post; + } else { + $post_obj = get_post( $post_id ); + } + + /** + * Variable. + * + * @var WP_Post + */ + return $post_obj; + } + + /** + * Wrapper around get_post function which must return WP_Post as an associative array. + * + * This function ensures strict typing in our codebase. + * + * @param int $post_id ID of the posts. + * + * @return array + */ + public function get_post_in_array( $post_id ) { + /** + * Variable. + * + * @var array + */ + return get_post( $post_id, 'ARRAY_A' ); + } + + /** + * Gets given test posts. + * + * @param int[] $post_ids IDs of the posts. + * + * @return WP_Post[] + */ + private function get_test_posts( $post_ids = array() ) { + $posts = array(); + + foreach ( $post_ids as $post_id ) { + array_push( $posts, get_post( $post_id ) ); + } + + /** + * Variable. + * + * @var WP_Post[] + */ + return $posts; + } + + /** + * Wrapper around get_permalink function which must return url. + * + * This function ensures strict typing in our codebase. + * + * @param int $post_id ID of the post. + * + * @return string + */ + public function get_permalink( $post_id ) { + /** + * Variable. + * + * @var string + */ + return get_permalink( $post_id ); + } + + /** + * Wrapper around get_term function which must return WP_Term. + * + * This function ensures strict typing in our codebase. + * + * @param int $term_id ID of the term. + * + * @return WP_Term + */ + public function get_term( $term_id ) { + /** + * Variable. + * + * @var WP_Term + */ + return get_term( $term_id ); + } + + /** + * Wrapper around get_term function which must return WP_Term in associative array. + * + * This function ensures strict typing in our codebase. + * + * @param int $term_id ID of the term. + * + * @return array + */ + public function get_term_in_array( $term_id ) { + /** + * Variable. + * + * @var array + */ + return get_term( $term_id, '', 'ARRAY_A' ); + } + + /** + * Wrapper around get_term_link function which must return url. + * + * This function ensures strict typing in our codebase. + * + * @param int $term_id ID of the term. + * + * @return string + */ + public function get_term_link( $term_id ) { + /** + * Variable. + * + * @var string + */ + return get_term_link( $term_id ); + } + + /** + * Wrapper around get_post_time function which must return time in int. + * + * This function ensures strict typing in our codebase. + * + * @param string $format Format to use for retrieving the time. + * @param bool $is_gmt Whether to retrieve the GMT time. + * @param int|WP_Post $post WP_Post object or ID. + * + * @return int + */ + public function get_post_time_in_int( $format, $is_gmt, $post ) { + /** + * Variable. + * + * @var int + */ + return get_post_time( $format, $is_gmt, $post ); + } + + /** + * Wrapper around wp_json_encode function which must return string. + * + * This function ensures strict typing in our codebase. + * + * @param mixed $data — Variable (usually an array or object) to encode as JSON. + * + * @return string + */ + public function wp_json_encode( $data ) { + $encoded_data = wp_json_encode( $data ); + + return false !== $encoded_data ? $encoded_data : ''; } /** @@ -187,10 +446,240 @@ public function go_to_new_post( string $post_status = 'publish' ): int { * * @param int $admin_user_id User ID for the site administrator. * Default is 1 which is assigned to first admin user while creating the site. - * - * @return void */ public function set_admin_user( $admin_user_id = 1 ): void { wp_set_current_user( $admin_user_id ); } + + /** + * Creates a user with role `contributor` and login. + */ + public function login_as_contributor(): void { + $user_id = $this->create_test_user( 'test_contributor', 'contributor' ); + wp_set_current_user( $user_id ); + } + + /** + * Verifies that given hooks are called or not. + * + * @param string[] $hooks WordPress hooks whose availability we have to verify. + * @param bool $availability_type TRUE if we want to check the presence of given hooks. + */ + public function assert_wp_hooks_availablility( $hooks, $availability_type ): void { + if ( ! $this->is_php_version_7dot2_or_higher() ) { + return; + } + + if ( true === $availability_type ) { + $this->assert_wp_hooks( $hooks ); + } else { + $this->assert_wp_hooks( array(), $hooks ); + } + } + + /** + * Asserts WordPress hooks. + * + * @param string[] $true_hooks Optional. Actions that should have been present. + * @param string[] $false_hooks Optional. Actions that should have not been present. + * + * @throws RiskyTestError If no assertions get passed to the function. + */ + private function assert_wp_hooks( array $true_hooks = array(), array $false_hooks = array() ): void { + if ( 0 === count( $true_hooks ) + count( $false_hooks ) ) { + throw new RiskyTestError( 'Function assert_wp_hooks() has been used without any arguments' ); + } + + foreach ( $true_hooks as $hook ) { + self::assertTrue( + has_action( $hook ), + "Unexpected hook status: $hook should have been called." + ); + } + + foreach ( $false_hooks as $hook ) { + self::assertFalse( + has_action( $hook ), + "Unexpected hook status: $hook should have not been called." + ); + } + } + + /** + * Asserts that a passed script is not registered. + * + * @param string $handle Script handle to test. + */ + public function assert_is_script_not_registered( string $handle ): void { + $this->assert_script_statuses( $handle, array(), array( 'registered' ) ); + } + + /** + * Asserts that a passed script is registered. + * + * @param string $handle Script handle to test. + */ + public function assert_is_script_registered( string $handle ): void { + $this->assert_script_statuses( $handle, array( 'registered' ) ); + } + + /** + * Asserts that a passed script is not enqueued. + * + * @param string $handle Script handle to test. + */ + public function assert_is_script_not_enqueued( string $handle ): void { + $this->assert_script_statuses( $handle, array(), array( 'enqueued' ) ); + } + + /** + * Asserts that a passed script is enqueued. + * + * @param string $handle Script handle to test. + */ + public function assert_is_script_enqueued( string $handle ): void { + $this->assert_script_statuses( $handle, array( 'enqueued' ) ); + } + + /** + * Asserts multiple enqueuing statuses for a script. + * + * @param string $handle Script handle to test. + * @param array $assert_true Optional. Statuses that should assert to true. Accepts 'enqueued', + * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array. + * @param array $assert_false Optional. Statuses that should assert to false. Accepts 'enqueued', + * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array. + * + * @throws RiskyTestError If no assertions ($assert_true, $assert_false) get passed to the function. + */ + private function assert_script_statuses( string $handle, array $assert_true = array(), array $assert_false = array() ): void { + if ( 0 === count( $assert_true ) + count( $assert_false ) ) { + throw new RiskyTestError( 'Function assert_script_statuses() has been used without any arguments' ); + } + + foreach ( $assert_true as $status ) { + self::assertTrue( + wp_script_is( $handle, $status ), + "Unexpected script status: $handle status should be '$status'" + ); + } + + foreach ( $assert_false as $status ) { + self::assertFalse( + wp_script_is( $handle, $status ), + "Unexpected script status: $handle status should NOT be '$status'" + ); + } + } + + /** + * Asserts that a passed style is not registered. + * + * @param string $handle Style handle to test. + */ + public function assert_is_style_not_registered( string $handle ): void { + $this->assert_style_statuses( $handle, array(), array( 'registered' ) ); + } + + /** + * Asserts that a passed style is registered. + * + * @param string $handle Style handle to test. + */ + public function assert_is_style_registered( string $handle ): void { + $this->assert_style_statuses( $handle, array( 'registered' ) ); + } + + /** + * Asserts that a passed style is not enqueued. + * + * @param string $handle Style handle to test. + */ + public function assert_is_style_not_enqueued( string $handle ): void { + $this->assert_style_statuses( $handle, array(), array( 'enqueued' ) ); + } + + /** + * Asserts that a passed style is enqueued. + * + * @param string $handle Style handle to test. + */ + public function assert_is_style_enqueued( string $handle ): void { + $this->assert_style_statuses( $handle, array( 'enqueued' ) ); + } + + /** + * Asserts multiple enqueuing statuses for a style. + * + * @param string $handle Style handle to test. + * @param array $assert_true Optional. Statuses that should assert to true. Accepts 'enqueued', + * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array. + * @param array $assert_false Optional. Statuses that should assert to false. Accepts 'enqueued', + * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array. + * + * @throws RiskyTestError If no assertions ($assert_true, $assert_false) get passed to the function. + */ + private function assert_style_statuses( string $handle, array $assert_true = array(), array $assert_false = array() ): void { + if ( 0 === count( $assert_true ) + count( $assert_false ) ) { + throw new RiskyTestError( 'Function assert_style_statuses() has been used without any arguments' ); + } + + foreach ( $assert_true as $status ) { + self::assertTrue( + wp_style_is( $handle, $status ), + "Unexpected style status: $handle status should be '$status'" + ); + } + + foreach ( $assert_false as $status ) { + self::assertFalse( + wp_style_is( $handle, $status ), + "Unexpected style status: $handle status should NOT be '$status'" + ); + } + } + + /** + * Returns TRUE if minimum PHP version is 7.2 or higher. We uses this if something works + * differently in PHP versions < 7.2 and >= 7.2. + * + * Note: Remove this function when we remove support for PHP 7.1. + */ + public function is_php_version_7dot2_or_higher(): bool { + return phpversion() >= '7.2'; + } + + /** + * Gets private property of a class. + * + * @param class-string $class_name Name of the class. + * @param string $property_name Name of the property. + * + * @return ReflectionProperty + */ + public function get_private_property( $class_name, $property_name ) { + $reflector = new ReflectionClass( $class_name ); + $property = $reflector->getProperty( $property_name ); + + $property->setAccessible( true ); + + return $property; + } + + /** + * Gets private method of a class. + * + * @param class-string $class_name Name of the class. + * @param string $method Name of the method. + * + * @return ReflectionMethod + */ + public function get_private_method( $class_name, $method ) { + $reflector = new ReflectionClass( $class_name ); + $method = $reflector->getMethod( $method ); + + $method->setAccessible( true ); + + return $method; + } } diff --git a/tests/Integration/UI/AdminColumnsParselyStatsTest.php b/tests/Integration/UI/AdminColumnsParselyStatsTest.php new file mode 100644 index 000000000..353ce1ee4 --- /dev/null +++ b/tests/Integration/UI/AdminColumnsParselyStatsTest.php @@ -0,0 +1,1240 @@ + array(), + 'error' => null, + ); + + /** + * Setup method called before each test. + */ + public function set_up(): void { + parent::set_up(); + + $this->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%' ); + $this->set_admin_user(); + } + + /** + * Teardown method called after each test. + */ + public function tear_down(): void { + parent::tear_down(); + + $this->set_permalink_structure( '' ); + } + + /** + * Verifies enqueued status of Parse.ly Stats styles. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles + */ + public function test_styles_of_parsely_stats_admin_column_on_empty_plugin_options(): void { + $this->set_empty_plugin_options(); + $this->assert_parsely_stats_admin_styles( false ); + } + + /** + * Verifies enqueued status of Parse.ly Stats styles. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles + */ + public function test_styles_of_parsely_stats_admin_column_on_empty_api_secret(): void { + $this->set_empty_api_secret(); + $this->assert_parsely_stats_admin_styles( true ); + } + + /** + * Verifies enqueued status of Parse.ly Stats styles. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles + */ + public function test_styles_of_parsely_stats_admin_column_on_empty_track_post_types(): void { + $this->set_empty_track_post_types(); + $this->assert_parsely_stats_admin_styles( false ); + } + + /** + * Verifies enqueued status of Parse.ly Stats styles. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles + */ + public function test_styles_of_parsely_stats_admin_column_on_invalid_track_post_type(): void { + $this->set_valid_plugin_options(); + set_current_screen( 'edit-page' ); + $this->assert_parsely_stats_admin_styles( false ); + } + + /** + * Verifies enqueued status of Parse.ly Stats styles. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles + */ + public function test_styles_of_parsely_stats_admin_column_on_valid_posts(): void { + $this->set_valid_conditions_for_parsely_stats(); + $this->assert_parsely_stats_admin_styles( true ); + } + + /** + * Asserts on Parse.ly Stats styles. + * + * @param bool $assert_type Indicates wether we are asserting for TRUE or FALSE. + */ + private function assert_parsely_stats_admin_styles( bool $assert_type ): void { + $obj = $this->init_admin_columns_parsely_stats(); + + if ( $this->is_php_version_7dot2_or_higher() ) { + do_action( 'current_screen' ); // phpcs:ignore + do_action( 'admin_enqueue_scripts' ); // phpcs:ignore + } else { + $obj->set_current_screen(); + $obj->enqueue_parsely_stats_styles(); + } + + $handle = 'admin-parsely-stats-styles'; + if ( $assert_type ) { + $this->assert_is_style_enqueued( $handle ); + wp_dequeue_style( $handle ); // Dequeue to start fresh for next test. + } else { + $this->assert_is_style_not_enqueued( $handle ); + } + } + + /** + * Verifies Parse.ly Stats column visibility. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view + */ + public function test_parsely_stats_column_visibility_on_empty_plugin_options(): void { + $this->set_empty_plugin_options(); + + $this->assert_hooks_for_parsely_stats_column( false ); + self::assertNotContains( self::$parsely_stats_column_header, $this->get_admin_columns() ); + } + + + /** + * Verifies Parse.ly Stats column visibility. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view + */ + public function test_parsely_stats_column_visibility_on_empty_api_secret(): void { + $this->set_empty_api_secret(); + + self::assertContains( self::$parsely_stats_column_header, $this->get_admin_columns() ); + $this->assert_hooks_for_parsely_stats_column( true ); + } + + /** + * Verifies Parse.ly Stats column visibility. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view + */ + public function test_parsely_stats_column_visibility_on_empty_track_post_types(): void { + $this->set_empty_track_post_types(); + + self::assertNotContains( self::$parsely_stats_column_header, $this->get_admin_columns() ); + $this->assert_hooks_for_parsely_stats_column( true ); + } + + /** + * Verifies Parse.ly Stats column visibility. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view + */ + public function test_parsely_stats_column_visibility_on_invalid_track_post_types(): void { + $this->set_valid_plugin_options(); + set_current_screen( 'edit-page' ); + + self::assertNotContains( self::$parsely_stats_column_header, $this->get_admin_columns() ); + $this->assert_hooks_for_parsely_stats_column( true ); + } + + /** + * Verifies Parse.ly Stats column visibility. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view + */ + public function test_parsely_stats_column_visibility_on_valid_posts(): void { + $this->set_valid_conditions_for_parsely_stats(); + + self::assertContains( self::$parsely_stats_column_header, $this->get_admin_columns() ); + $this->assert_hooks_for_parsely_stats_column( true ); + } + + /** + * Verifies Parse.ly Stats column visibility. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view + */ + public function test_parsely_stats_column_visibility_on_valid_pages(): void { + $this->set_valid_conditions_for_parsely_stats( 'page' ); + + self::assertContains( self::$parsely_stats_column_header, $this->get_admin_columns() ); + $this->assert_hooks_for_parsely_stats_column( true ); + } + + /** + * Gets Admin Columns. + * + * @return array + */ + private function get_admin_columns() { + $obj = $this->init_admin_columns_parsely_stats(); + + if ( $this->is_php_version_7dot2_or_higher() ) { + do_action( 'current_screen' ); // phpcs:ignore + } else { + $obj->set_current_screen(); + } + + return $obj->add_parsely_stats_column_on_list_view( array() ); + } + + /** + * Asserts status of hooks for Parse.ly Stats column. + * + * @param bool $assert_type Assert this condition on hooks. + */ + private function assert_hooks_for_parsely_stats_column( $assert_type ): void { + $this->assert_wp_hooks_availablility( + array( 'current_screen', 'manage_posts_columns', 'manage_pages_columns' ), + $assert_type + ); + } + + /** + * Verifies content of Parse.ly Stats column. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder + */ + public function test_content_of_parsely_stats_column_on_empty_plugin_options(): void { + $this->set_empty_plugin_options(); + + $obj = $this->init_admin_columns_parsely_stats(); + $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj ); + + $this->assert_hooks_for_parsely_stats_content( false ); + self::assertEquals( '', $output ); + self::assertEquals( array(), $this->get_utc_published_times_property( $obj ) ); + } + + /** + * Verifies content of Parse.ly Stats column. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder + */ + public function test_content_of_parsely_stats_column_on_empty_api_secret(): void { + $this->set_empty_api_secret(); + + $obj = $this->init_admin_columns_parsely_stats(); + $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj ); + + $this->assert_hooks_for_parsely_stats_content( true ); + self::assertEquals( + $this->get_parsely_stats_placeholder_content( '/2010/01/01/title-1-publish' ) . + $this->get_parsely_stats_placeholder_content( '/2010/01/02/title-2-publish' ) . + $this->get_parsely_stats_placeholder_content( '/2010/01/03/title-3-publish' ) . + $this->get_parsely_stats_placeholder_content( '/' ) . + $this->get_parsely_stats_placeholder_content( '/' ), + $output + ); + self::assertEquals( array(), $this->get_utc_published_times_property( $obj ) ); + } + + /** + * Verifies content of Parse.ly Stats column. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder + */ + public function test_content_of_parsely_stats_column_on_empty_track_post_types(): void { + $this->set_empty_track_post_types(); + + $obj = $this->init_admin_columns_parsely_stats(); + $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj ); + + $this->assert_hooks_for_parsely_stats_content( true ); + self::assertEquals( '', $output ); + self::assertEquals( array(), $this->get_utc_published_times_property( $obj ) ); + } + + /** + * Verifies content of Parse.ly Stats column. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder + */ + public function test_content_of_parsely_stats_column_on_invalid_track_post_types(): void { + $this->set_valid_plugin_options(); + set_current_screen( 'edit-page' ); + + $obj = $this->init_admin_columns_parsely_stats(); + $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj ); + + $this->assert_hooks_for_parsely_stats_content( true ); + self::assertEquals( '', $output ); + self::assertEquals( array(), $this->get_utc_published_times_property( $obj ) ); + } + + /** + * Verifies content of Parse.ly Stats column. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder + */ + public function test_content_of_parsely_stats_column_on_valid_posts(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $obj = $this->init_admin_columns_parsely_stats(); + $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj ); + + $this->assert_hooks_for_parsely_stats_content( true ); + self::assertEquals( + $this->get_parsely_stats_placeholder_content( '/2010/01/01/title-1-publish' ) . + $this->get_parsely_stats_placeholder_content( '/2010/01/02/title-2-publish' ) . + $this->get_parsely_stats_placeholder_content( '/2010/01/03/title-3-publish' ) . + $this->get_parsely_stats_placeholder_content( '/' ) . + $this->get_parsely_stats_placeholder_content( '/' ), + $output + ); + self::assertEquals( + array( '2010-01-01 05:00:00', '2010-01-02 05:00:00', '2010-01-03 05:00:00' ), + $this->get_utc_published_times_property( $obj ) + ); + } + + + /** + * Verifies content of Parse.ly Stats column. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder + */ + public function test_content_of_parsely_stats_column_on_valid_pages(): void { + $this->set_valid_conditions_for_parsely_stats( 'page' ); + + $obj = $this->init_admin_columns_parsely_stats(); + $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj ); + + $this->assert_hooks_for_parsely_stats_content( true ); + self::assertEquals( + $this->get_parsely_stats_placeholder_content( '/2010/01/01/title-1-publish' ) . + $this->get_parsely_stats_placeholder_content( '/2010/01/02/title-2-publish' ) . + $this->get_parsely_stats_placeholder_content( '/2010/01/03/title-3-publish' ) . + $this->get_parsely_stats_placeholder_content( '/' ) . + $this->get_parsely_stats_placeholder_content( '/' ), + $output + ); + self::assertEquals( + array( '2010-01-01 05:00:00', '2010-01-02 05:00:00', '2010-01-03 05:00:00' ), + $this->get_utc_published_times_property( $obj ) + ); + } + + /** + * Sets posts data and get content of Parse.ly Stats column. + * + * @param Admin_Columns_Parsely_Stats $obj Instance of Admin_Columns_Parsely_Stats. + * @param string $post_type Type of the post. + * + * @return string + */ + private function set_posts_data_and_get_content_of_parsely_stats_column( $obj, $post_type = 'post' ) { + $posts = $this->set_and_get_posts_data( 3, 2, $post_type ); + + return $this->get_content_of_parsely_stats_column( $obj, $posts, $post_type ); + } + + /** + * Sets posts data. + * + * @param int $publish_num_of_posts Number of publish posts that we have to create. + * @param int $draft_num_of_posts Number of draft posts that we have to create. + * @param string $post_type Type of the post. + * + * @return WP_Post[] + */ + private function set_and_get_posts_data( $publish_num_of_posts = 1, $draft_num_of_posts = 0, $post_type = 'post' ) { + return array_merge( + $this->create_and_get_test_posts( $publish_num_of_posts ), + $this->create_and_get_test_posts( $draft_num_of_posts, $post_type, 'draft' ) + ); + } + + /** + * Gets content of Parse.ly Stats column. + * + * @param Admin_Columns_Parsely_Stats $obj Instance of Admin_Columns_Parsely_Stats. + * @param WP_Post[] $posts Available posts. + * @param string $post_type Type of the post. + * + * @return string + */ + private function get_content_of_parsely_stats_column( $obj, $posts, $post_type ) { + ob_start(); + $this->show_content_on_parsely_stats_column( $obj, $posts, $post_type ); + + return (string) ob_get_clean(); + } + + /** + * Replicates behavior by which WordPress set post publish dates and then make API call + * to get Parse.ly stats. + * + * @param Admin_Columns_Parsely_Stats $obj Instance of Admin_Columns_Parsely_Stats. + * @param WP_Post[] $posts Available posts. + * @param string $post_type Type of the post. + */ + private function show_content_on_parsely_stats_column( $obj, $posts, $post_type ): void { + if ( $this->is_php_version_7dot2_or_higher() ) { + do_action( 'current_screen' ); // phpcs:ignore + } else { + $obj->set_current_screen(); + } + + foreach ( $posts as $current_post ) { + global $post; + $post = $current_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $column_name = 'parsely-stats'; + + if ( $this->is_php_version_7dot2_or_higher() ) { + do_action( "manage_{$post_type}s_custom_column", $column_name ); // phpcs:ignore + } else { + $obj->update_published_times_and_show_placeholder( $column_name ); + } + } + } + + /** + * Gets placeholder content of Parse.ly stats column. + * + * @param string $key Stats Key. + * + * @return string + */ + private function get_parsely_stats_placeholder_content( $key ) { + return "
      \n ...\n
      \n "; + } + + /** + * Gets utc_published_times property of given object. + * + * @param Admin_Columns_Parsely_Stats $obj Instance of Admin_Columns_Parsely_Stats. + * + * @return string + */ + private function get_utc_published_times_property( $obj ) { + /** + * Variable. + * + * @var string + */ + return $this->get_private_property( Admin_Columns_Parsely_Stats::class, 'utc_published_times' )->getValue( $obj ); + } + + /** + * Asserts status of hooks for showing Parse.ly Stats content inside column. + * + * @param bool $assert_type Assert this condition on hooks. + */ + private function assert_hooks_for_parsely_stats_content( $assert_type = true ): void { + $this->assert_wp_hooks_availablility( + array( 'current_screen', 'manage_posts_custom_column', 'manage_pages_custom_column' ), + $assert_type + ); + } + + /** + * Verifies enqueued status of Parse.ly Stats script. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data + */ + public function test_script_of_parsely_stats_admin_column_on_empty_plugin_options(): void { + $this->set_empty_plugin_options(); + $obj = $this->mock_parsely_stats_response( null ); + $this->assert_parsely_stats_admin_script( $obj, false ); + } + + /** + * Verifies enqueued status of Parse.ly Stats script. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data + */ + public function test_script_of_parsely_stats_admin_column_on_empty_api_secret(): void { + $this->set_empty_api_secret(); + $obj = $this->mock_parsely_stats_response( array() ); + $this->assert_parsely_stats_admin_script( $obj, true ); + } + + /** + * Verifies enqueued status of Parse.ly Stats script. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data + */ + public function test_script_of_parsely_stats_admin_column_on_empty_track_post_types(): void { + $this->set_empty_track_post_types(); + $obj = $this->mock_parsely_stats_response( null ); + $this->assert_parsely_stats_admin_script( $obj, false ); + } + + /** + * Verifies enqueued status of Parse.ly Stats script. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data + */ + public function test_script_of_parsely_stats_admin_column_on_invalid_track_post_types(): void { + $this->set_valid_plugin_options(); + set_current_screen( 'edit-page' ); + + $obj = $this->mock_parsely_stats_response( null ); + $this->assert_parsely_stats_admin_script( $obj, false ); + } + + /** + * Verifies enqueued status of Parse.ly Stats script. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data + */ + public function test_script_of_parsely_stats_admin_column_on_valid_posts_and_empty_response(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $obj = $this->mock_parsely_stats_response( null ); + $this->assert_parsely_stats_admin_script( $obj, false ); + } + + /** + * Verifies enqueued status of Parse.ly Stats script. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data + */ + public function test_script_of_parsely_stats_admin_column_on_valid_posts_and_valid_response(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $obj = $this->mock_parsely_stats_response( array() ); + $this->assert_parsely_stats_admin_script( $obj, true ); + + /** + * Internal Variable. + * + * @var WP_Scripts + */ + global $wp_scripts; + + ob_start(); + var_dump( $wp_scripts->print_inline_script ( 'admin-parsely-stats-script', 'before' ) ); // phpcs:ignore + $output = (string) ob_get_clean(); + + self::assertStringContainsString( 'window.wpParselyPostsStatsResponse = \'[]\';', $output ); + } + + /** + * Mock function get_parsely_stats_response from class. + * + * @param null|array $return_value Value that we have to return from mock function. + * + * @return Admin_Columns_Parsely_Stats + */ + private function mock_parsely_stats_response( $return_value ) { + $obj = Mockery::mock( Admin_Columns_Parsely_Stats::class, array( new Parsely() ) )->makePartial(); + $obj->shouldReceive( 'get_parsely_stats_response' )->once()->andReturn( $return_value ); + $obj->run(); + + return $obj; + } + + + /** + * Verifies Parse.ly API call and enqueued status of Parse.ly Stats script. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + */ + public function test_should_not_call_parsely_api_on_empty_api_secret_and_hidden_parsely_stats_column(): void { + $this->set_empty_api_secret(); + + $obj = $this->mock_is_parsely_stats_column_hidden( true ); + $this->assert_parsely_stats_admin_script( $obj, false ); + } + + /** + * Mock function is_parsely_stats_column_hidden from class. + * + * @param bool $return_value Value that we have to return from mock function. + * + * @return Admin_Columns_Parsely_Stats + */ + private function mock_is_parsely_stats_column_hidden( $return_value = false ) { + $obj = Mockery::mock( Admin_Columns_Parsely_Stats::class, array( new Parsely() ) )->makePartial(); + $obj->shouldReceive( 'is_parsely_stats_column_hidden' )->once()->andReturn( $return_value ); + $obj->run(); + + return $obj; + } + + /** + * Asserts script of Parse.ly Stats. + * + * @param Admin_Columns_Parsely_Stats $obj Instance of the class. + * @param bool $assert_type Indicates wether we are asserting for TRUE or FALSE. + */ + private function assert_parsely_stats_admin_script( $obj, $assert_type ): void { + if ( $this->is_php_version_7dot2_or_higher() ) { + do_action( 'current_screen' ); // phpcs:ignore + do_action( 'admin_footer' ); // phpcs:ignore + } else { + $obj->set_current_screen(); + $obj->enqueue_parsely_stats_script_with_data(); + } + + $handle = 'admin-parsely-stats-script'; + if ( $assert_type ) { + $this->assert_is_script_enqueued( $handle ); + wp_dequeue_script( $handle ); // Dequeue to start fresh for next test. + } else { + $this->assert_is_script_not_enqueued( $handle ); + } + } + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + */ + public function test_parsely_stats_response_on_empty_plugin_options(): void { + $this->set_empty_plugin_options(); + + $res = $this->get_parsely_stats_response(); + + $this->assert_hooks_for_parsely_stats_response( false ); + self::assertNull( $res ); + } + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + */ + public function test_parsely_stats_response_on_empty_api_secret(): void { + $this->set_empty_api_secret(); + + $res = $this->get_parsely_stats_response(); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertEquals( + array( + 'data' => null, + 'error' => array( + 'code' => 403, + 'message' => 'Forbidden.', + 'htmlMessage' => '

      ' . + 'We are unable to retrieve data for Parse.ly Stats. ' . + 'Please contact support@parsely.com for help resolving this issue.' . + '

      ', + ), + ), + $res + ); + } + + /** + * Verifies Parse.ly Stats API arguments. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api + */ + public function test_api_params_of_analytics_api_call_on_valid_post_type_and_having_single_record(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $posts = $this->set_and_get_posts_data( 1, 2 ); + $res = $this->get_parsely_stats_response( + $posts, + 'post', + null, + array( + 'pub_date_start' => '2010-01-01', + 'pub_date_end' => '2010-01-01', + ) + ); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertEquals( self::$parsely_api_empty_response, $res ); + } + + /** + * Verifies Parse.ly Stats API arguments. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api + */ + public function test_api_params_of_analytics_api_call_on_valid_post_type_and_having_multiple_records(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $posts = $this->set_and_get_posts_data( 3, 5 ); + $res = $this->get_parsely_stats_response( + $posts, + 'post', + null, + array( + 'pub_date_start' => '2010-01-01', + 'pub_date_end' => '2010-01-03', + ) + ); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertEquals( self::$parsely_api_empty_response, $res ); + } + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + */ + public function test_parsely_stats_response_on_empty_track_post_types(): void { + $this->set_empty_track_post_types(); + + $res = $this->get_parsely_stats_response(); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertNull( $res ); + } + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + */ + public function test_parsely_stats_response_on_invalid_track_post_types(): void { + $this->set_valid_plugin_options(); + set_current_screen( 'edit-page' ); + + $res = $this->get_parsely_stats_response(); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertNull( $res ); + } + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + */ + public function test_parsely_stats_response_on_valid_post_type_and_no_post_data(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $res = $this->get_parsely_stats_response(); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertNull( $res ); + } + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api + */ + public function test_parsely_stats_response_on_valid_post_type_and_null_response_from_api(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $posts = $this->set_and_get_posts_data(); + $res = $this->get_parsely_stats_response( $posts ); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertEquals( self::$parsely_api_empty_response, $res ); + } + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api + */ + public function test_parsely_stats_response_on_valid_post_type_and_error_response_from_api(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $posts = $this->set_and_get_posts_data( 1 ); + $res = $this->get_parsely_stats_response( $posts, 'post', new WP_Error( 404, 'Not Found.' ) ); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertNull( isset( $res['data'] ) ? $res['data'] : null ); + self::assertEquals( + array( + 'code' => 404, + 'message' => 'Not Found.', + 'htmlMessage' => '

      Error while getting data for Parse.ly Stats.
      Detail: (404) Not Found.

      ', + ), + isset( $res['error'] ) ? $res['error'] : null + ); + } + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_unique_stats_key_from_analytics + */ + public function test_parsely_stats_response_on_valid_post_type_and_having_data_from_api(): void { + $this->set_valid_conditions_for_parsely_stats(); + + $posts = $this->set_and_get_posts_data( 7, 10 ); + $api_response = array( + array( + 'url' => 'http://example.com/2010/01/01/title-1-publish', + 'metrics' => array( + 'views' => 0, + 'visitors' => 0, + 'avg_engaged' => 0, + ), + ), + array( + 'url' => 'http://example.com/2010/01/02/title-2-publish', + 'metrics' => array( + 'views' => 1, + 'visitors' => 1, + 'avg_engaged' => 0.01, + ), + ), + array( + 'url' => 'http://example.com/2010/01/03/title-3-publish', + 'metrics' => array( + 'views' => 1100, + 'visitors' => 1100000, + 'avg_engaged' => 1.1, + ), + ), + array( + 'url' => 'http://example.com/2010/01/04/title-4-publish', + ), + array( + 'url' => 'http://example.com/2010/01/05/title-5-publish', + 'metrics' => array( + 'views' => 1, + ), + ), + array( + 'url' => 'http://example.com/2010/01/06/title-6-publish', + 'metrics' => array( + 'visitors' => 1, + ), + ), + array( + 'url' => 'http://example.com/2010/01/07/title-7-publish', + 'metrics' => array( + 'avg_engaged' => 0.01, + ), + ), + ); + $res = $this->get_parsely_stats_response( + $posts, + 'post', + $api_response, + array( + 'pub_date_start' => '2010-01-01', + 'pub_date_end' => '2010-01-07', + ) + ); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertNull( isset( $res['error'] ) ? $res['error'] : null ); + self::assertEquals( + array( + '/2010/01/01/title-1-publish' => array( + 'page_views' => '0 page views', + 'visitors' => '0 visitors', + 'avg_time' => '0 sec. avg time', + ), + '/2010/01/02/title-2-publish' => array( + 'page_views' => '1 page view', + 'visitors' => '1 visitor', + 'avg_time' => '1 sec. avg time', + ), + '/2010/01/03/title-3-publish' => array( + 'page_views' => '1.1K page views', + 'visitors' => '1.1M visitors', + 'avg_time' => '1:06 avg time', + ), + '/2010/01/05/title-5-publish' => array( + 'page_views' => '1 page view', + 'visitors' => '0 visitors', + 'avg_time' => '0 sec. avg time', + ), + '/2010/01/06/title-6-publish' => array( + 'page_views' => '0 page views', + 'visitors' => '1 visitor', + 'avg_time' => '0 sec. avg time', + ), + '/2010/01/07/title-7-publish' => array( + 'page_views' => '0 page views', + 'visitors' => '0 visitors', + 'avg_time' => '1 sec. avg time', + ), + ), + isset( $res['data'] ) ? $res['data'] : null + ); + } + + + /** + * Verifies Parse.ly Stats response. + * + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api + * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_unique_stats_key_from_analytics + */ + public function test_parsely_stats_response_on_valid_hierarchal_post_type_and_having_data_from_api(): void { + $this->set_valid_conditions_for_parsely_stats( 'page' ); + + $pages = $this->set_and_get_posts_data( 1, 2, 'page' ); + $api_response = array( + array( + 'url' => 'http://example.com/2010/01/01/title-1-publish', + 'metrics' => array( + 'views' => 1100, + 'visitors' => 1100000, + 'avg_engaged' => 1.1, + ), + ), + ); + $res = $this->get_parsely_stats_response( + $pages, + 'page', + $api_response, + array( + 'pub_date_start' => '2010-01-01', + 'pub_date_end' => '2010-01-01', + ) + ); + + $this->assert_hooks_for_parsely_stats_response( true ); + self::assertNull( isset( $res['error'] ) ? $res['error'] : null ); + self::assertEquals( + array( + '/2010/01/01/title-1-publish' => array( + 'page_views' => '1.1K page views', + 'visitors' => '1.1M visitors', + 'avg_time' => '1:06 avg time', + ), + ), + isset( $res['data'] ) ? $res['data'] : null + ); + } + + /** + * Replicates behavior by which WordPress set post publish dates and then make API call + * to get Parse.ly stats. + * + * @param WP_Post[] $posts Available Posts. + * @param string $post_type Type of the post. + * @param Analytics_Post[]|WP_Error|null $api_response Mocked response that we return on calling API. + * @param Analytics_Post_API_Params|null $api_params API Parameters. + * + * @return Parsely_Posts_Stats_Response|null + */ + private function get_parsely_stats_response( $posts = array(), $post_type = 'post', $api_response = null, $api_params = null ) { + $obj = $this->init_admin_columns_parsely_stats(); + + ob_start(); + $this->show_content_on_parsely_stats_column( $obj, $posts, $post_type ); + ob_get_clean(); // Discard output to keep console clean while running tests. + + $api = Mockery::mock( Analytics_Posts_API::class, array( new Parsely() ) )->makePartial(); + if ( ! is_null( $api_params ) ) { + $api->shouldReceive( 'get_posts_analytics' ) + ->once() + ->withArgs( + array( + array_merge( + $api_params, + // Params which will not change. + array( + 'period_start' => get_utc_date_format( -7 ), + 'period_end' => get_utc_date_format(), + 'limit' => 2000, + 'sort' => 'avg_engaged', + ) + ), + ) + ) + ->andReturn( $api_response ); + } else { + $api->shouldReceive( 'get_posts_analytics' )->once()->andReturn( $api_response ); + } + + return $obj->get_parsely_stats_response( $api ); + } + + /** + * Asserts status of hooks for Parse.ly Stats response. + * + * @param bool $assert_type Assert this condition on hooks. + */ + private function assert_hooks_for_parsely_stats_response( $assert_type = true ): void { + $this->assert_wp_hooks_availablility( + array( 'current_screen', 'manage_posts_custom_column', 'manage_pages_custom_column', 'admin_footer' ), + $assert_type + ); + } + + /** + * Initializes Admin_Columns_Parsely_Stats object. + * + * @return Admin_Columns_Parsely_Stats + */ + private function init_admin_columns_parsely_stats() { + $obj = new Admin_Columns_Parsely_Stats( new Parsely() ); + $obj->run(); + + return $obj; + } + + /** + * Sets empty key and secret. + */ + private function set_empty_plugin_options(): void { + TestCase::set_options( + array( + 'apikey' => '', + 'api_secret' => '', + 'track_post_types' => array(), + ) + ); + + set_current_screen( 'edit-post' ); + } + + /** + * Sets empty API Secret. + * + * @param string $post_type Type of the post. + */ + private function set_empty_api_secret( $post_type = 'post' ): void { + TestCase::set_options( + array( + 'apikey' => 'test', + 'api_secret' => '', + 'track_post_types' => array( $post_type ), + ) + ); + + set_current_screen( 'edit-post' ); + } + + /** + * Sets empty track_post_types. + */ + private function set_empty_track_post_types(): void { + TestCase::set_options( + array( + 'apikey' => 'test', + 'api_secret' => 'test', + 'track_post_types' => array(), + ) + ); + + set_current_screen( 'edit-post' ); + } + + /** + * Sets valid plugin_options. + * + * @param string $post_type Type of the post. + */ + private function set_valid_plugin_options( $post_type = 'post' ): void { + TestCase::set_options( + array( + 'apikey' => 'test', + 'api_secret' => 'test', + 'track_post_types' => array( $post_type ), + ) + ); + } + + /** + * Sets valid conditions under which we add hooks for Parse.ly Stats. + * + * @param string $post_type Type of the post. + */ + private function set_valid_conditions_for_parsely_stats( $post_type = 'post' ): void { + $this->set_valid_plugin_options( $post_type ); + set_current_screen( "edit-$post_type" ); + } +} diff --git a/tests/Integration/UI/AdminWarningTest.php b/tests/Integration/UI/AdminWarningTest.php index 713b7e55d..131108898 100644 --- a/tests/Integration/UI/AdminWarningTest.php +++ b/tests/Integration/UI/AdminWarningTest.php @@ -41,8 +41,8 @@ public function set_up(): void { * * @covers \Parsely\UI\Admin_Warning::should_display_admin_warning * @covers \Parsely\UI\Admin_Warning::__construct - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options */ public function test_display_admin_warning_without_key(): void { @@ -51,7 +51,7 @@ public function test_display_admin_warning_without_key(): void { } $should_display_admin_warning = self::get_method( 'should_display_admin_warning', Admin_Warning::class ); - $this->set_options( array( 'apikey' => '' ) ); + self::set_options( array( 'apikey' => '' ) ); $response = $should_display_admin_warning->invoke( self::$admin_warning ); self::assertTrue( $response ); @@ -63,13 +63,13 @@ public function test_display_admin_warning_without_key(): void { * * @covers \Parsely\UI\Admin_Warning::should_display_admin_warning * @covers \Parsely\UI\Admin_Warning::__construct - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options */ public function test_display_admin_warning_without_key_old_wp(): void { $should_display_admin_warning = self::get_method( 'should_display_admin_warning', Admin_Warning::class ); - $this->set_options( array( 'apikey' => '' ) ); + self::set_options( array( 'apikey' => '' ) ); set_current_screen( 'settings_page_parsely' ); $response = $should_display_admin_warning->invoke( self::$admin_warning ); @@ -85,7 +85,7 @@ public function test_display_admin_warning_without_key_old_wp(): void { */ public function test_display_admin_warning_network_admin(): void { $should_display_admin_warning = self::get_method( 'should_display_admin_warning', Admin_Warning::class ); - $this->set_options( array( 'apikey' => '' ) ); + self::set_options( array( 'apikey' => '' ) ); set_current_screen( 'dashboard-network' ); $response = $should_display_admin_warning->invoke( self::$admin_warning ); @@ -98,13 +98,13 @@ public function test_display_admin_warning_network_admin(): void { * * @covers \Parsely\UI\Admin_Warning::should_display_admin_warning * @covers \Parsely\UI\Admin_Warning::__construct - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::get_options */ public function test_display_admin_warning_with_key(): void { $should_display_admin_warning = self::get_method( 'should_display_admin_warning', Admin_Warning::class ); - $this->set_options( array( 'apikey' => 'somekey' ) ); + self::set_options( array( 'apikey' => 'somekey' ) ); $response = $should_display_admin_warning->invoke( self::$admin_warning ); self::assertFalse( $response ); diff --git a/tests/Integration/UI/MetadataRendererTest.php b/tests/Integration/UI/MetadataRendererTest.php index 00b242da6..241fdae5a 100644 --- a/tests/Integration/UI/MetadataRendererTest.php +++ b/tests/Integration/UI/MetadataRendererTest.php @@ -100,8 +100,8 @@ public function test_run_wp_head_action_with_filter(): void { * @uses \Parsely\Metadata\Post_Builder::get_coauthor_names * @uses \Parsely\Metadata\Post_Builder::get_metadata * @uses \Parsely\Metadata\Post_Builder::get_tags - * @uses \Parsely\Parsely::api_key_is_missing - * @uses \Parsely\Parsely::api_key_is_set + * @uses \Parsely\Parsely::site_id_is_missing + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Parsely::convert_jsonld_to_parsely_type * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::post_has_trackable_status @@ -115,6 +115,11 @@ public function test_render_metadata_json_ld(): void { ob_start(); self::$metadata_renderer->render_metadata( 'json_ld' ); + /** + * Variable. + * + * @var string + */ $out = ob_get_clean(); self::assertStringContainsString( '` ); expect( content ).toContain( `` ); expect( content ).not.toContain( "` ); expect( content ).toContain( `` ); expect( content ).toContain( '
  • { impreciseNumber( data.views ) }{ impreciseNumber( data.visitors ) }{ formatToImpreciseNumber( data.views ) }{ formatToImpreciseNumber( data.visitors ) } { data.avgEngaged }
    { impreciseNumber( value.views ) }{ formatToImpreciseNumber( value.views ) }
    { impreciseNumber( value.views ) }{ formatToImpreciseNumber( value.views ) }