This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,186 @@
# Changelog
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.0.0] - 2024-11-14
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## [2.0.4] - 2024-11-04
### Added
- Enable test coverage. [#39961]
## [2.0.3] - 2024-09-30
### Fixed
- Added a check for function presence to avoid fatal errors. [#39581]
## [2.0.2] - 2024-08-23
### Changed
- Updated package dependencies. [#39004]
## [2.0.1] - 2024-03-12
### Changed
- Internal updates.
## [2.0.0] - 2023-11-20
### Changed
- Updated required PHP version to >= 7.0. [#34192]
## [1.4.22] - 2023-09-19
- Minor internal updates.
## [1.4.21] - 2023-08-23
### Changed
- Updated package dependencies. [#32605]
## [1.4.20] - 2023-04-10
### Added
- Add Jetpack Autoloader package suggestion. [#29988]
## [1.4.19] - 2023-02-20
### Changed
- Minor internal updates.
## [1.4.18] - 2023-01-11
### Changed
- Updated package dependencies.
## [1.4.17] - 2022-12-02
### Changed
- Updated package dependencies. [#27688]
## [1.4.16] - 2022-11-22
### Changed
- Updated package dependencies. [#27043]
## [1.4.15] - 2022-07-26
### Changed
- Updated package dependencies. [#25158]
## [1.4.14] - 2022-06-21
### Changed
- Renaming master to trunk.
## [1.4.13] - 2022-04-26
### Changed
- Updated package dependencies.
## [1.4.12] - 2022-01-25
### Changed
- Updated package dependencies.
## [1.4.11] - 2022-01-04
### Changed
- Switch to pcov for code coverage.
- Updated package dependencies
## [1.4.10] - 2021-12-14
### Changed
- Updated package dependencies.
## [1.4.9] - 2021-11-02
### Changed
- Set `convertDeprecationsToExceptions` true in PHPUnit config.
- Update PHPUnit configs to include just what needs coverage rather than include everything then try to exclude stuff that doesn't.
## [1.4.8] - 2021-10-13
### Changed
- Updated package dependencies.
## [1.4.7] - 2021-10-12
### Changed
- Updated package dependencies
## [1.4.6] - 2021-09-28
### Changed
- Updated package dependencies.
## [1.4.5] - 2021-08-30
### Changed
- Run composer update on test-php command instead of phpunit
- Tests: update PHPUnit polyfills dependency (yoast/phpunit-polyfills).
- updated annotations versions
## [1.4.4] - 2021-05-25
### Changed
- Updated package dependencies.
## [1.4.3] - 2021-04-08
### Changed
- Packaging and build changes, no change to the package itself.
## [1.4.2] - 2021-03-30
### Added
- Composer alias for dev-master, to improve dependencies
### Changed
- Update package dependencies.
### Fixed
- Use `composer update` rather than `install` in scripts, as composer.lock isn't checked in.
## [1.4.1] - 2021-02-05
- CI: Make tests more generic
## [1.4.0] - 2021-01-20
- Add mirror-repo information to all current composer packages
## [1.3.0] - 2020-12-17
- Coverage Update whitelist for backend tests
- Pin dependencies
- Packages: Update for PHP 8 testing
## [1.2.0] - 2020-09-17
## [1.1.1] - 2020-09-17
- a8c-mc-stats: Do not distribute test files
## [1.1.0] - 2020-08-13
- CI: Try collect js coverage
## 1.0.0 - 2020-07-27
- Creates the MC Stats package
[3.0.0]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v2.0.4...v3.0.0
[2.0.4]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v2.0.3...v2.0.4
[2.0.3]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v2.0.2...v2.0.3
[2.0.2]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.22...v2.0.0
[1.4.22]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.21...v1.4.22
[1.4.21]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.20...v1.4.21
[1.4.20]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.19...v1.4.20
[1.4.19]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.18...v1.4.19
[1.4.18]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.17...v1.4.18
[1.4.17]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.16...v1.4.17
[1.4.16]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.15...v1.4.16
[1.4.15]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.14...v1.4.15
[1.4.14]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.13...v1.4.14
[1.4.13]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.12...v1.4.13
[1.4.12]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.11...v1.4.12
[1.4.11]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.10...v1.4.11
[1.4.10]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.9...v1.4.10
[1.4.9]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.8...v1.4.9
[1.4.8]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.7...v1.4.8
[1.4.7]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.6...v1.4.7
[1.4.6]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.5...v1.4.6
[1.4.5]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.4...v1.4.5
[1.4.4]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.3...v1.4.4
[1.4.3]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.2...v1.4.3
[1.4.2]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.1...v1.4.2
[1.4.1]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v1.0.0...v1.1.0
@@ -0,0 +1,357 @@
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,47 @@
# Security Policy
Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/).
## Supported Versions
Generally, only the latest version of Jetpack and its associated plugins have continued support. If a critical vulnerability is found in the current version of a plugin, we may opt to backport any patches to previous versions.
## Reporting a Vulnerability
Our HackerOne program covers the below plugin software, as well as a variety of related projects and infrastructure:
* [Jetpack](https://jetpack.com/)
* Jetpack Backup
* Jetpack Boost
* Jetpack CRM
* Jetpack Protect
* Jetpack Search
* Jetpack Social
* Jetpack VideoPress
**For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.**
Our most critical targets are:
* Jetpack and the Jetpack composer packages (all within this repo)
* Jetpack.com -- the primary marketing site.
* cloud.jetpack.com -- a management site.
* wordpress.com -- the shared management site for both Jetpack and WordPress.com sites.
For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic).
_Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._
## Guidelines
We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production:
* Please **setup a local environment** instead whenever possible. Most of our code is open source (see above).
* If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC.
* **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels.
* To be eligible for a bounty, all of these guidelines must be followed.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties.
@@ -0,0 +1,182 @@
<?php
/**
* Jetpack MC Stats package.
*
* @package automattic/jetpack-mc-stats
*/
namespace Automattic\Jetpack;
/**
* Class MC Stats, used to record stats using https://pixel.wp.com/g.gif
*/
class A8c_Mc_Stats {
/**
* Holds the stats to be processed
*
* @var array
*/
private $stats = array();
/**
* Indicates whether to use the transparent pixel (b.gif) instead of the regular smiley (g.gif)
*
* @var boolean
*/
public $use_transparent_pixel = true;
/**
* Class Constructor
*
* @param boolean $use_transparent_pixel Use the transparent pixel instead of the smiley.
*/
public function __construct( $use_transparent_pixel = true ) {
$this->use_transparent_pixel = $use_transparent_pixel;
}
/**
* Store a stat for later output.
*
* @param string $group The stat group.
* @param string $name The stat name to bump.
*
* @return boolean true if stat successfully added
*/
public function add( $group, $name ) {
if ( ! \is_string( $group ) || ! \is_string( $name ) ) {
return false;
}
if ( ! isset( $this->stats[ $group ] ) ) {
$this->stats[ $group ] = array();
}
if ( \in_array( $name, $this->stats[ $group ], true ) ) {
return false;
}
$this->stats[ $group ][] = $name;
return true;
}
/**
* Gets current stats stored to be processed
*
* @return array $stats
*/
public function get_current_stats() {
return $this->stats;
}
/**
* Return the stats from a group in an array ready to be added as parameters in a query string
*
* @param string $group_name The name of the group to retrieve.
* @return array Array with one item, where the key is the prefixed group and the value are all stats concatenated with a comma. If group not found, an empty array will be returned
*/
public function get_group_query_args( $group_name ) {
$stats = $this->get_current_stats();
if ( isset( $stats[ $group_name ] ) && ! empty( $stats[ $group_name ] ) ) {
return array( "x_jetpack-{$group_name}" => implode( ',', $stats[ $group_name ] ) );
}
return array();
}
/**
* Gets a list of trac URLs for every stored URL
*
* @return array An array of URLs
*/
public function get_stats_urls() {
$urls = array();
foreach ( $this->get_current_stats() as $group => $stat ) {
$group_query_string = $this->get_group_query_args( $group );
$urls[] = $this->build_stats_url( $group_query_string );
}
return $urls;
}
/**
* Outputs the tracking pixels for the current stats and empty the stored stats from the object
*
* @return void
*/
public function do_stats() {
$urls = $this->get_stats_urls();
foreach ( $urls as $url ) {
echo '<img src="' . esc_url( $url ) . '" width="1" height="1" style="display:none;" />';
}
$this->stats = array();
}
/**
* Pings the stats server for the current stats and empty the stored stats from the object
*
* @return void
*/
public function do_server_side_stats() {
$urls = $this->get_stats_urls();
foreach ( $urls as $url ) {
$this->do_server_side_stat( $url );
}
$this->stats = array();
}
/**
* Runs stats code for a one-off, server-side.
*
* @param string $url string The URL to be pinged. Should include `x_jetpack-{$group}={$stats}` or whatever we want to store.
*
* @return bool If it worked.
*/
public function do_server_side_stat( $url ) {
$response = wp_remote_get( esc_url_raw( $url ) );
if ( is_wp_error( $response ) ) {
return false;
}
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return false;
}
return true;
}
/**
* Builds the stats url.
*
* @param array $args array|string The arguments to append to the URL.
*
* @return string The URL to be pinged.
*/
public function build_stats_url( $args ) {
$defaults = array(
'v' => 'wpcom2',
// phpcs:ignore WordPress.WP.AlternativeFunctions.rand_rand -- There can be a case where pluggables are not yet loaded.
'rand' => md5( ( function_exists( 'wp_rand' ) ? wp_rand( 0, 999 ) : rand( 0, 999 ) ) . time() ),
);
$args = wp_parse_args( $args, $defaults );
$gifname = true === $this->use_transparent_pixel ? 'b.gif' : 'g.gif';
/**
* Filter the URL used as the Stats tracking pixel.
*
* @since-jetpack 2.3.2
* @since 1.0.0
*
* @param string $url Base URL used as the Stats tracking pixel.
*/
$base_url = apply_filters(
'jetpack_stats_base_url',
'https://pixel.wp.com/' . $gifname
);
$url = add_query_arg( $args, $base_url );
return $url;
}
}
@@ -0,0 +1,218 @@
# Changelog
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).
## [0.5.2] - 2025-02-03
### Added
- Add `remove_menu` method to `Admin_Menu` class. [#41422]
## [0.5.1] - 2024-11-25
### Changed
- Updated dependencies. [#40286]
## [0.5.0] - 2024-11-14
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## [0.4.6] - 2024-11-04
### Added
- Enable test coverage. [#39961]
## [0.4.5] - 2024-09-05
### Changed
- Jetpack menu: only register Jetpack admin page for contributor roles and above. [#39081]
## [0.4.4] - 2024-08-29
### Changed
- Admin menu: change order of Jetpack sub-menu items [#39095]
## [0.4.3] - 2024-08-23
### Changed
- Updated package dependencies. [#39004]
## [0.4.2] - 2024-04-22
### Changed
- Internal updates.
## [0.4.1] - 2024-03-12
### Changed
- Internal updates.
## [0.4.0] - 2024-03-01
### Added
- Register menus in network admin as well as regular admin. [#36058]
## [0.3.2] - 2024-01-29
### Fixed
- Wait until 'admin_menu' action to call `add_menu()`, to avoid triggering the l10n load too early. [#35279]
## [0.3.1] - 2023-11-24
## [0.3.0] - 2023-11-20
### Changed
- Updated required PHP version to >= 7.0. [#34192]
## [0.2.25] - 2023-11-14
## [0.2.24] - 2023-10-30
### Fixed
- Handle Akismet submenu even if Jetpack is present, as Jetpack now relies on this package to do so. [#33559]
## [0.2.23] - 2023-09-19
### Changed
- Updated Jetpack submenu sort order so individual features are alpha-sorted. [#32958]
## [0.2.22] - 2023-09-11
### Fixed
- Akismet: update naming to common form [#32908]
## [0.2.21] - 2023-08-23
### Changed
- Updated package dependencies. [#32605]
## [0.2.20] - 2023-04-25
### Fixed
- Avoid errors when used in combination with an older version of the Logo package. [#30136]
## [0.2.19] - 2023-04-10
### Added
- Add Jetpack Autoloader package suggestion. [#29988]
## [0.2.18] - 2023-04-04
### Changed
- Menu icon: update to latest version of the Jetpack logo [#29418]
## [0.2.17] - 2023-02-20
### Changed
- Minor internal updates.
## [0.2.16] - 2023-01-25
### Changed
- Minor internal updates.
## [0.2.15] - 2023-01-11
### Changed
- Updated package dependencies.
## [0.2.14] - 2022-12-02
### Changed
- Updated package dependencies. [#27688]
## [0.2.13] - 2022-11-22
### Changed
- Updated package dependencies. [#27043]
## [0.2.12] - 2022-09-20
### Changed
- Updated package dependencies.
## [0.2.11] - 2022-07-26
### Changed
- Updated package dependencies. [#25158]
## [0.2.10] - 2022-07-12
### Changed
- Updated package dependencies.
## [0.2.9] - 2022-06-21
### Changed
- Renaming master to trunk.
## [0.2.8] - 2022-06-14
### Changed
- Updated package dependencies.
## [0.2.7] - 2022-04-26
### Changed
- Update package.json metadata.
## [0.2.6] - 2022-04-05
### Changed
- Updated package dependencies.
## [0.2.5] - 2022-03-08
### Fixed
- Do not handle Akismet submenu if Jetpack plugin is present
## [0.2.4] - 2022-02-09
### Added
- Support for akismet menu with stand-alone plugins
### Fixed
- Fixes menu order working around a bug in add_submenu_page
## [0.2.3] - 2022-01-25
### Changed
- Updated package dependencies.
## [0.2.2] - 2022-01-18
### Changed
- General: update required node version to v16.13.2
## [0.2.1] - 2022-01-04
### Changed
- Switch to pcov for code coverage.
- Updated package dependencies
## [0.2.0] - 2021-12-14
### Added
- New method to get the top level menu item
## [0.1.1] - 2021-11-17
### Changed
- Set `convertDeprecationsToExceptions` true in PHPUnit config.
- Update PHPUnit configs to include just what needs coverage rather than include everything then try to exclude stuff that doesn't.
## 0.1.0 - 2021-10-13
### Added
- Created the package.
### Changed
- Updated package dependencies.
### Fixed
- Fixing menu visibility issues.
[0.5.2]: https://github.com/Automattic/jetpack-admin-ui/compare/0.5.1...0.5.2
[0.5.1]: https://github.com/Automattic/jetpack-admin-ui/compare/0.5.0...0.5.1
[0.5.0]: https://github.com/Automattic/jetpack-admin-ui/compare/0.4.6...0.5.0
[0.4.6]: https://github.com/Automattic/jetpack-admin-ui/compare/0.4.5...0.4.6
[0.4.5]: https://github.com/Automattic/jetpack-admin-ui/compare/0.4.4...0.4.5
[0.4.4]: https://github.com/Automattic/jetpack-admin-ui/compare/0.4.3...0.4.4
[0.4.3]: https://github.com/Automattic/jetpack-admin-ui/compare/0.4.2...0.4.3
[0.4.2]: https://github.com/Automattic/jetpack-admin-ui/compare/0.4.1...0.4.2
[0.4.1]: https://github.com/Automattic/jetpack-admin-ui/compare/0.4.0...0.4.1
[0.4.0]: https://github.com/Automattic/jetpack-admin-ui/compare/0.3.2...0.4.0
[0.3.2]: https://github.com/Automattic/jetpack-admin-ui/compare/0.3.1...0.3.2
[0.3.1]: https://github.com/Automattic/jetpack-admin-ui/compare/0.3.0...0.3.1
[0.3.0]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.25...0.3.0
[0.2.25]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.24...0.2.25
[0.2.24]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.23...0.2.24
[0.2.23]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.22...0.2.23
[0.2.22]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.21...0.2.22
[0.2.21]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.20...0.2.21
[0.2.20]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.19...0.2.20
[0.2.19]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.18...0.2.19
[0.2.18]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.17...0.2.18
[0.2.17]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.16...0.2.17
[0.2.16]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.15...0.2.16
[0.2.15]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.14...0.2.15
[0.2.14]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.13...0.2.14
[0.2.13]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.12...0.2.13
[0.2.12]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.11...0.2.12
[0.2.11]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.10...0.2.11
[0.2.10]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.9...0.2.10
[0.2.9]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.8...0.2.9
[0.2.8]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.7...0.2.8
[0.2.7]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.6...0.2.7
[0.2.6]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.5...0.2.6
[0.2.5]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.4...0.2.5
[0.2.4]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.3...0.2.4
[0.2.3]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.2...0.2.3
[0.2.2]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.1...0.2.2
[0.2.1]: https://github.com/Automattic/jetpack-admin-ui/compare/0.2.0...0.2.1
[0.2.0]: https://github.com/Automattic/jetpack-admin-ui/compare/0.1.1...0.2.0
[0.1.1]: https://github.com/Automattic/jetpack-admin-ui/compare/0.1.0...0.1.1
@@ -0,0 +1,357 @@
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,47 @@
# Security Policy
Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/).
## Supported Versions
Generally, only the latest version of Jetpack and its associated plugins have continued support. If a critical vulnerability is found in the current version of a plugin, we may opt to backport any patches to previous versions.
## Reporting a Vulnerability
Our HackerOne program covers the below plugin software, as well as a variety of related projects and infrastructure:
* [Jetpack](https://jetpack.com/)
* Jetpack Backup
* Jetpack Boost
* Jetpack CRM
* Jetpack Protect
* Jetpack Search
* Jetpack Social
* Jetpack VideoPress
**For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.**
Our most critical targets are:
* Jetpack and the Jetpack composer packages (all within this repo)
* Jetpack.com -- the primary marketing site.
* cloud.jetpack.com -- a management site.
* wordpress.com -- the shared management site for both Jetpack and WordPress.com sites.
For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic).
_Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._
## Guidelines
We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production:
* Please **setup a local environment** instead whenever possible. Most of our code is open source (see above).
* If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC.
* **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels.
* To be eligible for a bounty, all of these guidelines must be followed.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties.
@@ -0,0 +1,228 @@
<?php
/**
* Admin Menu Registration
*
* @package automattic/jetpack-admin-ui
*/
namespace Automattic\Jetpack\Admin_UI;
/**
* This class offers a wrapper to add_submenu_page and makes sure stand-alone plugin's menu items are always added under the Jetpack top level menu.
* If the Jetpack top level was not previously registered by other plugin, it will be registered here.
*/
class Admin_Menu {
const PACKAGE_VERSION = '0.5.2';
/**
* Whether this class has been initialized
*
* @var boolean
*/
private static $initialized = false;
/**
* List of menu items enqueued to be added
*
* @var array
*/
private static $menu_items = array();
/**
* Initialize the class and set up the main hook
*
* @return void
*/
public static function init() {
if ( ! self::$initialized ) {
self::$initialized = true;
self::handle_akismet_menu();
add_action( 'admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 ); // Jetpack uses 998.
add_action( 'network_admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 ); // Jetpack uses 998.
}
}
/**
* Handles the Akismet menu item when used alongside other stand-alone plugins
*
* When Jetpack plugin is present, Akismet menu item is moved under the Jetpack top level menu, but if Akismet is active alongside other stand-alone plugins,
* we use this method to move the menu item.
*/
private static function handle_akismet_menu() {
if ( class_exists( 'Akismet_Admin' ) ) {
add_action(
'admin_menu',
function () {
// Prevent Akismet from adding a menu item.
remove_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );
// Add an Anti-spam menu item for Jetpack.
self::add_menu( __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ), 6 );
},
4
);
}
}
/**
* Callback to the admin_menu and network_admin_menu hooks that will register the enqueued menu items
*
* @return void
*/
public static function admin_menu_hook_callback() {
$can_see_toplevel_menu = true;
$jetpack_plugin_present = class_exists( 'Jetpack_React_Page' );
$icon = method_exists( '\Automattic\Jetpack\Assets\Logo', 'get_base64_logo' )
? ( new \Automattic\Jetpack\Assets\Logo() )->get_base64_logo()
: 'dashicons-admin-plugins';
if ( ! $jetpack_plugin_present ) {
add_menu_page(
'Jetpack',
'Jetpack',
'edit_posts',
'jetpack',
'__return_null',
$icon,
3
);
// If Jetpack plugin is not present, user will only be able to see this menu if they have enough capability to at least one of the sub menus being added.
$can_see_toplevel_menu = false;
}
/**
* The add_sub_menu function has a bug and will not keep the right order of menu items.
*
* @see https://core.trac.wordpress.org/ticket/52035
* Let's order the items before registering them.
* Since this all happens after the Jetpack plugin menu items were added, all items will be added after Jetpack plugin items - unless position is very low number (smaller than the number of menu items present in Jetpack plugin).
*/
usort(
self::$menu_items,
function ( $a, $b ) {
$position_a = empty( $a['position'] ) ? 0 : $a['position'];
$position_b = empty( $b['position'] ) ? 0 : $b['position'];
$result = $position_a <=> $position_b;
if ( 0 === $result ) {
$result = strcmp( $a['menu_title'], $b['menu_title'] );
}
return $result;
}
);
foreach ( self::$menu_items as $menu_item ) {
if ( ! current_user_can( $menu_item['capability'] ) ) {
continue;
}
$can_see_toplevel_menu = true;
add_submenu_page(
'jetpack',
$menu_item['page_title'],
$menu_item['menu_title'],
$menu_item['capability'],
$menu_item['menu_slug'],
$menu_item['function'],
$menu_item['position']
);
}
if ( ! $jetpack_plugin_present ) {
remove_submenu_page( 'jetpack', 'jetpack' );
}
if ( ! $can_see_toplevel_menu ) {
remove_menu_page( 'jetpack' );
}
}
/**
* Adds a new submenu to the Jetpack Top level menu
*
* The parameters this method accepts are the same as @see add_submenu_page. This class will
* aggreagate all menu items registered by stand-alone plugins and make sure they all go under the same
* Jetpack top level menu. It will also handle the top level menu registration in case the Jetpack plugin is not present.
*
* @param string $page_title The text to be displayed in the title tags of the page when the menu
* is selected.
* @param string $menu_title The text to be used for the menu.
* @param string $capability The capability required for this menu to be displayed to the user.
* @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu
* and only include lowercase alphanumeric, dashes, and underscores characters
* to be compatible with sanitize_key().
* @param callable $function The function to be called to output the content for this page.
* @param int $position The position in the menu order this item should appear. Leave empty typically.
*
* @return string The resulting page's hook_suffix
*/
public static function add_menu( $page_title, $menu_title, $capability, $menu_slug, $function, $position = null ) {
self::init();
self::$menu_items[] = compact( 'page_title', 'menu_title', 'capability', 'menu_slug', 'function', 'position' );
/**
* Let's return the page hook so consumers can use.
* We know all pages will be under Jetpack top level menu page, so we can hardcode the first part of the string.
* Using get_plugin_page_hookname here won't work because the top level page is not registered yet.
*/
return 'jetpack_page_' . $menu_slug;
}
/**
* Removes an already added submenu
*
* @param string $menu_slug The slug of the submenu to remove.
*
* @return array|false The removed submenu on success, false if not found.
*/
public static function remove_menu( $menu_slug ) {
foreach ( self::$menu_items as $index => $menu_item ) {
if ( $menu_item['menu_slug'] === $menu_slug ) {
unset( self::$menu_items[ $index ] );
return $menu_item;
}
}
return false;
}
/**
* Gets the slug for the first item under the Jetpack top level menu
*
* @return string|null
*/
public static function get_top_level_menu_item_slug() {
global $submenu;
if ( ! empty( $submenu['jetpack'] ) ) {
$item = reset( $submenu['jetpack'] );
if ( isset( $item[2] ) ) {
return $item[2];
}
}
}
/**
* Gets the URL for the first item under the Jetpack top level menu
*
* @param string $fallback If Jetpack menu is not there or no children is found, return this fallback instead. Default to admin_url().
* @return string
*/
public static function get_top_level_menu_item_url( $fallback = false ) {
$slug = self::get_top_level_menu_item_slug();
if ( $slug ) {
$url = menu_page_url( $slug, false );
return $url;
}
$url = $fallback ? $fallback : admin_url();
return $url;
}
}
@@ -0,0 +1,679 @@
# Changelog
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.0.4] - 2025-02-03
### Changed
- Updated package dependencies. [#41286]
## [4.0.3] - 2025-01-20
### Changed
- Updated package dependencies. [#41099]
## [4.0.2] - 2024-12-16
### Changed
- Updated package dependencies. [#40564]
## [4.0.1] - 2024-12-04
### Changed
- Updated package dependencies. [#40363]
## [4.0.0] - 2024-11-25
### Changed
- Updated package dependencies. [#40258] [#40288]
### Removed
- Remove JSX runtime polyfill, now that we've dropped support for WordPress < 6.6. [#40200]
## [3.0.0] - 2024-11-14
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## [2.3.14] - 2024-11-11
### Changed
- Updated package dependencies. [#39999]
## [2.3.13] - 2024-11-04
### Added
- Enable test coverage. [#39961]
## [2.3.12] - 2024-10-29
### Changed
- Internal updates.
## [2.3.11] - 2024-10-29
### Fixed
- Fixed the outdated JS build for script-data [#39937]
## [2.3.10] - 2024-10-10
### Changed
- Updated package dependencies.
## [2.3.9] - 2024-10-07
### Changed
- Updated package dependencies. [#39594]
## [2.3.8] - 2024-09-10
### Changed
- Updated package dependencies. [#39302]
## [2.3.7] - 2024-09-05
### Changed
- Internal updates.
## [2.3.6] - 2024-09-05
### Changed
- Updated package dependencies. [#39176]
### Fixed
- Fixed script data not available in block editor iframe [#39221]
## [2.3.5] - 2024-08-29
### Changed
- Updated package dependencies. [#39111]
## [2.3.4] - 2024-08-23
### Changed
- Updated package dependencies. [#39004]
## [2.3.3] - 2024-08-21
### Changed
- i18n loader script & React JSX runtime: load scripts in the footer. [#38929]
## [2.3.2] - 2024-08-15
### Changed
- Updated package dependencies. [#38662]
## [2.3.1] - 2024-08-12
### Fixed
- Fixed variable names. [#38606]
## [2.3.0] - 2024-08-08
### Added
- Added jetpack-initial-state package to consolidate the logic for Initial state. [#38430]
## [2.2.0] - 2024-07-23
### Added
- Assets: Add JSX runtime polyfill `react-jsx-runtime` for WordPress < 6.6. [#38428]
## [2.1.13] - 2024-07-03
### Changed
- Updated package dependencies. [#38132]
## [2.1.12] - 2024-06-05
### Changed
- Updated package dependencies. [#37669]
## [2.1.11] - 2024-05-20
### Changed
- Internal updates.
## [2.1.10] - 2024-05-16
### Added
- Assets: Adding comments to explain why we use variables within translation functions [#37397]
### Changed
- Updated package dependencies. [#37379]
## [2.1.9] - 2024-05-06
### Changed
- Updated package dependencies. [#37147]
## [2.1.8] - 2024-04-22
### Changed
- Internal updates.
## [2.1.7] - 2024-04-08
### Changed
- Updated package dependencies. [#36760]
## [2.1.6] - 2024-03-27
### Changed
- Updated package dependencies. [#36585]
## [2.1.5] - 2024-03-18
### Changed
- Internal updates.
## [2.1.4] - 2024-03-12
### Changed
- Internal updates.
## [2.1.3] - 2024-03-12
### Changed
- Updated package dependencies. [#36325]
## [2.1.2] - 2024-03-04
### Changed
- Updated package dependencies. [#36095]
## [2.1.1] - 2024-02-13
### Changed
- Updated package dependencies. [#35608]
## [2.1.0] - 2024-02-05
### Added
- Add support for script enqueuing strategies implemented in WordPress 6.3 [#34072]
### Changed
- Updated package dependencies. [#35384]
## [2.0.4] - 2024-01-04
### Changed
- Updated package dependencies. [#34815]
## [2.0.3] - 2023-12-11
### Changed
- Updated package dependencies. [#34492]
## [2.0.2] - 2023-12-03
### Changed
- Updated package dependencies. [#34411] [#34427]
## [2.0.1] - 2023-11-21
## [2.0.0] - 2023-11-20
### Changed
- Updated required PHP version to >= 7.0. [#34192]
## [1.18.15] - 2023-11-14
### Changed
- Updated package dependencies. [#34093]
## [1.18.14] - 2023-11-03
## [1.18.13] - 2023-10-19
### Changed
- Updated package dependencies. [#33687]
## [1.18.12] - 2023-10-10
### Changed
- Updated package dependencies. [#33428]
### Fixed
- Pass `false`, not `null`, to `WP_Scripts->add()`. [#33513]
## [1.18.11] - 2023-09-19
- Minor internal updates.
## [1.18.10] - 2023-09-04
### Changed
- Updated package dependencies. [#32803]
## [1.18.9] - 2023-08-23
### Changed
- Updated package dependencies. [#32605]
## [1.18.8] - 2023-08-09
### Changed
- Updated package dependencies. [#32166]
## [1.18.7] - 2023-07-11
### Changed
- Updated package dependencies. [#31785]
## [1.18.6] - 2023-07-05
### Changed
- Updated package dependencies. [#31659]
## [1.18.5] - 2023-06-21
### Changed
- Updated package dependencies. [#31468]
## [1.18.4] - 2023-06-06
### Changed
- Updated package dependencies. [#31129]
## [1.18.3] - 2023-05-15
### Changed
- Internal updates.
## [1.18.2] - 2023-05-02
### Changed
- Updated package dependencies. [#30375]
## [1.18.1] - 2023-04-10
### Added
- Add Jetpack Autoloader package suggestion. [#29988]
## [1.18.0] - 2023-04-04
### Changed
- Async script enqueuing: switch to static method. [#29780]
- Updated package dependencies. [#29854]
## [1.17.34] - 2023-03-20
### Changed
- Updated package dependencies. [#29471]
## [1.17.33] - 2023-03-08
### Changed
- Updated package dependencies. [#29216]
## [1.17.32] - 2023-02-20
### Changed
- Minor internal updates.
## [1.17.31] - 2023-02-15
### Changed
- Update to React 18. [#28710]
## [1.17.30] - 2023-01-25
### Changed
- Minor internal updates.
## [1.17.29] - 2023-01-11
### Changed
- Updated package dependencies.
## [1.17.28] - 2022-12-02
### Changed
- Updated package dependencies.
## [1.17.27] - 2022-11-28
### Changed
- Updated package dependencies. [#27576]
## [1.17.26] - 2022-11-22
### Changed
- Updated package dependencies. [#27043]
## [1.17.25] - 2022-11-08
### Changed
- Updated package dependencies. [#27289]
## [1.17.24] - 2022-11-01
### Changed
- Updated package dependencies.
## [1.17.23] - 2022-10-13
### Changed
- Updated package dependencies. [#26791]
## [1.17.22] - 2022-10-05
### Changed
- Updated package dependencies. [#26568]
## [1.17.21] - 2022-08-25
### Changed
- Updated package dependencies. [#25814]
## [1.17.20] - 2022-07-26
### Changed
- Updated package dependencies. [#25158]
## [1.17.19] - 2022-07-12
### Changed
- Updated package dependencies.
## [1.17.18] - 2022-07-06
### Changed
- Updated package dependencies
## [1.17.17] - 2022-06-21
### Changed
- Renaming master to trunk.
## [1.17.16] - 2022-06-14
## [1.17.15] - 2022-06-08
### Changed
- Reorder JS imports for `import/order` eslint rule. [#24601]
## [1.17.14] - 2022-05-18
### Changed
- Updated package dependencies [#24372]
## [1.17.13] - 2022-05-10
### Changed
- Updated package dependencies. [#24302]
## [1.17.12] - 2022-05-04
### Added
- Add missing JavaScript dependencies, and fix a test. [#24096]
## [1.17.11] - 2022-04-26
### Changed
- Updated package dependencies.
## [1.17.10] - 2022-04-19
### Fixed
- Assets: Defer the enqueued script instead of its translations
## [1.17.9] - 2022-04-05
### Changed
- Updated package dependencies.
## [1.17.8] - 2022-03-29
### Changed
- Updated package dependencies.
## [1.17.7] - 2022-03-23
### Changed
- Updated package dependencies.
## [1.17.6] - 2022-03-02
### Changed
- Updated package dependencies.
## [1.17.5] - 2022-02-16
### Changed
- Updated package dependencies.
## [1.17.4] - 2022-02-09
### Changed
- Updated package dependencies.
## [1.17.3] - 2022-02-02
### Fixed
- Fixed minor coding standard violation.
## [1.17.2] - 2022-02-01
### Changed
- Build: remove unneeded files from production build.
## [1.17.1] - 2022-01-27
### Changed
- Updated package dependencies.
## [1.17.0] - 2022-01-25
### Added
- Accept package path prefixes from jetpack-composer-plugin and use them when lazy-loading JS translations.
- Generate the `wp-jp-i18n-loader` module needed by the new i18n-loader-webpack-plugin.
### Deprecated
- Deprecated the `wp-jp-i18n-state` module.
## [1.16.2] - 2022-01-18
### Fixed
- Handle the case where `WP_LANG_DIR` is in `WP_CONTENT_DIR`, but `WP_CONTENT_DIR` is not in `ABSPATH`.
## [1.16.1] - 2022-01-05
### Fixed
- Don't issue a "doing it wrong" warning for registering aliases during plugin activation.
## [1.16.0] - 2022-01-04
### Added
- Document use of jetpack-assets, jetpack-composer-plugin, and i18n-loader-webpack-plugin together.
### Changed
- Switch to pcov for code coverage.
- Updated package dependencies
- Updated package textdomain from `jetpack` to `jetpack-assets`.
## [1.15.0] - 2021-12-20
### Added
- Add `alias_textdomain()`.
## [1.14.0] - 2021-12-14
### Added
- Generate `wp-jp-i18n-state` script.
## [1.13.1] - 2021-11-22
### Fixed
- Call `_doing_it_wrong` correctly.
## [1.13.0] - 2021-11-22
### Added
- Have `Assets::register_script()` accept a textdomain for `wp_set_script_translations` (and complain if no textdomain is passed when `wp-i18n` is depended on).
### Changed
- Updated package dependencies
### Fixed
- Added missing option doc for `Assets::register_script()`.
## [1.12.0] - 2021-11-15
### Added
- Add `Assets::register_script()` for easier loading of Webpack-built scripts.
## [1.11.10] - 2021-11-02
### Changed
- Set `convertDeprecationsToExceptions` true in PHPUnit config.
- Update PHPUnit configs to include just what needs coverage rather than include everything then try to exclude stuff that doesn't.
## [1.11.9] - 2021-10-13
### Changed
- Updated package dependencies.
## [1.11.8] - 2021-10-06
### Changed
- Updated package dependencies
## [1.11.7] - 2021-09-28
### Changed
- Updated package dependencies.
## [1.11.6] - 2021-08-30
### Changed
- Run composer update on test-php command instead of phpunit
- Tests: update PHPUnit polyfills dependency (yoast/phpunit-polyfills).
- update annotations versions
## [1.11.5] - 2021-05-25
### Changed
- Updated package dependencies.
## [1.11.4] - 2021-04-08
### Changed
- Packaging and build changes, no change to the package itself.
## [1.11.3] - 2021-03-30
### Added
- Composer alias for dev-master, to improve dependencies
### Changed
- Update package dependencies.
### Fixed
- Use `composer update` rather than `install` in scripts, as composer.lock isn't checked in.
## [1.11.2] - 2021-02-23
- CI: Make tests more generic
## [1.11.1] - 2021-01-26
- Add mirror-repo information to all current composer packages
- Monorepo: Reorganize all projects
## [1.11.0] - 2021-01-05
- Update dependency brain/monkey to v2.6.0
## [1.10.0] - 2020-12-08
- Assets: introduce new method to process static resources
- Assets: Use defer for script tags
- Pin dependencies
- Packages: Update for PHP 8 testing
## [1.9.1] - 2020-11-24
- Update dependency brain/monkey to v2.5.0
- Updated PHPCS: Packages and Debugger
## [1.9.0] - 2020-10-27
- Instagram oEmbed: Simplify
## [1.8.0] - 2020-09-29
- Consolidate the Lazy Images package to rely on the Assets package
## [1.7.0] - 2020-08-25
- Packages: Update filenames after #16810
- CI: Try collect js coverage
- Docker: Add package testing shortcut
## [1.6.0] - 2020-07-28
- Various: Use wp_resource_hints
## [1.5.0] - 2020-06-30
- PHPCS: Clean up the packages
- WooCommerce Analytics: avoid 404 error when enqueuing script
## [1.4.0] - 2020-05-26
- Add Jetpack Scan threat notifications
## [1.3.0] - 2020-04-28
- Update dependencies to latest stable
## [1.2.0] - 2020-03-31
- Update dependencies to latest stable
## [1.1.1] - 2020-01-27
- Pin dependency brain/monkey to 2.4.0
## [1.1.0] - 2020-01-14
- Packages: Various improvements for wp.com or self-contained consumers
## [1.0.3] - 2019-11-08
- Packages: Use classmap instead of PSR-4
## [1.0.1] - 2019-10-28
- PHPCS: JITM and Assets packages
- Packages: Add gitattributes files to all packages that need th…
## 1.0.0 - 2019-09-14
- Statically access asset tools
[4.0.4]: https://github.com/Automattic/jetpack-assets/compare/v4.0.3...v4.0.4
[4.0.3]: https://github.com/Automattic/jetpack-assets/compare/v4.0.2...v4.0.3
[4.0.2]: https://github.com/Automattic/jetpack-assets/compare/v4.0.1...v4.0.2
[4.0.1]: https://github.com/Automattic/jetpack-assets/compare/v4.0.0...v4.0.1
[4.0.0]: https://github.com/Automattic/jetpack-assets/compare/v3.0.0...v4.0.0
[3.0.0]: https://github.com/Automattic/jetpack-assets/compare/v2.3.14...v3.0.0
[2.3.14]: https://github.com/Automattic/jetpack-assets/compare/v2.3.13...v2.3.14
[2.3.13]: https://github.com/Automattic/jetpack-assets/compare/v2.3.12...v2.3.13
[2.3.12]: https://github.com/Automattic/jetpack-assets/compare/v2.3.11...v2.3.12
[2.3.11]: https://github.com/Automattic/jetpack-assets/compare/v2.3.10...v2.3.11
[2.3.10]: https://github.com/Automattic/jetpack-assets/compare/v2.3.9...v2.3.10
[2.3.9]: https://github.com/Automattic/jetpack-assets/compare/v2.3.8...v2.3.9
[2.3.8]: https://github.com/Automattic/jetpack-assets/compare/v2.3.7...v2.3.8
[2.3.7]: https://github.com/Automattic/jetpack-assets/compare/v2.3.6...v2.3.7
[2.3.6]: https://github.com/Automattic/jetpack-assets/compare/v2.3.5...v2.3.6
[2.3.5]: https://github.com/Automattic/jetpack-assets/compare/v2.3.4...v2.3.5
[2.3.4]: https://github.com/Automattic/jetpack-assets/compare/v2.3.3...v2.3.4
[2.3.3]: https://github.com/Automattic/jetpack-assets/compare/v2.3.2...v2.3.3
[2.3.2]: https://github.com/Automattic/jetpack-assets/compare/v2.3.1...v2.3.2
[2.3.1]: https://github.com/Automattic/jetpack-assets/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/Automattic/jetpack-assets/compare/v2.2.0...v2.3.0
[2.2.0]: https://github.com/Automattic/jetpack-assets/compare/v2.1.13...v2.2.0
[2.1.13]: https://github.com/Automattic/jetpack-assets/compare/v2.1.12...v2.1.13
[2.1.12]: https://github.com/Automattic/jetpack-assets/compare/v2.1.11...v2.1.12
[2.1.11]: https://github.com/Automattic/jetpack-assets/compare/v2.1.10...v2.1.11
[2.1.10]: https://github.com/Automattic/jetpack-assets/compare/v2.1.9...v2.1.10
[2.1.9]: https://github.com/Automattic/jetpack-assets/compare/v2.1.8...v2.1.9
[2.1.8]: https://github.com/Automattic/jetpack-assets/compare/v2.1.7...v2.1.8
[2.1.7]: https://github.com/Automattic/jetpack-assets/compare/v2.1.6...v2.1.7
[2.1.6]: https://github.com/Automattic/jetpack-assets/compare/v2.1.5...v2.1.6
[2.1.5]: https://github.com/Automattic/jetpack-assets/compare/v2.1.4...v2.1.5
[2.1.4]: https://github.com/Automattic/jetpack-assets/compare/v2.1.3...v2.1.4
[2.1.3]: https://github.com/Automattic/jetpack-assets/compare/v2.1.2...v2.1.3
[2.1.2]: https://github.com/Automattic/jetpack-assets/compare/v2.1.1...v2.1.2
[2.1.1]: https://github.com/Automattic/jetpack-assets/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/Automattic/jetpack-assets/compare/v2.0.4...v2.1.0
[2.0.4]: https://github.com/Automattic/jetpack-assets/compare/v2.0.3...v2.0.4
[2.0.3]: https://github.com/Automattic/jetpack-assets/compare/v2.0.2...v2.0.3
[2.0.2]: https://github.com/Automattic/jetpack-assets/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/Automattic/jetpack-assets/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/Automattic/jetpack-assets/compare/v1.18.15...v2.0.0
[1.18.15]: https://github.com/Automattic/jetpack-assets/compare/v1.18.14...v1.18.15
[1.18.14]: https://github.com/Automattic/jetpack-assets/compare/v1.18.13...v1.18.14
[1.18.13]: https://github.com/Automattic/jetpack-assets/compare/v1.18.12...v1.18.13
[1.18.12]: https://github.com/Automattic/jetpack-assets/compare/v1.18.11...v1.18.12
[1.18.11]: https://github.com/Automattic/jetpack-assets/compare/v1.18.10...v1.18.11
[1.18.10]: https://github.com/Automattic/jetpack-assets/compare/v1.18.9...v1.18.10
[1.18.9]: https://github.com/Automattic/jetpack-assets/compare/v1.18.8...v1.18.9
[1.18.8]: https://github.com/Automattic/jetpack-assets/compare/v1.18.7...v1.18.8
[1.18.7]: https://github.com/Automattic/jetpack-assets/compare/v1.18.6...v1.18.7
[1.18.6]: https://github.com/Automattic/jetpack-assets/compare/v1.18.5...v1.18.6
[1.18.5]: https://github.com/Automattic/jetpack-assets/compare/v1.18.4...v1.18.5
[1.18.4]: https://github.com/Automattic/jetpack-assets/compare/v1.18.3...v1.18.4
[1.18.3]: https://github.com/Automattic/jetpack-assets/compare/v1.18.2...v1.18.3
[1.18.2]: https://github.com/Automattic/jetpack-assets/compare/v1.18.1...v1.18.2
[1.18.1]: https://github.com/Automattic/jetpack-assets/compare/v1.18.0...v1.18.1
[1.18.0]: https://github.com/Automattic/jetpack-assets/compare/v1.17.34...v1.18.0
[1.17.34]: https://github.com/Automattic/jetpack-assets/compare/v1.17.33...v1.17.34
[1.17.33]: https://github.com/Automattic/jetpack-assets/compare/v1.17.32...v1.17.33
[1.17.32]: https://github.com/Automattic/jetpack-assets/compare/v1.17.31...v1.17.32
[1.17.31]: https://github.com/Automattic/jetpack-assets/compare/v1.17.30...v1.17.31
[1.17.30]: https://github.com/Automattic/jetpack-assets/compare/v1.17.29...v1.17.30
[1.17.29]: https://github.com/Automattic/jetpack-assets/compare/v1.17.28...v1.17.29
[1.17.28]: https://github.com/Automattic/jetpack-assets/compare/v1.17.27...v1.17.28
[1.17.27]: https://github.com/Automattic/jetpack-assets/compare/v1.17.26...v1.17.27
[1.17.26]: https://github.com/Automattic/jetpack-assets/compare/v1.17.25...v1.17.26
[1.17.25]: https://github.com/Automattic/jetpack-assets/compare/v1.17.24...v1.17.25
[1.17.24]: https://github.com/Automattic/jetpack-assets/compare/v1.17.23...v1.17.24
[1.17.23]: https://github.com/Automattic/jetpack-assets/compare/v1.17.22...v1.17.23
[1.17.22]: https://github.com/Automattic/jetpack-assets/compare/v1.17.21...v1.17.22
[1.17.21]: https://github.com/Automattic/jetpack-assets/compare/v1.17.20...v1.17.21
[1.17.20]: https://github.com/Automattic/jetpack-assets/compare/v1.17.19...v1.17.20
[1.17.19]: https://github.com/Automattic/jetpack-assets/compare/v1.17.18...v1.17.19
[1.17.18]: https://github.com/Automattic/jetpack-assets/compare/v1.17.17...v1.17.18
[1.17.17]: https://github.com/Automattic/jetpack-assets/compare/v1.17.16...v1.17.17
[1.17.16]: https://github.com/Automattic/jetpack-assets/compare/v1.17.15...v1.17.16
[1.17.15]: https://github.com/Automattic/jetpack-assets/compare/v1.17.14...v1.17.15
[1.17.14]: https://github.com/Automattic/jetpack-assets/compare/v1.17.13...v1.17.14
[1.17.13]: https://github.com/Automattic/jetpack-assets/compare/v1.17.12...v1.17.13
[1.17.12]: https://github.com/Automattic/jetpack-assets/compare/v1.17.11...v1.17.12
[1.17.11]: https://github.com/Automattic/jetpack-assets/compare/v1.17.10...v1.17.11
[1.17.10]: https://github.com/Automattic/jetpack-assets/compare/v1.17.9...v1.17.10
[1.17.9]: https://github.com/Automattic/jetpack-assets/compare/v1.17.8...v1.17.9
[1.17.8]: https://github.com/Automattic/jetpack-assets/compare/v1.17.7...v1.17.8
[1.17.7]: https://github.com/Automattic/jetpack-assets/compare/v1.17.6...v1.17.7
[1.17.6]: https://github.com/Automattic/jetpack-assets/compare/v1.17.5...v1.17.6
[1.17.5]: https://github.com/Automattic/jetpack-assets/compare/v1.17.4...v1.17.5
[1.17.4]: https://github.com/Automattic/jetpack-assets/compare/v1.17.3...v1.17.4
[1.17.3]: https://github.com/Automattic/jetpack-assets/compare/v1.17.2...v1.17.3
[1.17.2]: https://github.com/Automattic/jetpack-assets/compare/v1.17.1...v1.17.2
[1.17.1]: https://github.com/Automattic/jetpack-assets/compare/v1.17.0...v1.17.1
[1.17.0]: https://github.com/Automattic/jetpack-assets/compare/v1.16.2...v1.17.0
[1.16.2]: https://github.com/Automattic/jetpack-assets/compare/v1.16.1...v1.16.2
[1.16.1]: https://github.com/Automattic/jetpack-assets/compare/v1.16.0...v1.16.1
[1.16.0]: https://github.com/Automattic/jetpack-assets/compare/v1.15.0...v1.16.0
[1.15.0]: https://github.com/Automattic/jetpack-assets/compare/v1.14.0...v1.15.0
[1.14.0]: https://github.com/Automattic/jetpack-assets/compare/v1.13.1...v1.14.0
[1.13.1]: https://github.com/Automattic/jetpack-assets/compare/v1.13.0...v1.13.1
[1.13.0]: https://github.com/Automattic/jetpack-assets/compare/v1.12.0...v1.13.0
[1.12.0]: https://github.com/Automattic/jetpack-assets/compare/v1.11.10...v1.12.0
[1.11.10]: https://github.com/Automattic/jetpack-assets/compare/v1.11.9...v1.11.10
[1.11.9]: https://github.com/Automattic/jetpack-assets/compare/v1.11.8...v1.11.9
[1.11.8]: https://github.com/Automattic/jetpack-assets/compare/v1.11.7...v1.11.8
[1.11.7]: https://github.com/Automattic/jetpack-assets/compare/v1.11.6...v1.11.7
[1.11.6]: https://github.com/Automattic/jetpack-assets/compare/v1.11.5...v1.11.6
[1.11.5]: https://github.com/Automattic/jetpack-assets/compare/v1.11.4...v1.11.5
[1.11.4]: https://github.com/Automattic/jetpack-assets/compare/v1.11.3...v1.11.4
[1.11.3]: https://github.com/Automattic/jetpack-assets/compare/v1.11.2...v1.11.3
[1.11.2]: https://github.com/Automattic/jetpack-assets/compare/v1.11.1...v1.11.2
[1.11.1]: https://github.com/Automattic/jetpack-assets/compare/v1.11.0...v1.11.1
[1.11.0]: https://github.com/Automattic/jetpack-assets/compare/v1.10.0...v1.11.0
[1.10.0]: https://github.com/Automattic/jetpack-assets/compare/v1.9.1...v1.10.0
[1.9.1]: https://github.com/Automattic/jetpack-assets/compare/v1.9.0...v1.9.1
[1.9.0]: https://github.com/Automattic/jetpack-assets/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/Automattic/jetpack-assets/compare/v1.7.0...v1.8.0
[1.7.0]: https://github.com/Automattic/jetpack-assets/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/Automattic/jetpack-assets/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/Automattic/jetpack-assets/compare/v1.4.0...v1.5.0
[1.4.0]: https://github.com/Automattic/jetpack-assets/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/Automattic/jetpack-assets/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/Automattic/jetpack-assets/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/Automattic/jetpack-assets/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/Automattic/jetpack-assets/compare/v1.0.3...v1.1.0
[1.0.3]: https://github.com/Automattic/jetpack-assets/compare/v1.0.1...v1.0.3
[1.0.1]: https://github.com/Automattic/jetpack-assets/compare/v1.0.0...v1.0.1
@@ -0,0 +1,357 @@
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,47 @@
# Security Policy
Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/).
## Supported Versions
Generally, only the latest version of Jetpack and its associated plugins have continued support. If a critical vulnerability is found in the current version of a plugin, we may opt to backport any patches to previous versions.
## Reporting a Vulnerability
Our HackerOne program covers the below plugin software, as well as a variety of related projects and infrastructure:
* [Jetpack](https://jetpack.com/)
* Jetpack Backup
* Jetpack Boost
* Jetpack CRM
* Jetpack Protect
* Jetpack Search
* Jetpack Social
* Jetpack VideoPress
**For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.**
Our most critical targets are:
* Jetpack and the Jetpack composer packages (all within this repo)
* Jetpack.com -- the primary marketing site.
* cloud.jetpack.com -- a management site.
* wordpress.com -- the shared management site for both Jetpack and WordPress.com sites.
For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic).
_Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._
## Guidelines
We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production:
* Please **setup a local environment** instead whenever possible. Most of our code is open source (see above).
* If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC.
* **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels.
* To be eligible for a bounty, all of these guidelines must be followed.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties.
@@ -0,0 +1,25 @@
<?php
/**
* Action Hooks for Jetpack Assets module.
*
* @package automattic/jetpack-assets
*/
// If WordPress's plugin API is available already, use it. If not,
// drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`.
if ( function_exists( 'add_action' ) ) {
add_action( 'wp_default_scripts', array( Automattic\Jetpack\Assets::class, 'wp_default_scripts_hook' ) );
add_action( 'plugins_loaded', array( Automattic\Jetpack\Assets\Script_Data::class, 'configure' ), 1 );
} else {
global $wp_filter;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter['wp_default_scripts'][10][] = array(
'accepted_args' => 1,
'function' => array( Automattic\Jetpack\Assets::class, 'wp_default_scripts_hook' ),
);
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter['plugins_loaded'][1][] = array(
'accepted_args' => 0,
'function' => array( Automattic\Jetpack\Assets\Script_Data::class, 'configure' ),
);
}
@@ -0,0 +1 @@
<?php return array('dependencies' => array('wp-i18n'), 'version' => 'becd7d9884bc1b331e45');
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '3d6b6c0a2cee025f46f3');
@@ -0,0 +1 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.JetpackScriptDataModule=t():e.JetpackScriptDataModule=t()}(globalThis,(()=>(()=>{var e={729:(e,t,r)=>{"use strict";r.r(t),r.d(t,{getActiveFeatures:()=>a.mH,getAdminUrl:()=>a.hT,getJetpackAdminPageUrl:()=>a.oQ,getMyJetpackUrl:()=>a.e5,getScriptData:()=>a.au,getSiteData:()=>a.sV,siteHasFeature:()=>a.IT});var n=r(428),o={};for(const e in n)"default"!==e&&(o[e]=()=>n[e]);r.d(t,o);var a=r(336)},428:()=>{},336:(e,t,r)=>{"use strict";function n(){return window.JetpackScriptData}function o(){return n().site}function a(e=""){return`${n().site.admin_url}${e}`}function i(e=""){return a(`admin.php?page=jetpack${e}`)}function u(e=""){return a(`admin.php?page=my-jetpack${e}`)}function c(){return n().site.plan?.features?.active??[]}function p(e){return c().includes(e)}r.d(t,{IT:()=>p,au:()=>n,e5:()=>u,hT:()=>a,mH:()=>c,oQ:()=>i,sV:()=>o})}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};return(()=>{"use strict";r.r(n);var e=r(729),t={};for(const r in e)"default"!==r&&(t[r]=()=>e[r]);r.d(n,t)})(),n})()));
@@ -0,0 +1,27 @@
{
"private": true,
"browserslist": "extends @wordpress/browserslist-config",
"scripts": {
"build": "pnpm run clean && pnpm run build-js",
"build-js": "webpack",
"build-production": "pnpm run clean && pnpm run build-production-js && pnpm run validate",
"build-production-js": "NODE_ENV=production BABEL_ENV=production pnpm run build-js",
"clean": "rm -rf build",
"test": "jest --config=tests/jest.config.cjs",
"test-coverage": "pnpm run test --coverage",
"validate": "pnpm exec validate-es build/"
},
"dependencies": {
"@automattic/jetpack-script-data": "^0.1.9",
"react": "18.3.1"
},
"devDependencies": {
"@automattic/jetpack-webpack-config": "workspace:*",
"@wordpress/browserslist-config": "6.16.0",
"concurrently": "7.6.0",
"jest": "29.7.0",
"md5-es": "1.8.2",
"webpack": "5.94.0",
"webpack-cli": "6.0.1"
}
}
@@ -0,0 +1,763 @@
<?php
/**
* Jetpack Assets package.
*
* @package automattic/jetpack-assets
*/
namespace Automattic\Jetpack;
use Automattic\Jetpack\Assets\Semver;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use InvalidArgumentException;
/**
* Class Assets
*/
class Assets {
/**
* Holds all the scripts handles that should be loaded in a deferred fashion.
*
* @var array
*/
private $defer_script_handles = array();
/**
* The singleton instance of this class.
*
* @var Assets
*/
protected static $instance;
/**
* The registered textdomain mappings.
*
* @var array `array( mapped_domain => array( string target_domain, string target_type, string semver, string path_prefix ) )`.
*/
private static $domain_map = array();
/**
* Constructor.
*
* Static-only class, so nothing here.
*/
private function __construct() {}
// ////////////////////
// region Async script loading
/**
* Get the singleton instance of the class.
*
* @return Assets
*/
public static function instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new Assets();
}
return self::$instance;
}
/**
* A public method for adding the async script.
*
* @deprecated Since 2.1.0, the `strategy` feature should be used instead, with the "defer" setting.
*
* @param string $script_handle Script handle.
*/
public static function add_async_script( $script_handle ) {
_deprecated_function( __METHOD__, '2.1.0' );
wp_script_add_data( $script_handle, 'strategy', 'defer' );
}
/**
* Add an async attribute to scripts that can be loaded deferred.
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
*
* @deprecated Since 2.1.0, the `strategy` feature should be used instead.
*
* @param string $tag The <script> tag for the enqueued script.
* @param string $handle The script's registered handle.
*/
public function script_add_async( $tag, $handle ) {
_deprecated_function( __METHOD__, '2.1.0' );
if ( empty( $this->defer_script_handles ) ) {
return $tag;
}
if ( in_array( $handle, $this->defer_script_handles, true ) ) {
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
return preg_replace( '/<script( [^>]*)? src=/i', '<script defer$1 src=', $tag );
}
return $tag;
}
/**
* A helper function that lets you enqueue scripts in an async fashion.
*
* @deprecated Since 2.1.0 - use the strategy feature instead.
*
* @param string $handle Name of the script. Should be unique.
* @param string $min_path Minimized script path.
* @param string $non_min_path Full Script path.
* @param array $deps Array of script dependencies.
* @param bool $ver The script version.
* @param bool $in_footer Should the script be included in the footer.
*/
public static function enqueue_async_script( $handle, $min_path, $non_min_path, $deps = array(), $ver = false, $in_footer = true ) {
_deprecated_function( __METHOD__, '2.1.0' );
wp_enqueue_script( $handle, self::get_file_url_for_environment( $min_path, $non_min_path ), $deps, $ver, $in_footer );
wp_script_add_data( $handle, 'strategy', 'defer' );
}
// endregion .
// ////////////////////
// region Utils
/**
* Given a minified path, and a non-minified path, will return
* a minified or non-minified file URL based on whether SCRIPT_DEBUG is set and truthy.
*
* If $package_path is provided, then the minified or non-minified file URL will be generated
* relative to the root package directory.
*
* Both `$min_base` and `$non_min_base` can be either full URLs, or are expected to be relative to the
* root Jetpack directory.
*
* @param string $min_path minified path.
* @param string $non_min_path non-minified path.
* @param string $package_path Optional. A full path to a file inside a package directory
* The URL will be relative to its directory. Default empty.
* Typically this is done by passing __FILE__ as the argument.
*
* @return string The URL to the file
* @since 1.0.3
* @since-jetpack 5.6.0
*/
public static function get_file_url_for_environment( $min_path, $non_min_path, $package_path = '' ) {
$path = ( Jetpack_Constants::is_defined( 'SCRIPT_DEBUG' ) && Jetpack_Constants::get_constant( 'SCRIPT_DEBUG' ) )
? $non_min_path
: $min_path;
/*
* If the path is actually a full URL, keep that.
* We look for a host value, since enqueues are sometimes without a scheme.
*/
$file_parts = wp_parse_url( $path );
if ( ! empty( $file_parts['host'] ) ) {
$url = $path;
} else {
$plugin_path = empty( $package_path ) ? Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) : $package_path;
$url = plugins_url( $path, $plugin_path );
}
/**
* Filters the URL for a file passed through the get_file_url_for_environment function.
*
* @since 1.0.3
*
* @package assets
*
* @param string $url The URL to the file.
* @param string $min_path The minified path.
* @param string $non_min_path The non-minified path.
*/
return apply_filters( 'jetpack_get_file_for_environment', $url, $min_path, $non_min_path );
}
/**
* Passes an array of URLs to wp_resource_hints.
*
* @since 1.5.0
*
* @param string|array $urls URLs to hint.
* @param string $type One of the supported resource types: dns-prefetch (default), preconnect, prefetch, or prerender.
*/
public static function add_resource_hint( $urls, $type = 'dns-prefetch' ) {
add_filter(
'wp_resource_hints',
function ( $hints, $resource_type ) use ( $urls, $type ) {
if ( $resource_type === $type ) {
// Type casting to array required since the function accepts a single string.
foreach ( (array) $urls as $url ) {
$hints[] = $url;
}
}
return $hints;
},
10,
2
);
}
/**
* Serve a WordPress.com static resource via a randomized wp.com subdomain.
*
* @since 1.9.0
*
* @param string $url WordPress.com static resource URL.
*
* @return string $url
*/
public static function staticize_subdomain( $url ) {
// Extract hostname from URL.
$host = wp_parse_url( $url, PHP_URL_HOST );
// Explode hostname on '.'.
$exploded_host = explode( '.', $host );
// Retrieve the name and TLD.
if ( count( $exploded_host ) > 1 ) {
$name = $exploded_host[ count( $exploded_host ) - 2 ];
$tld = $exploded_host[ count( $exploded_host ) - 1 ];
// Rebuild domain excluding subdomains.
$domain = $name . '.' . $tld;
} else {
$domain = $host;
}
// Array of Automattic domains.
$domains_allowed = array( 'wordpress.com', 'wp.com' );
// Return $url if not an Automattic domain.
if ( ! in_array( $domain, $domains_allowed, true ) ) {
return $url;
}
if ( \is_ssl() ) {
return preg_replace( '|https?://[^/]++/|', 'https://s-ssl.wordpress.com/', $url );
}
/*
* Generate a random subdomain id by taking the modulus of the crc32 value of the URL.
* Valid values are 0, 1, and 2.
*/
$static_counter = abs( crc32( basename( $url ) ) % 3 );
return preg_replace( '|://[^/]+?/|', "://s$static_counter.wp.com/", $url );
}
/**
* Resolve '.' and '..' components in a path or URL.
*
* @since 1.12.0
* @param string $path Path or URL.
* @return string Normalized path or URL.
*/
public static function normalize_path( $path ) {
$parts = wp_parse_url( $path );
if ( ! isset( $parts['path'] ) ) {
return $path;
}
$ret = '';
$ret .= isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
$ret .= $parts['user'] ?? '';
$ret .= isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
$ret .= '@';
}
$ret .= $parts['host'] ?? '';
$ret .= isset( $parts['port'] ) ? ':' . $parts['port'] : '';
$pp = explode( '/', $parts['path'] );
if ( '' === $pp[0] ) {
$ret .= '/';
array_shift( $pp );
}
$i = 0;
while ( $i < count( $pp ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
if ( '' === $pp[ $i ] || '.' === $pp[ $i ] || 0 === $i && '..' === $pp[ $i ] ) {
array_splice( $pp, $i, 1 );
} elseif ( '..' === $pp[ $i ] ) {
array_splice( $pp, --$i, 2 );
} else {
++$i;
}
}
$ret .= implode( '/', $pp );
$ret .= isset( $parts['query'] ) ? '?' . $parts['query'] : '';
$ret .= isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
return $ret;
}
// endregion .
// ////////////////////
// region Webpack-built script registration
/**
* Register a Webpack-built script.
*
* Our Webpack-built scripts tend to need a bunch of boilerplate:
* - A call to `Assets::get_file_url_for_environment()` for possible debugging.
* - A call to `wp_register_style()` for extracted CSS, possibly with detection of RTL.
* - Loading of dependencies and version provided by `@wordpress/dependency-extraction-webpack-plugin`.
* - Avoiding WPCom's broken minifier.
*
* This wrapper handles all of that.
*
* @since 1.12.0
* @since 2.1.0 Add a new `strategy` option to leverage WP >= 6.3 script strategy feature. The `async` option is deprecated.
* @param string $handle Name of the script. Should be unique across both scripts and styles.
* @param string $path Minimized script path.
* @param string $relative_to File that `$path` is relative to. Pass `__FILE__`.
* @param array $options Additional options:
* - `asset_path`: (string|null) `.asset.php` to load. Default is to base it on `$path`.
* - `async`: (bool) Set true to register the script as deferred, like `Assets::enqueue_async_script()`. Deprecated in favor of `strategy`.
* - `css_dependencies`: (string[]) Additional style dependencies to queue.
* - `css_path`: (string|null) `.css` to load. Default is to base it on `$path`.
* - `dependencies`: (string[]) Additional script dependencies to queue.
* - `enqueue`: (bool) Set true to enqueue the script immediately.
* - `in_footer`: (bool) Set true to register script for the footer.
* - `media`: (string) Media for the css file. Default 'all'.
* - `minify`: (bool|null) Set true to pass `minify=true` in the query string, or `null` to suppress the normal `minify=false`.
* - `nonmin_path`: (string) Non-minified script path.
* - `strategy`: (string) Specify a script strategy to use, eg. `defer` or `async`. Default is `""`.
* - `textdomain`: (string) Text domain for the script. Required if the script depends on wp-i18n.
* - `version`: (string) Override the version from the `asset_path` file.
* @phan-param array{asset_path?:?string,async?:bool,css_dependencies?:string[],css_path?:?string,dependencies?:string[],enqueue?:bool,in_footer?:bool,media?:string,minify?:?bool,nonmin_path?:string,strategy?:string,textdomain?:string,version?:string} $options
* @throws \InvalidArgumentException If arguments are invalid.
*/
public static function register_script( $handle, $path, $relative_to, array $options = array() ) {
if ( substr( $path, -3 ) !== '.js' ) {
throw new \InvalidArgumentException( '$path must end in ".js"' );
}
if ( isset( $options['async'] ) ) {
_deprecated_argument( __METHOD__, '2.1.0', 'The `async` option is deprecated in favor of `strategy`' );
}
$dir = dirname( $relative_to );
$base = substr( $path, 0, -3 );
$options += array(
'asset_path' => "$base.asset.php",
'async' => false,
'css_dependencies' => array(),
'css_path' => "$base.css",
'dependencies' => array(),
'enqueue' => false,
'in_footer' => false,
'media' => 'all',
'minify' => false,
'strategy' => '',
'textdomain' => null,
);
'@phan-var array{asset_path:?string,async:bool,css_dependencies:string[],css_path:?string,dependencies:string[],enqueue:bool,in_footer:bool,media:string,minify:?bool,nonmin_path?:string,strategy:string,textdomain:string,version?:string} $options'; // Phan gets confused by the array addition.
if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && substr( $options['css_path'], -4 ) !== '.css' ) {
throw new \InvalidArgumentException( '$options[\'css_path\'] must end in ".css"' );
}
if ( isset( $options['nonmin_path'] ) ) {
$url = self::get_file_url_for_environment( $path, $options['nonmin_path'], $relative_to );
} else {
$url = plugins_url( $path, $relative_to );
}
$url = self::normalize_path( $url );
if ( null !== $options['minify'] ) {
$url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url );
}
if ( $options['asset_path'] && file_exists( "$dir/{$options['asset_path']}" ) ) {
$asset = require "$dir/{$options['asset_path']}"; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.NotAbsolutePath
$options['dependencies'] = array_merge( $asset['dependencies'], $options['dependencies'] );
$options['css_dependencies'] = array_merge(
array_filter(
$asset['dependencies'],
function ( $d ) {
return wp_style_is( $d, 'registered' );
}
),
$options['css_dependencies']
);
$ver = $options['version'] ?? $asset['version'];
} else {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$ver = $options['version'] ?? @filemtime( "$dir/$path" );
}
if ( $options['async'] && '' === $options['strategy'] ) { // Handle the deprecated `async` option
$options['strategy'] = 'defer';
}
wp_register_script(
$handle,
$url,
$options['dependencies'],
$ver,
array(
'in_footer' => $options['in_footer'],
'strategy' => $options['strategy'],
)
);
if ( $options['textdomain'] ) {
// phpcs:ignore Jetpack.Functions.I18n.DomainNotLiteral
wp_set_script_translations( $handle, $options['textdomain'] );
} elseif ( in_array( 'wp-i18n', $options['dependencies'], true ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s is the script handle. */
esc_html( sprintf( __( 'Script "%s" depends on wp-i18n but does not specify "textdomain"', 'jetpack-assets' ), $handle ) ),
''
);
}
if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && file_exists( "$dir/{$options['css_path']}" ) ) {
$csspath = $options['css_path'];
if ( is_rtl() ) {
$rtlcsspath = substr( $csspath, 0, -4 ) . '.rtl.css';
if ( file_exists( "$dir/$rtlcsspath" ) ) {
$csspath = $rtlcsspath;
}
}
$url = self::normalize_path( plugins_url( $csspath, $relative_to ) );
if ( null !== $options['minify'] ) {
$url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url );
}
wp_register_style( $handle, $url, $options['css_dependencies'], $ver, $options['media'] );
wp_script_add_data( $handle, 'Jetpack::Assets::hascss', true );
} else {
wp_script_add_data( $handle, 'Jetpack::Assets::hascss', false );
}
if ( $options['enqueue'] ) {
self::enqueue_script( $handle );
}
}
/**
* Enqueue a script registered with `Assets::register_script`.
*
* @since 1.12.0
* @param string $handle Name of the script. Should be unique across both scripts and styles.
*/
public static function enqueue_script( $handle ) {
wp_enqueue_script( $handle );
if ( wp_scripts()->get_data( $handle, 'Jetpack::Assets::hascss' ) ) {
wp_enqueue_style( $handle );
}
}
/**
* 'wp_default_scripts' action handler.
*
* This registers the `wp-jp-i18n-loader` script for use by Webpack bundles built with
* `@automattic/i18n-loader-webpack-plugin`.
*
* @since 1.14.0
* @param \WP_Scripts $wp_scripts WP_Scripts instance.
*/
public static function wp_default_scripts_hook( $wp_scripts ) {
$data = array(
'baseUrl' => false,
'locale' => determine_locale(),
'domainMap' => array(),
'domainPaths' => array(),
);
$lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
$content_dir = Jetpack_Constants::get_constant( 'WP_CONTENT_DIR' );
$abspath = Jetpack_Constants::get_constant( 'ABSPATH' );
// Note: str_starts_with() is not used here, as wp-includes/compat.php may not be loaded at this point.
if ( strpos( $lang_dir, $content_dir ) === 0 ) {
$data['baseUrl'] = content_url( substr( trailingslashit( $lang_dir ), strlen( trailingslashit( $content_dir ) ) ) );
} elseif ( strpos( $lang_dir, $abspath ) === 0 ) {
$data['baseUrl'] = site_url( substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) );
}
foreach ( self::$domain_map as $from => list( $to, $type, , $path ) ) {
$data['domainMap'][ $from ] = ( 'core' === $type ? '' : "{$type}/" ) . $to;
if ( '' !== $path ) {
$data['domainPaths'][ $from ] = trailingslashit( $path );
}
}
/**
* Filters the i18n state data for use by Webpack bundles built with
* `@automattic/i18n-loader-webpack-plugin`.
*
* @since 1.14.0
* @package assets
* @param array $data The state data to generate. Expected fields are:
* - `baseUrl`: (string|false) The URL to the languages directory. False if no URL could be determined.
* - `locale`: (string) The locale for the page.
* - `domainMap`: (string[]) A mapping from Composer package textdomains to the corresponding
* `plugins/textdomain` or `themes/textdomain` (or core `textdomain`, but that's unlikely).
* - `domainPaths`: (string[]) A mapping from Composer package textdomains to the corresponding package
* paths.
*/
$data = apply_filters( 'jetpack_i18n_state', $data );
// Can't use self::register_script(), this action is called too early.
if ( file_exists( __DIR__ . '/../build/i18n-loader.asset.php' ) ) {
$path = '../build/i18n-loader.js';
$asset = require __DIR__ . '/../build/i18n-loader.asset.php';
} else {
$path = 'js/i18n-loader.js';
$asset = array(
'dependencies' => array( 'wp-i18n' ),
'version' => filemtime( __DIR__ . "/$path" ),
);
}
$url = self::normalize_path( plugins_url( $path, __FILE__ ) );
$url = add_query_arg( 'minify', 'true', $url );
$handle = 'wp-jp-i18n-loader';
$wp_scripts->add( $handle, $url, $asset['dependencies'], $asset['version'] );
// Ensure the script is loaded in the footer and deferred.
$wp_scripts->add_data( $handle, 'group', 1 );
if ( ! is_array( $data ) ||
! isset( $data['baseUrl'] ) || ! ( is_string( $data['baseUrl'] ) || false === $data['baseUrl'] ) ||
! isset( $data['locale'] ) || ! is_string( $data['locale'] ) ||
! isset( $data['domainMap'] ) || ! is_array( $data['domainMap'] ) ||
! isset( $data['domainPaths'] ) || ! is_array( $data['domainPaths'] )
) {
$wp_scripts->add_inline_script( $handle, 'console.warn( "I18n state deleted by jetpack_i18n_state hook" );' );
} elseif ( ! $data['baseUrl'] ) {
$wp_scripts->add_inline_script( $handle, 'console.warn( "Failed to determine languages base URL. Is WP_LANG_DIR in the WordPress root?" );' );
} else {
$data['domainMap'] = (object) $data['domainMap']; // Ensure it becomes a json object.
$data['domainPaths'] = (object) $data['domainPaths']; // Ensure it becomes a json object.
$wp_scripts->add_inline_script( $handle, 'wp.jpI18nLoader.state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';' );
}
// Deprecated state module: Depend on wp-i18n to ensure global `wp` exists and because anything needing this will need that too.
$wp_scripts->add( 'wp-jp-i18n-state', false, array( 'wp-deprecated', $handle ) );
$wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.deprecated( "wp-jp-i18n-state", { alternative: "wp-jp-i18n-loader" } );' );
$wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.jpI18nState = wp.jpI18nLoader.state;' );
}
// endregion .
// ////////////////////
// region Textdomain aliasing
/**
* Register a textdomain alias.
*
* Composer packages included in plugins will likely not use the textdomain of the plugin, while
* WordPress's i18n infrastructure will include the translations in the plugin's domain. This
* allows for mapping the package's domain to the plugin's.
*
* Since multiple plugins may use the same package, we include the package's version here so
* as to choose the most recent translations (which are most likely to match the package
* selected by jetpack-autoloader).
*
* @since 1.15.0
* @param string $from Domain to alias.
* @param string $to Domain to alias it to.
* @param string $totype What is the target of the alias: 'plugins', 'themes', or 'core'.
* @param string $ver Version of the `$from` domain.
* @param string $path Path to prepend when lazy-loading from JavaScript.
* @throws InvalidArgumentException If arguments are invalid.
*/
public static function alias_textdomain( $from, $to, $totype, $ver, $path = '' ) {
if ( ! in_array( $totype, array( 'plugins', 'themes', 'core' ), true ) ) {
throw new InvalidArgumentException( 'Type must be "plugins", "themes", or "core"' );
}
if (
did_action( 'wp_default_scripts' ) &&
// Don't complain during plugin activation.
! defined( 'WP_SANDBOX_SCRAPING' )
) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: wp_default_scripts. 2: Name of the domain being aliased. */
esc_html__( 'Textdomain aliases should be registered before the %1$s hook. This notice was triggered by the %2$s domain.', 'jetpack-assets' ),
'<code>wp_default_scripts</code>',
'<code>' . esc_html( $from ) . '</code>'
),
''
);
}
if ( empty( self::$domain_map[ $from ] ) ) {
self::init_domain_map_hooks( $from, array() === self::$domain_map );
self::$domain_map[ $from ] = array( $to, $totype, $ver, $path );
} elseif ( Semver::compare( $ver, self::$domain_map[ $from ][2] ) > 0 ) {
self::$domain_map[ $from ] = array( $to, $totype, $ver, $path );
}
}
/**
* Register textdomain aliases from a mapping file.
*
* The mapping file is simply a PHP file that returns an array
* with the following properties:
* - 'domain': String, `$to`
* - 'type': String, `$totype`
* - 'packages': Array, mapping `$from` to `array( 'path' => $path, 'ver' => $ver )` (or to the string `$ver` for back compat).
*
* @since 1.15.0
* @param string $file Mapping file.
*/
public static function alias_textdomains_from_file( $file ) {
$data = require $file;
foreach ( $data['packages'] as $from => $fromdata ) {
if ( ! is_array( $fromdata ) ) {
$fromdata = array(
'path' => '',
'ver' => $fromdata,
);
}
self::alias_textdomain( $from, $data['domain'], $data['type'], $fromdata['ver'], $fromdata['path'] );
}
}
/**
* Register the hooks for textdomain aliasing.
*
* @param string $domain Domain to alias.
* @param bool $firstcall If this is the first call.
*/
private static function init_domain_map_hooks( $domain, $firstcall ) {
// If WordPress's plugin API is available already, use it. If not,
// drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`.
if ( function_exists( 'add_filter' ) ) {
$add_filter = 'add_filter';
} else {
$add_filter = function ( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
global $wp_filter;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter[ $hook_name ][ $priority ][] = array(
'accepted_args' => $accepted_args,
'function' => $callback,
);
};
}
$add_filter( "gettext_{$domain}", array( self::class, 'filter_gettext' ), 10, 3 );
$add_filter( "ngettext_{$domain}", array( self::class, 'filter_ngettext' ), 10, 5 );
$add_filter( "gettext_with_context_{$domain}", array( self::class, 'filter_gettext_with_context' ), 10, 4 );
$add_filter( "ngettext_with_context_{$domain}", array( self::class, 'filter_ngettext_with_context' ), 10, 6 );
if ( $firstcall ) {
$add_filter( 'load_script_translation_file', array( self::class, 'filter_load_script_translation_file' ), 10, 3 );
}
}
/**
* Filter for `gettext`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $text Text to translate.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_gettext( $translation, $text, $domain ) {
if ( $translation === $text ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$newtext = __( $text, self::$domain_map[ $domain ][0] );
if ( $newtext !== $text ) {
return $newtext;
}
}
return $translation;
}
/**
* Filter for `ngettext`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $single The text to be used if the number is singular.
* @param string $plural The text to be used if the number is plural.
* @param int $number The number to compare against to use either the singular or plural form.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_ngettext( $translation, $single, $plural, $number, $domain ) {
if ( $translation === $single || $translation === $plural ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _n( $single, $plural, $number, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `gettext_with_context`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $text Text to translate.
* @param string $context Context information for the translators.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_gettext_with_context( $translation, $text, $context, $domain ) {
if ( $translation === $text ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _x( $text, $context, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `ngettext_with_context`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $single The text to be used if the number is singular.
* @param string $plural The text to be used if the number is plural.
* @param int $number The number to compare against to use either the singular or plural form.
* @param string $context Context information for the translators.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_ngettext_with_context( $translation, $single, $plural, $number, $context, $domain ) {
if ( $translation === $single || $translation === $plural ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _nx( $single, $plural, $number, $context, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `load_script_translation_file`.
*
* @since 1.15.0
* @param string|false $file Path to the translation file to load. False if there isn't one.
* @param string $handle Name of the script to register a translation domain to.
* @param string $domain The text domain.
*/
public static function filter_load_script_translation_file( $file, $handle, $domain ) {
if ( false !== $file && isset( self::$domain_map[ $domain ] ) && ! is_readable( $file ) ) {
// Determine the part of the filename after the domain.
$suffix = basename( $file );
$l = strlen( $domain );
if ( substr( $suffix, 0, $l ) !== $domain || '-' !== $suffix[ $l ] ) {
return $file;
}
$suffix = substr( $suffix, $l );
$lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
// Look for replacement files.
list( $newdomain, $type ) = self::$domain_map[ $domain ];
$newfile = $lang_dir . ( 'core' === $type ? '/' : "/{$type}/" ) . $newdomain . $suffix;
if ( is_readable( $newfile ) ) {
return $newfile;
}
}
return $file;
}
// endregion .
}
// Enable section folding in vim:
// vim: foldmarker=//\ region,//\ endregion foldmethod=marker
// .
@@ -0,0 +1,220 @@
<?php
/**
* Jetpack script data.
*
* @package automattic/jetpack-assets
*/
namespace Automattic\Jetpack\Assets;
use Automattic\Jetpack\Assets;
/**
* Class script data
*/
class Script_Data {
const SCRIPT_HANDLE = 'jetpack-script-data';
/**
* Whether the script data has been rendered.
*
* @var bool
*/
private static $did_render_script_data = false;
/**
* Configure.
*/
public static function configure() {
/**
* Ensure that assets are registered on wp_loaded,
* which is fired before *_enqueue_scripts actions.
* It means that when the dependent scripts are registered,
* the scripts here are already registered.
*/
add_action( 'wp_loaded', array( self::class, 'register_assets' ) );
/**
* Notes:
* 1. wp_print_scripts action is fired on both admin and public pages.
* On admin pages, it's fired before admin_enqueue_scripts action,
* which can be a problem if the consumer package uses admin_enqueue_scripts
* to hook into the script data. Thus, we prefer to use admin_print_scripts on admin pages.
* 2. We want to render the script data on print, instead of init or enqueue actions,
* so that the hook callbacks have enough time and information
* to decide whether to update the script data or not.
*/
$hook = is_admin() ? 'admin_print_scripts' : 'wp_print_scripts';
add_action( $hook, array( self::class, 'render_script_data' ), 1 );
add_action( 'enqueue_block_editor_assets', array( self::class, 'render_script_data' ), 1 );
}
/**
* Register assets.
*
* @access private
*/
public static function register_assets() {
Assets::register_script(
self::SCRIPT_HANDLE,
'../build/jetpack-script-data.js',
__FILE__,
array(
'in_footer' => true,
'textdomain' => 'jetpack-assets',
)
);
}
/**
* Render the script data using an inline script.
*
* @access private
*
* @return void
*/
public static function render_script_data() {
// In case of 'enqueue_block_editor_assets' action, this can be called multiple times.
if ( self::$did_render_script_data ) {
return;
}
self::$did_render_script_data = true;
$script_data = is_admin() ? self::get_admin_script_data() : self::get_public_script_data();
$script_data = wp_json_encode(
$script_data,
JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE
);
wp_add_inline_script(
self::SCRIPT_HANDLE,
sprintf( 'window.JetpackScriptData = %s;', $script_data ),
'before'
);
}
/**
* Get the admin script data.
*
* @return array
*/
protected static function get_admin_script_data() {
global $wp_version;
$data = array(
'site' => array(
'admin_url' => esc_url_raw( admin_url() ),
'date_format' => get_option( 'date_format' ),
'icon' => self::get_site_icon(),
'is_multisite' => is_multisite(),
'plan' => array(
// The properties here should be updated by the consumer package/plugin.
// It includes properties like 'product_slug', 'features', etc.
'product_slug' => '',
),
'rest_nonce' => wp_create_nonce( 'wp_rest' ),
'rest_root' => esc_url_raw( rest_url() ),
'title' => self::get_site_title(),
'wp_version' => $wp_version,
'wpcom' => array(
// This should contain the connected site details like blog_id, is_atomic etc.
'blog_id' => 0,
),
),
'user' => array(
'current_user' => self::get_current_user_data(),
),
);
/**
* Filter the admin script data.
*
* When using this filter, ensure that the data is added only if it is used by some script.
* This filter may be called on almost every admin page load. So, one should check if the data is needed/used on that page.
* For example, the social (publicize) data is used only on Social admin page, Jetpack settings page and the post editor.
* So, the social data should be added only on those pages.
*
* @since 2.3.0
*
* @param array $data The script data.
*/
return apply_filters( 'jetpack_admin_js_script_data', $data );
}
/**
* Get the admin script data.
*
* @return array
*/
protected static function get_public_script_data() {
$data = array(
'site' => array(
'icon' => self::get_site_icon(),
'title' => self::get_site_title(),
),
);
/**
* Filter the public script data.
*
* See the docs for `jetpack_admin_js_script_data` filter for more information.
*
* @since 2.3.0
*
* @param array $data The script data.
*/
return apply_filters( 'jetpack_public_js_script_data', $data );
}
/**
* Get the site title.
*
* @return string
*/
protected static function get_site_title() {
$title = get_bloginfo( 'name' );
return $title ? $title : esc_url_raw( ( get_site_url() ) );
}
/**
* Get the site icon.
*
* @return string
*/
protected static function get_site_icon() {
if ( ! has_site_icon() ) {
return '';
}
/**
* Filters the site icon using Photon.
*
* @see https://developer.wordpress.com/docs/photon/
*
* @param string $url The URL of the site icon.
* @param array|string $args An array of arguments, e.g. array( 'w' => '300', 'resize' => array( 123, 456 ) ), or in string form (w=123&h=456).
*/
return apply_filters( 'jetpack_photon_url', get_site_icon_url(), array( 'w' => 64 ) );
}
/**
* Get the current user data.
*
* @return array
*/
protected static function get_current_user_data() {
$current_user = wp_get_current_user();
return array(
'display_name' => $current_user->display_name,
'id' => $current_user->ID,
);
}
}
@@ -0,0 +1,121 @@
<?php
/**
* Simple semver version handling.
*
* We use this instead of something like `composer/semver` to avoid
* plugins needing to include yet-another dependency package. The
* amount of code we need here is pretty small.
*
* We use this instead of PHP's `version_compare()` because that doesn't
* handle prerelease versions in the way anyone other than PHP devs would
* expect, and silently breaks on various unexpected input.
*
* @package automattic/jetpack-assets
*/
namespace Automattic\Jetpack\Assets;
use InvalidArgumentException;
/**
* Simple semver version handling.
*/
class Semver {
/**
* Parse a semver version.
*
* @param string $version Version.
* @return array With components:
* - major: (int) Major version.
* - minor: (int) Minor version.
* - patch: (int) Patch version.
* - version: (string) Major.minor.patch.
* - prerelease: (string|null) Pre-release string.
* - buildinfo: (string|null) Build metadata string.
* @throws InvalidArgumentException If the version number is not in a recognized format.
*/
public static function parse( $version ) {
// This is slightly looser than the official version from semver.org, in that leading zeros are allowed.
if ( ! preg_match( '/^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<prerelease>(?:[0-9a-zA-Z-]+)(?:\.(?:[0-9a-zA-Z-]+))*))?(?:\+(?P<buildinfo>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/', $version, $m ) ) {
throw new InvalidArgumentException( "Version number \"$version\" is not in a recognized format." );
}
$info = array(
'major' => (int) $m['major'],
'minor' => (int) $m['minor'],
'patch' => (int) $m['patch'],
'version' => sprintf( '%d.%d.%d', $m['major'], $m['minor'], $m['patch'] ),
'prerelease' => isset( $m['prerelease'] ) && '' !== $m['prerelease'] ? $m['prerelease'] : null,
'buildinfo' => isset( $m['buildinfo'] ) && '' !== $m['buildinfo'] ? $m['buildinfo'] : null,
);
if ( null !== $info['prerelease'] ) {
$sep = '';
$prerelease = '';
foreach ( explode( '.', $info['prerelease'] ) as $part ) {
if ( ctype_digit( $part ) ) {
$part = (int) $part;
}
$prerelease .= $sep . $part;
$sep = '.';
}
$info['prerelease'] = $prerelease;
}
return $info;
}
/**
* Compare two version numbers.
*
* @param string $a First version.
* @param string $b Second version.
* @return int Less than, equal to, or greater than 0 depending on whether `$a` is less than, equal to, or greater than `$b`.
* @throws InvalidArgumentException If the version numbers are not in a recognized format.
*/
public static function compare( $a, $b ) {
$aa = self::parse( $a );
$bb = self::parse( $b );
if ( $aa['major'] !== $bb['major'] ) {
return $aa['major'] - $bb['major'];
}
if ( $aa['minor'] !== $bb['minor'] ) {
return $aa['minor'] - $bb['minor'];
}
if ( $aa['patch'] !== $bb['patch'] ) {
return $aa['patch'] - $bb['patch'];
}
if ( null === $aa['prerelease'] ) {
return null === $bb['prerelease'] ? 0 : 1;
}
if ( null === $bb['prerelease'] ) {
return -1;
}
$aaa = explode( '.', $aa['prerelease'] );
$bbb = explode( '.', $bb['prerelease'] );
$al = count( $aaa );
$bl = count( $bbb );
for ( $i = 0; $i < $al && $i < $bl; $i++ ) {
$a = $aaa[ $i ];
$b = $bbb[ $i ];
if ( ctype_digit( $a ) ) {
if ( ctype_digit( $b ) ) {
if ( (int) $a !== (int) $b ) {
return (int) $a - (int) $b;
}
} else {
return -1;
}
} elseif ( ctype_digit( $b ) ) {
return 1;
} else {
$tmp = strcmp( $a, $b );
if ( 0 !== $tmp ) {
return $tmp;
}
}
}
return $al - $bl;
}
}
@@ -0,0 +1,76 @@
const i18n = require( '@wordpress/i18n' );
const { default: md5 } = require( 'md5-es' );
const locationMap = {
plugin: 'plugins/',
theme: 'themes/',
core: '',
};
const hasOwn = ( obj, prop ) => Object.prototype.hasOwnProperty.call( obj, prop );
module.exports = {
state: {
baseUrl: null,
locale: null,
domainMap: {},
domainPaths: {},
},
/**
* Download and register translations for a bundle.
*
* @param {string} path - Bundle path being fetched. May have a query part.
* @param {string} domain - Text domain to register into.
* @param {string} location - Location for the translation: 'plugin', 'theme', or 'core'.
* @return {Promise} Resolved when the translations are registered, or rejected with an `Error`.
*/
async downloadI18n( path, domain, location ) {
const state = this.state;
if ( ! state || typeof state.baseUrl !== 'string' ) {
throw new Error( 'wp.jpI18nLoader.state is not set' );
}
// "en_US" is the default, no translations are needed.
if ( state.locale === 'en_US' ) {
return;
}
// Check that fetch is available.
if ( typeof fetch === 'undefined' ) {
throw new Error( 'Fetch API is not available.' );
}
// Extract any query part and hash the script name like WordPress does.
const pathPrefix = hasOwn( state.domainPaths, domain ) ? state.domainPaths[ domain ] : '';
let hash, query;
const i = path.indexOf( '?' );
if ( i >= 0 ) {
hash = md5.hash( pathPrefix + path.substring( 0, i ) );
query = path.substring( i );
} else {
hash = md5.hash( pathPrefix + path );
query = '';
}
// Download.
const locationAndDomain = hasOwn( state.domainMap, domain )
? state.domainMap[ domain ]
: locationMap[ location ] + domain;
const res = await fetch(
// prettier-ignore
`${ state.baseUrl }${ locationAndDomain }-${ state.locale }-${ hash }.json${ query }`
);
if ( ! res.ok ) {
throw new Error( `HTTP request failed: ${ res.status } ${ res.statusText }` );
}
const data = await res.json();
// Extract the messages from the file and register them.
const localeData = hasOwn( data.locale_data, domain )
? data.locale_data[ domain ]
: data.locale_data.messages;
localeData[ '' ].domain = domain;
i18n.setLocaleData( localeData, domain );
},
};
@@ -0,0 +1 @@
export * from '@automattic/jetpack-script-data';
@@ -0,0 +1,74 @@
# Changelog
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).
## [0.3.2] - 2025-02-03
### Changed
- Internal updates.
## [0.3.1] - 2024-11-25
### Changed
- Updated dependencies. [#40286]
## [0.3.0] - 2024-11-14
### Changed
- Backup: added next daily backup schedule time on admin page [#39914]
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## [0.2.8] - 2024-11-04
### Added
- Enable test coverage. [#39961]
## [0.2.7] - 2024-08-26
### Changed
- Updated package dependencies. [#39004]
## [0.2.6] - 2024-04-08
### Changed
- Internal updates.
## [0.2.5] - 2024-03-25
### Fixed
- Backup: change some error messages to not trigger security scanners [#36496]
## [0.2.4] - 2024-03-18
### Changed
- Internal updates.
## [0.2.3] - 2024-03-14
### Changed
- Internal updates.
## [0.2.2] - 2024-02-27
### Added
- Increasing backup version for new endpoint [#35649]
## [0.2.1] - 2024-02-08
### Fixed
- Write helper script to ABSPATH by default, just like we did before [#35508]
## [0.2.0] - 2024-01-04
### Fixed
- Backup: Add namespace versioning to Helper_Script_Manager and other classes. [#34739]
## 0.1.0 - 2023-12-13
### Fixed
- Initial release (improved helper script installer logging). [#34297]
[0.3.2]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.8...v0.3.0
[0.2.8]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.7...v0.2.8
[0.2.7]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.6...v0.2.7
[0.2.6]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.5...v0.2.6
[0.2.5]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.4...v0.2.5
[0.2.4]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.3...v0.2.4
[0.2.3]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.2...v0.2.3
[0.2.2]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/Automattic/jetpack-backup-helper-script-manager/compare/v0.1.0...v0.2.0
@@ -0,0 +1,357 @@
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,47 @@
# Security Policy
Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/).
## Supported Versions
Generally, only the latest version of Jetpack and its associated plugins have continued support. If a critical vulnerability is found in the current version of a plugin, we may opt to backport any patches to previous versions.
## Reporting a Vulnerability
Our HackerOne program covers the below plugin software, as well as a variety of related projects and infrastructure:
* [Jetpack](https://jetpack.com/)
* Jetpack Backup
* Jetpack Boost
* Jetpack CRM
* Jetpack Protect
* Jetpack Search
* Jetpack Social
* Jetpack VideoPress
**For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.**
Our most critical targets are:
* Jetpack and the Jetpack composer packages (all within this repo)
* Jetpack.com -- the primary marketing site.
* cloud.jetpack.com -- a management site.
* wordpress.com -- the shared management site for both Jetpack and WordPress.com sites.
For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic).
_Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._
## Guidelines
We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production:
* Please **setup a local environment** instead whenever possible. Most of our code is open source (see above).
* If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC.
* **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels.
* To be eligible for a bounty, all of these guidelines must be followed.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties.
@@ -0,0 +1,620 @@
<?php
/**
* The Jetpack Backup Helper Script Manager class (implementation).
*
* @package automattic/jetpack-backup
*/
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
use Exception;
use WP_Error;
use function content_url;
use function get_site_url;
use function is_wp_error;
use function set_url_scheme;
use function trailingslashit;
use function wp_generate_password;
use function wp_http_validate_url;
use function wp_schedule_single_event;
use function wp_upload_dir;
use const ABSPATH;
use const WP_CONTENT_DIR;
/**
* Manage installation, deletion and cleanup of Helper Scripts to assist with backing up Jetpack Sites.
*
* Does *not* use WP_Filesystem, because if there are permissions issues between the webserver's user and the FTP/SSH
* user, then we'll just install the helper script and do a backup/restore using FTP/SSH credentials (that we collect
* ourselves), without using WP_Filesystem in any way.
*
* Also, if we can't write that helper script somewhere (due to writes being inaccessible to the webserver's user, or
* for other reasons), we want to know about it (in the form of an error response), instead of having that helper
* script silently uploaded via FTP/SFTP, so that we could fall back to a backup/restore using credentials.
*
* Lastly, PHP provides us with better error reporting than WP_Filesystem.
*/
class Helper_Script_Manager_Impl {
/**
* Name of a directory that will be created for storing the helper script.
*/
const TEMP_DIRECTORY = 'jetpack-temp';
/**
* How long until the helper script will "expire" and refuse taking requests, in seconds.
*/
const EXPIRY_TIME = 60 * 60 * 8;
/**
* Maximum size of the helper script, in bytes.
*/
const MAX_FILESIZE = 1024 * 1024;
/**
* Associative array of possible places to install a jetpack-temp directory, along with the URL to access each.
*
* Keys specify the full path of install locations, and values point to the equivalent URL.
*
* If null, then install locations will be determined dynamically at the point of an install.
*
* @var array|null
*/
protected $custom_install_locations;
/**
* Filenames to ignore in scandir()'s return value.
*
* @var string[]
*/
protected $scandir_ignored_names = array( '.', '..' );
/**
* Header that the helper script is expected to start with.
*/
const HELPER_HEADER = "<?php /* Jetpack Backup Helper Script */\n";
/**
* Lines that will be written to README in the helper directory.
*/
const README_LINES = array(
'These files have been put on your server by Jetpack to assist with backups, restores, and scans of your ' .
'site content. They are cleaned up automatically when we no longer need them.',
'If you no longer have Jetpack connected to your site, you can delete them manually.',
'If you have questions or need assistance, please contact Jetpack Support at https://jetpack.com/support/',
'If you like to build amazing things with WordPress, you should visit automattic.com/jobs and apply to join ' .
'the fun mention this file when you apply!',
);
/**
* Data that will be written to index.php in the helper directory.
*/
const INDEX_FILE = '<?php // Silence is golden';
/**
* Create Helper Script Manager.
*
* @param array|null $custom_install_locations Associative array of possible places to install a jetpack-temp
* directory, along with the URL to access each.
*/
public function __construct( $custom_install_locations = null ) {
$this->custom_install_locations = $custom_install_locations;
}
/**
* Get either the default install locations, or the ones configured in the constructor.
*
* Has to be done late, i.e. can't be done in constructor, because in __construct() not all constants / functions
* might be available.
*
* @return array<string, string|WP_Error> Array with keys specifying the full path of install locations, and values
* either pointing to the equivalent URL, or being WP_Error if a specific path is not accessible.
*/
public function install_locations() {
if ( $this->custom_install_locations !== null ) {
return $this->custom_install_locations;
}
$abspath_url = get_site_url();
$locations = array();
// Prioritize ABSPATH first, because even though ABSPATH constant's value might be weird sometimes, it's the
// path where the PHP scripts will be most likely be able to get executed.
try {
if ( Throw_On_Errors::t_is_dir( ABSPATH ) ) {
$abspath_dir = Throw_On_Errors::t_realpath( ABSPATH );
$locations[ $abspath_dir ] = $abspath_url;
}
} catch ( Exception $exception ) {
$locations[ ABSPATH ] = new WP_Error(
'abspath_missing',
'Unable to access WordPress root "' . ABSPATH . '": ' . $exception->getMessage(),
array( 'status' => 500 )
);
}
try {
if ( Throw_On_Errors::t_is_dir( WP_CONTENT_DIR ) ) {
$wp_content_dir = Throw_On_Errors::t_realpath( WP_CONTENT_DIR );
// Using content_url() instead of WP_CONTENT_URL as it tests for whether we're using SSL.
$wp_content_url = content_url();
// I think we mess up the order in which we load things somewhere in a test, so "wp-content" and
// "wp-content/uploads/" URLs don't actually have the scheme+host part in them.
if ( ! wp_http_validate_url( $wp_content_url ) ) {
$wp_content_url = $abspath_url . $wp_content_url;
}
$locations[ $wp_content_dir ] = $wp_content_url;
}
} catch ( Exception $exception ) {
$locations[ WP_CONTENT_DIR ] = new WP_Error(
'content_path_missing',
'Unable to access content path "' . WP_CONTENT_DIR . '"' . $exception->getMessage(),
array( 'status' => 500 )
);
}
$upload_dir_info = wp_upload_dir();
$wp_uploads_dir = $upload_dir_info['basedir'];
try {
if ( Throw_On_Errors::t_is_dir( $wp_uploads_dir ) ) {
$wp_uploads_dir = Throw_On_Errors::t_realpath( $wp_uploads_dir );
$wp_uploads_url = $upload_dir_info['baseurl'];
// wp_upload_dir() doesn't check for whether we're using SSL:
//
// https://core.trac.wordpress.org/ticket/25449
//
// so set the scheme manually.
$wp_uploads_url = set_url_scheme( $wp_uploads_url );
if ( ! wp_http_validate_url( $wp_uploads_url ) ) {
$wp_uploads_url = $abspath_url . $wp_uploads_url;
}
$locations[ $wp_uploads_dir ] = $wp_uploads_url;
}
} catch ( Exception $exception ) {
$locations[ $wp_uploads_dir ] = new WP_Error(
'uploads_path_missing',
'Unable to access uploads path "' . $wp_uploads_dir . '"' . $exception->getMessage(),
array( 'status' => 500 )
);
}
return $locations;
}
/**
* Installs a Helper Script, and returns its filesystem path and access url.
*
* @param string $script_body Helper Script file contents.
*
* @return array|WP_Error Either an array containing the filesystem path ("path"), the URL ("url") of the helper
* script, and the WordPress root ("abspath"), or an instance of WP_Error.
*/
public function install_helper_script( $script_body ) {
// Check that the script body contains the correct header.
$actual_header = static::string_starts_with_substring( $script_body, static::HELPER_HEADER );
if ( true !== $actual_header ) {
return new WP_Error(
'bad_header',
'Bad helper script header: 0x' . bin2hex( $actual_header ),
array( 'status' => 400 )
);
}
// Refuse to install a Helper Script that is too large.
$helper_script_size = strlen( $script_body );
if ( $helper_script_size > static::MAX_FILESIZE ) {
return new WP_Error(
'too_big',
"Helper script is bigger ($helper_script_size bytes) " .
'than the max. size (' . static::MAX_FILESIZE . ' bytes)',
array( 'status' => 413 )
);
}
// Replace '[wp_path]' in the Helper Script with the WordPress installation location. Allows the Helper Script
// to find WordPress.
$wp_path_marker = '[wp_path]';
try {
$normalized_abspath = addslashes( Throw_On_Errors::t_realpath( ABSPATH ) );
} catch ( Exception $exception ) {
return new WP_Error(
'abspath_missing',
'Error while resolving ABSPATH "' . ABSPATH . '": ' . $exception->getMessage(),
array( 'status' => 500 )
);
}
$script_body = str_replace(
$wp_path_marker,
$normalized_abspath,
$script_body,
$wp_path_marker_replacement_count
);
if ( 0 === $wp_path_marker_replacement_count ) {
return new WP_Error(
'no_wp_path_marker',
"Helper script does not have the '$wp_path_marker' marker",
array( 'status' => 400 )
);
}
$failure_paths_and_reasons = array();
foreach ( $this->install_locations() as $directory => $url ) {
if ( is_wp_error( $url ) ) {
$failure_paths_and_reasons[] = "directory '$directory': " . $url->get_error_message();
continue;
}
try {
$installed = $this->install_to_location_or_throw( $script_body, $directory, $url );
// Always schedule a cleanup run shortly after EXPIRY_TIME.
wp_schedule_single_event(
time() + static::EXPIRY_TIME + 60,
'jetpack_backup_cleanup_helper_scripts'
);
return array(
'path' => $installed['path'],
'url' => $installed['url'],
'abspath' => Throw_On_Errors::t_realpath( ABSPATH ),
);
} catch ( Exception $exception ) {
$failure_paths_and_reasons[] = "directory '$directory' (URL '$url'): " . $exception->getMessage();
}
}
return new WP_Error(
'all_locations_failed',
'Unable to write the helper script to any install locations; ' .
'tried: ' . implode( ';', $failure_paths_and_reasons ),
array( 'status' => 500 )
);
}
/**
* Install helper script to a directory, or throw an exception.
*
* @param string $script_body Helper script's body.
* @param string $directory Candidate directory to create "jetpack-temp" in and write the helper script.
* @param string $url Base URL that the files in a directory are expected to be available at.
*
* @return string[] Array with "path" (location to the installed helper script) and "url"
* (URL of the installed helper script) keys.
* @throws Exception On I/O errors.
*/
protected function install_to_location_or_throw( $script_body, $directory, $url ) {
if ( ! Throw_On_Errors::t_is_writable( $directory ) ) {
throw new Exception( "Directory '$directory' is not writable" );
}
$temp_dir = trailingslashit( $directory ) . static::TEMP_DIRECTORY;
if ( ! Throw_On_Errors::t_is_dir( $temp_dir ) ) {
Throw_On_Errors::t_mkdir( $temp_dir );
}
$readme_path = trailingslashit( $temp_dir ) . 'README';
Throw_On_Errors::t_file_put_contents( $readme_path, implode( "\n\n", static::README_LINES ) );
$index_path = trailingslashit( $temp_dir ) . 'index.php';
Throw_On_Errors::t_file_put_contents( $index_path, static::INDEX_FILE );
$file_key = wp_generate_password( 10, false );
$file_name = 'jp-helper-' . $file_key . '.php';
$file_path = trailingslashit( $temp_dir ) . $file_name;
// Very unlikely, but check nonetheless.
if ( Throw_On_Errors::t_file_exists( $file_path ) ) {
throw new Exception( "Helper script at '$file_path' already exists" );
}
Throw_On_Errors::t_file_put_contents( $file_path, $script_body );
return array(
'path' => $file_path,
'url' => trailingslashit( $url ) . trailingslashit( static::TEMP_DIRECTORY ) . $file_name,
);
}
/**
* Ensure that the helper script is gone (by deleting it, if needed).
*
* @param string $path Path to the helper script to delete.
*
* @return true|WP_Error True if the file helper script is gone (either it got deleted, or it was never there), or
* WP_Error instance on deletion failures.
*/
public function delete_helper_script( $path ) {
try {
$this->delete_helper_script_or_throw( $path );
} catch ( Exception $exception ) {
return new WP_Error(
'deletion_failure',
"Unable to delete helper script at '$path': " . $exception->getMessage(),
array( 'status' => 500 )
);
}
return true;
}
/**
* Ensure that the helper script is gone (by deleting it, if needed), throw an exception on errors.
*
* @param string $path Path to the helper script to delete.
*
* @return void
* @throws Exception On deletion failures.
*/
protected function delete_helper_script_or_throw( $path ) {
if ( ! Throw_On_Errors::t_file_exists( $path ) ) {
return;
}
if ( ! Throw_On_Errors::t_is_readable( $path ) ) {
throw new Exception( "File '$path' is not readable" );
}
if ( ! Throw_On_Errors::t_is_writable( $path ) ) {
throw new Exception( "File '$path' is not writable" );
}
$helper_script_size = Throw_On_Errors::t_filesize( $path );
// Check this file looks like a JPR helper script.
$helper_header_size = strlen( static::HELPER_HEADER );
if ( $helper_script_size < $helper_header_size ) {
throw new Exception(
"Helper script is smaller ($helper_script_size bytes) " .
"than the expected header ($helper_header_size bytes)"
);
}
if ( $helper_script_size > static::MAX_FILESIZE ) {
throw new Exception(
"Helper script is bigger ($helper_script_size bytes) " .
'than the max. size (' . static::MAX_FILESIZE . ' bytes)'
);
}
$actual_header = static::verify_file_header( $path, static::HELPER_HEADER );
if ( true !== $actual_header ) {
throw new Exception( 'Bad helper script header: 0x' . bin2hex( $actual_header ) );
}
Throw_On_Errors::t_unlink( $path );
$this->delete_helper_directory_if_empty( dirname( $path ) );
}
/**
* Search for Helper Scripts that are suspiciously old, and clean them out.
*
* @return true|WP_Error True if all expired helper scripts got cleaned up successfully, or an instance of
* WP_Error if one or more expired helper scripts didn't manage to get cleaned up.
*/
public function cleanup_expired_helper_scripts() {
try {
$this->cleanup_helper_scripts( time() - static::EXPIRY_TIME );
} catch ( Exception $exception ) {
return new WP_Error(
'cleanup_failed',
'Unable to clean up expired helper scripts: ' . $exception->getMessage(),
array( 'status' => 500 )
);
}
return true;
}
/**
* Search for and delete all Helper Scripts. Used during uninstallation.
*
* @return true|WP_Error True if all helper scripts got deleted successfully, or an instance of WP_Error if one or
* more helper scripts didn't manage to get deleted.
*/
public function delete_all_helper_scripts() {
try {
$this->cleanup_helper_scripts();
} catch ( Exception $exception ) {
return new WP_Error(
'cleanup_failed',
'Unable to clean up all helper scripts: ' . $exception->getMessage(),
array( 'status' => 500 )
);
}
return true;
}
/**
* Search for and delete Helper Scripts. If an $expiry_time is specified, only delete Helper Scripts
* with a mtime older than $expiry_time. Otherwise, delete them all.
*
* @param int|null $expiry_time If specified, only delete scripts older than this UNIX timestamp.
*
* @return void
* @throws Exception If one or more helper scripts doesn't manage to get cleaned up.
*/
protected function cleanup_helper_scripts( $expiry_time = null ) {
$error_messages = array();
foreach ( $this->install_locations() as $directory => $url ) {
if ( is_wp_error( $url ) ) {
$error_messages[] = $url->get_error_message();
continue;
}
$temp_dir = trailingslashit( trailingslashit( $directory ) . static::TEMP_DIRECTORY );
if ( Throw_On_Errors::t_is_dir( $temp_dir ) ) {
// Find expired helper scripts and delete them.
$temp_dir_contents = Throw_On_Errors::t_scandir( $temp_dir );
foreach ( $temp_dir_contents as $name ) {
if ( in_array( $name, $this->scandir_ignored_names, true ) ) {
continue;
}
$full_path = $temp_dir . $name;
$last_modified = Throw_On_Errors::t_filemtime( $full_path );
if ( preg_match( '/^jp-helper-.*\.php$/', $name ) ) {
if ( null === $expiry_time || $last_modified < $expiry_time ) {
try {
$this->delete_helper_script_or_throw( $full_path );
} catch ( Exception $exception ) {
$error_messages[] = $exception->getMessage();
}
}
}
}
// Delete the directory if it's empty now.
$this->delete_helper_directory_if_empty( $temp_dir );
}
}
if ( count( $error_messages ) > 0 ) {
throw new Exception(
'Unable to clean up one or more helper scripts: ' . implode( ';', $error_messages )
);
}
}
/**
* Delete a helper script directory if it's empty.
*
* @param string $dir Path to the helper script directory.
*
* @return bool True if the directory is missing, or was empty and got deleted; false if directory still contains
* something and wasn't deleted.
* @throws Exception On I/O errors.
*/
protected function delete_helper_directory_if_empty( $dir ) {
if ( ! Throw_On_Errors::t_is_dir( $dir ) ) {
return true;
}
// Check that the only remaining files are a README and index.php generated by this system.
$allowed_files_and_headers = array(
'README' => static::README_LINES[0],
'index.php' => static::INDEX_FILE,
);
$dir_contents = Throw_On_Errors::t_scandir( $dir );
if ( count( $dir_contents ) > count( $allowed_files_and_headers ) + count( $this->scandir_ignored_names ) ) {
return false;
}
foreach ( $dir_contents as $name ) {
if ( in_array( $name, $this->scandir_ignored_names, true ) ) {
continue;
}
$full_path = trailingslashit( $dir ) . $name;
if ( ! isset( $allowed_files_and_headers[ $name ] ) ) {
return false;
}
// Verify the file starts with the expected contents.
$actual_header = static::verify_file_header( $full_path, $allowed_files_and_headers[ $name ] );
if ( true !== $actual_header ) {
throw new Exception( "Bad header for file '$full_path': 0x" . bin2hex( $actual_header ) );
}
Throw_On_Errors::t_unlink( $full_path );
}
// If the directory is now empty, delete it.
$dir_contents_after_cleanup = Throw_On_Errors::t_scandir( $dir );
if ( count( $dir_contents_after_cleanup ) <= count( $this->scandir_ignored_names ) ) {
Throw_On_Errors::t_rmdir( $dir );
}
return true;
}
/**
* Test if string starts with a substring, and if it doesn't, return the actual prefix.
*
* @param string $string String to search in.
* @param string $expected_prefix Expected prefix.
*
* @return bool|string True if string starts with a substring, or the actual prefix that was found instead of the
* expected prefix.
*/
protected static function string_starts_with_substring( $string, $expected_prefix ) {
$actual_prefix = substr( $string, 0, strlen( $expected_prefix ) );
if ( $actual_prefix !== $expected_prefix ) {
return $actual_prefix;
}
return true;
}
/**
* Verify that a file exists, is readable, and has the expected header.
*
* @param string $path File to verify.
* @param string $expected_header Header that the file should have.
*
* @return bool|string True if header matches, or an actual header if it doesn't match.
* @throws Exception If the file doesn't exist, isn't readable, or is of the wrong size.
*/
protected static function verify_file_header( $path, $expected_header ) {
if ( ! Throw_On_Errors::t_file_exists( $path ) ) {
throw new Exception( "File '$path' does not exist" );
}
if ( ! Throw_On_Errors::t_is_readable( $path ) ) {
throw new Exception( "File '$path' is not readable" );
}
$file_size = Throw_On_Errors::t_filesize( $path );
// Check this file looks like a JPR helper script.
$expected_header_size = strlen( $expected_header );
if ( $file_size < $expected_header_size ) {
throw new Exception(
"File is smaller ($file_size bytes) " .
"than the expected header ($expected_header_size bytes)"
);
}
if ( $file_size > static::MAX_FILESIZE ) {
throw new Exception(
"File is bigger ($file_size bytes) " .
'than the max. size (' . static::MAX_FILESIZE . ' bytes)'
);
}
$file_contents = Throw_On_Errors::t_file_get_contents( $path );
return static::string_starts_with_substring( $file_contents, $expected_header );
}
}
@@ -0,0 +1,89 @@
<?php
/**
* Jetpack Backup Helper Script Manager class (static wrapper).
*
* @package automattic/jetpack-backup
*/
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
/**
* Manage installation, deletion and cleanup of Helper Scripts to assist with backing up Jetpack Sites.
*
* A static wrapper around an "implementation" class so that it gets autoloaded late, and we always get the latest
* version of the class instead of a random version of it:
*
* https://github.com/Automattic/jetpack/pull/34297#discussion_r1424227489
*/
class Helper_Script_Manager {
/**
* Instance of helper script manager implementation, or null if not initialized yet.
*
* @var Helper_Script_Manager_Impl|null
*/
protected static $impl = null;
/**
* Initialize an instance of helper script manager implementation (if needed).
*
* @return void
*/
protected static function initialize_impl_if_needed() {
if ( null === static::$impl ) {
static::$impl = new Helper_Script_Manager_Impl();
}
}
/**
* Install a Helper Script, and returns its filesystem path and access url.
*
* @param string $script_body Helper Script file contents.
*
* @return array|\WP_Error Either an array containing the filesystem path ("path"), the URL ("url") of the helper
* script, and the WordPress root ("abspath"), or an instance of WP_Error.
*/
public static function install_helper_script( $script_body ) {
static::initialize_impl_if_needed();
return static::$impl->install_helper_script( $script_body );
}
/**
* Ensure that the helper script is gone (by deleting it, if needed).
*
* @param string $path Path to the helper script to delete.
*
* @return true|\WP_Error True if the file helper script is gone (either it got deleted, or it was never there), or
* WP_Error instance on deletion failures.
*/
public static function delete_helper_script( $path ) {
static::initialize_impl_if_needed();
return static::$impl->delete_helper_script( $path );
}
/**
* Search for Helper Scripts that are suspiciously old, and clean them out.
*
* @return true|\WP_Error True if all expired helper scripts got cleaned up successfully, or an instance of
* WP_Error if one or more expired helper scripts didn't manage to get cleaned up.
*/
public static function cleanup_expired_helper_scripts() {
static::initialize_impl_if_needed();
return static::$impl->cleanup_expired_helper_scripts();
}
/**
* Search for and delete all Helper Scripts. Used during uninstallation.
*
* @return true|\WP_Error True if all helper scripts got deleted successfully, or an instance of WP_Error if one or
* more helper scripts didn't manage to get deleted.
*/
public static function delete_all_helper_scripts() {
static::initialize_impl_if_needed();
return static::$impl->delete_all_helper_scripts();
}
}
@@ -0,0 +1,498 @@
<?php // phpcs:disable Squiz.Commenting.FileComment.Missing
// phpcs:disable WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting
// phpcs:disable WordPress.PHP.IniSet.display_errors_Disallowed
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
use Exception;
use Throwable;
/**
* Wrappers for functions which throw an exception on errors and warnings instead of silently continuing operation.
*
* PHP is pretty lax with error reporting when doing I/O operations, e.g. a typical I/O helper function returns false,
* null, and/or emits a warning, but the whole PHP application continues operation. It's for the caller of the I/O
* helper function to test the returned result of the function, and notice + act upon I/O errors.
*
* We really want to know about each and every I/O error. Therefore, this class provides wrappers for some common
* (mostly) I/O operations that throw an exception on errors instead of just returning false, null, or something else.
* This wrapper class treats warnings as errors too.
*
* Given that static method names are similar to the ones used by PHP, they're prefixed with "t_" to not erroneously
* trigger various security scanners.
*/
class Throw_On_Errors {
/**
* Execute a callable, throw an exception (together with a descriptive label) on PHP warnings / errors.
*
* @param callable $callable Callable to execute.
* @param string $label Label to add to the thrown exception to clarify what was attempted.
*
* @return mixed Callable's return value, if any.
* @throws Exception On warnings thrown by the callable.
* @noinspection PhpUnusedParameterInspection
*/
private static function throw_on_warnings( $callable, $label ) {
$old_error_reporting = error_reporting( - 1 );
$old_display_errors = ini_set( 'display_errors', 'stderr' );
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
set_error_handler(
/**
* Temporary error handler.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.set-error-handler
* @see https://www.php.net/manual/en/function.set-error-handler.php
*
* @param int $errno Level of the error raised.
* @param string $errstr Error message.
* @param string|null $errfile Filename that the error was raised in.
* @param int|null $errline Line number where the error was raised.
* @param array|null $errcontext Deprecated, unused.
*
* @return never
* @throws Exception
*/
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
function ( $errno, $errstr, $errfile = null, $errline = null, $errcontext = null ) {
throw new Exception( "$errstr (file: $errfile; line: $errline)" );
}
);
$result = null;
$error_message = null;
try {
$result = $callable();
} catch ( Throwable $throwable ) {
$error_message = $throwable->getMessage();
}
restore_error_handler();
ini_set( 'display_errors', $old_display_errors );
error_reporting( $old_error_reporting );
if ( $error_message !== null ) {
throw new Exception( "$label failed: $error_message" );
}
return $result;
}
/**
* Return canonicalized absolute pathname, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.realpath
* @see https://www.php.net/manual/en/function.realpath.php
*
* @param string $path Path being checked.
*
* @return string Canonicalized absolute pathname
* @throws Exception On invalid parameters, or if realpath() has returned false or thrown warnings.
*/
public static function t_realpath( $path ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $path ) {
throw new Exception( 'Filename for realpath() is unset' );
}
$label = "realpath( '$path' )";
$realpath_result = static::throw_on_warnings(
function () use ( $path ) {
return realpath( $path );
},
$label
);
if ( false === $realpath_result ) {
throw new Exception( "Unable to $label" );
}
return $realpath_result;
}
/**
* Check whether a file or directory exists, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-exists
* @see https://www.php.net/manual/en/function.file-exists.php
*
* @param string $filename Path to the file or directory.
*
* @return bool True if the file or directory specified by filename exists; false otherwise.
* @throws Exception On invalid parameters, or if file_exists() has thrown warnings.
*/
public static function t_file_exists( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for file_exists() is unset' );
}
return static::throw_on_warnings(
function () use ( $filename ) {
return file_exists( $filename );
},
"file_exists( '$filename' )"
);
}
/**
* Tell whether the filename (or a directory) is readable, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-readable
* @see https://www.php.net/manual/en/function.is-readable.php
*
* @param string $filename Filename (or directory) to check.
*
* @return bool True if the filename (or a directory) exists and is readable, false otherwise.
* @throws Exception On invalid parameters, or if is_readable() has thrown warnings.
*/
public static function t_is_readable( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for is_readable() is unset' );
}
return static::throw_on_warnings(
function () use ( $filename ) {
return is_readable( $filename );
},
"is_readable( '$filename' )"
);
}
/**
* Tell whether the filename (or a directory) is writable, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-writable
* @see https://www.php.net/manual/en/function.is-writable.php
*
* @param string $filename Filename (or directory) to check.
*
* @return bool True if the filename (or a directory) exists and is writable, false otherwise.
* @throws Exception On invalid parameters, or if is_writable() has thrown warnings.
*/
public static function t_is_writable( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for is_writable() is unset' );
}
return static::throw_on_warnings(
function () use ( $filename ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
return is_writable( $filename );
},
"is_writable( '$filename' )"
);
}
/**
* Get file size, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.filesize
* @see https://www.php.net/manual/en/function.filesize.php
*
* @param string $filename Path to the file.
*
* @return int Size of the file in bytes
* @throws Exception On invalid parameters, or if filesize() has thrown warnings.
*/
public static function t_filesize( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for filesize() is unset' );
}
$label = "filesize( '$filename' )";
$filesize_result = static::throw_on_warnings(
function () use ( $filename ) {
return filesize( $filename );
},
$label
);
if ( false === $filesize_result ) {
throw new Exception( "Unable to $label" );
}
return $filesize_result;
}
/**
* Get file modification time, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.filemtime
* @see https://www.php.net/manual/en/function.filemtime.php
*
* @param string $filename Path to the file.
*
* @return int The time the file was last modified
* @throws Exception On invalid parameters, or if filemtime() has thrown warnings.
*/
public static function t_filemtime( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for filemtime() is unset' );
}
$label = "filemtime( '$filename' )";
$filemtime_result = static::throw_on_warnings(
function () use ( $filename ) {
return filemtime( $filename );
},
$label
);
if ( false === $filemtime_result ) {
throw new Exception( "Unable to $label" );
}
return $filemtime_result;
}
/**
* Tell whether the filename is a directory (follow symlinks), throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.is-dir
* @see https://www.php.net/manual/en/function.is-dir.php
*
* @param string $filename Path to the file.
*
* @return bool True if the filename (or the symlink's target) exists and is a directory, false otherwise.
* @throws Exception On invalid parameters, if is_dir() has thrown warnings, or has failed.
*/
public static function t_is_dir( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for is_dir() is unset' );
}
return static::throw_on_warnings(
function () use ( $filename ) {
return is_dir( $filename );
},
"is_dir( '$filename' )"
);
}
/**
* Make a directory, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.mkdir
* @see https://www.php.net/manual/en/function.mkdir.php
*
* @param string $directory Directory path.
* @param int $permissions Permissions of the newly created directory.
* @param bool $recursive If true, then any parent directories to the directory specified will also be created,
* with the same permissions.
*
* @return void
* @throws Exception On invalid parameters, if mkdir() has thrown warnings, or has failed.
*/
public static function t_mkdir( $directory, $permissions = 0777, $recursive = false ) {
// PHP 5.x won't complain about permissions being null, so let's do it ourselves.
if ( $permissions === null ) {
throw new Exception( 'Permissions for mkdir() are unset' );
}
$label = "mkdir( '$directory', 0" . decoct( $permissions ) . ', ' . ( $recursive ? 'true' : 'false' ) . ' )';
$mkdir_result = static::throw_on_warnings(
function () use ( $directory, $permissions, $recursive ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
return mkdir( $directory, $permissions, $recursive );
},
$label
);
if ( false === $mkdir_result ) {
throw new Exception( "Unable to $label" );
}
}
/**
* List files and directories inside the specified path, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.scandir
* @see https://www.php.net/manual/en/function.scandir.php
*
* @param string $directory Directory that will be scanned.
*
* @return string An array of filenames.
* @throws Exception If scandir() has thrown warnings, or has failed.
*/
public static function t_scandir( $directory ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $directory ) {
throw new Exception( 'Directory for scandir() is unset' );
}
$label = "scandir( '$directory' )";
$scandir_result = static::throw_on_warnings(
function () use ( $directory ) {
return scandir( $directory );
},
$label
);
if ( false === $scandir_result ) {
throw new Exception( "Unable to $label" );
}
return $scandir_result;
}
/**
* Remove a directory, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.rmdir
* @see https://www.php.net/manual/en/function.rmdir.php
*
* @param string $directory Directory path.
*
* @return void
* @throws Exception On invalid parameters, if rmdir() has thrown warnings, or has failed.
*/
public static function t_rmdir( $directory ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $directory ) {
throw new Exception( 'Directory for mkdir() is unset' );
}
$label = "rmdir( '$directory' )";
$rmdir_result = static::throw_on_warnings(
function () use ( $directory ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
return rmdir( $directory );
},
$label
);
if ( false === $rmdir_result ) {
throw new Exception( "Unable to $label" );
}
}
/**
* Delete a file, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.unlink
* @see https://www.php.net/manual/en/function.unlink.php
*
* @param string $filename Path to the file.
*
* @return void
* @throws Exception If unlink() has thrown warnings, or has failed.
*/
public static function t_unlink( $filename ) {
$label = "unlink( '$filename' )";
$unlink_result = static::throw_on_warnings(
function () use ( $filename ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
return unlink( $filename );
},
$label
);
if ( false === $unlink_result ) {
throw new Exception( "Unable to $label" );
}
}
/**
* Write data to a file, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-put-contents
* @see https://www.php.net/manual/en/function.file-put-contents.php
*
* @param string $filename Path to the file where to write the data.
* @param string $data The data to write.
*
* @return void
* @throws Exception If file_put_contents() has thrown warnings, has failed, or if it didn't write all the bytes.
*/
public static function t_file_put_contents( $filename, $data ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for f_p_c() is unset' );
}
if ( $data === null ) {
throw new Exception( 'Data to write is null' );
}
$data_length = strlen( $data );
// Weird label is intentional, otherwise security scanners find this label suspicious.
$label = "f_p_c( '$filename', $data_length bytes of data )";
$number_of_bytes_written = static::throw_on_warnings(
function () use ( $filename, $data ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
return file_put_contents( $filename, $data );
},
$label
);
if ( false === $number_of_bytes_written ) {
throw new Exception( "Unable to $label" );
}
if ( $number_of_bytes_written !== $data_length ) {
throw new Exception(
"$label was expected to write $data_length bytes, but wrote $number_of_bytes_written bytes"
);
}
}
/**
* Read entire file into a string, throw on warnings / errors.
*
* @see https://php-legacy-docs.zend.com/manual/php5/en/function.file-get-contents
* @see https://www.php.net/manual/en/function.file-get-contents.php
*
* @param string $filename Name of the file to read.
*
* @return string The read data.
* @throws Exception If file_get_contents() has thrown warnings, or has failed.
*/
public static function t_file_get_contents( $filename ) {
// PHP 5.x won't complain about parameter being unset, so let's do it ourselves.
if ( ! $filename ) {
throw new Exception( 'Filename for f_g_c() is unset' );
}
// Weird label is intentional, otherwise security scanners find this label suspicious.
$label = "f_g_c( '$filename' )";
$fgc_result = static::throw_on_warnings(
function () use ( $filename ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
return file_get_contents( $filename );
},
$label
);
if ( false === $fgc_result ) {
throw new Exception( "Unable to $label" );
}
return $fgc_result;
}
}
@@ -0,0 +1,934 @@
# Changelog
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.0.8] - 2025-02-03
### Changed
- Updated package dependencies. [#41286]
## [4.0.7] - 2025-01-20
### Changed
- Code: Use function-style exit() and die() with a default status code of 0. [#41167]
- Updated package dependencies. [#41099]
## [4.0.6] - 2025-01-06
### Changed
- Updated package dependencies. [#40798] [#40812]
## [4.0.5] - 2024-12-16
### Changed
- Updated package dependencies. [#40564]
## [4.0.4] - 2024-12-09
### Changed
- Updated package dependencies. [#40363] [#40372]
## [4.0.3] - 2024-11-26
### Changed
- Update dependencies. [#39855]
## [4.0.2] - 2024-11-25
### Changed
- Updated dependencies. [#40286]
- Updated package dependencies. [#40288]
## [4.0.1] - 2024-11-18
### Changed
- Update dependencies. [#39855]
## [4.0.0] - 2024-11-14
### Added
- Backup: added next daily backup schedule time on admin page [#39914]
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## [3.4.17] - 2024-11-11
### Changed
- Updated package dependencies. [#39999] [#40060]
## [3.4.16] - 2024-11-04
### Added
- Enable test coverage. [#39961]
## [3.4.15] - 2024-10-21
### Changed
- Update dependencies. [#39781]
## [3.4.14] - 2024-10-14
### Changed
- Only include `wp-polyfill` as a script dependency when needed. [#39629]
- Updated package dependencies. [#39707]
## [3.4.13] - 2024-10-07
### Changed
- Updated package dependencies. [#39594]
## [3.4.12] - 2024-09-30
### Changed
- Update dependencies. [#39528]
## [3.4.11] - 2024-09-23
### Changed
- Update dependencies. [#38958]
## [3.4.10] - 2024-09-16
### Changed
- Updated package dependencies. [#39332]
### Fixed
- Remove unnecessary leading space from i18n message. [#39305]
- Stop considering discarded backups as successful backups on the admin page [#39385]
## [3.4.9] - 2024-09-10
### Changed
- Updated package dependencies. [#39302]
## [3.4.8] - 2024-09-09
### Changed
- Updated package dependencies. [#39278]
## [3.4.7] - 2024-09-05
### Changed
- Updated package dependencies. [#39176]
## [3.4.6] - 2024-09-02
### Changed
- Admin menu: change order of Jetpack sub-menu items [#39095]
- Updated package dependencies. [#39111]
## [3.4.5] - 2024-08-26
### Changed
- Updated package dependencies. [#39004]
## [3.4.4] - 2024-08-21
### Fixed
- Decoupled backup connection screens from useConnection hook to avoid unnecessary loading and prevent duplicated API calls. [#38948]
- Revert recent SVG image optimizations. [#38981]
## [3.4.3] - 2024-08-19
### Changed
- Update dependencies. [#38861] [#38662]
### Fixed
- Lossless image optimization for images (should improve performance with no visible changes). [#38750]
## [3.4.2] - 2024-07-22
### Changed
- Update dependencies. [#38402]
## [3.4.1] - 2024-07-08
### Changed
- Updated package dependencies. [#38132]
## [3.4.0] - 2024-06-27
### Added
- Add on-demand backups feature in the backup package [#37998]
## [3.3.17] - 2024-06-24
### Changed
- Update backup header cards layout for responsive design [#37944]
## [3.3.16] - 2024-06-17
### Changed
- Updated package dependencies. [#37796]
## [3.3.15] - 2024-06-10
### Changed
- Updated package dependencies. [#37669]
## [3.3.14] - 2024-06-03
### Changed
- Remove the 'jetpack-identity-crisis' dependency. [#36968]
## [3.3.13] - 2024-05-27
### Changed
- Update dependencies. [#37323]
## [3.3.12] - 2024-05-20
### Changed
- Updated package dependencies. [#37379] [#37380] [#37382]
## [3.3.11] - 2024-05-09
### Changed
- Update dependencies. [#37280]
## [3.3.10] - 2024-05-06
### Added
- Add missing package dependencies. [#37141]
### Changed
- Updated package dependencies. [#37147]
## [3.3.9] - 2024-04-25
### Changed
- Internal updates.
## [3.3.8] - 2024-04-22
### Changed
- Internal updates.
## [3.3.7] - 2024-04-15
### Changed
- Internal updates.
## [3.3.6] - 2024-04-08
### Changed
- Updated package dependencies. [#36760]
## [3.3.5] - 2024-04-01
### Changed
- Update dependencies. [#36655]
## [3.3.4] - 2024-03-27
### Changed
- Updated package dependencies. [#36585]
## [3.3.3] - 2024-03-25
### Fixed
- Backup: change some error messages to not trigger security scanners [#36496]
## [3.3.2] - 2024-03-18
### Changed
- Internal updates.
## [3.3.1] - 2024-03-12
### Changed
- Update dependencies. [#36243]
- Updated package dependencies. [#36325]
## [3.3.0] - 2024-03-04
### Added
- Add endpoint to query backup preflight checks [#36032]
### Changed
- Updated package dependencies.
### Fixed
- Real time backups: Fix getting more than one row in the related orders table [#36096]
## [3.2.0] - 2024-02-27
### Added
- Real time backups: Add endpoints orders to be used in real-time backups jetpack [#35649]
## [3.1.5] - 2024-02-13
### Changed
- Updated package dependencies. [#35608]
## [3.1.4] - 2024-02-08
### Fixed
- Write helper script to ABSPATH by default, just like we did before [#35508]
## [3.1.3] - 2024-02-07
### Changed
- Update dependencies. [#35520]
## [3.1.2] - 2024-02-05
### Changed
- Updated package dependencies.
## [3.1.1] - 2024-01-29
### Changed
- Update dependencies.
## [3.1.0] - 2024-01-22
### Changed
- Use blog ID instead site slug for checkout and WPCOM links. [#35020]
## [3.0.0] - 2024-01-04
### Fixed
- Backup: Add namespace versioning to Helper_Script_Manager and other classes [#34739]
## [2.0.5] - 2024-01-04
### Changed
- Updated package dependencies. [#34815] [#34816]
## [2.0.4] - 2023-12-20
### Changed
- Updated package dependencies. [#34694]
## [2.0.3] - 2023-12-13
### Fixed
- Backup: Bug fixes in helper script installation class. [#34297]
## [2.0.2] - 2023-12-11
### Changed
- Updated package dependencies. [#34416]
## [2.0.1] - 2023-12-03
### Changed
- Updated package dependencies. [#34411] [#34427]
## [2.0.0] - 2023-11-20
### Changed
- Updated required PHP version to >= 7.0. [#34192]
## [1.17.12] - 2023-11-14
### Changed
- Updated package dependencies. [#34093]
## [1.17.11] - 2023-11-13
### Changed
- Updated dependencies.
## [1.17.10] - 2023-11-03
## [1.17.9] - 2023-10-23
### Changed
- Replace Calypso progress bar with one from VideoPress. [#33054]
- Updated package dependencies. [#33646] [#33687]
## [1.17.8] - 2023-10-16
### Changed
- Updated package dependencies. [#33429]
## [1.17.7] - 2023-10-10
### Changed
- Updated package dependencies. [#33428]
## [1.17.6] - 2023-09-19
### Changed
- Updated Jetpack submenu sort order so individual features are alpha-sorted. [#32958]
- Updated package dependencies. [#33001]
## [1.17.5] - 2023-09-11
### Changed
- General: remove WP 6.1 backwards compatibility checks [#32772]
## [1.17.4] - 2023-09-04
### Changed
- Updated package dependencies. [#32803] [#32804]
## [1.17.3] - 2023-08-28
### Added
- Backup Admin: add backup file browser reference in the backup admin page [#32463]
## [1.17.2] - 2023-08-23
### Changed
- Updated package dependencies. [#32605]
## [1.17.1] - 2023-08-21
### Changed
- Use the new method to render Connection initial state. [#32499]
## [1.17.0] - 2023-08-14
### Added
- Add backup undo feature. [#32442]
## [1.16.6] - 2023-08-09
### Changed
- Updated package dependencies. [#32166]
## [1.16.5] - 2023-08-01
### Changed
- Minor internal updates.
## [1.16.4] - 2023-07-25
### Changed
- Updated package dependencies. [#32040]
## [1.16.3] - 2023-07-17
### Changed
- Updated package dependencies. [#31785]
## [1.16.2] - 2023-07-05
### Changed
- Updated package dependencies. [#31659]
## [1.16.1] - 2023-06-26
### Changed
- Updated package dependencies. [#31468]
## [1.16.0] - 2023-06-15
### Added
- Add testimonial component and use it on the backup connect screen [#31221]
## [1.15.0] - 2023-06-12
### Added
- Add "Why I need VaultPress Backup" section to connect page [#31285]
- Add video section to backup connect page [#31260]
## [1.14.0] - 2023-06-06
### Changed
- Update connection module to have an RNA option that updates the design [#31201]
- Updated package dependencies. [#31129]
## [1.13.0] - 2023-05-29
### Added
- Add connection screen for secondary admins [#30862]
- Add loading placeholder in backup dashboard while fetching capabilities and backup state. [#30972]
### Changed
- Add a loading placeholder while fetching backup plan when a secondary admin (not connected) is accessing the backup page. [#30963]
### Fixed
- Fix "Over storage space" message for sites with plans that have no storage limit [#30885]
## [1.12.17] - 2023-05-22
### Added
- Added backup storage help popover with forecast info [#30731]
## [1.12.16] - 2023-05-02
### Changed
- Updated package dependencies.
## [1.12.15] - 2023-05-01
### Changed
- Internal updates.
## [1.12.14] - 2023-04-25
- Minor internal updates.
## [1.12.13] - 2023-04-17
### Changed
- Updated package dependencies. [#30019]
## [1.12.12] - 2023-04-10
### Added
- Add Jetpack Autoloader package suggestion. [#29988]
## [1.12.11] - 2023-04-04
### Changed
- Updated package dependencies. [#29854]
## [1.12.10] - 2023-04-03
### Changed
- Internal updates.
## [1.12.9] - 2023-03-28
### Changed
- Minor internal updates.
## [1.12.8] - 2023-03-27
### Added
- (Backup, Boost, Search, Social) Add links on upgrade pages to activate a license key, if you already have one. [#29443]
### Fixed
- Backup: validate if storage details has loaded before attempting to fetch it again. [#29645]
## [1.12.7] - 2023-03-20
### Changed
- Updated package dependencies. [#29471]
## [1.12.6] - 2023-03-08
### Changed
- Switch to use tracking check from connection package [#29187]
- Updated package dependencies. [#29216]
## [1.12.5] - 2023-02-28
### Changed
- Update billing language [#29126]
- Update days of saved backups link to use external link instead of plain link. [#29137]
## [1.12.4] - 2023-02-20
### Changed
- Minor internal updates.
## [1.12.3] - 2023-02-15
### Changed
- Update to React 18. [#28710]
## [1.12.2] - 2023-02-08
### Added
- Add filter to redirect users who have a license to license activation page. [#28509]
### Changed
- Updated package dependencies. [#28682]
## [1.12.1] - 2023-01-30
### Added
- Add track event when user clicks on upgrade storage CTA [#28647]
## [1.12.0] - 2023-01-30
### Added
- Move usage storage level to a global state [#28603]
### Changed
- Backup storage details improvement [#28581]
## [1.11.0] - 2023-01-26
### Added
- Add backup storage UI on backup plugin [#28085]
## [1.10.8] - 2023-01-23
### Fixed
- Clean up JavaScript eslint issues. [#28441]
- Fixes the price display for products with intro offers for the first month. [#28424]
## [1.10.7] - 2023-01-11
### Added
- Setup js tests and add some tests to existing reducers, selectors and hooks [#28130]
### Changed
- Updated package dependencies. [#28127]
- Use `WP_Filesystem` more consistently in `Helper_Script_Manager`. [#28198]
## [1.10.6] - 2022-12-19
### Changed
- Update Backup logo [#27802]
### Fixed
- Update for PHP 8.2 deprecations. [#27949]
## [1.10.5] - 2022-12-06
### Changed
- Updated backup layout to improve consistency and remove redundancy. [#27222]
- Updated package dependencies. [#27340, #27688, #27696, #27697]
## [1.10.4] - 2022-11-28
### Changed
- Rename Jetpack Backup to Jetpack VaultPress Backup [#27432]
- Updated package dependencies. [#26069]
## [1.10.3] - 2022-11-14
### Changed
- Updated package dependencies. [#26930]
## [1.10.2] - 2022-11-07
### Changed
- Updated package dependencies.
### Fixed
- Updated how backup determines if the site has a plan. [#26943]
## [1.10.1] - 2022-11-01
### Changed
- Updated package dependencies. [#27196]
## [1.10.0] - 2022-10-25
### Changed
- Backup: add a new event to track when a customer dismisses a review request. [#26980]
- Updated package dependencies. [#26705]
### Fixed
- Stopped continuous state loading after good backup. [#27014]
## [1.9.2] - 2022-10-19
### Changed
- Updated package dependencies. [#26808]
## [1.9.1] - 2022-10-17
### Changed
- Updated package dependencies. [#26826, #26851]
## [1.9.0] - 2022-10-13
### Added
- Integrate the new connection error message React component into the Backup plugin. [#26545]
### Changed
- Updated package dependencies. [#26790]
## [1.8.4] - 2022-10-11
### Changed
- Updated package dependencies. [#26640, #26683]
## [1.8.3] - 2022-10-05
### Changed
- Updated package dependencies. [#26457]
## [1.8.2] - 2022-09-27
### Changed
- Updated package dependencies.
### Removed
- Removed dependency connection-ui [#26381]
### Fixed
- Do not show header footer on connection screen [#26421]
- Replace antippatern where components are returned from non-functionl components called renderSomething [#26411]
## [1.8.1] - 2022-09-20
### Changed
- Updated package dependencies.
### Fixed
- Allow other non owner admin to see Backup dashboard [#26105]
## [1.8.0] - 2022-09-08
### Added
- Add support for JITMs to Backup plugin [#25945]
### Changed
- Modify review request logic [#25979]
- Updated package dependencies.
### Fixed
- Backup: Fixed Automattic link in admin footer [#26075]
## [1.7.3] - 2022-08-30
### Changed
- Updated package dependencies. [#25694, #25814]
## [1.7.2] - 2022-08-23
### Changed
- Updated package dependencies. [#25338, #25339, #25377, #25628, #25665, #25762, #25764]
## [1.7.1] - 2022-08-09
### Changed
- Updated package dependencies. [#24477, #25265]
## [1.7.0] - 2022-08-03
### Changed
- Removed calls to deprecated components of the soft disconnect system as it is no longer in use. [#25315]
- Updated package dependencies. [#25300, #25315]
## [1.6.0] - 2022-07-26
### Added
- Add plugin review request [#24929]
### Changed
- Updated package dependencies. [#25140]
## [1.5.0] - 2022-07-19
### Changed
- Added page-view and link tracking analytics. [#24998]
- Updated package dependencies. [#25086]
## [1.4.3] - 2022-07-12
### Changed
- Make dashboard text more clear about realtime backups. [#24955]
## [1.4.2] - 2022-07-06
### Changed
- Updated package dependencies. [#24923]
## [1.4.1] - 2022-06-28
### Changed
- Updated package dependencies. [#24827]
## [1.4.0] - 2022-06-21
### Added
- Added UI to support backup warning state [#24680]
### Changed
- Renaming master to trunk. [#24661]
- Updated package dependencies. [#24679]
## [1.3.9] - 2022-06-14
### Changed
- Updated package dependencies. [#24529]
### Removed
- Removed extra headline from connection screen. [#24696]
## [1.3.8] - 2022-06-08
### Changed
- Reorder JS imports for `import/order` eslint rule. [#24601]
- Updated package dependencies. [#24510]
## [1.3.7] - 2022-05-31
### Changed
- Updated package dependencies. [#24432] [#24573] [#24475] [#24505] [#24515]
## [1.3.6] - 2022-05-24
### Changed
- Updated package dependencies. [#24396] [#24449] [#24453] [#24468]
## [1.3.5] - 2022-05-20
### Changed
- Improve the build process to ensure availability of built assets. [#24442]
## [1.3.4] - 2022-05-19
### Changed
- Updated package dependencies. [#24419]
## [1.3.3] - 2022-05-18
### Changed
- Changed method used to disconnect upon deactivation [#24300]
- Updated package dependencies. [#23795] [#24372] [#24153] [#24334] [#24347] [#24344]
### Fixed
- Fix new PHPCS sniffs. [#24366]
## [1.3.2] - 2022-05-10
### Changed
- Updated package dependencies. [#24167]
## [1.3.1] - 2022-05-04
### Changed
- Remove use of `pnpx` in preparation for pnpm 7.0. [#24210]
- Updated package dependencies. [#24095] [#24198]
## [1.3.0] - 2022-04-26
### Changed
- Backup plugin UI now lives in the Backup package
## [1.2.6] - 2022-04-19
### Changed
- Updated package dependencies.
## [1.2.5] - 2022-03-02
### Changed
- Updated package dependencies.
## [1.2.4] - 2022-02-22
### Changed
- Updated package dependencies.
## [1.2.3] - 2022-01-25
### Changed
- Dependency Update - Sync from 1.29 to 1.29
## [1.2.2] - 2022-01-18
### Changed
- Updated package dependencies.
## [1.2.1] - 2022-01-13
### Changed
- Updated package dependencies.
## [1.2.0] - 2022-01-04
### Changed
- Switch to pcov for code coverage.
- Updated package dependencies
- Updated package textdomain from `jetpack` to `jetpack-backup-pkg`.
## [1.1.11] - 2021-12-14
### Changed
- Updated package dependencies.
## [1.1.10] - 2021-11-30
### Changed
- Updated package dependencies.
## [1.1.9] - 2021-11-23
### Changed
- Updated package dependencies.
## [1.1.8] - 2021-11-02
### Changed
- Set `convertDeprecationsToExceptions` true in PHPUnit config.
- Update PHPUnit configs to include just what needs coverage rather than include everything then try to exclude stuff that doesn't.
## [1.1.7] - 2021-10-26
### Changed
- Updated package dependencies.
## [1.1.6] - 2021-10-13
### Changed
- Updated package dependencies.
## [1.1.5] - 2021-10-12
### Changed
- Updated package dependencies
## [1.1.4] - 2021-09-28
### Fixed
- Register WP hooks even if WP isn't loaded yet.
## [1.1.3] - 2021-08-31
### Changed
- Bump changelogger version
- Tests: update PHPUnit polyfills dependency (yoast/phpunit-polyfills).
- Updated package dependencies.
## [1.1.2] - 2021-08-12
### Added
- Add package version tracking.
## [1.1.1] - 2021-07-27
### Added
- Add a package version constant.
### Changed
- Updated package dependencies.
## [1.1.0] - 2021-06-29
### Added
- Add backup-helper-script endpoints under the jetpack/v4 namespace.
- Add backup real time endpoints.
## [1.0.6] - 2021-05-25
### Changed
- Updated package dependencies.
## [1.0.5] - 2021-04-27
### Changed
- Updated package dependencies.
## [1.0.4] - 2021-03-30
### Added
- Composer alias for dev-master, to improve dependencies
### Changed
- Update package dependencies.
## [1.0.3] - 2021-01-19
- Add mirror-repo information to all current composer packages
- Monorepo: Reorganize all projects
## [1.0.2] - 2019-11-08
- Packages: Use classmap instead of PSR-4
## 1.0.0 - 2019-10-29
- Add API endpoints and Jetpack Backup package for managing Help…
[4.0.8]: https://github.com/Automattic/jetpack-backup/compare/v4.0.7...v4.0.8
[4.0.7]: https://github.com/Automattic/jetpack-backup/compare/v4.0.6...v4.0.7
[4.0.6]: https://github.com/Automattic/jetpack-backup/compare/v4.0.5...v4.0.6
[4.0.5]: https://github.com/Automattic/jetpack-backup/compare/v4.0.4...v4.0.5
[4.0.4]: https://github.com/Automattic/jetpack-backup/compare/v4.0.3...v4.0.4
[4.0.3]: https://github.com/Automattic/jetpack-backup/compare/v4.0.2...v4.0.3
[4.0.2]: https://github.com/Automattic/jetpack-backup/compare/v4.0.1...v4.0.2
[4.0.1]: https://github.com/Automattic/jetpack-backup/compare/v4.0.0...v4.0.1
[4.0.0]: https://github.com/Automattic/jetpack-backup/compare/v3.4.17...v4.0.0
[3.4.17]: https://github.com/Automattic/jetpack-backup/compare/v3.4.16...v3.4.17
[3.4.16]: https://github.com/Automattic/jetpack-backup/compare/v3.4.15...v3.4.16
[3.4.15]: https://github.com/Automattic/jetpack-backup/compare/v3.4.14...v3.4.15
[3.4.14]: https://github.com/Automattic/jetpack-backup/compare/v3.4.13...v3.4.14
[3.4.13]: https://github.com/Automattic/jetpack-backup/compare/v3.4.12...v3.4.13
[3.4.12]: https://github.com/Automattic/jetpack-backup/compare/v3.4.11...v3.4.12
[3.4.11]: https://github.com/Automattic/jetpack-backup/compare/v3.4.10...v3.4.11
[3.4.10]: https://github.com/Automattic/jetpack-backup/compare/v3.4.9...v3.4.10
[3.4.9]: https://github.com/Automattic/jetpack-backup/compare/v3.4.8...v3.4.9
[3.4.8]: https://github.com/Automattic/jetpack-backup/compare/v3.4.7...v3.4.8
[3.4.7]: https://github.com/Automattic/jetpack-backup/compare/v3.4.6...v3.4.7
[3.4.6]: https://github.com/Automattic/jetpack-backup/compare/v3.4.5...v3.4.6
[3.4.5]: https://github.com/Automattic/jetpack-backup/compare/v3.4.4...v3.4.5
[3.4.4]: https://github.com/Automattic/jetpack-backup/compare/v3.4.3...v3.4.4
[3.4.3]: https://github.com/Automattic/jetpack-backup/compare/v3.4.2...v3.4.3
[3.4.2]: https://github.com/Automattic/jetpack-backup/compare/v3.4.1...v3.4.2
[3.4.1]: https://github.com/Automattic/jetpack-backup/compare/v3.4.0...v3.4.1
[3.4.0]: https://github.com/Automattic/jetpack-backup/compare/v3.3.17...v3.4.0
[3.3.17]: https://github.com/Automattic/jetpack-backup/compare/v3.3.16...v3.3.17
[3.3.16]: https://github.com/Automattic/jetpack-backup/compare/v3.3.15...v3.3.16
[3.3.15]: https://github.com/Automattic/jetpack-backup/compare/v3.3.14...v3.3.15
[3.3.14]: https://github.com/Automattic/jetpack-backup/compare/v3.3.13...v3.3.14
[3.3.13]: https://github.com/Automattic/jetpack-backup/compare/v3.3.12...v3.3.13
[3.3.12]: https://github.com/Automattic/jetpack-backup/compare/v3.3.11...v3.3.12
[3.3.11]: https://github.com/Automattic/jetpack-backup/compare/v3.3.10...v3.3.11
[3.3.10]: https://github.com/Automattic/jetpack-backup/compare/v3.3.9...v3.3.10
[3.3.9]: https://github.com/Automattic/jetpack-backup/compare/v3.3.8...v3.3.9
[3.3.8]: https://github.com/Automattic/jetpack-backup/compare/v3.3.7...v3.3.8
[3.3.7]: https://github.com/Automattic/jetpack-backup/compare/v3.3.6...v3.3.7
[3.3.6]: https://github.com/Automattic/jetpack-backup/compare/v3.3.5...v3.3.6
[3.3.5]: https://github.com/Automattic/jetpack-backup/compare/v3.3.4...v3.3.5
[3.3.4]: https://github.com/Automattic/jetpack-backup/compare/v3.3.3...v3.3.4
[3.3.3]: https://github.com/Automattic/jetpack-backup/compare/v3.3.2...v3.3.3
[3.3.2]: https://github.com/Automattic/jetpack-backup/compare/v3.3.1...v3.3.2
[3.3.1]: https://github.com/Automattic/jetpack-backup/compare/v3.3.0...v3.3.1
[3.3.0]: https://github.com/Automattic/jetpack-backup/compare/v3.2.0...v3.3.0
[3.2.0]: https://github.com/Automattic/jetpack-backup/compare/v3.1.5...v3.2.0
[3.1.5]: https://github.com/Automattic/jetpack-backup/compare/v3.1.4...v3.1.5
[3.1.4]: https://github.com/Automattic/jetpack-backup/compare/v3.1.3...v3.1.4
[3.1.3]: https://github.com/Automattic/jetpack-backup/compare/v3.1.2...v3.1.3
[3.1.2]: https://github.com/Automattic/jetpack-backup/compare/v3.1.1...v3.1.2
[3.1.1]: https://github.com/Automattic/jetpack-backup/compare/v3.1.0...v3.1.1
[3.1.0]: https://github.com/Automattic/jetpack-backup/compare/v3.0.0...v3.1.0
[3.0.0]: https://github.com/Automattic/jetpack-backup/compare/v2.0.5...v3.0.0
[2.0.5]: https://github.com/Automattic/jetpack-backup/compare/v2.0.4...v2.0.5
[2.0.4]: https://github.com/Automattic/jetpack-backup/compare/v2.0.3...v2.0.4
[2.0.3]: https://github.com/Automattic/jetpack-backup/compare/v2.0.2...v2.0.3
[2.0.2]: https://github.com/Automattic/jetpack-backup/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/Automattic/jetpack-backup/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/Automattic/jetpack-backup/compare/v1.17.12...v2.0.0
[1.17.12]: https://github.com/Automattic/jetpack-backup/compare/v1.17.11...v1.17.12
[1.17.11]: https://github.com/Automattic/jetpack-backup/compare/v1.17.10...v1.17.11
[1.17.10]: https://github.com/Automattic/jetpack-backup/compare/v1.17.9...v1.17.10
[1.17.9]: https://github.com/Automattic/jetpack-backup/compare/v1.17.8...v1.17.9
[1.17.8]: https://github.com/Automattic/jetpack-backup/compare/v1.17.7...v1.17.8
[1.17.7]: https://github.com/Automattic/jetpack-backup/compare/v1.17.6...v1.17.7
[1.17.6]: https://github.com/Automattic/jetpack-backup/compare/v1.17.5...v1.17.6
[1.17.5]: https://github.com/Automattic/jetpack-backup/compare/v1.17.4...v1.17.5
[1.17.4]: https://github.com/Automattic/jetpack-backup/compare/v1.17.3...v1.17.4
[1.17.3]: https://github.com/Automattic/jetpack-backup/compare/v1.17.2...v1.17.3
[1.17.2]: https://github.com/Automattic/jetpack-backup/compare/v1.17.1...v1.17.2
[1.17.1]: https://github.com/Automattic/jetpack-backup/compare/v1.17.0...v1.17.1
[1.17.0]: https://github.com/Automattic/jetpack-backup/compare/v1.16.6...v1.17.0
[1.16.6]: https://github.com/Automattic/jetpack-backup/compare/v1.16.5...v1.16.6
[1.16.5]: https://github.com/Automattic/jetpack-backup/compare/v1.16.4...v1.16.5
[1.16.4]: https://github.com/Automattic/jetpack-backup/compare/v1.16.3...v1.16.4
[1.16.3]: https://github.com/Automattic/jetpack-backup/compare/v1.16.2...v1.16.3
[1.16.2]: https://github.com/Automattic/jetpack-backup/compare/v1.16.1...v1.16.2
[1.16.1]: https://github.com/Automattic/jetpack-backup/compare/v1.16.0...v1.16.1
[1.16.0]: https://github.com/Automattic/jetpack-backup/compare/v1.15.0...v1.16.0
[1.15.0]: https://github.com/Automattic/jetpack-backup/compare/v1.14.0...v1.15.0
[1.14.0]: https://github.com/Automattic/jetpack-backup/compare/v1.13.0...v1.14.0
[1.13.0]: https://github.com/Automattic/jetpack-backup/compare/v1.12.17...v1.13.0
[1.12.17]: https://github.com/Automattic/jetpack-backup/compare/v1.12.16...v1.12.17
[1.12.16]: https://github.com/Automattic/jetpack-backup/compare/v1.12.15...v1.12.16
[1.12.15]: https://github.com/Automattic/jetpack-backup/compare/v1.12.14...v1.12.15
[1.12.14]: https://github.com/Automattic/jetpack-backup/compare/v1.12.13...v1.12.14
[1.12.13]: https://github.com/Automattic/jetpack-backup/compare/v1.12.12...v1.12.13
[1.12.12]: https://github.com/Automattic/jetpack-backup/compare/v1.12.11...v1.12.12
[1.12.11]: https://github.com/Automattic/jetpack-backup/compare/v1.12.10...v1.12.11
[1.12.10]: https://github.com/Automattic/jetpack-backup/compare/v1.12.9...v1.12.10
[1.12.9]: https://github.com/Automattic/jetpack-backup/compare/v1.12.8...v1.12.9
[1.12.8]: https://github.com/Automattic/jetpack-backup/compare/v1.12.7...v1.12.8
[1.12.7]: https://github.com/Automattic/jetpack-backup/compare/v1.12.6...v1.12.7
[1.12.6]: https://github.com/Automattic/jetpack-backup/compare/v1.12.5...v1.12.6
[1.12.5]: https://github.com/Automattic/jetpack-backup/compare/v1.12.4...v1.12.5
[1.12.4]: https://github.com/Automattic/jetpack-backup/compare/v1.12.3...v1.12.4
[1.12.3]: https://github.com/Automattic/jetpack-backup/compare/v1.12.2...v1.12.3
[1.12.2]: https://github.com/Automattic/jetpack-backup/compare/v1.12.1...v1.12.2
[1.12.1]: https://github.com/Automattic/jetpack-backup/compare/v1.12.0...v1.12.1
[1.12.0]: https://github.com/Automattic/jetpack-backup/compare/v1.11.0...v1.12.0
[1.11.0]: https://github.com/Automattic/jetpack-backup/compare/v1.10.8...v1.11.0
[1.10.8]: https://github.com/Automattic/jetpack-backup/compare/v1.10.7...v1.10.8
[1.10.7]: https://github.com/Automattic/jetpack-backup/compare/v1.10.6...v1.10.7
[1.10.6]: https://github.com/Automattic/jetpack-backup/compare/v1.10.5...v1.10.6
[1.10.5]: https://github.com/Automattic/jetpack-backup/compare/v1.10.4...v1.10.5
[1.10.4]: https://github.com/Automattic/jetpack-backup/compare/v1.10.3...v1.10.4
[1.10.3]: https://github.com/Automattic/jetpack-backup/compare/v1.10.2...v1.10.3
[1.10.2]: https://github.com/Automattic/jetpack-backup/compare/v1.10.1...v1.10.2
[1.10.1]: https://github.com/Automattic/jetpack-backup/compare/v1.10.0...v1.10.1
[1.10.0]: https://github.com/Automattic/jetpack-backup/compare/v1.9.2...v1.10.0
[1.9.2]: https://github.com/Automattic/jetpack-backup/compare/v1.9.1...v1.9.2
[1.9.1]: https://github.com/Automattic/jetpack-backup/compare/v1.9.0...v1.9.1
[1.9.0]: https://github.com/Automattic/jetpack-backup/compare/v1.8.4...v1.9.0
[1.8.4]: https://github.com/Automattic/jetpack-backup/compare/v1.8.3...v1.8.4
[1.8.3]: https://github.com/Automattic/jetpack-backup/compare/v1.8.2...v1.8.3
[1.8.2]: https://github.com/Automattic/jetpack-backup/compare/v1.8.1...v1.8.2
[1.8.1]: https://github.com/Automattic/jetpack-backup/compare/v1.8.0...v1.8.1
[1.8.0]: https://github.com/Automattic/jetpack-backup/compare/v1.7.3...v1.8.0
[1.7.3]: https://github.com/Automattic/jetpack-backup/compare/v1.7.2...v1.7.3
[1.7.2]: https://github.com/Automattic/jetpack-backup/compare/v1.7.1...v1.7.2
[1.7.1]: https://github.com/Automattic/jetpack-backup/compare/v1.7.0...v1.7.1
[1.7.0]: https://github.com/Automattic/jetpack-backup/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/Automattic/jetpack-backup/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/Automattic/jetpack-backup/compare/v1.4.3...v1.5.0
[1.4.3]: https://github.com/Automattic/jetpack-backup/compare/v1.4.2...v1.4.3
[1.4.2]: https://github.com/Automattic/jetpack-backup/compare/v1.4.1...v1.4.2
[1.4.1]: https://github.com/Automattic/jetpack-backup/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/Automattic/jetpack-backup/compare/v1.3.9...v1.4.0
[1.3.9]: https://github.com/Automattic/jetpack-backup/compare/v1.3.8...v1.3.9
[1.3.8]: https://github.com/Automattic/jetpack-backup/compare/v1.3.7...v1.3.8
[1.3.7]: https://github.com/Automattic/jetpack-backup/compare/v1.3.6...v1.3.7
[1.3.6]: https://github.com/Automattic/jetpack-backup/compare/v1.3.5...v1.3.6
[1.3.5]: https://github.com/Automattic/jetpack-backup/compare/v1.3.4...v1.3.5
[1.3.4]: https://github.com/Automattic/jetpack-backup/compare/v1.3.3...v1.3.4
[1.3.3]: https://github.com/Automattic/jetpack-backup/compare/v1.3.2...v1.3.3
[1.3.2]: https://github.com/Automattic/jetpack-backup/compare/v1.3.1...v1.3.2
[1.3.1]: https://github.com/Automattic/jetpack-backup/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/Automattic/jetpack-backup/compare/v1.2.6...v1.3.0
[1.2.6]: https://github.com/Automattic/jetpack-backup/compare/v1.2.5...v1.2.6
[1.2.5]: https://github.com/Automattic/jetpack-backup/compare/v1.2.4...v1.2.5
[1.2.4]: https://github.com/Automattic/jetpack-backup/compare/v1.2.3...v1.2.4
[1.2.3]: https://github.com/Automattic/jetpack-backup/compare/v1.2.2...v1.2.3
[1.2.2]: https://github.com/Automattic/jetpack-backup/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/Automattic/jetpack-backup/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/Automattic/jetpack-backup/compare/v1.1.11...v1.2.0
[1.1.11]: https://github.com/Automattic/jetpack-backup/compare/v1.1.10...v1.1.11
[1.1.10]: https://github.com/Automattic/jetpack-backup/compare/v1.1.9...v1.1.10
[1.1.9]: https://github.com/Automattic/jetpack-backup/compare/v1.1.8...v1.1.9
[1.1.8]: https://github.com/Automattic/jetpack-backup/compare/v1.1.7...v1.1.8
[1.1.7]: https://github.com/Automattic/jetpack-backup/compare/v1.1.6...v1.1.7
[1.1.6]: https://github.com/Automattic/jetpack-backup/compare/v1.1.5...v1.1.6
[1.1.5]: https://github.com/Automattic/jetpack-backup/compare/v1.1.4...v1.1.5
[1.1.4]: https://github.com/Automattic/jetpack-backup/compare/v1.1.3...v1.1.4
[1.1.3]: https://github.com/Automattic/jetpack-backup/compare/v1.1.2...v1.1.3
[1.1.2]: https://github.com/Automattic/jetpack-backup/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/Automattic/jetpack-backup/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/Automattic/jetpack-backup/compare/v1.0.6...v1.1.0
[1.0.6]: https://github.com/Automattic/jetpack-backup/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/Automattic/jetpack-backup/compare/v1.0.4...v1.0.5
[1.0.4]: https://github.com/Automattic/jetpack-backup/compare/v1.0.3...v1.0.4
[1.0.3]: https://github.com/Automattic/jetpack-backup/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/Automattic/jetpack-backup/compare/v1.0.0...v1.0.2
@@ -0,0 +1,357 @@
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,47 @@
# Security Policy
Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/).
## Supported Versions
Generally, only the latest version of Jetpack and its associated plugins have continued support. If a critical vulnerability is found in the current version of a plugin, we may opt to backport any patches to previous versions.
## Reporting a Vulnerability
Our HackerOne program covers the below plugin software, as well as a variety of related projects and infrastructure:
* [Jetpack](https://jetpack.com/)
* Jetpack Backup
* Jetpack Boost
* Jetpack CRM
* Jetpack Protect
* Jetpack Search
* Jetpack Social
* Jetpack VideoPress
**For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.**
Our most critical targets are:
* Jetpack and the Jetpack composer packages (all within this repo)
* Jetpack.com -- the primary marketing site.
* cloud.jetpack.com -- a management site.
* wordpress.com -- the shared management site for both Jetpack and WordPress.com sites.
For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic).
_Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._
## Guidelines
We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production:
* Please **setup a local environment** instead whenever possible. Most of our code is open source (see above).
* If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC.
* **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels.
* To be eligible for a bounty, all of these guidelines must be followed.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties.
@@ -0,0 +1,32 @@
<?php
/**
* Action Hooks for Jetpack Backup module.
*
* @package automattic/jetpack-backup
*/
// If WordPress's plugin API is available already, use it. If not,
// drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`.
if ( function_exists( 'add_filter' ) ) {
$add_filter = 'add_filter';
$add_action = 'add_action';
} else {
$add_filter = function ( $name, $cb, $priority = 10, $accepted_args = 1 ) {
global $wp_filter;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter[ $name ][ $priority ][] = array(
'accepted_args' => $accepted_args,
'function' => $cb,
);
};
$add_action = $add_filter;
}
// Clean up expired Helper Scripts from a scheduled event.
$add_action( 'jetpack_backup_cleanup_helper_scripts', array( 'Automattic\\Jetpack\\Backup\\V0005\\Helper_Script_Manager', 'cleanup_expired_helper_scripts' ) );
// Register REST routes.
$add_action( 'rest_api_init', array( 'Automattic\\Jetpack\\Backup\\V0005\\REST_Controller', 'register_rest_routes' ) );
// Set up package version hook.
$add_filter( 'jetpack_package_versions', 'Automattic\\Jetpack\\Backup\\Package_Version::send_package_version_to_tracker' );
@@ -0,0 +1,10 @@
const config = {
presets: [
[
'@automattic/jetpack-webpack-config/babel/preset',
{ pluginReplaceTextdomain: { textdomain: 'jetpack-backup-pkg' } },
],
],
};
module.exports = config;
@@ -0,0 +1,9 @@
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="32" height="32" rx="4" fill="#008710"/>
<mask id="mask0_3026_4949" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="9" y="6" width="15" height="20">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5002 13.5002V11.0002C13.7418 11.0002 11.5002 13.2418 11.5002 16.0002C11.5002 16.8418 11.7085 17.6418 12.0835 18.3335L10.8668 19.5502C10.2168 18.5252 9.8335 17.3085 9.8335 16.0002C9.8335 12.3168 12.8168 9.3335 16.5002 9.3335V6.8335L19.8335 10.1668L16.5002 13.5002ZM20.9168 13.6668L22.1335 12.4502C22.7835 13.4752 23.1668 14.6918 23.1668 16.0002C23.1668 19.6835 20.1835 22.6668 16.5002 22.6668V25.1668L13.1668 21.8335L16.5002 18.5002V21.0002C19.2585 21.0002 21.5002 18.7585 21.5002 16.0002C21.5002 15.1585 21.2835 14.3668 20.9168 13.6668Z" fill="white"/>
</mask>
<g mask="url(#mask0_3026_4949)">
<rect x="6.5" y="6" width="20" height="20" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 992 B

@@ -0,0 +1,33 @@
<svg
class="backup__animation-el-1"
width="176"
height="212"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter1_d)">
<rect x="40" y="40" width="96" height="132" rx="3" fill="#98C6D9"></rect>
</g>
<defs>
<filter
id="filter1_d"
x="0"
y="0"
width="176"
height="212"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
<feColorMatrix
in="SourceAlpha"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
></feColorMatrix>
<feOffset></feOffset>
<feGaussianBlur stdDeviation="20"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"></feColorMatrix>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow"></feBlend>
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"></feBlend>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 922 B

@@ -0,0 +1,33 @@
<svg
class="backup__animation-el-2"
width="248"
height="200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter2_d)">
<rect x="40" y="40" width="168" height="120" rx="3" fill="#F2D76B"></rect>
</g>
<defs>
<filter
id="filter2_d"
x="0"
y="0"
width="248"
height="200"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
<feColorMatrix
in="SourceAlpha"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
></feColorMatrix>
<feOffset></feOffset>
<feGaussianBlur stdDeviation="20"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"></feColorMatrix>
<feBlend in2="BackgroundImageFix" result="effect2_dropShadow"></feBlend>
<feBlend in="SourceGraphic" in2="effect2_dropShadow" result="shape"></feBlend>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 924 B

@@ -0,0 +1,42 @@
<svg
class="backup__animation-el-3"
width="536"
height="196"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter3_d)">
<rect x="40" y="40" width="456" height="116" rx="8" fill="#fff"></rect>
</g>
<path
d="M475.35 62.04A7.49 7.49 0 00468 56c-2.89 0-5.4 1.64-6.65 4.04A5.994 5.994 0 00456 66c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"
fill="#E9EFF5"
></path>
<circle cx="100" cy="98" r="36" fill="#F7A8C3"></circle>
<path
d="M160 84a6 6 0 016-6h174a6 6 0 110 12H166a6 6 0 01-6-6zM160 112a6 6 0 016-6h276a6 6 0 110 12H166a6 6 0 01-6-6z"
fill="#E9EFF5"
></path>
<defs>
<filter
id="filter3_d"
x="0"
y="0"
width="536"
height="196"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
<feColorMatrix
in="SourceAlpha"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
></feColorMatrix>
<feOffset></feOffset>
<feGaussianBlur stdDeviation="20"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"></feColorMatrix>
<feBlend in2="BackgroundImageFix" result="effect3_dropShadow"></feBlend>
<feBlend in="SourceGraphic" in2="effect3_dropShadow" result="shape"></feBlend>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" maskUnits="userSpaceOnUse" x="0" y="5" width="32" height="22"><path fill-rule="evenodd" clip-rule="evenodd" d="M16 5.333c4.853 0 8.893 3.453 9.8 8.053 3.467.24 6.2 3.094 6.2 6.614a6.67 6.67 0 01-6.667 6.666H8c-4.413 0-8-3.586-8-8 0-4.12 3.12-7.52 7.133-7.946A9.994 9.994 0 0116 5.333zM8.667 18l4.666 4.666 8.787-8.786L20.24 12l-6.907 6.906-2.786-2.786L8.667 18z" fill="#fff"></path></mask><g mask="url(#a)"><path fill="#069E08" d="M0 0h32v32H0z"></path></g></svg>

After

Width:  |  Height:  |  Size: 551 B

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="32" height="32" viewBox="0 0 24 24"><path fill="#d63639" d="M19 20H6C2.7 20 0 17.3 0 14C0 10.9 2.3 8.4 5.3 8C6.6 5.6 9.1 4 12 4C15.6 4 18.7 6.6 19.4 10C22 10.2 24 12.3 24 15C24 17.7 21.7 20 19 20M11 15V17H13V15H11M11 13H13V7H11V13Z" /></svg>

After

Width:  |  Height:  |  Size: 481 B

@@ -0,0 +1,9 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="4" fill="#008710"/>
<mask id="mask0_3026_4944" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="6" y="9" width="20" height="14">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.125 14.3668C21.5583 11.4918 19.0333 9.3335 16 9.3335C13.5917 9.3335 11.5 10.7002 10.4583 12.7002C7.95 12.9668 6 15.0918 6 17.6668C6 20.4252 8.24167 22.6668 11 22.6668H21.8333C24.1333 22.6668 26 20.8002 26 18.5002C26 16.3002 24.2917 14.5168 22.125 14.3668ZM21.8333 21.0002H11C9.15833 21.0002 7.66667 19.5085 7.66667 17.6668C7.66667 15.9585 8.94167 14.5335 10.6333 14.3585L11.525 14.2668L11.9417 13.4752C12.7333 11.9502 14.2833 11.0002 16 11.0002C18.1833 11.0002 20.0667 12.5502 20.4917 14.6918L20.7417 15.9418L22.0167 16.0335C23.3167 16.1168 24.3333 17.2085 24.3333 18.5002C24.3333 19.8752 23.2083 21.0002 21.8333 21.0002ZM12.5917 16.0752L14.3333 17.8168L18.1667 13.9835L19.3417 15.1585L14.3333 20.1668L11.4167 17.2502L12.5917 16.0752Z" fill="white"/>
</mask>
<g mask="url(#mask0_3026_4944)">
<rect x="6" y="6" width="20" height="20" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,9 @@
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="32" height="32" rx="4" fill="#008710"/>
<mask id="mask0_3026_4960" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="8" y="7" width="17" height="18">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4998 7.6665C11.8998 7.6665 8.1665 11.3998 8.1665 15.9998C8.1665 20.5998 11.8998 24.3332 16.4998 24.3332C21.0998 24.3332 24.8332 20.5998 24.8332 15.9998C24.8332 11.3998 21.0998 7.6665 16.4998 7.6665ZM16.4998 22.6665C12.8248 22.6665 9.83317 19.6748 9.83317 15.9998C9.83317 12.3248 12.8248 9.33317 16.4998 9.33317C20.1748 9.33317 23.1665 12.3248 23.1665 15.9998C23.1665 19.6748 20.1748 22.6665 16.4998 22.6665ZM11.4998 17.6665C11.4998 16.746 12.246 15.9998 13.1665 15.9998C14.087 15.9998 14.8332 16.746 14.8332 17.6665C14.8332 18.587 14.087 19.3332 13.1665 19.3332C12.246 19.3332 11.4998 18.587 11.4998 17.6665ZM16.4998 10.9998C15.5794 10.9998 14.8332 11.746 14.8332 12.6665C14.8332 13.587 15.5794 14.3332 16.4998 14.3332C17.4203 14.3332 18.1665 13.587 18.1665 12.6665C18.1665 11.746 17.4203 10.9998 16.4998 10.9998ZM18.1665 17.6665C18.1665 16.746 18.9127 15.9998 19.8332 15.9998C20.7536 15.9998 21.4998 16.746 21.4998 17.6665C21.4998 18.587 20.7536 19.3332 19.8332 19.3332C18.9127 19.3332 18.1665 18.587 18.1665 17.6665Z" fill="white"/>
</mask>
<g mask="url(#mask0_3026_4960)">
<rect x="6.5" y="6" width="20" height="20" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,9 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="4" fill="#008710"/>
<mask id="mask0_3026_4955" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="6" y="8" width="18" height="16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75 16C8.75 11.8583 12.1083 8.5 16.25 8.5C20.3917 8.5 23.75 11.8583 23.75 16C23.75 20.1417 20.3917 23.5 16.25 23.5C14.175 23.5 12.3083 22.6583 10.95 21.3L12.1333 20.1167C13.1833 21.175 14.6417 21.8333 16.25 21.8333C19.475 21.8333 22.0833 19.225 22.0833 16C22.0833 12.775 19.475 10.1667 16.25 10.1667C13.025 10.1667 10.4167 12.775 10.4167 16H12.9167L9.55 19.3583L9.49167 19.2417L6.25 16H8.75ZM15.4167 16.8333V12.6667H16.6667V16.125L19.6 17.8667L18.9583 18.9333L15.4167 16.8333Z" fill="white"/>
</mask>
<g mask="url(#mask0_3026_4955)">
<rect x="5" y="6" width="20" height="20" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 920 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="4.75" y="7.75" width="14.5" height="10.5" rx="1.25" stroke="#069E08" stroke-width="1.5"></rect><path fill="#069E08" d="M6 5h5v3H6zM13 5h5v3h-5z"></path></svg>

After

Width:  |  Height:  |  Size: 242 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 7L8 13.5c-.167-1.167 1-5.167 1.5-6 .365-.608 1-1.5 2.5-2.5s5-2 5-2c0 1-1 2.5-2.5 4z" fill="#069E08"></path><path d="M7 16l1-2.5m0 0L14.5 7C16 5.5 17 4 17 3c0 0-3.5 1-5 2S9.865 6.892 9.5 7.5c-.5.833-1.667 4.833-1.5 6z" stroke="#069E08" stroke-width="1.5" stroke-linecap="round"></path><path d="M17.743 3.1a.75.75 0 10-1.486-.2l1.486.2zm-1.486-.2c-.089.662-.131 1.443-.17 2.235-.04.804-.075 1.63-.146 2.423-.072.795-.176 1.525-.344 2.134-.17.62-.386 1.036-.627 1.278l1.06 1.06c.509-.508.814-1.216 1.013-1.94.203-.735.317-1.568.392-2.398.074-.831.111-1.694.15-2.483.04-.802.08-1.52.158-2.11l-1.486-.198zm-1.287 8.07c-.276.275-.767.574-1.435.866-.653.287-1.415.541-2.175.754-.758.213-1.502.38-2.114.493-.63.117-1.071.167-1.246.167v1.5c.325 0 .885-.075 1.52-.192a26.28 26.28 0 002.245-.523 18.402 18.402 0 002.372-.825c.722-.316 1.419-.705 1.893-1.18l-1.06-1.06z" fill="#069E08"></path><path stroke="#069E08" stroke-width="1.5" d="M6 19.25h8"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17.44 13.905a5.345 5.345 0 01-10.69 0c0-.652.307-1.557.866-2.619.547-1.039 1.284-2.137 2.034-3.15a47.06 47.06 0 012.445-3.014 48.194 48.194 0 012.445 3.015c.75 1.012 1.488 2.11 2.034 3.15.56 1.061.866 1.966.866 2.618z" stroke="#069E08" stroke-width="1.5"></path></svg>

After

Width:  |  Height:  |  Size: 353 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M19.293 13.758a7.503 7.503 0 01-14.168 1.243l3.67-2.177 3.456 1.217a.75.75 0 00.686-.098l2.887-2.073.08.04c.296.145.703.349 1.15.58.78.406 1.64.877 2.239 1.268zm.206-1.635a37.63 37.63 0 00-1.754-.964 63.44 63.44 0 00-1.538-.771l-.099-.048-.026-.012-.01-.005a.75.75 0 00-.76.068l-2.932 2.105-3.417-1.203a.75.75 0 00-.631.062l-3.675 2.18A7.5 7.5 0 1119.5 12.123zM21 12a9 9 0 11-18 0 9 9 0 0118 0z" fill="#069E08"></path></svg>

After

Width:  |  Height:  |  Size: 548 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 4C16.42 4 20 7.58 20 12C20 16.42 16.42 20 12 20C7.58 20 4 16.42 4 12C4 7.58 7.58 4 12 4ZM13.13 13.38L13.48 6.92H10.52L10.87 13.38H13.13ZM13.04 16.74C13.28 16.51 13.41 16.19 13.41 15.78C13.41 15.36 13.29 15.04 13.05 14.81C12.81 14.58 12.46 14.46 11.99 14.46C11.52 14.46 11.17 14.58 10.92 14.81C10.67 15.04 10.55 15.36 10.55 15.78C10.55 16.19 10.68 16.51 10.93 16.74C11.19 16.97 11.54 17.08 11.99 17.08C12.44 17.08 12.79 16.97 13.04 16.74V16.74Z" fill="#D63638"/></svg>

After

Width:  |  Height:  |  Size: 556 B

@@ -0,0 +1 @@
<?php return array('dependencies' => array('jetpack-connection', 'jetpack-script-data', 'moment', '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' => '836b3de6e3dddd620bf5');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
/*
* Exposes number format capability
*
* @copyright Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io) and Contributors (http://phpjs.org/authors).
* @license See CREDITS.md
* @see https://github.com/kvz/phpjs/blob/ffe1356af23a6f2512c84c954dd4e828e92579fa/functions/strings/number_format.js
*/
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
{
"private": true,
"description": "Easily restore or download a backup of your site from a specific moment in time.",
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/backup/#readme",
"bugs": {
"url": "https://github.com/Automattic/jetpack/labels/[Package] Backup"
},
"repository": {
"type": "git",
"url": "https://github.com/Automattic/jetpack.git",
"directory": "projects/packages/backup"
},
"license": "GPL-2.0-or-later",
"author": "Automattic",
"scripts": {
"build": "pnpm run clean && pnpm run build-client",
"build-client": "webpack",
"build-concurrently": "pnpm run clean && concurrently 'pnpm:build-client' 'pnpm:build-php'",
"build-production-concurrently": "pnpm run clean && concurrently 'NODE_ENV=production BABEL_ENV=production pnpm run build-client' && pnpm run validate",
"clean": "rm -rf build/",
"test": "jest --config=tests/jest.config.js",
"test-coverage": "pnpm run test --coverage",
"validate": "pnpm exec validate-es build/",
"watch": "pnpm run build && webpack watch"
},
"browserslist": [
"extends @wordpress/browserslist-config"
],
"dependencies": {
"@automattic/format-currency": "1.0.1",
"@automattic/jetpack-analytics": "^0.1.35",
"@automattic/jetpack-api": "^0.17.22",
"@automattic/jetpack-components": "^0.65.5",
"@automattic/jetpack-connection": "^0.36.4",
"@tanstack/react-query": "5.20.5",
"@wordpress/api-fetch": "7.16.0",
"@wordpress/components": "29.2.0",
"@wordpress/data": "10.16.0",
"@wordpress/date": "5.16.0",
"@wordpress/element": "6.16.0",
"@wordpress/i18n": "5.16.0",
"moment": "2.30.1",
"prop-types": "^15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@automattic/jetpack-base-styles": "^0.6.41",
"@automattic/jetpack-webpack-config": "workspace:*",
"@babel/core": "7.26.0",
"@babel/preset-env": "7.26.0",
"@babel/runtime": "7.26.0",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.0.1",
"@testing-library/user-event": "14.5.2",
"@types/react": "18.3.18",
"@wordpress/browserslist-config": "6.16.0",
"concurrently": "7.6.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"sass": "1.64.1",
"sass-loader": "12.4.0",
"typescript": "5.0.4",
"webpack": "5.94.0",
"webpack-cli": "6.0.1"
}
}
@@ -0,0 +1,69 @@
<?php
/**
* The React initial state.
*
* @package automattic/jetpack-backup-plugin
*/
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
use Automattic\Jetpack\Connection\Plugin_Storage as Connection_Plugin_Storage;
use Automattic\Jetpack\Status;
use Jetpack_Options;
use function add_action;
use function admin_url;
use function esc_url;
use function esc_url_raw;
use function get_bloginfo;
use function get_site_url;
use function plugins_url;
use function rest_url;
use function wp_create_nonce;
use function wp_json_encode;
/**
* The React initial state.
*/
class Initial_State {
/**
* Get the initial state data.
*
* @return array
*/
private function get_data() {
return array(
'API' => array(
'WP_API_root' => esc_url_raw( rest_url() ),
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
),
'jetpackStatus' => array(
'calypsoSlug' => ( new Status() )->get_site_suffix(),
),
'connectedPlugins' => Connection_Plugin_Storage::get_all(),
'siteData' => array(
'id' => Jetpack_Options::get_option( 'id' ),
'title' => get_bloginfo( 'name' ) ? get_bloginfo( 'name' ) : get_site_url(),
'adminUrl' => esc_url( admin_url() ),
),
'assets' => array(
'buildUrl' => plugins_url( '../build/', __FILE__ ),
),
);
}
/**
* Render the initial state into a JavaScript variable.
*
* @return string
*/
public function render() {
add_action( 'jetpack_use_iframe_authorization_flow', '__return_true' );
return 'var JPBACKUP_INITIAL_STATE=JSON.parse(decodeURIComponent("' . rawurlencode( wp_json_encode( $this->get_data() ) ) . '"));';
}
}
@@ -0,0 +1,48 @@
<?php
/**
* Handle Backup plugin upgrades
*
* @package automattic/jetpack-backup-plugin
*/
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
use function get_option;
use function update_option;
/**
* The Upgrades class.
*/
class Jetpack_Backup_Upgrades {
/**
* Run all methods only once and store an option to make sure it never runs again
*/
public static function upgrade() {
$upgrades = get_class_methods( __CLASS__ );
foreach ( $upgrades as $upgrade ) {
$option_name = '_upgrade_' . $upgrade;
if ( 'upgrade' === $upgrade || get_option( $option_name ) ) {
continue;
}
update_option( $option_name, 1 );
call_user_func( array( __CLASS__, $upgrade ) );
}
}
/**
* The plugin is not checking if it was disabled and reactivating it when we reconnect, therefore we need to clear this information from DB so other plugins know we are still using the connection
*
* @deprecated since 1.7.0 No longer required after removing soft disconnect functionality.
*/
public static function clear_disabled_plugin() {}
}
@@ -0,0 +1,835 @@
<?php
/**
* Primary class file for the Jetpack Backup plugin.
*
* @package automattic/jetpack-backup-plugin
*/
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Backup\V0005\Initial_State as Backup_Initial_State;
use Automattic\Jetpack\Config;
use Automattic\Jetpack\Connection\Client;
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\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Terms_Of_Service;
use Automattic\Jetpack\Tracking;
use Jetpack_Options;
use WP_Error;
use WP_REST_Server;
// phpcs:ignore WordPress.Utils.I18nTextDomainFixer.MissingArgs
use function __;
// phpcs:ignore WordPress.Utils.I18nTextDomainFixer.MissingArgs
use function _x;
use function add_action;
use function add_filter;
use function did_action;
use function do_action;
use function esc_url_raw;
use function get_option;
use function is_wp_error;
use function rest_ensure_response;
use function update_option;
use function wp_add_inline_script;
use function wp_remote_get;
use function wp_remote_retrieve_body;
use function wp_remote_retrieve_response_code;
/**
* Class Jetpack_Backup
*/
class Jetpack_Backup {
/**
* Slug.
*
* @var string
*/
const JETPACK_BACKUP_SLUG = 'jetpack-backup';
/**
* Backup name.
*
* @var string
*/
const JETPACK_BACKUP_NAME = 'Jetpack Backup';
/**
* Backup URL.
*
* @var string
*/
const JETPACK_BACKUP_URI = 'https://jetpack.com/jetpack-backup';
/**
* Promoted product.
*
* @var string
*/
const JETPACK_BACKUP_PROMOTED_PRODUCT = 'jetpack_backup_t1_yearly';
/**
* Licenses product ID.
*
* @var string
*/
const JETPACK_BACKUP_PRODUCT_IDS = array(
2014, // JETPACK_COMPLETE.
2015, // JETPACK_COMPLETE_MONTHLY.
2016, // JETPACK_SECURITY_TIER_1_YEARLY.
2017, // JETPACK_SECURITY_TIER_1_MONTHLY.
2019, // JETPACK_SECURITY_TIER_2_YEARLY.
2020, // JETPACK_SECURITY_TIER_2_MONTHLY.
2112, // JETPACK_BACKUP_TIER_1_YEARLY.
2113, // JETPACK_BACKUP_TIER_1_MONTHLY.
2114, // JETPACK_BACKUP_TIER_2_YEARLY.
2115, // JETPACK_BACKUP_TIER_2_MONTHLY.
);
/**
* Jetpack Backup DB version.
*
* @var string
*/
const JETPACK_BACKUP_DB_VERSION = '2';
/**
* Constructor.
*/
public static function initialize() {
if ( did_action( 'jetpack_backup_initialized' ) ) {
return;
}
// Set up the REST authentication hooks.
Connection_Rest_Authentication::init();
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
$page_suffix = Admin_Menu::add_menu(
__( 'Jetpack VaultPress Backup', 'jetpack-backup-pkg' ),
_x( 'VaultPress Backup', 'The Jetpack VaultPress Backup product name, without the Jetpack prefix', 'jetpack-backup-pkg' ),
'manage_options',
'jetpack-backup',
array( __CLASS__, 'plugin_settings_page' ),
7
);
add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
// Init Jetpack packages.
add_action(
'plugins_loaded',
function () {
$config = new Config();
// Connection package.
$config->ensure(
'connection',
array(
'slug' => self::JETPACK_BACKUP_SLUG,
'name' => self::JETPACK_BACKUP_NAME,
'url_info' => self::JETPACK_BACKUP_URI,
)
);
// Sync package.
$config->ensure( 'sync' );
// Identity crisis package.
$config->ensure( 'identity_crisis' );
},
1
);
add_action( 'plugins_loaded', array( __CLASS__, 'maybe_upgrade_db' ), 20 );
add_filter( 'jetpack_connection_user_has_license', array( __CLASS__, 'jetpack_check_user_licenses' ), 10, 3 );
/**
* Runs right after the Jetpack Backup package is initialized.
*
* @since 1.3.0
*/
do_action( 'jetpack_backup_initialized' );
}
/**
* Initialize the admin resources.
*/
public static function admin_init() {
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_scripts' ) );
}
/**
* Checks current version against version in code and run upgrades if we are running a new version
*/
public static function maybe_upgrade_db() {
$current_db_version = get_option( 'jetpack_backup_db_version' );
if ( version_compare( $current_db_version, self::JETPACK_BACKUP_DB_VERSION, '<' ) ) {
update_option( 'jetpack_backup_db_version', self::JETPACK_BACKUP_DB_VERSION );
Jetpack_Backup_Upgrades::upgrade();
}
}
/**
* Returns whether we are in condition to track to use
* Analytics functionality like Tracks, MC, or GA.
*/
public static function can_use_analytics() {
$status = new Status();
$connection = new Connection_Manager( 'jetpack-backup' );
$tracking = new Tracking( 'jetpack', $connection );
return $tracking->should_enable_tracking( new Terms_Of_Service(), $status );
}
/**
* Enqueue plugin admin scripts and styles.
*/
public static function enqueue_admin_scripts() {
Assets::register_script(
'jetpack-backup',
'../build/index.js',
__FILE__,
array(
'in_footer' => true,
'textdomain' => 'jetpack-backup-pkg',
)
);
Assets::enqueue_script( 'jetpack-backup' );
// Initial JS state including JP Connection data.
wp_add_inline_script( 'jetpack-backup', self::get_initial_state(), 'before' );
Connection_Initial_State::render_script( 'jetpack-backup' );
// Load script for analytics.
if ( self::can_use_analytics() ) {
Tracking::register_tracks_functions_scripts( true );
}
}
/**
* Main plugin settings page.
*/
public static function plugin_settings_page() {
?>
<div id="jetpack-backup-root"></div>
<?php
}
/**
* Return the rendered initial state JavaScript code.
*
* @return string
*/
private static function get_initial_state() {
return ( new Backup_Initial_State() )->render();
}
/**
* Register REST API
*/
public static function register_rest_routes() {
// Get information on most recent 10 backups.
register_rest_route(
'jetpack/v4',
'/backups',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_recent_backups',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get site backup/scan/anti-spam capabilities.
register_rest_route(
'jetpack/v4',
'/backup-capabilities',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_backup_capabilities',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get whether the site has a backup plan
register_rest_route(
'jetpack/v4',
'/has-backup-plan',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::has_backup_plan',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get site rewind data.
register_rest_route(
'jetpack/v4',
'/restores',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_recent_restores',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get information on site products.
// Backup plugin version of /site/purchases from JP plugin.
// Revert once this route and MyPlan component are extracted to a common package.
register_rest_route(
'jetpack/v4',
'/site/current-purchases',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_current_purchases',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get currently promoted product from the product's endpoint.
register_rest_route(
'jetpack/v4',
'/backup-promoted-product-info',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_backup_promoted_product_info',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get and set value of dismissed_backup_review_request option
register_rest_route(
'jetpack/v4',
'/site/dismissed-review-request',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::manage_dismissed_backup_review_request',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
'args' => array(
'option_name' => array(
'required' => true,
'type' => 'string',
),
'should_dismiss' => array(
'required' => true,
'type' => 'boolean',
),
),
)
);
// Get site size
register_rest_route(
'jetpack/v4',
'/site/backup/size',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_size',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get backup schedule time
register_rest_route(
'jetpack/v4',
'/site/backup/schedule',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_schedule_time',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get site policies
register_rest_route(
'jetpack/v4',
'/site/backup/policies',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_policies',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
// Get site add-on offer
register_rest_route(
'jetpack/v4',
'/site/backup/addon-offer',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_addon_offer',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
'args' => array(
'storage_size' => array(
'required' => true,
'type' => 'numeric',
),
'storage_limit' => array(
'required' => true,
'type' => 'numeric',
),
),
)
);
// Enqueue a new backup
register_rest_route(
'jetpack/v4',
'/site/backup/enqueue',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => __CLASS__ . '::enqueue_backup',
'permission_callback' => __CLASS__ . '::backups_permissions_callback',
)
);
}
/**
* The backup calls should only occur from a signed in admin user
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function backups_permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* Get information about recent backups
*
* @access public
* @static
*
* @return array An array of recent backups
*/
public static function get_recent_backups() {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
'/sites/' . $blog_id . '/rewind/backups',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== $response['response']['code'] ) {
return null;
}
return rest_ensure_response(
json_decode( $response['body'], true )
);
}
/**
* Hits the wpcom api to check rewind status.
*
* @return Object|WP_Error
*/
private static function get_rewind_state_from_wpcom() {
static $status = null;
if ( $status !== null ) {
return $status;
}
$site_id = Jetpack_Options::get_option( 'id' );
$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' );
}
$body = wp_remote_retrieve_body( $response );
$status = json_decode( $body );
return $status;
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_backup_plan() {
$rewind_data = static::get_rewind_state_from_wpcom();
if ( is_wp_error( $rewind_data ) ) {
return false;
}
return is_object( $rewind_data ) && isset( $rewind_data->state ) && 'unavailable' !== $rewind_data->state;
}
/**
* Get an array of backup/scan/anti-spam site capabilities
*
* @access public
* @static
*
* @return array An array of capabilities
*/
public static function get_backup_capabilities() {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/rewind/capabilities',
'v2',
array(),
null,
'wpcom'
);
if ( is_wp_error( $response ) ) {
return null;
}
if ( 200 !== $response['response']['code'] ) {
return null;
}
return rest_ensure_response(
json_decode( $response['body'], true )
);
}
/**
* Get information about recent restores
*
* @access public
* @static
*
* @return array An array of recent restores
*/
public static function get_recent_restores() {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
'/sites/' . $blog_id . '/rewind/restores',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== $response['response']['code'] ) {
return null;
}
return rest_ensure_response(
json_decode( $response['body'], true )
);
}
/**
* Gets information about the currently promoted backup product.
*
* @return string|WP_Error A JSON object of the current backup product being promoted if the request was successful, or a WP_Error otherwise.
*/
public static function get_backup_promoted_product_info() {
$request_url = 'https://public-api.wordpress.com/rest/v1.1/products?locale=' . get_user_locale() . '&type=jetpack';
$wpcom_request = wp_remote_get( esc_url_raw( $request_url ) );
$response_code = wp_remote_retrieve_response_code( $wpcom_request );
if ( 200 === $response_code ) {
$products = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
return $products->{self::JETPACK_BACKUP_PROMOTED_PRODUCT};
} else {
// Something went wrong so we'll just return the response without caching.
return new WP_Error(
'failed_to_fetch_data',
esc_html__( 'Unable to fetch the requested data.', 'jetpack-backup-pkg' ),
array(
'status' => $response_code,
'request' => $wpcom_request,
)
);
}
}
/**
* Check for user licenses.
*
* @param boolean $has_license If the user already has a license found.
* @param array $licenses List of unattached licenses belonging to the user.
* @param string $plugin_slug The plugin that initiated the flow.
*
* @return boolean
*/
public static function jetpack_check_user_licenses( $has_license, $licenses, $plugin_slug ) {
if ( $plugin_slug !== static::JETPACK_BACKUP_SLUG || $has_license ) {
return $has_license;
}
$license_found = false;
foreach ( $licenses as $license ) {
if ( in_array( $license->product_id, static::JETPACK_BACKUP_PRODUCT_IDS, true ) ) {
$license_found = true;
break;
}
}
// Checking for existing backup plan is costly, so only check if there's an appropriate license.
return $license_found && ! static::has_backup_plan();
}
/**
* Returns the result of `/sites/%d/purchases` endpoint call.
*
* @return array of site purchases.
*/
public static function get_site_current_purchases() {
$request = sprintf( '/sites/%d/purchases', Jetpack_Options::get_option( 'id' ) );
$response = Client::wpcom_json_api_request_as_blog( $request, '1.1' );
// Bail if there was an error or malformed response.
if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
return self::get_failed_fetch_error();
}
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
return self::get_failed_fetch_error();
}
return rest_ensure_response(
json_decode( $response['body'], true )
);
}
/**
* Set value of the dismissed_backup_review_request Jetack option.
* Get value if should_dismiss is false
*
* @access public
* @static
* @param array $request arguments should_dismiss and option_name.
* @return bool value of option if value is requested | updated or not if value is updated.
*/
public static function manage_dismissed_backup_review_request( $request ) {
if ( ! $request['should_dismiss'] ) {
return rest_ensure_response(
Jetpack_Options::get_option( 'dismissed_backup_review_' . $request['option_name'] )
);
}
return Jetpack_Options::update_option( 'dismissed_backup_review_' . $request['option_name'], true );
}
/**
* Get site storage size
*
* @return string|WP_Error A JSON object with the site storage size if the request was successful, or a WP_Error otherwise.
*/
public static function get_site_backup_size() {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/rewind/size?force=wpcom',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
return rest_ensure_response(
json_decode( $response['body'], true )
);
}
/**
* Get site policies from WPCOM. It includes the storage limit and activity log limit, if apply.
*
* @return string|WP_Error A JSON object with the site storage policies if the request was successful,
* or a WP_Error otherwise.
*/
public static function get_site_backup_policies() {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/rewind/policies?force=wpcom',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
return rest_ensure_response(
json_decode( $response['body'], true )
);
}
/**
* Get suggested storage addon based on storage usage
*
* @param int $bytes_used Storage used.
* @param int $bytes_available Storage limit.
* @return string Suggested addon storage slug
*/
public static function get_storage_addon_upsell_slug( $bytes_used, $bytes_available ) {
$bytes_10gb = 10 * 1024 * 1024 * 1024; // 10GB in bytes
$bytes_100gb = 100 * 1024 * 1024 * 1024; // 100GB in bytes
$bytes_1tb = 1024 * 1024 * 1024 * 1024; // 1TB in bytes
$upsell_products = array(
$bytes_10gb => 'jetpack_backup_addon_storage_10gb_monthly',
$bytes_100gb => 'jetpack_backup_addon_storage_100gb_monthly',
$bytes_1tb => 'jetpack_backup_addon_storage_1tb_monthly',
);
// If usage has crossed over the storage limit, then dynamically calculate the upgrade option
if ( $bytes_used > $bytes_available ) {
$additional_bytes_used = $bytes_used - $bytes_available;
// Add aditional 25% buffer
$additional_bytes_needed = $additional_bytes_used + $additional_bytes_used * 0.25;
// Since 1TB is our max upgrade but the additional storage needed is greater than 1TB, then just return 1TB
if ( $additional_bytes_needed > $bytes_1tb ) {
return $upsell_products[ $bytes_1tb ];
}
foreach ( $upsell_products as $bytes => $product ) {
if ( $bytes > $additional_bytes_needed ) {
$matched_bytes = $bytes;
break;
}
}
if ( ! $matched_bytes ) {
$matched_bytes = $bytes_10gb;
}
return $upsell_products[ $matched_bytes ];
}
// For 1 TB we are going to offer 1 TB by default
if ( $bytes_1tb === $bytes_available ) {
return $upsell_products[ $bytes_1tb ];
}
// Otherwise, we are going to offer 10 GB
return $upsell_products[ $bytes_10gb ];
}
/**
* Get the best addon offer for this site, including pricing details
*
* @param \WP_REST_Request $request Object including storage usage.
*
* @return string|WP_Error A JSON object with the suggested storage addon details if the request was successful,
* or a WP_Error otherwise.
*/
public static function get_site_backup_addon_offer( $request ) {
$suggested_addon = self::get_storage_addon_upsell_slug(
$request['storage_size'],
$request['storage_limit']
);
$addons_size_text_map = array(
'jetpack_backup_addon_storage_10gb_monthly' => '10GB',
'jetpack_backup_addon_storage_100gb_monthly' => '100GB',
'jetpack_backup_addon_storage_1tb_monthly' => '1TB',
);
// Fetch addon storage price information
$pricing_info = Wpcom_Products::get_product_pricing( $suggested_addon );
// Response
$response = array(
'slug' => $suggested_addon,
'size_text' => $addons_size_text_map[ $suggested_addon ],
'pricing' => $pricing_info,
);
return rest_ensure_response( $response );
}
/**
* Enqueue a new backup on demand
*
* @return string|WP_Error A JSON object with `success` if the request was successful,
* or a WP_Error otherwise.
*/
public static function enqueue_backup() {
$blog_id = Jetpack_Options::get_option( 'id' );
$endpoint = sprintf( '/sites/%d/rewind/backups/enqueue', $blog_id );
$response = Client::wpcom_json_api_request_as_user(
$endpoint,
'v2',
array(
'method' => 'POST',
),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
return rest_ensure_response(
json_decode( $response['body'], true )
);
}
/**
* Get site backup schedule time
*
* @return string|WP_Error A JSON object with the backup schedule time if the request was successful, or a WP_Error otherwise.
*/
public static function get_site_backup_schedule_time() {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/rewind/scheduled',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
return rest_ensure_response(
json_decode( $response['body'], true )
);
}
/**
* Removes plugin from the connection manager
* If it's the last plugin using the connection, the site will be disconnected.
*
* @access public
* @static
*/
public static function plugin_deactivation() {
$manager = new Connection_Manager( 'jetpack-backup' );
$manager->remove_connection();
}
}
@@ -0,0 +1,23 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* The Package_Version class's compatibility shim.
*
* @package automattic/jetpack-backup
*/
// Do *not* update the "V0001" namespace version on changes.
namespace Automattic\Jetpack\Backup\V0001;
/**
* Package_Version proxy class to accommodate upgrades from plugin version 2.4.
*
* Backup plugin version 2.4 had a versioned class defined ("Automattic\Jetpack\Backup\V0001\Package_Version"), so
* the "jetpack_package_versions" filter will try to look for the class with this namespace + name in the newer
* plugin's code.
*/
class Package_Version {
// phpcs:ignore Squiz.Commenting.FunctionComment.Missing
public static function send_package_version_to_tracker( $package_versions ) {
return \Automattic\Jetpack\Backup\Package_Version::send_package_version_to_tracker( $package_versions );
}
}
@@ -0,0 +1,35 @@
<?php
/**
* The Package_Version class.
*
* @package automattic/jetpack-backup
*/
namespace Automattic\Jetpack\Backup;
/**
* The Package_Version class.
*
* Does *not* use namespaced versioning ("VXXXX") because send_package_version_to_tracker() is used as a
* "jetpack_package_versions" filter, and said filter gets run during a plugin upgrade, so it always expects to
* find the "Package_Version" class with the same namespace, name, and interface.
*/
class Package_Version {
const PACKAGE_VERSION = '4.0.8';
const PACKAGE_SLUG = 'backup';
/**
* Adds the package slug and version to the package version tracker's data.
*
* @param array $package_versions The package version array.
*
* @return array The package version array.
*/
public static function send_package_version_to_tracker( $package_versions ) {
$package_versions[ self::PACKAGE_SLUG ] = self::PACKAGE_VERSION;
return $package_versions;
}
}
@@ -0,0 +1,752 @@
<?php
/**
* The Backup Rest Controller class.
* Registers the REST routes for Backup.
*
* @package automattic/jetpack-backup
*/
// After changing this file, consider increasing the version number ("VXXX") in all the files using this namespace, in
// order to ensure that the specific version of this file always get loaded. Otherwise, Jetpack autoloader might decide
// to load an older/newer version of the class (if, for example, both the standalone and bundled versions of the plugin
// are installed, or in some other cases).
namespace Automattic\Jetpack\Backup\V0005;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Rest_Authentication;
use Automattic\Jetpack\Sync\Actions as Sync_Actions;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Jetpack_Options;
use WP_Error;
use WP_REST_Request;
use WP_REST_Server;
// phpcs:ignore WordPress.Utils.I18nTextDomainFixer.MissingArgs
use function esc_html__;
use function get_comment;
use function get_comment_meta;
use function get_metadata;
use function get_post;
use function get_post_meta;
use function get_term;
use function get_term_meta;
use function get_user_by;
use function get_user_meta;
use function is_wp_error;
use function register_rest_route;
use function rest_authorization_required_code;
use function rest_ensure_response;
use function wp_remote_retrieve_response_code;
/**
* Registers the REST routes for Backup.
*/
class REST_Controller {
/**
* Registers the REST routes for Backup.
*
* @access public
* @static
*/
public static function register_rest_routes() {
// Install a Helper Script to assist Jetpack Backup fetch data.
register_rest_route(
'jetpack/v4',
'/backup-helper-script',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => __CLASS__ . '::install_backup_helper_script',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
'args' => array(
'helper' => array(
'description' => __( 'base64 encoded Backup Helper Script body.', 'jetpack-backup-pkg' ),
'type' => 'string',
'required' => true,
),
),
)
);
// Delete a Backup Helper Script.
register_rest_route(
'jetpack/v4',
'/backup-helper-script',
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => __CLASS__ . '::delete_backup_helper_script',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
'args' => array(
'path' => array(
'description' => __( 'Path to Backup Helper Script', 'jetpack-backup-pkg' ),
'type' => 'string',
'required' => true,
),
),
)
);
// Fetch a backup of a database object, along with all of its metadata.
register_rest_route(
'jetpack/v4',
'/database-object/backup',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::fetch_database_object_backup',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
'args' => array(
'object_type' => array(
'description' => __( 'Type of object to fetch from the database', 'jetpack-backup-pkg' ),
'required' => true,
'validate_callback' => function ( $value ) {
if ( ! is_string( $value ) ) {
return new WP_Error(
'rest_invalid_param',
__( 'The object_type argument must be a non-empty string.', 'jetpack-backup-pkg' ),
array( 'status' => 400 )
);
}
$allowed_object_types = array_keys( self::get_allowed_object_types() );
if ( ! in_array( $value, $allowed_object_types, true ) ) {
return new WP_Error(
'rest_invalid_param',
sprintf(
/* translators: %s: comma-separated list of allowed object types */
__( 'The object_type argument should be one of %s', 'jetpack-backup-pkg' ),
implode( ', ', $allowed_object_types )
),
array( 'status' => 400 )
);
}
return true;
},
),
'object_id' => array(
'description' => __( 'ID of the database object to fetch', 'jetpack-backup-pkg' ),
'type' => 'integer',
'required' => true,
),
),
)
);
// Fetch a backup of an option.
register_rest_route(
'jetpack/v4',
'/options/backup',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::fetch_options_backup',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
'args' => array(
'name' => array(
'description' => __( 'One or more option names to include in the backup', 'jetpack-backup-pkg' ),
'validate_callback' => function ( $value ) {
$is_valid = is_array( $value ) || is_string( $value );
if ( ! $is_valid ) {
return new WP_Error( 'rest_invalid_param', __( 'The name argument should be an option name or an array of option names', 'jetpack-backup-pkg' ), array( 'status' => 400 ) );
}
return true;
},
'required' => true,
),
),
)
);
// Fetch a backup of a comment, along with all of its metadata.
register_rest_route(
'jetpack/v4',
'/comments/(?P<id>\d+)/backup',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::fetch_comment_backup',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
)
);
// Fetch a backup of a post, along with all of its metadata.
register_rest_route(
'jetpack/v4',
'/posts/(?P<id>\d+)/backup',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::fetch_post_backup',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
)
);
// Fetch a backup of a term, along with all of its metadata.
register_rest_route(
'jetpack/v4',
'/terms/(?P<id>\d+)/backup',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::fetch_term_backup',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
)
);
// Fetch a backup of a user, along with all of its metadata.
register_rest_route(
'jetpack/v4',
'/users/(?P<id>\d+)/backup',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::fetch_user_backup',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
)
);
// Get backup undo event
register_rest_route(
'jetpack/v4',
'/site/backup/undo-event',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_undo_event',
'permission_callback' => __NAMESPACE__ . '\Jetpack_Backup::backups_permissions_callback',
)
);
// Fetch a backup of a wc_order along with all of its data.
register_rest_route(
'jetpack/v4',
'/orders/(?P<id>\d+)/backup',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::fetch_wc_orders_backup',
'permission_callback' => __CLASS__ . '::backup_permissions_callback',
)
);
// Fetch backup preflight status
register_rest_route(
'jetpack/v4',
'/site/backup/preflight',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_preflight',
'permission_callback' => __NAMESPACE__ . '\Jetpack_Backup::backups_permissions_callback',
)
);
}
/**
* The Backup endpoints should only be available via site-level authentication.
* This means that the corresponding endpoints can only be accessible from WPCOM.
*
* @access public
* @static
*
* @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise.
*/
public static function backup_permissions_callback() {
if ( Rest_Authentication::is_signed_with_blog_token() ) {
return true;
}
$error_msg = esc_html__(
'You are not allowed to perform this action.',
'jetpack-backup-pkg'
);
return new WP_Error( 'rest_forbidden', $error_msg, array( 'status' => rest_authorization_required_code() ) );
}
/**
* Install the Backup Helper Script.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array|WP_Error Array with installation info on success:
*
* 'path' (string) Helper script installation path on the filesystem.
* 'url' (string) URL to the helper script.
* 'abspath' (string) WordPress root.
*
* or an instance of WP_Error on failure.
*/
public static function install_backup_helper_script( $request ) {
$helper_script = $request->get_param( 'helper' );
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$helper_script = base64_decode( $helper_script );
if ( ! $helper_script ) {
return new WP_Error( 'invalid_args', __( 'Helper script body must be base64 encoded', 'jetpack-backup-pkg' ), 400 );
}
$installation_info = Helper_Script_Manager::install_helper_script( $helper_script );
Helper_Script_Manager::cleanup_expired_helper_scripts();
return rest_ensure_response( $installation_info );
}
/**
* Delete a Backup Helper Script.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array|WP_Error An array with 'success' key, or an instance of WP_Error on failure.
*/
public static function delete_backup_helper_script( $request ) {
$path_to_helper_script = $request->get_param( 'path' );
$delete_result = Helper_Script_Manager::delete_helper_script( $path_to_helper_script );
Helper_Script_Manager::cleanup_expired_helper_scripts();
if ( is_wp_error( $delete_result ) ) {
return $delete_result;
}
return rest_ensure_response( array( 'success' => true ) );
}
/**
* Fetch a backup of a database object, along with all of its metadata.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array
*/
public static function fetch_database_object_backup( $request ) {
global $wpdb;
// Disable Sync as this is a read-only operation and triggered by sync activity.
Sync_Actions::mark_sync_read_only();
$allowed_object_types = self::get_allowed_object_types();
// Safe to do this as we have already validated the object_type key exists in self::get_allowed_object_types().
$object_type = $allowed_object_types[ $request->get_param( 'object_type' ) ];
$object_id = $request->get_param( 'object_id' );
$table = $wpdb->prefix . $object_type['table'];
$id_field = $object_type['id_field'];
// Fetch the requested object.
$object = $wpdb->get_row(
$wpdb->prepare(
'SELECT * FROM `' . $table . '` WHERE `' . $id_field . '` = %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$object_id
)
);
if ( empty( $object ) ) {
return new WP_Error( 'object_not_found', __( 'Object not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
}
$result = array( 'object' => $object );
// Fetch associated metadata (if this object type has any).
if ( ! empty( $object_type['meta_type'] ) ) {
$result['meta'] = get_metadata( $object_type['meta_type'], $object_id );
}
// If there is a child linked table (eg: woocommerce_tax_rate_locations), fetch linked records.
if ( ! empty( $object_type['child_table'] ) ) {
$child_table = $wpdb->prefix . $object_type['child_table'];
$child_id_field = $object_type['child_id_field'];
$result['children'] = $wpdb->get_results(
$wpdb->prepare(
'SELECT * FROM `' . $child_table . '` where `' . $child_id_field . '` = %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$object_id
)
);
}
return $result;
}
/**
* Fetch a backup of an option.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array
*/
public static function fetch_options_backup( $request ) {
// Disable Sync as this is a read-only operation and triggered by sync activity.
Sync_Actions::mark_sync_read_only();
$option_names = (array) $request->get_param( 'name' );
$options = array_map( self::class . '::get_option_row', $option_names );
return array( 'options' => $options );
}
/**
* Fetch a backup of a comment, along with all of its metadata.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array
*/
public static function fetch_comment_backup( $request ) {
// Disable Sync as this is a read-only operation and triggered by sync activity.
Sync_Actions::mark_sync_read_only();
$comment_id = $request['id'];
$comment = get_comment( $comment_id );
if ( empty( $comment ) ) {
return new WP_Error( 'comment_not_found', __( 'Comment not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
}
$allowed_keys = array(
'comment_ID',
'comment_post_ID',
'comment_author',
'comment_author_email',
'comment_author_url',
'comment_author_IP',
'comment_date',
'comment_date_gmt',
'comment_content',
'comment_karma',
'comment_approved',
'comment_agent',
'comment_type',
'comment_parent',
'user_id',
);
$comment = array_intersect_key( $comment->to_array(), array_flip( $allowed_keys ) );
$comment_meta = get_comment_meta( $comment['comment_ID'] );
return array(
'comment' => $comment,
'meta' => is_array( $comment_meta ) ? $comment_meta : array(),
);
}
/**
* Fetch a backup of a post, along with all of its metadata.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array
*/
public static function fetch_post_backup( $request ) {
global $wpdb;
// Disable Sync as this is a read-only operation and triggered by sync activity.
Sync_Actions::mark_sync_read_only();
$post_id = $request['id'];
$post = get_post( $post_id );
if ( empty( $post ) ) {
return new WP_Error( 'post_not_found', __( 'Post not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
}
// Fetch terms associated with this post object.
$terms = $wpdb->get_results(
$wpdb->prepare(
"SELECT term_taxonomy_id, term_order FROM {$wpdb->term_relationships} WHERE object_id = %d;",
$post->ID
)
);
return array(
'post' => (array) $post,
'meta' => get_post_meta( $post->ID ),
'terms' => (array) $terms,
);
}
/**
* Fetch a backup of a term, along with all of its metadata.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array
*/
public static function fetch_term_backup( $request ) {
// Disable Sync as this is a read-only operation and triggered by sync activity.
Sync_Actions::mark_sync_read_only();
$term_id = $request['id'];
$term = get_term( $term_id );
if ( empty( $term ) ) {
return new WP_Error( 'term_not_found', __( 'Term not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
}
return array(
'term' => (array) $term,
'meta' => get_term_meta( $term_id ),
);
}
/**
* Fetch a backup of a user, along with all of its metadata.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
* @return array
*/
public static function fetch_user_backup( $request ) {
// Disable Sync as this is a read-only operation and triggered by sync activity.
Sync_Actions::mark_sync_read_only();
$user_id = $request['id'];
$user = get_user_by( 'id', $user_id );
if ( empty( $user ) ) {
return new WP_Error( 'user_not_found', __( 'User not found', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
}
return array(
'user' => $user->to_array(),
'meta' => get_user_meta( $user->ID ),
);
}
/**
* Get allowed object types for the '/database-object/backup' endpoint.
*
* @access private
* @static
*
* @return array
*/
private static function get_allowed_object_types() {
return array(
'woocommerce_attribute' => array(
'table' => 'woocommerce_attribute_taxonomies',
'id_field' => 'attribute_id',
),
'woocommerce_downloadable_product_permission' => array(
'table' => 'woocommerce_downloadable_product_permissions',
'id_field' => 'permission_id',
),
'woocommerce_order_item' => array(
'table' => 'woocommerce_order_items',
'id_field' => 'order_item_id',
'meta_type' => 'order_item',
),
'woocommerce_payment_token' => array(
'table' => 'woocommerce_payment_tokens',
'id_field' => 'token_id',
'meta_type' => 'payment_token',
),
'woocommerce_tax_rate' => array(
'table' => 'woocommerce_tax_rates',
'id_field' => 'tax_rate_id',
'child_table' => 'woocommerce_tax_rate_locations',
'child_id_field' => 'tax_rate_id',
),
'woocommerce_webhook' => array(
'table' => 'wc_webhooks',
'id_field' => 'webhook_id',
),
);
}
/**
* This will fetch the last rewindable event from the Activity Log and
* the last rewind_id prior to that.
*/
public static function get_site_backup_undo_event() {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/activity?force=wpcom',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
$body = json_decode( $response['body'], true );
if ( ! isset( $body['current'] ) ) {
return null;
}
if ( ! isset( $body['current']['orderedItems'] ) ) {
return null;
}
// Preparing the response structure
$undo_event = array(
'last_rewindable_event' => null,
'undo_backup_id' => null,
);
// List of events that will not be considered to be undo.
// Basically we should not `undo` a full backup event, but we could
// use them to undo any other action like plugin updates.
$last_event_exceptions = array(
'rewind__backup_only_complete_full',
'rewind__backup_only_complete_initial',
'rewind__backup_only_complete',
'rewind__backup_complete_full',
'rewind__backup_complete_initial',
'rewind__backup_complete',
);
// Looping through the events to find the last rewindable event and the last backup_id.
// The idea is to find the last rewindable event and then the last rewind_id before that.
$found_last_event = false;
foreach ( $body['current']['orderedItems'] as $event ) {
if ( $event['is_rewindable'] ) {
if ( ! $found_last_event && ! in_array( $event['name'], $last_event_exceptions, true ) ) {
$undo_event['last_rewindable_event'] = $event;
$found_last_event = true;
} elseif ( $found_last_event ) {
$undo_event['undo_backup_id'] = $event['rewind_id'];
break;
}
}
}
// Ensure that we have a rewindable event and a backup_id to undo.
if ( $undo_event['last_rewindable_event'] === null || $undo_event['undo_backup_id'] === null ) {
return null;
}
return rest_ensure_response( $undo_event );
}
/**
* Fetch a backup of a order, along with all of its data.
*
* @access public
* @static
*
* @param WP_REST_Request $request The request sent to the WP REST API.
*
* @return array
*/
public static function fetch_wc_orders_backup( $request ) {
global $wpdb;
// Disable Sync as this is a read-only operation and triggered by sync activity.
Sync_Actions::mark_sync_read_only();
$order_id = $request['id'];
$order = array();
$order_addresses = array();
$order_operational_data = array();
$order_meta = array();
if ( ! class_exists( OrdersTableDataStore::class ) ) {
return new WP_Error( 'order_not_allowed', __( 'Not allowed to get the order with current configuration', 'jetpack-backup-pkg' ), array( 'status' => 403 ) );
}
if ( method_exists( OrdersTableDataStore::class, 'get_orders_table_name' ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
$order = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_orders_table_name() . '` WHERE id = %s', $order_id ) );
}
if ( empty( $order ) ) {
// No order in HPOS
return new WP_Error( 'order_not_found', __( 'Order not found ', 'jetpack-backup-pkg' ), array( 'status' => 404 ) );
}
if ( method_exists( OrdersTableDataStore::class, 'get_addresses_table_name' ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
$order_addresses = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_addresses_table_name() . '` WHERE order_id = %s', $order_id ) );
}
if ( method_exists( OrdersTableDataStore::class, 'get_operational_data_table_name' ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
$order_operational_data = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_operational_data_table_name() . '` WHERE order_id = %s', $order_id ) );
}
if ( method_exists( OrdersTableDataStore::class, 'get_meta_table_name' ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
$order_meta = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM `' . OrdersTableDataStore::get_meta_table_name() . '` WHERE order_id = %s', $order_id ) );
}
return array(
'order' => (array) $order,
'order_addresses' => (array) $order_addresses,
'order_operational_data' => (array) $order_operational_data,
'order_meta' => (array) $order_meta,
);
}
/**
* Fetch backup preflight status
*
* @return array
*/
public static function get_site_backup_preflight() {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/rewind/preflight?force=wpcom',
'v2',
array(),
null,
'wpcom'
);
if ( is_wp_error( $response ) ) {
return new WP_Error(
'wp_error_fetch_preflight',
$response->get_error_message(),
array( 'status' => 500 )
);
}
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $response_code ) {
return new WP_Error(
'http_error_fetch_preflight',
wp_remote_retrieve_response_message( $response ),
array( 'status' => $response_code )
);
}
$body = json_decode( $response['body'], true );
return rest_ensure_response( $body );
}
/**
* Fetch option row by option name.
*
* @access private
* @static
*
* @param string $name The option name.
* @return object|null Database query result as object format specified or null on failure.
*/
private static function get_option_row( $name ) {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare( "select * from `{$wpdb->options}` where option_name = %s", $name ) );
}
}
@@ -0,0 +1,107 @@
import apiFetch from '@wordpress/api-fetch';
import {
SITE_BACKUP_SIZE_GET,
SITE_BACKUP_SIZE_GET_FAILED,
SITE_BACKUP_SIZE_GET_SUCCESS,
SITE_BACKUP_POLICIES_GET,
SITE_BACKUP_POLICIES_GET_FAILED,
SITE_BACKUP_POLICIES_GET_SUCCESS,
SITE_BACKUP_STORAGE_SET,
SITE_BACKUP_STORAGE_ADDON_OFFER_SET,
SITE_BACKUPS_GET,
SITE_BACKUPS_GET_FAILED,
SITE_BACKUPS_GET_SUCCESS,
} from './types';
const getSiteSize =
() =>
( { dispatch } ) => {
dispatch( { type: SITE_BACKUP_SIZE_GET } );
apiFetch( { path: '/jetpack/v4/site/backup/size' } ).then(
res => {
if ( ! res.ok ) {
dispatch( { type: SITE_BACKUP_SIZE_GET_FAILED } );
return;
}
const payload = {
size: res.size,
lastBackupSize: res.last_backup_size,
minDaysOfBackupsAllowed: res.min_days_of_backups_allowed,
daysOfBackupsAllowed: res.days_of_backups_allowed,
daysOfBackupsSaved: res.days_of_backups_saved,
retentionDays: res.retention_days,
backupsStopped: res.backups_stopped,
};
dispatch( { type: SITE_BACKUP_SIZE_GET_SUCCESS, payload } );
},
() => {
dispatch( { type: SITE_BACKUP_SIZE_GET_FAILED } );
}
);
};
const getSitePolicies =
() =>
( { dispatch } ) => {
dispatch( { type: SITE_BACKUP_POLICIES_GET } );
apiFetch( { path: '/jetpack/v4/site/backup/policies' } ).then(
res => {
const payload = {
activityLogLimitDays: res.policies?.activity_log_limit_days ?? null,
storageLimitBytes: res.policies?.storage_limit_bytes ?? null,
};
dispatch( { type: SITE_BACKUP_POLICIES_GET_SUCCESS, payload } );
},
() => {
dispatch( { type: SITE_BACKUP_POLICIES_GET_FAILED } );
}
);
};
const setStorageUsageLevel =
usageLevel =>
( { dispatch } ) => {
dispatch( {
type: SITE_BACKUP_STORAGE_SET,
usageLevel,
} );
};
const setAddonStorageOfferSlug =
addonSlug =>
( { dispatch } ) => {
dispatch( {
type: SITE_BACKUP_STORAGE_ADDON_OFFER_SET,
addonOfferSlug: addonSlug,
} );
};
const getBackups =
() =>
( { dispatch } ) => {
dispatch( { type: SITE_BACKUPS_GET } );
apiFetch( { path: '/jetpack/v4/backups' } ).then(
res => {
dispatch( { type: SITE_BACKUPS_GET_SUCCESS, payload: res } );
},
() => {
dispatch( { type: SITE_BACKUPS_GET_FAILED } );
}
);
};
const actions = {
getBackups,
getSiteSize,
getSitePolicies,
setStorageUsageLevel,
setAddonStorageOfferSlug,
};
export default actions;
@@ -0,0 +1,70 @@
import apiFetch from '@wordpress/api-fetch';
import actions from '../index';
import { SITE_BACKUPS_GET, SITE_BACKUPS_GET_SUCCESS, SITE_BACKUPS_GET_FAILED } from '../types';
jest.mock( '@wordpress/api-fetch' );
const anyFunction = () => {};
const apiFixtures = {
requestOptions: {
path: '/jetpack/v4/backups',
},
successResponse: [
{
id: '123456789',
started: '2024-06-26 11:40:54',
last_updated: '2024-06-26 11:44:55',
status: 'not-accessible',
period: '321321321',
percent: '0',
is_backup: '1',
is_scan: '0',
},
{
id: '987654321',
started: '2024-06-26 06:36:08',
last_updated: '2024-06-26 06:39:05',
status: 'finished',
period: '123123123',
percent: '100',
is_backup: '1',
is_scan: '0',
has_snapshot: true,
discarded: '0',
stats: {},
},
],
failureResponse: 'Timeout error',
};
describe( 'getBackups', () => {
beforeEach( () => jest.clearAllMocks() );
it( 'dispatches SITE_BACKUPS_GET and SITE_BACKUPS_GET_SUCCESS on successful fetch', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.resolve( apiFixtures.successResponse ) );
await actions.getBackups()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( { type: SITE_BACKUPS_GET } );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUPS_GET_SUCCESS,
payload: apiFixtures.successResponse,
} );
} );
it( 'dispatches SITE_BACKUPS_GET and SITE_BACKUPS_GET_FAILED when API call fails', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.reject( apiFixtures.failureResponse ) );
await actions.getBackups()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( { type: SITE_BACKUPS_GET } );
expect( dispatch ).toHaveBeenCalledWith( { type: SITE_BACKUPS_GET_FAILED } );
} );
} );
@@ -0,0 +1,104 @@
import apiFetch from '@wordpress/api-fetch';
import actions from '../index';
import {
SITE_BACKUP_POLICIES_GET,
SITE_BACKUP_POLICIES_GET_FAILED,
SITE_BACKUP_POLICIES_GET_SUCCESS,
} from '../types';
const anyFunction = () => {};
jest.mock( '@wordpress/api-fetch' );
const apiFixtures = {
requestOptions: {
path: '/jetpack/v4/site/backup/policies',
},
successWithPoliciesResponse: {
policies: {
activity_log_limit_days: 30,
storage_limit_bytes: 7516192768,
},
},
successWithNoPoliciesResponse: {
policies: null,
},
failureResponse: '',
};
const successWithPoliciesPayload = {
activityLogLimitDays: 30,
storageLimitBytes: 7516192768,
};
const successWithNoPoliciesPayload = {
activityLogLimitDays: null,
storageLimitBytes: null,
};
describe( 'getSiteSize', () => {
beforeEach( () => jest.clearAllMocks() );
it( 'dispatches SITE_BACKUP_SIZE_GET and SITE_BACKUP_POLICIES_GET_SUCCESS with policy when fetches site with policies', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.resolve( apiFixtures.successWithPoliciesResponse ) );
await actions.getSitePolicies()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_POLICIES_GET,
} );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_POLICIES_GET_SUCCESS,
payload: successWithPoliciesPayload,
} );
} );
it.each( [
{
apiMockResponse: apiFixtures.successWithNoPoliciesResponse,
},
{
apiMockResponse: apiFixtures.failureResponse,
},
{
apiMockResponse: '',
},
] )(
'dispatches SITE_BACKUP_SIZE_GET and SITE_BACKUP_POLICIES_GET_SUCCESS with no policy when fetches site without policies',
async ( { apiMockResponse } ) => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.resolve( apiMockResponse ) );
await actions.getSitePolicies()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_POLICIES_GET,
} );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_POLICIES_GET_SUCCESS,
payload: successWithNoPoliciesPayload,
} );
}
);
it( 'dispatches SITE_BACKUP_SIZE_GET and SITE_BACKUP_POLICIES_GET_FAILED when API call fails', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.reject( 'Timeout error' ) );
await actions.getSitePolicies()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_POLICIES_GET,
} );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_POLICIES_GET_FAILED,
} );
} );
} );
@@ -0,0 +1,83 @@
import apiFetch from '@wordpress/api-fetch';
import actions from '../index';
import {
SITE_BACKUP_SIZE_GET,
SITE_BACKUP_SIZE_GET_FAILED,
SITE_BACKUP_SIZE_GET_SUCCESS,
} from '../types';
const anyFunction = () => {};
jest.mock( '@wordpress/api-fetch' );
const apiFixtures = {
requestOptions: {
path: '/jetpack/v4/site/backup/size',
},
successResponse: {
ok: true,
error: '',
size: 7516192768,
},
failureResponse: {
ok: false,
error: 'Unexpected error',
size: null,
},
};
const successPayload = {
size: apiFixtures.successResponse.size,
};
describe( 'getSiteSize', () => {
beforeEach( () => jest.clearAllMocks() );
it( 'dispatches SITE_BACKUP_SIZE_GET and SITE_BACKUP_SIZE_GET_SUCCESS when fetches successfully', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.resolve( apiFixtures.successResponse ) );
await actions.getSiteSize()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_SIZE_GET,
} );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_SIZE_GET_SUCCESS,
payload: successPayload,
} );
} );
it( 'dispatches SITE_BACKUP_SIZE_GET and SITE_BACKUP_SIZE_GET_FAILED when API call fails', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.reject( 'Timeout error' ) );
await actions.getSiteSize()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_SIZE_GET,
} );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_SIZE_GET_FAILED,
} );
} );
it( 'dispatches SITE_BACKUP_SIZE_GET and SITE_BACKUP_SIZE_GET_FAILED when API returns an error', async () => {
const dispatch = jest.fn( anyFunction );
apiFetch.mockReturnValue( Promise.resolve( apiFixtures.failureResponse ) );
await actions.getSiteSize()( { dispatch } );
expect( apiFetch ).toHaveBeenCalledWith( apiFixtures.requestOptions );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_SIZE_GET,
} );
expect( dispatch ).toHaveBeenCalledWith( {
type: SITE_BACKUP_SIZE_GET_FAILED,
} );
} );
} );
@@ -0,0 +1,11 @@
export const SITE_BACKUP_POLICIES_GET = 'SITE_BACKUP_POLICIES_GET';
export const SITE_BACKUP_POLICIES_GET_FAILED = 'SITE_BACKUP_POLICIES_GET_FAILED';
export const SITE_BACKUP_POLICIES_GET_SUCCESS = 'SITE_BACKUP_POLICIES_GET_SUCCESS';
export const SITE_BACKUP_SIZE_GET = 'SITE_BACKUP_SIZE_GET';
export const SITE_BACKUP_SIZE_GET_FAILED = 'SITE_BACKUP_SIZE_GET_FAILED';
export const SITE_BACKUP_SIZE_GET_SUCCESS = 'SITE_BACKUP_SIZE_GET_SUCCESS';
export const SITE_BACKUP_STORAGE_ADDON_OFFER_SET = 'SITE_BACKUP_STORAGE_ADDON_OFFER_SET';
export const SITE_BACKUP_STORAGE_SET = 'SITE_BACKUP_STORAGE_SET';
export const SITE_BACKUPS_GET = 'SITE_BACKUPS_GET';
export const SITE_BACKUPS_GET_FAILED = 'SITE_BACKUPS_GET_FAILED';
export const SITE_BACKUPS_GET_SUCCESS = 'SITE_BACKUPS_GET_SUCCESS';
@@ -0,0 +1,81 @@
import { JetpackVaultPressBackupLogo } from '@automattic/jetpack-components';
import { useSelect } from '@wordpress/data';
import { createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { useMemo } from 'react';
import useCapabilities from '../../hooks/useCapabilities';
import useConnection from '../../hooks/useConnection';
import { STORE_ID } from '../../store';
import { useShowBackUpNow } from '../back-up-now/hooks';
import { BackupNowButton } from '../back-up-now/index';
import { useIsFullyConnected } from './hooks';
const Header = () => {
const showActivateLicenseLink = useShowActivateLicenseLink();
const showBackUpNowButton = useShowBackUpNow();
return (
<div className="jetpack-admin-page__header">
<span className="jetpack-admin-page__logo">
<JetpackVaultPressBackupLogo />
</span>
{ showActivateLicenseLink && <ActivateLicenseLink /> }
{ showBackUpNowButton && (
<BackupNowButton variant="primary" tracksEventName="jetpack_backup_plugin_backup_now">
{ __( 'Back up now', 'jetpack-backup-pkg' ) }
</BackupNowButton>
) }
</div>
);
};
const useShowActivateLicenseLink = () => {
const connectionStatus = useConnection();
const isFullyConnected = useIsFullyConnected();
const { capabilitiesLoaded, hasBackupPlan } = useCapabilities();
// Give people a chance to activate a license if they're not fully connected,
// OR if they have a full user connection but no Backup capabilities
return useMemo( () => {
// At least wait until we know the status of the site and user connections
const connectionLoaded = Object.keys( connectionStatus ).length > 0;
if ( ! connectionLoaded ) {
return false;
}
if ( ! isFullyConnected ) {
return true;
}
// Even if we're fully connected, wait until we know the site's capabilities
// before deciding to show an activation link
if ( ! capabilitiesLoaded ) {
return false;
}
return ! hasBackupPlan;
}, [ connectionStatus, isFullyConnected, hasBackupPlan, capabilitiesLoaded ] );
};
const ActivateLicenseLink = () => {
const activateLicenseUrl = useSelect( select => {
const wpAdminUrl = select( STORE_ID ).getSiteData().adminUrl;
return `${ wpAdminUrl }admin.php?page=my-jetpack#/add-license`;
}, [] );
return (
<p>
{ createInterpolateElement(
__(
'Already have an existing plan or license key? <a>Click here to get started</a>',
'jetpack-backup-pkg'
),
{
a: <a href={ activateLicenseUrl } />,
}
) }
</p>
);
};
export default Header;
@@ -0,0 +1,41 @@
import apiFetch from '@wordpress/api-fetch';
import { useEffect, useMemo, useState } from '@wordpress/element';
import useConnection from '../../hooks/useConnection';
export const useIsFullyConnected = () => {
const connectionStatus = useConnection();
return useMemo( () => {
const connectionLoaded = 0 < Object.keys( connectionStatus ).length;
return connectionLoaded && connectionStatus.hasConnectedOwner && connectionStatus.isRegistered;
}, [ connectionStatus ] );
};
export const useIsSecondaryAdminNotConnected = () => {
const isFullyConnected = useIsFullyConnected();
const connectionStatus = useConnection();
return useMemo( () => {
return isFullyConnected && ! connectionStatus.isUserConnected;
}, [ isFullyConnected, connectionStatus ] );
};
export const useSiteHasBackupProduct = () => {
const isFullyConnected = useIsFullyConnected();
const [ siteHasBackupProduct, setSiteHasBackupProduct ] = useState( false );
const [ isLoading, setIsLoading ] = useState( true );
useEffect( () => {
if ( ! isFullyConnected ) {
setIsLoading( false );
return;
}
apiFetch( { path: '/jetpack/v4/has-backup-plan' } ).then( res => {
setSiteHasBackupProduct( res );
setIsLoading( false );
} );
}, [ isFullyConnected ] );
return { siteHasBackupProduct, isLoadingBackupProduct: isLoading };
};
@@ -0,0 +1,412 @@
import {
AdminPage,
AdminSection,
AdminSectionHero,
Container,
Col,
getRedirectUrl,
LoadingPlaceholder,
} from '@automattic/jetpack-components';
import { useConnectionErrorNotice, ConnectionError } from '@automattic/jetpack-connection';
import apiFetch from '@wordpress/api-fetch';
import { ExternalLink } from '@wordpress/components';
import { createInterpolateElement, useState, useEffect, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import useAnalytics from '../../hooks/useAnalytics';
import useBackupsState from '../../hooks/useBackupsState';
import useCapabilities from '../../hooks/useCapabilities';
import useConnection from '../../hooks/useConnection';
import { Backups, Loading as BackupsLoadingPlaceholder } from '../Backups';
import { BackupConnectionScreen } from '../backup-connection-screen';
import { BackupSecondaryAdminConnectionScreen } from '../backup-connection-screen/secondary-admin';
import BackupStorageSpace from '../backup-storage-space';
import ReviewRequest from '../review-request';
import Header from './header';
import {
useIsFullyConnected,
useIsSecondaryAdminNotConnected,
useSiteHasBackupProduct,
} from './hooks';
import NoBackupCapabilities from './no-backup-capabilities';
import './style.scss';
import '../masthead/masthead-style.scss';
/* eslint react/react-in-jsx-scope: 0 */
const Admin = () => {
const connectionStatus = useConnection();
const { tracks } = useAnalytics();
const { hasConnectionError } = useConnectionErrorNotice();
const connectionLoaded = 0 < Object.keys( connectionStatus ).length;
const isFullyConnected = useIsFullyConnected();
// If the site is fully connected and the current user is not connected it means the user
// is a secondary admin. We should ask them to log in to Jetpack.
const secondaryAdminNotConnected = useIsSecondaryAdminNotConnected();
const { siteHasBackupProduct, isLoadingBackupProduct } = useSiteHasBackupProduct();
useEffect( () => {
tracks.recordEvent( 'jetpack_backup_admin_page_view' );
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );
const { capabilities, capabilitiesError, capabilitiesLoaded, hasBackupPlan } = useCapabilities();
// If the user is a secondary admin not connected and the site has a backup product,
// let's show the login screen.
// @TODO: Review the use case where the site is fully connected but the backup product expires and
// a secondary admin is not connected. Currently it will display the default `Site backups are
// managed by the owner of this site's Jetpack connection.` message.
if ( secondaryAdminNotConnected ) {
if ( isLoadingBackupProduct ) {
return (
<SecondaryAdminConnectionLayout>
<LoadingPlaceholder width="100%" height={ 500 } />
</SecondaryAdminConnectionLayout>
);
}
if ( ! isLoadingBackupProduct && siteHasBackupProduct ) {
return (
<SecondaryAdminConnectionLayout>
<BackupSecondaryAdminConnectionScreen />
</SecondaryAdminConnectionLayout>
);
}
}
return (
<AdminPage
showHeader
showFooter
moduleName={ __( 'VaultPress Backup', 'jetpack-backup-pkg' ) }
header={ <Header /> }
>
<div id="jetpack-backup-admin-container" className="jp-content">
<div className="content">
<AdminSectionHero>
<Container horizontalSpacing={ 0 }>
{ hasConnectionError && (
<Col className="jetpack-connection-verified-error">
<ConnectionError />
</Col>
) }
<Col>
<div id="jp-admin-notices" className="jetpack-backup-jitm-card" />
</Col>
</Container>
<LoadedState
connectionLoaded={ connectionLoaded }
connectionStatus={ connectionStatus }
capabilitiesLoaded={ capabilitiesLoaded }
hasBackupPlan={ hasBackupPlan }
capabilitiesError={ capabilitiesError }
capabilities={ capabilities }
isFullyConnected={ isFullyConnected }
/>
</AdminSectionHero>
<AdminSection>
{ isFullyConnected && (
<BackupSegments
hasBackupPlan={ hasBackupPlan }
connectionLoaded={ connectionLoaded }
/>
) }
</AdminSection>
</div>
</div>
</AdminPage>
);
};
// Render additional segments for the backup admin page under the jp-hero section.
// If the user has a backup plan and is connected, we render the storage space segment.
const BackupSegments = ( { hasBackupPlan, connectionLoaded } ) => {
const connectionStatus = useConnection();
const { tracks } = useAnalytics();
const trackLearnMoreClick = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_learn_more_click' );
}, [ tracks ] );
const trackLearnBackupBrowserClick = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_learn_file_browser_click' );
}, [ tracks ] );
return (
<Container horizontalSpacing={ 3 } horizontalGap={ 3 } className="backup-segments">
<Col lg={ 6 } md={ 6 }>
<h2>{ __( 'Restore points created with every edit', 'jetpack-backup-pkg' ) }</h2>
<p>
{ __(
'No need to run a manual backup before you make changes to your site.',
'jetpack-backup-pkg'
) }
</p>
<p>
<ExternalLink
href={ getRedirectUrl( 'jetpack-blog-realtime-mechanics' ) }
onClick={ trackLearnMoreClick }
>
{ __( 'Learn about real-time backups', 'jetpack-backup-pkg' ) }
</ExternalLink>
</p>
</Col>
{ hasBackupPlan && connectionStatus.isUserConnected && (
<>
<Col lg={ 1 } md={ 1 } />
<Col lg={ 5 } md={ 5 } className="backup-segments__storage-section">
{ <BackupStorageSpace /> }
</Col>
</>
) }
<Col lg={ 6 } md={ 6 }>
<h2>{ __( 'Manage your backup files', 'jetpack-backup-pkg' ) }</h2>
<p>
{ __(
'The backup file browser allows you to access, preview and download all your backup files.',
'jetpack-backup-pkg'
) }
</p>
<p>
<ExternalLink
href={ getRedirectUrl( 'jetpack-blog-backup-file-browser' ) }
onClick={ trackLearnBackupBrowserClick }
>
{ __( 'Learn about the file browser', 'jetpack-backup-pkg' ) }
</ExternalLink>
</p>
</Col>
<ReviewMessage connectionLoaded={ connectionLoaded } />
</Container>
);
};
const ReviewMessage = connectionLoaded => {
const [ restores ] = useRestores( connectionLoaded );
const { backups } = useBackupsState();
const { tracks } = useAnalytics();
let requestReason = '';
let reviewText = '';
// Check if the site has a successful restore not older than 15 days
const hasRecentSuccesfulRestore = () => {
if ( restores[ 0 ] && restores[ 0 ].status === 'finished' ) {
// Number of days we consider the restore recent
const maxDays = 15;
const daysDifference = ( new Date() - Date.parse( restores[ 0 ].when ) ) / 86400000;
if ( daysDifference < maxDays ) {
return true;
}
}
return false;
};
// Check if the last 5 backups were successful
const hasFiveSuccessfulBackups = () => {
if ( ! Array.isArray( backups ) || backups.length < 5 ) {
return false;
}
let fiveSuccessfulBackups = true;
backups.slice( 0, 5 ).forEach( backup => {
if ( ! 'finished' === backup.status || ! backup.stats ) {
fiveSuccessfulBackups = false;
}
} );
return fiveSuccessfulBackups;
};
const trackSendToReview = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_new_review_click' );
}, [ tracks ] );
const tracksDismissReview = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_dismiss_review_click' );
}, [ tracks ] );
if ( hasRecentSuccesfulRestore() ) {
requestReason = 'restore';
reviewText = __( 'Was it easy to restore your site?', 'jetpack-backup-pkg' );
} else if ( hasFiveSuccessfulBackups() ) {
requestReason = 'backups';
reviewText = __(
'Do you enjoy the peace of mind of having real-time backups?',
'jetpack-backup-pkg'
);
}
const [ dismissedReview, dismissMessage ] = useDismissedReviewRequest(
connectionLoaded,
requestReason,
tracksDismissReview
);
if ( ! hasRecentSuccesfulRestore() && ! hasFiveSuccessfulBackups() ) {
return null;
}
return (
<Col lg={ 6 } md={ 6 }>
<ReviewRequest
cta={ createInterpolateElement(
__(
'<strong>Please leave a review and help us spread the word!</strong>',
'jetpack-backup-pkg'
),
{
strong: <strong></strong>,
}
) }
href={ getRedirectUrl( 'jetpack-backup-new-review' ) }
onClick={ trackSendToReview }
requestReason={ requestReason }
reviewText={ reviewText }
dismissedReview={ dismissedReview }
dismissMessage={ dismissMessage }
/>
</Col>
);
};
const useRestores = connectionLoaded => {
const [ restores, setRestores ] = useState( [] );
useEffect( () => {
if ( ! connectionLoaded ) {
setRestores( [] );
return;
}
apiFetch( { path: '/jetpack/v4/restores' } ).then(
res => {
setRestores( res );
},
() => {
setRestores( [] );
}
);
}, [ setRestores, connectionLoaded ] );
return [ restores, setRestores ];
};
const useDismissedReviewRequest = ( connectionLoaded, requestReason, tracksDismissReview ) => {
const [ dismissedReview, setDismissedReview ] = useState( true );
useEffect( () => {
if ( ! connectionLoaded || ! requestReason ) {
return;
}
apiFetch( {
path: '/jetpack/v4/site/dismissed-review-request',
method: 'POST',
data: {
option_name: requestReason,
should_dismiss: false,
},
} ).then(
res => {
setDismissedReview( res );
},
() => {
setDismissedReview( true );
}
);
}, [ setDismissedReview, connectionLoaded, requestReason ] );
const dismissMessage = e => {
e.preventDefault();
tracksDismissReview();
apiFetch( {
path: '/jetpack/v4/site/dismissed-review-request',
method: 'POST',
data: {
option_name: requestReason,
should_dismiss: true,
},
} ).then( setDismissedReview( true ) );
};
return [ dismissedReview, dismissMessage, setDismissedReview ];
};
const LoadedState = ( {
capabilitiesLoaded,
hasBackupPlan,
capabilitiesError,
capabilities,
isFullyConnected,
} ) => {
if ( ! isFullyConnected ) {
return (
<Container horizontalSpacing={ 3 } horizontalGap={ 3 }>
<Col lg={ 12 } md={ 8 } sm={ 4 }>
<BackupConnectionScreen />
</Col>
</Container>
);
}
if ( ! capabilitiesLoaded ) {
return (
<Container horizontalSpacing={ 5 } fluid>
<Col>
<div className="jp-wrap jp-content backup-panel">
<BackupsLoadingPlaceholder />
</div>
</Col>
</Container>
);
}
if ( hasBackupPlan ) {
return (
<Container horizontalSpacing={ 5 } fluid>
<Col>
<Backups />
</Col>
</Container>
);
}
// Render an error state, this shouldn't occurr since we've passed userConnected checks
if ( capabilitiesError === 'is_unlinked' ) {
return (
<Container horizontalSpacing={ 3 }>
<Col lg={ 12 } md={ 8 } sm={ 4 }>
<h2>
{ __(
"Site backups are managed by the owner of this site's Jetpack connection.",
'jetpack-backup-pkg'
) }
</h2>
</Col>
</Container>
);
}
if ( capabilitiesError === 'fetch_capabilities_failed' ) {
return (
<Container horizontalSpacing={ 3 }>
<Col lg={ 12 } md={ 8 } sm={ 4 }>
<h2>{ __( 'Failed to fetch site capabilities', 'jetpack-backup-pkg' ) }</h2>
</Col>
</Container>
);
}
if ( Array.isArray( capabilities ) && capabilities.length === 0 ) {
return <NoBackupCapabilities />;
}
return null;
};
const SecondaryAdminConnectionLayout = ( { children } ) => (
<AdminPage showHeader={ false } moduleName={ __( 'VaultPress Backup', 'jetpack-backup-pkg' ) }>
<Container horizontalSpacing={ 8 } horizontalGap={ 0 }>
<Col>{ children }</Col>
</Container>
</AdminPage>
);
export default Admin;
@@ -0,0 +1,94 @@
import { Container, Col, getRedirectUrl, PricingCard } from '@automattic/jetpack-components';
import apiFetch from '@wordpress/api-fetch';
import { useSelect } from '@wordpress/data';
import { useState, useEffect, useCallback } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import useAnalytics from '../../hooks/useAnalytics';
import { STORE_ID } from '../../store';
const NoBackupCapabilities = () => {
const { tracks } = useAnalytics();
const [ priceAfter, setPriceAfter ] = useState( 0 );
const [ price, setPrice ] = useState( 0 );
const [ currencyCode, setCurrencyCode ] = useState( 'USD' );
const [ introOffer, setIntroOffer ] = useState( null );
const domain = useSelect( select => select( STORE_ID ).getCalypsoSlug(), [] );
const blogID = useSelect( select => select( STORE_ID ).getBlogId(), [] );
useEffect( () => {
apiFetch( { path: '/jetpack/v4/backup-promoted-product-info' } ).then( res => {
setCurrencyCode( res.currency_code );
setPrice( res.cost / 12 );
if ( res.introductory_offer ) {
setIntroOffer( res.introductory_offer );
setPriceAfter( res.introductory_offer.cost_per_interval / 12 );
} else {
setPriceAfter( res.cost / 12 );
}
} );
}, [] );
const sendToCart = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_plugin_upgrade_click', { site: domain } );
window.location.href = getRedirectUrl( 'backup-plugin-upgrade-10gb', {
site: blogID ?? domain,
} );
}, [ tracks, domain, blogID ] );
const basicInfoText = __( '14 day money back guarantee.', 'jetpack-backup-pkg' );
const introductoryInfoText = __(
'Special introductory pricing, all renewals are at full price. 14 day money back guarantee.',
'jetpack-backup-pkg'
);
const priceDetails =
introOffer?.interval_unit === 'month' && introOffer?.interval_count === 1
? sprintf(
// translators: %s is the regular monthly price
__( 'trial for the first month, then $%s /month, billed yearly', 'jetpack-backup-pkg' ),
price
)
: __(
'per month, billed yearly',
'jetpack-backup-pkg',
/* dummy arg to avoid bad minification */ 0
);
return (
<Container horizontalSpacing={ 3 } horizontalGap={ 3 }>
<Col lg={ 6 } md={ 6 } sm={ 4 }>
<h1>{ __( 'Secure your site with a Backup subscription.', 'jetpack-backup-pkg' ) }</h1>
<p>
{ ' ' }
{ __(
'Get peace of mind knowing that all your work will be saved, and get back online quickly with one-click restores.',
'jetpack-backup-pkg'
) }
</p>
<ul className="jp-product-promote">
<li>{ __( 'Automated real-time backups', 'jetpack-backup-pkg' ) }</li>
<li>{ __( 'Easy one-click restores', 'jetpack-backup-pkg' ) }</li>
<li>{ __( 'Complete list of all site changes', 'jetpack-backup-pkg' ) }</li>
<li>{ __( 'Global server infrastructure', 'jetpack-backup-pkg' ) }</li>
<li>{ __( 'Best-in-class support', 'jetpack-backup-pkg' ) }</li>
</ul>
</Col>
<Col lg={ 1 } md={ 1 } sm={ 0 } />
<Col lg={ 5 } md={ 6 } sm={ 4 }>
<PricingCard
ctaText={ __( 'Get VaultPress Backup', 'jetpack-backup-pkg' ) }
icon="data:image/svg+xml,%3Csvg width='32' height='32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m21.092 15.164.019-1.703v-.039c0-1.975-1.803-3.866-4.4-3.866-2.17 0-3.828 1.351-4.274 2.943l-.426 1.524-1.581-.065a2.92 2.92 0 0 0-.12-.002c-1.586 0-2.977 1.344-2.977 3.133 0 1.787 1.388 3.13 2.973 3.133H22.399c1.194 0 2.267-1.016 2.267-2.4 0-1.235-.865-2.19-1.897-2.368l-1.677-.29Zm-10.58-3.204a4.944 4.944 0 0 0-.201-.004c-2.75 0-4.978 2.298-4.978 5.133s2.229 5.133 4.978 5.133h12.088c2.357 0 4.267-1.97 4.267-4.4 0-2.18-1.538-3.99-3.556-4.339v-.06c0-3.24-2.865-5.867-6.4-5.867-2.983 0-5.49 1.871-6.199 4.404Z' fill='%23000'/%3E%3C/svg%3E"
infoText={ priceAfter === price ? basicInfoText : introductoryInfoText }
onCtaClick={ sendToCart }
priceAfter={ priceAfter }
priceBefore={ price }
currencyCode={ currencyCode }
priceDetails={ priceDetails }
title={ __( 'VaultPress Backup', 'jetpack-backup-pkg' ) }
/>
</Col>
</Container>
);
};
export default NoBackupCapabilities;
@@ -0,0 +1,184 @@
@import '@automattic/jetpack-base-styles/style';
@import '../masthead/calypso-mixins';
.jp-header,
.jp-footer {
padding: 20px 0;
@include for-tablet-up {
padding: 40px 0;
}
}
.jp-content {
position: relative;
font-size: var(--font-body);
line-height: 1.5;
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 0;
line-height: 1.2;
}
h1 {
font-size: var(--font-title-large);
font-weight: 600;
}
h2 {
font-size: var(--font-title-small);
font-weight: 500;
}
.jp-section {
h2,
h3 {
margin-bottom: 16px;
}
p {
margin-top: 16px;
}
}
p,
li {
font-size: 16px;
line-height: 1.5;
}
.jp-connection-status-card h3,
.jpb-my-plan-container h3 {
margin-top: 48px;
font-size: var(--font-title-small);
font-weight: 500;
}
a {
color: var(--jp-black);
transition: color, background-color 0.15s ease-out;
&:hover {
text-decoration-thickness: var(--jp-underline-thickness);
}
&:focus {
outline-color: var(--jp-black);
}
svg.components-external-link__icon {
margin-left: 4px;
}
}
.button {
display: inline-block;
padding: 8px 24px;
font-weight: 500;
font-size: 16px;
color: var(--jp-white);
background: var(--jp-black);
text-decoration: none;
border-radius: var(--jp-border-radius);
border: 0;
border-color: var(--jp-black);
&.is-full-width {
width: 100%;
text-align: center;
}
&:hover,
&:active {
background: var(--jp-black-80);
color: var(--jp-white);
}
&:focus {
box-shadow: 0 0 0 1px var(--jp-white) inset, 0 0 0 2px var(--jp-black);
}
&:disabled,
&.disabled {
background: var(--jp-gray);
pointer-events: none;
}
}
ul.jp-product-promote li {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAANlBMVEVHcEwFnwUInggGnggGnggHnAcAnwUFnQcAnwcGnwkFnQgGnQgFnwcGnQYFnQcFnAcGnQkDnwdhiL0pAAAAEnRSTlMAMF//f2Aw7yBQ3+9gcIBgcED+HDbkAAAAZklEQVR4Ae3LNwICARDDQC0+cv7/Y8mwV9odSfWIcf/+VegnGkIvDaGXKvTTn/Gz+Uf5xTL0K1XotS7fs5H6GHvvaO8d7c3j7rdgHne/A/PYt/cO+R42oYdN6OEQetiFHo4A//6dAXqtBEkmtWutAAAAAElFTkSuQmCC)
no-repeat;
background-size: 24px;
padding-left: 30px;
margin-bottom: 9px;
color: var(--jp-black);
}
}
.jp-hero {
margin-bottom: 64px;
padding: 64px 0;
background: var(--jp-white-off);
&.is-backup-performing {
background-image: url("data:image/svg+xml,%3Csvg width='624' height='400' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M624 88.387C575.321 34.082 504.674 0 426 0 323.405 0 234.3 58.22 189.925 143.42 83.07 154.78 0 245.305 0 355c0 38.16 10.072 73.999 27.698 105H624V88.387z' fill='%23fff'/%3E%3C/svg%3E");
background-position: 100% 100%;
background-repeat: no-repeat;
}
&.is-backup-healthy,
&.is-backup-error {
display: none;
}
p + .button {
margin-top: 18px;
}
}
.jetpack-backup-jitm-card {
.jitm-card {
margin-right: 0;
}
}
.jp-dashboard-footer {
padding: 40px 0;
}
.jp-connection-status-card--status {
margin: 30px 0;
}
.jp-connection__connect-screen-layout__left {
h2 {
font-weight: bold;
font-size: 36px;
margin-top: 32px;
}
h3 {
margin-top: 32px;
}
}
.jetpack-connection-verified-error {
margin: 25px 0;
}
.jetpack-admin-page__header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: calc(var(--horizontal-spacing) * 3);
.jetpack-admin-page__logo {
flex-shrink: 0;
}
}
@@ -0,0 +1,364 @@
import {
getProductCheckoutUrl,
getRedirectUrl,
LoadingPlaceholder,
} from '@automattic/jetpack-components';
import { ExternalLink } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { getDate, dateI18n } from '@wordpress/date';
import { createInterpolateElement, useCallback } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { BACKUP_STATE } from '../constants';
import useAnalytics from '../hooks/useAnalytics';
import useBackupsState from '../hooks/useBackupsState.js';
import { STORE_ID } from '../store';
import StatBlock from './StatBlock';
import './backups-style.scss';
import { StorageUsageLevels } from './backup-storage-space/storage-usage-levels';
import BackupAnim1 from './icons/backup-animation-1.svg';
import BackupAnim2 from './icons/backup-animation-2.svg';
import BackupAnim3 from './icons/backup-animation-3.svg';
import CloudAlertIcon from './icons/cloud-alert.svg';
import CloudIcon from './icons/cloud.svg';
import PluginsIcon from './icons/plugins.svg';
import PostsIcon from './icons/posts.svg';
import ThemesIcon from './icons/themes.svg';
import UploadsIcon from './icons/uploads.svg';
import WarningIcon from './icons/warning.svg';
import NextScheduledBackup from './next-scheduled-backup';
/* eslint react/react-in-jsx-scope: 0 */
export const Backups = () => {
const { backupState, isInitialBackup, latestTime, progress, stats } = useBackupsState();
return (
<div className="jp-wrap jp-content backup-panel">
{ BACKUP_STATE.LOADING === backupState && <Loading /> }
{ BACKUP_STATE.NO_BACKUPS === backupState && <InProgressBackup progress={ progress } /> }
{ BACKUP_STATE.NO_BACKUPS_RETRY === backupState && (
<InProgressBackup
isInitialBackup={ isInitialBackup }
progress={ progress }
showProgressBar={ false }
/>
) }
{ BACKUP_STATE.IN_PROGRESS === backupState && (
<InProgressBackup isInitialBackup={ isInitialBackup } progress={ progress } />
) }
{ BACKUP_STATE.COMPLETE === backupState && (
<CompleteBackup latestTime={ latestTime } stats={ stats } />
) }
{ BACKUP_STATE.NO_GOOD_BACKUPS === backupState && <NoGoodBackups /> }
</div>
);
};
const NoGoodBackups = () => {
const domain = useSelect( select => select( STORE_ID ).getCalypsoSlug(), [] );
return (
<div className="jp-row">
<div className="lg-col-span-5 md-col-span-4 sm-col-span-4">
<img src={ CloudAlertIcon } alt="" />
<h1>{ __( "We're having trouble backing up your site", 'jetpack-backup-pkg' ) }</h1>
<p>
{ createInterpolateElement(
__(
'<a>Get in touch with us</a> to get your site backups going again.',
'jetpack-backup-pkg'
),
{
a: (
<a
//TODO: we may want to add a specific redirect for Backup plugin related issues
href={ getRedirectUrl( 'jetpack-contact-support', { site: domain } ) }
target="_blank"
rel="noreferrer"
/>
),
}
) }
</p>
</div>
<div className="lg-col-span-1 md-col-span-4 sm-col-span-0"></div>
<div className="lg-col-span-6 md-col-span-2 sm-col-span-2"></div>
</div>
);
};
export const Loading = () => {
return (
<div className="jp-row">
<div className="lg-col-span-4 md-col-span-4 sm-col-span-4">
<LoadingPlaceholder width={ 344 } height={ 182 } />
</div>
<div className="lg-col-span-0 md-col-span-4 sm-col-span-0"></div>
<div className="lg-col-span-2 md-col-span-2 sm-col-span-2">
<LoadingPlaceholder width={ 160 } height={ 152 } />
</div>
<div className="lg-col-span-2 md-col-span-2 sm-col-span-2">
<LoadingPlaceholder width={ 160 } height={ 152 } />
</div>
<div className="lg-col-span-2 md-col-span-2 sm-col-span-2">
<LoadingPlaceholder width={ 160 } height={ 152 } />
</div>
<div className="lg-col-span-2 md-col-span-2 sm-col-span-2">
<LoadingPlaceholder width={ 160 } height={ 152 } />
</div>
</div>
);
};
const formatDateString = dateString => {
const todayString = __( 'Today', 'jetpack-backup-pkg' );
const todayDate = getDate();
let backupDate = todayString;
if ( dateI18n( 'zY', todayDate ) !== dateI18n( 'zY', dateString ) ) {
backupDate = dateI18n( 'M j', dateString );
}
const backupTime = dateI18n( 'g:i A', dateString );
return backupDate + ', ' + backupTime;
};
const CompleteBackup = ( { latestTime, stats } ) => {
const domain = useSelect( select => select( STORE_ID ).getCalypsoSlug(), [] );
const { tracks } = useAnalytics();
const trackSeeBackupsCtaClick = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_see_backups_cta_click', { site: domain } );
}, [ tracks, domain ] );
const trackRecentRestorePointClick = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_view_recent_restore_points_click', { site: domain } );
}, [ tracks, domain ] );
const storageUsageLevel = useSelect( select => select( STORE_ID ).getStorageUsageLevel() );
const storageLimit = useSelect( select => select( STORE_ID ).getBackupStorageLimit() ) ?? 0;
const storageSize = useSelect( select => select( STORE_ID ).getBackupSize() ) ?? 0;
const storageOverlimit = storageSize > storageLimit;
const backupsStopped = storageUsageLevel === StorageUsageLevels.Full;
const addonSlug = useSelect( select => select( STORE_ID ).getStorageAddonOfferSlug() );
const siteSlug = useSelect( select => select( STORE_ID ).getCalypsoSlug() );
const adminUrl = useSelect( select => select( STORE_ID ).getSiteData().adminUrl );
const trackUpgradeStorageClick = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_upgrade_storage_header_cta', { site: domain } );
}, [ tracks, domain ] );
return (
<div className="jp-row">
<div className="lg-col-span-4 md-col-span-4 sm-col-span-4">
{ ! backupsStopped && (
<>
<div className="backup__latest">
<img
src={ CloudIcon }
alt=""
className={ stats.warnings ? 'backup__warning-color' : '' }
/>
<h2>{ __( 'Latest Backup', 'jetpack-backup-pkg' ) }</h2>
</div>
<div className="backup__latest-time">{ formatDateString( latestTime ) }</div>
<NextScheduledBackup />
</>
) }
{ backupsStopped && (
<>
<div className="backup__latest">
<img src={ WarningIcon } alt="" className="warning-icon" />
<h2>
{ storageOverlimit && __( 'Over storage space', 'jetpack-backup-pkg' ) }
{ ! storageOverlimit && __( 'Out of storage space', 'jetpack-backup-pkg' ) }
</h2>
</div>
<h1>{ __( 'Backups stopped', 'jetpack-backup-pkg' ) }</h1>
</>
) }
{ stats.warnings && ! backupsStopped && (
<div className="backup__warning-text">
{ createInterpolateElement(
__(
'Backup is completed with some files missing. See your <a>backup in the cloud</a> for more details.',
'jetpack-backup-pkg'
),
{
a: (
<a
href={ getRedirectUrl( 'jetpack-backup', { site: domain } ) }
target="_blank"
rel="noreferrer"
/>
),
}
) }
</div>
) }
{ ! stats.warnings &&
! backupsStopped &&
createInterpolateElement(
__(
'<Button>See your backups in the cloud</Button><br/><ExternalLink>View activity log</ExternalLink>',
'jetpack-backup-pkg'
),
{
Button: (
<ExternalLink
className="button"
href={ getRedirectUrl( 'jetpack-backup', { site: domain } ) }
onClick={ trackSeeBackupsCtaClick }
target="_blank"
rel="noreferrer"
/>
),
br: <br />,
ExternalLink: (
<ExternalLink
className="backup__restore-point-link"
href={ getRedirectUrl( 'backup-plugin-activity-log', { site: domain } ) }
onClick={ trackRecentRestorePointClick }
/>
),
}
) }
{ ! stats.warnings &&
backupsStopped &&
createInterpolateElement(
__(
'<Button>Upgrade your storage</Button><br/><a>Or view your most recent backup</a>',
'jetpack-backup-pkg'
),
{
Button: (
<a
className="button"
href={ getProductCheckoutUrl(
addonSlug,
siteSlug,
`${ adminUrl }admin.php?page=jetpack-backup`,
true
) }
onClick={ trackUpgradeStorageClick }
target="_blank"
rel="noreferrer"
/>
),
a: (
<a
className="backup__restore-point-link"
href={ getRedirectUrl( 'jetpack-backup', { site: domain } ) }
onClick={ trackSeeBackupsCtaClick }
target="_blank"
rel="noreferrer"
/>
),
br: <br />,
}
) }
</div>
<div className="lg-col-span-0 md-col-span-4 sm-col-span-0"></div>
<div className="lg-col-span-2 md-col-span-4 sm-col-span-4">
<StatBlock
icon={ PostsIcon }
label={ __( 'Posts', 'jetpack-backup-pkg' ) }
value={ stats.posts }
/>
</div>
<div className="lg-col-span-2 md-col-span-4 sm-col-span-4">
<StatBlock
icon={ UploadsIcon }
label={ __( 'Uploads', 'jetpack-backup-pkg' ) }
value={ stats.uploads }
/>
</div>
<div className="lg-col-span-2 md-col-span-4 sm-col-span-4">
<StatBlock
icon={ PluginsIcon }
label={ __( 'Plugins', 'jetpack-backup-pkg' ) }
value={ stats.plugins }
/>
</div>
<div className="lg-col-span-2 md-col-span-4 sm-col-span-4">
<StatBlock
icon={ ThemesIcon }
label={ __( 'Themes', 'jetpack-backup-pkg' ) }
value={ stats.themes }
/>
</div>
</div>
);
};
const InProgressBackup = ( { isInitialBackup, progress, showProgressBar = true } ) => {
const domain = useSelect( select => select( STORE_ID ).getCalypsoSlug(), [] );
const blogID = useSelect( select => select( STORE_ID ).getBlogId(), [] );
const siteTitle = useSelect( select => select( STORE_ID ).getSiteTitle(), [] );
const firstBackupTitle = __( 'Your first cloud backup will be ready soon', 'jetpack-backup-pkg' );
const regularBackupTitle = __( 'Your backup will be ready soon', 'jetpack-backup-pkg' );
const title = isInitialBackup ? firstBackupTitle : regularBackupTitle;
return (
<div className="jp-row">
<div className="lg-col-span-5 md-col-span-8 sm-col-span-4">
{ showProgressBar && (
<div className="backup__progress">
<div className="backup__progress-info">
<p>
{ sprintf(
/* translators: placeholder is the Site Title */
__( 'Backing up %s', 'jetpack-backup-pkg' ),
siteTitle
) }
</p>
<p className="backup__progress-info-percentage">{ progress }%</p>
</div>
<div className="backup__progress-bar">
<div
className="backup__progress-bar-actual"
style={ { width: progress + '%' } }
></div>
</div>
</div>
) }
<h1>{ title }</h1>
{ isInitialBackup && (
<>
<p>
{ __(
'The first backup usually takes a few minutes, so it will become available soon.',
'jetpack-backup-pkg'
) }
</p>
<p>
{ createInterpolateElement(
__(
'In the meanwhile, you can start getting familiar with your <a>backup management on Jetpack.com</a>.',
'jetpack-backup-pkg'
),
{
a: (
<a
href={ getRedirectUrl( 'jetpack-backup', { site: blogID ?? domain } ) }
target="_blank"
rel="noreferrer"
/>
),
}
) }
</p>
</>
) }
</div>
<div className="lg-col-span-1 md-col-span-4 sm-col-span-0"></div>
<div className="backup__animation lg-col-span-6 md-col-span-2 sm-col-span-2">
<img className="backup__animation-el-1" src={ BackupAnim1 } alt="" />
<img className="backup__animation-el-2" src={ BackupAnim2 } alt="" />
<img className="backup__animation-el-3" src={ BackupAnim3 } alt="" />
</div>
</div>
);
};
export default Backups;
@@ -0,0 +1,16 @@
import './stat-block-style.scss';
/* eslint react/react-in-jsx-scope: 0 */
const StatBlock = props => {
return (
<div className="backup__card">
<img src={ props.icon } alt="" />
<div className="backup__card-details">
<div className="backup__card-details-items">{ props.label }</div>
<div className="backup__card-details-amount">{ props.value }</div>
</div>
</div>
);
};
export default StatBlock;
@@ -0,0 +1,14 @@
import { useMemo } from 'react';
import useCapabilities from '../../hooks/useCapabilities';
import useConnection from '../../hooks/useConnection';
import { useIsFullyConnected } from '../Admin/hooks';
export const useShowBackUpNow = () => {
const connectionStatus = useConnection();
const isFullyConnected = useIsFullyConnected();
const { capabilitiesLoaded, hasBackupPlan } = useCapabilities();
return useMemo( () => {
return connectionStatus && isFullyConnected && capabilitiesLoaded && hasBackupPlan;
}, [ connectionStatus, isFullyConnected, hasBackupPlan, capabilitiesLoaded ] );
};
@@ -0,0 +1,117 @@
import { Button } from '@automattic/jetpack-components';
import apiFetch from '@wordpress/api-fetch';
import { Tooltip } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useState } from 'react';
import { BACKUP_STATE } from '../../constants';
import useAnalytics from '../../hooks/useAnalytics';
import useBackupsState from '../../hooks/useBackupsState.js';
import { STORE_ID } from '../../store';
export const BackupNowButton = ( {
children,
tooltipText,
tracksEventName,
variant = 'primary',
weight = 'regular',
onClick,
} ) => {
const { tracks } = useAnalytics();
const [ buttonContent, setButtonContent ] = useState( children );
const [ currentTooltip, setCurrentTooltip ] = useState( tooltipText );
const [ isEnqueuing, setIsEnqueuing ] = useState( false );
const [ enqueued, setEnqueued ] = useState( false );
const areBackupsStopped = useSelect( select => select( STORE_ID ).getBackupStoppedFlag() );
const { backupState, fetchBackupsState } = useBackupsState( enqueued );
const backupCurrentlyInProgress = backupState === BACKUP_STATE.IN_PROGRESS;
const disabled = isEnqueuing || enqueued || backupCurrentlyInProgress || areBackupsStopped;
const onClickHandler = useCallback(
event => {
if ( tracksEventName ) {
tracks.recordEvent( tracksEventName );
}
setIsEnqueuing( true );
apiFetch( { method: 'POST', path: `/jetpack/v4/site/backup/enqueue` } ).then( () => {
setIsEnqueuing( false );
setEnqueued( true );
fetchBackupsState();
} );
if ( onClick ) {
onClick( event );
}
},
[ tracksEventName, onClick, tracks, fetchBackupsState ]
);
useEffect( () => {
const statusLabels = {
QUEUEING: __( 'Queueing backup', 'jetpack-backup-pkg' ),
QUEUED: __( 'Backup enqueued', 'jetpack-backup-pkg' ),
IN_PROGRESS: __( 'Backup in progress', 'jetpack-backup-pkg' ),
};
const statusTooltipTexts = {
QUEUING: null,
QUEUED: __( 'A backup has been queued and will start shortly.', 'jetpack-backup-pkg' ),
IN_PROGRESS: __( 'A backup is currently in progress.', 'jetpack-backup-pkg' ),
};
if ( areBackupsStopped ) {
setCurrentTooltip(
__( 'Cannot queue backups due to reaching storage limits.', 'jetpack-backup-pkg' )
);
} else if ( backupCurrentlyInProgress ) {
setCurrentTooltip( statusTooltipTexts.IN_PROGRESS );
setButtonContent( statusLabels.IN_PROGRESS );
setEnqueued( false );
} else if ( isEnqueuing ) {
setButtonContent( statusLabels.QUEUEING );
setCurrentTooltip( statusTooltipTexts.QUEUING );
} else if ( enqueued ) {
setButtonContent( statusLabels.QUEUED );
setCurrentTooltip( statusTooltipTexts.QUEUED );
} else {
setButtonContent( children );
setCurrentTooltip( tooltipText );
}
}, [
backupCurrentlyInProgress,
tooltipText,
enqueued,
children,
areBackupsStopped,
isEnqueuing,
] );
const button = (
<div>
<Button
variant={ variant }
onClick={ onClickHandler }
disabled={ disabled }
isBusy={ isEnqueuing || backupCurrentlyInProgress }
weight={ weight }
>
{ buttonContent }
</Button>
</div>
);
return <>{ currentTooltip ? <Tooltip text={ currentTooltip }>{ button }</Tooltip> : button }</>;
};
BackupNowButton.propTypes = {
children: PropTypes.node,
tooltipText: PropTypes.string,
tracksEventName: PropTypes.string,
variant: PropTypes.oneOf( [ 'primary', 'secondary', 'tertiary' ] ),
weight: PropTypes.oneOf( [ 'regular', 'bold' ] ),
onClick: PropTypes.func,
};
export default BackupNowButton;
@@ -0,0 +1,83 @@
import { JetpackVaultPressBackupLogo, Testimonials } from '@automattic/jetpack-components';
import { ConnectScreenRequiredPlan } from '@automattic/jetpack-connection';
import apiFetch from '@wordpress/api-fetch';
import { useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import React from 'react';
import { useBackupProductInfo } from '../../hooks/use-backup-product-info';
import { STORE_ID } from '../../store';
import BackupPromotionBlock from '../backup-promotion';
import { BackupVideoSection } from '../backup-video-section';
import { WhyINeedVPBackup } from '../why-i-need-vp-backup';
import benGiordanoTestimonial from './assets/ben-giordano-testimonial.png';
import timFerrissTestimonial from './assets/tim-ferriss-testimonial.png';
const testimonials = [
{
quote: __(
'Millions of people depend on my site, and downtime isnt an option. Jetpack VaultPress Backup handles my site security and backups so I can focus on creation.',
'jetpack-backup-pkg'
),
author: 'Tim Ferriss',
profession: __( 'Author / Investor / Podcaster', 'jetpack-backup-pkg' ),
img: timFerrissTestimonial,
},
{
quote: __(
'Our developers use VaultPress Backup all the time. Its a oneclick way to return to where we were before things got wonky. It gives us a little emergency parachute so if were working on a customization that breaks everything, we lose minutes, not hours.',
'jetpack-backup-pkg'
),
author: 'Ben Giordano',
profession: __( 'Founder, FreshySites.com', 'jetpack-backup-pkg' ),
img: benGiordanoTestimonial,
},
];
export const BackupConnectionScreen = () => {
const APINonce = useSelect( select => select( STORE_ID ).getAPINonce(), [] );
const APIRoot = useSelect( select => select( STORE_ID ).getAPIRoot(), [] );
const registrationNonce = useSelect( select => select( STORE_ID ).getRegistrationNonce(), [] );
const { price, priceAfter } = useBackupProductInfo();
const checkSiteHasBackupProduct = useCallback(
() => apiFetch( { path: '/jetpack/v4/has-backup-plan' } ),
[]
);
return (
<>
<ConnectScreenRequiredPlan
buttonLabel={ __( 'Get VaultPress Backup', 'jetpack-backup-pkg' ) }
priceAfter={ priceAfter }
priceBefore={ price }
pricingIcon={ <JetpackVaultPressBackupLogo showText={ false } /> }
pricingTitle={ __( 'VaultPress Backup', 'jetpack-backup-pkg' ) }
title={ __( 'The best real-time WordPress backups', 'jetpack-backup-pkg' ) }
apiRoot={ APIRoot }
apiNonce={ APINonce }
registrationNonce={ registrationNonce }
from="jetpack-backup"
redirectUri="admin.php?page=jetpack-backup"
wpcomProductSlug="jetpack_backup_t1_yearly"
siteProductAvailabilityHandler={ checkSiteHasBackupProduct }
logo={ <></> }
rna
>
<BackupPromotionBlock />
</ConnectScreenRequiredPlan>
<Testimonials testimonials={ testimonials } />
<BackupVideoSection
registrationNonce={ registrationNonce }
apiRoot={ APIRoot }
apiNonce={ APINonce }
siteProductAvailabilityHandler={ checkSiteHasBackupProduct }
/>
<WhyINeedVPBackup />
</>
);
};
@@ -0,0 +1,32 @@
import { JetpackVaultPressBackupLogo } from '@automattic/jetpack-components';
import { ConnectScreen } from '@automattic/jetpack-connection';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import React from 'react';
import { STORE_ID } from '../../store';
import connectImage from './assets/connect-backup.png';
export const BackupSecondaryAdminConnectionScreen = () => {
const APINonce = useSelect( select => select( STORE_ID ).getAPINonce(), [] );
const APIRoot = useSelect( select => select( STORE_ID ).getAPIRoot(), [] );
const registrationNonce = useSelect( select => select( STORE_ID ).getRegistrationNonce(), [] );
return (
<ConnectScreen
title={ __( 'Save every change and get back online quickly', 'jetpack-backup-pkg' ) }
buttonLabel={ __( 'Log in to continue', 'jetpack-backup-pkg' ) }
apiRoot={ APIRoot }
apiNonce={ APINonce }
registrationNonce={ registrationNonce }
images={ [ connectImage ] }
from="jetpack-backup"
redirectUri="admin.php?page=jetpack-backup"
logo={ <JetpackVaultPressBackupLogo /> }
>
<p>
It looks like your site already has a backup plan activated. All you need to do is log in
with your WordPress account.
</p>
</ConnectScreen>
);
};
@@ -0,0 +1,37 @@
import { __, sprintf } from '@wordpress/i18n';
import React from 'react';
/**
* BackupPromotion component definition.
*
* @return {React.Component} BackupPromotion component.
*/
export default function BackupPromotion() {
return (
<div className="jp-backup-dashboard-promotion">
<h3>
{ __(
'VaultPress Backup is the most proven WordPress backup plugin with 270 million site backups over the last ten years.',
'jetpack-backup-pkg'
) }
</h3>
<ul className="jp-product-promote">
<li>
{ sprintf(
// translators: %s is the amount of storage.
__( 'Automated real-time backups with %s of storage', 'jetpack-backup-pkg' ),
'10 GB'
) }
</li>
<li>{ __( 'Easy one-click restores from desktop or mobile', 'jetpack-backup-pkg' ) }</li>
<li>{ __( 'Complete list of all site changes', 'jetpack-backup-pkg' ) }</li>
<li>{ __( 'Global server infrastructure', 'jetpack-backup-pkg' ) }</li>
<li>{ __( 'Best-in-class support', 'jetpack-backup-pkg' ) }</li>
<li>{ __( 'Easy to use; no developer required', 'jetpack-backup-pkg' ) }</li>
<li>
{ __( 'Backups of all WooCommerce customer and order data', 'jetpack-backup-pkg' ) }
</li>
</ul>
</div>
);
}
@@ -0,0 +1,122 @@
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import useConnection from '../../hooks/useConnection';
import { STORE_ID } from '../../store';
import { StorageAddonUpsellPrompt } from './storage-addon-upsell-prompt';
import StorageMeter from './storage-meter';
import StorageUsageDetails from './storage-usage-details';
import { getUsageLevel, StorageUsageLevels } from './storage-usage-levels';
const BackupStorageSpace = () => {
const connectionStatus = useConnection();
const isFetchingPolicies = useSelect( select => select( STORE_ID ).isFetchingBackupPolicies() );
const isFetchingSize = useSelect( select => select( STORE_ID ).isFetchingBackupSize() );
const hasBackupSizeLoaded = useSelect( select => select( STORE_ID ).hasBackupSizeLoaded() );
const hasBackupPoliciesLoaded = useSelect( select =>
select( STORE_ID ).hasBackupPoliciesLoaded()
);
const storageLimit = useSelect( select => select( STORE_ID ).getBackupStorageLimit() );
const storageSize = useSelect( select => select( STORE_ID ).getBackupSize() );
const lastBackupSize = useSelect( select => select( STORE_ID ).getLastBackupSize() );
const planRetentionDays = useSelect( select => select( STORE_ID ).getActivityLogLimitDays() );
const minDaysOfBackupsAllowed = useSelect( select =>
select( STORE_ID ).getMinDaysOfBackupsAllowed()
);
const daysOfBackupsAllowed = useSelect( select => select( STORE_ID ).getDaysOfBackupsAllowed() );
const daysOfBackupsSaved = useSelect( select => select( STORE_ID ).getDaysOfBackupsSaved() );
const showComponent = storageSize !== null && storageLimit > 0;
const usageLevel = useSelect( select => select( STORE_ID ).getStorageUsageLevel() );
const backupRetentionDays = useSelect( select => select( STORE_ID ).getBackupRetentionDays() );
const retentionDays = backupRetentionDays || planRetentionDays;
const dispatch = useDispatch( STORE_ID );
// Fetch backup policies and site size
useEffect( () => {
const connectionLoaded = 0 < Object.keys( connectionStatus ).length;
if ( ! connectionLoaded ) {
return;
}
if ( ! isFetchingPolicies && ! hasBackupPoliciesLoaded ) {
dispatch.getSitePolicies();
}
if ( ! isFetchingSize && ! hasBackupSizeLoaded ) {
dispatch.getSiteSize();
}
}, [
connectionStatus,
dispatch,
hasBackupPoliciesLoaded,
hasBackupSizeLoaded,
isFetchingPolicies,
isFetchingSize,
] );
useEffect( () => {
dispatch.setStorageUsageLevel(
getUsageLevel(
storageSize,
storageLimit,
minDaysOfBackupsAllowed,
daysOfBackupsAllowed,
retentionDays,
daysOfBackupsSaved
)
);
}, [
dispatch,
storageSize,
storageLimit,
minDaysOfBackupsAllowed,
daysOfBackupsAllowed,
retentionDays,
daysOfBackupsSaved,
] );
const sectionHeader = useMemo( () => {
if ( usageLevel === StorageUsageLevels.Full ) {
return __( 'Cloud storage full', 'jetpack-backup-pkg' );
}
if ( usageLevel === StorageUsageLevels.Critical ) {
return __( 'Cloud storage is almost full', 'jetpack-backup-pkg' );
}
return __( 'Cloud storage space', 'jetpack-backup-pkg' );
}, [ usageLevel ] );
if ( ! showComponent ) {
return null;
}
return (
showComponent && (
<>
<h2>{ sectionHeader }</h2>
<StorageMeter
storageUsed={ storageSize }
storageLimit={ storageLimit }
usageLevel={ usageLevel }
/>
<StorageUsageDetails
storageUsed={ storageSize }
storageLimit={ storageLimit }
lastBackupSize={ lastBackupSize }
usageLevel={ usageLevel }
planRetentionDays={ planRetentionDays }
/>
{ usageLevel !== StorageUsageLevels.Normal && (
<StorageAddonUpsellPrompt usageLevel={ usageLevel } />
) }
</>
)
);
};
export default BackupStorageSpace;
@@ -0,0 +1,84 @@
import { getProductCheckoutUrl } from '@automattic/jetpack-components';
import { Button } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useState, createInterpolateElement, useEffect, useCallback } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import useAnalytics from '../../../hooks/useAnalytics';
import { STORE_ID } from '../../../store';
import Price from './price';
import './style.scss';
import useAddonStorageOffer from './use-addon-storage-offer';
import useStorageStatusText from './use-storage-status-text';
export const StorageAddonUpsellPrompt = ( { usageLevel } ) => {
const addonSlug = useSelect( select => select( STORE_ID ).getStorageAddonOfferSlug() );
const { addonSizeText, addonPricing, addOnLoaded } = useAddonStorageOffer();
const siteSlug = useSelect( select => select( STORE_ID ).getCalypsoSlug() );
const adminUrl = useSelect( select => select( STORE_ID ).getSiteData().adminUrl );
const minDaysOfBackupsAllowed = useSelect( select =>
select( STORE_ID ).getMinDaysOfBackupsAllowed()
);
const daysOfBackupsSaved = useSelect( select => select( STORE_ID ).getDaysOfBackupsSaved() );
const storageStatusText = useStorageStatusText(
usageLevel,
daysOfBackupsSaved,
minDaysOfBackupsAllowed
);
const [ pricingText, setPricingText ] = useState( '' );
const { tracks } = useAnalytics();
const trackUpgradeStorageClick = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_upgrade_storage_prompt_cta', { site: siteSlug } );
}, [ tracks, siteSlug ] );
useEffect( () => {
if ( addOnLoaded ) {
setPricingText(
createInterpolateElement(
sprintf(
// translators: %1$s: Storage unit, <Price>: Additional charge.
__(
'Add %1$s additional storage for <Price />/month, billed monthly',
'jetpack-backup-pkg'
),
addonSizeText
),
{
Price: (
<Price
fullPrice={ addonPricing.full_price }
discountPrice={ addonPricing.discount_price }
currency={ addonPricing.currencyCode }
/>
),
}
)
);
}
}, [ addonSizeText, addonPricing, addOnLoaded ] );
const showUpsellPrompt = addOnLoaded;
return (
showUpsellPrompt && (
<Button
className="usage-warning__action-button has-clickable-action"
href={ getProductCheckoutUrl(
addonSlug,
siteSlug,
`${ adminUrl }admin.php?page=jetpack-backup`,
true
) }
onClick={ trackUpgradeStorageClick }
>
<div className="action-button__copy">
<div className="action-button__status">{ storageStatusText }</div>
<div className="action-button__action-text">{ pricingText }</div>
</div>
<span className="action-button__arrow">&#8594;</span>
</Button>
)
);
};
@@ -0,0 +1,17 @@
import { getCurrencyObject } from '@automattic/format-currency';
const Price = ( { fullPrice, discountedPrice, currency, hidePriceFraction } ) => {
const finalPrice = discountedPrice > 0 ? discountedPrice : fullPrice;
const { symbol, integer, fraction } = getCurrencyObject( finalPrice, currency );
const showPriceFraction = hidePriceFraction === false || ! fraction.endsWith( '00' );
return (
<span>
{ symbol }
{ integer }
{ showPriceFraction && fraction }
</span>
);
};
export default Price;
@@ -0,0 +1,60 @@
.usage-warning__storage-full {
font-size: 1rem;
font-weight: bold;
margin-bottom: 16px;
}
.usage-warning__action-button {
width: 100%;
height: auto;
margin-top: 24px;
flex-wrap: nowrap;
justify-content: space-between;
column-gap: 8px;
padding: 16px 24px;
border: 2px solid var(--jp-green);
border-radius: calc(2 * 2px);
font-size: 1rem;
color: var(--jp-gray-80);
text-align: initial;
&:visited {
color: var(--jp-gray-80);
}
&:focus,
&:hover {
cursor: initial;
background: none;
}
&.has-clickable-action:focus,
&.has-clickable-action:hover {
cursor: pointer;
.action-button__arrow {
transform: translateX(8px);
}
}
.action-button__status {
margin-bottom: 0.25rem;
}
.action-button__action-text {
font-weight: bold;
}
.action-button__arrow {
transition: transform 0.15s ease-out;
color: var(--jp-green);
font-size: var(--font-title-small);
font-weight: 700;
line-height: 24px;
}
}
@@ -0,0 +1,45 @@
import apiFetch from '@wordpress/api-fetch';
import { useDispatch, useSelect } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';
import { STORE_ID } from '../../../store';
const useAddonStorageOffer = () => {
const addonSlug = useSelect( select => select( STORE_ID ).getStorageAddonOfferSlug() );
const [ addonSizeText, setAddonSizeText ] = useState( null );
const [ addonPricing, setAddonPricing ] = useState( null );
const storageLimit = useSelect( select => select( STORE_ID ).getBackupStorageLimit() );
const storageSize = useSelect( select => select( STORE_ID ).getBackupSize() );
const dispatch = useDispatch( STORE_ID );
const [ addOnLoaded, setAddonLoaded ] = useState( false );
const fetchAddOnOffer = () =>
apiFetch( {
path: `/jetpack/v4/site/backup/addon-offer?storage_size=${ storageSize }&storage_limit=${ storageLimit }`,
} ).then(
res => {
if ( res.slug && res.pricing && res.size_text ) {
dispatch.setAddonStorageOfferSlug( res.slug );
setAddonSizeText( res.size_text );
setAddonPricing( res.pricing );
setAddonLoaded( true );
}
},
() => {
setAddonLoaded( false );
}
);
// Start the initial state fetch
useEffect( () => {
fetchAddOnOffer();
}, [ storageSize, storageLimit ] ); // eslint-disable-line react-hooks/exhaustive-deps
return {
addonSlug,
addonSizeText,
addonPricing,
addOnLoaded,
};
};
export default useAddonStorageOffer;
@@ -0,0 +1,42 @@
import { useMemo } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { StorageUsageLevels } from '../storage-usage-levels';
const useStorageStatusText = ( usageLevel, daysOfBackupsSaved, minDaysOfBackupsAllowed ) => {
return useMemo( () => {
switch ( usageLevel ) {
case StorageUsageLevels.Warning:
return __(
'You are close to reaching your storage limit. Once you do, we will delete your oldest backups to make space for new ones.',
'jetpack-backup-pkg'
);
case StorageUsageLevels.Critical:
return __(
'You are very close to reaching your storage limit. Once you do, we will delete your oldest backups to make space for new ones.',
'jetpack-backup-pkg'
);
case StorageUsageLevels.Full:
return sprintf(
/* translators: %s is a number greather than 0 that means a number of days. */
__(
'You have reached your storage limit with %s day(s) of backups saved. Backups have been stopped. Please upgrade your storage to resume backups.',
'jetpack-backup-pkg'
),
daysOfBackupsSaved
);
case StorageUsageLevels.BackupsDiscarded:
return sprintf(
/* translators: %s is a number greather than 0 that means a number of days. */
__(
'We removed your oldest backup(s) to make space for new ones. We will continue to remove old backups as needed, up to the last %s days.',
'jetpack-backup-pkg'
),
minDaysOfBackupsAllowed
);
}
return null;
}, [ usageLevel, daysOfBackupsSaved, minDaysOfBackupsAllowed ] );
};
export default useStorageStatusText;
@@ -0,0 +1,118 @@
import { Button, Gridicon, getProductCheckoutUrl } from '@automattic/jetpack-components';
import { ExternalLink, Popover } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { createInterpolateElement, useCallback, useRef, useState } from '@wordpress/element';
import { __, _n, sprintf } from '@wordpress/i18n';
import useAnalytics from '../../../hooks/useAnalytics';
import { STORE_ID } from '../../../store';
import './style.scss';
const StorageHelpPopover = ( { className, forecastInDays } ) => {
const STORAGE_USAGE_HELP_POPOVER_STATE_KEY = 'storage_usage_help_popover_state';
const popoverState = null === localStorage.getItem( STORAGE_USAGE_HELP_POPOVER_STATE_KEY );
const [ isPopoverVisible, setPopoverVisible ] = useState( popoverState );
const addonSlug = useSelect( select => select( STORE_ID ).getStorageAddonOfferSlug() );
const siteSlug = useSelect( select => select( STORE_ID ).getCalypsoSlug() );
const adminUrl = useSelect( select => select( STORE_ID ).getSiteData().adminUrl );
const popover = useRef( null );
const { tracks } = useAnalytics();
const trackUpgradeStorageClick = useCallback( () => {
tracks.recordEvent( 'jetpack_backup_upgrade_storage_prompt_from_popover_cta', {
site: siteSlug,
} );
}, [ tracks, siteSlug ] );
const toggleHelpPopover = useCallback(
event => {
setPopoverVisible( ! isPopoverVisible );
localStorage.setItem( STORAGE_USAGE_HELP_POPOVER_STATE_KEY, 'shown' );
// when the info popover inside a button, we don't want clicking it to propagate up
event.stopPropagation();
},
[ isPopoverVisible ]
);
if ( ! forecastInDays ) {
return null;
}
const storageUpgradeUrl = getProductCheckoutUrl(
addonSlug,
siteSlug,
`${ adminUrl }admin.php?page=jetpack-backup`,
true
);
const forecastStatement = sprintf(
/* translators: %d: is number of days of the forecast */
_n(
'Based on the current size of your site, Jetpack will save <strong>%d day of full backup</strong>.',
'Based on the current size of your site, Jetpack will save <strong>%d days of full backups</strong>.',
forecastInDays,
'jetpack-backup-pkg'
),
forecastInDays
);
return (
<span className={ className }>
<Button
size="small"
variant="tertiary"
className="backup-storage-space__toggle-popover"
onClick={ toggleHelpPopover }
ref={ popover }
>
<Gridicon icon="info-outline" size={ 20 } />
</Button>
{ isPopoverVisible && (
<Popover
className="backup-storage-space__popover"
position="bottom right"
context={ popover.current }
noArrow={ false }
>
<h3> { __( 'Backup archive size', 'jetpack-backup-pkg' ) }</h3>
<p>
{ createInterpolateElement( forecastStatement, {
strong: <strong />,
} ) }
<Button
size="small"
variant="tertiary"
className="backup-storage-space__close-popover"
onClick={ toggleHelpPopover }
>
<Gridicon icon="cross" size={ 18 } />
</Button>
</p>
<p>
{ createInterpolateElement(
__(
'If you need more backup days, try <link>reducing the backup size</link> or adding more storage.',
'jetpack-backup-pkg'
),
{
link: (
<ExternalLink href="https://jetpack.com/support/backup/jetpack-vaultpress-backup-storage-and-retention/#reduce-storage-size" />
),
}
) }
</p>
<div className="backup-storage-space__button-section">
<Button
variant="primary"
href={ storageUpgradeUrl }
onClick={ trackUpgradeStorageClick }
>
{ __( 'Add more storage', 'jetpack-backup-pkg' ) }
</Button>
</div>
</Popover>
) }
</span>
);
};
export default StorageHelpPopover;
@@ -0,0 +1,88 @@
.backup-storage-space {
&__help-popover {
line-height: 28px;
vertical-align: top;
display: inline-block;
}
&__popover.components-popover {
.components-popover__content {
padding: 24px;
color: var(--jp-gray-70);
background: #fff;
border-radius: 2px;
box-shadow: 0 12px 20px rgba(0, 0, 0, 0.08);
border: 1px solid #dcdcde;
text-align: start;
width: 304px;
h3 {
margin-bottom: 16px;
}
p {
font-size: 1rem;
color: var(--jp-gray-80);
}
hr {
margin-bottom: 0.25rem;
}
p > a {
text-decoration: underline;
}
}
}
&__toggle-popover {
&.components-button {
padding: 0px 4px !important;
height: auto !important;
box-shadow: none !important;
background: transparent;
.gridicon {
color: var(--jp-gray-30);
}
}
&.components-button:focus {
box-shadow: none !important;
background: transparent;
}
}
&__close-popover {
position: absolute;
right: 24px;
top: 24px;
&.components-button {
height: 24px;
width: 24px;
padding: 0 !important;
color: var(--jp-gray-50);
box-shadow: none !important;
span {
height: 18px;
border: none;
}
}
&.components-button:hover {
border: 0 !important;
}
}
&__button-section {
display: flex;
justify-content: flex-end;
margin-top: 24px;
.button {
padding: 8px 24px;
}
}
}
@@ -0,0 +1,26 @@
import { ProgressBar } from '@automattic/jetpack-components';
import { StorageUsageLevels } from '../storage-usage-levels';
import './style.scss';
const StorageMeter = ( { storageUsed, storageLimit, usageLevel } ) => {
const STORAGE_METER_CLASS_NAMES = {
[ StorageUsageLevels.Full ]: 'full-warning',
[ StorageUsageLevels.Critical ]: 'red-warning',
[ StorageUsageLevels.Warning ]: 'yellow-warning',
[ StorageUsageLevels.Normal ]: 'no-warning',
[ StorageUsageLevels.BackupsDiscarded ]: 'full-warning',
};
return (
<>
<div className="backup-storage-space__progress-bar">
<ProgressBar
className={ [ 'progress-bar', STORAGE_METER_CLASS_NAMES[ usageLevel ] ] }
progressClassName={ 'progress-bar__progress' }
progress={ ( storageUsed ?? 0 ) / ( storageLimit ?? Infinity ) }
/>
</div>
</>
);
};
export default StorageMeter;
@@ -0,0 +1,57 @@
.backup-storage-space__progress-bar {
height: 24px;
line-height: 0;
margin-top: 24px;
margin-bottom: 16px;
.progress-bar {
height: 100%;
border-radius: 12px; /* stylelint-disable-line scales/radii */
background-color: var(--jp-gray-5);
.progress-bar__progress {
// Unless we're 100% full, the left side of the bar
// is rounded and the right side is flat
/* stylelint-disable-next-line scales/radii */
border-radius: 12px 0 0 12px;
// We always expect some amount of used storage,
// so keep a border-radius sized buffer so
// the left side of the bar looks correctly rounded
min-width: 12px;
// Only allow full width if storage is full;
// otherwise, leave a border-radius sized buffer,
// so the right side looks okay without a radius
max-width: calc(100% - 12px);
}
&.no-warning {
.progress-bar__progress {
background-color: var(--jp-black);;
}
}
&.yellow-warning {
.progress-bar__progress {
background-color: var(--jp-yellow-20);
}
}
&.red-warning {
.progress-bar__progress {
background-color: var(--jp-red-40);
}
}
// When the bar is full, we can show the filled portion
// at full width, with a rounded right side
&.full-warning {
.progress-bar__progress {
max-width: initial;
background-color: var(--jp-red-40);
border-radius: 12px; /* stylelint-disable-line scales/radii */
}
}
}
}
@@ -0,0 +1,70 @@
import './style.scss';
import { getRedirectUrl } from '@automattic/jetpack-components';
import { ExternalLink } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { createInterpolateElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { STORE_ID } from '../../../store';
import StorageHelpPopover from '../storage-help-popover';
import { StorageUsageLevels } from '../storage-usage-levels';
import { useStorageUsageText } from './use-storage-usage-text';
const StorageUsageDetails = ( {
storageUsed,
storageLimit,
lastBackupSize,
planRetentionDays,
usageLevel,
onClickedPurchase,
} ) => {
const domain = useSelect( select => select( STORE_ID ).getCalypsoSlug() );
const usageText = useStorageUsageText( storageUsed, storageLimit );
const daysOfBackupsSaved = useSelect( select => select( STORE_ID ).getDaysOfBackupsSaved() );
let forecastInDays = 0;
if ( storageLimit > 0 && lastBackupSize > 0 ) {
forecastInDays = Math.floor( storageLimit / lastBackupSize );
}
const singularDaysOfBackupLabel = __( '<a>1 day of backups saved</a>', 'jetpack-backup-pkg' );
const pluralDaysOfBackupLabel = sprintf(
/* translators: %s: Number of days of backups saved. */
__( '<a>%s days of backups saved</a>', 'jetpack-backup-pkg' ),
daysOfBackupsSaved
);
return (
<>
<div className="backup-storage-space__meta">
<div className="backup-storage-space__usage-text">
{ usageText }
{
// Show popover only when usage level is normal, for other levels,
// we already show separate message with CTA under progress bar
forecastInDays < planRetentionDays && StorageUsageLevels.Normal === usageLevel && (
<StorageHelpPopover
className="backup-storage-space__help-popover"
forecastInDays={ forecastInDays }
onClickedPurchase={ onClickedPurchase }
/>
)
}
</div>
<div className="backup-storage-space__retention">
{ createInterpolateElement(
daysOfBackupsSaved === 1 ? singularDaysOfBackupLabel : pluralDaysOfBackupLabel,
{
a: (
<ExternalLink
href={ getRedirectUrl( 'backup-plugin-storage-backups-saved', { site: domain } ) }
/>
),
}
) }
</div>
</div>
</>
);
};
export default StorageUsageDetails;
@@ -0,0 +1,25 @@
@import '@automattic/jetpack-base-styles/style';
.backup-storage-space__meta {
display: flex;
justify-content: space-between;
@include for-phone-down {
flex-direction: column;
gap: 4px;
}
div, a {
color: var(--jp-gray-80);
font-weight: 400;
line-height: 20px;
}
.backup-storage-space__usage-text {
font-size: 16px;
}
.backup-storage-space__retention a {
font-size: 14px;
}
}
@@ -0,0 +1,62 @@
import { useMemo, createInterpolateElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
const StorageUnits = {
Gigabyte: 2 ** 30,
Terabyte: 2 ** 40,
};
const getAppropriateStorageUnit = bytes => {
if ( bytes < StorageUnits.Terabyte ) {
return StorageUnits.Gigabyte;
}
return StorageUnits.Terabyte;
};
const bytesToUnit = ( bytes, unit ) => bytes / unit;
export const useStorageUsageText = ( bytesUsed, bytesAvailable ) => {
return useMemo( () => {
if ( bytesUsed === undefined ) {
return null;
}
const usedGigabytes = bytesToUnit( bytesUsed, StorageUnits.Gigabyte );
if ( bytesAvailable === undefined ) {
// translators: Must use unit abbreviation; describes an amount of storage space in gigabytes (e.g., 15.4GB used)
return sprintf( __( '%1$dGB used', 'jetpack-backup-pkg' ), usedGigabytes );
}
const availableUnit = getAppropriateStorageUnit( bytesAvailable );
const availableUnitAmount = bytesToUnit( bytesAvailable, availableUnit );
if ( availableUnit === StorageUnits.Gigabyte ) {
return createInterpolateElement(
// eslint-disable-next-line @wordpress/valid-sprintf
sprintf(
// translators: Must use unit abbreviation; describes used vs available storage amounts (e.g., 20.0GB of 30GB used, 0.5GB of 20GB used)
__( 'Using <strong>%1.1fGB</strong> of %2fGB', 'jetpack-backup-pkg' ),
usedGigabytes,
availableUnitAmount
),
{
strong: <strong />,
}
);
}
return createInterpolateElement(
sprintf(
// translators: Must use unit abbreviation; describes used vs available storage amounts (e.g., 20.0GB of 1TB used, 0.5GB of 2TB used)
__( 'Using <strong>%1$dGB</strong> of %2$dTB', 'jetpack-backup-pkg' ),
usedGigabytes,
availableUnitAmount
),
{
strong: <strong />,
}
);
}, [ bytesUsed, bytesAvailable ] );
};
@@ -0,0 +1,69 @@
export type StorageUsageLevelName = 'Full' | 'Critical' | 'Warning' | 'Normal' | 'BackupsDiscarded';
export const StorageUsageLevels: Record< StorageUsageLevelName, StorageUsageLevelName > = {
Full: 'Full',
Critical: 'Critical',
Warning: 'Warning',
Normal: 'Normal',
BackupsDiscarded: 'BackupsDiscarded',
} as const;
const THRESHOLDS: Record< number, StorageUsageLevelName > = {
100: StorageUsageLevels.Full,
80: StorageUsageLevels.Critical,
65: StorageUsageLevels.Warning,
0: StorageUsageLevels.Normal,
};
const THRESHOLD_VALUES = Object.keys( THRESHOLDS )
.map( Number )
// Sorting from highest to lowest is important for getUsageLevel,
// because it looks at the elements *in order*.
.sort( ( a, b ) => b - a );
export const getUsageLevel = (
used: number | undefined,
available: number | undefined,
minDaysOfBackupsAllowed: number,
daysOfBackupsAllowed: number,
retentionDays: number,
daysOfBackupsSaved: number
): StorageUsageLevelName | null => {
if ( available === undefined || used === undefined ) {
return null;
}
if ( available === null || used === null ) {
return null;
}
if (
!! minDaysOfBackupsAllowed &&
!! daysOfBackupsAllowed &&
!! retentionDays &&
!! daysOfBackupsSaved
) {
// if current days of backups saved is less than or equal to the minimum and storage is overlimit.
if (
minDaysOfBackupsAllowed >= daysOfBackupsSaved &&
used > 0 &&
available > 0 &&
used >= available
) {
return StorageUsageLevels.Full;
}
// if current allowed days of backups is less than plan's retention days, that means
// we discarded some backups to make other fit in current storage limit.
if ( daysOfBackupsAllowed < retentionDays ) {
return StorageUsageLevels.BackupsDiscarded;
}
}
// Guard against divide-by-zero
if ( available === 0 ) {
return StorageUsageLevels.Normal;
}
const percentUsed = ( 100 * used ) / available;
const thresholdValue = THRESHOLD_VALUES.find( value => percentUsed >= value ) ?? 0;
return THRESHOLDS[ thresholdValue ];
};
@@ -0,0 +1,79 @@
import { ActionButton, getRedirectUrl } from '@automattic/jetpack-components';
import { useProductCheckoutWorkflow, useConnection } from '@automattic/jetpack-connection';
import { createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import React from 'react';
import backupVideoThumbnail from './images/jetpack-backup-video-thumbnail.png';
import './style.scss';
const BackupVideoSection = ( {
siteProductAvailabilityHandler,
apiRoot,
apiNonce,
registrationNonce,
} ) => {
const { run: handleCheckoutWorkflow, hasCheckoutStarted } = useProductCheckoutWorkflow( {
productSlug: 'jetpack_backup_t1_yearly',
redirectUrl: 'admin.php?page=jetpack-backup',
siteProductAvailabilityHandler,
from: 'jetpack-backup',
} );
const { siteIsRegistering, userIsConnecting, isOfflineMode, registrationError } = useConnection( {
registrationNonce: registrationNonce,
redirectUri: 'admin.php?page=jetpack-backup',
apiRoot: apiRoot,
apiNonce: apiNonce,
autoTrigger: false,
from: 'jetpack-backup',
} );
const errorMessage = isOfflineMode
? createInterpolateElement( __( 'Unavailable in <a>Offline Mode</a>', 'jetpack-backup-pkg' ), {
a: (
<a
href={ getRedirectUrl( 'jetpack-support-development-mode' ) }
target="_blank"
rel="noopener noreferrer"
/>
),
} )
: undefined;
const buttonIsLoading = siteIsRegistering || userIsConnecting || hasCheckoutStarted;
const displayButtonError = Boolean( registrationError );
return (
<div className="jp-backup-video-section">
<div className="jp-backup-video-section__content">
<h2>{ __( 'Take a walkthrough of VaultPress Backup', 'jetpack-backup-pkg' ) }</h2>
<p>
{ __(
'Save every change and get back online quickly with one-click restores.',
'jetpack-backup-pkg'
) }
</p>
<ActionButton
label={ __( 'Get VaultPress Backup', 'jetpack-backup-pkg' ) }
onClick={ handleCheckoutWorkflow }
displayError={ displayButtonError }
errorMessage={ errorMessage }
isLoading={ buttonIsLoading }
isDisabled={ isOfflineMode }
/>
</div>
<video poster={ backupVideoThumbnail } controls>
<source
src="https://videos.files.wordpress.com/VNRR7Mkj/audio_jetpack_backup_-3.mov"
type="video/mp4"
/>
</video>
</div>
);
};
export { BackupVideoSection };
@@ -0,0 +1,68 @@
.jp-backup-video-section {
margin-top: 1.5rem;
padding: calc(var(--spacing-base) * 3);
background-color: var(--jp-white);
border-radius: var(--jp-border-radius);
box-shadow: 0 0 40px rgba(0, 0, 0, 0.08);
.jp-backup-video-section__content {
h2 {
margin-bottom: 1.5rem;
font-size: var(--font-title-large);
font-weight: 700;
}
p {
margin-bottom: 3rem;
font-weight: 500;
font-size: var(--font-title-medium);
line-height: 1.2;
color: var(--jp-black);
}
.jp-action-button--button.components-button {
margin-bottom: 3rem;
padding: 0.875rem 1.5rem;
width: 100%;
max-width: 375px;
font-weight: 500;
font-size: 18px;
}
}
video {
width: clamp( 400px, 500px, 100% );
}
@media (min-width: 600px) {
padding: calc(var(--spacing-base) * 8);
}
@media (min-width: 1080px) {
display: flex;
align-items: flex-start;
gap: 2rem;
.jp-backup-video-section__content {
.jp-action-button--button.components-button {
margin-bottom: 0;
}
}
video {
max-width: 400px;
}
}
@media (min-width: 1200px ) {
gap: 4rem;
video {
max-width: 500px;
}
}
}

Some files were not shown because too many files have changed in this diff Show More