updated plugin Jetpack Protect version 5.0.0

This commit is contained in:
2026-06-03 21:29:05 +00:00
committed by Gitium
parent 0490bb3940
commit af21e84842
399 changed files with 17749 additions and 5470 deletions

View File

@ -5,9 +5,290 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.35.0] - 2026-04-06
### Added
- Sync: Add documentation_category to taxonomy allowlist. [#47951]
## [4.34.0] - 2026-03-30
### Added
- Add 'note' to the default whitelisted comment types and centralize the list in the Defaults class. [#47746]
### Fixed
- Users: Use the effective role when the role-change payload is empty, and avoid syncing empty roles. [#47754]
## [4.33.0] - 2026-03-23
### Added
- Add a filter to customize the actor. [#47634]
- Instant Search: Add article-type taxonomy. [#47595]
- Jetpack Sync: Add page_condition taxonomy to approved sync list. [#47691]
### Changed
- Reduce debug_backtrace overhead in Users sync module. [#47658]
- Sync: Improve checksum performance for meta tables. [#47579]
### Fixed
- Prevent incremental sync for posts with unregistered post types. [#47580]
## [4.32.1] - 2026-03-16
### Changed
- Add `wpcom_newsletter_send_default` site option to control whether posts are sent to subscribers by default. [#47564]
## [4.32.0] - 2026-03-09
### Added
- `REST_Endpoints`: Add `use_memory_limit` argument to the checkout endpoint, enabling memory-based queue checkout that skips item count validation. [#47338]
## [4.31.0] - 2026-03-02
### Added
- Add `protocol` to the comment meta whitelist for syncing. [#47379]
### Changed
- Documentation: Update variable type in docblock to match reality. [#47346]
### Fixed
- `REST_Endpoints`: Fix sync health check to verify queue size and lag before allowing status to be set to `in_sync`. [#47323]
## [4.30.0] - 2026-02-26
### Added
- REST_Endpoints: Add `clear-queue` endpoint to allow clearing a Sync queue via REST API. [#47303]
### Changed
- Full Sync: Store `last_item` in status to avoid re-running an expensive query on every invocation. [#47292]
- Listener: Add per-request in-memory cache to avoid repeated `get_transient` calls when checking queue state. [#47282]
### Fixed
- Full Sync: Fix chunk size adjustment when sync is stuck to correctly preserve adjusted chunk size and stuck count across invocations. [#47293]
## [4.29.0] - 2026-02-23
### Added
- Add MCP information to the actor. [#47223]
### Fixed
- Listener: Replace redundant `filter_var` with `sanitize_text_field` for user agent sanitization. [#47244]
## [4.28.0] - 2026-02-19
### Added
- Add WooCommerce store address details (`address`, `address_2`, `city`, `postcode`) and admin install timestamp to sync whitelist for improved WooCommerce site settings sync. [#47192]
- Add triggering action name to data sent with `jetpack_sync_data_loss` action. [#47161]
## [4.27.0] - 2026-02-10
### Changed
- Ensure we sync directly during cron. [#46887]
## [4.26.0] - 2026-02-02
### Added
- Introduce Sync blacklist for `jetpack_options`. [#46852]
### Changed
- Stop syncing deleted_post_meta actions when deleting posts. [#46775]
### Removed
- Stop syncing `updating_jetpack_version` actions to WordPress.com. [#46762]
## [4.25.2] - 2026-01-26
### Changed
- Move installation and update failure actions to shutdown to decrease their associated lag. [#46723]
- Search Sync Settings: Add some more meta fields for WooCommerce.com products to allow list. [#46739]
- Stats Module: Send Heartbeat data immediately. [#46725]
- Stop sending redundant actions when Dedicated Sync fails to enable. [#46700]
### Fixed
- Prevent warnings when unexpected data is passed into filters or added meta consumed by Sync. [#46637]
## [4.25.1] - 2026-01-19
### Added
- Add _wpas_connection_overrides and _wpas_customize_per_network to post meta whitelist for Jetpack Social. [#46563]
## [4.25.0] - 2026-01-14
### Added
- Instant Search: Add 'guest' custom taxonomy for Sync. [#46562]
- Introduce Sync actions blacklist. [#46496]
### Fixed
- Prevent attempting to sync modules that we are not ready to sync. [#46475]
## [4.24.2] - 2025-12-22
### Changed
- Search Sync Settings: Add WooCommerce.com product search keywords meta to allow list. [#46290]
- General cleanup and clarity added in a couple of module files. [#46318]
### Removed
- Remove an option from the sync whitelist as it is not in use. [#46305]
## [4.24.1] - 2025-12-15
### Changed
- Internal updates.
## [4.24.0] - 2025-12-11
### Deprecated
- Jetpack Sync: Deprecate WooCommerce_Products module [#46222]
## [4.23.0] - 2025-12-08
### Changed
- Jetpack Sync: Update Dedicated Sync locking logic for spawning requests. [#45813]
### Removed
- Jetpack Sync: Stop syncing `woocommerce_update_order_item` WooCommerce action. [#46217]
### Fixed
- Ensure proper flags are used with `json_encode()`. [#46092]
## [4.22.6] - 2025-12-01
### Changed
- Sync: Only send a single updated_post_meta action per attachment metadata request. [#45921]
## [4.22.5] - 2025-11-20
### Fixed
- Improve compatibility with old WooCommerce versions. [#46003]
- Phan: Address PhanPossiblyUndeclaredVariable violations. [#45911]
## [4.22.4] - 2025-11-10
### Changed
- Sync: Ignore the `shop_order_placehold` post-type from Woo. [#45772]
- Sync: Prevent unnecessary sync actions from running via Jetpack CRM's WooSync. [#45761]
- Tests: Ensure PHP 8.5 compatibility. [#45769]
## [4.22.3] - 2025-11-03
### Added
- Ensure order item meta is only synced if it is in the whitelist. [#45684]
### Changed
- Enable WPCOM REST API feature by default. [#45672]
- Only enqueue a single HPOS order save event per request. [#45697]
## [4.22.2] - 2025-10-27
### Changed
- Internal updates.
## [4.22.1] - 2025-10-10
### Changed
- Handle lazy-loading of `WP_User` object properties. [#45450]
## [4.22.0] - 2025-09-19
### Added
- Instant Search: Add additional WC attribute taxonomies for Sync. [#45211]
### Changed
- Jetpack Sync: Add 'is_connected' property to synced users. [#45210]
## [4.21.3] - 2025-09-15
### Changed
- Jetpack Sync: Stop triggering 'jetpack_wp_login' actions with empty user ID. [#45132]
## [4.21.2] - 2025-09-08
### Changed
- Internal updates.
## [4.21.1] - 2025-09-02
### Changed
- Remove unnecessary filter for deleted WooCommerce products in the WooCommerce_Products module. [#45011]
## [4.21.0] - 2025-09-01
### Changed
- Add product sync checksum support and include trash/auto-draft post statuses in queries. [#44951]
- Sync: Ensure proper data type before setting plugin action links in callables. [#44975]
### Fixed
- Fix WooCommerce product sync to handle trashed products. [#44993]
## [4.20.0] - 2025-08-25
### Changed
- Enhance WooCommerce Products Sync to include post-related fields. [#44810]
- Sync: Add default value when updating sidebar widgets via Themes module. [#44878]
## [4.19.0] - 2025-08-18
### Added
- Add WooCommerce product meta lookup sync module. [#44601]
## [4.18.2] - 2025-08-14
### Changed
- Internal updates.
## [4.18.1] - 2025-08-11
### Fixed
- Prevent PHP errors when directly accessing various files. [#44646]
## [4.18.0] - 2025-08-05
### Changed
- Full Sync: Ensure the full sync limits values are integers, no matter what is stored in Settings. [#44626]
- Full Sync: Update chunk_size default full sync limit to 500 for posts module. [#44609]
### Fixed
- Prevent PHP fatal during login. [#44620]
## [4.17.0] - 2025-08-04
### Removed
- Remove custom environment_type handling. [#44576]
## [4.16.0] - 2025-07-28
### Changed
- Full Sync: Adjust chunk size in case full sync gets stuck. [#44454]
- Full Sync: For stuck adjustment, consider started time of full sync to avoid checks for different full syncs. [#44491]
## [4.15.2] - 2025-07-21
### Changed
- Internal updates.
## [4.15.1] - 2025-07-08
### Added
- Sync: Do not sync the ActivityPub outbox CPT. [#44222]
## [4.15.0] - 2025-07-07
### Added
- Sync: Validate objects in sync hooks to avoid PHP warnings. [#44183]
## [4.14.2] - 2025-06-27
### Changed
- Sync: Add taxonomies as empty array when using `get_terms` for better support of plugins/themes. [#44104]
- Updates: Prevent fatals when data is not in expected array format. [#44005]
## [4.14.1] - 2025-06-03
### Fixed
- Sync: Ensure HPOS Orders are retrieved in Descending Order [#43744]
## [4.14.0] - 2025-06-02
### Added
- Sync: Send context in `jetpack_full_sync_end` action. [#43589]
## [4.13.0] - 2025-05-15
### Changed
- Full Sync: Calculate metadata based on entire item, not just value. [#43421]
- Sync: Add support for 'sandbox' environment type. [#43430]
## [4.12.0] - 2025-05-05
### Added
- Full Sync: Ensure procesing of start, end, and cancelled actions. [#43287]
### Changed
- Full Sync: Update status properly when sending non-chunked full sync actions. [#43229]
### Fixed
- Jetpack Sync: Fix checksum support for 'woocommerce_order_itemeta' table. [#43281]
## [4.11.1] - 2025-04-28
### Changed
- Jetpack Sync: Switch `jetpack_package_version` to a callable. [#43188]
### Fixed
- Posts: Prevent deprecation warning when updating post meta with null values. [#43149]
- Sync: Avoid error on some invalid REST API requests when using PHP 8. [#41662]
## [4.11.0] - 2025-04-21
### Added
- Sync: Add `jetpack_sync_active_modules` callable to whitelist of must-sync callables. [#43134]
## [4.10.1] - 2025-04-14
### Changed
- Internal updates.
## [4.10.0] - 2025-03-31
### Changed
- Full Sync: Calculate ranges for all chunked modules. [#42692]
- Full Sync: Ensure comment type whitelisting is used when full syncing. [#42777]
- Full Sync: Use term_id to determine ranges in terms module. [#42740]
## [4.9.2] - 2025-03-21
### Added
- Sync: Consider blacklisted taxonomies when doing full sync for term relationships [#42618]
- Sync: Consider blacklisted taxonomies when doing full sync for term relationships. [#42618]
## [4.9.1] - 2025-03-17
### Changed
@ -1175,7 +1456,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- WPCOM Block Editor: Update meta key name
- Resolve PHP Warning with array_filter usage in sync of action_links.
- Sync: Seperate theme data ( name, version, slug and uri) from theme support data
- Sync: Separate theme data ( name, version, slug and uri) from theme support data
- Replaced intval() with (int) as part of issue #17432.
- Replaced strval() with type casting (string) as part of issue #17432.
- Replaced floatval() with type cast (float) as part of issue #17432.
@ -1436,6 +1717,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Packages: Move sync to a classmapped package
[4.35.0]: https://github.com/Automattic/jetpack-sync/compare/v4.34.0...v4.35.0
[4.34.0]: https://github.com/Automattic/jetpack-sync/compare/v4.33.0...v4.34.0
[4.33.0]: https://github.com/Automattic/jetpack-sync/compare/v4.32.1...v4.33.0
[4.32.1]: https://github.com/Automattic/jetpack-sync/compare/v4.32.0...v4.32.1
[4.32.0]: https://github.com/Automattic/jetpack-sync/compare/v4.31.0...v4.32.0
[4.31.0]: https://github.com/Automattic/jetpack-sync/compare/v4.30.0...v4.31.0
[4.30.0]: https://github.com/Automattic/jetpack-sync/compare/v4.29.0...v4.30.0
[4.29.0]: https://github.com/Automattic/jetpack-sync/compare/v4.28.0...v4.29.0
[4.28.0]: https://github.com/Automattic/jetpack-sync/compare/v4.27.0...v4.28.0
[4.27.0]: https://github.com/Automattic/jetpack-sync/compare/v4.26.0...v4.27.0
[4.26.0]: https://github.com/Automattic/jetpack-sync/compare/v4.25.2...v4.26.0
[4.25.2]: https://github.com/Automattic/jetpack-sync/compare/v4.25.1...v4.25.2
[4.25.1]: https://github.com/Automattic/jetpack-sync/compare/v4.25.0...v4.25.1
[4.25.0]: https://github.com/Automattic/jetpack-sync/compare/v4.24.2...v4.25.0
[4.24.2]: https://github.com/Automattic/jetpack-sync/compare/v4.24.1...v4.24.2
[4.24.1]: https://github.com/Automattic/jetpack-sync/compare/v4.24.0...v4.24.1
[4.24.0]: https://github.com/Automattic/jetpack-sync/compare/v4.23.0...v4.24.0
[4.23.0]: https://github.com/Automattic/jetpack-sync/compare/v4.22.6...v4.23.0
[4.22.6]: https://github.com/Automattic/jetpack-sync/compare/v4.22.5...v4.22.6
[4.22.5]: https://github.com/Automattic/jetpack-sync/compare/v4.22.4...v4.22.5
[4.22.4]: https://github.com/Automattic/jetpack-sync/compare/v4.22.3...v4.22.4
[4.22.3]: https://github.com/Automattic/jetpack-sync/compare/v4.22.2...v4.22.3
[4.22.2]: https://github.com/Automattic/jetpack-sync/compare/v4.22.1...v4.22.2
[4.22.1]: https://github.com/Automattic/jetpack-sync/compare/v4.22.0...v4.22.1
[4.22.0]: https://github.com/Automattic/jetpack-sync/compare/v4.21.3...v4.22.0
[4.21.3]: https://github.com/Automattic/jetpack-sync/compare/v4.21.2...v4.21.3
[4.21.2]: https://github.com/Automattic/jetpack-sync/compare/v4.21.1...v4.21.2
[4.21.1]: https://github.com/Automattic/jetpack-sync/compare/v4.21.0...v4.21.1
[4.21.0]: https://github.com/Automattic/jetpack-sync/compare/v4.20.0...v4.21.0
[4.20.0]: https://github.com/Automattic/jetpack-sync/compare/v4.19.0...v4.20.0
[4.19.0]: https://github.com/Automattic/jetpack-sync/compare/v4.18.2...v4.19.0
[4.18.2]: https://github.com/Automattic/jetpack-sync/compare/v4.18.1...v4.18.2
[4.18.1]: https://github.com/Automattic/jetpack-sync/compare/v4.18.0...v4.18.1
[4.18.0]: https://github.com/Automattic/jetpack-sync/compare/v4.17.0...v4.18.0
[4.17.0]: https://github.com/Automattic/jetpack-sync/compare/v4.16.0...v4.17.0
[4.16.0]: https://github.com/Automattic/jetpack-sync/compare/v4.15.2...v4.16.0
[4.15.2]: https://github.com/Automattic/jetpack-sync/compare/v4.15.1...v4.15.2
[4.15.1]: https://github.com/Automattic/jetpack-sync/compare/v4.15.0...v4.15.1
[4.15.0]: https://github.com/Automattic/jetpack-sync/compare/v4.14.2...v4.15.0
[4.14.2]: https://github.com/Automattic/jetpack-sync/compare/v4.14.1...v4.14.2
[4.14.1]: https://github.com/Automattic/jetpack-sync/compare/v4.14.0...v4.14.1
[4.14.0]: https://github.com/Automattic/jetpack-sync/compare/v4.13.0...v4.14.0
[4.13.0]: https://github.com/Automattic/jetpack-sync/compare/v4.12.0...v4.13.0
[4.12.0]: https://github.com/Automattic/jetpack-sync/compare/v4.11.1...v4.12.0
[4.11.1]: https://github.com/Automattic/jetpack-sync/compare/v4.11.0...v4.11.1
[4.11.0]: https://github.com/Automattic/jetpack-sync/compare/v4.10.1...v4.11.0
[4.10.1]: https://github.com/Automattic/jetpack-sync/compare/v4.10.0...v4.10.1
[4.10.0]: https://github.com/Automattic/jetpack-sync/compare/v4.9.2...v4.10.0
[4.9.2]: https://github.com/Automattic/jetpack-sync/compare/v4.9.1...v4.9.2
[4.9.1]: https://github.com/Automattic/jetpack-sync/compare/v4.9.0...v4.9.1
[4.9.0]: https://github.com/Automattic/jetpack-sync/compare/v4.8.4...v4.9.0

View File

@ -9,24 +9,23 @@ 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
along with this program; if not, see <https://www.gnu.org/licenses/>.
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
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.
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
Preamble
The licenses for most software are designed to take away your
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
@ -36,48 +35,48 @@ 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
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
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
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
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
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
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
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
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"
@ -94,7 +93,7 @@ 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
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
@ -105,29 +104,29 @@ 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
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.
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.
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.)
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,
@ -149,26 +148,26 @@ 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,
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,
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,
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.)
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
@ -187,7 +186,7 @@ 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
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.
@ -195,7 +194,7 @@ 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
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
@ -204,7 +203,7 @@ 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
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
@ -212,7 +211,7 @@ 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
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
@ -244,7 +243,7 @@ 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
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
@ -252,7 +251,7 @@ 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
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.
@ -265,7 +264,7 @@ 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
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
@ -273,9 +272,9 @@ 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
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS 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
@ -285,7 +284,7 @@ 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
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
@ -295,45 +294,44 @@ 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
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
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 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>
<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 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.
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.
You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.
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.
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
@ -344,11 +342,11 @@ 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.
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
<signature of Moe Ghoul>, 1 April 1989
Moe Ghoul, 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

View File

@ -5,20 +5,20 @@
"license": "GPL-2.0-or-later",
"require": {
"php": ">=7.2",
"automattic/jetpack-connection": "^6.7.7",
"automattic/jetpack-constants": "^3.0.5",
"automattic/jetpack-password-checker": "^0.4.7",
"automattic/jetpack-ip": "^0.4.6",
"automattic/jetpack-roles": "^3.0.5",
"automattic/jetpack-status": "^5.0.10"
"automattic/jetpack-connection": "^8.2.0",
"automattic/jetpack-constants": "^3.0.8",
"automattic/jetpack-password-checker": "^0.4.9",
"automattic/jetpack-ip": "^0.4.10",
"automattic/jetpack-roles": "^3.0.8",
"automattic/jetpack-status": "^6.1.2"
},
"require-dev": {
"automattic/jetpack-changelogger": "^6.0.2",
"yoast/phpunit-polyfills": "^3.0.0",
"automattic/jetpack-changelogger": "^6.0.14",
"yoast/phpunit-polyfills": "^4.0.0",
"automattic/jetpack-search": "@dev",
"automattic/jetpack-waf": "@dev",
"automattic/jetpack-waf": "^0.28.2",
"automattic/jetpack-test-environment": "@dev",
"automattic/phpunit-select-config": "^1.0.1"
"automattic/phpunit-select-config": "^1.0.4"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
@ -52,7 +52,7 @@
"link-template": "https://github.com/Automattic/jetpack-sync/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "4.9.x-dev"
"dev-trunk": "4.35.x-dev"
},
"dependencies": {
"test-only": [

View File

@ -175,6 +175,7 @@ class Actions {
) ) {
self::initialize_sender();
add_action( 'shutdown', array( self::$sender, 'do_sync' ), 9998 );
if ( self::should_initialize_sender( true ) ) {
add_action( 'shutdown', array( self::$sender, 'do_full_sync' ), 9999 );
}
@ -362,6 +363,10 @@ class Actions {
$queue = self::$sender->get_sync_queue();
$full_queue = self::$sender->get_full_sync_queue();
// We are sending the expiry vs the actual dedicated lock value to ensure backwards compatibility
// with previous versions where the lock value was a timestamp.
$dedicated_sync_lock_option_name = Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
$dedicated_sync_lock_expires_name = $dedicated_sync_lock_option_name . '_expires';
$debug['debug_details']['sync_locks'] = array(
'retry_time_sync' => get_option( self::RETRY_AFTER_PREFIX . 'sync' ),
@ -370,7 +375,7 @@ class Actions {
'next_sync_time_full_sync' => self::$sender->get_next_sync_time( 'full_sync' ),
'queue_locked_sync' => $queue->is_locked(),
'queue_locked_full_sync' => $full_queue->is_locked(),
'dedicated_sync_request_lock' => \Jetpack_Options::get_raw_option( Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null ),
'dedicated_sync_request_lock' => \Jetpack_Options::get_raw_option( $dedicated_sync_lock_expires_name, null ),
'dedicated_sync_temporary_disable_flag' => get_transient( Dedicated_Sender::DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG ),
);
@ -666,11 +671,9 @@ class Actions {
*/
public static function jetpack_cron_schedule( $schedules ) {
if ( ! isset( $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] ) ) {
$minutes = (int) ( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 );
$display = ( 1 === $minutes ) ?
__( 'Every minute', 'jetpack-sync' ) :
/* translators: %d is an integer indicating the number of minutes. */
sprintf( __( 'Every %d minutes', 'jetpack-sync' ), $minutes );
$minutes = ( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 );
/* translators: %d is an integer indicating the number of minutes. */
$display = sprintf( __( 'Every %d minutes', 'jetpack-sync' ), $minutes );
$schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] = array(
'interval' => self::DEFAULT_SYNC_CRON_INTERVAL_VALUE,
'display' => $display,
@ -686,7 +689,45 @@ class Actions {
* @static
*/
public static function do_cron_sync() {
self::do_cron_sync_by_type( 'sync' );
if ( ! self::sync_allowed() ) {
return;
}
self::initialize_sender();
$time_limit = Settings::get_setting( 'cron_sync_time_limit' );
$start_time = time();
$executions = 0;
$lock_id = Dedicated_Sender::try_lock_spawn_request();
do {
$next_sync_time = self::$sender->get_next_sync_time( 'sync' );
if ( $next_sync_time ) {
$delay = $next_sync_time - time() + 1;
if ( $delay > 15 ) {
break;
} elseif ( $delay > 0 ) {
sleep( (int) $delay );
}
}
$result = self::$sender->do_sync_and_set_delays( self::$sender->get_sync_queue() );
if ( is_wp_error( $result ) && in_array( $result->get_error_code(), array( 'unclosed_buffer', 'sync_throttled' ), true ) ) {
$result = true; // Give it some time.
}
// # of send actions performed.
++$executions;
} while ( $result && ! is_wp_error( $result ) && ( $start_time + $time_limit ) > time() );
if ( $lock_id ) {
Dedicated_Sender::try_release_lock_spawn_request( $lock_id );
}
return $executions;
}
/**
@ -696,7 +737,31 @@ class Actions {
* @static
*/
public static function do_cron_full_sync() {
self::do_cron_sync_by_type( 'full_sync' );
if ( ! self::sync_allowed() ) {
return;
}
self::initialize_sender();
$executions = 0;
$next_sync_time = self::$sender->get_next_sync_time( 'full_sync' );
if ( $next_sync_time ) {
$delay = $next_sync_time - time() + 1;
if ( $delay > 15 ) {
return;
} elseif ( $delay > 0 ) {
sleep( (int) $delay );
}
}
// Explicitly only allow 1 do_full_sync call until issue with Immediate Full Sync is resolved.
// For more context see p1HpG7-9pe-p2.
self::$sender->do_full_sync();
++$executions;
return $executions;
}
/**
@ -728,7 +793,7 @@ class Actions {
if ( $delay > 15 ) {
break;
} elseif ( $delay > 0 ) {
sleep( $delay );
sleep( (int) $delay );
}
}
@ -857,6 +922,23 @@ class Actions {
return $sync_modules;
}
/**
* Adds Woo's Products sync module to existing modules for sending.
*
* Note: This module is currently used for WooCommerce Analytics only.
*
* @param array $sync_modules The list of sync modules declared prior to this filter.
*
* @access public
* @static
*
* @return array A list of sync modules that now includes Woo's Products module.
*/
public static function add_woocommerce_products_sync_module( $sync_modules ) {
$sync_modules[] = 'Automattic\\Jetpack\\Sync\\Modules\\WooCommerce_Products';
return $sync_modules;
}
/**
* Initializes sync for WP Super Cache.
*
@ -1127,7 +1209,11 @@ class Actions {
delete_option( self::RETRY_AFTER_PREFIX . 'sync' );
delete_option( self::RETRY_AFTER_PREFIX . 'full_sync' );
// Dedicated sync locks.
\Jetpack_Options::delete_raw_option( Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME );
$dedicated_sync_lock_option = Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
$dedicated_sync_lock_expires_option = $dedicated_sync_lock_option . '_expires';
\Jetpack_Options::delete_raw_option( $dedicated_sync_lock_option );
\Jetpack_Options::delete_raw_option( $dedicated_sync_lock_expires_option );
delete_transient( Dedicated_Sender::DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG );
// Lock for disabling Sync sending temporarily.
delete_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME );
@ -1155,7 +1241,7 @@ class Actions {
"\n",
array_map(
function ( $key, $value ) {
return wp_json_encode( array( $key => $value ) );
return wp_json_encode( array( $key => $value ), JSON_UNESCAPED_SLASHES );
},
array_keys( (array) $data ),
array_values( (array) $data )
@ -1184,7 +1270,7 @@ class Actions {
}
if ( $response_code !== 200 || false === isset( $decoded_response['processed_items'] ) ) {
if ( is_array( $decoded_response ) && isset( $decoded_response['code'] ) && isset( $decoded_response['message'] ) ) {
if ( isset( $decoded_response['code'] ) && isset( $decoded_response['message'] ) ) {
return new WP_Error(
'jetpack_sync_send_error_' . $decoded_response['code'],
$decoded_response['message'],

View File

@ -30,12 +30,14 @@ class Data_Settings {
'get_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins' ),
'get_themes' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_themes' ),
'jetpack_connection_active_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_connection_active_plugins' ),
'jetpack_package_versions' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_package_versions' ),
'paused_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_paused_plugins' ),
'paused_themes' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_paused_themes' ),
'timezone' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_timezone' ),
'wp_get_environment_type' => 'wp_get_environment_type',
'wp_max_upload_size' => 'wp_max_upload_size',
'wp_version' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ),
'jetpack_sync_active_modules' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_sync_active_modules' ),
),
'jetpack_sync_constants_whitelist' => array(
'ABSPATH',
@ -72,10 +74,6 @@ class Data_Settings {
'jetpack_sync_settings_dedicated_sync_enabled',
'jetpack_sync_settings_custom_queue_table_enabled',
'jetpack_sync_settings_wpcom_rest_api_enabled',
/**
* Connection related options
*/
'jetpack_package_versions',
/**
* Generic site options
*/

View File

@ -43,7 +43,7 @@ class Dedicated_Sender {
*
* 5 seconds as default value seems sane, but we might want to adjust that in the future.
*/
const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 5;
const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 60;
/**
* The query parameter name to use when passing the current lock id.
@ -97,7 +97,7 @@ class Dedicated_Sender {
* We want to check it too, to make sure we managed to cover more cases and be more certain we actually
* catch calls to the endpoint.
*/
if ( ! isset( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['rest_route'] ) || ! is_string( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
return false;
}
@ -133,6 +133,10 @@ class Dedicated_Sender {
return new WP_Error( 'empty_queue_' . $queue->id );
}
if ( get_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME ) ) {
return new WP_Error( 'sender_temporarily_disabled_while_pulling' );
}
// Return early if we've gotten a retry-after header response that is not expired.
$retry_time = get_option( Actions::RETRY_AFTER_PREFIX . $queue->id );
if ( $retry_time && $retry_time >= microtime( true ) ) {
@ -201,41 +205,50 @@ class Dedicated_Sender {
* To avoid spawning multiple requests at the same time, we need to have a quick lock that will
* allow only a single request to continue if we try to spawn multiple at the same time.
*
* @return false|mixed|string
* @return string|false
*/
public static function try_lock_spawn_request() {
$current_microtime = (string) microtime( true );
$option_name = self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
$expires_name = $option_name . '_expires';
$ttl = self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT;
$lock_id = wp_generate_uuid4();
$now = microtime( true );
// Fast path: external object cache is atomic.
if ( wp_using_ext_object_cache() ) {
if ( true !== wp_cache_add( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, $current_microtime, 'jetpack', self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT ) ) {
// Cache lock has been claimed already.
return false;
if ( wp_cache_add( $option_name, $lock_id, 'jetpack', $ttl ) ) {
return $lock_id;
}
return false; // Worker already active
}
$current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
global $wpdb;
if ( ! empty( $current_lock_value ) ) {
// Check if time has passed to overwrite the lock - min 5s?
if ( is_numeric( $current_lock_value ) && ( ( $current_microtime - $current_lock_value ) < self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT ) ) {
// Still in previous lock, quit
return false;
}
// If the value is not numeric (float/current time), we want to just overwrite it and continue.
// 1) Check & clear expired lock (best effort; failure here is harmless)
$expiry = (float) \Jetpack_Options::get_raw_option( $expires_name, 0 );
if ( ! $expiry || $expiry < $now ) {
// Either missing (edge case) or expired → clean up
\Jetpack_Options::delete_raw_option( $option_name );
\Jetpack_Options::delete_raw_option( $expires_name );
}
// Update. We don't want it to autoload, as we want to fetch it right before the checks.
\Jetpack_Options::update_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, $current_microtime, false );
// Give some time for the update to happen
usleep( wp_rand( 1000, 3000 ) );
// 2) Atomic acquisition: INSERT IGNORE (succeeds only if the lock doesn't exist)
$inserted = $wpdb->query( // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching --- Ensure atomicity.
$wpdb->prepare(
"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload )
VALUES ( %s, %s, 'no' )",
$option_name,
maybe_serialize( $lock_id )
)
);
$updated_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
if ( $updated_value === $current_microtime ) {
return $current_microtime;
if ( $inserted ) {
// 3) We own the lock — store expiry separately
\Jetpack_Options::update_raw_option( $expires_name, $now + $ttl, false );
return $lock_id; // Success
}
// Lock already present → normal state → do not spawn
return false;
}
@ -253,18 +266,24 @@ class Dedicated_Sender {
}
// If it's still not a valid lock_id, throw an error and let the lock process figure it out.
if ( empty( $lock_id ) || ! is_numeric( $lock_id ) ) {
if ( empty( $lock_id ) ) {
return new WP_Error( 'dedicated_request_lock_invalid', 'Invalid lock_id supplied for unlock' );
}
if ( wp_using_ext_object_cache() ) {
if ( (string) $lock_id === wp_cache_get( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack', true ) ) {
$cached = wp_cache_get( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack', true );
if ( (string) $lock_id === $cached ) {
wp_cache_delete( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack' );
return true;
}
return false;
}
// If this is the flow that has the lock, let's release it so we can spawn other requests afterwards
$current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
if ( (string) $lock_id === $current_lock_value ) {
\Jetpack_Options::delete_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME );
return true;
@ -280,7 +299,7 @@ class Dedicated_Sender {
*/
public static function get_request_lock_id_from_request() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) || ! is_numeric( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
return null;
}
@ -397,6 +416,13 @@ class Dedicated_Sender {
}
}
$current_setting = Settings::is_dedicated_sync_enabled();
// No need to update if current setting matches header value.
if ( $current_setting === (bool) $dedicated_sync_enabled ) {
return $current_setting;
}
Settings::update_settings(
array(
'dedicated_sync_enabled' => $dedicated_sync_enabled,

View File

@ -185,6 +185,7 @@ class Defaults {
'wp_page_for_privacy_policy',
'wpcom_ai_site_prompt',
'wpcom_classic_early_release',
'wpcom_newsletter_send_default',
'wpcom_featured_image_in_email',
'jetpack_gravatar_in_email',
'jetpack_author_in_email',
@ -204,7 +205,6 @@ class Defaults {
'jetpack_subscriptions_from_name',
'jetpack_verbum_subscription_modal',
'jetpack_blocks_disabled',
'jetpack_package_versions',
'jetpack_newsletters_publishing_default_frequency',
'jetpack_scheduled_plugins_update',
'jetpack_waf_automatic_rules',
@ -357,6 +357,7 @@ class Defaults {
'wp_version' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ),
'active_modules' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_active_modules' ),
'jetpack_connection_active_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_connection_active_plugins' ),
'jetpack_package_versions' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_package_versions' ),
'jetpack_sync_active_modules' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_sync_active_modules' ),
);
@ -467,6 +468,8 @@ class Defaults {
'secupress_log_err404', // SecuPress Plugin - Log 404 pages
'iw_omnibus_price_log', // Omnibus Plugin - Log price changes.
'od_url_metrics', // Optimization Detective - Log URL metrics.
'ap_outbox', // ActivityPub Outbox; only used for broadcasting ActivityPub activity to followers.
'shop_order_placehold', // WooCommerce placeholder - Used to maintain compatibility and references when switching between WP Posts-based order storage and the newer HPOS tables.
);
/**
@ -773,6 +776,8 @@ class Defaults {
'_wp_page_template',
'_wp_trash_meta_comments_status',
'_wpas_feature_enabled',
'_wpas_connection_overrides',
'_wpas_customize_per_network',
'_wpas_mess',
'_wpas_options',
'advanced_seo_description', // Jetpack_SEO_Posts::DESCRIPTION_META_KEY.
@ -820,6 +825,7 @@ class Defaults {
'hc_foreign_user_id',
'hc_post_as',
'hc_wpcom_id_sig',
'protocol',
);
/**
@ -841,6 +847,32 @@ class Defaults {
return apply_filters( 'jetpack_sync_comment_meta_whitelist', self::$comment_meta_whitelist );
}
/**
* Comment types whitelist.
*
* @var array Comment types that are synced.
*/
public static $comment_types_whitelist = array( '', 'comment', 'trackback', 'pingback', 'review', 'note' );
/**
* Get the comment types whitelist.
*
* @return array
*/
public static function get_comment_types_whitelist() {
/**
* Comment types present in this list will be synced to WordPress.com.
*
* @module sync
*
* @since 1.6.3
* @since-jetpack 7.6.0
*
* @param array A list of comment types.
*/
return apply_filters( 'jetpack_sync_whitelisted_comment_types', self::$comment_types_whitelist );
}
/**
* Default theme support whitelist.
*
@ -1185,6 +1217,13 @@ class Defaults {
*/
public static $default_comment_meta_whitelist = array();
/**
* Default for sync actions blacklist.
*
* @var array Empty array.
*/
public static $default_sync_actions_blacklist = array();
/**
* Default for disabling sync across the site.
*
@ -1329,7 +1368,7 @@ class Defaults {
'max_chunks' => 10,
),
'posts' => array(
'chunk_size' => 100,
'chunk_size' => 500,
'max_chunks' => 1,
),
'term_relationships' => array(
@ -1371,7 +1410,16 @@ class Defaults {
/**
* Default for enabling wpcom rest api for Sync.
*
* @var int Bool-ish. Default 0.
* @var int Bool-ish. Default 1.
*/
public static $default_wpcom_rest_api_enabled = 0;
public static $default_wpcom_rest_api_enabled = 1;
/**
* A list of 'jetpack_options' specific keys we want to ignore.
*
* @var array
*/
public static $jetpack_options_blacklist = array(
'last_heartbeat',
);
}

View File

@ -71,7 +71,7 @@ class Functions {
public static function sanitize_taxonomy( $taxonomy ) {
// Lets clone the taxonomy object instead of modifing the global one.
$cloned_taxonomy = json_decode( wp_json_encode( $taxonomy ) );
$cloned_taxonomy = json_decode( wp_json_encode( $taxonomy, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
// recursive taxonomies are no fun.
if ( $cloned_taxonomy === null ) {
@ -146,7 +146,7 @@ class Functions {
$post_type_object->add_rewrite_rules();
$post_type_object->add_hooks();
$post_type_object->register_taxonomies();
return (object) $post_type_object;
return $post_type_object;
}
/**
@ -534,7 +534,7 @@ class Functions {
$formatted_gmt_offset = str_replace(
array( '.25', '.5', '.75' ),
array( ':15', ':30', ':45' ),
(string) $formatted_gmt_offset
$formatted_gmt_offset
);
/* translators: %s is UTC offset, e.g. "+1" */
@ -604,7 +604,19 @@ class Functions {
*/
public static function json_wrap( &$any, $seen_nodes = array() ) {
if ( is_object( $any ) ) {
$input = get_object_vars( $any );
$input = get_object_vars( $any );
// WordPress 6.9 introduced lazy-loading of WP_User `roles`, `caps`, and `allcaps` properties.
// It also made said properties protected, so we need to access them and set them as keys manually.
if ( $any instanceof \WP_User ) {
$roles = $any->roles;
$caps = $any->caps;
$allcaps = $any->allcaps;
$input['roles'] = $roles;
$input['caps'] = $caps;
$input['allcaps'] = $allcaps;
}
$input['__o'] = 1;
} else {
$input = &$any;
@ -737,4 +749,15 @@ class Functions {
$modules = array_map( 'wp_normalize_path', $modules );
return $modules;
}
/**
* Return the list of Jetpack package versions.
*
* @since 4.11.1
*
* @return array
*/
public static function get_jetpack_package_versions() {
return apply_filters( 'jetpack_package_versions', array() );
}
}

View File

@ -174,6 +174,24 @@ class Health {
}
}
/**
* Check if the sync queue is healthy based on size and lag.
*
* The queue is considered healthy if either size OR lag is within its limit.
* This matches the logic in Listener::can_add_to_queue(): data loss only occurs
* when both limits are exceeded simultaneously.
*
* @param int $queue_size Current queue size.
* @param float $queue_lag Current queue lag in seconds.
* @param int $max_queue_size Maximum allowed queue size.
* @param float $max_queue_lag Maximum allowed queue lag in seconds.
* @return bool Whether the queue is healthy.
*/
public static function is_queue_healthy( $queue_size, $queue_lag, $max_queue_size, $max_queue_lag ) {
return ( $queue_size < $max_queue_size )
|| ( $queue_lag < $max_queue_lag );
}
/**
* Update Sync Status if Full Sync ended of Posts
*

View File

@ -8,6 +8,10 @@
namespace Automattic\Jetpack\Sync;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses gzip's DEFLATE
* algorithm to compress objects serialized using json_encode
@ -57,7 +61,7 @@ class JSON_Deflate_Array_Codec implements Codec_Interface {
* @return false|string
*/
protected function json_serialize( $any ) {
return wp_json_encode( Functions::json_wrap( $any ) );
return wp_json_encode( Functions::json_wrap( $any ), JSON_UNESCAPED_SLASHES );
}
/**

View File

@ -15,8 +15,9 @@ use Automattic\Jetpack\Roles;
* This class monitors actions and logs them to the queue to be sent.
*/
class Listener {
const QUEUE_STATE_CHECK_TRANSIENT = 'jetpack_sync_last_checked_queue_state';
const QUEUE_STATE_CHECK_TIMEOUT = 30; // 30 seconds.
const QUEUE_STATE_CHECK_TRANSIENT = 'jetpack_sync_last_checked_queue_state';
const QUEUE_STATE_CHECK_TIMEOUT = 30; // 30 seconds.
const REQUEST_STATE_CACHE_INVALIDATE_AFTER = 50; // Number of queue adds per request before invalidating the cache.
/**
* Sync queue.
@ -46,6 +47,23 @@ class Listener {
*/
private $sync_queue_lag_limit;
/**
* Per-request in-memory cache of queue state.
* Avoids repeated get_transient() calls for every
* can_add_to_queue check within a single request.
*
* @var array<string, array{int, float}>
*/
private $request_queue_state_cache = array();
/**
* Count of successful adds per queue within this request, used to decide when
* to invalidate the in-memory cache so the size estimate stays accurate.
*
* @var array<string, int>
*/
private $request_adds_count = array();
/**
* Singleton implementation.
*
@ -92,9 +110,6 @@ class Listener {
add_action( 'jetpack_activate_module', $handler );
add_action( 'jetpack_deactivate_module', $handler );
// Jetpack Upgrade.
add_action( 'updating_jetpack_version', $handler, 10, 2 );
// Send periodic checksum.
add_action( 'jetpack_sync_checksum', $handler );
}
@ -151,6 +166,8 @@ class Listener {
public function force_recheck_queue_limit() {
delete_transient( self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $this->sync_queue->id );
delete_transient( self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $this->full_sync_queue->id );
$this->request_queue_state_cache = array();
$this->request_adds_count = array();
}
/**
@ -167,6 +184,18 @@ class Listener {
return false;
}
// Per-request in-memory cache: avoids a get_transient() call on every can_add_to_queue
// check within the same request when many actions are enqueued.
if ( isset( $this->request_queue_state_cache[ $queue->id ] ) ) {
list( $queue_size, $queue_age ) = $this->request_queue_state_cache[ $queue->id ];
return Health::is_queue_healthy(
$queue_size + ( $this->request_adds_count[ $queue->id ] ?? 0 ) + 1,
$queue_age,
$this->sync_queue_size_limit,
$this->sync_queue_lag_limit
);
}
$state_transient_name = self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $queue->id;
$queue_state = get_transient( $state_transient_name );
@ -176,11 +205,18 @@ class Listener {
set_transient( $state_transient_name, $queue_state, self::QUEUE_STATE_CHECK_TIMEOUT );
}
// Populate in-memory cache from the transient result so subsequent calls this request
// don't need to hit the object cache or DB at all.
$this->request_queue_state_cache[ $queue->id ] = $queue_state;
list( $queue_size, $queue_age ) = $queue_state;
return ( $queue_age < $this->sync_queue_lag_limit )
||
( ( $queue_size + 1 ) < $this->sync_queue_size_limit );
return Health::is_queue_healthy(
$queue_size + 1,
$queue_age,
$this->sync_queue_size_limit,
$this->sync_queue_lag_limit
);
}
/**
@ -280,6 +316,21 @@ class Listener {
return;
}
// Skip enqueueing any Sync action when triggered by Jetpack CRM Woo Sync background job to avoid periodic noise.
if (
( function_exists( 'doing_action' ) && doing_action( 'jpcrm_woosync_sync' ) )
|| defined( 'jpcrm_woosync_running' )
|| defined( 'jpcrm_woosync_cron_running' )
) {
return;
}
// Skip enqueueing if current action is blacklisted.
$sync_actions_blacklist = Settings::get_setting( 'sync_actions_blacklist' );
if ( is_array( $sync_actions_blacklist ) && in_array( $current_filter, $sync_actions_blacklist, true ) ) {
return;
}
/**
* Add an action hook to execute when anything on the whitelist gets sent to the queue to sync.
*
@ -311,7 +362,7 @@ class Listener {
*/
if ( ! $this->can_add_to_queue( $queue ) ) {
if ( 'sync' === $queue->id ) {
$this->sync_data_loss( $queue );
$this->sync_data_loss( $queue, $current_filter );
}
return;
}
@ -359,6 +410,16 @@ class Listener {
);
}
// Track how many items have been added to this queue during this request and periodically
// invalidate the in-memory queue-state cache so the size/lag estimate stays accurate.
// Without this, a burst that fills the queue mid-request would keep passing can_add_to_queue
// with a stale "queue is fine" result captured at the start of the request.
$this->request_adds_count[ $queue->id ] = ( $this->request_adds_count[ $queue->id ] ?? 0 ) + 1;
if ( $this->request_adds_count[ $queue->id ] >= self::REQUEST_STATE_CACHE_INVALIDATE_AFTER ) {
unset( $this->request_queue_state_cache[ $queue->id ] );
$this->request_adds_count[ $queue->id ] = 0;
}
// since we've added some items, let's try to load the sender so we can send them as quickly as possible.
if ( ! Actions::$sender ) {
add_filter( 'jetpack_sync_sender_should_load', __NAMESPACE__ . '\Actions::should_initialize_sender_enqueue', 10, 1 );
@ -371,10 +432,13 @@ class Listener {
/**
* Sync Data Loss Handler
*
* @param Queue $queue Sync queue.
* Sends a single 'jetpack_sync_data_loss' action to WP.com with timestamp, queue_size, queue_lag and current_filter.
*
* @param Queue $queue Sync queue.
* @param string $current_filter Name of action that triggered sync.
* @return boolean was send successful
*/
public function sync_data_loss( $queue ) {
public function sync_data_loss( $queue, $current_filter = '' ) {
if ( ! Settings::is_sync_enabled() ) {
return;
}
@ -388,6 +452,9 @@ class Listener {
'timestamp' => microtime( true ),
'queue_size' => $queue->size(),
'queue_lag' => $queue->lag(),
'extra' => array(
'current_filter' => $current_filter,
),
);
$sender = Sender::get_instance();
@ -432,7 +499,56 @@ class Listener {
$ip = IP_Utils::get_ip();
$actor['ip'] = $ip ? $ip : '';
$actor['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : 'unknown';
$actor['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : 'unknown';
}
$raw_mcp_header = '';
if ( isset( $_SERVER['HTTP_X_WPCOM_MCP'] ) && is_string( $_SERVER['HTTP_X_WPCOM_MCP'] ) ) {
$raw_mcp_header = trim( wp_unslash( $_SERVER['HTTP_X_WPCOM_MCP'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization happens below.
}
if ( ! empty( $raw_mcp_header ) && preg_match( '/^[A-Za-z0-9+\/=]+$/', $raw_mcp_header ) ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding MCP header payload.
$decoded = base64_decode( $raw_mcp_header, true );
if ( false !== $decoded ) {
$mcp_data = json_decode( $decoded, true );
if ( is_array( $mcp_data ) ) {
if ( isset( $mcp_data['mcp_client_name'] ) && is_string( $mcp_data['mcp_client_name'] ) ) {
$actor['mcp_client_name'] = sanitize_text_field( $mcp_data['mcp_client_name'] );
}
if ( isset( $mcp_data['mcp_client_version'] ) && is_string( $mcp_data['mcp_client_version'] ) ) {
$actor['mcp_client_version'] = sanitize_text_field( $mcp_data['mcp_client_version'] );
}
if ( ! empty( $actor['mcp_client_name'] ) || ! empty( $actor['mcp_client_version'] ) ) {
$actor['is_mcp_agent'] = true;
}
}
}
}
/**
* Filters the actor data attached to sync events.
*
* Actor data identifies who or what triggered a sync event (user info,
* request context, MCP client details, etc.) and is sent alongside every
* event to WordPress.com.
*
* @since 4.33.0
*
* @param array $actor Associative array of actor information.
*/
$actor = apply_filters( 'jetpack_sync_actor_data', $actor );
// Ensure the filter returns a valid array.
if ( ! is_array( $actor ) ) {
$actor = array();
}
// Sanitize string values added via the filter.
foreach ( $actor as $key => $value ) {
if ( is_string( $value ) ) {
$actor[ $key ] = sanitize_text_field( $value );
}
}
return $actor;

View File

@ -12,7 +12,7 @@ namespace Automattic\Jetpack\Sync;
*/
class Package_Version {
const PACKAGE_VERSION = '4.9.2';
const PACKAGE_VERSION = '4.35.0';
const PACKAGE_SLUG = 'sync';

View File

@ -121,7 +121,7 @@ class Queue {
}
/**
* Get the front-most item on the queue without checking it out.
* Get the front-most items on the queue without checking them out.
*
* @param int $count Number of items to return when looking at the items.
*
@ -136,6 +136,22 @@ class Queue {
return array();
}
/**
* Get the last-added items on the queue without checking them out.
*
* @param int $count Number of items to return when looking at the items.
*
* @return array
*/
public function peek_newest( $count = 1 ) {
$items = $this->fetch_items( $count, 'DESC' );
if ( $items ) {
return Utils::get_item_values( $items );
}
return array();
}
/**
* Gets items with particular IDs.
*
@ -245,15 +261,40 @@ class Queue {
}
/**
* Pop elements from the queue.
* Remove the oldest items from the queue.
*
* @param int $limit Number of items to pop from the queue.
* @param int $limit Number of items to remove from the queue.
*
* @return array|object|null
*/
public function pop( $limit ) {
$items = $this->fetch_items( $limit );
if ( ! $items ) {
return array();
}
$ids = $this->get_ids( $items );
$this->delete( $ids );
return $items;
}
/**
* Remove the newest items from the queue.
*
* @param int $limit Number of items to remove from the queue.
*
* @return array|object|null
*/
public function pop_newest( $limit ) {
$items = $this->fetch_items( $limit, 'DESC' );
if ( ! $items ) {
return array();
}
$ids = $this->get_ids( $items );
$this->delete( $ids );
@ -329,9 +370,8 @@ class Queue {
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while ( ( $current_item = array_shift( $current_items ) ) !== null ) {
// @codingStandardsIgnoreStart
$current_item->value = unserialize( $current_item->value );
$current_item->value = @unserialize( $current_item->value );
// @codingStandardsIgnoreEnd
$items[] = $current_item;
}
}
@ -610,11 +650,15 @@ class Queue {
* Return the items in the queue.
*
* @param null|int $limit Limit to the number of items we fetch at once.
* @param string $order Sort direction for the items. Accepts 'ASC' or 'DESC'.
* Any other value will be treated as 'ASC'.
*
* @return array|object|null
*/
private function fetch_items( $limit = null ) {
$items = $this->queue_storage->fetch_items( $limit );
private function fetch_items( $limit = null, $order = 'ASC' ) {
$order = 'DESC' === $order ? 'DESC' : 'ASC';
$items = $this->queue_storage->fetch_items( $limit, $order );
return $this->unserialize_values( $items );
}

View File

@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Replicastore\Table_Checksum_Users;
use Exception;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* An implementation of Replicastore Interface which returns data stored in a WordPress.org DB.
* This is useful to compare values in the local WP DB to values in the synced replica store
@ -869,6 +873,7 @@ class Replicastore implements Replicastore_Interface {
if ( ! $t || is_wp_error( $t ) ) {
return $t;
}
// @phan-suppress-next-line PhanAccessMethodInternal @phan-suppress-current-line UnusedSuppression -- Fixed in WP 6.9, but then we need a suppression for the WP 6.8 compat run. @todo Remove this suppression when we drop WP <6.9.
return get_terms( $taxonomy );
}
@ -1289,7 +1294,7 @@ class Replicastore implements Replicastore_Interface {
try {
$range_edges = $checksum_table->get_range_edges( $start_id, $end_id );
} catch ( Exception $ex ) {
return new WP_Error( 'invalid_range_edges', '[' . $start_id . '-' . $end_id . ']: ' . $ex->getMessage() );
return new WP_Error( 'invalid_range_edges', '[' . ( $start_id ?? 'null' ) . '-' . ( $end_id ?? 'null' ) . ']: ' . $ex->getMessage() );
}
if ( $only_range_edges ) {

View File

@ -77,7 +77,7 @@ class REST_Endpoints {
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'fields' => array(
'description' => __( 'Comma seperated list of additional fields that should be included in status.', 'jetpack-sync' ),
'description' => __( 'Comma-separated list of additional fields that should be included in status.', 'jetpack-sync' ),
'type' => 'string',
'required' => false,
),
@ -175,6 +175,43 @@ class REST_Endpoints {
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::checkout',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'queue' => array(
'description' => __( 'Name of Sync queue.', 'jetpack-sync' ),
'type' => 'string',
'required' => false,
),
'number_of_items' => array(
'description' => __( 'Number of items to checkout from the queue.', 'jetpack-sync' ),
'type' => 'integer',
'required' => false,
),
'pop' => array(
'description' => __( 'Pop items from the queue instead of checking out.', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
),
'force' => array(
'description' => __( 'Force unlock the queue before checkout.', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
),
'encode' => array(
'description' => __( 'Encode the items before sending.', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
'default' => true,
'sanitize_callback' => 'rest_sanitize_boolean',
),
'use_memory_limit' => array(
'description' => __( 'Use memory-based checkout instead of fixed item count.', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
),
),
)
);
@ -332,6 +369,17 @@ class REST_Endpoints {
'permission_callback' => __CLASS__ . '::verify_default_permissions',
)
);
// Clear Sync queue.
register_rest_route(
'jetpack/v4',
'/sync/clear-queue',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::clear_queue',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
)
);
}
/**
@ -470,21 +518,53 @@ class REST_Endpoints {
/**
* Update Sync health.
*
* IN_SYNC is only set if the incremental queue is within size and lag limits.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
* @return \WP_REST_Response|WP_Error
*/
public static function sync_health( $request ) {
$requested_status = $request->get_param( 'status' );
switch ( $request->get_param( 'status' ) ) {
switch ( $requested_status ) {
case Health::STATUS_IN_SYNC:
// Only allow setting IN_SYNC if the incremental queue is healthy.
$sync_queue = Listener::get_instance()->get_sync_queue();
$queue_size = $sync_queue->size();
$queue_lag = $sync_queue->lag();
$queue_healthy = Health::is_queue_healthy(
$queue_size,
$queue_lag,
Settings::get_setting( 'max_queue_size' ),
Settings::get_setting( 'max_queue_lag' )
);
if ( ! $queue_healthy ) {
Health::update_status( Health::STATUS_OUT_OF_SYNC );
return rest_ensure_response(
array(
'success' => Health::get_status(),
'message' => 'Sync queue is not healthy (size and lag over limit). Status not set to in_sync.',
'queue_size' => $queue_size,
'queue_lag' => $queue_lag,
)
);
}
Health::update_status( $requested_status );
break;
case Health::STATUS_OUT_OF_SYNC:
Health::update_status( $request->get_param( 'status' ) );
Health::update_status( $requested_status );
break;
default:
return new WP_Error( 'invalid_status', 'Invalid Sync Status Provided.' );
return new WP_Error(
'invalid_status',
'Invalid Sync Status Provided.',
array(
'status' => 400,
)
);
}
// re-fetch so we see what's really being stored.
@ -617,9 +697,16 @@ class REST_Endpoints {
return $queue_name;
}
$number_of_items = $args['number_of_items'];
if ( $number_of_items < 1 || $number_of_items > 100 ) {
return new WP_Error( 'invalid_number_of_items', 'Number of items needs to be an integer that is larger than 0 and less then 100', 400 );
$use_memory_limit = ! empty( $args['use_memory_limit'] );
if ( $use_memory_limit && ! empty( $args['pop'] ) ) {
return new WP_Error( 'invalid_args', 'pop cannot be used with use_memory_limit', 400 );
}
if ( ! $use_memory_limit ) {
if ( empty( $args['number_of_items'] ) || $args['number_of_items'] < 1 || $args['number_of_items'] > 100 ) {
return new WP_Error( 'invalid_number_of_items', 'Number of items needs to be an integer that is larger than 0 and up to 100', 400 );
}
}
// REST Sender.
@ -629,7 +716,8 @@ class REST_Endpoints {
return rest_ensure_response( $sender->immediate_full_sync_pull() );
}
$response = $sender->queue_pull( $queue_name, $number_of_items, $args );
$number_of_items = $use_memory_limit ? null : $args['number_of_items'];
$response = $sender->queue_pull( $queue_name, $number_of_items, $args );
// Disable sending while pulling.
if ( ! is_wp_error( $response ) ) {
set_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME, time(), Sender::TEMP_SYNC_DISABLE_TRANSIENT_EXPIRY );
@ -810,6 +898,27 @@ class REST_Endpoints {
);
}
/**
* Clear the Sync queue.
*
* @since 4.30.0
*
* @return \WP_REST_Response
*/
public static function clear_queue() {
$queue = new Queue( 'sync' );
$queue->reset();
// Re-enable sending in case it was temporarily disabled during a pull.
delete_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME );
return rest_ensure_response(
array(
'success' => true,
)
);
}
/**
* Verify that request has default permissions to perform sync actions.
*

View File

@ -26,9 +26,9 @@ class REST_Sender {
/**
* Checkout objects from the queue
*
* @param string $queue_name Name of Queue.
* @param int $number_of_items Number of Items.
* @param array $args arguments.
* @param string $queue_name Name of Queue.
* @param int|null $number_of_items Number of Items. Null for memory-based checkout.
* @param array $args Request arguments. Supports 'pop', 'force', 'encode', and 'use_memory_limit'.
*
* @return array|WP_Error
*/
@ -117,22 +117,30 @@ class REST_Sender {
/**
* Checkout items out of the sync queue.
*
* @param Queue $queue Sync Queue.
* @param int $number_of_items Number of items to checkout.
* @param Queue $queue Sync Queue.
* @param int|null $number_of_items Number of items to checkout. When null, uses memory-based checkout with default settings.
*
* @return WP_Error
* @return Queue_Buffer|WP_Error
*/
protected function get_buffer( $queue, $number_of_items ) {
$start = time();
$max_duration = 5; // this will try to get the buffer.
$buffer = $queue->checkout( $number_of_items );
$use_memory_limit = null === $number_of_items;
$memory_limit = $use_memory_limit ? Settings::get_setting( 'dequeue_max_bytes' ) : null;
$max_rows = $use_memory_limit ? Settings::get_setting( 'upload_max_rows' ) : null;
$buffer = $use_memory_limit
? $queue->checkout_with_memory_limit( $memory_limit, $max_rows )
: $queue->checkout( $number_of_items );
$duration = time() - $start;
while ( is_wp_error( $buffer ) && $duration < $max_duration ) {
sleep( 2 );
$duration = time() - $start;
$buffer = $queue->checkout( $number_of_items );
$buffer = $use_memory_limit
? $queue->checkout_with_memory_limit( $memory_limit, $max_rows )
: $queue->checkout( $number_of_items );
}
if ( false === $buffer ) {

View File

@ -358,7 +358,10 @@ class Sender {
* @return boolean|WP_Error True if this sync sending was successful, error object otherwise.
*/
public function do_sync() {
if ( ! Settings::is_dedicated_sync_enabled() ) {
// Sync directly during cron. We are doing this because otherwise
// the dedicated sync flow would be spawning HTTP requests during cron shutdown,
// which can be unreliable and cause sync lag for time-sensitive events like updates.
if ( ! Settings::is_dedicated_sync_enabled() || Settings::is_doing_cron() ) {
$result = $this->do_sync_and_set_delays( $this->sync_queue );
} else {
$result = Dedicated_Sender::spawn_sync( $this->sync_queue );
@ -418,12 +421,12 @@ class Sender {
session_write_close();
}
// Output not used right now. Try to release dedicated sync lock
Dedicated_Sender::try_release_lock_spawn_request();
// Actually try to send Sync events.
$result = $this->do_sync_and_set_delays( $this->sync_queue );
// Output not used right now. Try to release dedicated sync lock
Dedicated_Sender::try_release_lock_spawn_request();
// If no errors occurred, re-spawn a dedicated Sync request.
if ( true === $result ) {
Dedicated_Sender::spawn_sync( $this->sync_queue );
@ -497,8 +500,10 @@ class Sender {
if ( 'wpcom_error' === $sync_result->get_error_code() ) {
$this->set_next_sync_time( time() + self::WPCOM_ERROR_SYNC_DELAY, $queue->id );
}
} elseif ( $exceeded_sync_wait_threshold ) {
// If we actually sent data and it took a while, wait before sending again.
} elseif ( $exceeded_sync_wait_threshold && ! Settings::is_doing_cron() ) {
// If a send was slow, briefly pause before the next one.
// Applies only to Dedicated/Normal Sync to avoid impacting user traffic;
// cron jobs are exempt.
$this->set_next_sync_time( time() + $this->get_sync_wait_time(), $queue->id );
}

View File

@ -139,7 +139,7 @@ class Server {
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param token The token object of the misbehaving site
* @param object|null $token The token object of the misbehaving site
*/
do_action( 'jetpack_sync_multi_request_fail', $token );
@ -155,7 +155,8 @@ class Server {
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param array Array of actions received from the remote site
* @param array $events Array of actions received from the remote site
* @param object|null $token The auth token used to invoke the API
*/
do_action( 'jetpack_sync_remote_actions', $events, $token );
@ -168,14 +169,14 @@ class Server {
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param string $action_name The name of the action executed on the remote site
* @param array $args The arguments passed to the action
* @param int $user_id The external_user_id who did the action
* @param bool $silent Whether the item was created via import
* @param double $timestamp Timestamp (in seconds) when the action occurred
* @param double $sent_timestamp Timestamp (in seconds) when the action was transmitted
* @param string $queue_id ID of the queue from which the event was sent (sync or full_sync)
* @param array $token The auth token used to invoke the API
* @param string $action_name The name of the action executed on the remote site
* @param array $args The arguments passed to the action
* @param int $user_id The external_user_id who did the action
* @param bool $silent Whether the item was created via import
* @param double $timestamp Timestamp (in seconds) when the action occurred
* @param double $sent_timestamp Timestamp (in seconds) when the action was transmitted
* @param string $queue_id ID of the queue from which the event was sent (sync or full_sync)
* @param object|null $token The auth token used to invoke the API
*/
do_action( 'jetpack_sync_remote_action', $action_name, $args, $user_id, $silent, $timestamp, $sent_timestamp, $queue_id, $token );

View File

@ -62,6 +62,7 @@ class Settings {
'dedicated_sync_enabled' => true,
'custom_queue_table_enabled' => true,
'wpcom_rest_api_enabled' => true,
'sync_actions_blacklist' => true,
);
/**
@ -283,6 +284,7 @@ class Settings {
/**
* Regular option update and handling
*/
$updated = false;
if ( self::is_network_setting( $setting ) ) {
if ( is_multisite() && is_main_site() ) {
$updated = update_site_option( self::SETTINGS_OPTION_PREFIX . $setting, $value );
@ -302,26 +304,21 @@ class Settings {
if ( 'dedicated_sync_enabled' === $setting && $updated && (bool) $value ) {
if ( ! Dedicated_Sender::can_spawn_dedicated_sync_request() ) {
update_option( self::SETTINGS_OPTION_PREFIX . $setting, 0, true );
}
}
$listener = Listener::get_instance();
// Remove the last two actions from the queue since we failed to enable Dedicated Sync.
// Those would be `updated_option` with `jetpack_sync_settings_dedicated_sync_enabled` set to 1 and then 0 again.
$queue = $listener->get_sync_queue();
$items = $queue->peek_newest( 2 );
$key = 'jetpack_sync_settings_dedicated_sync_enabled';
// Do not enable wpcom rest api if we cannot send a test request.
if ( 'wpcom_rest_api_enabled' === $setting && $updated && (bool) $value ) {
$sender = Sender::get_instance();
$data = array(
'timestamp' => microtime( true ),
);
$items = $sender->send_action( 'jetpack_sync_wpcom_rest_api_enable_test', $data );
// If we can't send a test request, disable the setting and send action tolog the error.
if ( is_wp_error( $items ) ) {
update_option( self::SETTINGS_OPTION_PREFIX . $setting, 0, true );
$data = array(
'timestamp' => microtime( true ),
'response_code' => $items->get_error_code(),
'response_body' => $items->get_error_message() ?? '',
);
$sender->send_action( 'jetpack_sync_wpcom_rest_api_enable_error', $data );
if (
isset( $items[0][1][0] )
&& isset( $items[1][1][0] )
&& $items[0][1][0] === $key
&& $items[1][1][0] === $key
) {
$queue->pop_newest( 2 );
}
}
}
}
@ -523,6 +520,23 @@ class Settings {
);
}
/**
* Returns structured filter values for allowed comment types.
*
* @access public
* @static
*
* @return array Comment type filter values.
*/
public static function get_allowed_comment_types_structured() {
return array(
'comment_type' => array(
'operator' => 'IN',
'values' => array_map( 'esc_sql', Defaults::get_comment_types_whitelist() ),
),
);
}
/**
* Returns escaped SQL for comments, excluding any spam comments.
* Can be injected directly into a WHERE clause.

View File

@ -7,6 +7,10 @@
namespace Automattic\Jetpack\Sync;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses base64
* algorithm to compress objects serialized using json_encode.

View File

@ -54,7 +54,9 @@ class Users {
public static function user_role_change( $user_id ) {
$connection = new Jetpack_Connection();
if ( $connection->is_user_connected( $user_id ) ) {
self::get_role( $user_id );
self::update_role_on_com( $user_id );
// Try to choose a new master if we're demoting the current one.
self::maybe_demote_master_user( $user_id );
}
@ -66,11 +68,12 @@ class Users {
* @access public
* @static
*
* @param int $user_id ID of the user.
* @param int $user_id ID of the user.
* @param bool $reload Forces reading the role from the database.
* @return string Role of the user.
*/
public static function get_role( $user_id ) {
if ( isset( self::$user_roles[ $user_id ] ) ) {
public static function get_role( $user_id, $reload = false ) {
if ( ! $reload && isset( self::$user_roles[ $user_id ] ) ) {
return self::$user_roles[ $user_id ];
}

View File

@ -7,6 +7,10 @@
namespace Automattic\Jetpack\Sync\Modules;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for attachments.
*/

View File

@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Defaults;
use Automattic\Jetpack\Sync\Functions;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for callables.
*/
@ -291,17 +295,23 @@ class Callables extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @param int $send_until The timestamp until the current request can send.
* @param array $status This Module Full Sync Status.
* @param int $send_until The timestamp until the current request can send.
* @param int $started The timestamp when the full sync started.
*
* @return array This Module Full Sync Status.
*/
public function send_full_sync_actions( $config, $send_until, $status ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// we call this instead of do_action when sending immediately.
$this->send_action( 'jetpack_full_sync_callables', array( true ) );
$result = $this->send_action( 'jetpack_full_sync_callables', array( true ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
if ( is_wp_error( $result ) ) {
$status['error'] = true;
return $status;
}
$status['finished'] = true;
$status['sent'] = $status['total'];
return $status;
}
/**
@ -310,7 +320,7 @@ class Callables extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
@ -384,6 +394,9 @@ class Callables extends Module {
return;
}
$plugins = Functions::get_plugins();
if ( ! is_array( $plugins ) ) {
return;
}
foreach ( $plugins as $plugin_file => $plugin_data ) {
/**
* Plugins often like to unset things but things break if they are not able to.

View File

@ -7,9 +7,14 @@
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Defaults;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for comments.
*/
@ -201,21 +206,22 @@ class Comments extends Module {
*
* @access public
*
* @return array Defaults to [ '', 'trackback', 'pingback' ].
* @return array Defaults to [ '', 'comment', 'trackback', 'pingback', 'review', 'note' ].
*/
public function get_whitelisted_comment_types() {
/**
* Comment types present in this list will sync their status changes to WordPress.com.
*
* @since 1.6.3
* @since-jetpack 7.6.0
*
* @param array A list of comment types.
*/
return apply_filters(
'jetpack_sync_whitelisted_comment_types',
array( '', 'comment', 'trackback', 'pingback', 'review' )
);
return Defaults::get_comment_types_whitelist();
}
/**
* Returns escaped SQL for whitelisted comment types.
* Can be injected directly into a WHERE clause.
*
* @access public
*
* @return string SQL WHERE clause.
*/
public function get_whitelisted_comment_types_sql() {
return 'comment_type IN (\'' . implode( '\', \'', array_map( 'esc_sql', $this->get_whitelisted_comment_types() ) ) . '\')';
}
/**
@ -378,11 +384,13 @@ class Comments extends Module {
* @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
*/
public function get_where_sql( $config ) {
if ( is_array( $config ) ) {
$where_sql = $this->get_whitelisted_comment_types_sql();
if ( is_array( $config ) && ! empty( $config ) ) {
return 'comment_ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
return '1=1';
return $where_sql;
}
/**
@ -490,6 +498,9 @@ class Comments extends Module {
* @return array|boolean False if not whitelisted, the original hook args otherwise.
*/
public function filter_meta( $args ) {
if ( ! is_array( $args ) || count( $args ) < 3 ) {
return false;
}
if ( $this->is_comment_type_allowed( $args[1] ) && $this->is_whitelisted_comment_meta( $args[2] ) ) {
return $args;
}

View File

@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Defaults;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for constants.
*/
@ -146,17 +150,23 @@ class Constants extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @param array $status This module Full Sync status.
* @param int $send_until The timestamp until the current request can send.
* @param array $state This module Full Sync status.
* @param int $started The timestamp when the full sync started.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// we call this instead of do_action when sending immediately.
$this->send_action( 'jetpack_full_sync_constants', array( true ) );
$result = $this->send_action( 'jetpack_full_sync_constants', array( true ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
if ( is_wp_error( $result ) ) {
$status['error'] = true;
return $status;
}
$status['finished'] = true;
$status['sent'] = $status['total'];
return $status;
}
/**
@ -166,7 +176,7 @@ class Constants extends Module {
*
* @param array $config Full sync configuration for this sync module.
*
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;

View File

@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Lock;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* This class does a full resync of the database by
* sending an outbound action for every single object
@ -65,21 +69,20 @@ class Full_Sync_Immediately extends Module {
* @return bool Always returns true at success.
*/
public function start( $full_sync_config = null, $context = null ) {
// There was a full sync in progress.
if ( $this->is_started() && ! $this->is_finished() ) {
/**
* Fires when a full sync is cancelled.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*/
do_action( 'jetpack_full_sync_cancelled' );
$this->send_action( 'jetpack_full_sync_cancelled' );
}
// Check if there was a full sync in progress already before resetting the data.
$should_process_cancelled_action = $this->get_status()['start_action_processed'] && ! $this->is_finished() ? true : false;
// Remove all evidence of previous full sync items and status.
$this->reset_data();
// Update status to indicate that a new full sync is starting and need to cancel previous one.
if ( $should_process_cancelled_action ) {
$this->update_status(
array(
'cancelled_action_processed' => false,
)
);
}
if ( ! is_array( $full_sync_config ) ) {
/*
* Filter default sync config to allow injecting custom configuration.
@ -102,29 +105,12 @@ class Full_Sync_Immediately extends Module {
$this->update_status(
array(
'started' => time(),
'config' => $full_sync_config,
'progress' => $this->get_initial_progress( $full_sync_config ),
'started' => time(),
'config' => $full_sync_config,
'context' => $context,
)
);
$range = $this->get_content_range();
/**
* Fires when a full sync begins. This action is serialized
* and sent to the server so that it knows a full sync is coming.
*
* @param array $full_sync_config Sync configuration for all sync modules.
* @param array $range Range of the sync items, containing min and max IDs for some item types.
* @param mixed $context The context where the full sync was initiated from.
*
* @since 1.6.3
* @since-jetpack 4.2.0
* @since-jetpack 7.3.0 Added $range arg.
* @since 4.4.0 Added $context arg.
*/
do_action( 'jetpack_full_sync_start', $full_sync_config, $range );
$this->send_action( 'jetpack_full_sync_start', array( $full_sync_config, $range, $context ) );
return true;
}
@ -148,10 +134,13 @@ class Full_Sync_Immediately extends Module {
*/
public function get_status() {
$default = array(
'started' => false,
'finished' => false,
'progress' => array(),
'config' => array(),
'start_action_processed' => false,
'cancelled_action_processed' => true, // true by default to avoid sending the action when there is no need,
'started' => false,
'finished' => false,
'progress' => array(),
'config' => array(),
'context' => null,
);
return wp_parse_args( \Jetpack_Options::get_raw_option( self::STATUS_OPTION ), $default );
@ -249,10 +238,11 @@ class Full_Sync_Immediately extends Module {
* Given an initial Full Sync configuration get the initial status.
*
* @param array $full_sync_config Full sync configuration.
* @param array $range Range of the sync items, containing min, max and count IDs for some item types.
*
* @return array Initial Sent status.
*/
public function get_initial_progress( $full_sync_config ) {
public function get_initial_progress( $full_sync_config, $range = null ) {
// Set default configuration, calculate totals, and save configuration if totals > 0.
$status = array();
foreach ( $full_sync_config as $name => $config ) {
@ -261,7 +251,8 @@ class Full_Sync_Immediately extends Module {
continue;
}
$status[ $name ] = array(
'total' => $module->total( $config ),
// If we have a range for the module, use the count from the range to avoid querying the database again.
'total' => $range[ $name ]->count ?? $module->total( $config ),
'sent' => 0,
'finished' => false,
);
@ -275,18 +266,24 @@ class Full_Sync_Immediately extends Module {
*
* @access private
*
* @param array $full_sync_config Full sync configuration.
*
* @return array Array of range (min ID, max ID, total items) for all content types.
*/
private function get_content_range() {
$range = array();
$config = $this->get_status()['config'];
// Add range only when syncing all objects.
if ( true === isset( $config['posts'] ) && $config['posts'] ) {
$range['posts'] = $this->get_range( 'posts' );
}
if ( true === isset( $config['comments'] ) && $config['comments'] ) {
$range['comments'] = $this->get_range( 'comments' );
private function get_content_range( $full_sync_config ) {
$range = array();
foreach ( $full_sync_config as $module_name => $config ) {
// Calculate ranges only for modules that get chunked.
if ( in_array( $module_name, array( 'constants', 'functions', 'network_options', 'options', 'themes', 'updates' ), true ) ) {
continue;
}
$module = Modules::get_module( $module_name );
if ( ! $module ) {
continue;
}
if ( true === isset( $config ) && $config ) {
$range[ $module_name ] = $this->get_range( $module_name );
}
}
return $range;
@ -303,23 +300,17 @@ class Full_Sync_Immediately extends Module {
*/
public function get_range( $type ) {
global $wpdb;
if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
$module = Modules::get_module( $type );
if ( ! $module ) {
return array();
}
switch ( $type ) {
case 'posts':
$table = $wpdb->posts;
$id = 'ID';
$where_sql = Settings::get_blacklisted_post_types_sql();
break;
case 'comments':
$table = $wpdb->comments;
$id = 'comment_ID';
$where_sql = Settings::get_comments_filter_sql();
break;
$table = $module->table();
$id = $module->id_field();
if ( 'terms' === $module ) { // Terms module relies on the term_taxonomy and term_taxonomy_id for the where sql, let's use term_id instead.
$id = 'term_id';
}
$where_sql = $module->get_where_sql( array() );
// TODO: Call $wpdb->prepare on the following query.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
@ -385,6 +376,14 @@ class Full_Sync_Immediately extends Module {
* @access public
*/
public function send() {
if ( ! $this->maybe_send_cancelled_action() ) {
return false;
}
if ( ! $this->maybe_send_full_sync_start() ) {
return false;
}
$config = $this->get_status()['config'];
$max_duration = Settings::get_setting( 'full_sync_send_duration' );
@ -394,22 +393,38 @@ class Full_Sync_Immediately extends Module {
$started = $this->get_status()['started'];
foreach ( $this->get_remaining_modules_to_send() as $module ) {
$progress[ $module->name() ] = $module->send_full_sync_actions( $config[ $module->name() ], $progress[ $module->name() ], $send_until );
if ( isset( $progress[ $module->name() ]['error'] ) ) {
unset( $progress[ $module->name() ]['error'] );
$this->update_status( array( 'progress' => $progress ) );
return false;
} elseif ( ! $progress[ $module->name() ]['finished'] ) {
$this->update_status( array( 'progress' => $progress ) );
return true;
$remaining_modules = $this->get_remaining_modules_to_send();
foreach ( $remaining_modules as $module ) {
$module_name = $module->name();
if ( array_key_exists( $module_name, $progress ) && array_key_exists( $module_name, $config ) ) {
$progress[ $module_name ] = $module->send_full_sync_actions( $config[ $module_name ], $progress[ $module_name ], $send_until, $started );
if ( isset( $progress[ $module_name ]['error'] ) ) {
unset( $progress[ $module_name ]['error'] );
$this->update_status( array( 'progress' => $progress ) );
return false;
} elseif ( ! $progress[ $module_name ]['finished'] ) {
$this->update_status( array( 'progress' => $progress ) );
return true;
}
}
if ( $this->get_status()['started'] !== $started ) {
// Full sync was restarted, stop sending.
return false;
}
}
// Check that all remaining modules in progress are actually finished.
// If a module was skipped in the main loop (due to being unfinished), but still exists in progress, we shouldn't mark the sync as complete.
foreach ( $remaining_modules as $module ) {
$name = $module->name();
if ( array_key_exists( $name, $progress ) && empty( $progress[ $name ]['finished'] ) ) {
$this->update_status( array( 'progress' => $progress ) );
return true;
}
}
$this->send_full_sync_end();
$this->update_status( array( 'progress' => $progress ) );
return true;
@ -443,13 +458,101 @@ class Full_Sync_Immediately extends Module {
}
/**
* Send 'jetpack_full_sync_end' and update 'finished' status.
* Sends the `jetpack_full_sync_start` action if it hasn't been processed yet.
*
* Prepares the full sync start action, sends it to WordPress.com, fires the local action,
* and updates the sync status to reflect that the start action has been processed.
*
* @return bool True if the action was successfully sent or already processed, false on failure.
*/
private function maybe_send_full_sync_start() {
$status = $this->get_status();
// If already processed, nothing to do.
if ( true === $status['start_action_processed'] ) {
return true;
}
$config = $status['config'];
$context = $status['context'];
$range = $this->get_content_range( $config );
$result = $this->send_action( 'jetpack_full_sync_start', array( $config, $range, $context ) );
// If the action failed on WordPress.com, return false.
if ( is_wp_error( $result ) ) {
return false;
}
/**
* Fires when a full sync begins. This action is serialized
* and sent to the server so that it knows a full sync is coming.
*
* @param array $config Sync configuration for all sync modules.
* @param array $range Range of the sync items, containing min and max IDs for some item types.
* @param mixed $context The context where the full sync was initiated from.
*
* @since 1.6.3
* @since-jetpack 4.2.0
* @since-jetpack 7.3.0 Added $range arg.
* @since 4.4.0 Added $context arg.
*/
do_action( 'jetpack_full_sync_start', $config, $range );
$this->update_status(
array(
'start_action_processed' => true,
'progress' => $this->get_initial_progress( $config, $range ),
)
);
return true;
}
/**
* Sends the `jetpack_full_sync_cancelled` action if it hasn't been processed yet.
*
* @return bool True if the action was successfully sent or already processed, false on failure.
*/
private function maybe_send_cancelled_action() {
$status = $this->get_status();
if ( true === $status['cancelled_action_processed'] ) {
return true;
}
$result = $this->send_action( 'jetpack_full_sync_cancelled' );
if ( is_wp_error( $result ) ) {
return false;
}
/**
* Fires when a full sync is cancelled.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*/
do_action( 'jetpack_full_sync_cancelled' );
$this->update_status( array( 'cancelled_action_processed' => true ) );
return true;
}
/**
* Sends the `jetpack_full_sync_end` action and updates the status when the full sync end action is processed.
*
* @access public
*/
public function send_full_sync_end() {
$range = $this->get_content_range();
$status = $this->get_status();
$range = $this->get_content_range( $status['config'] );
$context = $status['context'];
$result = $this->send_action( 'jetpack_full_sync_end', array( '', $range, $context ) );
if ( is_wp_error( $result ) ) { // Do not set finished status if we get an error.
return;
}
/**
* Fires when a full sync ends. This action is serialized
* and sent to the server.
@ -462,7 +565,6 @@ class Full_Sync_Immediately extends Module {
* @since-jetpack 7.3.0 Added $range arg.
*/
do_action( 'jetpack_full_sync_end', '', $range );
$this->send_action( 'jetpack_full_sync_end', array( '', $range ) );
// Setting autoload to true means that it's faster to check whether we should continue enqueuing.
$this->update_status( array( 'finished' => time() ) );

View File

@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Queue;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* This class does a full resync of the database by
* enqueuing an outbound action for every single object
@ -336,6 +340,10 @@ class Full_Sync extends Module {
$id = 'comment_ID';
$where_sql = Settings::get_comments_filter_sql();
break;
default:
// This should never be reached due to the guard condition above,
// but Phan complains so let's make it happy.
return array();
}
// TODO: Call $wpdb->prepare on the following query.

View File

@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for imports.
*/
@ -179,7 +183,7 @@ class Import extends Module {
}
$action = current_filter();
$backtrace = debug_backtrace( false ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$backtrace = debug_backtrace( 0 ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$do_action_pos = -1;
$backtrace_len = count( $backtrace );

View File

@ -7,6 +7,10 @@
namespace Automattic\Jetpack\Sync\Modules;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for menus.
*/

View File

@ -7,6 +7,10 @@
namespace Automattic\Jetpack\Sync\Modules;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for meta.
*/

View File

@ -14,6 +14,10 @@ use Automattic\Jetpack\Sync\Replicastore;
use Automattic\Jetpack\Sync\Sender;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Basic methods implemented by Jetpack Sync extensions.
*
@ -195,7 +199,7 @@ abstract class Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) {
// In subclasses, return the number of items yet to be enqueued.
@ -242,7 +246,12 @@ abstract class Module {
if ( $sort && is_array( $values ) ) {
$this->recursive_ksort( $values );
}
return crc32( wp_json_encode( Functions::json_wrap( $values ) ) );
return crc32(
wp_json_encode(
Functions::json_wrap( $values ),
0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because we don't want disrupt the checksum algorithm.
)
);
}
/**
@ -398,13 +407,14 @@ abstract class Module {
*
* @access protected
*
* @param string $config Full sync configuration for this module.
* @param array $status the current module full sync status.
* @param float $send_until timestamp until we want this request to send full sync events.
* @param array $config Full sync configuration for this module.
* @param array $status the current module full sync status.
* @param float $send_until timestamp until we want this request to send full sync events.
* @param int $started The timestamp when the full sync started.
*
* @return array Status, the module full sync status updated.
*/
public function send_full_sync_actions( $config, $status, $send_until ) {
public function send_full_sync_actions( $config, $status, $send_until, $started ) {
global $wpdb;
if ( empty( $status['last_sent'] ) ) {
@ -414,13 +424,25 @@ abstract class Module {
$limits = Settings::get_setting( 'full_sync_limits' )[ $this->name() ] ??
Defaults::get_default_setting( 'full_sync_limits' )[ $this->name() ] ??
array(
'max_chunks' => 10,
'chunk_size' => 100,
'max_chunks' => null,
'chunk_size' => null,
);
$limits = array(
'max_chunks' => is_numeric( $limits['max_chunks'] ) ? (int) $limits['max_chunks'] : 10,
'chunk_size' => is_numeric( $limits['chunk_size'] ) ? (int) $limits['chunk_size'] : 100,
);
$limits['chunk_size'] = $this->adjust_chunk_size_if_stuck( $status['last_sent'], $limits['chunk_size'], $started );
$chunks_sent = 0;
$last_item = $this->get_last_item( $config );
// Store last_item in status to avoid re-running this expensive query on every invocation.
// The minimum ID does not change during a Full Sync.
if ( ! isset( $status['last_item'] ) ) {
$status['last_item'] = $this->get_last_item( $config );
}
$last_item = $status['last_item'];
while ( $chunks_sent < $limits['max_chunks'] && microtime( true ) < $send_until ) {
$objects = $this->get_next_chunk( $config, $status, $limits['chunk_size'] );
@ -437,7 +459,7 @@ abstract class Module {
// If we have objects as a key it means get_next_chunk is being overridden, we need to check for it being an empty array.
// In case it is an empty array, we should not send the action or increase the chunks_sent, we just need to update the status.
if ( ! isset( $objects['objects'] ) || array() !== $objects['objects'] ) {
$key = $this->full_sync_action_name() . '_' . crc32( wp_json_encode( $status['last_sent'] ) );
$key = $this->full_sync_action_name() . '_' . crc32( wp_json_encode( $status['last_sent'], JSON_UNESCAPED_SLASHES ) );
$result = $this->send_action( $this->full_sync_action_name(), array( $objects, $status['last_sent'] ), $key );
if ( is_wp_error( $result ) || $wpdb->last_error ) {
$status['error'] = true;
@ -457,6 +479,72 @@ abstract class Module {
return $status;
}
/**
* Adjust chunk size using adaptive logic and update transient for tracking stuck state.
*
* @param string $last_sent The current last_sent marker.
* @param int $default_chunk_size The default chunk size.
* @param int $started The timestamp when the full sync started.
* @return int Adjusted chunk size.
*/
private function adjust_chunk_size_if_stuck( $last_sent, $default_chunk_size, $started ) {
$transient_key = 'jetpack_sync_last_sent_' . $this->name() . '_' . $started;
$stuck_data = get_transient( $transient_key );
$is_stuck = isset( $stuck_data['last_sent'] ) && $stuck_data['last_sent'] === $last_sent;
// Preserve the adjusted chunk size and stuck count from the transient when stuck.
$stuck_count = $is_stuck && isset( $stuck_data['stuck_count'] ) ? $stuck_data['stuck_count'] : 0;
$adjusted_chunk_size = $is_stuck && isset( $stuck_data['adjusted_chunk_size'] ) ? $stuck_data['adjusted_chunk_size'] : $default_chunk_size;
if ( $is_stuck && $stuck_data['adjusted_chunk_size'] === 1 ) {
// Refresh transient TTL to prevent expiry-driven reset cycles.
set_transient( $transient_key, $stuck_data, HOUR_IN_SECONDS );
return 1; // If we are already at the minimum chunk size, do not adjust further.
}
// We will adjust if it is stuck after 10 minutes.
if (
$is_stuck &&
( time() - $stuck_data['timestamp'] ) >= 10 * MINUTE_IN_SECONDS
) {
++$stuck_count;
$adjusted_chunk_size = max( 1, (int) ( $default_chunk_size / ( 2 ** $stuck_count ) ) ); // Halve the chunk size for each stuck iteration.
// Send one HTTP notification when chunk size reaches the minimum (1)
// so the stuck state is visible for monitoring. Intermediate cascade
// steps skip the HTTP request to avoid consuming the time budget.
if ( 1 === $adjusted_chunk_size ) {
$this->send_action(
'jetpack_full_sync_stuck_adjustment',
array(
'module' => $this->name(),
'last_sent' => $last_sent,
'stuck_count' => $stuck_count,
'adjusted_chunk_size' => $adjusted_chunk_size,
)
);
}
}
// Set or update the transient with the new last_sent, timestamp, and stuck_count.
// Reset the timestamp when not stuck or after an adjustment, so each new chunk size
// gets a 10-minute window to prove itself before halving further.
$previous_chunk_size = $stuck_data['adjusted_chunk_size'] ?? null;
$reset_timestamp = ! $is_stuck || $adjusted_chunk_size !== $previous_chunk_size;
set_transient(
$transient_key,
array(
'last_sent' => $last_sent,
'timestamp' => $reset_timestamp ? time() : $stuck_data['timestamp'],
'stuck_count' => $stuck_count,
'adjusted_chunk_size' => $adjusted_chunk_size,
),
HOUR_IN_SECONDS
);
return $adjusted_chunk_size;
}
/**
* Set the status of the full sync action based on the objects that were sent.
* Used to update the status of the module after sending a chunk of objects.
@ -737,7 +825,7 @@ abstract class Module {
$current_size = 0;
foreach ( $objects as $object ) {
$object_size = strlen( maybe_serialize( $object ) );
$object_size = strlen( (string) maybe_serialize( $object ) );
$current_metadata = array();
$metadata_size = 0;
$id_field = $this->id_field();
@ -745,12 +833,13 @@ abstract class Module {
foreach ( $metadata as $key => $metadata_item ) {
if ( (int) $metadata_item->{$type . '_id'} === $object_id ) {
$metadata_item_size = strlen( maybe_serialize( $metadata_item->meta_value ) );
$metadata_item_size = strlen( (string) maybe_serialize( $metadata_item ) );
if ( $metadata_item_size >= $max_meta_size ) {
$metadata_item->meta_value = ''; // Trim metadata if too large.
$metadata_item_size = strlen( (string) maybe_serialize( $metadata_item ) );
}
$current_metadata[] = $metadata_item;
$metadata_size += $metadata_item_size >= $max_meta_size ? 0 : $metadata_item_size;
$metadata_size += $metadata_item_size;
if ( ! empty( $filtered_object_ids ) && ( $current_size + $object_size + $metadata_size ) > $max_total_size ) {
break 2; // Exit both loops.

View File

@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Defaults;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for network options.
*/
@ -120,17 +124,23 @@ class Network_Options extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @param array $status This module Full Sync status.
* @param int $send_until The timestamp until the current request can send.
* @param array $state This module Full Sync status.
* @param int $started The timestamp when the full sync started.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// we call this instead of do_action when sending immediately.
$this->send_action( 'jetpack_full_sync_network_options', array( true ) );
$result = $this->send_action( 'jetpack_full_sync_network_options', array( true ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
if ( is_wp_error( $result ) ) {
$status['error'] = true;
return $status;
}
$status['finished'] = true;
$status['sent'] = $status['total'];
return $status;
}
/**
@ -139,7 +149,7 @@ class Network_Options extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;

View File

@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Defaults;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for options.
*/
@ -172,17 +176,23 @@ class Options extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @param array $status This module Full Sync status.
* @param int $send_until The timestamp until the current request can send.
* @param array $state This module Full Sync status.
* @param int $started The timestamp when the full sync started.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// we call this instead of do_action when sending immediately.
$this->send_action( 'jetpack_full_sync_options', array( true ) );
$result = $this->send_action( 'jetpack_full_sync_options', array( true ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
if ( is_wp_error( $result ) ) {
$status['error'] = true;
return $status;
}
$status['finished'] = true;
$status['sent'] = $status['total'];
return $status;
}
/**
@ -307,6 +317,15 @@ class Options extends Module {
return false;
}
// Check if 'jetpack_options' were updated and reject if the change affected only blacklisted keys.
if ( 'jetpack_options' === $args[0] && 3 === count( $args ) ) {
if ( ! $this->should_enqueue_jetpack_options_update( $args[1], $args[2] ) ) {
return false;
}
return $args;
}
// Filter our weird array( false ) value for theme_mods_*.
if ( str_starts_with( $args[0], 'theme_mods_' ) ) {
$this->filter_theme_mods( $args[1] );
@ -477,4 +496,49 @@ class Options extends Module {
return 'OPTION-DOES-NOT-EXIST';
}
/**
* Check if 'jetpack_options' option update should be processed based on excluded keys.
*
* @param mixed $old_value The old option value.
* @param mixed $value The new option value.
* @return bool False if only excluded keys changed (or no change), true otherwise.
*/
private function should_enqueue_jetpack_options_update( $old_value, $value ) {
// No changes at all.
if ( $old_value === $value ) {
return false;
}
// Values are different but not both arrays - meaningful change.
if ( ! is_array( $old_value ) || ! is_array( $value ) ) {
return true;
}
// Determine all top-level keys present in either the old or new value.
$all_keys = array_unique(
array_merge(
array_keys( $old_value ),
array_keys( $value )
)
);
// Short-circuit as soon as we find a changed key that is not blacklisted.
foreach ( $all_keys as $key ) {
$old_has_key = array_key_exists( $key, $old_value );
$new_has_key = array_key_exists( $key, $value );
// Key was added or removed.
if ( ! $old_has_key || ! $new_has_key ) {
if ( ! in_array( $key, Defaults::$jetpack_options_blacklist, true ) ) {
return true;
}
continue;
}
// Key exists in both arrays but the value changed.
if ( $old_value[ $key ] !== $value[ $key ] ) {
if ( ! in_array( $key, Defaults::$jetpack_options_blacklist, true ) ) {
return true;
}
}
}
// Either there were no effective changes, or all changed keys are excluded.
return false;
}
}

View File

@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for plugins.
*/
@ -50,6 +54,24 @@ class Plugins extends Module {
*/
private $plugins_updated = array();
/**
* List of plugins installed during this request.
*
* @access private
*
* @var array
*/
private $plugins_installed = array();
/**
* List of all plugin update failures during this request.
*
* @access private
*
* @var array
*/
private $plugins_update_failures = array();
/**
* State
*
@ -154,20 +176,16 @@ class Plugins extends Module {
);
$errors = $this->get_errors( $upgrader->skin );
if ( $errors ) {
foreach ( $plugins as $slug ) {
/**
* Sync that a plugin update failed
*
* @since 1.6.3
* @since-jetpack 5.8.0
*
* @module sync
*
* @param string $plugin , Plugin slug
* @param string Error code
* @param string Error message
*/
do_action( 'jetpack_plugin_update_failed', $this->get_plugin_info( $slug ), $errors['code'], $errors['message'], $this->state );
foreach ( $plugins as $slug ) { // Accumulate failures and defer to shutdown, to reduce request-time lag.
$this->plugins_update_failures[] = array(
'plugin' => $this->get_plugin_info( $slug ),
'code' => $errors['code'],
'message' => $errors['message'],
'state' => $this->state,
);
}
if ( ! has_action( 'shutdown', array( $this, 'sync_plugins_update_failed' ) ) ) {
add_action( 'shutdown', array( $this, 'sync_plugins_update_failed' ), 9 );
}
return;
@ -178,18 +196,16 @@ class Plugins extends Module {
break;
case 'install':
/**
* Signals to the sync listener that a plugin was installed and a sync action
* reflecting the installation and the plugin info should be sent
*
* @since 1.6.3
* @since-jetpack 5.8.0
*
* @module sync
*
* @param array () $plugin, Plugin Data
*/
do_action( 'jetpack_plugin_installed', array_map( array( $this, 'get_plugin_info' ), $plugins ) );
// Accumulate installs and defer to shutdown.
$this->plugins_installed = array_merge(
$this->plugins_installed,
array_map( array( $this, 'get_plugin_info' ), $plugins )
);
if ( ! has_action( 'shutdown', array( $this, 'sync_plugins_installed' ) ) ) {
add_action( 'shutdown', array( $this, 'sync_plugins_installed' ), 9 );
}
break;
}
}
@ -404,4 +420,54 @@ class Plugins extends Module {
*/
do_action( 'jetpack_plugins_updated', $this->plugins_updated, $this->state );
}
/**
* Helper method for firing the 'jetpack_plugin_installed' action on shutdown.
*
* @access public
*/
public function sync_plugins_installed() {
if ( empty( $this->plugins_installed ) ) {
return;
}
/**
* Signals to the sync listener that a plugin was installed and a sync action
* reflecting the installation and the plugin info should be sent.
*
* @since 1.6.3
* @since-jetpack 5.8.0
*
* @module sync
*
* @param array () $plugin, Plugin Data
*/
do_action( 'jetpack_plugin_installed', $this->plugins_installed );
}
/**
* Helper method for firing the 'jetpack_plugin_update_failed' actions on shutdown.
*
* @access public
*/
public function sync_plugins_update_failed() {
if ( empty( $this->plugins_update_failures ) ) {
return;
}
foreach ( $this->plugins_update_failures as $failure ) {
/**
* Sync that a plugin update failed
*
* @since 1.6.3
* @since-jetpack 5.8.0
*
* @module sync
*
* @param array $plugin Plugin Data
* @param string $code Error code
* @param string $message Error message
* @param array $state State data
*/
do_action( 'jetpack_plugin_update_failed', $failure['plugin'], $failure['code'], $failure['message'], $failure['state'] );
}
}
}

View File

@ -12,6 +12,10 @@ use Automattic\Jetpack\Roles;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for posts.
*/
@ -43,6 +47,15 @@ class Posts extends Module {
*/
private $action_handler;
/**
* Mark posts that are deleted in the current request.
*
* @access private
*
* @var array
*/
private static $deleted_posts_in_request = array();
/**
* Import end.
*
@ -140,6 +153,7 @@ class Posts extends Module {
public function init_listeners( $callable ) {
$this->action_handler = $callable;
add_action( 'before_delete_post', array( $this, 'mark_post_is_being_deleted' ), 0, 1 );
add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ), 11, 3 );
add_action( 'wp_after_insert_post', array( $this, 'wp_after_insert_post' ), 11, 2 );
add_action( 'jetpack_sync_save_post', $callable, 10, 4 );
@ -154,12 +168,17 @@ class Posts extends Module {
$this->init_listeners_for_meta_type( 'post', $callable );
$this->init_meta_whitelist_handler( 'post', array( $this, 'filter_meta' ) );
add_filter( 'jetpack_sync_before_enqueue_updated_post_meta', array( $this, 'on_before_enqueue_updated_attachment_metadata' ), 1 );
add_filter( 'jetpack_sync_before_enqueue_deleted_post_meta', array( $this, 'maybe_skip_deleted_post_meta' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_post', array( $this, 'filter_jetpack_sync_before_enqueue_jetpack_sync_save_post' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_published_post', array( $this, 'filter_jetpack_sync_before_enqueue_jetpack_published_post' ) );
add_action( 'jetpack_daily_akismet_meta_cleanup_before', array( $this, 'daily_akismet_meta_cleanup_before' ) );
add_action( 'jetpack_daily_akismet_meta_cleanup_after', array( $this, 'daily_akismet_meta_cleanup_after' ) );
add_action( 'jetpack_post_meta_batch_delete', $callable, 10, 2 );
add_action( 'deleted_post', array( $this, 'unmark_post_being_deleted' ), 11, 1 );
}
/**
@ -222,8 +241,8 @@ class Posts extends Module {
*/
public function init_before_send() {
// meta.
add_filter( 'jetpack_sync_before_send_added_post_meta', array( $this, 'trim_post_meta' ) );
add_filter( 'jetpack_sync_before_send_updated_post_meta', array( $this, 'trim_post_meta' ) );
add_filter( 'jetpack_sync_before_send_added_post_meta', array( $this, 'filter_added_post_meta_before_send' ), 5 ); // Incase this filter is used elsewhere, we run early.
add_filter( 'jetpack_sync_before_send_updated_post_meta', array( $this, 'filter_updated_post_meta_before_send' ), 5 ); // Incase this filter is used elsewhere, we run early.
add_filter( 'jetpack_sync_before_send_deleted_post_meta', array( $this, 'trim_post_meta' ) );
// Full sync.
$sync_module = Modules::get_module( 'full-sync' );
@ -258,7 +277,7 @@ class Posts extends Module {
* @todo Use $wpdb->prepare for the SQL query.
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) {
global $wpdb;
@ -282,7 +301,7 @@ class Posts extends Module {
$where_sql = Settings::get_blacklisted_post_types_sql();
// Config is a list of post IDs to sync.
if ( is_array( $config ) ) {
if ( is_array( $config ) && ! empty( $config ) ) {
$where_sql .= ' AND ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
@ -312,12 +331,116 @@ class Posts extends Module {
// Explicitly truncate meta_value when it exceeds limit.
// Large content will cause OOM issues and break Sync.
$serialized_value = maybe_serialize( $meta_value );
if ( strlen( $serialized_value ) >= self::MAX_META_LENGTH ) {
if ( $serialized_value === null || strlen( $serialized_value ) >= self::MAX_META_LENGTH ) {
$meta_value = '';
}
return array( $meta_id, $object_id, $meta_key, $meta_value );
}
/**
* Updated post meta send-time filter: refreshes _wp_attachment_metadata to the latest DB value, then trims.
*
* @param array $args [ $meta_id, $object_id, $meta_key, $meta_value ].
* @return array Filtered args.
*/
public function filter_updated_post_meta_before_send( $args ) {
if ( ! is_array( $args ) || count( $args ) < 4 ) {
return $args;
}
list( $meta_id, $object_id, $meta_key, $meta_value ) = $args;
if ( '_wp_attachment_metadata' !== $meta_key || 'attachment' !== get_post_type( (int) $object_id ) ) {
return $this->trim_post_meta( $args );
}
$current_value = wp_get_attachment_metadata( (int) $object_id );
if ( is_array( $current_value ) && ! empty( $current_value ) ) {
$meta_value = $current_value;
}
return $this->trim_post_meta( array( $meta_id, $object_id, $meta_key, $meta_value ) );
}
/**
* Added post meta send-time filter: refreshes _wp_attachment_metadata to the latest DB value, then trims.
*
* @param array $args [ $meta_id, $object_id, $meta_key, $meta_value ].
* @return array|false Filtered args, or false to skip sending when the snapshot is clearly incomplete.
*/
public function filter_added_post_meta_before_send( $args ) {
if ( ! is_array( $args ) || count( $args ) < 4 ) {
return $args;
}
list( $meta_id, $object_id, $meta_key, $meta_value ) = $args;
if ( '_wp_attachment_metadata' !== $meta_key || 'attachment' !== get_post_type( (int) $object_id ) ) {
return $this->trim_post_meta( $args );
}
$current_value = wp_get_attachment_metadata( (int) $object_id );
// For added_post_meta, skip clearly incomplete snapshots (e.g., missing or empty sizes).
if ( ! is_array( $current_value ) || empty( $current_value ) ) {
return false;
}
if ( isset( $current_value['sizes'] ) && is_array( $current_value['sizes'] ) && count( $current_value['sizes'] ) === 0 ) {
return false;
}
$meta_value = $current_value;
return $this->trim_post_meta( array( $meta_id, $object_id, $meta_key, $meta_value ) );
}
/**
* Mark a post as being deleted in the current request.
*
* @param int $post_id ID of the post being deleted.
*/
public function mark_post_is_being_deleted( $post_id ) {
self::$deleted_posts_in_request[ (int) $post_id ] = true;
}
/**
* Enqueue-time per-request dedupe for deleted post metadata, if the post itself is being deleted.
*
* @param array $args [ $meta_id, $post_id, $meta_key, $meta_value ].
* @return array|false
*/
public function maybe_skip_deleted_post_meta( $args ) {
if ( is_array( $args ) && isset( $args[1] ) && is_numeric( $args[1] ) ) {
$post_id = (int) $args[1];
if ( isset( self::$deleted_posts_in_request[ $post_id ] ) ) {
return false;
}
}
return $args;
}
/**
* Unmark a post as being deleted in the current request, to clean up.
*
* @param int $post_id ID of the post.
*/
public function unmark_post_being_deleted( $post_id ) {
unset( self::$deleted_posts_in_request[ (int) $post_id ] );
}
/**
* Enqueue-time per-request dedupe for updated attachment metadata.
*
* @param array $args [ $meta_id, $object_id, $meta_key, $meta_value ].
* @return array|false
*/
public function on_before_enqueue_updated_attachment_metadata( $args ) {
if ( ! is_array( $args ) || count( $args ) < 3 ) {
return $args;
}
$post_id = (int) $args[1];
$meta_key = $args[2];
if ( '_wp_attachment_metadata' !== $meta_key || 'attachment' !== get_post_type( $post_id ) ) {
return $args;
}
static $seen_updated_meta_for_post = array();
if ( isset( $seen_updated_meta_for_post[ $post_id ] ) ) {
return false;
}
$seen_updated_meta_for_post[ $post_id ] = true;
return $args;
}
/**
* Process content before send.
*
@ -337,25 +460,47 @@ class Posts extends Module {
* @return array|false Hook arguments, or false if the post type is a blacklisted one.
*/
public function filter_jetpack_sync_before_enqueue_jetpack_sync_save_post( $args ) {
list( $post_id, $post, $update, $previous_state ) = $args;
if (
! is_array( $args )
|| ! array_key_exists( 0, $args ) || ! is_numeric( $args[0] )
|| ! array_key_exists( 1, $args ) || ! ( $args[1] instanceof \WP_Post )
) {
return false;
}
list( $post_id, $post, $update, $previous_state ) = array_pad( $args, 4, null );
if ( in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true ) ) {
return false;
}
return array( $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state );
// During incremental sync, skip posts whose type is not registered (e.g. CPT unregistered before sync).
// Full sync may have already sent them; we simply don't enqueue incremental updates for them.
if ( ! get_post_type_object( $post->post_type ) ) {
return false;
}
return array( (int) $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state );
}
/**
* Add filtered post content.
*
* @param array $args Hook arguments.
* @return array Hook arguments.
* @return array|false Hook arguments, or false if the arguments are invalid.
*/
public function filter_jetpack_sync_before_enqueue_jetpack_published_post( $args ) {
list( $post_id, $flags, $post ) = $args;
if (
! is_array( $args )
|| ! array_key_exists( 0, $args ) || ! is_numeric( $args[0] )
|| ! array_key_exists( 1, $args ) || ! is_array( $args[1] )
|| ! array_key_exists( 2, $args ) || ! ( $args[2] instanceof \WP_Post )
) {
return false;
}
return array( $post_id, $flags, $this->filter_post_content_and_add_links( $post ) );
list( $post_id, $flags, $post ) = $args;
return array( (int) $post_id, $flags, $this->filter_post_content_and_add_links( $post ) );
}
/**
@ -365,7 +510,9 @@ class Posts extends Module {
* @return array|false Hook arguments, or false if the post type is a blacklisted one.
*/
public function filter_blacklisted_post_types_deleted( $args ) {
if ( ! is_array( $args ) || ! array_key_exists( 0, $args ) || ! is_numeric( $args[0] ) ) {
return false;
}
// deleted_post is called after the SQL delete but before cache cleanup.
// There is the potential we can't detect post_type at this point.
if ( ! $this->is_post_type_allowed( $args[0] ) ) {
@ -378,10 +525,16 @@ class Posts extends Module {
/**
* Filter all meta that is not blacklisted, or is stored for a disallowed post type.
*
* @param array $args Hook arguments.
* @param array|false $args Hook arguments.
* @return array|false Hook arguments, or false if meta was filtered.
*/
public function filter_meta( $args ) {
if ( ! is_array( $args ) || count( $args ) < 3 ) {
return false;
}
if ( ! is_numeric( $args[1] ) || ! is_string( $args[2] ) ) {
return false;
}
if ( $this->is_post_type_allowed( $args[1] ) && $this->is_whitelisted_post_meta( $args[2] ) ) {
return $args;
}
@ -396,8 +549,11 @@ class Posts extends Module {
* @return boolean Whether the post meta key is whitelisted.
*/
public function is_whitelisted_post_meta( $meta_key ) {
// The _wpas_skip_ meta key is used by Publicize.
return in_array( $meta_key, Settings::get_setting( 'post_meta_whitelist' ), true ) || str_starts_with( $meta_key, '_wpas_skip_' );
if ( ! is_string( $meta_key ) ) {
return false;
}
// The '_wpas_skip_' meta key prefix is used by Publicize to mark posts that should be skipped.
return str_starts_with( $meta_key, '_wpas_skip_' ) || in_array( $meta_key, Settings::get_setting( 'post_meta_whitelist' ), true );
}
/**
@ -597,6 +753,9 @@ class Posts extends Module {
* @param \WP_Post $post Post object.
*/
public function save_published( $new_status, $old_status, $post ) {
if ( ! $post instanceof \WP_Post ) {
return;
}
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
$this->just_published[ $post->ID ] = true;
}
@ -634,12 +793,12 @@ class Posts extends Module {
* @param boolean $update Whether this is an existing post being updated or not.
*/
public function wp_insert_post( $post_ID, $post = null, $update = null ) {
if ( ! is_numeric( $post_ID ) || $post === null ) {
if ( ! is_numeric( $post_ID ) || ! $post instanceof \WP_Post ) {
return;
}
// Workaround for https://github.com/woocommerce/woocommerce/issues/18007.
if ( $post && 'shop_order' === $post->post_type ) {
if ( 'shop_order' === $post->post_type ) {
$post = get_post( $post_ID );
}
@ -678,12 +837,12 @@ class Posts extends Module {
* @param \WP_Post $post Post object.
**/
public function wp_after_insert_post( $post_ID, $post ) {
if ( ! is_numeric( $post_ID ) || $post === null ) {
if ( ! is_numeric( $post_ID ) || ! $post instanceof \WP_Post ) {
return;
}
// Workaround for https://github.com/woocommerce/woocommerce/issues/18007.
if ( $post && 'shop_order' === $post->post_type ) {
if ( 'shop_order' === $post->post_type ) {
$post = get_post( $post_ID );
}
@ -756,6 +915,9 @@ class Posts extends Module {
*/
if ( 'customize_changeset' === $post->post_type ) {
$post_content = json_decode( $post->post_content, true );
if ( ! is_iterable( $post_content ) ) {
return;
}
foreach ( $post_content as $key => $value ) {
// Skip if it isn't a widget.
if ( 'widget_' !== substr( $key, 0, strlen( 'widget_' ) ) ) {
@ -771,7 +933,7 @@ class Posts extends Module {
$widget_data = array(
'name' => $wp_registered_widgets[ $key ]['name'],
'id' => $key,
'title' => $value['value']['title'],
'title' => $value['value']['title'] ?? '',
);
do_action( 'jetpack_widget_edited', $widget_data );
}

View File

@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for Protect.
* Logs BruteProtect failed logins via sync.
@ -50,7 +54,7 @@ class Protect extends Module {
return $brute_force_protection->has_login_ability();
}
// If the login ability can not be determined, the feature is not active,
// If the login ability cannot be determined, the feature is not active,
// or something is wrong, default to not syncing failed login attempts.
return false;
}

View File

@ -23,6 +23,10 @@
namespace Automattic\Jetpack\Sync\Modules;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for Jetpack Search.
*/
@ -437,6 +441,10 @@ class Search extends Module {
'_wc_general_product_dependency_theme' => array(),
'_wc_general_product_dependency_plugin' => array(),
'wpcom_marketplace_product_extra_fields' => array(),
'wccom_product_search_keywords' => array( 'searchable_in_all_content' => true ),
'_wccom_product_faqs' => array( 'searchable_in_all_content' => true ),
'wccom_product_features' => array( 'searchable_in_all_content' => true ),
'wccom_product_compatibility' => array( 'searchable_in_all_content' => true ),
); // end indexed post meta.
@ -764,6 +772,7 @@ class Search extends Module {
'translation_priority',
// woocommerce.
'documentation_category',
'pa_accessory-type',
'pa_actor',
'pa_age',
@ -802,6 +811,7 @@ class Search extends Module {
'pa_colour',
'pa_compactor',
'pa_condition',
'pa_conditions-options',
'pa_cor',
'pa_couleur',
'pa_country',
@ -870,6 +880,7 @@ class Search extends Module {
'pa_high-blow-tank',
'pa_hoehe',
'pa_inhoud',
'pa_interchange-part-number',
'pa_isadultproduct',
'pa_isbn',
'pa_iseligiblefortradein',
@ -1243,6 +1254,7 @@ class Search extends Module {
'group',
'group-documents-category',
'groups',
'guest',
'hashtags',
'hotel_facility',
'ia_invited_groups',
@ -1321,6 +1333,7 @@ class Search extends Module {
'organization',
'our_team_category',
'page_category',
'page_condition',
'parisrestaurant',
'parissauna',
'partner_category',
@ -1733,6 +1746,9 @@ class Search extends Module {
'kb_category',
'kb_tag',
// coolhunting.com
'article-type',
); // end taxonomies.
/**

View File

@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Heartbeat;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for stats.
*/
@ -29,30 +33,20 @@ class Stats extends Module {
*
* @access public
*
* @param callable $callback Action handler callable.
* @param callable $callback Unused - required for parent class compatibility.
*/
public function init_listeners( $callback ) {
public function init_listeners( $callback ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
add_action( 'jetpack_heartbeat', array( $this, 'sync_site_stats' ), 20 );
add_action( 'jetpack_sync_heartbeat_stats', $callback );
}
/**
* This namespaces the action that we sync.
* So that we can differentiate it from future actions.
* Send Heartbeat stats immediately.
*
* @access public
*/
public function sync_site_stats() {
do_action( 'jetpack_sync_heartbeat_stats' );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_sync_heartbeat_stats', array( $this, 'add_stats' ) );
$this->send_action( 'jetpack_sync_heartbeat_stats', $this->add_stats() );
}
/**

View File

@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Listener;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for term relationships.
*/

View File

@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for terms.
*/
@ -204,7 +208,7 @@ class Terms extends Module {
public function get_where_sql( $config ) {
$where_sql = Settings::get_blacklisted_taxonomies_sql();
if ( is_array( $config ) ) {
if ( is_array( $config ) && ! empty( $config ) ) {
$where_sql .= ' AND term_taxonomy_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
@ -339,12 +343,14 @@ class Terms extends Module {
list( $term_taxonomy_ids, $previous_end ) = $args;
return array(
// @phan-suppress-next-line PhanAccessMethodInternal @phan-suppress-current-line UnusedSuppression -- Fixed in WP 6.9, but then we need a suppression for the WP 6.8 compat run. @todo Remove this suppression when we drop WP <6.9.
'terms' => get_terms(
array(
'hide_empty' => false,
'term_taxonomy_id' => $term_taxonomy_ids,
'orderby' => 'term_taxonomy_id',
'order' => 'DESC',
'taxonomy' => array(),
'term_taxonomy_id' => $term_taxonomy_ids,
)
),
'previous_end' => $previous_end,

View File

@ -7,6 +7,10 @@
namespace Automattic\Jetpack\Sync\Modules;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for themes.
*/
@ -509,17 +513,23 @@ class Themes extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @param array $status This module Full Sync status.
* @param int $send_until The timestamp until the current request can send.
* @param array $state This module Full Sync status.
* @param int $started The timestamp when the full sync started.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// we call this instead of do_action when sending immediately.
$this->send_action( 'jetpack_full_sync_theme_data', array( true ) );
$result = $this->send_action( 'jetpack_full_sync_theme_data', array( true ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
if ( is_wp_error( $result ) ) {
$status['error'] = true;
return $status;
}
$status['finished'] = true;
$status['sent'] = $status['total'];
return $status;
}
/**
@ -528,7 +538,7 @@ class Themes extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
@ -735,6 +745,7 @@ class Themes extends Module {
$moved_to_inactive_ids = array();
$moved_to_sidebar = array();
$new_inactive_widgets = $new_value['wp_inactive_widgets'] ?? array();
foreach ( $new_value as $sidebar => $new_widgets ) {
if ( in_array( $sidebar, array( 'array_version', 'wp_inactive_widgets' ), true ) ) {
@ -747,8 +758,7 @@ class Themes extends Module {
if ( ! is_array( $new_widgets ) ) {
$new_widgets = array();
}
$moved_to_inactive_recently = $this->sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $new_value['wp_inactive_widgets'] );
$moved_to_inactive_recently = $this->sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $new_inactive_widgets );
$moved_to_inactive_ids = array_merge( $moved_to_inactive_ids, $moved_to_inactive_recently );
$moved_to_sidebar_recently = $this->sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar );

View File

@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for updates.
*/
@ -230,9 +234,10 @@ class Updates extends Module {
switch ( $transient ) {
case 'update_plugins':
if ( ! empty( $update->response ) && is_array( $update->response ) ) {
foreach ( $update->response as $plugin_slug => $response ) {
if ( ! empty( $plugin_slug ) && isset( $response->new_version ) ) {
$updates[] = array( $plugin_slug => $response->new_version );
foreach ( $update->response as $plugin_slug => $plugin_data ) {
$plugin_data = (array) $plugin_data;
if ( ! empty( $plugin_slug ) && isset( $plugin_data['new_version'] ) ) {
$updates[] = array( $plugin_slug => $plugin_data['new_version'] );
}
}
}
@ -247,9 +252,10 @@ class Updates extends Module {
break;
case 'update_themes':
if ( ! empty( $update->response ) && is_array( $update->response ) ) {
foreach ( $update->response as $theme_slug => $response ) {
if ( ! empty( $theme_slug ) && isset( $response['new_version'] ) ) {
$updates[] = array( $theme_slug => $response['new_version'] );
foreach ( $update->response as $theme_slug => $theme_data ) {
$theme_data = (array) $theme_data;
if ( ! empty( $theme_slug ) && isset( $theme_data['new_version'] ) ) {
$updates[] = array( $theme_slug => $theme_data['new_version'] );
}
}
}
@ -261,12 +267,12 @@ class Updates extends Module {
break;
case 'update_core':
if ( ! empty( $update->updates ) && is_array( $update->updates ) ) {
foreach ( $update->updates as $response ) {
if ( ! empty( $response->response ) && 'latest' === $response->response ) {
foreach ( $update->updates as $core_update ) {
if ( ! empty( $core_update->response ) && 'latest' === $core_update->response ) {
continue;
}
if ( ! empty( $response->response ) && isset( $response->packages->full ) ) {
$updates[] = array( $response->response => $response->packages->full );
if ( ! empty( $core_update->response ) && isset( $core_update->packages->full ) ) {
$updates[] = array( $core_update->response => $core_update->packages->full );
}
}
}
@ -389,17 +395,23 @@ class Updates extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @param array $status This module Full Sync status.
* @param int $send_until The timestamp until the current request can send.
* @param array $state This module Full Sync status.
* @param int $started The timestamp when the full sync started.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// we call this instead of do_action when sending immediately.
$this->send_action( 'jetpack_full_sync_updates', array( true ) );
$result = $this->send_action( 'jetpack_full_sync_updates', array( true ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
if ( is_wp_error( $result ) ) {
$status['error'] = true;
return $status;
}
$status['finished'] = true;
$status['sent'] = $status['total'];
return $status;
}
/**
@ -408,7 +420,7 @@ class Updates extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
@ -502,12 +514,16 @@ class Updates extends Module {
}
if ( ! is_array( $args[0]->response ) ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error( 'Warning: Not an Array as expected but -> ' . wp_json_encode( $args[0]->response ) . ' instead', E_USER_WARNING );
trigger_error( 'Warning: Not an Array as expected but -> ' . wp_json_encode( $args[0]->response, JSON_UNESCAPED_SLASHES ) . ' instead', E_USER_WARNING );
return $args;
}
foreach ( $args[0]->response as $stylesheet => &$theme_data ) {
$theme = wp_get_theme( $stylesheet );
$theme_data['name'] = $theme->name;
$theme_data = (array) $theme_data;
// Make sure the theme data array is not empty and has data that would indicate it is in the correct format.
if ( isset( $theme_data['theme'] ) ) {
$theme = wp_get_theme( $stylesheet );
$theme_data['name'] = $theme->name;
}
}
return $args;
}

View File

@ -7,10 +7,15 @@
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use Automattic\Jetpack\Password_Checker;
use Automattic\Jetpack\Sync\Defaults;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for users.
*/
@ -137,7 +142,7 @@ class Users extends Module {
add_action( 'deleted_user', array( $this, 'deleted_user_handler' ), 10, 2 );
add_action( 'jetpack_deleted_user', $callable, 10, 3 );
add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 2 );
add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 3 );
add_action( 'jetpack_removed_user_from_blog', $callable, 10, 2 );
// User roles.
@ -249,6 +254,8 @@ class Users extends Module {
$user->locale = get_user_locale( $user->ID );
}
$user->is_connected = ( new Manager( 'jetpack' ) )->is_user_connected( $user->ID );
return $user;
}
@ -357,7 +364,11 @@ class Users extends Module {
* @param string $user_login The user login.
* @param \WP_User $user The user object.
*/
public function wp_login_handler( $user_login, $user ) {
public function wp_login_handler( $user_login, $user = null ) {
if ( ! $user instanceof \WP_User || empty( $user->ID ) ) {
return;
}
/**
* Fires when a user is logged into a site.
*
@ -621,7 +632,9 @@ class Users extends Module {
);
// The jetpack_sync_register_user payload is identical to jetpack_sync_save_user, don't send both.
if ( $this->is_create_user() || $this->is_add_user_to_blog() ) {
if ( $this->is_function_in_backtrace(
array_merge( $this->get_create_user_functions(), $this->get_add_user_to_blog_functions() )
) ) {
return;
}
/**
@ -692,7 +705,9 @@ class Users extends Module {
$this->add_flags( $user_id, array( 'capabilities_changed' => true ) );
}
if ( $this->is_create_user() || $this->is_add_user_to_blog() || $this->is_delete_user() ) {
if ( $this->is_function_in_backtrace(
array_merge( $this->get_create_user_functions(), $this->get_add_user_to_blog_functions(), $this->get_delete_user_functions() )
) ) {
return;
}
@ -728,7 +743,7 @@ class Users extends Module {
* @todo Refactor to prepare the SQL query before executing it.
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) {
global $wpdb;
@ -760,7 +775,7 @@ class Users extends Module {
$query = "meta_key = '{$wpdb->prefix}user_level' AND meta_value > 0";
// The $config variable is a list of user IDs to sync.
if ( is_array( $config ) ) {
if ( is_array( $config ) && ! empty( $config ) ) {
$query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
@ -832,16 +847,17 @@ class Users extends Module {
*
* @access public
*
* @param int $user_id ID of the user.
* @param int $blog_id ID of the blog.
* @param int $user_id ID of the user.
* @param int $blog_id ID of the blog.
* @param int $reassign ID of the user to whom to reassign posts.
*/
public function remove_user_from_blog_handler( $user_id, $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function remove_user_from_blog_handler( $user_id, $blog_id, $reassign = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// User is removed on add, see https://github.com/WordPress/WordPress/blob/0401cee8b36df3def8e807dd766adc02b359dfaf/wp-includes/ms-functions.php#L2114.
if ( $this->is_add_new_user_to_blog() ) {
return;
}
$reassigned_user_id = $this->get_reassigned_network_user_id();
$reassigned_user_id = $reassign;
// Note that we are in the context of the blog the user is removed from, see https://github.com/WordPress/WordPress/blob/473e1ba73bc5c18c72d7f288447503713d518790/wp-includes/ms-functions.php#L233.
/**
@ -868,61 +884,40 @@ class Users extends Module {
}
/**
* Whether we're adding an existing user to a blog in this request.
* Get the function names that indicate a user is being created.
*
* @access protected
*
* @return boolean
* @return array
*/
protected function is_add_user_to_blog() {
return $this->is_function_in_backtrace( 'add_user_to_blog' );
}
/**
* Whether we're removing a user from a blog in this request.
*
* @access protected
*
* @return boolean
*/
protected function is_delete_user() {
return $this->is_function_in_backtrace( array( 'wp_delete_user', 'remove_user_from_blog' ) );
}
/**
* Whether we're creating a user or adding a new user to a blog.
*
* @access protected
*
* @return boolean
*/
protected function is_create_user() {
$functions = array(
protected function get_create_user_functions() {
return array(
'add_new_user_to_blog', // Used to suppress jetpack_sync_save_user in save_user_cap_handler when user registered on multi site.
'wp_create_user', // Used to suppress jetpack_sync_save_user in save_user_role_handler when user registered on multi site.
'wp_insert_user', // Used to suppress jetpack_sync_save_user in save_user_cap_handler and save_user_role_handler when user registered on single site.
);
return $this->is_function_in_backtrace( $functions );
}
/**
* Retrieve the ID of the user the removed user's posts are reassigned to (if any).
* Get the function names that indicate a user is being added to a blog.
*
* @return int ID of the user that got reassigned as the author of the posts.
* @access protected
*
* @return array
*/
protected function get_reassigned_network_user_id() {
$backtrace = debug_backtrace( false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
foreach ( $backtrace as $call ) {
if (
'remove_user_from_blog' === $call['function'] &&
3 === count( $call['args'] )
) {
return $call['args'][2];
}
}
protected function get_add_user_to_blog_functions() {
return array( 'add_user_to_blog' );
}
return false;
/**
* Get the function names that indicate a user is being deleted.
*
* @access protected
*
* @return array
*/
protected function get_delete_user_functions() {
return array( 'wp_delete_user', 'remove_user_from_blog' );
}
/**
@ -934,7 +929,7 @@ class Users extends Module {
* @return bool
*/
protected function is_function_in_backtrace( $names ) {
$backtrace = debug_backtrace( false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
if ( ! is_array( $names ) ) {
$names = array( $names );
}

View File

@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Adds WooCommerce HPOS specific data to sync when HPOS is enabled on the site.
*/
@ -115,7 +119,7 @@ class WooCommerce_HPOS_Orders extends Module {
public function init_listeners( $callable ) {
foreach ( self::get_order_types_to_sync() as $type ) {
add_action( "woocommerce_after_{$type}_object_save", $callable );
add_filter( "jetpack_sync_before_enqueue_woocommerce_after_{$type}_object_save", array( $this, 'expand_order_object' ) );
add_filter( "jetpack_sync_before_enqueue_woocommerce_after_{$type}_object_save", array( $this, 'on_before_enqueue_order_save' ) );
}
add_action( 'woocommerce_delete_order', $callable );
add_action( 'woocommerce_delete_subscription', $callable );
@ -144,6 +148,10 @@ class WooCommerce_HPOS_Orders extends Module {
* @access public
*/
public function init_before_send() {
// Incremental Sync
foreach ( self::get_order_types_to_sync() as $type ) {
add_filter( "jetpack_sync_before_send_woocommerce_after_{$type}_object_save", array( $this, 'expand_order_object' ) );
}
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_hpos_orders', array( $this, 'build_full_sync_action_array' ) );
}
@ -189,7 +197,7 @@ class WooCommerce_HPOS_Orders extends Module {
}
/**
* Retrieves multiple orders data by their ID.
* Retrieves multiple orders data by their ID. Sorted by ID in descending order.
*
* @access public
*
@ -209,6 +217,8 @@ class WooCommerce_HPOS_Orders extends Module {
'type' => self::get_order_types_to_sync( true ),
'post_status' => self::get_all_possible_order_status_keys(),
'limit' => -1,
'orderby' => 'ID',
'order' => 'DESC',
)
);
@ -256,6 +266,29 @@ class WooCommerce_HPOS_Orders extends Module {
);
}
/**
* Retrieve filtered order data before sending.
*
* @access public
*
* @param array $args An array with order data.
*
* @return array|false
*/
public function expand_order_object( $args ) {
if ( empty( $args['id'] ) ) {
return false;
}
$order_object = wc_get_order( $args['id'] );
if ( ! $order_object instanceof \WC_Abstract_Order ) {
return false;
}
return $this->filter_order_data( $order_object );
}
/**
* Retrieve order data by its ID.
*
@ -263,9 +296,12 @@ class WooCommerce_HPOS_Orders extends Module {
*
* @param array $args Order ID.
*
* @return array
* @return array|false
*/
public function expand_order_object( $args ) {
public function on_before_enqueue_order_save( $args ) {
// Prevent multiple triggers on a single request.
static $processed = array();
if ( ! is_array( $args ) || ! isset( $args[0] ) ) {
return false;
}
@ -279,7 +315,19 @@ class WooCommerce_HPOS_Orders extends Module {
return false;
}
return $this->filter_order_data( $order_object );
$order_id = $order_object->get_id();
if ( empty( $order_id ) ) {
return false;
}
if ( isset( $processed[ $order_id ] ) ) {
return false;
}
$processed[ $order_id ] = true;
return array( 'id' => $order_id );
}
/**
@ -478,7 +526,7 @@ class WooCommerce_HPOS_Orders extends Module {
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- We return all order count for full sync, so confit is not required.
global $wpdb;

View File

@ -0,0 +1,567 @@
<?php
/**
* WooCommerce Products sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use DateTimeZone;
use WC_DateTime;
use WP_Error;
/**
* Class to handle sync for WooCommerce Products table.
*
* Note: This module is currently used for analytics purposes only.
*/
class WooCommerce_Products extends Module {
const PRODUCT_POST_TYPES = array( 'product', 'product_variation' );
/**
* Constructor.
*/
public function __construct() {
_deprecated_class( 'WooCommerce_Products', '4.24.0', 'Automattic\Jetpack\Sync\Modules\Posts' );
// Preprocess action to be sent by Jetpack sync for wp_delete_post.
add_action( 'delete_post', array( $this, 'action_wp_delete_post' ), 10, 1 );
add_action( 'trashed_post', array( $this, 'action_wp_trash_post' ), 10, 1 );
}
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'woocommerce_products';
}
/**
* The table in the database with the prefix.
*
* @access public
*
* @return string|bool
*/
public function table() {
global $wpdb;
return $wpdb->prefix . 'wc_product_meta_lookup';
}
/**
* The id field in the database.
*
* @access public
*
* @return string
*/
public function id_field() {
return 'product_id';
}
/**
* The full sync action name for this module.
*
* @access public
*
* @return string
*/
public function full_sync_action_name() {
return 'jetpack_full_sync_woocommerce_products';
}
/**
* Initialize WooCommerce Products action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
// Listen to product creation and updates - these hooks trigger products table updates
add_action( 'woocommerce_new_product', $callable, 10, 1 );
add_action( 'woocommerce_update_product', $callable, 10, 1 );
// Listen to variation creation and updates (they also affect products table)
add_action( 'woocommerce_new_product_variation', $callable, 10, 1 );
add_action( 'woocommerce_update_product_variation', $callable, 10, 1 );
// Listen to specific stock update.
add_action( 'woocommerce_updated_product_stock', $callable, 10, 1 );
// Listen to product trashed.
add_action( 'jetpack_sync_woocommerce_product_trashed', $callable, 10, 1 );
// Listen to product deletion via wp_delete_post (more reliable than WC hooks)
add_action( 'jetpack_sync_woocommerce_product_deleted', $callable, 10, 1 );
// Add filters to expand product data before sync
add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_product', array( $this, 'expand_product_data' ) );
add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_product', array( $this, 'expand_product_data' ) );
add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_product_variation', array( $this, 'expand_product_data' ) );
add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_product_variation', array( $this, 'expand_product_data' ) );
add_filter( 'jetpack_sync_before_enqueue_woocommerce_updated_product_stock', array( $this, 'expand_product_data' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_woocommerce_product_trashed', array( $this, 'expand_product_data' ) );
}
/**
* Initialize WooCommerce Products action listeners for full sync.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_woocommerce_products', $callable );
}
/**
* Retrieve the actions that will be sent for this module during a full sync.
*
* @access public
*
* @return array Full sync actions of this module.
*/
public function get_full_sync_actions() {
return array( 'jetpack_full_sync_woocommerce_products' );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_products', array( $this, 'build_full_sync_action_array' ) );
}
/**
* Handle wp_delete_post action and trigger custom product deletion sync for WooCommerce products.
*
* @param int $post_id The post ID being deleted.
*/
public function action_wp_delete_post( $post_id ) {
if ( $this->is_a_product_post( $post_id ) ) {
/**
* Fires when a WooCommerce product is deleted via wp_delete_post.
*
* @param int $post_id The product ID being deleted.
*/
do_action( 'jetpack_sync_woocommerce_product_deleted', $post_id );
}
}
/**
* Handle wp_trash_post action and trigger custom product trashed sync for WooCommerce products.
*
* @param int $post_id The post ID being trashed.
*/
public function action_wp_trash_post( $post_id ) {
if ( $this->is_a_product_post( $post_id ) ) {
/**
* Fires when a WooCommerce product is trashed via wp_trash_post.
*
* @param int $post_id The product ID being trashed.
*/
do_action( 'jetpack_sync_woocommerce_product_trashed', $post_id );
}
}
/**
* Expand product data to include products table information.
*
* @param array $args The hook arguments.
* @return array $args The hook arguments with expanded data.
*/
public function expand_product_data( $args ) {
if ( empty( $args[0] ) ) {
return $args;
}
$product_id = $args[0];
// Get the product data
$product_data = $this->get_product_by_ids( array( $product_id ) );
if ( ! empty( $product_data ) ) {
$args[1] = reset( $product_data ); // Get the first (and only) result
}
return $args;
}
/**
* Enqueue the WooCommerce Products actions for full sync.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @param int $max_items_to_enqueue Maximum number of items to enqueue.
* @param boolean $state True if full sync has finished enqueueing this module, false otherwise.
* @return array Number of actions enqueued, and next module state.
*/
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
return $this->enqueue_all_ids_as_action(
'jetpack_full_sync_woocommerce_products',
$this->table(),
'product_id',
$this->get_where_sql( $config ),
$max_items_to_enqueue,
$state
);
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM {$this->table()} WHERE " . $this->get_where_sql( $config );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$count = (int) $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
/**
* Return a list of objects by their type and IDs
*
* @param string $object_type Object type.
* @param array $ids IDs of objects to return.
*
* @access public
*
* @return array|object|WP_Error|null
*/
public function get_objects_by_id( $object_type, $ids ) {
if ( 'product' !== $object_type || empty( $ids ) || ! is_array( $ids ) ) {
return array();
}
return $this->get_product_by_ids( $ids );
}
/**
* Returns a list of product objects by their IDs.
*
* @param array $ids List of product IDs to fetch.
* @param string $order Either 'ASC' or 'DESC'.
*
* @access public
*
* @return array|object|null
*/
public function get_product_by_ids( $ids, $order = '' ) {
if ( ! is_array( $ids ) ) {
return array();
}
// Make sure the IDs are numeric and are non-zero.
$ids = array_filter( array_map( 'intval', $ids ) );
if ( empty( $ids ) ) {
return array();
}
$posts = $this->get_product_posts( $ids, $order );
$product_types = $this->get_product_types( $ids, $order );
$products = array();
// Build base product data from posts.
foreach ( $posts as $post ) {
$products[ $post->ID ] = array(
'product_id' => $post->ID,
'title' => $post->post_title,
'post_status' => $post->post_status,
'slug' => $post->post_name,
'date_created' => $this->datetime_to_object( $post->post_date ),
'date_modified' => $this->datetime_to_object( $post->post_modified ),
);
$post_type = $post->post_type;
// ProductType::VARIATION and ProductType::SIMPLE have only existed since WooCommerce 9.7, so
// we can't rely on that existing, but using the strings is probably safe enough.
if ( 'product_variation' === $post_type ) {
$product_type = 'variation';
} elseif ( 'product' === $post_type ) {
$product_type = $product_types[ $post->ID ] ?? 'simple';
} else {
$product_type = null;
}
$products[ $post->ID ]['type'] = $product_type;
}
// Merge in product meta data.
$product_meta_data = $this->get_product_meta_data( $ids, $order );
foreach ( $product_meta_data as $meta ) {
$product_id = $meta['product_id'];
if ( isset( $products[ $product_id ] ) ) {
$products[ $product_id ] = array_merge( $products[ $product_id ], $meta );
} else {
$products[ $product_id ] = $meta;
}
}
// Add COGS data.
$cogs_data = $this->get_product_cogs_data( $ids, $order );
foreach ( $cogs_data as $product_id => $cogs_value ) {
if ( ! isset( $products[ $product_id ] ) ) {
$products[ $product_id ] = array();
}
$products[ $product_id ]['cogs_amount'] = $cogs_value;
}
return $products;
}
/**
* Build the full sync action object for WooCommerce products.
*
* @access public
*
* @param array $args An array with the product data and the previous end.
*
* @return array An array with the product data and the previous end.
*/
public function build_full_sync_action_array( $args ) {
list( $filtered_product, $previous_end ) = $args;
return array(
'product' => $filtered_product['objects'],
'previous_end' => $previous_end,
);
}
/**
* Given the Module Configuration and Status return the next chunk of items to send.
*
* @param array $config This module Full Sync configuration.
* @param array $status This module Full Sync status.
* @param int $chunk_size Chunk size.
*
* @return array
*/
public function get_next_chunk( $config, $status, $chunk_size ) {
$product_ids = parent::get_next_chunk( $config, $status, $chunk_size );
if ( empty( $product_ids ) ) {
return array();
}
// Fetch the product data in DESC order for the next chunk logic to work.
$product_data = $this->get_product_by_ids( $product_ids, 'DESC' );
// If no data was fetched, make sure to return the expected structure so that status is updated correctly.
if ( empty( $product_data ) ) {
return array(
'object_ids' => $product_ids,
'objects' => array(),
);
}
// Filter the product data based on the maximum size constraints.
// We don't have separate metadata, so we pass empty array for metadata.
list( $filtered_product_ids, $filtered_product_data, ) = $this->filter_objects_and_metadata_by_size(
'product',
$product_data,
array(), // No separate metadata for products table
0, // No individual meta size limit since we don't have separate metadata
self::MAX_SIZE_FULL_SYNC
);
return array(
'object_ids' => $filtered_product_ids,
'objects' => $filtered_product_data,
);
}
/**
* Get the product meta data from the product meta lookup table.
*
* @param array $ids List of product IDs to fetch.
* @param string $order Either 'ASC' or 'DESC'.
*
* @return array
*/
private function get_product_meta_data( $ids, $order = '' ) {
global $wpdb;
// Prepare the placeholders for the prepared query below.
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
$query = "SELECT * FROM {$this->table()} WHERE product_id IN ( $placeholders )";
if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
$query .= " ORDER BY product_id $order";
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Hardcoded query, no user variable
$product_meta_data = $wpdb->get_results( $wpdb->prepare( $query, $ids ), ARRAY_A );
if ( ! is_array( $product_meta_data ) ) {
return array();
}
return $product_meta_data;
}
/**
* Get the product data from the posts table.
*
* @param array $ids List of product IDs to fetch.
* @param string $order Either 'ASC' or 'DESC'.
*
* @return array
*/
private function get_product_posts( $ids, $order = '' ) {
$posts = get_posts(
array(
'include' => $ids,
'order' => $order,
'post_type' => self::PRODUCT_POST_TYPES,
'post_status' => array( 'any', 'trash', 'auto-draft' ),
'numberposts' => -1, // Get all posts.
)
);
return $posts;
}
/**
* Get the product cogs data from the product meta lookup table.
*
* @param array $ids List of product IDs to fetch.
* @param string $order Either 'ASC' or 'DESC'.
*
* @return array
*/
private function get_product_cogs_data( $ids, $order = '' ) {
// @phan-suppress-current-line UnusedPluginSuppression @phan-suppress-next-line PhanUndeclaredClassMethod -- we're checking for the class (around since WooCommerce 7.1) before calling the method (introduced as part of the original class). See also: https://github.com/phan/phan/issues/1204
$is_cogs_enabled = class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) && \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'cost_of_goods_sold' );
if ( ! $is_cogs_enabled ) {
return array();
}
global $wpdb;
// Prepare the placeholders for the prepared query below.
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
$query = "
SELECT post_id, meta_value
FROM {$wpdb->postmeta}
WHERE post_id IN ( $placeholders )
AND meta_key = '_cogs_total_value'
";
if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
$query .= " ORDER BY post_id $order";
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Hardcoded query, no user variable
$results = $wpdb->get_results( $wpdb->prepare( $query, $ids ), ARRAY_A );
if ( ! is_array( $results ) ) {
return array();
}
$product_cogs_data = array();
foreach ( $results as $result ) {
$cogs_value = '' === $result['meta_value'] ? null : (float) $result['meta_value'];
$product_cogs_data[ $result['post_id'] ] = $cogs_value;
}
return $product_cogs_data;
}
/**
* Get product types for multiple product IDs in bulk.
*
* @param array $ids List of product IDs to fetch types for.
* @param string $order Either 'ASC' or 'DESC'.
*
* @return array Array of product_id => product_type mapping.
*/
private function get_product_types( $ids, $order = '' ) {
if ( empty( $ids ) ) {
return array();
}
global $wpdb;
// Bulk load term relationships and term data
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
$query = "
SELECT tr.object_id, t.name
FROM {$wpdb->term_relationships} tr
INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
WHERE tr.object_id IN ( $placeholders )
AND tt.taxonomy = 'product_type'
";
if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
$query .= " ORDER BY tr.object_id $order";
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Hardcoded query, no user variable
$results = $wpdb->get_results( $wpdb->prepare( $query, $ids ) );
if ( ! is_array( $results ) ) {
return array();
}
$product_types = array();
foreach ( $results as $result ) {
$product_types[ $result->object_id ] = sanitize_title( $result->name );
}
return $product_types;
}
/**
* Convert the WC_DateTime objects to stdClass objects to ensure they are properly encoded.
*
* @param WC_DateTime|mixed $wc_datetime The datetime object.
* @param bool $utc Whether to convert to UTC.
* @return object|null
*/
private function datetime_to_object( $wc_datetime, $utc = false ) {
if ( is_string( $wc_datetime ) ) {
$wc_datetime = new WC_DateTime( $wc_datetime, new DateTimeZone( wc_timezone_string() ) );
}
if ( is_a( $wc_datetime, 'WC_DateTime' ) ) {
if ( $utc ) {
$wc_datetime->setTimezone( new DateTimeZone( 'UTC' ) );
} else {
$wc_datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
}
return (object) (array) $wc_datetime;
}
return null;
}
/**
* Check if the post is a product post.
*
* @param int $post_id The post ID to check.
* @return bool True if the post is a product post, false otherwise.
*/
private function is_a_product_post( $post_id ) {
$post_type = get_post_type( $post_id );
return in_array( $post_type, self::PRODUCT_POST_TYPES, true );
}
}

View File

@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
use WC_Order;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for WooCommerce.
*/
@ -125,7 +129,6 @@ class WooCommerce extends Module {
add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 );
add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) );
add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_order_item', array( $this, 'filter_order_item' ) );
add_filter( 'jetpack_sync_whitelisted_comment_types', array( $this, 'add_review_comment_types' ) );
// Blacklist Action Scheduler comment types.
@ -166,10 +169,10 @@ class WooCommerce extends Module {
// Order items.
add_action( 'woocommerce_new_order_item', $callable, 10, 4 );
add_action( 'woocommerce_update_order_item', $callable, 10, 4 );
add_action( 'woocommerce_delete_order_item', $callable, 10, 1 );
add_action( 'woocommerce_remove_order_item_ids', $callable, 10, 1 );
$this->init_listeners_for_meta_type( 'order_item', $callable );
$this->init_meta_whitelist_handler( 'order_item', array( $this, 'filter_meta' ) );
// Payment tokens.
add_action( 'woocommerce_new_payment_token', $callable, 10, 1 );
@ -182,6 +185,7 @@ class WooCommerce extends Module {
add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 );
// Tax rates.
// These are ignored on WP.com: tax items are derived from order data via wc_order_tax_lookup, which isnt present there.
add_action( 'woocommerce_tax_rate_added', $callable, 10, 2 );
add_action( 'woocommerce_tax_rate_updated', $callable, 10, 2 );
add_action( 'woocommerce_tax_rate_deleted', $callable, 10, 1 );
@ -238,6 +242,38 @@ class WooCommerce extends Module {
return $args;
}
/**
* Handler for filtering out non-whitelisted order item meta.
*
* @since 4.22.3
*
* @param array $args Hook arguments.
* @return array|false False if not whitelisted, the original hook args otherwise.
*/
public function filter_meta( $args ) {
if (
! empty( $args[2] ) && $this->is_whitelisted_order_item_meta( $args[2] )
) {
return $args;
}
return false;
}
/**
* Whether an order item meta key is whitelisted for sync.
*
* @access public
*
* @since 4.22.3
*
* @param string $meta_key Order item meta key.
* @return bool True if whitelisted.
*/
public function is_whitelisted_order_item_meta( $meta_key ) {
return is_string( $meta_key ) && in_array( $meta_key, self::$order_item_meta_whitelist, true );
}
/**
* Retrieve the order item ids to be removed and send them as one action
*
@ -292,15 +328,13 @@ class WooCommerce extends Module {
*
* @access public
*
* @todo Refactor table name to use a $wpdb->prepare placeholder.
*
* @param int $order_item_id Order item ID.
* @return object Order item.
*/
public function build_order_item( $order_item_id ) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->order_item_table_name WHERE order_item_id = %d", $order_item_id ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct database access is intentional; caching is not required for this query.
return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE order_item_id = %d', $this->order_item_table_name, $order_item_id ) );
}
/**
@ -325,7 +359,7 @@ class WooCommerce extends Module {
* @todo Refactor the SQL query to use $wpdb->prepare().
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) {
global $wpdb;
@ -467,8 +501,12 @@ class WooCommerce extends Module {
'woocommerce_api_enabled',
'woocommerce_allow_tracking',
'woocommerce_task_list_hidden',
'woocommerce_onboarding_profile',
'woocommerce_cod_settings',
'woocommerce_store_address',
'woocommerce_store_address_2',
'woocommerce_store_city',
'woocommerce_store_postcode',
'woocommerce_admin_install_timestamp',
);
/**

View File

@ -7,6 +7,10 @@
namespace Automattic\Jetpack\Sync\Modules;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for WP_Super_Cache.
*/

View File

@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Modules;
use WP_Error;
use WP_User_Query;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle Table Checksums for the User Meta table.
*/

View File

@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Replicastore;
use Exception;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle Table Checksums for the Users table.
*/

View File

@ -213,18 +213,14 @@ class Table_Checksum {
'range_field' => 'comment_ID',
'key_fields' => array( 'comment_ID' ),
'checksum_fields' => array( 'comment_date_gmt' ),
'filter_values' => array(
'comment_type' => array(
'operator' => 'IN',
'values' => apply_filters(
'jetpack_sync_whitelisted_comment_types',
array( '', 'comment', 'trackback', 'pingback', 'review' )
'filter_values' => array_merge(
Sync\Settings::get_allowed_comment_types_structured(),
array(
'comment_approved' => array(
'operator' => 'NOT IN',
'values' => array( 'spam' ),
),
),
'comment_approved' => array(
'operator' => 'NOT IN',
'values' => array( 'spam' ),
),
)
),
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'comments' );
@ -289,6 +285,21 @@ class Table_Checksum {
),
'links' => $wpdb->links, // TODO describe in the array format or add exceptions.
'options' => $wpdb->options, // TODO describe in the array format or add exceptions.
'wc_product_lookup' => array( // wc_product_lookup is a table in the cache database
'table' => $wpdb->posts,
'range_field' => 'ID',
'key_fields' => array( 'ID' ),
'checksum_fields' => array( 'post_modified_gmt' ),
'filter_values' => array(
'post_type' => array(
'operator' => 'IN',
'values' => array( 'product', 'product_variation' ),
),
),
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'woocommerce_products' );
},
),
'woocommerce_order_items' => array(
'table' => "{$wpdb->prefix}woocommerce_order_items",
'range_field' => 'order_item_id',
@ -306,7 +317,9 @@ class Table_Checksum {
'parent_table' => 'woocommerce_order_items',
'parent_join_field' => 'order_item_id',
'table_join_field' => 'order_item_id',
'is_table_enabled_callback' => 'Automattic\Jetpack\Sync\Replicastore\Table_Checksum::enable_woocommerce_tables',
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'meta' ) && self::enable_woocommerce_tables();
},
),
'wc_orders' => array(
'table' => "{$wpdb->prefix}wc_orders",
@ -398,14 +411,14 @@ class Table_Checksum {
protected function prepare_fields( $table_configuration ) {
$this->key_fields = $table_configuration['key_fields'];
$this->range_field = $table_configuration['range_field'];
$this->checksum_fields = isset( $table_configuration['checksum_fields'] ) ? $table_configuration['checksum_fields'] : array();
$this->checksum_text_fields = isset( $table_configuration['checksum_text_fields'] ) ? $table_configuration['checksum_text_fields'] : array();
$this->filter_values = isset( $table_configuration['filter_values'] ) ? $table_configuration['filter_values'] : null;
$this->checksum_fields = $table_configuration['checksum_fields'] ?? array();
$this->checksum_text_fields = $table_configuration['checksum_text_fields'] ?? array();
$this->filter_values = $table_configuration['filter_values'] ?? null;
$this->additional_filter_sql = ! empty( $table_configuration['filter_sql'] ) ? $table_configuration['filter_sql'] : '';
$this->parent_table = isset( $table_configuration['parent_table'] ) ? $table_configuration['parent_table'] : null;
$this->parent_join_field = isset( $table_configuration['parent_join_field'] ) ? $table_configuration['parent_join_field'] : $table_configuration['range_field'];
$this->table_join_field = isset( $table_configuration['table_join_field'] ) ? $table_configuration['table_join_field'] : $table_configuration['range_field'];
$this->is_table_enabled_callback = isset( $table_configuration['is_table_enabled_callback'] ) ? $table_configuration['is_table_enabled_callback'] : false;
$this->parent_table = $table_configuration['parent_table'] ?? null;
$this->parent_join_field = $table_configuration['parent_join_field'] ?? $table_configuration['range_field'];
$this->table_join_field = $table_configuration['table_join_field'] ?? $table_configuration['range_field'];
$this->is_table_enabled_callback = $table_configuration['is_table_enabled_callback'] ?? false;
}
/**
@ -654,7 +667,10 @@ class Table_Checksum {
$filter_stamenet = $this->build_filter_statement( $range_from, $range_to, $filter_values );
$join_statement = '';
if ( $this->parent_table ) {
// On WPCOM the checksum comparison does not use the parent table INNER JOIN.
// WPCOM sets parent_table in its config solely for the count optimization in
// get_range_edges(), so we skip the JOIN to avoid query differences.
if ( $this->parent_table && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
$parent_table_obj = new Table_Checksum( $this->parent_table );
$parent_filter_query = $parent_table_obj->build_filter_statement( null, null, null, 'parent_table' );
@ -724,19 +740,26 @@ class Table_Checksum {
$this->validate_fields( array( $this->range_field ) );
// Performance :: When getting the postmeta range we do not want to filter by the whitelist.
// The reason for this is that it leads to a non-performant query that can timeout.
// Instead lets get the range based on posts regardless of meta.
// Performance :: For meta tables (postmeta, commentmeta, termmeta, woocommerce_order_itemmeta)
// we strip the filter_values (e.g. meta_key whitelist) when building the range edges query.
// These filters cause non-performant queries that can timeout on large tables.
// The actual data filtering happens during checksum calculation — via the filter_values
// WHERE clause and, when enabled, the parent table INNER JOIN.
$is_meta_table = in_array(
$this->table,
array( $wpdb->postmeta, $wpdb->commentmeta, $wpdb->termmeta, "{$wpdb->prefix}woocommerce_order_itemmeta" ),
true
);
$filter_values = $this->filter_values;
if ( $wpdb->postmeta === $this->table ) {
if ( $is_meta_table ) {
$this->filter_values = null;
}
// `trim()` to make sure we don't add the statement if it's empty.
$filters = trim( $this->build_filter_statement( $range_from, $range_to ) );
// Reset Post meta filter.
if ( $wpdb->postmeta === $this->table ) {
// Restore filter values.
if ( $is_meta_table ) {
$this->filter_values = $filter_values;
}
@ -766,6 +789,33 @@ class Table_Checksum {
* If `$limit` is not specified, we can directly use the table.
*/
if ( ! $limit ) {
// For tables that would use COUNT(DISTINCT), avoid the expensive full table scan
// by using the parent table's count instead. Only for full-table calls — sub-range
// calls need the actual COUNT(DISTINCT) scoped to the range, and those are cheap
// because the WHERE clause limits the scan.
if ( $distinct_count && null === $range_from && null === $range_to ) {
$parent_count = $this->get_parent_table_count();
if ( (int) $parent_count > 0 ) {
$min_max_query = "
SELECT
MIN({$this->range_field}) as min_range,
MAX({$this->range_field}) as max_range
FROM
{$this->table}
{$filter_statement}
";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$result = $wpdb->get_row( $min_max_query, ARRAY_A );
if ( $result && is_array( $result ) ) {
$result['item_count'] = $parent_count;
self::$range_edges_cache[ $this->table ] = $result;
return $result;
}
}
}
$query .= "
{$this->table}
{$filter_statement}
@ -798,9 +848,93 @@ class Table_Checksum {
throw new Exception( 'Unable to get range edges' );
}
// Cache full-range results so child meta tables can reuse the parent's count.
// Only cache when no range constraints — sub-range counts would pollute the cache.
if ( ! $limit && null === $range_from && null === $range_to ) {
self::$range_edges_cache[ $this->table ] = $result;
}
return $result;
}
/**
* Static cache for range edge results, keyed by table name.
*
* When checksum_all() processes tables sequentially, the parent table's
* get_range_edges() result is cached so child tables can reuse the
* item_count without re-querying.
*
* @var array
*/
private static $range_edges_cache = array();
/**
* Reset the static range edges cache.
*
* Should be called when the underlying data changes and cached
* counts may be stale (e.g. between test runs).
*/
public static function reset_range_edges_cache() {
self::$range_edges_cache = array();
}
/**
* Get the row count from the parent table as an approximate item count.
*
* For tables with compound keys or non-unique range fields, COUNT(DISTINCT range_field)
* causes expensive full table scans. Since item_count is only used for bucket sizing
* in checksum_histogram(), the parent table's row count is an acceptable approximation.
* In typical cases the parent count >= the distinct child count, producing slightly
* more (smaller) buckets. The caller guards against a zero parent count (e.g. orphaned
* child rows) by falling back to the original COUNT(DISTINCT) query.
*
* Returns false when the parent table's count is not a reliable proxy (e.g.
* term_taxonomy, whose count does not correlate with distinct range_field values
* in terms, termmeta, or term_relationships).
*
* Uses a static cache so that if the parent table was already processed
* (e.g. posts before postmeta in checksum_all), no additional query is needed.
*
* @return int|false The parent table row count, or false if not applicable.
*/
private function get_parent_table_count() {
if ( ! $this->parent_table ) {
return false;
}
// term_taxonomy's count is not a reliable proxy for the distinct range_field
// values in terms, termmeta, or term_relationships.
if ( 'term_taxonomy' === $this->parent_table ) {
return false;
}
try {
$parent_table_obj = new Table_Checksum( $this->parent_table );
} catch ( Exception $e ) {
return false;
}
// Check static cache first — the parent may have been queried already
// (e.g. posts processed before postmeta in checksum_all).
if ( isset( self::$range_edges_cache[ $parent_table_obj->table ] ) ) {
return (int) self::$range_edges_cache[ $parent_table_obj->table ]['item_count'];
}
// Query the parent table's range edges. For single-key parent tables this is
// a simple COUNT (no DISTINCT), so it's fast.
try {
$parent_range = $parent_table_obj->get_range_edges();
if ( is_array( $parent_range ) && isset( $parent_range['item_count'] ) ) {
return (int) $parent_range['item_count'];
}
return false;
} catch ( Exception $e ) {
return false;
}
}
/**
* Update the results to have key/checksum format.
*

View File

@ -67,35 +67,37 @@ class Queue_Storage_Options {
* Fetch items from the queue.
*
* @param int|null $item_count How many items to fetch from the queue.
* The parameter is null-able, if no limit on the amount of items.
* Null for no limit.
* @param string $order Sort direction for the items. Accepts 'ASC' or 'DESC'.
* Any other value will be treated as 'ASC'.
*
* @return array|object|\stdClass[]|null
* @return array|object|null Array of result objects on success, or null on failure.
*/
public function fetch_items( $item_count ) {
public function fetch_items( $item_count, $order = 'ASC' ) {
global $wpdb;
// TODO make it more simple for the $item_count
$order = 'DESC' === $order ? 'DESC' : 'ASC';
$sql_order = "ORDER BY option_name {$order}";
$sql = "SELECT option_name AS id, option_value AS value
FROM $wpdb->options
WHERE option_name LIKE %s
{$sql_order}";
$params = array( "jpsq_{$this->queue_id}-%" );
if ( $item_count ) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
$items = $wpdb->get_results(
$wpdb->prepare(
"SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d",
"jpsq_{$this->queue_id}-%",
$item_count
),
OBJECT
);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
$items = $wpdb->get_results(
$wpdb->prepare(
"SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC",
"jpsq_{$this->queue_id}-%"
),
OBJECT
);
$sql .= ' LIMIT %d';
$params[] = $item_count;
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$items = $wpdb->get_results(
$wpdb->prepare( $sql, $params ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
OBJECT
);
return $items;
}

View File

@ -228,55 +228,38 @@ class Queue_Storage_Table {
* Fetch items from the queue.
*
* @param int|null $item_count How many items to fetch from the queue.
* The parameter is null-able, if no limit on the amount of items.
* Null for no limit.
* @param string $order Sort direction for the items. Accepts 'ASC' or 'DESC'.
* Any other value will be treated as 'ASC'.
*
* @return object[]|null
* @return array|object|null Array of result objects on success, or null on failure.
*/
public function fetch_items( $item_count ) {
public function fetch_items( $item_count, $order = 'ASC' ) {
global $wpdb;
/**
* Ignoring the linting warning, as there's still no placeholder replacement for DB field name,
* in this case this is `$this->table_name`
*/
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$order = ( 'DESC' === $order ) ? 'DESC' : 'ASC';
$sql_order = "ORDER BY event_id {$order}";
$sql = "
SELECT
event_id AS id,
event_payload AS value
FROM {$this->table_name}
WHERE queue_id LIKE %s
{$sql_order}
";
$params = array( $this->queue_id );
// TODO make it more simple for the $item_count
if ( $item_count ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$items = $wpdb->get_results(
$wpdb->prepare(
"
SELECT
event_id AS id,
event_payload AS value
FROM {$this->table_name}
WHERE queue_id LIKE %s
ORDER BY event_id ASC
LIMIT %d
",
$this->queue_id,
$item_count
)
);
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$items = $wpdb->get_results(
$wpdb->prepare(
"
SELECT
event_id AS id,
event_payload AS value
FROM {$this->table_name}
WHERE queue_id LIKE %s
ORDER BY event_id ASC
",
$this->queue_id
)
);
$sql .= ' LIMIT %d';
$params[] = $item_count;
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$items = $wpdb->get_results(
$wpdb->prepare( $sql, $params ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
return $items;
}