updated plugin Jetpack Protect version 3.0.2

This commit is contained in:
2024-10-09 12:44:31 +00:00
committed by Gitium
parent a35dc419bc
commit f970470c59
283 changed files with 6970 additions and 2338 deletions

View File

@ -5,6 +5,230 @@ 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).
## [4.35.3] - 2024-09-06
### Fixed
- Optimize repeated requests for unavailable WPCOM. [#39218]
## [4.35.2] - 2024-09-06
### Changed
- Internal updates.
## [4.35.1] - 2024-09-05
### Changed
- Internal updates.
## [4.35.0] - 2024-09-05
### Added
- My Jetpack: add support for feature-specific handling to product interstitials. [#38661]
### Changed
- Jetpack AI product page: add fair usage link on over quota notice [#39192]
- Updated package dependencies. [#39176]
### Fixed
- Gracefully fail when user data fetch failed. [#39179]
## [4.34.0] - 2024-09-02
### Changed
- My Jetpack: show over quota notice and period usage counter for unlimited plans [#39129]
- We update Welcome Flow A/B experiment to test other things [#39154]
### Fixed
- My Jetpack: change Jetpack AI post checkout URL so the redirect works [#39166]
## [4.33.1] - 2024-08-29
### Changed
- Admin menu: change order of Jetpack sub-menu items [#39095]
- My Jetpack: reflect tier filters properly on product class and UI. Fix little nuances with date constructor [#39074]
- Updated package dependencies. [#39111]
### Fixed
- Don't consider user lifecycle status when repeating evaluation [#39069]
- My Jetpack: Fix the popover and active button of the Bboost card are overlapping [#39067]
## [4.33.0] - 2024-08-26
### Added
- Add context switching to videopress card from yearly views to monthly views [#38979]
### Changed
- My Jetpack: AI product class to handle better disabling tiers [#38989]
- Updated package dependencies. [#39004]
### Fixed
- allow user currency in My Jetpack pricing [#38977]
- Fix tooltip title for videopress card [#39030]
## [4.32.4] - 2024-08-21
### Changed
- Removed unnecessary WAF dependency in My Jetpack. [#38942]
### Fixed
- Notification bubbles: avoid PHP warning when information is missing. [#38963]
- Revert recent SVG image optimizations. [#38981]
## [4.32.3] - 2024-08-19
### Added
- Add stat trends for videopress card. [#38868]
- Add tooltip to VideoPress card. [#38842]
- Add value to active card state on VideoPress My Jetpack card. [#38812]
### Changed
- Updated package dependencies. [#38893]
### Fixed
- Lossless image optimization for images (should improve performance with no visible changes). [#38750]
- My Jetpack: ensure product screens redirect to the correct post-checkout URLs. [#38704]
## [4.32.2] - 2024-08-15
### Changed
- Updated package dependencies. [#38665]
### Fixed
- fix empty hrefs causing page reload [#38845]
## [4.32.1] - 2024-08-09
### Added
- Add value to the inactive state on the VideoPress card in My Jetpack [#38748]
### Changed
- Update uses of useConnection to useMyJetpackConnection and improve typing in a few places [#38721]
### Removed
- Tests: Removed react-test-renderer. [#38755]
### Fixed
- My Jetpack: fix a bug where a user would see and infinite loading spinner when trying to connect Jetpack with one click in the Welcome flow. [#38813]
## [4.32.0] - 2024-08-05
### Added
- Add Tracks events for Welcome Flow [#38641]
### Changed
- change Jetpack AI product page link redirect [#38691]
- copy updates [#38648]
- Modify the Google Analytics notice to notify of the feature removal. [#38701]
## [4.31.0] - 2024-08-01
### Added
- Update Welcome Banner and set async site-only connection [#38534]
### Changed
- Fixup versions [#38612]
- My Jetpack: modify Jetpack AI product class and interstitial links [#38602]
- React: Changing global JSX namespace to React.JSX [#38585]
## [4.30.0] - 2024-07-29
### Added
- Async card update after async site connection [#38549]
- My Jetpack: add A/A experiment to test for correct random assignment and prevent bias in metric performance. [#38327]
- Show View button on product card along with upgrade cta [#38550]
### Changed
- Final minor enhancements to the Protect product card in My Jetpack. [#38420]
### Removed
- Remove functionality that hid 1 value on the backup card for mid-sized screens [#38441]
### Fixed
- Include Jetpack Legacy plans when checking if user has social included with plan [#38516]
- Update all tracks to snake case, camel case is not supported [#38576]
## [4.29.0] - 2024-07-22
### Added
- Added the auto-firewall status to the Protect product card in My Jetpack. [#38332]
- Added the number of logins blocked to the Protect card in My Jetpack. [#38396]
### Changed
- Connection Screen: Removed mention of Stats from the list of available free features. [#38328]
- Display My Jetpack products segemented by ownership. [#38283]
- My Jetpack: Updated social icons in the connection screen. [#38334]
- Updated statuses to make more sense in relation to ownership. [#38390]
## [4.28.0] - 2024-07-15
### Added
- Add scan/threat info to the Protect card in My Jetpack. [#38165]
### Changed
- Protect card: Add the Last scan time (with tooltip) based on product state. [#38091]
- Updated package dependencies. [#38091]
### Fixed
- Fix Learn More link destination when the site was never connected. [#38225]
## [4.27.2] - 2024-07-10
### Fixed
- Social: Fixed plan slug [#38222]
## [4.27.1] - 2024-07-03
### Changed
- Updated package dependencies. [#38132]
## [4.27.0] - 2024-07-01
### Added
- Add the Google Analytics deprecation notice. [#38078]
### Changed
- Reduce amount of historically active modules updates by only triggering on activation of plugin is a jetpack plugin [#38065]
### Fixed
- Fix an issue where the connection error hook was always resetting the notice [#38120]
## [4.26.0] - 2024-06-27
### Changed
- Jetpack AI: Add Title Optimization section on the product page. [#38072]
## [4.25.4] - 2024-06-26
### Fixed
- Move historically active modules sync to admin_init [#38041]
## [4.25.3] - 2024-06-24
### Added
- Add more default args for tracks events [#37974]
## [4.25.2] - 2024-06-21
### Fixed
- Remove the need for api requests in broken modules check [#37908]
## [4.25.1] - 2024-06-17
### Fixed
- Remove check for broken modules from red bubble connection check [#37911]
## [4.25.0] - 2024-06-17
### Changed
- Show tooltip on card hover instead of letter hover [#37858]
- Update connection footer to conditionally render warnings or errors [#37802]
- Update connection status card tests to TS [#37829]
## [4.24.7] - 2024-06-13
### Changed
- Updated package dependencies. [#37796]
## [4.24.6] - 2024-06-11
### Changed
- Conditionally show connection banner as error or info [#37707]
- Updated package dependencies. [#37779]
## [4.24.5] - 2024-06-10
### Changed
- Change codebase to use clsx instead of classnames. [#37708]
## [4.24.4] - 2024-06-10
### Added
- Add mechanism to track previously working plugins [#37537]
### Changed
- Move PRODUCT_STATUSES to constants file [#37748]
- Send user to siteless pricing page if site is not connected [#37667]
## [4.24.3] - 2024-06-05
### Changed
- Updated package dependencies. [#37669]
## [4.24.2] - 2024-06-03
### Changed
- Update ToS wording. [#37536]
- Update CTA on A4A banner in My Jetpack. [#37628]
## [4.24.1] - 2024-05-24
### Changed
- Update Search to require user connection. [#37496]
@ -1486,6 +1710,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Created package
[4.35.3]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.35.2...4.35.3
[4.35.2]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.35.1...4.35.2
[4.35.1]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.35.0...4.35.1
[4.35.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.34.0...4.35.0
[4.34.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.33.1...4.34.0
[4.33.1]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.33.0...4.33.1
[4.33.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.32.4...4.33.0
[4.32.4]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.32.3...4.32.4
[4.32.3]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.32.2...4.32.3
[4.32.2]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.32.1...4.32.2
[4.32.1]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.32.0...4.32.1
[4.32.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.31.0...4.32.0
[4.31.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.30.0...4.31.0
[4.30.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.29.0...4.30.0
[4.29.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.28.0...4.29.0
[4.28.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.27.2...4.28.0
[4.27.2]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.27.1...4.27.2
[4.27.1]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.27.0...4.27.1
[4.27.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.26.0...4.27.0
[4.26.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.25.4...4.26.0
[4.25.4]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.25.3...4.25.4
[4.25.3]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.25.2...4.25.3
[4.25.2]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.25.1...4.25.2
[4.25.1]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.25.0...4.25.1
[4.25.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.24.7...4.25.0
[4.24.7]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.24.6...4.24.7
[4.24.6]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.24.5...4.24.6
[4.24.5]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.24.4...4.24.5
[4.24.4]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.24.3...4.24.4
[4.24.3]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.24.2...4.24.3
[4.24.2]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.24.1...4.24.2
[4.24.1]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.24.0...4.24.1
[4.24.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.23.3...4.24.0
[4.23.3]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.23.2...4.23.3

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0083 0C4.47395 0 0 4.58332 0 10.2535C0 14.786 2.86662 18.6226 6.84337 19.9805C7.34057 20.0826 7.52269 19.7599 7.52269 19.4885C7.52269 19.2508 7.5063 18.436 7.5063 17.5871C4.72224 18.1983 4.14249 16.3648 4.14249 16.3648C3.69507 15.1764 3.03214 14.871 3.03214 14.871C2.12092 14.2428 3.09852 14.2428 3.09852 14.2428C4.1093 14.3107 4.63969 15.2954 4.63969 15.2954C5.53431 16.857 6.97592 16.4158 7.55588 16.1442C7.63864 15.482 7.90393 15.0237 8.18562 14.7691C5.96514 14.5315 3.62891 13.6487 3.62891 9.71017C3.62891 8.58976 4.02633 7.67309 4.65608 6.96018C4.55672 6.7056 4.20866 5.65289 4.75563 4.24394C4.75563 4.24394 5.60069 3.97227 7.50609 5.29644C8.32187 5.072 9.16316 4.95781 10.0083 4.95686C10.8533 4.95686 11.7148 5.07581 12.5102 5.29644C14.4159 3.97227 15.2609 4.24394 15.2609 4.24394C15.8079 5.65289 15.4596 6.7056 15.3602 6.96018C16.0066 7.67309 16.3876 8.58976 16.3876 9.71017C16.3876 13.6487 14.0514 14.5144 11.8143 14.7691C12.1789 15.0916 12.4936 15.7026 12.4936 16.6704C12.4936 18.0454 12.4772 19.1489 12.4772 19.4883C12.4772 19.7599 12.6596 20.0826 13.1566 19.9808C17.1334 18.6224 20 14.786 20 10.2535C20.0163 4.58332 15.526 0 10.0083 0Z" fill="#24292F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;fill:#C4C4C4" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000115494289508776443440000006419494924222942340_);}
.st1{stroke:#FFFFFF;stroke-width:2;}
.st2{fill:#FFFFFF;}
</style>
<g>
<defs>
<rect id="SVGID_1_" x="0" y="0" width="32" height="32"/>
</defs>
<clipPath id="SVGID_00000114031482566589492640000009417605807093364883_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000114031482566589492640000009417605807093364883_);">
<path class="st1" d="M16,33c9.4,0,17-7.6,17-17c0-9.4-7.6-17-17-17C6.6-1-1,6.6-1,16C-1,25.4,6.6,33,16,33z"/>
<path class="st2" d="M16.8,13.3v15.5l8-15.5H16.8z"/>
<path class="st2" d="M15.2,18.7V3.2l-8,15.5H15.2z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="22" height="26" viewBox="0 0 22 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1538 1L21.3077 5.45957V12.1076C21.3077 17.7747 17.5408 23.1405 12.2289 24.8332C11.531 25.0556 10.7767 25.0556 10.0787 24.8332C4.76694 23.1405 1 17.7747 1 12.1076V5.45957L11.1538 1Z" stroke="#A7AAAD"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@ -0,0 +1,5 @@
<svg width="21" height="24" viewBox="0 0 21 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1538 0L20.3077 4.45957V11.1076C20.3077 16.7747 16.5408 22.1405 11.2289 23.8332C10.531 24.0556 9.77674 24.0556 9.07875 23.8332C3.76694 22.1405 0 16.7747 0 11.1076V4.45957L10.1538 0Z" fill="#A7AAAD"/>
<rect x="8.63086" y="15.5" width="3.04615" height="3" rx="1.5" fill="white"/>
<path d="M8.69834 7.06333C8.66178 6.48729 9.11913 6 9.69633 6H10.6115C11.1887 6 11.6461 6.48729 11.6095 7.06333L11.2288 13.0633C11.1953 13.59 10.7585 14 10.2308 14H10.0771C9.54941 14 9.11253 13.59 9.07911 13.0633L8.69834 7.06333Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 679 B

View File

@ -0,0 +1,4 @@
<svg width="21" height="23" viewBox="0 0 21 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.3077 0.541687L19.6154 4.62962V10.7237C19.6154 15.9185 16.1624 20.8371 11.2932 22.3888C10.6534 22.5927 9.96201 22.5927 9.32218 22.3888C4.45303 20.8371 1 15.9185 1 10.7237V4.62962L10.3077 0.541687Z" fill="white" stroke="#00A32A" stroke-width="0.916667" stroke-dasharray="3.67 0.92"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1708 8.28525L9.23134 16.1521L5.46863 13.3968L6.30169 12.2933L8.94402 14.2283L14.0504 7.46481L15.1708 8.28525Z" fill="#069E08"/>
</svg>

After

Width:  |  Height:  |  Size: 619 B

View File

@ -0,0 +1,4 @@
<svg width="21" height="25" viewBox="0 0 21 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1538 0.5L20.3077 4.95957V11.6076C20.3077 17.2747 16.5408 22.6405 11.2289 24.3332C10.531 24.5556 9.77674 24.5556 9.07875 24.3332C3.76694 22.6405 0 17.2747 0 11.6076V4.95957L10.1538 0.5Z" fill="#069E08"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4591 8.94752L8.97971 17.5296L4.87493 14.5237L5.78373 13.32L8.66627 15.4308L14.2369 8.05249L15.4591 8.94752Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => 'cc72b3b90919a178b53e');
<?php return array('dependencies' => array('jetpack-connection', 'jetpack-script-data', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '1c3aa81dbb7f888e2783');

View File

@ -7,19 +7,10 @@
*/
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* cookie
* Copyright(c) 2012-2014 Roman Shtylman
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/**

View File

@ -5,21 +5,24 @@
"license": "GPL-2.0-or-later",
"require": {
"php": ">=7.0",
"automattic/jetpack-admin-ui": "^0.4.2",
"automattic/jetpack-assets": "^2.1.11",
"automattic/jetpack-boost-speed-score": "^0.3.11",
"automattic/jetpack-connection": "^2.8.4",
"automattic/jetpack-jitm": "^3.1.11",
"automattic/jetpack-licensing": "^2.0.5",
"automattic/jetpack-plugins-installer": "^0.4.0",
"automattic/jetpack-redirect": "^2.0.2",
"automattic/jetpack-constants": "^2.0.2",
"automattic/jetpack-plans": "^0.4.7",
"automattic/jetpack-status": "^3.2.0"
"automattic/jetpack-admin-ui": "^0.4.5",
"automattic/jetpack-assets": "^2.3.7",
"automattic/jetpack-boost-speed-score": "^0.3.12",
"automattic/jetpack-connection": "^4.0.1",
"automattic/jetpack-explat": "^0.1.7",
"automattic/jetpack-jitm": "^3.1.21",
"automattic/jetpack-licensing": "^2.0.9",
"automattic/jetpack-plugins-installer": "^0.4.3",
"automattic/jetpack-redirect": "^2.0.4",
"automattic/jetpack-constants": "^2.0.4",
"automattic/jetpack-plans": "^0.4.10",
"automattic/jetpack-status": "^4.0.1",
"automattic/jetpack-sync": "^3.10.0",
"automattic/jetpack-protect-status": "^0.1.5"
},
"require-dev": {
"yoast/phpunit-polyfills": "1.1.0",
"automattic/jetpack-changelogger": "^4.2.4",
"yoast/phpunit-polyfills": "^1.1.1",
"automattic/jetpack-changelogger": "^4.2.6",
"automattic/wordbless": "@dev"
},
"suggest": {
@ -68,7 +71,7 @@
"link-template": "https://github.com/Automattic/jetpack-my-jetpack/compare/${old}...${new}"
},
"branch-alias": {
"dev-trunk": "4.24.x-dev"
"dev-trunk": "4.35.x-dev"
},
"version-constants": {
"::PACKAGE_VERSION": "src/class-initializer.php"

View File

@ -7,8 +7,65 @@ declare module '*.scss';
// These libraries don't have types, this suppresses the TypeScript errors
declare module '@wordpress/components';
declare module '@wordpress/compose';
declare module '@wordpress/i18n';
declare module '@wordpress/icons';
declare module '@automattic/jetpack-connection';
declare module '@wordpress/url';
type ProductStatus =
| 'active'
| 'inactive'
| 'module_disabled'
| 'site_connection_error'
| 'plugin_absent'
| 'plugin_absent_with_plan'
| 'needs_plan'
| 'needs_activation'
| 'needs_first_site_connection'
| 'user_connection_error'
| 'can_upgrade';
type JetpackModule =
| 'anti-spam'
| 'backup'
| 'boost'
| 'crm'
| 'creator'
| 'extras'
| 'ai'
| 'jetpack-ai'
| 'scan'
| 'search'
| 'social'
| 'security'
| 'protect'
| 'videopress'
| 'stats';
type ThreatItem = {
// Protect API properties (free plan)
id: string;
title: string;
fixed_in: string;
description: string | null;
source: string | null;
// Scan API properties (paid plan)
context: string | null;
filename: string | null;
first_detected: string | null;
fixable: boolean | null;
severity: number | null;
signature: string | null;
status: number | null;
};
type ScanItem = {
checked: boolean;
name: string;
slug: string;
threats: ThreatItem[];
type: string;
version: string;
};
interface Window {
myJetpackInitialState?: {
@ -43,9 +100,16 @@ interface Window {
videoPressStats: boolean;
};
lifecycleStats: {
historicallyActiveModules: JetpackModule[];
brokenModules: {
needs_site_connection: JetpackModule[];
needs_user_connection: JetpackModule[];
};
isSiteConnected: boolean;
isUserConnected: boolean;
jetpackPlugins: Array< string >;
ownedProducts: JetpackModule[];
unownedProducts: JetpackModule[];
modules: Array< string >;
purchases: Array< string >;
};
@ -77,10 +141,12 @@ interface Window {
description: string;
disclaimers: Array< string[] >;
features: string[];
has_free_offering: boolean;
has_paid_plan_for_product: boolean;
features_by_tier: Array< string >;
is_bundle: boolean;
is_plugin_active: boolean;
is_upgradable: boolean;
is_upgradable_by_bundle: string[];
long_description: string;
manage_url: string;
@ -91,6 +157,7 @@ interface Window {
pricing_for_ui?: {
available: boolean;
wpcom_product_slug: string;
wpcom_free_product_slug?: string;
product_term: string;
currency_code: string;
full_price: number;
@ -105,6 +172,26 @@ interface Window {
transition_after_renewal_count: number;
usage_limit?: number;
};
tiers?: {
[ key: string ]: {
available: boolean;
currencyCode: string;
discountPrice: number;
fullPrice: number;
introductoryOffer?: {
costPerInterval: number;
intervalCount: number;
intervalUnit: string;
shouldProrateWhenOfferEnds: boolean;
transitionAfterRenewalCount: number;
usageLimit?: number;
};
isIntroductoryOffer: boolean;
productTerm: string;
wpcomProductSlug: string;
quantity: number;
};
};
};
purchase_url?: string;
requires_user_connection: boolean;
@ -114,7 +201,7 @@ interface Window {
is_standalone_installed: boolean;
is_standalone_active: boolean;
};
status: string;
status: ProductStatus;
supported_products: string[];
tiers: string[];
title: string;
@ -122,6 +209,60 @@ interface Window {
};
};
};
protect: {
scanData: {
core: ScanItem;
current_progress?: string;
data_source: string;
database: string[];
error: boolean;
error_code?: string;
error_message?: string;
files: string[];
has_unchecked_items: boolean;
last_checked: string;
num_plugins_threats: number;
num_themes_threats: number;
num_threats: number;
plugins: ScanItem[];
status: string;
themes: ScanItem[];
};
wafConfig: {
automatic_rules_available: boolean;
blocked_logins: number;
bootstrap_path: string;
brute_force_protection: boolean;
jetpack_waf_automatic_rules: '1' | '';
jetpack_waf_ip_allow_list: '1' | '';
jetpack_waf_ip_block_list: boolean;
jetpack_waf_ip_list: boolean;
jetpack_waf_share_data: '1' | '';
jetpack_waf_share_debug_data: boolean;
standalone_mode: boolean;
};
};
videopress: {
featuredStats?: {
label: string;
period: 'day' | 'year';
data: {
views: {
current: number;
previous: number;
};
impressions: {
current: number;
previous: number;
};
watch_time: {
current: number;
previous: number;
};
};
};
videoCount: number;
};
purchases: {
items: Array< {
ID: string;
@ -195,7 +336,10 @@ interface Window {
} >;
};
redBubbleAlerts: {
'missing-site-connection'?: null;
'missing-connection'?: {
type: string;
is_error: boolean;
};
'welcome-banner-active'?: null;
[ key: `${ string }-bad-installation` ]: {
data: {
@ -203,7 +347,27 @@ interface Window {
};
};
};
recommendedModules: {
modules: JetpackModule[] | null;
dismissed: boolean;
};
themes: {
[ key: string ]: {
Author: string;
Name: string;
RequiresPHP: string;
RequiresWP: string;
Status: string;
Template: string;
TextDomain: string;
ThemeURI: string;
Version: string;
active: boolean;
is_block_theme: boolean;
};
};
topJetpackMenuItemUrl: string;
isAtomic: boolean;
userIsAdmin: string;
userIsNewToJetpack: string;
};

View File

@ -51,7 +51,7 @@ class Activitylog {
'manage_options',
esc_url( Redirect::get_url( 'cloud-activity-log-wp-menu', $args ) ),
null,
1
8
);
}
}

View File

@ -16,14 +16,19 @@ use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use Automattic\Jetpack\ExPlat;
use Automattic\Jetpack\JITMS\JITM;
use Automattic\Jetpack\Licensing;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
use Automattic\Jetpack\Protect_Status\Status as Protect_Status;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host as Status_Host;
use Automattic\Jetpack\Sync\Functions as Sync_Functions;
use Automattic\Jetpack\Terms_Of_Service;
use Automattic\Jetpack\Tracking;
use Automattic\Jetpack\VideoPress\Stats as VideoPress_Stats;
use Automattic\Jetpack\Waf\Waf_Runner;
use Jetpack;
use WP_Error;
@ -37,7 +42,7 @@ class Initializer {
*
* @var string
*/
const PACKAGE_VERSION = '4.24.1';
const PACKAGE_VERSION = '4.35.3';
/**
* HTML container ID for the IDC screen on My Jetpack page.
@ -55,9 +60,11 @@ class Initializer {
'jetpack-search',
);
const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
const MISSING_SITE_CONNECTION_NOTIFICATION_KEY = 'missing-site-connection';
const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
const UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY = 'update-historically-active-jetpack-modules';
const MISSING_CONNECTION_NOTIFICATION_KEY = 'missing-connection';
const VIDEOPRESS_STATS_KEY = 'my-jetpack-videopress-stats';
const VIDEOPRESS_PERIOD_KEY = 'my-jetpack-videopress-period';
/**
* Holds info/data about the site (from the /sites/%d endpoint)
@ -102,9 +109,13 @@ class Initializer {
);
add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
add_action( 'admin_init', array( __CLASS__, 'setup_historically_active_jetpack_modules_sync' ) );
// This is later than the admin-ui package, which runs on 1000
add_action( 'admin_init', array( __CLASS__, 'maybe_show_red_bubble' ), 1001 );
//  Set up the ExPlat package endpoints
ExPlat::init();
// Sets up JITMS.
JITM::configure();
@ -206,6 +217,13 @@ class Initializer {
$previous_score = $speed_score_history->latest( 1 );
}
$latest_score['previousScores'] = $previous_score['scores'] ?? array();
$scan_data = Protect_Status::get_status();
self::update_historically_active_jetpack_modules();
$waf_config = array();
if ( class_exists( 'Automattic\Jetpack\Waf\Waf_Runner' ) ) {
$waf_config = Waf_Runner::get_config();
}
wp_localize_script(
'my_jetpack_main_app',
@ -218,6 +236,7 @@ class Initializer {
'items' => array(),
),
'plugins' => Plugins_Installer::get_plugins(),
'themes' => Sync_Functions::get_themes(),
'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ),
'myJetpackCheckoutUri' => admin_url( 'admin.php?page=my-jetpack' ),
'topJetpackMenuItemUrl' => Admin_Menu::get_top_level_menu_item_url(),
@ -233,13 +252,21 @@ class Initializer {
'userIsAdmin' => current_user_can( 'manage_options' ),
'userIsNewToJetpack' => self::is_jetpack_user_new(),
'lifecycleStats' => array(
'jetpackPlugins' => self::get_installed_jetpack_plugins(),
'isSiteConnected' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'purchases' => self::get_purchases(),
'modules' => self::get_active_modules(),
'jetpackPlugins' => self::get_installed_jetpack_plugins(),
'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
'ownedProducts' => Products::get_products_by_ownership( 'owned' ),
'unownedProducts' => Products::get_products_by_ownership( 'unowned' ),
'brokenModules' => self::check_for_broken_modules(),
'isSiteConnected' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'purchases' => self::get_purchases(),
'modules' => self::get_active_modules(),
),
'redBubbleAlerts' => self::get_red_bubble_alerts(),
'recommendedModules' => array(
'modules' => self::get_recommended_modules(),
'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
),
'isStatsModuleActive' => $modules->is_active( 'stats' ),
'isUserFromKnownHost' => self::is_user_from_known_host(),
'isCommercial' => self::is_commercial_site(),
@ -249,6 +276,14 @@ class Initializer {
'isAgencyAccount' => Jetpack_Manage::is_agency_account(),
),
'latestBoostSpeedScores' => $latest_score,
'protect' => array(
'scanData' => $scan_data,
'wafConfig' => array_merge(
$waf_config,
array( 'blocked_logins' => (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 ) )
),
),
'videopress' => self::get_videopress_stats(),
)
);
@ -270,6 +305,67 @@ class Initializer {
}
}
/**
* Get stats for VideoPress
*
* @return array|WP_Error
*/
public static function get_videopress_stats() {
$video_count = array_sum( (array) wp_count_attachments( 'video' ) );
if ( ! class_exists( 'Automattic\Jetpack\VideoPress\Stats' ) ) {
return array(
'videoCount' => $video_count,
);
}
$featured_stats = get_transient( self::VIDEOPRESS_STATS_KEY );
if ( $featured_stats ) {
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
$stats_period = get_transient( self::VIDEOPRESS_PERIOD_KEY );
$videopress_stats = new VideoPress_Stats();
// If the stats period exists, retrieve that information without checking the view count.
// If it does not, check the view count of monthly stats and determine if we want to show yearly or monthly stats.
if ( $stats_period ) {
if ( $stats_period === 'day' ) {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
} else {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
} else {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
if (
! is_wp_error( $featured_stats ) &&
$featured_stats &&
( $featured_stats['data']['views']['current'] < 500 || $featured_stats['data']['views']['previous'] < 500 )
) {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
}
if ( is_wp_error( $featured_stats ) || ! $featured_stats ) {
return array(
'videoCount' => $video_count,
);
}
set_transient( self::VIDEOPRESS_PERIOD_KEY, $featured_stats['period'], WEEK_IN_SECONDS );
set_transient( self::VIDEOPRESS_STATS_KEY, $featured_stats, DAY_IN_SECONDS );
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
/**
* Get product slugs of the active purchases
*
@ -285,7 +381,7 @@ class Initializer {
function ( $purchase ) {
return $purchase->product_slug;
},
$purchases
(array) $purchases
);
}
@ -324,7 +420,7 @@ class Initializer {
if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) {
$active_modules = array_diff( $active_modules, Jetpack::get_default_modules() );
}
return $active_modules;
return array_values( $active_modules );
}
/**
@ -345,12 +441,7 @@ class Initializer {
// TODO: add a data point for the last known connection/ disconnection time
// are any modules active?
$modules = new Modules();
$active_modules = $modules->get_active();
// if the Jetpack plugin is active, filter out the modules that are active by default
if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) {
$active_modules = array_diff( $active_modules, Jetpack::get_default_modules() );
}
$active_modules = self::get_active_modules();
if ( ! empty( $active_modules ) ) {
return false;
}
@ -427,6 +518,7 @@ class Initializer {
new REST_Zendesk_Chat();
new REST_Product_Data();
new REST_AI();
new REST_Recommendations_Evaluation();
register_rest_route(
'my-jetpack/v1',
@ -486,6 +578,85 @@ class Initializer {
return apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
}
/**
* Set transient to queue an update to the historically active Jetpack modules on the next wp-admin load
*
* @param string $plugin The plugin that triggered the update. This will be present if the function was queued by a plugin activation.
*
* @return void
*/
public static function queue_historically_active_jetpack_modules_update( $plugin = null ) {
$plugin_filenames = Products::get_all_plugin_filenames();
if ( ! $plugin || in_array( $plugin, $plugin_filenames, true ) ) {
set_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY, true );
}
}
/**
* Hook into several connection-based actions to update the historically active Jetpack modules
* If the transient that indicates the list needs to be synced, update it and delete the transient
*
* @return void
*/
public static function setup_historically_active_jetpack_modules_sync() {
if ( get_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
self::update_historically_active_jetpack_modules();
delete_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
}
$actions = array(
'jetpack_site_registered',
'jetpack_user_authorized',
'activated_plugin',
);
foreach ( $actions as $action ) {
add_action( $action, array( __CLASS__, 'queue_historically_active_jetpack_modules_update' ), 5 );
}
// Modules are often updated async, so we need to update them right away as there will sometimes be no page reload.
add_action( 'jetpack_activate_module', array( __CLASS__, 'update_historically_active_jetpack_modules' ), 5 );
}
/**
* Update historically active Jetpack plugins
* Historically active is defined as the Jetpack plugins that are installed and active with the required connections
* This array will consist of any plugins that were active at one point in time and are still enabled on the site
*
* @return void
*/
public static function update_historically_active_jetpack_modules() {
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
$products = Products::get_products();
foreach ( $products as $product ) {
$status = $product['status'];
$product_slug = $product['slug'];
// We want to leave modules in the array if they've been active in the past
// and were not manually disabled by the user.
if ( in_array( $status, Products::$broken_module_statuses, true ) ) {
continue;
}
// If the module is active and not already in the array, add it
if (
in_array( $status, Products::$active_module_statuses, true ) &&
! in_array( $product_slug, $historically_active_modules, true )
) {
$historically_active_modules[] = $product_slug;
}
// If the module has been disabled due to a manual user action,
// or because of a missing plan error, remove it from the array
if ( in_array( $status, Products::$disabled_module_statuses, true ) ) {
$historically_active_modules = array_values( array_diff( $historically_active_modules, array( $product_slug ) ) );
}
}
\Jetpack_Options::update_option( 'historically_active_modules', array_unique( $historically_active_modules ) );
}
/**
* Site full-data endpoint.
*
@ -503,7 +674,7 @@ class Initializer {
return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body, 200 );
return rest_ensure_response( $body );
}
/**
@ -537,7 +708,7 @@ class Initializer {
/**
* Returns whether a site has been determined "commercial" or not.
*
* @return bool
* @return bool|null
*/
public static function is_commercial_site() {
if ( is_wp_error( self::$site_info ) ) {
@ -563,13 +734,13 @@ class Initializer {
*/
public static function dismiss_welcome_banner() {
\Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
return rest_ensure_response( array( 'success' => true ), 200 );
return rest_ensure_response( array( 'success' => true ) );
}
/**
* Returns true if the site has file write access to the plugins folder, false otherwise.
*
* @return bool
* @return string
**/
public static function has_file_system_write_access() {
@ -625,7 +796,13 @@ class Initializer {
global $menu;
// filters for the items in this file
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( __CLASS__, 'add_red_bubble_alerts' ) );
$red_bubble_alerts = self::get_red_bubble_alerts();
$red_bubble_alerts = array_filter(
self::get_red_bubble_alerts(),
function ( $alert ) {
// We don't want to show silent alerts
return empty( $alert['is_silent'] );
}
);
// The Jetpack menu item should be on index 3
if (
@ -657,6 +834,63 @@ class Initializer {
return $red_bubble_alerts;
}
/**
* Get list of module names sorted by their recommendation score
*
* @return array|null
*/
public static function get_recommended_modules() {
$recommendations_evaluation = \Jetpack_Options::get_option( 'recommendations_evaluation', null );
if ( ! $recommendations_evaluation ) {
return null;
}
arsort( $recommendations_evaluation ); // Sort by scores in descending order
return array_keys( $recommendations_evaluation ); // Get only module names
}
/**
* Check for features broken by a disconnected user or site
*
* @return array
*/
public static function check_for_broken_modules() {
$connection = new Connection_Manager();
$is_user_connected = $connection->is_user_connected() || $connection->has_connected_owner();
$is_site_connected = $connection->is_connected();
$broken_modules = array(
'needs_site_connection' => array(),
'needs_user_connection' => array(),
);
if ( $is_user_connected && $is_site_connected ) {
return $broken_modules;
}
$products = Products::get_products_classes();
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
foreach ( $products as $product ) {
if ( ! in_array( $product::$slug, $historically_active_modules, true ) ) {
continue;
}
if ( $product::$requires_user_connection && ! $is_user_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_user_connection'], true ) ) {
$broken_modules['needs_user_connection'][] = $product::$slug;
}
} elseif ( ! $is_site_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_site_connection'], true ) ) {
$broken_modules['needs_site_connection'][] = $product::$slug;
}
}
}
return $broken_modules;
}
/**
* Add relevant red bubble notifications
*
@ -664,12 +898,18 @@ class Initializer {
* @return array
*/
public static function add_red_bubble_alerts( array $red_bubble_slugs ) {
if ( wp_doing_ajax() ) {
return array();
}
$connection = new Connection_Manager();
$welcome_banner_dismissed = \Jetpack_Options::get_option( 'dismissed_welcome_banner', false );
if ( self::is_jetpack_user_new() && ! $welcome_banner_dismissed ) {
$red_bubble_slugs['welcome-banner-active'] = null;
$red_bubble_slugs['welcome-banner-active'] = array(
'is_silent' => $connection->is_connected(), // we don't display the red bubble if the user is connected
);
return $red_bubble_slugs;
} else {
return self::alert_if_missing_site_connection( $red_bubble_slugs );
return self::alert_if_missing_connection( $red_bubble_slugs );
}
}
@ -679,9 +919,40 @@ class Initializer {
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_missing_site_connection( array $red_bubble_slugs ) {
if ( ! ( new Connection_Manager() )->is_connected() ) {
$red_bubble_slugs[ self::MISSING_SITE_CONNECTION_NOTIFICATION_KEY ] = null;
public static function alert_if_missing_connection( array $red_bubble_slugs ) {
$broken_modules = self::check_for_broken_modules();
$connection = new Connection_Manager();
if ( ! empty( $broken_modules['needs_user_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! empty( $broken_modules['needs_site_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! $connection->is_user_connected() && ! $connection->has_connected_owner() ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => false,
);
return $red_bubble_slugs;
}
if ( ! $connection->is_connected() ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => false,
);
return $red_bubble_slugs;
}
return $red_bubble_slugs;

View File

@ -49,7 +49,7 @@ class Jetpack_Manage {
'manage_options',
esc_url( Redirect::get_url( 'cloud-manage-dashboard-wp-menu', $args ) ),
null,
100
15
);
}

View File

@ -11,6 +11,89 @@ namespace Automattic\Jetpack\My_Jetpack;
* A class for everything related to product handling in My Jetpack
*/
class Products {
/**
* Constants for the status of a product on a site
*
* @var string
*/
const STATUS_SITE_CONNECTION_ERROR = 'site_connection_error';
const STATUS_USER_CONNECTION_ERROR = 'user_connection_error';
const STATUS_ACTIVE = 'active';
const STATUS_CAN_UPGRADE = 'can_upgrade';
const STATUS_INACTIVE = 'inactive';
const STATUS_MODULE_DISABLED = 'module_disabled';
const STATUS_PLUGIN_ABSENT = 'plugin_absent';
const STATUS_PLUGIN_ABSENT_WITH_PLAN = 'plugin_absent_with_plan';
const STATUS_NEEDS_PLAN = 'needs_plan';
const STATUS_NEEDS_ACTIVATION = 'needs_activation';
const STATUS_NEEDS_FIRST_SITE_CONNECTION = 'needs_first_site_connection';
/**
* List of statuses that display the module as disabled
* This is defined as the statuses in which the user willingly has the module disabled whether it be by
* default, uninstalling the plugin, disabling the module, or not renewing their plan.
*
* @var array
*/
public static $disabled_module_statuses = array(
self::STATUS_INACTIVE,
self::STATUS_MODULE_DISABLED,
self::STATUS_PLUGIN_ABSENT,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_ACTIVATION,
self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
);
/**
* List of statuses that display the module as broken
*
* @var array
*/
public static $broken_module_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
self::STATUS_USER_CONNECTION_ERROR,
);
/**
* List of statuses that display the module as needing attention with a warning
*
* @var array
*/
public static $warning_module_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
self::STATUS_USER_CONNECTION_ERROR,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_PLAN,
);
/**
* List of statuses that display the module as active
*
* @var array
*/
public static $active_module_statuses = array(
self::STATUS_ACTIVE,
self::STATUS_CAN_UPGRADE,
);
/**
* List of all statuses that a product can have
*
* @var array
*/
public static $all_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
self::STATUS_USER_CONNECTION_ERROR,
self::STATUS_ACTIVE,
self::STATUS_CAN_UPGRADE,
self::STATUS_INACTIVE,
self::STATUS_MODULE_DISABLED,
self::STATUS_PLUGIN_ABSENT,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_PLAN,
self::STATUS_NEEDS_ACTIVATION,
self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
);
/**
* Get the list of Products classes
@ -80,6 +163,84 @@ class Products {
return $products;
}
/**
* Get a list of products sorted by whether or not the user owns them
* An owned product is defined as a product that is any of the following
* - Active
* - Has historically been active
* - The user has a plan that includes the product
* - The user has the standalone plugin for the product installed
*
* @param string $type The type of ownership to return ('owned' or 'unowned').
*
* @return array
*/
public static function get_products_by_ownership( $type ) {
$owned_active_products = array();
$owned_warning_products = array();
$owned_inactive_products = array();
$unowned_products = array();
foreach ( self::get_products_classes() as $class ) {
$product_slug = $class::$slug;
$status = $class::get_status();
if ( $class::is_owned() ) {
// This sorts the the products in the order of active -> warning -> inactive.
// This enables the frontend to display them in that order.
// This is not needed for unowned products as those will always have a status of 'inactive'
if ( in_array( $status, self::$active_module_statuses, true ) ) {
array_push( $owned_active_products, $product_slug );
} elseif ( in_array( $status, self::$warning_module_statuses, true ) ) {
array_push( $owned_warning_products, $product_slug );
} else {
array_push( $owned_inactive_products, $product_slug );
}
continue;
}
array_push( $unowned_products, $product_slug );
}
$data = array(
'owned' => array_values(
array_unique(
array_merge(
$owned_active_products,
$owned_warning_products,
$owned_inactive_products
)
)
),
'unowned' => array_values(
array_unique( $unowned_products )
),
);
return $data[ $type ];
}
/**
* Get all plugin filenames associated with the products.
*
* @return array
*/
public static function get_all_plugin_filenames() {
$filenames = array();
foreach ( self::get_products_classes() as $class ) {
if ( ! isset( $class::$plugin_filename ) ) {
continue;
}
if ( is_array( $class::$plugin_filename ) ) {
$filenames = array_merge( $filenames, $class::$plugin_filename );
} else {
$filenames[] = $class::$plugin_filename;
}
}
return $filenames;
}
/**
* Get one product data by its slug
*
@ -156,7 +317,7 @@ class Products {
'status' => array(
'title' => 'The product status',
'type' => 'string',
'enum' => array( 'active', 'inactive', 'plugin_absent', 'needs_purchase', 'needs_purchase_or_free', 'needs_first_site_connection', 'user_connection_error', 'site_connection_error' ),
'enum' => self::$all_statuses,
),
'class' => array(
'title' => 'The product class handler',

View File

@ -107,14 +107,14 @@ class REST_Product_Data {
}
}
return rest_ensure_response( $undo_event, 200 );
return rest_ensure_response( $undo_event );
}
/**
* This will collect a count of all the items that could be backed up
* This is used to show what backup could be doing if it is not enabled
*
* @return array
* @return WP_Error|\WP_REST_Response
*/
public static function count_things_that_can_be_backed_up() {
$image_mime_type = 'image';
@ -142,6 +142,6 @@ class REST_Product_Data {
// Add all audio attachments together to get the total audio count
$data['total_audio_count'] = array_sum( (array) wp_count_attachments( $audio_mime_type ) );
return rest_ensure_response( $data, 200 );
return rest_ensure_response( $data );
}
}

View File

@ -0,0 +1,149 @@
<?php
/**
* Sets up the Evaluation Recommendations REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use WP_Error;
/**
* Registers the REST routes for Evaluation Recommendations.
*/
class REST_Recommendations_Evaluation {
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::evaluate_site_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/result/',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::save_evaluation_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/result/',
array(
array(
'methods' => \WP_REST_Server::DELETABLE,
'callback' => __CLASS__ . '::dismiss_evaluation_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
$connection = new Connection_Manager();
$is_site_connected = $connection->is_connected();
if ( ! $is_site_connected ) {
return new WP_Error(
'not_connected',
__( 'Your site is not connected to Jetpack.', 'jetpack-my-jetpack' ),
array(
'status' => 400,
)
);
}
return true; // We require site to be connected.
}
/**
* Recommendations Evaluation endpoint.
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error of 3 product slugs (recommendations).
*/
public static function evaluate_site_recommendations( $request ) {
$goals = $request->get_param( 'goals' );
if ( ! isset( $goals ) ) {
return new WP_Error( 'missing_goals', 'Goals are required', array( 'status' => 400 ) );
}
$site_id = \Jetpack_Options::get_option( 'id' );
$wpcom_endpoint = sprintf( '/sites/%1$d/jetpack-recommendations/evaluation?goals=%2$s', $site_id, implode( ',', $goals ) );
$response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, '2', array(), null, 'wpcom' );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $body ) || 200 !== $response_code ) {
return new WP_Error( 'recommendations_evaluation_fetch_failed', 'Evaluation processing failed', array( 'status' => $response_code ? $response_code : 400 ) );
}
return rest_ensure_response( $body );
}
/**
* Endpoint to save recommendations results.
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error success response.
*/
public static function save_evaluation_recommendations( $request ) {
$json = $request->get_json_params();
if ( ! isset( $json['recommendations'] ) ) {
return new WP_Error( 'missing_recommendations', 'Recommendations are required', array( 'status' => 400 ) );
}
\Jetpack_Options::update_option( 'recommendations_evaluation', $json['recommendations'] );
\Jetpack_Options::delete_option( 'dismissed_recommendations' );
return rest_ensure_response( Initializer::get_recommended_modules() );
}
/**
* Endpoint to dismiss the recommendation section
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error success response.
*/
public static function dismiss_evaluation_recommendations( $request ) {
$show_welcome_banner = $request->get_param( 'showWelcomeBanner' );
\Jetpack_Options::update_option( 'dismissed_recommendations', true );
if ( isset( $show_welcome_banner ) && $show_welcome_banner === 'true' ) {
\Jetpack_Options::delete_option( 'dismissed_welcome_banner' );
}
return rest_ensure_response( array() );
}
}

View File

@ -8,6 +8,7 @@
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Status\Visitor;
use Jetpack_Options;
use WP_Error;
@ -30,25 +31,48 @@ class Wpcom_Products {
*/
const CACHE_META_NAME = 'my-jetpack-cache';
const CACHE_CHECK_HASH_NAME = 'my-jetpack-wpcom-product-check-hash';
const MY_JETPACK_PURCHASES_TRANSIENT_KEY = 'my-jetpack-purchases';
/**
* Store the data on failed WPCOM requests.
*
* @var array
*/
private static $wpcom_request_failures = array();
/**
* Fetches the list of products from WPCOM
*
* @return Object|WP_Error
*/
private static function get_products_from_wpcom() {
$blog_id = \Jetpack_Options::get_option( 'id' );
$ip = ( new Visitor() )->get_ip( true );
$headers = array(
$connection = new Connection_Manager();
$blog_id = \Jetpack_Options::get_option( 'id' );
$ip = ( new Visitor() )->get_ip( true );
$headers = array(
'X-Forwarded-For' => $ip,
);
// If has a blog id, use connected endpoint.
if ( $blog_id ) {
$request_label = 'get_products_from_wpcom_blog_' . $blog_id;
$request_failure = static::get_request_failure( $request_label );
if ( null !== $request_failure ) {
return $request_failure;
}
// If has a blog id, use connected endpoint.
$endpoint = sprintf( '/sites/%d/products/?_locale=%s&type=jetpack', $blog_id, get_user_locale() );
// If available in the user data, set the user's currency as one of the params
if ( $connection->is_user_connected() ) {
$user_details = $connection->get_connected_user_data();
if ( ! empty( $user_details['user_currency'] ) && $user_details['user_currency'] !== 'USD' ) {
$endpoint .= sprintf( '&currency=%s', $user_details['user_currency'] );
}
}
$wpcom_request = Client::wpcom_json_api_request_as_blog(
$endpoint,
'1.1',
@ -58,6 +82,12 @@ class Wpcom_Products {
)
);
} else {
$request_label = 'get_products_from_wpcom';
$request_failure = static::get_request_failure( $request_label );
if ( null !== $request_failure ) {
return $request_failure;
}
$endpoint = 'https://public-api.wordpress.com/rest/v1.1/products?locale=' . get_user_locale() . '&type=jetpack';
$wpcom_request = wp_remote_get(
@ -73,14 +103,47 @@ class Wpcom_Products {
if ( 200 === $response_code ) {
return json_decode( wp_remote_retrieve_body( $wpcom_request ) );
} else {
return new WP_Error(
$error = new WP_Error(
'failed_to_fetch_wpcom_products',
esc_html__( 'Unable to fetch the products list from WordPress.com', 'jetpack-my-jetpack' ),
array( 'status' => $response_code )
);
static::set_request_failure( $request_label, $error );
return $error;
}
}
/**
* Super unintelligent hash string that can help us reset the cache after connection changes
* This is important because the currency can change after a user connects depending on what is set in their profile
*
* @return string
*/
private static function build_check_hash() {
static $has_user_data_fetch_error = false;
$hash_string = 'check_hash_';
$connection = new Connection_Manager();
if ( $connection->is_connected() ) {
$hash_string .= 'site_connected_';
}
if ( $connection->is_user_connected() ) {
$hash_string .= 'user_connected';
// Add the user's currency
$user_details = $has_user_data_fetch_error ? false : $connection->get_connected_user_data();
if ( $user_details === false ) {
$has_user_data_fetch_error = true;
} elseif ( ! empty( $user_details['user_currency'] ) ) {
$hash_string .= '_' . $user_details['user_currency'];
}
}
return md5( $hash_string );
}
/**
* Update the cache with new information retrieved from WPCOM
*
@ -92,6 +155,7 @@ class Wpcom_Products {
*/
private static function update_cache( $products_list ) {
update_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, time() );
update_user_meta( get_current_user_id(), self::CACHE_CHECK_HASH_NAME, self::build_check_hash() );
return update_user_meta( get_current_user_id(), self::CACHE_META_NAME, $products_list );
}
@ -102,8 +166,15 @@ class Wpcom_Products {
if ( empty( self::get_products_from_cache() ) ) {
return true;
}
// This allows the cache to reset after the site or user connects/ disconnects
$check_hash = get_user_meta( get_current_user_id(), self::CACHE_CHECK_HASH_NAME, true );
if ( $check_hash !== self::build_check_hash() ) {
return true;
}
$cache_date = get_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, true );
return time() - (int) $cache_date > ( 7 * DAY_IN_SECONDS );
return time() - (int) $cache_date > DAY_IN_SECONDS;
}
/**
@ -250,6 +321,11 @@ class Wpcom_Products {
return $stored_purchases;
}
$request_failure = static::get_request_failure( 'get_site_current_purchases' );
if ( null !== $request_failure ) {
return $request_failure;
}
$site_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
@ -260,7 +336,9 @@ class Wpcom_Products {
)
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'purchases_state_fetch_failed' );
$error = new WP_Error( 'purchases_state_fetch_failed' );
static::set_request_failure( 'get_site_current_purchases', $error );
return $error;
}
$body = wp_remote_retrieve_body( $response );
@ -270,4 +348,40 @@ class Wpcom_Products {
return $purchases;
}
/**
* Reset the request failures to retry the API requests.
*
* @return void
*/
public static function reset_request_failures() {
static::$wpcom_request_failures = array();
}
/**
* Record the request failure to prevent repeated requests.
*
* @param string $request_label The request label.
* @param WP_Error $error The error.
*
* @return void
*/
private static function set_request_failure( $request_label, WP_Error $error ) {
static::$wpcom_request_failures[ $request_label ] = $error;
}
/**
* Get the pre-saved request failure if exists.
*
* @param string $request_label The request label.
*
* @return null|WP_Error
*/
private static function get_request_failure( $request_label ) {
if ( array_key_exists( $request_label, static::$wpcom_request_failures ) ) {
return static::$wpcom_request_failures[ $request_label ];
}
return null;
}
}

View File

@ -50,6 +50,13 @@ class Anti_Spam extends Product {
*/
public static $has_free_offering = true;
/**
* Akismet has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
@ -74,7 +81,7 @@ class Anti_Spam extends Product {
* @return string
*/
public static function get_description() {
return __( 'Stop comment and form spam', 'jetpack-my-jetpack' );
return __( 'Keep your site free from spam and bots', 'jetpack-my-jetpack' );
}
/**
@ -106,6 +113,14 @@ class Anti_Spam extends Product {
* @return bool - whether an API key was found
*/
public static function has_paid_plan_for_product() {
$products_with_anti_spam = array(
'jetpack_anti_spam',
'jetpack_complete',
'jetpack_security',
'jetpack_personal',
'jetpack_premium',
'jetpack_business',
);
// Check if the site has an API key for Akismet
$akismet_api_key = apply_filters( 'akismet_get_api_key', defined( 'WPCOM_API_KEY' ) ? constant( 'WPCOM_API_KEY' ) : get_option( 'wordpress_api_key' ) );
$fallback = ! empty( $akismet_api_key );
@ -118,13 +133,10 @@ class Anti_Spam extends Product {
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Anti-spam is available as standalone bundle and as part of the Security and Complete plans.
if (
strpos( $purchase->product_slug, 'jetpack_anti_spam' ) !== false ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_security' )
) {
return true;
foreach ( $products_with_anti_spam as $product ) {
if ( strpos( $purchase->product_slug, $product ) !== false ) {
return true;
}
}
}
}

View File

@ -93,7 +93,7 @@ class Backup extends Hybrid_Product {
return __( 'Save every change', 'jetpack-my-jetpack' );
}
return __( 'Your site is not backed up', 'jetpack-my-jetpack' );
return __( 'Secure your site with automatic backups and one-click restores', 'jetpack-my-jetpack' );
}
/**
@ -184,7 +184,8 @@ class Backup extends Hybrid_Product {
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/rewind', $site_id ) . '?force=wpcom', '2', array( 'timeout' => 2 ), null, 'wpcom' );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'rewind_state_fetch_failed' );
$status = new WP_Error( 'rewind_state_fetch_failed' );
return $status;
}
$body = wp_remote_retrieve_body( $response );

View File

@ -44,6 +44,13 @@ class Boost extends Product {
*/
public static $plugin_slug = 'jetpack-boost';
/**
* Boost has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product requires a user connection
*
@ -82,7 +89,7 @@ class Boost extends Product {
* @return string
*/
public static function get_description() {
return __( 'Speed up your site in seconds', 'jetpack-my-jetpack' );
return __( 'Speed up your site and improve SEO in seconds', 'jetpack-my-jetpack' );
}
/**
@ -91,7 +98,7 @@ class Boost extends Product {
* @return string
*/
public static function get_long_description() {
return __( 'Jetpack Boost gives your site the same performance advantages as the worlds leading websites, no developer required.', 'jetpack-my-jetpack' );
return __( 'Fast sites get more page visits, more conversions, and better SEO rankings. Boost speeds up your site in seconds.', 'jetpack-my-jetpack' );
}
/**

View File

@ -80,7 +80,7 @@ class Creator extends Product {
* @return string
*/
public static function get_description() {
return __( 'Create, grow, and monetize your audience', 'jetpack-my-jetpack' );
return __( 'Get more subscribers and keep them engaged with our creator tools', 'jetpack-my-jetpack' );
}
/**
@ -89,7 +89,7 @@ class Creator extends Product {
* @return string
*/
public static function get_long_description() {
return __( 'Create, grow, and monetize your audience with powerful tools for creators.', 'jetpack-my-jetpack' );
return __( 'Craft stunning content, boost your subscriber base, and monetize your audience with subscriptions.', 'jetpack-my-jetpack' );
}
/**

View File

@ -53,6 +53,13 @@ class Crm extends Product {
*/
public static $has_free_offering = true;
/**
* CRM has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
@ -77,7 +84,7 @@ class Crm extends Product {
* @return string
*/
public static function get_description() {
return __( 'Nurture your contacts to grow your business', 'jetpack-my-jetpack' );
return __( 'Strengthen customer relationships and grow your business', 'jetpack-my-jetpack' );
}
/**
@ -86,7 +93,7 @@ class Crm extends Product {
* @return string
*/
public static function get_long_description() {
return __( 'All of your contacts in one place. Build better relationships with your customers and clients.', 'jetpack-my-jetpack' );
return __( 'Build better relationships with your customers and grow your business.', 'jetpack-my-jetpack' );
}
/**

View File

@ -81,7 +81,6 @@ class Extras extends Product {
*/
public static function get_features() {
return array(
__( 'Measure your impact with beautiful stats', 'jetpack-my-jetpack' ),
__( 'Speed up your site with optimized images', 'jetpack-my-jetpack' ),
__( 'Protect your site against bot attacks', 'jetpack-my-jetpack' ),
__( 'Get notifications if your site goes offline', 'jetpack-my-jetpack' ),

View File

@ -93,6 +93,10 @@ class Jetpack_Ai extends Product {
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
if ( ! self::are_tier_plans_enabled() ) {
return parent::get_tiers();
}
return array(
self::UPGRADED_TIER_SLUG,
self::CURRENT_TIER_SLUG,
@ -105,6 +109,10 @@ class Jetpack_Ai extends Product {
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
if ( ! self::are_tier_plans_enabled() ) {
return parent::get_features_by_tier();
}
$current_tier = self::get_current_usage_tier();
$current_description = 0 === $current_tier
? __( 'Up to 20 requests', 'jetpack-my-jetpack' )
@ -194,13 +202,15 @@ class Jetpack_Ai extends Product {
*/
public static function get_next_usage_tier() {
if ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) {
// without site connection we can't know if tiers are enabled or not,
// hence we can't know if the next tier is 100 or 1 (unlimited).
return 100;
}
$info = self::get_ai_assistant_feature();
// Bail early if it's not possible to fetch the feature data.
if ( is_wp_error( $info ) ) {
// Bail early if it's not possible to fetch the feature data or if it's included in a plan.
if ( is_wp_error( $info ) || empty( $info ) ) {
return null;
}
@ -216,7 +226,7 @@ class Jetpack_Ai extends Product {
* @return string
*/
public static function get_description() {
return __( 'The most powerful AI tool for WordPress', 'jetpack-my-jetpack' );
return __( 'Enhance your writing and productivity with our AI suite', 'jetpack-my-jetpack' );
}
/**
@ -253,14 +263,21 @@ class Jetpack_Ai extends Product {
* @return string
*/
public static function get_features_by_usage_tier( $tier ) {
$is_tier_plan = $tier && intval( $tier ) > 1;
if ( $tier === 100 && ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) ) {
// in these cases, get_next_usage_tier() will return 100
// 100 is fine as default when tiered plans are enabled, but not otherwise
$is_tier_plan = false;
}
$features = array(
1 => array(
__( 'Artificial intelligence chatbot', 'jetpack-my-jetpack' ),
__( 'Generate text, tables, lists, and forms', 'jetpack-my-jetpack' ),
__( 'Refine the tone and content to your liking', 'jetpack-my-jetpack' ),
__( 'Get feedback about your post', 'jetpack-my-jetpack' ),
__( 'Seamless WordPress editor integration', 'jetpack-my-jetpack' ),
),
__( 'Generate text, tables, lists, and forms', 'jetpack-my-jetpack' ),
__( 'Easily refine content to your liking', 'jetpack-my-jetpack' ),
__( 'Make your content easier to read', 'jetpack-my-jetpack' ),
__( 'Generate images with one-click', 'jetpack-my-jetpack' ),
__( 'Optimize your titles for better performance', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
);
$tiered_features = array(
@ -274,7 +291,7 @@ class Jetpack_Ai extends Product {
sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $tier ),
);
return isset( $features[ $tier ] ) ? $features[ $tier ] : $tiered_features;
return $is_tier_plan ? $tiered_features : $features;
}
/**
@ -305,11 +322,7 @@ class Jetpack_Ai extends Product {
return array();
}
// get info about the feature.
$info = self::get_ai_assistant_feature();
// flag to indicate if the tiers are enabled, case the info is available.
$tier_plans_enabled = ( ! is_wp_error( $info ) && isset( $info['tier-plans-enabled'] ) ) ? boolval( $info['tier-plans-enabled'] ) : false;
$tier_plans_enabled = self::are_tier_plans_enabled();
/*
* when tiers are enabled and the price tier list is empty,
@ -360,6 +373,18 @@ class Jetpack_Ai extends Product {
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
// no tiers
if ( ! self::are_tier_plans_enabled() ) {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
// hardcoding 1 as next tier if tiers are not enabled
self::get_pricing_for_ui_by_usage_tier( 1 )
);
}
$next_tier = self::get_next_usage_tier();
$current_tier = self::get_current_usage_tier();
$current_call_to_action = $current_tier === 0
@ -444,8 +469,21 @@ class Jetpack_Ai extends Product {
* @return boolean
*/
public static function is_upgradable() {
$has_ai_feature = static::does_site_have_feature( 'ai-assistant' );
$current_tier = self::get_current_usage_tier();
$has_ai_feature = static::does_site_have_feature( 'ai-assistant' );
$tier_plans_enabled = self::are_tier_plans_enabled();
$current_tier = self::get_current_usage_tier();
if ( $has_ai_feature && ! $tier_plans_enabled && $current_tier >= 1 ) {
return false;
}
$next_tier = self::get_next_usage_tier();
// The check below is debatable, not having the feature should not flag as not upgradable.
// If user is free (tier = 0), not unlimited (tier = 1) and has a next tier, then it's upgradable.
if ( $current_tier !== null && $current_tier !== 1 && $next_tier ) {
return true;
}
// Mark as not upgradable if user is on unlimited tier or does not have any plan.
if ( ! $has_ai_feature || null === $current_tier || 1 === $current_tier ) {
@ -461,7 +499,7 @@ class Jetpack_Ai extends Product {
* @return ?string
*/
public static function get_post_checkout_url() {
return '/wp-admin/admin.php?page=my-jetpack#/jetpack-ai';
return 'admin.php?page=my-jetpack#/jetpack-ai';
}
/**
@ -479,7 +517,7 @@ class Jetpack_Ai extends Product {
* @return ?string
*/
public static function get_manage_url() {
return '/wp-admin/admin.php?page=my-jetpack#/add-jetpack-ai';
return '/wp-admin/admin.php?page=my-jetpack#/jetpack-ai';
}
/**
@ -536,6 +574,25 @@ class Jetpack_Ai extends Product {
return \Jetpack_AI_Helper::get_ai_assistance_feature();
}
/**
* Get the AI Assistant tiered plans status
*
* @return boolean
*/
public static function are_tier_plans_enabled() {
$info = self::get_ai_assistant_feature();
if ( is_wp_error( $info ) ) {
// this is another faulty default value, we'll assume disabled while
// production is enabled
return false;
}
if ( ! empty( $info ) && isset( $info['tier-plans-enabled'] ) ) {
return boolval( $info['tier-plans-enabled'] );
}
return false;
}
/**
* Checks whether the site is connected to WordPress.com.
*

View File

@ -86,8 +86,8 @@ abstract class Module_Product extends Product {
*/
public static function get_status() {
$status = parent::get_status();
if ( 'inactive' === $status && ! static::is_module_active() ) {
$status = 'module_disabled';
if ( Products::STATUS_INACTIVE === $status && ! static::is_module_active() ) {
$status = Products::STATUS_MODULE_DISABLED;
}
return $status;
}

View File

@ -38,7 +38,7 @@ abstract class Product {
*
* @var string|string[]
*/
protected static $plugin_filename = null;
public static $plugin_filename = null;
/**
* The slug of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
@ -150,35 +150,37 @@ abstract class Product {
throw new \Exception( 'Product classes must declare the $slug attribute.' );
}
return array(
'slug' => static::$slug,
'plugin_slug' => static::$plugin_slug,
'name' => static::get_name(),
'title' => static::get_title(),
'description' => static::get_description(),
'long_description' => static::get_long_description(),
'tiers' => static::get_tiers(),
'features' => static::get_features(),
'features_by_tier' => static::get_features_by_tier(),
'disclaimers' => static::get_disclaimers(),
'status' => static::get_status(),
'pricing_for_ui' => static::get_pricing_for_ui(),
'is_bundle' => static::is_bundle_product(),
'is_plugin_active' => static::is_plugin_active(),
'is_upgradable' => static::is_upgradable(),
'is_upgradable_by_bundle' => static::is_upgradable_by_bundle(),
'supported_products' => static::get_supported_products(),
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'requires_user_connection' => static::$requires_user_connection,
'has_any_plan_for_product' => static::has_any_plan_for_product(),
'has_free_plan_for_product' => static::has_free_plan_for_product(),
'has_paid_plan_for_product' => static::has_paid_plan_for_product(),
'has_free_offering' => static::$has_free_offering,
'manage_url' => static::get_manage_url(),
'purchase_url' => static::get_purchase_url(),
'post_activation_url' => static::get_post_activation_url(),
'standalone_plugin_info' => static::get_standalone_info(),
'class' => static::class,
'post_checkout_url' => static::get_post_checkout_url(),
'slug' => static::$slug,
'plugin_slug' => static::$plugin_slug,
'name' => static::get_name(),
'title' => static::get_title(),
'description' => static::get_description(),
'long_description' => static::get_long_description(),
'tiers' => static::get_tiers(),
'features' => static::get_features(),
'features_by_tier' => static::get_features_by_tier(),
'disclaimers' => static::get_disclaimers(),
'status' => static::get_status(),
'pricing_for_ui' => static::get_pricing_for_ui(),
'is_bundle' => static::is_bundle_product(),
'is_plugin_active' => static::is_plugin_active(),
'is_upgradable' => static::is_upgradable(),
'is_upgradable_by_bundle' => static::is_upgradable_by_bundle(),
'supported_products' => static::get_supported_products(),
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'requires_user_connection' => static::$requires_user_connection,
'has_any_plan_for_product' => static::has_any_plan_for_product(),
'has_free_plan_for_product' => static::has_free_plan_for_product(),
'has_paid_plan_for_product' => static::has_paid_plan_for_product(),
'has_free_offering' => static::$has_free_offering,
'manage_url' => static::get_manage_url(),
'purchase_url' => static::get_purchase_url(),
'post_activation_url' => static::get_post_activation_url(),
'post_activation_urls_by_feature' => static::get_manage_urls_by_feature(),
'standalone_plugin_info' => static::get_standalone_info(),
'class' => static::class,
'post_checkout_url' => static::get_post_checkout_url(),
'post_checkout_urls_by_feature' => static::get_post_checkout_urls_by_feature(),
);
}
@ -198,7 +200,8 @@ abstract class Product {
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/features', $site_id ), '1.1' );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'site_features_fetch_failed' );
$features = new WP_Error( 'site_features_fetch_failed' );
return $features;
}
$body = wp_remote_retrieve_body( $response );
@ -305,6 +308,15 @@ abstract class Product {
*/
abstract public static function get_manage_url();
/**
* Get the URL where the user manages the product for each product feature
*
* @return ?array
*/
public static function get_manage_urls_by_feature() {
return null;
}
/**
* Get the URL the user is taken after activating the product
*
@ -323,6 +335,15 @@ abstract class Product {
return null;
}
/**
* Get the URL the user is taken after purchasing the product through the checkout for each product feature
*
* @return ?array
*/
public static function get_post_checkout_urls_by_feature() {
return null;
}
/**
* Get the WPCOM product slug used to make the purchase
*
@ -432,12 +453,36 @@ abstract class Product {
* return all the products it contains.
* Empty array by default.
*
* @return Array Product slugs
* @return array Product slugs
*/
public static function get_supported_products() {
return array();
}
/**
* Determine if the product is owned or not
* An owned product is defined as a product that is any of the following
* - Active
* - Has historically been active
* - The user has a plan that includes the product
* - The user has the standalone plugin for the product installed
*
* @return boolean
*/
public static function is_owned() {
$historically_active_modules = Jetpack_Options::get_option( 'historically_active_modules', array() );
$standalone_info = static::get_standalone_info();
if ( ( static::is_active() && Jetpack_Options::get_option( 'id' ) ) ||
$standalone_info['is_standalone_installed'] ||
in_array( static::$slug, $historically_active_modules, true ) ||
static::has_any_plan_for_product()
) {
return true;
}
return false;
}
/**
* Undocumented function
*
@ -445,48 +490,48 @@ abstract class Product {
*/
public static function get_status() {
if ( ! static::is_plugin_installed() ) {
$status = 'plugin_absent';
$status = Products::STATUS_PLUGIN_ABSENT;
if ( static::has_paid_plan_for_product() ) {
$status = 'plugin_absent_with_plan';
$status = Products::STATUS_PLUGIN_ABSENT_WITH_PLAN;
}
} elseif ( static::is_active() ) {
$status = 'active';
$status = Products::STATUS_ACTIVE;
// We only consider missing site & user connection an error when the Product is active.
if ( static::$requires_site_connection && ! ( new Connection_Manager() )->is_connected() ) {
// Site has never been connected before
if ( ! \Jetpack_Options::get_option( 'id' ) ) {
$status = 'needs_first_site_connection';
if ( ! Jetpack_Options::get_option( 'id' ) && ! static::is_owned() ) {
$status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
} else {
$status = 'site_connection_error';
$status = Products::STATUS_SITE_CONNECTION_ERROR;
}
} elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
$status = 'user_connection_error';
$status = Products::STATUS_USER_CONNECTION_ERROR;
} elseif ( static::is_upgradable() ) {
$status = 'can_upgrade';
$status = Products::STATUS_CAN_UPGRADE;
}
// Check specifically for inactive modules, which will prevent a product from being active
} elseif ( static::$module_name && ! static::is_module_active() ) {
$status = 'module_disabled';
$status = Products::STATUS_MODULE_DISABLED;
// If there is not a plan associated with the disabled module, encourage a plan first
// Getting a plan set up should help resolve any connection issues
// However if the standalone plugin for this product is active, then we will defer to showing errors that prevent the module from being active
// This is because if a standalone plugin is installed, we expect the product to not show as "inactive" on My Jetpack
if ( static::$requires_plan || ( ! static::has_any_plan_for_product() && static::$has_standalone_plugin && ! self::is_plugin_active() ) ) {
$status = static::$has_free_offering ? 'needs_purchase_or_free' : 'needs_purchase';
$status = static::is_owned() && static::$has_free_offering && ! static::$requires_plan ? Products::STATUS_NEEDS_ACTIVATION : Products::STATUS_NEEDS_PLAN;
} elseif ( static::$requires_site_connection && ! ( new Connection_Manager() )->is_connected() ) {
// Site has never been connected before
if ( ! \Jetpack_Options::get_option( 'id' ) ) {
$status = 'needs_first_site_connection';
// Site has never been connected before and product is not owned
if ( ! Jetpack_Options::get_option( 'id' ) && ! static::is_owned() ) {
$status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
} else {
$status = 'site_connection_error';
$status = Products::STATUS_SITE_CONNECTION_ERROR;
}
} elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
$status = 'user_connection_error';
$status = Products::STATUS_USER_CONNECTION_ERROR;
}
} elseif ( ! static::has_any_plan_for_product() ) {
$status = static::$has_free_offering ? 'needs_purchase_or_free' : 'needs_purchase';
$status = static::is_owned() && static::$has_free_offering && ! static::$requires_plan ? Products::STATUS_NEEDS_ACTIVATION : Products::STATUS_NEEDS_PLAN;
} else {
$status = 'inactive';
$status = Products::STATUS_INACTIVE;
}
return $status;
}

View File

@ -22,6 +22,9 @@ class Protect extends Product {
const UPGRADED_TIER_SLUG = 'upgraded';
const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_scan';
const SCAN_FEATURE_SLUG = 'scan';
const FIREWALL_FEATURE_SLUG = 'firewall';
/**
* The product slug
*
@ -61,6 +64,13 @@ class Protect extends Product {
*/
public static $has_free_offering = true;
/**
* Protect has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
@ -85,7 +95,7 @@ class Protect extends Product {
* @return string
*/
public static function get_description() {
return __( 'Powerful, automated site security', 'jetpack-my-jetpack' );
return __( 'Guard against malware and bad actors 24/7', 'jetpack-my-jetpack' );
}
/**
@ -94,7 +104,7 @@ class Protect extends Product {
* @return string
*/
public static function get_long_description() {
return __( 'Protect your site and scan for security vulnerabilities listed in our database.', 'jetpack-my-jetpack' );
return __( 'Protect your site from bad actors and malware 24/7. Clean up security vulnerabilities with one click.', 'jetpack-my-jetpack' );
}
/**
@ -255,16 +265,42 @@ class Protect extends Product {
}
/**
* Checks if the site has a paid plan for the product
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return bool
* @return boolean
*/
public static function has_paid_plan_for_product() {
$scan_data = static::get_state_from_wpcom();
if ( is_wp_error( $scan_data ) ) {
$plans_with_scan = array(
'jetpack_scan',
'jetpack_security',
'jetpack_complete',
'jetpack_premium',
'jetpack_business',
);
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
return is_object( $scan_data ) && isset( $scan_data->state ) && 'unavailable' !== $scan_data->state;
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $plans_with_scan as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
}
return false;
}
/**
* Checks whether the product can be upgraded - i.e. this shows the /#add-protect interstitial
*
* @return boolean
*/
public static function is_upgradable() {
return ! self::has_paid_plan_for_product();
}
/**
@ -276,6 +312,18 @@ class Protect extends Product {
return self::get_manage_url();
}
/**
* Get the URL the user is taken after purchasing the product through the checkout for each product feature
*
* @return ?array
*/
public static function get_post_checkout_urls_by_feature() {
return array(
self::SCAN_FEATURE_SLUG => self::get_post_checkout_url(),
self::FIREWALL_FEATURE_SLUG => admin_url( 'admin.php?page=jetpack-protect#/firewall' ),
);
}
/**
* Get the URL where the user manages the product
*
@ -285,6 +333,18 @@ class Protect extends Product {
return admin_url( 'admin.php?page=jetpack-protect' );
}
/**
* Get the URL where the user manages the product for each product feature
*
* @return ?array
*/
public static function get_manage_urls_by_feature() {
return array(
self::SCAN_FEATURE_SLUG => self::get_manage_url(),
self::FIREWALL_FEATURE_SLUG => admin_url( 'admin.php?page=jetpack-protect#/firewall' ),
);
}
/**
* Return product bundles list
* that supports the product.
@ -292,6 +352,6 @@ class Protect extends Product {
* @return array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security' );
return array( 'security', 'complete' );
}
}

View File

@ -126,7 +126,8 @@ class Scan extends Module_Product {
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/scan', $site_id ) . '?force=wpcom', '2', array( 'timeout' => 2 ), null, 'wpcom' );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'scan_state_fetch_failed' );
$status = new WP_Error( 'scan_state_fetch_failed' );
return $status;
}
$body = wp_remote_retrieve_body( $response );

View File

@ -8,6 +8,7 @@
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
@ -103,7 +104,7 @@ class Search extends Hybrid_Product {
* @return string
*/
public static function get_description() {
return __( 'Custom instant site search', 'jetpack-my-jetpack' );
return __( 'Help your visitors find what they are looking for with instant search results', 'jetpack-my-jetpack' );
}
/**
@ -204,7 +205,7 @@ class Search extends Hybrid_Product {
}
/**
* Override status to `needs_purchase_or_free` when status is `needs_purchase`.
* Override status to `needs_activation` when status is `needs_plan`.
*/
public static function get_status() {
$status = parent::get_status();
@ -222,22 +223,33 @@ class Search extends Hybrid_Product {
*/
public static function get_pricing_from_wpcom( $record_count ) {
static $pricings = array();
$connection = new Connection_Manager();
$blog_id = \Jetpack_Options::get_option( 'id' );
if ( isset( $pricings[ $record_count ] ) ) {
return $pricings[ $record_count ];
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
// For simple sites fetch the response directly.
// If the site is connected, request pricing with the blog token
if ( $blog_id ) {
$endpoint = sprintf( '/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() );
// If available in the user data, set the user's currency as one of the params
if ( $connection->is_user_connected() ) {
$user_details = $connection->get_connected_user_data();
if ( ! empty( $user_details['user_currency'] ) && $user_details['user_currency'] !== 'USD' ) {
$endpoint .= sprintf( '&currency=%s', $user_details['user_currency'] );
}
}
$response = Client::wpcom_json_api_request_as_blog(
sprintf( '/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() ),
$endpoint,
'2',
array( 'timeout' => 5 ),
null,
'wpcom'
);
} else {
// For non-simple sites we have to use the wp_remote_get, as connection might not be available.
$response = wp_remote_get(
sprintf( Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ) . '/wpcom/v2/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() ),
array( 'timeout' => 5 )

View File

@ -1,6 +1,6 @@
<?php
/**
* Search product
* Jetpack Social product
*
* @package my-jetpack
*/
@ -86,7 +86,7 @@ class Social extends Hybrid_Product {
* @return string
*/
public static function get_description() {
return __( 'Auto-publish to social media', 'jetpack-my-jetpack' );
return __( 'Effortlessly share content across social media. Right from within WordPress', 'jetpack-my-jetpack' );
}
/**
@ -95,7 +95,7 @@ class Social extends Hybrid_Product {
* @return string
*/
public static function get_long_description() {
return __( 'Promote your content on social media by automatically publishing when you publish on your site.', 'jetpack-my-jetpack' );
return __( 'Grow your following by sharing your content across social media automatically.', 'jetpack-my-jetpack' );
}
/**
@ -141,7 +141,7 @@ class Social extends Hybrid_Product {
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_social_basic_yearly';
return 'jetpack_social_v1_yearly';
}
/**
@ -150,6 +150,13 @@ class Social extends Hybrid_Product {
* @return boolean
*/
public static function has_paid_plan_for_product() {
$plans_with_social = array(
'jetpack_social',
'jetpack_complete',
'jetpack_business',
'jetpack_premium',
'jetpack_personal',
);
// For atomic sites, do a feature check to see if the republicize feature is available
// This feature is available by default on all Jetpack sites
if ( ( new Host() )->is_woa_site() ) {
@ -160,11 +167,13 @@ class Social extends Hybrid_Product {
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Social is available as standalone bundle and as part of the Complete plan.
if ( strpos( $purchase->product_slug, 'jetpack_social' ) !== false || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
foreach ( $plans_with_social as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
}

View File

@ -9,6 +9,7 @@ namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Initializer;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Status\Host;
use Jetpack_Options;
@ -90,7 +91,7 @@ class Stats extends Module_Product {
* @return string
*/
public static function get_description() {
return __( 'Simple, yet powerful analytics', 'jetpack-my-jetpack' );
return __( 'The simplest way to track visitor insights and unlock your sites growth', 'jetpack-my-jetpack' );
}
/**
@ -99,7 +100,7 @@ class Stats extends Module_Product {
* @return string
*/
public static function get_long_description() {
return __( 'With Jetpack Stats, you dont need to be a data scientist to see how your site is performing.', 'jetpack-my-jetpack' );
return __( 'With Jetpack Stats, you dont need to be a data scientist to see how your site is performing, understand your visitors, and grow your site.', 'jetpack-my-jetpack' );
}
/**
@ -169,10 +170,10 @@ class Stats extends Module_Product {
*/
public static function get_status() {
$status = parent::get_status();
if ( 'module_disabled' === $status && ! Initializer::is_registered() ) {
if ( Products::STATUS_MODULE_DISABLED === $status && ! Initializer::is_registered() ) {
// If the site has never been connected before, show the "Learn more" CTA,
// that points to the add Stats product interstitial.
$status = 'needs_purchase_or_free';
$status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
}
return $status;
}

View File

@ -92,7 +92,7 @@ class Videopress extends Hybrid_Product {
* @return string
*/
public static function get_description() {
return __( 'High quality, ad-free video', 'jetpack-my-jetpack' );
return __( 'Stunning-quality, ad-free video in the WordPress Editor', 'jetpack-my-jetpack' );
}
/**
@ -101,7 +101,7 @@ class Videopress extends Hybrid_Product {
* @return string
*/
public static function get_long_description() {
return __( 'High-quality, ad-free video built specifically for WordPress.', 'jetpack-my-jetpack' );
return __( 'Stunning-quality, ad-free video in the WordPress Editor', 'jetpack-my-jetpack' );
}
/**
@ -179,14 +179,22 @@ class Videopress extends Hybrid_Product {
* @return boolean
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
$plans_with_videopress = array(
'jetpack_videopress',
'jetpack_complete',
'jetpack_business',
'jetpack_premium',
);
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_contains( $purchase->product_slug, 'jetpack_videopress' ) || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
foreach ( $plans_with_videopress as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
}