installed plugin Jetpack Protect version 1.0.2

This commit is contained in:
2022-07-28 18:42:13 +00:00
committed by Gitium
parent d55c4af45c
commit a3483bf62f
286 changed files with 64090 additions and 0 deletions

View File

@ -0,0 +1,809 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.37.0] - 2022-07-26
### Changed
- Jetpack Sync: Add Sync lock related info in Sync debug details. [#25140]
- Updated package dependencies. [#25158]
### Fixed
- Dedicated Sync: Enable sending of callables outside of admin context, since Dedicated Sync requests always work outside of admin scope. [#25143]
## [1.36.1] - 2022-07-06
### Added
- Add new WordPress core `block-templates` theme feature to `Defaults::$default_theme_support_whitelist` [#24960]
## [1.36.0] - 2022-06-28
### Added
- Posts: added a Sync call to make sure post content is up to date before publishing. [#24827]
### Changed
- Minimum Sync Config: Update required modules and options [#24831]
### Fixed
- Sync Table Checksums: Table checksum should be enabled depending on corresponding Sync modulee [#24772]
## [1.35.2] - 2022-06-21
### Changed
- Renaming master to trunk. [#24661]
## [1.35.1] - 2022-06-14
### Added
- Add a request lock to prevent multiple requests being spawned at once [#24734]
### Changed
- Updated package dependencies. [#24529]
## [1.35.0] - 2022-05-30
### Changed
- Sync: Add '_jetpack_blogging_prompt_key' to default post meta whitelist
## [1.34.0] - 2022-05-24
### Changed
- Dedicated Sync - Introduce custom endpoint for spawning Sync requests [#24468]
- Sync: Add 'active_modules' to default whitelisted callables. [#24453]
## [1.33.1] - 2022-05-19
### Removed
- Removed dedicated sync custom endpoints pending error investigation [#24419]
## [1.33.0] - 2022-05-18
### Changed
- Dedicated Sync: Introduce custom endpoint for spawning Sync requests [#24344]
## [1.32.0] - 2022-05-10
### Added
- Search: add search options to option whitelist [#24167]
## [1.31.1] - 2022-05-04
### Changed
- Updated package dependencies. [#24095]
- WordPress 6.1 Compatibilty [#24083]
### Deprecated
- Moved the options class into Connection. [#24095]
## [1.31.0] - 2022-04-26
### Added
- Adds filter to get_themes callable
### Deprecated
- Removed Heartbeat by hoisting it into Connection.
## [1.30.8] - 2022-04-19
### Added
- Added get_themes Callable to sync the list of installed themes on a site
- Added get_themes to Sync defaults
### Changed
- PHPCS: Fix `WordPress.Security.ValidatedSanitizedInput`
- Updated package dependencies.
## [1.30.7] - 2022-04-12
### Added
- Adding new site option to be synced.
## [1.30.6] - 2022-04-06
### Changed
- Updated package dependencies.
### Fixed
- Dedicated Sync: Only try to run the sender once if Dedicated Sync is enabled as it has its own requeueing mechanism.
## [1.30.5] - 2022-03-29
### Changed
- Microperformance: Use === null instead of is_null
## [1.30.4] - 2022-03-23
### Changed
- Enable syncing of dedicated_sync_enabled Sync setting
### Fixed
- Dedicated Sync: Allow spawning request with expired Retry-After
## [1.30.3] - 2022-03-15
### Changed
- Search Sync Settings :: Add ETB taxonomy to allow list.
## [1.30.2] - 2022-03-08
### Changed
- Disallow syncing of _term_meta post_type
## [1.30.1] - 2022-03-02
### Added
- Dedicated Sync flow: Allow enabling or disabling via WPCOM response header
## [1.30.0] - 2022-02-22
### Added
- Add Sync dedicated request flow.
### Changed
- Updated package dependencies.
## [1.29.2] - 2022-02-09
### Added
- Allow sync package consumers to provide custom data settings.
### Fixed
- Fixed some new PHPCS warnings.
## [1.29.1] - 2022-02-02
### Changed
- Updated package dependencies.
## [1.29.0] - 2022-01-25
### Added
- Jetpack Search: update the allowed post meta when search is active to include all indexable meta.
## [1.28.2] - 2022-01-18
### Changed
- Updated package dependencies.
## [1.28.1] - 2022-01-13
### Changed
- Updated package dependencies.
## [1.28.0] - 2022-01-04
### Changed
- Listener: Do not enqueue actions when the site is disconnected
- Switch to pcov for code coverage.
- Theme deletions: rely on Core WP hook now that the package requires WP 5.8.
- Updated package dependencies
- Updated package textdomain from `jetpack` to `jetpack-sync`.
## [1.27.6] - 2021-12-14
### Changed
- Updated package dependencies.
## [1.27.5] - 2021-11-30
### Changed
- Updated package dependencies.
## [1.27.4] - 2021-11-23
### Changed
- Updated package dependencies.
## [1.27.3] - 2021-11-16
### Changed
- Actions: add the do_only_first_initial_sync method which starts an initial sync only when one hasn't already been done
## [1.27.2] - 2021-11-09
### Added
- Constants: Now syncing Atomic platform constant
### Changed
- Full Sync : limit included users to contributors and above (based on wp_user_limit)
- Updated package dependencies.
- User Checksums - limit scope to users with wp_user_level > 0
### Fixed
- Fix PHP 8.1 deprecation warnings.
## [1.27.1] - 2021-11-02
### Changed
- Set `convertDeprecationsToExceptions` true in PHPUnit config.
- Update PHPUnit configs to include just what needs coverage rather than include everything then try to exclude stuff that doesn't.
## [1.27.0] - 2021-10-26
### Added
- Added the _wpas_feature_enabled meta key to the sync list
- Sync Error Log to capture failed sync requests.
### Fixed
- Check the return value of get_comment() before to use it.
- Increase send timeout to 20 seconds allowing capture of WP.com 408 responses.
## [1.26.4] - 2021-10-13
### Changed
- Sync Checksums: Convert text fields to latin1 before generating checksum.
- Updated package dependencies.
### Fixed
- Sync Checksums - Update distinct clause to use $wpdb-> table names to accouunt for differences in prefixes.
## [1.26.3] - 2021-10-12
### Changed
- Updated package dependencies
### Removed
- Remove initialization of the identity-crisis package. That will be handled by the Config package.
### Fixed
- Reduce transient expiration for how often we check the state of the queue.
- Sync Checksums - exclude locale from checksum if same as site setting
- Sync Checksums - use distinct query when calculating count of Term Relationships
## [1.26.2] - 2021-09-28
### Added
- Add support for checksumming user-related tabled: wp_users and wp_usermeta
### Changed
- Update annotations versions.
- Updated package dependencies.
### Fixed
- Resolve indirect modification notice.
- Sync Checksums: utilize distinct clause in term counts.
- Sync Queue: better handling of serialization issues and empty actions.
## [1.26.1] - 2021-09-03
### Fixed
- Add better checks if the WooCommerce tables should be enabled for checksum/fix.
- Prevent PHP notices on queue_pull if all args are not set.
## [1.26.0] - 2021-08-30
### Added
- Add support for WooCommerce table to the checksum/fix process.
- Enable support for utf8 conversion during checksum calculation.
### Changed
- Don't run composer install on regular phpunit script
- Tests: update PHPUnit polyfills dependency (yoast/phpunit-polyfills).
### Fixed
- Sync Checksums - ensure last object is included in histogram
## [1.25.0] - 2021-08-12
### Added
- Add package version tracking.
- Add `wpcom_is_fse_activated` to sync list
- Made /sync/object endpoint accessible over POST, not only GET, to allow fetching more items in a single request.
## [1.24.2] - 2021-08-02
- Reverted: Sync option for the Carousel to display colorized slide background.
## [1.24.1] - 2021-07-29
### Changed
- Utilize an import for WP_Error in all instances.
### Fixed
- Fixed unqualified WP_Error use in the Rest_Sender class.
## [1.24.0] - 2021-07-27
### Added
- Add a package version constant.
- Add Full Site Editing support to callback options.
- Sync option for the Carousel to display colorized slide background.
### Fixed
- Update Sender so it adheres to max upload bytes when not encoding items.
## [1.23.3] - 2021-07-16
### Fixed
- Update Options module to return jetpack_sync_settings_* values from the Settings class vs direct option lookup.
## [1.23.2] - 2021-07-13
### Changed
- Updated package dependencies.
### Fixed
- Performance of Sync checksums degraded with the update to correlated subquery. This restricts its usage to term_taxonomy joins only."
## [1.23.1] - 2021-07-01
### Changed
- Checksum parent_table joins need distinct selection to account for possibility of multiple rows.
### Fixed
- Update term_taxonomy checksum query to an allowed list vs disallowed
## [1.23.0] - 2021-06-29
### Added
- Add jetpack_idc_disonnect action to clear Sync options on disconnect.
- Add support to callables to sync/object endpoint.
- Enable sync/object endpoint support for theme-info.
- Enhance updates module to support get_objects_by_id.
- Expand sync/object to support constants.
- Extend sync/object to support callables.
- Implement v4 REST endpoints.
- Initialize the IDC package in the Sync package.
### Removed
- Remove product_cat from blocked taxonomies
## [1.22.0] - 2021-06-15
### Changed
- Sync: Adding the Identity_Crisis package.
- Updated package dependencies.
### Deprecated
- Deprecated URL methods in `Automattic\Jetpack\Sync\Functions` in favor of `Automattic\Jetpack\Connection\Urls`.
## [1.21.3] - 2021-05-25
### Changed
- Performance: If no Full Sync is in process early return before we update options.
### Fixed
- Janitorial: avoid PHP notices in some edge-cases
- Update Meta Module so get_object_by_id returns all meta values.
## [1.21.2] - 2021-04-27
### Added
- Added the password-checker package the the Sync package composer.json file.
### Changed
- Updated package dependencies.
### Fixed
- Sync: removed references to the JETPACK__PLUGIN_DIR constant.
- Sync Checksums : updated postmeta range query performance #19337.
## [1.21.1] - 2021-03-30
### Added
- Composer alias for dev-master, to improve dependencies
- Implement a 60 second back-off for non-200 respones, if no retry-after header is present in the response.
- Impose a max limit of 2MB on post meta values that are synced.
- Impose a max limit of 5MB on post_content that can be synced.
### Changed
- Sync: Use the new Password_Checker package instead of Jetpack_Password_Checker.
- Update package dependencies.
- Use the Heartbeat package to generate the stats array
### Fixed
- Migrate locks to update_option to avaoid memcache inconsistencies that can be introduced by delete_option usage.
- Update Sync Queue so that serialize is wrapped to catch errors
## [1.21.0] - 2021-02-23
- General: update WordPress version requirements to WP 5.6
- Update Checksums to support blacklisted taxonomies.
- Refactor Jetpack callables into the plugin using existing filter jetpack_sync_callable_whitelist
- Wrap call_user_func in is_callable so that we don't trigger warnings for callables that don't exist.
- Sync: Trigger initial sync on jetpack_site_registered
- Update Comments checksum field to comment_date_gmt. We cannot use comment_content directly due to charset/filters.
- Deprecate jetpack_json_wrap
- Remove Sync's usage of wp_startswith
## [1.20.2] - 2021-02-08
- Update dependencies to latest stable
## [1.20.1] - 2021-01-28
- Update dependencies to latest stable
## [1.20.0] - 2021-01-26
- Sync Concurrency / Race Conditions
- Sync: Prevent an PHP warning
- Jetpack Sync: Checksums: Use a better way to fetch and validate fields against table
- Add mirror-repo information to all current composer packages
- Full Sync :: Reduce Concurrency.
- Monorepo: Reorganize all projects
- Various PHPCS and Cleanup
## [1.19.4] - 2021-01-18
- Update dependencies to latest stable
## [1.19.3] - 2021-01-18
- Full Sync :: Reduce Concurrency.
## [1.19.2] - 2020-12-21
- Update the do_full_sync function to early return if we are in SYNC READ ONLY mode.
- Return an empty array if the specified range is empty. (It was returning the checksum for the WHOLE dataset).
## [1.19.1] - 2020-12-17
## [1.19.0] - 2020-12-17
- sync: Improve sync checksum algorithm and endpoints
- wp_get_environment_type as callable.
- Disallow amp_validated_url as it is not site content but instead validation errors for amp mark-up.
- Whitelist (allow) jetpack_sync_settings_* options to be synced
- Re-order Sync default option whitelist (allowlist)
## [1.18.1] - 2020-11-24
- Version packages for release
## [1.18.0] - 2020-11-24
- Migrate jetpack_published_post to wp_after_insert_post hook
- Check value to determine if we should enable sync after an action enqueuement.
- General: update minimum required version to WordPress 5.5
- Fix remaining phpcs warnings in most of requirelist
- Update access of comment_status_to_approval_value to allow extension.
- Update get_term Replicastore function to handle term_taxonomy_id option
- Update get_terms to utilize ensure_taxonomy so that the Taxonomy is registered.
- Addtion of note on explict return of null instead of false if option not found.
- Alignment of comment_status_to_approval_value function. Addition of post-trashed status and cleanup of cases.
- Alignment with implemenations. Call ensure_taxonomy to ensure Taxonomies have been initialized.
- Call ensure_taxonomy within get_object_terms so that the taxonomy is registered before action is performed.
- Updated PHPCS: Packages and Debugger
## [1.17.2] - 2020-11-05
- Update dependencies to latest stable
## [1.17.1] - 2020-10-29
- Update dependencies to latest stable
## [1.17.0] - 2020-10-27
- 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
- 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.
- Make XMLRPC methods available for blog token
## [1.16.4] - 2020-10-14
- Update dependencies to latest stable
## [1.16.3] - 2020-10-09
- Update dependencies to latest stable
## [1.16.2] - 2020-10-06
- Update dependencies to latest stable
## [1.16.1] - 2020-10-01
- Update dependencies to latest stable
## [1.16.0] - 2020-09-29
- Publicize: Allow publishing a post as a Twitter thread.
- props @jmdodd - filter out set_object_terms actions that don't perform any update. Includes unit tests.
- Sort Arrays by keys before generating callable checksums
- Packages: avoid PHPCS warnings
- Adding 'review' to whitelisted comment types
- Disable Sync sending on Backup API Requests
- Sync: stop trying to check for edit_comment capability
- Added options to sync wc whitelist
- Sync: Improve theme support syncing
## [1.15.1] - 2020-09-09
- Update dependencies to latest stable
## [1.15.0] - 2020-08-26
- Sync: add Creative Mail configuration option to synced options
- Extend sync_status endpoint with optional debug_details field
- REST API endpoints: expand management endpoints
- Sync: Fix nonce action string in theme edit sync
- WP 5.5 Compat: Align Jetpack and Core's plugin autoupdates
- use current user token to updateRole request
- Resolve Sync Errors from empty edge case and WP.com returning concurrent_request_error
- Rework Sender to properly return state during do_full_sync
## [1.14.4] - 2020-08-10
- WP 5.5 Compat: Align Jetpack and Core's plugin autoupdates
## [1.14.3] - 2020-08-10
- Update dependencies to latest stable
## [1.14.2] - 2020-08-10
- Update dependencies to latest stable
## [1.14.1] - 2020-08-10
- Resolve Sync Errors from empty edge case and WP.com returning concurrent_request_error
## [1.14.0] - 2020-07-28
- Core Compat: Site Environment
- Unit Tests: fix tests according to changes in Core
- Utilize the blog token vs master user token to send sync actions.
## [1.13.2] - 2020-07-06
- Update dependencies to latest stable
## [1.13.1] - 2020-07-01
- Update dependencies to latest stable
## [1.13.0] - 2020-06-30
- Block Flamingo Plugin post types in Jetpack Sync
- Explicit single execution of do_full_sync from cron
- Update to reference the property defined in the Jetpack Connection Manager class
- PHPCS: Clean up the packages
- WordAds: Add consent support for California Consumer Privacy Act (CCPA)
- Sync: Add additional support for theme_support_whitelist
## [1.12.4] - 2020-06-02
- Revert "Fix `jetpack sync start` CLI command (#16010)"
## [1.12.3] - 2020-06-01
- Update dependencies to latest stable
## [1.12.2] - 2020-06-01
- Fix `jetpack sync start` CLI command
## [1.12.1] - 2020-05-29
- Sync: Add additional support for theme_support_whitelist
## [1.12.0] - 2020-05-26
- Update ReplicaStore to call clean_comment_cache when comments are upserted or a reset is perofrmed.
- Store the list of active plugins that uses connection in an option
- Jetpack Sync :: Alternate non-blocking flow
- Settings - Writing: add a toggle to Carousel so users can hide comment area
- Sender needs to load consistently utilizing logic
- Always delete items from the queue even if the buffer is no longer checked out.
- Update the hook of Sync's Comment module to not send meta actions when the comment_type is not whitelisted.
- Sync Comments apply whitelist to all actions
## [1.11.0] - 2020-04-28
- Correct inline documentation "Array" type
- Filter out blacklisted post_types for deleted_post actions.
- Publicize: Add jetpack_publicize_options
- Blacklisting Post Types from Sync
- Comments: update default comment type
- Jetpack Sync: Split `jetpack_post_meta_batch_delete` in action to be called in chunks of 100 items, compared to all at once.
- Update Sync limits based on analysis of data loss events.
## [1.10.0] - 2020-03-31
- Update dependencies to latest stable
## [1.9.0] - 2020-03-31
- Debugger: Add sync health progress bar
- Add main network WPCOM blog ID to sync functions
- Masterbar: send wpcom user ID to wpcom when attempting to log…
- Sync: a better readme
## [1.8.0] - 2020-02-25
- Minileven: add options back as they still exist on sites
- Sync: add queue size to actions
- Mobile Theme: remove feature
## [1.7.6] - 2020-02-14
- get_sync_status does not properly account for unexpected states.
## [1.7.5] - 2020-02-14
- Empty Helper function for checkin handler
- Sync Health: fix excessive data loss reports
- Initial Sync Health Status Class and Data Loss Handler
- Stop REST API Log entries from being synced
## [1.7.4+vip] - 2020-02-14
- Empty Helper function for checkin handler
## [1.7.4] - 2020-01-23
- Sync Chunk Keys need to be unique
## [1.7.3] - 2020-01-20
- Sync: ensure we run the initial sync on new connections
## [1.7.2] - 2020-01-17
- Sync Package: use Full_Sync_Immediately by default
- Adding new managed WordPress hosts to be identified in class-functions.php.
## [1.7.1] - 2020-01-14
- Packages: Various improvements for wp.com or self-contained consumers
## [1.7.0] - 2020-01-14
- Trying to add deterministic initialization.
## [1.6.3] - 2020-01-07
- Fix git history.
## [1.6.2] - 2019-12-31
- Sync: Remove DEFAULT_SYNC_MODULES legacy map
- Connection: Loose Comparison for Port Number in Signatures
## [1.6.1] - 2019-12-13
- tweak default sync settings
## [1.6.0] - 2019-12-02
- Sync: Full Sync: Send immediately.
## [1.5.1] - 2019-11-26
- Marked the xmlrpc_api_url method as deprecated.
## [1.5.0] - 2019-11-25
- Remove sync settings cache
## [1.4.0] - 2019-11-19
- Full Sync: Don't allow more than one request to enqueue
- Sync: Update Max Int
## [1.3.4] - 2019-11-08
- Packages: Use classmap instead of PSR-4
## [1.3.3] - 2019-11-08
- Deprecate Jetpack::is_development_mode() in favor of the packaged Status()->is_development_mode()
## [1.3.2] - 2019-11-01
- Full Sync updates to allow full enqueuing of chunks.
## [1.3.1] - 2019-10-29
- PHPCS: Rest of the packages
## [1.3.0] - 2019-10-29
- Sync: Checkout Endpoint: Add `pop` argument 😱
## [1.2.1] - 2019-10-28
- Sync: Add Settings to enable/disable the sender for a particular queue
## [1.2.0] - 2019-10-24
- Sync: Fix how we enqueue term_relationships on full sync 🏝
- WP 5.3: Use modern wp_timezone
- Check for last_error when enqueuing IDs
## [1.1.1] - 2019-10-23
- Use spread operator instead of func_get_args
## [1.1.0] - 2019-10-07
- Sync: Ensure a post object is returned
- PHPCS: Sync Functions
- Sync: Bail initial sync if there is an ongoing full sync
## [1.0.2] - 2019-09-25
- Sync: Only allow white listed comment types to be inserted.
- Sync: Move sync_object XML-RPC method from connection to sync
- Sync: do not sync comments made via Action Scheduler
- Docs: Unify usage of @package phpdoc tags
## [1.0.1] - 2019-09-14
## 1.0.0 - 2019-09-14
- Packages: Move sync to a classmapped package
[1.37.0]: https://github.com/Automattic/jetpack-sync/compare/v1.36.1...v1.37.0
[1.36.1]: https://github.com/Automattic/jetpack-sync/compare/v1.36.0...v1.36.1
[1.36.0]: https://github.com/Automattic/jetpack-sync/compare/v1.35.2...v1.36.0
[1.35.2]: https://github.com/Automattic/jetpack-sync/compare/v1.35.1...v1.35.2
[1.35.1]: https://github.com/Automattic/jetpack-sync/compare/v1.35.0...v1.35.1
[1.35.0]: https://github.com/Automattic/jetpack-sync/compare/v1.34.0...v1.35.0
[1.34.0]: https://github.com/Automattic/jetpack-sync/compare/v1.33.1...v1.34.0
[1.33.1]: https://github.com/Automattic/jetpack-sync/compare/v1.33.0...v1.33.1
[1.33.0]: https://github.com/Automattic/jetpack-sync/compare/v1.32.0...v1.33.0
[1.32.0]: https://github.com/Automattic/jetpack-sync/compare/v1.31.1...v1.32.0
[1.31.1]: https://github.com/Automattic/jetpack-sync/compare/v1.31.0...v1.31.1
[1.31.0]: https://github.com/Automattic/jetpack-sync/compare/v1.30.8...v1.31.0
[1.30.8]: https://github.com/Automattic/jetpack-sync/compare/v1.30.7...v1.30.8
[1.30.7]: https://github.com/Automattic/jetpack-sync/compare/v1.30.6...v1.30.7
[1.30.6]: https://github.com/Automattic/jetpack-sync/compare/v1.30.5...v1.30.6
[1.30.5]: https://github.com/Automattic/jetpack-sync/compare/v1.30.4...v1.30.5
[1.30.4]: https://github.com/Automattic/jetpack-sync/compare/v1.30.3...v1.30.4
[1.30.3]: https://github.com/Automattic/jetpack-sync/compare/v1.30.2...v1.30.3
[1.30.2]: https://github.com/Automattic/jetpack-sync/compare/v1.30.1...v1.30.2
[1.30.1]: https://github.com/Automattic/jetpack-sync/compare/v1.30.0...v1.30.1
[1.30.0]: https://github.com/Automattic/jetpack-sync/compare/v1.29.2...v1.30.0
[1.29.2]: https://github.com/Automattic/jetpack-sync/compare/v1.29.1...v1.29.2
[1.29.1]: https://github.com/Automattic/jetpack-sync/compare/v1.29.0...v1.29.1
[1.29.0]: https://github.com/Automattic/jetpack-sync/compare/v1.28.2...v1.29.0
[1.28.2]: https://github.com/Automattic/jetpack-sync/compare/v1.28.1...v1.28.2
[1.28.1]: https://github.com/Automattic/jetpack-sync/compare/v1.28.0...v1.28.1
[1.28.0]: https://github.com/Automattic/jetpack-sync/compare/v1.27.6...v1.28.0
[1.27.6]: https://github.com/Automattic/jetpack-sync/compare/v1.27.5...v1.27.6
[1.27.5]: https://github.com/Automattic/jetpack-sync/compare/v1.27.4...v1.27.5
[1.27.4]: https://github.com/Automattic/jetpack-sync/compare/v1.27.3...v1.27.4
[1.27.3]: https://github.com/Automattic/jetpack-sync/compare/v1.27.2...v1.27.3
[1.27.2]: https://github.com/Automattic/jetpack-sync/compare/v1.27.1...v1.27.2
[1.27.1]: https://github.com/Automattic/jetpack-sync/compare/v1.27.0...v1.27.1
[1.27.0]: https://github.com/Automattic/jetpack-sync/compare/v1.26.4...v1.27.0
[1.26.4]: https://github.com/Automattic/jetpack-sync/compare/v1.26.3...v1.26.4
[1.26.3]: https://github.com/Automattic/jetpack-sync/compare/v1.26.2...v1.26.3
[1.26.2]: https://github.com/Automattic/jetpack-sync/compare/v1.26.1...v1.26.2
[1.26.1]: https://github.com/Automattic/jetpack-sync/compare/v1.26.0...v1.26.1
[1.26.0]: https://github.com/Automattic/jetpack-sync/compare/v1.25.0...v1.26.0
[1.25.0]: https://github.com/Automattic/jetpack-sync/compare/v1.24.2...v1.25.0
[1.24.2]: https://github.com/Automattic/jetpack-sync/compare/v1.24.1...v1.24.2
[1.24.1]: https://github.com/Automattic/jetpack-sync/compare/v1.24.0...v1.24.1
[1.24.0]: https://github.com/Automattic/jetpack-sync/compare/v1.23.3...v1.24.0
[1.23.3]: https://github.com/Automattic/jetpack-sync/compare/v1.23.2...v1.23.3
[1.23.2]: https://github.com/Automattic/jetpack-sync/compare/v1.23.1...v1.23.2
[1.23.1]: https://github.com/Automattic/jetpack-sync/compare/v1.23.0...v1.23.1
[1.23.0]: https://github.com/Automattic/jetpack-sync/compare/v1.22.0...v1.23.0
[1.22.0]: https://github.com/Automattic/jetpack-sync/compare/v1.21.3...v1.22.0
[1.21.3]: https://github.com/Automattic/jetpack-sync/compare/v1.21.2...v1.21.3
[1.21.2]: https://github.com/Automattic/jetpack-sync/compare/v1.21.1...v1.21.2
[1.21.1]: https://github.com/Automattic/jetpack-sync/compare/v1.21.0...v1.21.1
[1.21.0]: https://github.com/Automattic/jetpack-sync/compare/v1.20.2...v1.21.0
[1.20.2]: https://github.com/Automattic/jetpack-sync/compare/v1.20.1...v1.20.2
[1.20.1]: https://github.com/Automattic/jetpack-sync/compare/v1.20.0...v1.20.1
[1.20.0]: https://github.com/Automattic/jetpack-sync/compare/v1.19.4...v1.20.0
[1.19.4]: https://github.com/Automattic/jetpack-sync/compare/v1.19.3...v1.19.4
[1.19.3]: https://github.com/Automattic/jetpack-sync/compare/v1.19.2...v1.19.3
[1.19.2]: https://github.com/Automattic/jetpack-sync/compare/v1.19.1...v1.19.2
[1.19.1]: https://github.com/Automattic/jetpack-sync/compare/v1.19.0...v1.19.1
[1.19.0]: https://github.com/Automattic/jetpack-sync/compare/v1.18.1...v1.19.0
[1.18.1]: https://github.com/Automattic/jetpack-sync/compare/v1.18.0...v1.18.1
[1.18.0]: https://github.com/Automattic/jetpack-sync/compare/v1.17.2...v1.18.0
[1.17.2]: https://github.com/Automattic/jetpack-sync/compare/v1.17.1...v1.17.2
[1.17.1]: https://github.com/Automattic/jetpack-sync/compare/v1.17.0...v1.17.1
[1.17.0]: https://github.com/Automattic/jetpack-sync/compare/v1.16.4...v1.17.0
[1.16.4]: https://github.com/Automattic/jetpack-sync/compare/v1.16.3...v1.16.4
[1.16.3]: https://github.com/Automattic/jetpack-sync/compare/v1.16.2...v1.16.3
[1.16.2]: https://github.com/Automattic/jetpack-sync/compare/v1.16.1...v1.16.2
[1.16.1]: https://github.com/Automattic/jetpack-sync/compare/v1.16.0...v1.16.1
[1.16.0]: https://github.com/Automattic/jetpack-sync/compare/v1.15.1...v1.16.0
[1.15.1]: https://github.com/Automattic/jetpack-sync/compare/v1.15.0...v1.15.1
[1.15.0]: https://github.com/Automattic/jetpack-sync/compare/v1.14.4...v1.15.0
[1.14.4]: https://github.com/Automattic/jetpack-sync/compare/v1.14.3...v1.14.4
[1.14.3]: https://github.com/Automattic/jetpack-sync/compare/v1.14.2...v1.14.3
[1.14.2]: https://github.com/Automattic/jetpack-sync/compare/v1.14.1...v1.14.2
[1.14.1]: https://github.com/Automattic/jetpack-sync/compare/v1.14.0...v1.14.1
[1.14.0]: https://github.com/Automattic/jetpack-sync/compare/v1.13.2...v1.14.0
[1.13.2]: https://github.com/Automattic/jetpack-sync/compare/v1.13.1...v1.13.2
[1.13.1]: https://github.com/Automattic/jetpack-sync/compare/v1.13.0...v1.13.1
[1.13.0]: https://github.com/Automattic/jetpack-sync/compare/v1.12.4...v1.13.0
[1.12.4]: https://github.com/Automattic/jetpack-sync/compare/v1.12.3...v1.12.4
[1.12.3]: https://github.com/Automattic/jetpack-sync/compare/v1.12.2...v1.12.3
[1.12.2]: https://github.com/Automattic/jetpack-sync/compare/v1.12.1...v1.12.2
[1.12.1]: https://github.com/Automattic/jetpack-sync/compare/v1.12.0...v1.12.1
[1.12.0]: https://github.com/Automattic/jetpack-sync/compare/v1.11.0...v1.12.0
[1.11.0]: https://github.com/Automattic/jetpack-sync/compare/v1.10.0...v1.11.0
[1.10.0]: https://github.com/Automattic/jetpack-sync/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/Automattic/jetpack-sync/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/Automattic/jetpack-sync/compare/v1.7.6...v1.8.0
[1.7.6]: https://github.com/Automattic/jetpack-sync/compare/v1.7.5...v1.7.6
[1.7.5]: https://github.com/Automattic/jetpack-sync/compare/v1.7.4+vip...v1.7.5
[1.7.4+vip]: https://github.com/Automattic/jetpack-sync/compare/v1.7.4...v1.7.4+vip
[1.7.4]: https://github.com/Automattic/jetpack-sync/compare/v1.7.3...v1.7.4
[1.7.3]: https://github.com/Automattic/jetpack-sync/compare/v1.7.2...v1.7.3
[1.7.2]: https://github.com/Automattic/jetpack-sync/compare/v1.7.1...v1.7.2
[1.7.1]: https://github.com/Automattic/jetpack-sync/compare/v1.7.0...v1.7.1
[1.7.0]: https://github.com/Automattic/jetpack-sync/compare/v1.6.3...v1.7.0
[1.6.3]: https://github.com/Automattic/jetpack-sync/compare/v1.6.2...v1.6.3
[1.6.2]: https://github.com/Automattic/jetpack-sync/compare/v1.6.1...v1.6.2
[1.6.1]: https://github.com/Automattic/jetpack-sync/compare/v1.6.0...v1.6.1
[1.6.0]: https://github.com/Automattic/jetpack-sync/compare/v1.5.1...v1.6.0
[1.5.1]: https://github.com/Automattic/jetpack-sync/compare/v1.5.0...v1.5.1
[1.5.0]: https://github.com/Automattic/jetpack-sync/compare/v1.4.0...v1.5.0
[1.4.0]: https://github.com/Automattic/jetpack-sync/compare/v1.3.4...v1.4.0
[1.3.4]: https://github.com/Automattic/jetpack-sync/compare/v1.3.3...v1.3.4
[1.3.3]: https://github.com/Automattic/jetpack-sync/compare/v1.3.2...v1.3.3
[1.3.2]: https://github.com/Automattic/jetpack-sync/compare/v1.3.1...v1.3.2
[1.3.1]: https://github.com/Automattic/jetpack-sync/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/Automattic/jetpack-sync/compare/v1.2.1...v1.3.0
[1.2.1]: https://github.com/Automattic/jetpack-sync/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/Automattic/jetpack-sync/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/Automattic/jetpack-sync/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/Automattic/jetpack-sync/compare/v1.0.2...v1.1.0
[1.0.2]: https://github.com/Automattic/jetpack-sync/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/Automattic/jetpack-sync/compare/v1.0.0...v1.0.1

View File

@ -0,0 +1,357 @@
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@ -0,0 +1,38 @@
# Security Policy
Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/).
## Supported Versions
Generally, only the latest version of Jetpack has continued support. If a critical vulnerability is found in the current version of Jetpack, we may opt to backport any patches to previous versions.
## Reporting a Vulnerability
[Jetpack](https://jetpack.com/) is an open-source plugin for WordPress. Our HackerOne program covers the plugin software, as well as a variety of related projects and infrastructure.
**For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.**
Our most critical targets are:
* Jetpack and the Jetpack composer packages (all within this repo)
* Jetpack.com -- the primary marketing site.
* cloud.jetpack.com -- a management site.
* wordpress.com -- the shared management site for both Jetpack and WordPress.com sites.
For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic).
_Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._
## Guidelines
We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production:
* Please **setup a local environment** instead whenever possible. Most of our code is open source (see above).
* If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC.
* **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels.
* To be eligible for a bounty, all of these guidelines must be followed.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties.

View File

@ -0,0 +1,57 @@
{
"name": "automattic/jetpack-sync",
"description": "Everything needed to allow syncing to the WP.com infrastructure.",
"type": "jetpack-library",
"license": "GPL-2.0-or-later",
"require": {
"automattic/jetpack-connection": "^1.41",
"automattic/jetpack-constants": "^1.6",
"automattic/jetpack-identity-crisis": "^0.8",
"automattic/jetpack-password-checker": "^0.2",
"automattic/jetpack-roles": "^1.4",
"automattic/jetpack-status": "^1.14"
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.2",
"yoast/phpunit-polyfills": "1.0.3",
"automattic/wordbless": "@dev"
},
"autoload": {
"classmap": [
"src/"
]
},
"scripts": {
"phpunit": [
"./vendor/phpunit/phpunit/phpunit --colors=always"
],
"test-coverage": [
"php -dpcov.directory=. ./vendor/bin/phpunit --coverage-clover \"$COVERAGE_DIR/clover.xml\""
],
"test-php": [
"@composer phpunit"
],
"post-update-cmd": "php -r \"copy('vendor/automattic/wordbless/src/dbless-wpdb.php', 'wordpress/wp-content/db.php');\""
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-sync",
"textdomain": "jetpack-sync",
"version-constants": {
"::PACKAGE_VERSION": "src/class-package-version.php"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-sync/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.37.x-dev"
}
},
"config": {
"allow-plugins": {
"roots/wordpress-core-installer": true
}
}
}

View File

@ -0,0 +1,415 @@
<?php
/**
* The Data Settings class.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* The Data_Settings class
*/
class Data_Settings {
/**
* The data that must be synced for every synced site.
*/
const MUST_SYNC_DATA_SETTINGS = array(
'jetpack_sync_modules' => array(
'Automattic\\Jetpack\\Sync\\Modules\\Callables',
'Automattic\\Jetpack\\Sync\\Modules\\Constants',
'Automattic\\Jetpack\\Sync\\Modules\\Full_Sync_Immediately', // enable Initial Sync on Site Connection.
'Automattic\\Jetpack\\Sync\\Modules\\Options',
),
'jetpack_sync_callable_whitelist' => array(
'site_url' => array( 'Automattic\\Jetpack\\Connection\\Urls', 'site_url' ),
'home_url' => array( 'Automattic\\Jetpack\\Connection\\Urls', 'home_url' ),
'get_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins' ),
'get_themes' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_themes' ),
'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_constants_whitelist' => array(
'ABSPATH',
'ALTERNATE_WP_CRON',
'ATOMIC_CLIENT_ID',
'AUTOMATIC_UPDATER_DISABLED',
'DISABLE_WP_CRON',
'DISALLOW_FILE_EDIT',
'DISALLOW_FILE_MODS',
'EMPTY_TRASH_DAYS',
'FS_METHOD',
'IS_PRESSABLE',
'PHP_VERSION',
'WP_ACCESSIBLE_HOSTS',
'WP_AUTO_UPDATE_CORE',
'WP_CONTENT_DIR',
'WP_CRON_LOCK_TIMEOUT',
'WP_DEBUG',
'WP_HTTP_BLOCK_EXTERNAL',
'WP_MAX_MEMORY_LIMIT',
'WP_MEMORY_LIMIT',
'WP_POST_REVISIONS',
),
'jetpack_sync_options_whitelist' => array(
/**
* Sync related options
*/
'jetpack_sync_non_blocking',
'jetpack_sync_non_public_post_stati',
'jetpack_sync_settings_comment_meta_whitelist',
'jetpack_sync_settings_post_meta_whitelist',
'jetpack_sync_settings_post_types_blacklist',
'jetpack_sync_settings_taxonomies_blacklist',
'jetpack_sync_settings_dedicated_sync_enabled',
/**
* Connection related options
*/
'jetpack_connection_active_plugins',
/**
* Generic site options
*/
'blog_charset',
'blog_public',
'blogdescription',
'blogname',
'permalink_structure',
'stylesheet',
'time_format',
'timezone_string',
),
);
const MODULE_FILTER_MAPPING = array(
'Automattic\\Jetpack\\Sync\\Modules\\Options' => array(
'jetpack_sync_options_whitelist',
'jetpack_sync_options_contentless',
),
'Automattic\\Jetpack\\Sync\\Modules\\Constants' => array(
'jetpack_sync_constants_whitelist',
),
'Automattic\\Jetpack\\Sync\\Modules\\Callables' => array(
'jetpack_sync_callable_whitelist',
'jetpack_sync_multisite_callable_whitelist',
),
'Automattic\\Jetpack\\Sync\\Modules\\Posts' => array(
'jetpack_sync_post_meta_whitelist',
),
'Automattic\\Jetpack\\Sync\\Modules\\Comments' => array(
'jetpack_sync_comment_meta_whitelist',
),
'Automattic\\Jetpack\\Sync\\Modules\\Users' => array(
'jetpack_sync_capabilities_whitelist',
),
'Automattic\\Jetpack\\Sync\\Modules\\Import' => array(
'jetpack_sync_known_importers',
),
);
const MODULES_FILTER_NAME = 'jetpack_sync_modules';
/**
* The static data settings array which contains the aggregated data settings for
* each sync filter.
*
* @var array
*/
private static $data_settings = array();
/**
* The static array which contains the list of filter hooks that have already been set up.
*
* @var array
*/
private static $set_filter_hooks = array();
/**
* Adds the data settings provided by a plugin to the Sync data settings.
*
* @param array $plugin_settings The array provided by the plugin. The array must use filters
* from the DATA_FILTER_DEFAULTS list as keys.
*/
public function add_settings_list( $plugin_settings = array() ) {
if ( empty( $plugin_settings ) || ! is_array( $plugin_settings ) ) {
/*
* No custom plugin settings, so use defaults for everything and bail early.
*/
$this->set_all_defaults();
return;
}
$this->add_filters_custom_settings_and_hooks( $plugin_settings );
if ( ! did_action( 'jetpack_sync_add_required_data_settings' ) ) {
$this->add_required_settings();
/**
* Fires when the required settings have been adding to the static
* data_settings array.
*
* @since 1.29.2
*
* @module sync
*/
do_action( 'jetpack_sync_add_required_data_settings' );
}
}
/**
* Sets the default values for sync modules and all sync data filters.
*/
private function set_all_defaults() {
$this->add_sync_filter_setting( self::MODULES_FILTER_NAME, Modules::DEFAULT_SYNC_MODULES );
foreach ( array_keys( Default_Filter_Settings::DATA_FILTER_DEFAULTS ) as $filter ) {
$this->add_sync_filter_setting( $filter, $this->get_default_setting_for_filter( $filter ) );
}
}
/**
* Returns the default settings for the given filter.
*
* @param string $filter The filter name.
*
* @return array The filter's default settings array.
*/
private function get_default_setting_for_filter( $filter ) {
if ( self::MODULES_FILTER_NAME === $filter ) {
return Modules::DEFAULT_SYNC_MODULES;
}
return ( new Default_Filter_Settings() )->get_default_settings( $filter );
}
/**
* Adds the custom settings and sets up the necessary filter hooks.
*
* @param array $filters_settings The custom settings.
*/
private function add_filters_custom_settings_and_hooks( $filters_settings ) {
if ( isset( $filters_settings[ self::MODULES_FILTER_NAME ] ) && is_array( $filters_settings[ self::MODULES_FILTER_NAME ] ) ) {
$this->add_custom_filter_setting( self::MODULES_FILTER_NAME, $filters_settings[ self::MODULES_FILTER_NAME ] );
$enabled_modules = $filters_settings[ self::MODULES_FILTER_NAME ];
} else {
$this->add_sync_filter_setting( self::MODULES_FILTER_NAME, Modules::DEFAULT_SYNC_MODULES );
$enabled_modules = Modules::DEFAULT_SYNC_MODULES;
}
$all_modules = Modules::DEFAULT_SYNC_MODULES;
foreach ( $all_modules as $module ) {
if ( in_array( $module, $enabled_modules, true ) || in_array( $module, self::MUST_SYNC_DATA_SETTINGS['jetpack_sync_modules'], true ) ) {
$this->add_filters_for_enabled_module( $module, $filters_settings );
} else {
$this->add_filters_for_disabled_module( $module );
}
}
}
/**
* Adds the filters for the provided enabled module. If the settings provided custom filter settings
* for the module's filters, those are used. Otherwise, the filter's default settings are used.
*
* @param string $module The module name.
* @param array $filters_settings The settings for the filters.
*/
private function add_filters_for_enabled_module( $module, $filters_settings ) {
$module_mapping = self::MODULE_FILTER_MAPPING;
$filters_for_module = isset( $module_mapping[ $module ] ) ? $module_mapping[ $module ] : array();
foreach ( $filters_for_module as $filter ) {
if ( isset( $filters_settings[ $filter ] ) ) {
$this->add_custom_filter_setting( $filter, $filters_settings[ $filter ] );
} else {
$this->add_sync_filter_setting( $filter, $this->get_default_setting_for_filter( $filter ) );
}
}
}
/**
* Adds the filters for the provided disabled module. The disabled module's associated filter settings are
* set to an empty array.
*
* @param string $module The module name.
*/
private function add_filters_for_disabled_module( $module ) {
$module_mapping = self::MODULE_FILTER_MAPPING;
$filters_for_module = isset( $module_mapping[ $module ] ) ? $module_mapping[ $module ] : array();
foreach ( $filters_for_module as $filter ) {
$this->add_custom_filter_setting( $filter, array() );
}
}
/**
* Adds the provided custom setting for a filter. If the filter setting isn't valid, the default
* value is used.
*
* If the filter's hook hasn't already been set up, it gets set up.
*
* @param string $filter The filter.
* @param array $setting The filter setting.
*/
private function add_custom_filter_setting( $filter, $setting ) {
if ( ! $this->is_valid_filter_setting( $filter, $setting ) ) {
/*
* The provided setting isn't valid, so use the default for this filter.
* We're using the default values so there's no need to set the filter hook.
*/
$this->add_sync_filter_setting( $filter, $this->get_default_setting_for_filter( $filter ) );
return;
}
if ( ! isset( static::$set_filter_hooks[ $filter ] ) ) {
// First time a custom modules setting is provided, so set the filter hook.
add_filter( $filter, array( $this, 'sync_data_filter_hook' ) );
static::$set_filter_hooks[ $filter ] = 1;
}
$this->add_sync_filter_setting( $filter, $setting );
}
/**
* Determines whether the filter setting is valid. The setting array is in the correct format (associative or indexed).
*
* @param string $filter The filter to check.
* @param array $filter_settings The filter settings.
*
* @return bool Whether the filter settings can be used.
*/
private function is_valid_filter_setting( $filter, $filter_settings ) {
if ( ! is_array( $filter_settings ) ) {
// The settings for each filter must be an array.
return false;
}
if ( empty( $filter_settings ) ) {
// Empty settings are allowed.
return true;
}
$indexed_array = isset( $filter_settings[0] );
if ( in_array( $filter, Default_Filter_Settings::ASSOCIATIVE_FILTERS, true ) && ! $indexed_array ) {
return true;
} elseif ( ! in_array( $filter, Default_Filter_Settings::ASSOCIATIVE_FILTERS, true ) && $indexed_array ) {
return true;
}
return false;
}
/**
* Adds the data settings that are always required for every plugin that uses Sync.
*/
private function add_required_settings() {
foreach ( self::MUST_SYNC_DATA_SETTINGS as $filter => $setting ) {
// If the corresponding setting is already set and matches the default one, no need to proceed.
if ( isset( static::$data_settings[ $filter ] ) && static::$data_settings[ $filter ] === $this->get_default_setting_for_filter( $filter ) ) {
continue;
}
$this->add_custom_filter_setting( $filter, $setting );
}
}
/**
* Adds the provided data setting for the provided filter.
*
* @param string $filter The filter name.
* @param array $value The data setting.
*/
private function add_sync_filter_setting( $filter, $value ) {
if ( ! isset( static::$data_settings[ $filter ] ) ) {
static::$data_settings[ $filter ] = $value;
return;
}
if ( in_array( $filter, Default_Filter_Settings::ASSOCIATIVE_FILTERS, true ) ) {
$this->add_associative_filter_setting( $filter, $value );
} else {
$this->add_indexed_filter_setting( $filter, $value );
}
}
/**
* Adds the provided data setting for the provided filter. This method handles
* adding settings to data that is stored as an associative array.
*
* @param string $filter The filter name.
* @param array $settings The data settings.
*/
private function add_associative_filter_setting( $filter, $settings ) {
foreach ( $settings as $key => $item ) {
if ( ! array_key_exists( $key, static::$data_settings[ $filter ] ) ) {
static::$data_settings[ $filter ][ $key ] = $item;
}
}
}
/**
* Adds the provided data setting for the provided filter. This method handles
* adding settings to data that is stored as an indexed array.
*
* @param string $filter The filter name.
* @param array $settings The data settings.
*/
private function add_indexed_filter_setting( $filter, $settings ) {
static::$data_settings[ $filter ] = array_unique(
array_merge(
static::$data_settings[ $filter ],
$settings
)
);
}
/**
* The callback function added to the sync data filters. Combines the list in the $data_settings property
* with any non-default values from the received array.
*
* @param array $filtered_values The data revieved from the filter.
*
* @return array The data settings for the filter.
*/
public function sync_data_filter_hook( $filtered_values ) {
if ( ! is_array( $filtered_values ) ) {
// Something is wrong with the input, so set it to an empty array.
$filtered_values = array();
}
$current_filter = current_filter();
if ( ! isset( static::$data_settings[ $current_filter ] ) ) {
return $filtered_values;
}
if ( in_array( $current_filter, Default_Filter_Settings::ASSOCIATIVE_FILTERS, true ) ) {
$extra_filters = array_diff_key( $filtered_values, $this->get_default_setting_for_filter( $current_filter ) );
$this->add_associative_filter_setting( $current_filter, $extra_filters );
return static::$data_settings[ $current_filter ];
}
$extra_filters = array_diff( $filtered_values, $this->get_default_setting_for_filter( $current_filter ) );
$this->add_indexed_filter_setting( $current_filter, $extra_filters );
return static::$data_settings[ $current_filter ];
}
/**
* Sets the $data_settings property to an empty array. This is useful for testing.
*/
public function empty_data_settings_and_hooks() {
static::$data_settings = array();
static::$set_filter_hooks = array();
}
/**
* Returns the $data_settings property.
*
* @return array The data_settings property.
*/
public function get_data_settings() {
return static::$data_settings;
}
}

View File

@ -0,0 +1,315 @@
<?php
/**
* Dedicated Sender.
*
* The class is responsible for spawning dedicated Sync requests.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use WP_Error;
/**
* Class to manage Sync spawning.
* The purpose of this class is to provide the means to unblock Sync
* from running in the shutdown hook of regular requests by spawning a
* dedicated Sync request instead which will trigger Sync to run.
*/
class Dedicated_Sender {
/**
* The transient name for storing the response code
* after spawning a dedicated sync test request.
*/
const DEDICATED_SYNC_CHECK_TRANSIENT = 'jetpack_sync_dedicated_sync_spawn_check';
/**
* Validation string to check if the endpoint is working correctly.
*
* This is extracted and not hardcoded, as we might want to change it in the future.
*/
const DEDICATED_SYNC_VALIDATION_STRING = 'DEDICATED SYNC OK';
/**
* Option name to use to keep the current request lock.
*
* The option format is `microtime(true)`.
*/
const DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME = 'jetpack_sync_dedicated_spawn_lock';
/**
* What's the timeout for the request lock in seconds.
*
* 5 seconds as default value seems sane, but we might want to adjust that in the future.
*/
const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 5;
/**
* The query parameter name to use when passing the current lock id.
*/
const DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME = 'request_lock_id';
/**
* Filter a URL to check if Dedicated Sync is enabled.
* We need to remove slashes and then run it through `urldecode` as sometimes the
* URL is in an encoded form, depending on server configuration.
*
* @param string $url The URL to filter.
*
* @return string
*/
public static function prepare_url_for_dedicated_request_check( $url ) {
return urldecode( $url );
}
/**
* Check if this request should trigger Sync to run.
*
* @access public
*
* @return boolean True if this is a 'jetpack/v4/sync/spawn-sync', false otherwise.
*/
public static function is_dedicated_sync_request() {
/**
* Check $_SERVER['REQUEST_URI'] first, to see if we're in the right context.
* This is done to make sure we can hook in very early in the initialization of WordPress to
* be able to send sync requests to the backend as fast as possible, without needing to continue
* loading things for the request.
*/
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return false;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
$check_url = self::prepare_url_for_dedicated_request_check( wp_unslash( $_SERVER['REQUEST_URI'] ) );
if ( strpos( $check_url, 'jetpack/v4/sync/spawn-sync' ) !== false ) {
return true;
}
/**
* If the above check failed, we might have an issue with detecting calls to the REST endpoint early on.
* Sometimes, like when permalinks are disabled, the REST path is sent via the `rest_route` GET parameter.
* 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
return false;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
$check_url = self::prepare_url_for_dedicated_request_check( wp_unslash( $_GET['rest_route'] ) );
if ( strpos( $check_url, 'jetpack/v4/sync/spawn-sync' ) !== false ) {
return true;
}
return false;
}
/**
* Send a request to run Sync for a certain sync queue
* through HTTP request that doesn't halt page loading.
*
* @access public
*
* @param Automattic\Jetpack\Sync\Queue $queue Queue object.
*
* @return boolean|WP_Error True if spawned, WP_Error otherwise.
*/
public static function spawn_sync( $queue ) {
if ( ! Settings::is_dedicated_sync_enabled() ) {
return new WP_Error( 'dedicated_sync_disabled', 'Dedicated Sync flow is disabled.' );
}
if ( $queue->is_locked() ) {
return new WP_Error( 'locked_queue_' . $queue->id );
}
if ( $queue->size() === 0 ) {
return new WP_Error( 'empty_queue_' . $queue->id );
}
// 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 ) ) {
return new WP_Error( 'retry_after_' . $queue->id );
}
// Don't sync if we are throttled.
$sync_next_time = Sender::get_instance()->get_next_sync_time( $queue->id );
if ( $sync_next_time > microtime( true ) ) {
return new WP_Error( 'sync_throttled_' . $queue->id );
}
/**
* Try to acquire a request lock, so we don't spawn multiple requests at the same time.
* This should prevent cases where sites might have limits on the amount of simultaneous requests.
*/
$request_lock = self::try_lock_spawn_request();
if ( ! $request_lock ) {
return new WP_Error( 'dedicated_request_lock', 'Unable to acquire request lock' );
}
$url = rest_url( 'jetpack/v4/sync/spawn-sync' );
$url = add_query_arg( 'time', time(), $url ); // Enforce Cache busting.
$url = add_query_arg( self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME, $request_lock, $url );
$args = array(
'cookies' => $_COOKIE,
'blocking' => false,
'timeout' => 0.01,
/** This filter is documented in wp-includes/class-wp-http-streams.php */
'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
);
$result = wp_remote_get( $url, $args );
if ( is_wp_error( $result ) ) {
return $result;
}
return true;
}
/**
* Attempt to acquire a request lock.
*
* 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
*/
public static function try_lock_spawn_request() {
$current_microtime = (string) microtime( true );
$current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
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.
}
// 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 ) );
$updated_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
if ( $updated_value === $current_microtime ) {
return $current_microtime;
}
return false;
}
/**
* Attempt to release the request lock.
*
* @param string $lock_id The request lock that's currently being held.
*
* @return bool|WP_Error
*/
public static function try_release_lock_spawn_request( $lock_id = '' ) {
// Try to get the lock_id from the current request if it's not supplied.
if ( empty( $lock_id ) ) {
$lock_id = self::get_request_lock_id_from_request();
}
// 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 ) ) {
return new WP_Error( 'dedicated_request_lock_invalid', 'Invalid lock_id supplied for unlock' );
}
$current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
// If this is the flow that has the lock, let's release it so we can spawn other requests afterwards
if ( (string) $lock_id === $current_lock_value ) {
\Jetpack_Options::delete_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME );
return true;
}
return false;
}
/**
* Try to get the request lock id from the current request.
*
* @return array|string|string[]|null
*/
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 ] ) ) {
return null;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return wp_unslash( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] );
}
/**
* Test Sync spawning functionality by making a request to the
* Sync spawning endpoint and storing the result (status code) in a transient.
*
* @since $$next_version$$
*
* @return bool True if we got a successful response, false otherwise.
*/
public static function can_spawn_dedicated_sync_request() {
$dedicated_sync_check_transient = self::DEDICATED_SYNC_CHECK_TRANSIENT;
$dedicated_sync_response_body = get_transient( $dedicated_sync_check_transient );
if ( false === $dedicated_sync_response_body ) {
$url = rest_url( 'jetpack/v4/sync/spawn-sync' );
$url = add_query_arg( 'time', time(), $url ); // Enforce Cache busting.
$args = array(
'cookies' => $_COOKIE,
'timeout' => 30,
/** This filter is documented in wp-includes/class-wp-http-streams.php */
'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
);
$response = wp_remote_get( $url, $args );
$dedicated_sync_response_code = wp_remote_retrieve_response_code( $response );
$dedicated_sync_response_body = trim( wp_remote_retrieve_body( $response ) );
/**
* Limit the size of the body that we save in the transient to avoid cases where an error
* occurs and a whole generated HTML page is returned. We don't need to store the whole thing.
*
* The regexp check is done to make sure we can detect the string even if the body returns some additional
* output, like some caching plugins do when they try to pad the request.
*/
$regexp = '!' . preg_quote( self::DEDICATED_SYNC_VALIDATION_STRING, '!' ) . '!uis';
if ( preg_match( $regexp, $dedicated_sync_response_body ) ) {
$saved_response_body = self::DEDICATED_SYNC_VALIDATION_STRING;
} else {
$saved_response_body = time();
}
set_transient( $dedicated_sync_check_transient, $saved_response_body, HOUR_IN_SECONDS );
// Send a bit more information to WordPress.com to help debugging issues.
if ( $saved_response_body !== self::DEDICATED_SYNC_VALIDATION_STRING ) {
$data = array(
'timestamp' => microtime( true ),
'response_code' => $dedicated_sync_response_code,
'response_body' => $dedicated_sync_response_body,
// Send the flow type that was attempted.
'sync_flow_type' => 'dedicated',
);
$sender = Sender::get_instance();
$sender->send_action( 'jetpack_sync_flow_error_enable', $data );
}
}
return self::DEDICATED_SYNC_VALIDATION_STRING === $dedicated_sync_response_body;
}
}

View File

@ -0,0 +1,80 @@
<?php
/**
* The Default Filter Settings class.
*
* This class provides the default whitelist values for the Sync data filters.
* See the DATA_FILTER_DEFAULTS constant for the list of filters.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* The Default_Filter_Settings class
*/
class Default_Filter_Settings {
/**
* The class that contains the default values of the filters.
*/
const DEFAULT_FILTER_CLASS = 'Automattic\Jetpack\Sync\Defaults';
/**
* A map of each Sync filter name to the associated property name in the Defaults class.
*/
const DATA_FILTER_DEFAULTS = array(
'jetpack_sync_options_whitelist' => 'default_options_whitelist',
'jetpack_sync_options_contentless' => 'default_options_contentless',
'jetpack_sync_constants_whitelist' => 'default_constants_whitelist',
'jetpack_sync_callable_whitelist' => 'default_callable_whitelist',
'jetpack_sync_multisite_callable_whitelist' => 'default_multisite_callable_whitelist',
'jetpack_sync_post_meta_whitelist' => 'post_meta_whitelist',
'jetpack_sync_comment_meta_whitelist' => 'comment_meta_whitelist',
'jetpack_sync_capabilities_whitelist' => 'default_capabilities_whitelist',
'jetpack_sync_known_importers' => 'default_known_importers',
);
/**
* The data associated with these filters are stored as associative arrays.
* (All other filters store data as indexed arrays.)
*/
const ASSOCIATIVE_FILTERS = array(
'jetpack_sync_callable_whitelist',
'jetpack_sync_multisite_callable_whitelist',
'jetpack_sync_known_importers',
);
/**
* Returns the default data settings list for the provided filter.
*
* @param string $filter The filter name.
*
* @return array|false The default list of data settings. Returns false if the provided
* filter doesn't not have an array of default settings.
*/
public function get_default_settings( $filter ) {
if ( ! is_string( $filter ) || ! array_key_exists( $filter, self::DATA_FILTER_DEFAULTS ) ) {
return false;
}
$property = self::DATA_FILTER_DEFAULTS[ $filter ];
$class = self::DEFAULT_FILTER_CLASS;
return $class::$$property;
}
/**
* Returns an array containing the default values for all of the filters shown
* in DATA_FILTER_DEFAULTS.
*
* @return array The array containing all sync data filters and their default values.
*/
public function get_all_filters_default_settings() {
$defaults = array();
foreach ( self::DATA_FILTER_DEFAULTS as $filter => $default_location ) {
$defaults[ $filter ] = $this->get_default_settings( $filter );
}
return $defaults;
}
}

View File

@ -0,0 +1,675 @@
<?php
/**
* Utility functions to generate data synced to wpcom
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use Automattic\Jetpack\Connection\Urls;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Modules as Jetpack_Modules;
/**
* Utility functions to generate data synced to wpcom
*/
class Functions {
const HTTPS_CHECK_OPTION_PREFIX = 'jetpack_sync_https_history_';
const HTTPS_CHECK_HISTORY = 5;
/**
* Return array of Jetpack modules.
*
* @return array
*/
public static function get_modules() {
if ( defined( 'JETPACK__PLUGIN_DIR' ) ) {
require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php';
return \Jetpack_Admin::init()->get_modules();
}
return array();
}
/**
* Return array of taxonomies registered on the site.
*
* @return array
*/
public static function get_taxonomies() {
global $wp_taxonomies;
$wp_taxonomies_without_callbacks = array();
foreach ( $wp_taxonomies as $taxonomy_name => $taxonomy ) {
$sanitized_taxonomy = self::sanitize_taxonomy( $taxonomy );
if ( ! empty( $sanitized_taxonomy ) ) {
$wp_taxonomies_without_callbacks[ $taxonomy_name ] = $sanitized_taxonomy;
}
}
return $wp_taxonomies_without_callbacks;
}
/**
* Return array of registered shortcodes.
*
* @return array
*/
public static function get_shortcodes() {
global $shortcode_tags;
return array_keys( $shortcode_tags );
}
/**
* Removes any callback data since we will not be able to process it on our side anyways.
*
* @param \WP_Taxonomy $taxonomy \WP_Taxonomy item.
*
* @return mixed|null
*/
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 ) );
// recursive taxonomies are no fun.
if ( $cloned_taxonomy === null ) {
return null;
}
// Remove any meta_box_cb if they are not the default wp ones.
if ( isset( $cloned_taxonomy->meta_box_cb ) &&
! in_array( $cloned_taxonomy->meta_box_cb, array( 'post_tags_meta_box', 'post_categories_meta_box' ), true ) ) {
$cloned_taxonomy->meta_box_cb = null;
}
// Remove update call back.
if ( isset( $cloned_taxonomy->update_count_callback ) &&
$cloned_taxonomy->update_count_callback !== null ) {
$cloned_taxonomy->update_count_callback = null;
}
// Remove rest_controller_class if it something other then the default.
if ( isset( $cloned_taxonomy->rest_controller_class ) &&
'WP_REST_Terms_Controller' !== $cloned_taxonomy->rest_controller_class ) {
$cloned_taxonomy->rest_controller_class = null;
}
return $cloned_taxonomy;
}
/**
* Return array of registered post types.
*
* @return array
*/
public static function get_post_types() {
global $wp_post_types;
$post_types_without_callbacks = array();
foreach ( $wp_post_types as $post_type_name => $post_type ) {
$sanitized_post_type = self::sanitize_post_type( $post_type );
if ( ! empty( $sanitized_post_type ) ) {
$post_types_without_callbacks[ $post_type_name ] = $sanitized_post_type;
}
}
return $post_types_without_callbacks;
}
/**
* Sanitizes by cloning post type object.
*
* @param object $post_type \WP_Post_Type.
*
* @return object
*/
public static function sanitize_post_type( $post_type ) {
// Lets clone the post type object instead of modifing the global one.
$sanitized_post_type = array();
foreach ( Defaults::$default_post_type_attributes as $attribute_key => $default_value ) {
if ( isset( $post_type->{ $attribute_key } ) ) {
$sanitized_post_type[ $attribute_key ] = $post_type->{ $attribute_key };
}
}
return (object) $sanitized_post_type;
}
/**
* Return information about a synced post type.
*
* @param array $sanitized_post_type Array of args used in constructing \WP_Post_Type.
* @param string $post_type Post type name.
*
* @return object \WP_Post_Type
*/
public static function expand_synced_post_type( $sanitized_post_type, $post_type ) {
$post_type = sanitize_key( $post_type );
$post_type_object = new \WP_Post_Type( $post_type, $sanitized_post_type );
$post_type_object->add_supports();
$post_type_object->add_rewrite_rules();
$post_type_object->add_hooks();
$post_type_object->register_taxonomies();
return (object) $post_type_object;
}
/**
* Returns site's post_type_features.
*
* @return array
*/
public static function get_post_type_features() {
global $_wp_post_type_features;
return $_wp_post_type_features;
}
/**
* Return hosting provider.
*
* Uses a set of known constants, classes, or functions to help determine the hosting platform.
*
* @return string Hosting provider.
*/
public static function get_hosting_provider() {
$hosting_provider_detection_methods = array(
'get_hosting_provider_by_known_constant',
'get_hosting_provider_by_known_class',
'get_hosting_provider_by_known_function',
);
$functions = new Functions();
foreach ( $hosting_provider_detection_methods as $method ) {
$hosting_provider = call_user_func( array( $functions, $method ) );
if ( false !== $hosting_provider ) {
return $hosting_provider;
}
}
return 'unknown';
}
/**
* Return a hosting provider using a set of known constants.
*
* @return mixed A host identifier string or false.
*/
public function get_hosting_provider_by_known_constant() {
$hosting_provider_constants = array(
'GD_SYSTEM_PLUGIN_DIR' => 'gd-managed-wp',
'MM_BASE_DIR' => 'bh',
'PAGELYBIN' => 'pagely',
'KINSTAMU_VERSION' => 'kinsta',
'FLYWHEEL_CONFIG_DIR' => 'flywheel',
'IS_PRESSABLE' => 'pressable',
'VIP_GO_ENV' => 'vip-go',
);
foreach ( $hosting_provider_constants as $constant => $constant_value ) {
if ( Constants::is_defined( $constant ) ) {
if ( 'VIP_GO_ENV' === $constant && false === Constants::get_constant( 'VIP_GO_ENV' ) ) {
continue;
}
return $constant_value;
}
}
return false;
}
/**
* Return a hosting provider using a set of known classes.
*
* @return mixed A host identifier string or false.
*/
public function get_hosting_provider_by_known_class() {
$hosting_provider = false;
switch ( true ) {
case ( class_exists( '\\WPaaS\\Plugin' ) ):
$hosting_provider = 'gd-managed-wp';
break;
}
return $hosting_provider;
}
/**
* Return a hosting provider using a set of known functions.
*
* @return mixed A host identifier string or false.
*/
public function get_hosting_provider_by_known_function() {
$hosting_provider = false;
switch ( true ) {
case ( function_exists( 'is_wpe' ) || function_exists( 'is_wpe_snapshot' ) ):
$hosting_provider = 'wpe';
break;
}
return $hosting_provider;
}
/**
* Return array of allowed REST API post types.
*
* @return array Array of allowed post types.
*/
public static function rest_api_allowed_post_types() {
/** This filter is already documented in class.json-api-endpoints.php */
return apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) );
}
/**
* Return array of allowed REST API public metadata.
*
* @return array Array of allowed metadata.
*/
public static function rest_api_allowed_public_metadata() {
/**
* Filters the meta keys accessible by the REST API.
*
* @see https://developer.wordpress.com/2013/04/26/custom-post-type-and-metadata-support-in-the-rest-api/
*
* @module json-api
*
* @since 1.6.3
* @since-jetpack 2.2.3
*
* @param array $whitelisted_meta Array of metadata that is accessible by the REST API.
*/
return apply_filters( 'rest_api_allowed_public_metadata', array() );
}
/**
* Finds out if a site is using a version control system.
*
* @return bool
**/
public static function is_version_controlled() {
if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
$updater = new \WP_Automatic_Updater();
return (bool) (string) $updater->is_vcs_checkout( ABSPATH );
}
/**
* Returns true if the site has file write access false otherwise.
*
* @return bool
**/
public static function file_system_write_access() {
if ( ! function_exists( 'get_filesystem_method' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
require_once ABSPATH . 'wp-admin/includes/template.php';
$filesystem_method = get_filesystem_method();
if ( 'direct' === $filesystem_method ) {
return true;
}
ob_start();
if ( ! function_exists( 'request_filesystem_credentials' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
ob_end_clean();
if ( $filesystem_credentials_are_stored ) {
return true;
}
return false;
}
/**
* Helper function that is used when getting home or siteurl values. Decides
* whether to get the raw or filtered value.
*
* @deprecated 1.23.1
*
* @param string $url_type URL to get, home or siteurl.
* @return string
*/
public static function get_raw_or_filtered_url( $url_type ) {
_deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::get_raw_or_filtered_url' );
return Urls::get_raw_or_filtered_url( $url_type );
}
/**
* Return the escaped home_url.
*
* @deprecated 1.23.1
*
* @return string
*/
public static function home_url() {
_deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::home_url' );
return Urls::home_url();
}
/**
* Return the escaped siteurl.
*
* @deprecated 1.23.1
*
* @return string
*/
public static function site_url() {
_deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::site_url' );
return Urls::site_url();
}
/**
* Return main site URL with a normalized protocol.
*
* @deprecated 1.23.1
*
* @return string
*/
public static function main_network_site_url() {
_deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::main_network_site_url' );
return Urls::main_network_site_url();
}
/**
* Return main site WordPress.com site ID.
*
* @return string
*/
public static function main_network_site_wpcom_id() {
/**
* Return the current site WPCOM ID for single site installs
*/
if ( ! is_multisite() ) {
return \Jetpack_Options::get_option( 'id' );
}
/**
* Return the main network site WPCOM ID for multi-site installs
*/
$current_network = get_network();
switch_to_blog( $current_network->blog_id );
$wpcom_blog_id = \Jetpack_Options::get_option( 'id' );
restore_current_blog();
return $wpcom_blog_id;
}
/**
* Return URL with a normalized protocol.
*
* @deprecated 1.23.1
*
* @param callable $callable Function to retrieve URL option.
* @param string $new_value URL Protocol to set URLs to.
* @return string Normalized URL.
*/
public static function get_protocol_normalized_url( $callable, $new_value ) {
_deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::get_protocol_normalized_url' );
return Urls::get_protocol_normalized_url( $callable, $new_value );
}
/**
* Return URL from option or PHP constant.
*
* @deprecated 1.23.1
*
* @param string $option_name (e.g. 'home').
*
* @return mixed|null URL.
*/
public static function get_raw_url( $option_name ) {
_deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::get_raw_url' );
return Urls::get_raw_url( $option_name );
}
/**
* Normalize domains by removing www unless declared in the site's option.
*
* @deprecated 1.23.1
*
* @param string $option Option value from the site.
* @param callable $url_function Function retrieving the URL to normalize.
* @return mixed|string URL.
*/
public static function normalize_www_in_url( $option, $url_function ) {
_deprecated_function( __METHOD__, '1.23.1', '\\Automattic\\Jetpack\\Connection\\Urls::normalize_www_in_url' );
return Urls::normalize_www_in_url( $option, $url_function );
}
/**
* Return filtered value of get_plugins.
*
* @return mixed|void
*/
public static function get_plugins() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
return apply_filters( 'all_plugins', get_plugins() );
}
/**
* Get custom action link tags that the plugin is using
* Ref: https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name)
*
* @param string $plugin_file_singular Particular plugin.
* @return array of plugin action links (key: link name value: url)
*/
public static function get_plugins_action_links( $plugin_file_singular = null ) {
// Some sites may have DOM disabled in PHP fail early.
if ( ! class_exists( 'DOMDocument' ) ) {
return array();
}
$plugins_action_links = get_option( 'jetpack_plugin_api_action_links', array() );
if ( ! empty( $plugins_action_links ) ) {
if ( $plugin_file_singular === null ) {
return $plugins_action_links;
}
return ( isset( $plugins_action_links[ $plugin_file_singular ] ) ? $plugins_action_links[ $plugin_file_singular ] : null );
}
return array();
}
/**
* Return the WP version as defined in the $wp_version global.
*
* @return string
*/
public static function wp_version() {
global $wp_version;
return $wp_version;
}
/**
* Return site icon url used on the site.
*
* @param int $size Size of requested icon in pixels.
* @return mixed|string|void
*/
public static function site_icon_url( $size = 512 ) {
$site_icon = get_site_icon_url( $size );
return $site_icon ? $site_icon : get_option( 'jetpack_site_icon_url' );
}
/**
* Return roles registered on the site.
*
* @return array
*/
public static function roles() {
$wp_roles = wp_roles();
return $wp_roles->roles;
}
/**
* Determine time zone from WordPress' options "timezone_string"
* and "gmt_offset".
*
* 1. Check if `timezone_string` is set and return it.
* 2. Check if `gmt_offset` is set, formats UTC-offset from it and return it.
* 3. Default to "UTC+0" if nothing is set.
*
* Note: This function is specifically not using wp_timezone() to keep consistency with
* the existing formatting of the timezone string.
*
* @return string
*/
public static function get_timezone() {
$timezone_string = get_option( 'timezone_string' );
if ( ! empty( $timezone_string ) ) {
return str_replace( '_', ' ', $timezone_string );
}
$gmt_offset = get_option( 'gmt_offset', 0 );
$formatted_gmt_offset = sprintf( '%+g', (float) $gmt_offset );
$formatted_gmt_offset = str_replace(
array( '.25', '.5', '.75' ),
array( ':15', ':30', ':45' ),
(string) $formatted_gmt_offset
);
/* translators: %s is UTC offset, e.g. "+1" */
return sprintf( __( 'UTC%s', 'jetpack-sync' ), $formatted_gmt_offset );
}
/**
* Return list of paused themes.
*
* @return array|bool Array of paused themes or false if unsupported.
*/
public static function get_paused_themes() {
$paused_themes = wp_paused_themes();
return $paused_themes->get_all();
}
/**
* Return list of paused plugins.
*
* @return array|bool Array of paused plugins or false if unsupported.
*/
public static function get_paused_plugins() {
$paused_plugins = wp_paused_plugins();
return $paused_plugins->get_all();
}
/**
* Return the theme's supported features.
* Used for syncing the supported feature that we care about.
*
* @return array List of features that the theme supports.
*/
public static function get_theme_support() {
global $_wp_theme_features;
$theme_support = array();
foreach ( Defaults::$default_theme_support_whitelist as $theme_feature ) {
$has_support = current_theme_supports( $theme_feature );
if ( $has_support ) {
$theme_support[ $theme_feature ] = $_wp_theme_features[ $theme_feature ];
}
}
return $theme_support;
}
/**
* Returns if the current theme is a Full Site Editing theme.
*
* @return bool Theme is a Full Site Editing theme.
*/
public static function get_is_fse_theme() {
return function_exists( 'gutenberg_is_fse_theme' ) && gutenberg_is_fse_theme();
}
/**
* Wraps data in a way so that we can distinguish between objects and array and also prevent object recursion.
*
* @since 1.21.0
*
* @param array|obj $any Source data to be cleaned up.
* @param array $seen_nodes Built array of nodes.
*
* @return array
*/
public static function json_wrap( &$any, $seen_nodes = array() ) {
if ( is_object( $any ) ) {
$input = get_object_vars( $any );
$input['__o'] = 1;
} else {
$input = &$any;
}
if ( is_array( $input ) ) {
$seen_nodes[] = &$any;
$return = array();
foreach ( $input as $k => &$v ) {
if ( ( is_array( $v ) || is_object( $v ) ) ) {
if ( in_array( $v, $seen_nodes, true ) ) {
continue;
}
$return[ $k ] = self::json_wrap( $v, $seen_nodes );
} else {
$return[ $k ] = $v;
}
}
return $return;
}
return $any;
}
/**
* Return the list of installed themes
*
* @since 1.31.0
*
* @return array
*/
public static function get_themes() {
$current_stylesheet = get_stylesheet();
$installed_themes = wp_get_themes();
$synced_headers = array( 'Name', 'ThemeURI', 'Author', 'Version', 'Template', 'Status', 'TextDomain', 'RequiresWP', 'RequiresPHP' );
$themes = array();
foreach ( $installed_themes as $stylesheet => $theme ) {
$themes[ $stylesheet ] = array();
foreach ( $synced_headers as $header ) {
$themes[ $stylesheet ][ $header ] = $theme->get( $header );
}
$themes[ $stylesheet ]['active'] = $stylesheet === $current_stylesheet;
if ( method_exists( $theme, 'is_block_theme' ) ) {
$themes[ $stylesheet ]['is_block_theme'] = $theme->is_block_theme();
}
}
/**
* Filters the output of Sync's get_theme callable
*
* @since 1.31.0
*
* @param array $themes The list of installed themes formatted in an array with a collection of information extracted from the Theme's headers
*/
return apply_filters( 'jetpack_sync_get_themes_callable', $themes );
}
/**
* Return the list of active Jetpack modules.
*
* @since $$next_version$$
*
* @return array
*/
public static function get_active_modules() {
return ( new Jetpack_Modules() )->get_active();
}
}

View File

@ -0,0 +1,190 @@
<?php
/**
* Health class.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* Health class.
*/
class Health {
/**
* Prefix of the blog lock transient.
*
* @access public
*
* @var string
*/
const STATUS_OPTION = 'sync_health_status';
/**
* Status key in option array.
*
* @access public
*
* @var string
*/
const OPTION_STATUS_KEY = 'status';
/**
* Timestamp key in option array.
*
* @access public
*
* @var string
*/
const OPTION_TIMESTAMP_KEY = 'timestamp';
/**
* Unknown status code.
*
* @access public
*
* @var string
*/
const STATUS_UNKNOWN = 'unknown';
/**
* Disabled status code.
*
* @access public
*
* @var string
*/
const STATUS_DISABLED = 'disabled';
/**
* Out of sync status code.
*
* @access public
*
* @var string
*/
const STATUS_OUT_OF_SYNC = 'out_of_sync';
/**
* In sync status code.
*
* @access public
*
* @var string
*/
const STATUS_IN_SYNC = 'in_sync';
/**
* If sync is active, Health-related hooks will be initialized after plugins are loaded.
*/
public static function init() {
add_action( 'jetpack_full_sync_end', array( __ClASS__, 'full_sync_end_update_status' ), 10, 2 );
}
/**
* Gets health status code.
*
* @return string Sync Health Status
*/
public static function get_status() {
$status = \Jetpack_Options::get_option( self::STATUS_OPTION );
if ( false === $status || ! is_array( $status ) || empty( $status[ self::OPTION_STATUS_KEY ] ) ) {
return self::STATUS_UNKNOWN;
}
switch ( $status[ self::OPTION_STATUS_KEY ] ) {
case self::STATUS_DISABLED:
case self::STATUS_OUT_OF_SYNC:
case self::STATUS_IN_SYNC:
return $status[ self::OPTION_STATUS_KEY ];
default:
return self::STATUS_UNKNOWN;
}
}
/**
* When the Jetpack plugin is upgraded, set status to disabled if sync is not enabled,
* or to unknown, if the status has never been set before.
*/
public static function on_jetpack_upgraded() {
if ( ! Settings::is_sync_enabled() ) {
self::update_status( self::STATUS_DISABLED );
return;
}
if ( false === self::is_status_defined() ) {
self::update_status( self::STATUS_UNKNOWN );
}
}
/**
* When the Jetpack plugin is activated, set status to disabled if sync is not enabled,
* or to unknown.
*/
public static function on_jetpack_activated() {
if ( ! Settings::is_sync_enabled() ) {
self::update_status( self::STATUS_DISABLED );
return;
}
self::update_status( self::STATUS_UNKNOWN );
}
/**
* Updates sync health status with either a valid status, or an unknown status.
*
* @param string $status Sync Status.
*
* @return bool True if an update occoured, or false if the status didn't change.
*/
public static function update_status( $status ) {
if ( self::get_status() === $status ) {
return false;
}
// Default Status Option.
$new_status = array(
self::OPTION_STATUS_KEY => self::STATUS_UNKNOWN,
self::OPTION_TIMESTAMP_KEY => microtime( true ),
);
switch ( $status ) {
case self::STATUS_DISABLED:
case self::STATUS_OUT_OF_SYNC:
case self::STATUS_IN_SYNC:
$new_status[ self::OPTION_STATUS_KEY ] = $status;
break;
}
\Jetpack_Options::update_option( self::STATUS_OPTION, $new_status );
return true;
}
/**
* Check if Status has been previously set.
*
* @return bool is a Status defined
*/
public static function is_status_defined() {
$status = \Jetpack_Options::get_option( self::STATUS_OPTION );
if ( false === $status || ! is_array( $status ) || empty( $status[ self::OPTION_STATUS_KEY ] ) ) {
return false;
} else {
return true;
}
}
/**
* Update Sync Status if Full Sync ended of Posts
*
* @param string $checksum The checksum that's currently being processed.
* @param array $range The ranges of object types being processed.
*/
public static function full_sync_end_update_status( $checksum, $range ) {
if ( isset( $range['posts'] ) ) {
self::update_status( self::STATUS_IN_SYNC );
}
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses gzip's DEFLATE
* algorithm to compress objects serialized using json_encode.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses gzip's DEFLATE
* algorithm to compress objects serialized using json_encode
*/
class JSON_Deflate_Array_Codec implements Codec_Interface {
const CODEC_NAME = 'deflate-json-array';
/**
* Return the name of the codec.
*
* @return string
*/
public function name() {
return self::CODEC_NAME;
}
/**
* Encodes an object.
*
* @param object $object Item to encode.
* @return string
*/
public function encode( $object ) {
return base64_encode( gzdeflate( $this->json_serialize( $object ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}
/**
* Decode compressed serialized value.
*
* @param string $input Item to decode.
* @return array|mixed|object
*/
public function decode( $input ) {
return $this->json_unserialize( gzinflate( base64_decode( $input ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
}
/**
* Serialize JSON
*
* @see https://gist.github.com/muhqu/820694
*
* @param string $any Value to serialize and wrap.
*
* @return false|string
*/
protected function json_serialize( $any ) {
return wp_json_encode( Functions::json_wrap( $any ) );
}
/**
* Unserialize JSON
*
* @param string $str JSON string.
* @return array|object Unwrapped JSON.
*/
protected function json_unserialize( $str ) {
return $this->json_unwrap( json_decode( $str, true ) );
}
/**
* Unwraps a json_decode return.
*
* @param array|object $any json_decode object.
* @return array|object
*/
private function json_unwrap( $any ) {
if ( is_array( $any ) ) {
foreach ( $any as $k => $v ) {
if ( '__o' === $k ) {
continue;
}
$any[ $k ] = $this->json_unwrap( $v );
}
if ( isset( $any['__o'] ) ) {
unset( $any['__o'] );
$any = (object) $any;
}
}
return $any;
}
}

View File

@ -0,0 +1,488 @@
<?php
/**
* Jetpack's Sync Listener
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
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.
/**
* Sync queue.
*
* @var object
*/
private $sync_queue;
/**
* Full sync queue.
*
* @var object
*/
private $full_sync_queue;
/**
* Sync queue size limit.
*
* @var int size limit.
*/
private $sync_queue_size_limit;
/**
* Sync queue lag limit.
*
* @var int Lag limit.
*/
private $sync_queue_lag_limit;
/**
* Singleton implementation.
*
* @var Listener
*/
private static $instance;
/**
* Get the Listener instance.
*
* @return Listener
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Listener constructor.
*
* This is necessary because you can't use "new" when you declare instance properties >:(
*/
protected function __construct() {
$this->set_defaults();
$this->init();
}
/**
* Sync Listener init.
*/
private function init() {
$handler = array( $this, 'action_handler' );
$full_sync_handler = array( $this, 'full_sync_action_handler' );
foreach ( Modules::get_modules() as $module ) {
$module->init_listeners( $handler );
$module->init_full_sync_listeners( $full_sync_handler );
}
// Module Activation.
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 );
}
/**
* Get incremental sync queue.
*/
public function get_sync_queue() {
return $this->sync_queue;
}
/**
* Gets the full sync queue.
*/
public function get_full_sync_queue() {
return $this->full_sync_queue;
}
/**
* Sets queue size limit.
*
* @param int $limit Queue size limit.
*/
public function set_queue_size_limit( $limit ) {
$this->sync_queue_size_limit = $limit;
}
/**
* Get queue size limit.
*/
public function get_queue_size_limit() {
return $this->sync_queue_size_limit;
}
/**
* Sets the queue lag limit.
*
* @param int $age Queue lag limit.
*/
public function set_queue_lag_limit( $age ) {
$this->sync_queue_lag_limit = $age;
}
/**
* Return value of queue lag limit.
*/
public function get_queue_lag_limit() {
return $this->sync_queue_lag_limit;
}
/**
* Force a recheck of the queue limit.
*/
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 );
}
/**
* Determine if an item can be added to the queue.
*
* Prevent adding items to the queue if it hasn't sent an item for 15 mins
* AND the queue is over 1000 items long (by default).
*
* @param object $queue Sync queue.
* @return bool
*/
public function can_add_to_queue( $queue ) {
if ( ! Settings::is_sync_enabled() ) {
return false;
}
$state_transient_name = self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $queue->id;
$queue_state = get_transient( $state_transient_name );
if ( false === $queue_state ) {
$queue_state = array( $queue->size(), $queue->lag() );
set_transient( $state_transient_name, $queue_state, self::QUEUE_STATE_CHECK_TIMEOUT );
}
list( $queue_size, $queue_age ) = $queue_state;
return ( $queue_age < $this->sync_queue_lag_limit )
||
( ( $queue_size + 1 ) < $this->sync_queue_size_limit );
}
/**
* Full sync action handler.
*
* @param mixed ...$args Args passed to the action.
*/
public function full_sync_action_handler( ...$args ) {
$this->enqueue_action( current_filter(), $args, $this->full_sync_queue );
}
/**
* Action handler.
*
* @param mixed ...$args Args passed to the action.
*/
public function action_handler( ...$args ) {
$this->enqueue_action( current_filter(), $args, $this->sync_queue );
}
// add many actions to the queue directly, without invoking them.
/**
* Bulk add action to the queue.
*
* @param string $action_name The name the full sync action.
* @param array $args_array Array of chunked arguments.
*/
public function bulk_enqueue_full_sync_actions( $action_name, $args_array ) {
$queue = $this->get_full_sync_queue();
/*
* If we add any items to the queue, we should try to ensure that our script
* can't be killed before they are sent.
*/
if ( function_exists( 'ignore_user_abort' ) ) {
ignore_user_abort( true );
}
$data_to_enqueue = array();
$user_id = get_current_user_id();
$currtime = microtime( true );
$is_importing = Settings::is_importing();
foreach ( $args_array as $args ) {
$previous_end = isset( $args['previous_end'] ) ? $args['previous_end'] : null;
$args = isset( $args['ids'] ) ? $args['ids'] : $args;
/**
* Modify or reject the data within an action before it is enqueued locally.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @module sync
*
* @param array The action parameters
*/
$args = apply_filters( "jetpack_sync_before_enqueue_$action_name", $args );
$action_data = array( $args );
if ( $previous_end !== null ) {
$action_data[] = $previous_end;
}
// allow listeners to abort.
if ( false === $args ) {
continue;
}
$data_to_enqueue[] = array(
$action_name,
$action_data,
$user_id,
$currtime,
$is_importing,
);
}
$queue->add_all( $data_to_enqueue );
}
/**
* Enqueue the action.
*
* @param string $current_filter Current WordPress filter.
* @param object $args Sync args.
* @param string $queue Sync queue.
*/
public function enqueue_action( $current_filter, $args, $queue ) {
// don't enqueue an action during the outbound http request - this prevents recursion.
if ( Settings::is_sending() ) {
return;
}
if ( ! ( new Connection_Manager() )->is_connected() ) {
// Don't enqueue an action if the site is disconnected.
return;
}
/**
* Add an action hook to execute when anything on the whitelist gets sent to the queue to sync.
*
* @module sync
*
* @since 1.6.3
* @since-jetpack 5.9.0
*/
do_action( 'jetpack_sync_action_before_enqueue' );
/**
* Modify or reject the data within an action before it is enqueued locally.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param array The action parameters
*/
$args = apply_filters( "jetpack_sync_before_enqueue_$current_filter", $args );
// allow listeners to abort.
if ( false === $args ) {
return;
}
/*
* Periodically check the size of the queue, and disable adding to it if
* it exceeds some limit AND the oldest item exceeds the age limit (i.e. sending has stopped).
*/
if ( ! $this->can_add_to_queue( $queue ) ) {
if ( 'sync' === $queue->id ) {
$this->sync_data_loss( $queue );
}
return;
}
/*
* If we add any items to the queue, we should try to ensure that our script
* can't be killed before they are sent.
*/
if ( function_exists( 'ignore_user_abort' ) ) {
ignore_user_abort( true );
}
if (
'sync' === $queue->id ||
in_array(
$current_filter,
array(
'jetpack_full_sync_start',
'jetpack_full_sync_end',
'jetpack_full_sync_cancel',
),
true
)
) {
$queue->add(
array(
$current_filter,
$args,
get_current_user_id(),
microtime( true ),
Settings::is_importing(),
$this->get_actor( $current_filter, $args ),
)
);
} else {
$queue->add(
array(
$current_filter,
$args,
get_current_user_id(),
microtime( true ),
Settings::is_importing(),
)
);
}
// 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 );
if ( did_action( 'init' ) ) {
Actions::add_sender_shutdown();
}
}
}
/**
* Sync Data Loss Handler
*
* @param Queue $queue Sync queue.
* @return boolean was send successful
*/
public function sync_data_loss( $queue ) {
if ( ! Settings::is_sync_enabled() ) {
return;
}
$updated = Health::update_status( Health::STATUS_OUT_OF_SYNC );
if ( ! $updated ) {
return;
}
$data = array(
'timestamp' => microtime( true ),
'queue_size' => $queue->size(),
'queue_lag' => $queue->lag(),
);
$sender = Sender::get_instance();
return $sender->send_action( 'jetpack_sync_data_loss', $data );
}
/**
* Get the event's actor.
*
* @param string $current_filter Current wp-admin page.
* @param object $args Sync event.
* @return array Actor information.
*/
public function get_actor( $current_filter, $args ) {
if ( 'wp_login' === $current_filter ) {
$user = get_user_by( 'ID', $args[1]->data->ID );
} else {
$user = wp_get_current_user();
}
$roles = new Roles();
$translated_role = $roles->translate_user_to_role( $user );
$actor = array(
'wpcom_user_id' => null,
'external_user_id' => isset( $user->ID ) ? $user->ID : null,
'display_name' => isset( $user->display_name ) ? $user->display_name : null,
'user_email' => isset( $user->user_email ) ? $user->user_email : null,
'user_roles' => isset( $user->roles ) ? $user->roles : null,
'translated_role' => $translated_role ? $translated_role : null,
'is_cron' => defined( 'DOING_CRON' ) ? DOING_CRON : false,
'is_rest' => defined( 'REST_API_REQUEST' ) ? REST_API_REQUEST : false,
'is_xmlrpc' => defined( 'XMLRPC_REQUEST' ) ? XMLRPC_REQUEST : false,
'is_wp_rest' => defined( 'REST_REQUEST' ) ? REST_REQUEST : false,
'is_ajax' => defined( 'DOING_AJAX' ) ? DOING_AJAX : false,
'is_wp_admin' => is_admin(),
'is_cli' => defined( 'WP_CLI' ) ? WP_CLI : false,
'from_url' => $this->get_request_url(),
);
if ( $this->should_send_user_data_with_actor( $current_filter ) ) {
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
if ( defined( 'JETPACK__PLUGIN_DIR' ) ) {
if ( ! function_exists( 'jetpack_protect_get_ip' ) ) {
require_once JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php';
}
$ip = jetpack_protect_get_ip();
}
$actor['ip'] = $ip;
$actor['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : 'unknown';
}
return $actor;
}
/**
* Should user data be sent as the actor?
*
* @param string $current_filter The current WordPress filter being executed.
* @return bool
*/
public function should_send_user_data_with_actor( $current_filter ) {
$should_send = in_array( $current_filter, array( 'jetpack_wp_login', 'wp_logout', 'jetpack_valid_failed_login_attempt' ), true );
/**
* Allow or deny sending actor's user data ( IP and UA ) during a sync event
*
* @since 1.6.3
* @since-jetpack 5.8.0
*
* @module sync
*
* @param bool True if we should send user data
* @param string The current filter that is performing the sync action
*/
return apply_filters( 'jetpack_sync_actor_user_data', $should_send, $current_filter );
}
/**
* Sets Listener defaults.
*/
public function set_defaults() {
$this->sync_queue = new Queue( 'sync' );
$this->full_sync_queue = new Queue( 'full_sync' );
$this->set_queue_size_limit( Settings::get_setting( 'max_queue_size' ) );
$this->set_queue_lag_limit( Settings::get_setting( 'max_queue_lag' ) );
}
/**
* Get the request URL.
*
* @return string Request URL, if known. Otherwise, wp-admin or home_url.
*/
public function get_request_url() {
if ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- False positive, sniff misses the call to esc_url_raw.
return esc_url_raw( 'http' . ( isset( $_SERVER['HTTPS'] ) ? 's' : '' ) . '://' . wp_unslash( "{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}" ) );
}
return is_admin() ? get_admin_url( get_current_blog_id() ) : home_url();
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Lock class.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* Lock class
*/
class Lock {
/**
* Prefix of the blog lock transient.
*
* @access public
*
* @var string
*/
const LOCK_PREFIX = 'jp_sync_lock_';
/**
* Default Lifetime of the lock.
* This is the expiration value as such we are setting it high to handle cases where there are
* long running requests. Short expiration value leads to concurrent requests and performance issues.
*
* @access public
*
* @var int
*/
const LOCK_TRANSIENT_EXPIRY = 180; // Seconds.
/**
* Attempt to lock.
*
* @access public
*
* @param string $name lock name.
* @param int $expiry lock duration in seconds.
*
* @return boolean True if succeeded, false otherwise.
*/
public function attempt( $name, $expiry = self::LOCK_TRANSIENT_EXPIRY ) {
$lock_name = self::LOCK_PREFIX . $name;
$locked_time = get_option( $lock_name );
if ( $locked_time ) {
// If expired update to false but don't send. Send will occurr in new request to avoid race conditions.
if ( microtime( true ) > $locked_time ) {
update_option( $lock_name, false, false );
}
return false;
}
$locked_time = microtime( true ) + $expiry;
update_option( $lock_name, $locked_time, false );
return $locked_time;
}
/**
* Remove the lock.
*
* @access public
*
* @param string $name lock name.
* @param bool|float $lock_expiration lock expiration.
*/
public function remove( $name, $lock_expiration = false ) {
$lock_name = self::LOCK_PREFIX . $name;
// Only remove lock if current value matches our lock.
if ( true === $lock_expiration || (string) get_option( $lock_name ) === (string) $lock_expiration ) {
update_option( $lock_name, false, false );
}
}
}

View File

@ -0,0 +1,114 @@
<?php
/**
* This class hooks the main sync actions.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use Automattic\Jetpack\Sync\Actions as Sync_Actions;
/**
* Jetpack Sync main class.
*/
class Main {
/**
* Sets up event handlers for the Sync package. Is used from the Config package.
*
* @action plugins_loaded
*/
public static function configure() {
if ( Actions::sync_allowed() ) {
add_action( 'plugins_loaded', array( __CLASS__, 'on_plugins_loaded_early' ), 5 );
add_action( 'plugins_loaded', array( __CLASS__, 'on_plugins_loaded_late' ), 90 );
}
// Add REST endpoints.
add_action( 'rest_api_init', array( 'Automattic\\Jetpack\\Sync\\REST_Endpoints', 'initialize_rest_api' ) );
// Add IDC disconnect action.
add_action( 'jetpack_idc_disconnect', array( __CLASS__, 'on_jetpack_idc_disconnect' ), 100 );
// Any hooks below are special cases that need to be declared even if Sync is not allowed.
add_action( 'jetpack_site_registered', array( 'Automattic\\Jetpack\\Sync\\Actions', 'do_initial_sync' ), 10, 0 );
// Set up package version hook.
add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
}
/**
* Delete all sync related data on Identity Crisis disconnect.
*/
public static function on_jetpack_idc_disconnect() {
Sender::get_instance()->uninstall();
}
/**
* Sets the Sync data settings.
*
* @param array $data_settings An array containing the Sync data options. An empty array indicates that the default
* values will be used for all Sync data.
*/
public static function set_sync_data_options( $data_settings = array() ) {
( new Data_Settings() )->add_settings_list( $data_settings );
}
/**
* Initialize the main sync actions.
*
* @action plugins_loaded
*/
public static function on_plugins_loaded_early() {
/**
* Additional Sync modules can be carried out into their own packages and they
* will get their own config settings.
*
* For now additional modules are enabled based on whether the third party plugin
* class exists or not.
*/
Sync_Actions::initialize_search();
Sync_Actions::initialize_woocommerce();
Sync_Actions::initialize_wp_super_cache();
// We need to define this here so that it's hooked before `updating_jetpack_version` is called.
add_action( 'updating_jetpack_version', array( 'Automattic\\Jetpack\\Sync\\Actions', 'cleanup_on_upgrade' ), 10, 2 );
}
/**
* Runs after most of plugins_loaded hook functions have been run.
*
* @action plugins_loaded
*/
public static function on_plugins_loaded_late() {
/*
* Init after plugins loaded and before the `init` action. This helps with issues where plugins init
* with a high priority or sites that use alternate cron.
*/
Sync_Actions::init();
// Enable non-blocking Jetpack Sync flow.
$non_block_enabled = (bool) get_option( 'jetpack_sync_non_blocking', false );
/**
* Filters the option to enable non-blocking sync.
*
* Default value is false, filter to true to enable non-blocking mode which will have
* WP.com return early and use the sync/close endpoint to check-in processed items.
*
* @since 1.12.3
*
* @param bool $enabled Should non-blocking flow be enabled.
*/
$filtered = (bool) apply_filters( 'jetpack_sync_non_blocking', $non_block_enabled );
if ( $non_block_enabled !== $filtered ) {
update_option( 'jetpack_sync_non_blocking', $filtered, false );
}
// Initialize health-related hooks after plugins have loaded.
Health::init();
}
}

View File

@ -0,0 +1,160 @@
<?php
/**
* Simple wrapper that allows enumerating cached static instances
* of sync modules.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use Automattic\Jetpack\Sync\Modules\Module;
/**
* A class to handle loading of sync modules.
*/
class Modules {
/**
* Lists classnames of sync modules we load by default.
*
* @access public
*
* @var array
*/
const DEFAULT_SYNC_MODULES = array(
'Automattic\\Jetpack\\Sync\\Modules\\Constants',
'Automattic\\Jetpack\\Sync\\Modules\\Callables',
'Automattic\\Jetpack\\Sync\\Modules\\Network_Options',
'Automattic\\Jetpack\\Sync\\Modules\\Options',
'Automattic\\Jetpack\\Sync\\Modules\\Terms',
'Automattic\\Jetpack\\Sync\\Modules\\Menus',
'Automattic\\Jetpack\\Sync\\Modules\\Themes',
'Automattic\\Jetpack\\Sync\\Modules\\Users',
'Automattic\\Jetpack\\Sync\\Modules\\Import',
'Automattic\\Jetpack\\Sync\\Modules\\Posts',
'Automattic\\Jetpack\\Sync\\Modules\\Protect',
'Automattic\\Jetpack\\Sync\\Modules\\Comments',
'Automattic\\Jetpack\\Sync\\Modules\\Updates',
'Automattic\\Jetpack\\Sync\\Modules\\Attachments',
'Automattic\\Jetpack\\Sync\\Modules\\Meta',
'Automattic\\Jetpack\\Sync\\Modules\\Plugins',
'Automattic\\Jetpack\\Sync\\Modules\\Stats',
'Automattic\\Jetpack\\Sync\\Modules\\Full_Sync_Immediately',
'Automattic\\Jetpack\\Sync\\Modules\\Term_Relationships',
);
/**
* Keeps track of initialized sync modules.
*
* @access private
* @static
*
* @var null|array
*/
private static $initialized_modules = null;
/**
* Gets a list of initialized modules.
*
* @access public
* @static
*
* @return Module[]
*/
public static function get_modules() {
if ( null === self::$initialized_modules ) {
self::$initialized_modules = self::initialize_modules();
}
return self::$initialized_modules;
}
/**
* Sets defaults for all initialized modules.
*
* @access public
* @static
*/
public static function set_defaults() {
foreach ( self::get_modules() as $module ) {
$module->set_defaults();
}
}
/**
* Gets the name of an initialized module. Returns false if given module has not been initialized.
*
* @access public
* @static
*
* @param string $module_name A module name.
*
* @return bool|Automattic\Jetpack\Sync\Modules\Module
*/
public static function get_module( $module_name ) {
foreach ( self::get_modules() as $module ) {
if ( $module->name() === $module_name ) {
return $module;
}
}
return false;
}
/**
* Loads and sets defaults for all declared modules.
*
* @access public
* @static
*
* @return array
*/
public static function initialize_modules() {
/**
* Filters the list of class names of sync modules.
* If you add to this list, make sure any classes implement the
* Jetpack_Sync_Module interface.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*/
$modules = apply_filters( 'jetpack_sync_modules', self::DEFAULT_SYNC_MODULES );
$modules = array_map( array( __CLASS__, 'load_module' ), $modules );
return array_map( array( __CLASS__, 'set_module_defaults' ), $modules );
}
/**
* Returns an instance of the given module class.
*
* @access public
* @static
*
* @param string $module_class The classname of a Jetpack sync module.
*
* @return Automattic\Jetpack\Sync\Modules\Module
*/
public static function load_module( $module_class ) {
return new $module_class();
}
/**
* Sets defaults for the given instance of a Jetpack sync module.
*
* @access public
* @static
*
* @param Automattic\Jetpack\Sync\Modules\Module $module Instance of a Jetpack sync module.
*
* @return Automattic\Jetpack\Sync\Modules\Module
*/
public static function set_module_defaults( $module ) {
$module->set_defaults();
if ( method_exists( $module, 'set_late_default' ) ) {
add_action( 'init', array( $module, 'set_late_default' ), 90 );
}
return $module;
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* The Package_Version class.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* The Package_Version class.
*/
class Package_Version {
const PACKAGE_VERSION = '1.37.0';
const PACKAGE_SLUG = 'sync';
/**
* Adds the package slug and version to the package version tracker's data.
*
* @param array $package_versions The package version array.
*
* @return array The packge version array.
*/
public static function send_package_version_to_tracker( $package_versions ) {
$package_versions[ self::PACKAGE_SLUG ] = self::PACKAGE_VERSION;
return $package_versions;
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* Sync queue buffer.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* A buffer of items from the queue that can be checked out.
*/
class Queue_Buffer {
/**
* Sync queue buffer ID.
*
* @access public
*
* @var int
*/
public $id;
/**
* Sync items.
*
* @access public
*
* @var array
*/
public $items_with_ids;
/**
* Constructor.
* Initializes the queue buffer.
*
* @access public
*
* @param int $id Sync queue buffer ID.
* @param array $items_with_ids Items for the buffer to work with.
*/
public function __construct( $id, $items_with_ids ) {
$this->id = $id;
$this->items_with_ids = $items_with_ids;
}
/**
* Retrieve the sync items in the buffer, in an ID => value form.
*
* @access public
*
* @return bool|array Sync items in the buffer.
*/
public function get_items() {
return array_combine( $this->get_item_ids(), $this->get_item_values() );
}
/**
* Retrieve the values of the sync items in the buffer.
*
* @access public
*
* @return array Sync items values.
*/
public function get_item_values() {
return Utils::get_item_values( $this->items_with_ids );
}
/**
* Retrieve the IDs of the sync items in the buffer.
*
* @access public
*
* @return array Sync items IDs.
*/
public function get_item_ids() {
return Utils::get_item_ids( $this->items_with_ids );
}
}

View File

@ -0,0 +1,753 @@
<?php
/**
* The class that describes the Queue for the sync package.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use WP_Error;
/**
* A persistent queue that can be flushed in increments of N items,
* and which blocks reads until checked-out buffers are checked in or
* closed. This uses raw SQL for two reasons: speed, and not triggering
* tons of added_option callbacks.
*/
class Queue {
/**
* The queue id.
*
* @var string
*/
public $id;
/**
* Keeps track of the rows.
*
* @var int
*/
private $row_iterator;
/**
* Queue constructor.
*
* @param string $id Name of the queue.
*/
public function __construct( $id ) {
$this->id = str_replace( '-', '_', $id ); // Necessary to ensure we don't have ID collisions in the SQL.
$this->row_iterator = 0;
$this->random_int = wp_rand( 1, 1000000 );
}
/**
* Add a single item to the queue.
*
* @param object $item Event object to add to queue.
*/
public function add( $item ) {
global $wpdb;
$added = false;
// If empty, don't add.
if ( empty( $item ) ) {
return;
}
// Attempt to serialize data, if an exception (closures) return early.
try {
$item = serialize( $item ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
} catch ( \Exception $ex ) {
return;
}
// This basically tries to add the option until enough time has elapsed that
// it has a unique (microtime-based) option key.
while ( ! $added ) {
$rows_added = $wpdb->query(
$wpdb->prepare(
"INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES (%s, %s,%s)",
$this->get_next_data_row_option_name(),
$item,
'no'
)
);
$added = ( 0 !== $rows_added );
}
}
/**
* Insert all the items in a single SQL query. May be subject to query size limits!
*
* @param array $items Array of events to add to the queue.
*
* @return bool|\WP_Error
*/
public function add_all( $items ) {
global $wpdb;
$base_option_name = $this->get_next_data_row_option_name();
$query = "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES ";
$rows = array();
$count_items = count( $items );
for ( $i = 0; $i < $count_items; ++$i ) {
// skip empty items.
if ( empty( $items[ $i ] ) ) {
continue;
}
try {
$option_name = esc_sql( $base_option_name . '-' . $i );
$option_value = esc_sql( serialize( $items[ $i ] ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$rows[] = "('$option_name', '$option_value', 'no')";
} catch ( \Exception $e ) {
// Item cannot be serialized so skip.
continue;
}
}
$rows_added = $wpdb->query( $query . join( ',', $rows ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( count( $items ) !== $rows_added ) {
return new WP_Error( 'row_count_mismatch', "The number of rows inserted didn't match the size of the input array" );
}
return true;
}
/**
* Get the front-most item on the queue without checking it out.
*
* @param int $count Number of items to return when looking at the items.
*
* @return array
*/
public function peek( $count = 1 ) {
$items = $this->fetch_items( $count );
if ( $items ) {
return Utils::get_item_values( $items );
}
return array();
}
/**
* Gets items with particular IDs.
*
* @param array $item_ids Array of item IDs to retrieve.
*
* @return array
*/
public function peek_by_id( $item_ids ) {
$items = $this->fetch_items_by_id( $item_ids );
if ( $items ) {
return Utils::get_item_values( $items );
}
return array();
}
/**
* Gets the queue lag.
* Lag is the difference in time between the age of the oldest item
* (aka first or frontmost item) and the current time.
*
* @param microtime $now The current time in microtime.
*
* @return float|int|mixed|null
*/
public function lag( $now = null ) {
global $wpdb;
$first_item_name = $wpdb->get_var(
$wpdb->prepare(
"SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 1",
"jpsq_{$this->id}-%"
)
);
if ( ! $first_item_name ) {
return 0;
}
if ( null === $now ) {
$now = microtime( true );
}
// Break apart the item name to get the timestamp.
$matches = null;
if ( preg_match( '/^jpsq_' . $this->id . '-(\d+\.\d+)-/', $first_item_name, $matches ) ) {
return $now - (float) $matches[1];
} else {
return 0;
}
}
/**
* Resets the queue.
*/
public function reset() {
global $wpdb;
$this->delete_checkout_id();
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
"jpsq_{$this->id}-%"
)
);
}
/**
* Return the size of the queue.
*
* @return int
*/
public function size() {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT count(*) FROM $wpdb->options WHERE option_name LIKE %s",
"jpsq_{$this->id}-%"
)
);
}
/**
* Lets you know if there is any items in the queue.
*
* We use this peculiar implementation because it's much faster than count(*).
*
* @return bool
*/
public function has_any_items() {
global $wpdb;
$value = $wpdb->get_var(
$wpdb->prepare(
"SELECT exists( SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s )",
"jpsq_{$this->id}-%"
)
);
return ( '1' === $value );
}
/**
* Used to checkout the queue.
*
* @param int $buffer_size Size of the buffer to checkout.
*
* @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error
*/
public function checkout( $buffer_size ) {
if ( $this->get_checkout_id() ) {
return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
}
$buffer_id = uniqid();
$result = $this->set_checkout_id( $buffer_id );
if ( ! $result || is_wp_error( $result ) ) {
return $result;
}
$items = $this->fetch_items( $buffer_size );
if ( count( $items ) === 0 ) {
return false;
}
$buffer = new Queue_Buffer( $buffer_id, array_slice( $items, 0, $buffer_size ) );
return $buffer;
}
/**
* Given a list of items return the items ids.
*
* @param array $items List of item objects.
*
* @return array Ids of the items.
*/
public function get_ids( $items ) {
return array_map(
function ( $item ) {
return $item->id;
},
$items
);
}
/**
* Pop elements from the queue.
*
* @param int $limit Number of items to pop from the queue.
*
* @return array|object|null
*/
public function pop( $limit ) {
$items = $this->fetch_items( $limit );
$ids = $this->get_ids( $items );
$this->delete( $ids );
return $items;
}
/**
* Get the items from the queue with a memory limit.
*
* This checks out rows until it either empties the queue or hits a certain memory limit
* it loads the sizes from the DB first so that it doesn't accidentally
* load more data into memory than it needs to.
* The only way it will load more items than $max_size is if a single queue item
* exceeds the memory limit, but in that case it will send that item by itself.
*
* @param int $max_memory (bytes) Maximum memory threshold.
* @param int $max_buffer_size Maximum buffer size (number of items).
*
* @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error
*/
public function checkout_with_memory_limit( $max_memory, $max_buffer_size = 500 ) {
if ( $this->get_checkout_id() ) {
return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
}
$buffer_id = uniqid();
$result = $this->set_checkout_id( $buffer_id );
if ( ! $result || is_wp_error( $result ) ) {
return $result;
}
// Get the map of buffer_id -> memory_size.
global $wpdb;
$items_with_size = $wpdb->get_results(
$wpdb->prepare(
"SELECT option_name AS id, LENGTH(option_value) AS value_size FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d",
"jpsq_{$this->id}-%",
$max_buffer_size
),
OBJECT
);
if ( count( $items_with_size ) === 0 ) {
return false;
}
$total_memory = 0;
$max_item_id = $items_with_size[0]->id;
$min_item_id = $max_item_id;
foreach ( $items_with_size as $id => $item_with_size ) {
$total_memory += $item_with_size->value_size;
// If this is the first item and it exceeds memory, allow loop to continue
// we will exit on the next iteration instead.
if ( $total_memory > $max_memory && $id > 0 ) {
break;
}
$max_item_id = $item_with_size->id;
}
$query = $wpdb->prepare(
"SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name >= %s and option_name <= %s ORDER BY option_name ASC",
$min_item_id,
$max_item_id
);
$items = $wpdb->get_results( $query, OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
foreach ( $items as $item ) {
// @codingStandardsIgnoreStart
$item->value = @unserialize( $item->value );
// @codingStandardsIgnoreEnd
}
if ( count( $items ) === 0 ) {
$this->delete_checkout_id();
return false;
}
$buffer = new Queue_Buffer( $buffer_id, $items );
return $buffer;
}
/**
* Check in the queue.
*
* @param Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue_Buffer object.
*
* @return bool|\WP_Error
*/
public function checkin( $buffer ) {
$is_valid = $this->validate_checkout( $buffer );
if ( is_wp_error( $is_valid ) ) {
return $is_valid;
}
$this->delete_checkout_id();
return true;
}
/**
* Close the buffer.
*
* @param Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue_Buffer object.
* @param null|array $ids_to_remove Ids to remove from the queue.
*
* @return bool|\WP_Error
*/
public function close( $buffer, $ids_to_remove = null ) {
$is_valid = $this->validate_checkout( $buffer );
if ( is_wp_error( $is_valid ) ) {
// Always delete ids_to_remove even when buffer is no longer checked-out.
// They were processed by WP.com so safe to remove from queue.
if ( $ids_to_remove !== null ) {
$this->delete( $ids_to_remove );
}
return $is_valid;
}
$this->delete_checkout_id();
// By default clear all items in the buffer.
if ( $ids_to_remove === null ) {
$ids_to_remove = $buffer->get_item_ids();
}
$this->delete( $ids_to_remove );
return true;
}
/**
* Delete elements from the queue.
*
* @param array $ids Ids to delete.
*
* @return bool|int
*/
private function delete( $ids ) {
if ( 0 === count( $ids ) ) {
return 0;
}
global $wpdb;
$sql = "DELETE FROM $wpdb->options WHERE option_name IN (" . implode( ', ', array_fill( 0, count( $ids ), '%s' ) ) . ')';
$query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $ids ) );
return $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Flushes all items from the queue.
*
* @return array
*/
public function flush_all() {
$items = Utils::get_item_values( $this->fetch_items() );
$this->reset();
return $items;
}
/**
* Get all the items from the queue.
*
* @return array|object|null
*/
public function get_all() {
return $this->fetch_items();
}
/**
* Forces Checkin of the queue.
* Use with caution, this could allow multiple processes to delete
* and send from the queue at the same time
*/
public function force_checkin() {
$this->delete_checkout_id();
}
/**
* Checks if the queue is locked.
*
* @return bool
*/
public function is_locked() {
return (bool) $this->get_checkout_id();
}
/**
* Locks checkouts from the queue
* tries to wait up to $timeout seconds for the queue to be empty.
*
* @param int $timeout The wait time in seconds for the queue to be empty.
*
* @return bool|int|\WP_Error
*/
public function lock( $timeout = 30 ) {
$tries = 0;
while ( $this->has_any_items() && $tries < $timeout ) {
sleep( 1 );
++$tries;
}
if ( 30 === $tries ) {
return new WP_Error( 'lock_timeout', 'Timeout waiting for sync queue to empty' );
}
if ( $this->get_checkout_id() ) {
return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
}
// Hopefully this means we can acquire a checkout?
$result = $this->set_checkout_id( 'lock' );
if ( ! $result || is_wp_error( $result ) ) {
return $result;
}
return true;
}
/**
* Unlocks the queue.
*
* @return bool|int
*/
public function unlock() {
return $this->delete_checkout_id();
}
/**
* This option is specifically chosen to, as much as possible, preserve time order
* and minimise the possibility of collisions between multiple processes working
* at the same time.
*
* @return string
*/
protected function generate_option_name_timestamp() {
return sprintf( '%.6f', microtime( true ) );
}
/**
* Gets the checkout ID.
*
* @return bool|string
*/
private function get_checkout_id() {
global $wpdb;
$checkout_value = $wpdb->get_var(
$wpdb->prepare(
"SELECT option_value FROM $wpdb->options WHERE option_name = %s",
$this->get_lock_option_name()
)
);
if ( $checkout_value ) {
list( $checkout_id, $timestamp ) = explode( ':', $checkout_value );
if ( (int) $timestamp > time() ) {
return $checkout_id;
}
}
return false;
}
/**
* Sets the checkout id.
*
* @param string $checkout_id The ID of the checkout.
*
* @return bool|int
*/
private function set_checkout_id( $checkout_id ) {
global $wpdb;
$expires = time() + Defaults::$default_sync_queue_lock_timeout;
$updated_num = $wpdb->query(
$wpdb->prepare(
"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
"$checkout_id:$expires",
$this->get_lock_option_name()
)
);
if ( ! $updated_num ) {
$updated_num = $wpdb->query(
$wpdb->prepare(
"INSERT INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )",
$this->get_lock_option_name(),
"$checkout_id:$expires"
)
);
}
return $updated_num;
}
/**
* Deletes the checkout ID.
*
* @return bool|int
*/
private function delete_checkout_id() {
global $wpdb;
// Rather than delete, which causes fragmentation, we update in place.
return $wpdb->query(
$wpdb->prepare(
"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
'0:0',
$this->get_lock_option_name()
)
);
}
/**
* Return the lock option name.
*
* @return string
*/
private function get_lock_option_name() {
return "jpsq_{$this->id}_checkout";
}
/**
* Return the next data row option name.
*
* @return string
*/
private function get_next_data_row_option_name() {
$timestamp = $this->generate_option_name_timestamp();
// Row iterator is used to avoid collisions where we're writing data waaay fast in a single process.
if ( PHP_INT_MAX === $this->row_iterator ) {
$this->row_iterator = 0;
} else {
$this->row_iterator += 1;
}
return 'jpsq_' . $this->id . '-' . $timestamp . '-' . $this->random_int . '-' . $this->row_iterator;
}
/**
* Return the items in the queue.
*
* @param null|int $limit Limit to the number of items we fetch at once.
*
* @return array|object|null
*/
private function fetch_items( $limit = null ) {
global $wpdb;
if ( $limit ) {
$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->id}-%",
$limit
),
OBJECT
);
} else {
$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->id}-%"
),
OBJECT
);
}
return $this->unserialize_values( $items );
}
/**
* Return items with specific ids.
*
* @param array $items_ids Array of event ids.
*
* @return array|object|null
*/
private function fetch_items_by_id( $items_ids ) {
global $wpdb;
// return early if $items_ids is empty or not an array.
if ( empty( $items_ids ) || ! is_array( $items_ids ) ) {
return null;
}
$ids_placeholders = implode( ', ', array_fill( 0, count( $items_ids ), '%s' ) );
$query_with_placeholders = "SELECT option_name AS id, option_value AS value
FROM $wpdb->options
WHERE option_name IN ( $ids_placeholders )";
$items = $wpdb->get_results(
$wpdb->prepare(
$query_with_placeholders, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$items_ids
),
OBJECT
);
return $this->unserialize_values( $items );
}
/**
* Unserialize item values.
*
* @param array $items Events from the Queue to be unserialized.
*
* @return mixed
*/
private function unserialize_values( $items ) {
array_walk(
$items,
function ( $item ) {
// @codingStandardsIgnoreStart
$item->value = @unserialize( $item->value );
// @codingStandardsIgnoreEnd
}
);
return $items;
}
/**
* Return true if the buffer is still valid or an Error other wise.
*
* @param Automattic\Jetpack\Sync\Queue_Buffer $buffer The Queue_Buffer.
*
* @return bool|WP_Error
*/
private function validate_checkout( $buffer ) {
if ( ! $buffer instanceof Queue_Buffer ) {
return new WP_Error( 'not_a_buffer', 'You must checkin an instance of Automattic\\Jetpack\\Sync\\Queue_Buffer' );
}
$checkout_id = $this->get_checkout_id();
if ( ! $checkout_id ) {
return new WP_Error( 'buffer_not_checked_out', 'There are no checked out buffers' );
}
// TODO: change to strict comparison.
if ( $checkout_id != $buffer->id ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
return new WP_Error( 'buffer_mismatch', 'The buffer you checked in was not checked out' );
}
return true;
}
}

View File

@ -0,0 +1,847 @@
<?php
/**
* Sync package.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use Automattic\Jetpack\Connection\Rest_Authentication;
use WP_Error;
use WP_REST_Server;
/**
* This class will handle Sync v4 REST Endpoints.
*
* @since 1.23.1
*/
class REST_Endpoints {
/**
* Items pending send.
*
* @var array
*/
public $items = array();
/**
* Initialize REST routes.
*/
public static function initialize_rest_api() {
// Request a Full Sync.
register_rest_route(
'jetpack/v4',
'/sync/full-sync',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::full_sync_start',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'modules' => array(
'description' => __( 'Data Modules that should be included in Full Sync', 'jetpack-sync' ),
'type' => 'array',
'required' => false,
),
'users' => array(
'description' => __( 'User IDs to include in Full Sync or "initial"', 'jetpack-sync' ),
'required' => false,
),
'posts' => array(
'description' => __( 'Post IDs to include in Full Sync', 'jetpack-sync' ),
'type' => 'array',
'required' => false,
),
'comments' => array(
'description' => __( 'Comment IDs to include in Full Sync', 'jetpack-sync' ),
'type' => 'array',
'required' => false,
),
),
)
);
// Obtain Sync status.
register_rest_route(
'jetpack/v4',
'/sync/status',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::sync_status',
'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' ),
'type' => 'string',
'required' => false,
),
),
)
);
// Update Sync health status.
register_rest_route(
'jetpack/v4',
'/sync/health',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::sync_health',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'status' => array(
'description' => __( 'New Sync health status', 'jetpack-sync' ),
'type' => 'string',
'required' => true,
),
),
)
);
// Obtain Sync settings.
register_rest_route(
'jetpack/v4',
'/sync/settings',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_sync_settings',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::update_sync_settings',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
),
)
);
// Retrieve Sync Object(s).
register_rest_route(
'jetpack/v4',
'/sync/object',
array(
'methods' => WP_REST_Server::ALLMETHODS,
'callback' => __CLASS__ . '::get_sync_objects',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'module_name' => array(
'description' => __( 'Name of Sync module', 'jetpack-sync' ),
'type' => 'string',
'required' => false,
),
'object_type' => array(
'description' => __( 'Object Type', 'jetpack-sync' ),
'type' => 'string',
'required' => false,
),
'object_ids' => array(
'description' => __( 'Objects Identifiers', 'jetpack-sync' ),
'type' => 'array',
'required' => false,
),
),
)
);
// Retrieve Sync Object(s).
register_rest_route(
'jetpack/v4',
'/sync/now',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::do_sync',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'queue' => array(
'description' => __( 'Name of Sync queue.', 'jetpack-sync' ),
'type' => 'string',
'required' => true,
),
),
)
);
// Checkout Sync Objects.
register_rest_route(
'jetpack/v4',
'/sync/checkout',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::checkout',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
)
);
// Checkin Sync Objects.
register_rest_route(
'jetpack/v4',
'/sync/close',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::close',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
)
);
// Unlock Sync Queue.
register_rest_route(
'jetpack/v4',
'/sync/unlock',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::unlock_queue',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'queue' => array(
'description' => __( 'Name of Sync queue.', 'jetpack-sync' ),
'type' => 'string',
'required' => true,
),
),
)
);
// Retrieve range of Object Ids.
register_rest_route(
'jetpack/v4',
'/sync/object-id-range',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_object_id_range',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'sync_module' => array(
'description' => __( 'Name of Sync module.', 'jetpack-sync' ),
'type' => 'string',
'required' => true,
),
'batch_size' => array(
'description' => __( 'Size of batches', 'jetpack-sync' ),
'type' => 'int',
'required' => true,
),
),
)
);
// Obtain table checksums.
register_rest_route(
'jetpack/v4',
'/sync/data-check',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::data_check',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'perform_text_conversion' => array(
'description' => __( 'If text fields should be converted to latin1 in checksum calculation.', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
),
),
)
);
// Obtain histogram.
register_rest_route(
'jetpack/v4',
'/sync/data-histogram',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::data_histogram',
'permission_callback' => __CLASS__ . '::verify_default_permissions',
'args' => array(
'columns' => array(
'description' => __( 'Column mappings', 'jetpack-sync' ),
'type' => 'array',
'required' => false,
),
'object_type' => array(
'description' => __( 'Object Type', 'jetpack-sync' ),
'type' => 'string',
'required' => false,
),
'buckets' => array(
'description' => __( 'Number of histogram buckets.', 'jetpack-sync' ),
'type' => 'int',
'required' => false,
),
'start_id' => array(
'description' => __( 'Start ID for the histogram', 'jetpack-sync' ),
'type' => 'int',
'required' => false,
),
'end_id' => array(
'description' => __( 'End ID for the histogram', 'jetpack-sync' ),
'type' => 'int',
'required' => false,
),
'strip_non_ascii' => array(
'description' => __( 'Strip non-ascii characters?', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
),
'shared_salt' => array(
'description' => __( 'Shared Salt to use when generating checksum', 'jetpack-sync' ),
'type' => 'string',
'required' => false,
),
'only_range_edges' => array(
'description' => __( 'Should only range endges be returned', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
),
'detailed_drilldown' => array(
'description' => __( 'Do we want the checksum or object ids.', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
),
'perform_text_conversion' => array(
'description' => __( 'If text fields should be converted to latin1 in checksum calculation.', 'jetpack-sync' ),
'type' => 'boolean',
'required' => false,
),
),
)
);
// Trigger Dedicated Sync request.
register_rest_route(
'jetpack/v4',
'/sync/spawn-sync',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::spawn_sync',
'permission_callback' => '__return_true',
)
);
}
/**
* Trigger a Full Sync of specified modules.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response|WP_Error
*/
public static function full_sync_start( $request ) {
$modules = $request->get_param( 'modules' );
// convert list of modules into array format of "$modulename => true".
if ( ! empty( $modules ) ) {
$modules = array_map( '__return_true', array_flip( $modules ) );
}
// Process additional options.
foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
if ( 'users' === $module_name && 'initial' === $request->get_param( 'users' ) ) {
$modules['users'] = 'initial';
} elseif ( is_array( $request->get_param( $module_name ) ) ) {
$ids = $request->get_param( $module_name );
if ( count( $ids ) > 0 ) {
$modules[ $module_name ] = $ids;
}
}
}
if ( empty( $modules ) ) {
$modules = null;
}
return rest_ensure_response(
array(
'scheduled' => Actions::do_full_sync( $modules ),
)
);
}
/**
* Return Sync's status.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function sync_status( $request ) {
$fields = $request->get_param( 'fields' );
return rest_ensure_response( Actions::get_sync_status( $fields ) );
}
/**
* Return table checksums.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function data_check( $request ) {
// Disable Sync during this call, so we can resolve faster.
Actions::mark_sync_read_only();
$store = new Replicastore();
$perform_text_conversion = false;
if ( true === $request->get_param( 'perform_text_conversion' ) ) {
$perform_text_conversion = true;
}
return rest_ensure_response( $store->checksum_all( $perform_text_conversion ) );
}
/**
* Return Histogram.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function data_histogram( $request ) {
// Disable Sync during this call, so we can resolve faster.
Actions::mark_sync_read_only();
$args = $request->get_params();
if ( empty( $args['columns'] ) ) {
$args['columns'] = null; // go with defaults.
}
if ( false !== $args['strip_non_ascii'] ) {
$args['strip_non_ascii'] = true;
}
if ( true !== $args['perform_text_conversion'] ) {
$args['perform_text_conversion'] = false;
}
/**
* Hack: nullify the values of `start_id` and `end_id` if we're only requesting ranges.
*
* The endpoint doesn't support nullable values :(
*/
if ( true === $args['only_range_edges'] ) {
if ( 0 === $args['start_id'] ) {
$args['start_id'] = null;
}
if ( 0 === $args['end_id'] ) {
$args['end_id'] = null;
}
}
$store = new Replicastore();
$histogram = $store->checksum_histogram( $args['object_type'], $args['buckets'], $args['start_id'], $args['end_id'], $args['columns'], $args['strip_non_ascii'], $args['shared_salt'], $args['only_range_edges'], $args['detailed_drilldown'], $args['perform_text_conversion'] );
return rest_ensure_response(
array(
'histogram' => $histogram,
'type' => $store->get_checksum_type(),
)
);
}
/**
* Update Sync health.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function sync_health( $request ) {
switch ( $request->get_param( 'status' ) ) {
case Health::STATUS_IN_SYNC:
case Health::STATUS_OUT_OF_SYNC:
Health::update_status( $request->get_param( 'status' ) );
break;
default:
return new WP_Error( 'invalid_status', 'Invalid Sync Status Provided.' );
}
// re-fetch so we see what's really being stored.
return rest_ensure_response(
array(
'success' => Health::get_status(),
)
);
}
/**
* Obtain Sync settings.
*
* @since 1.23.1
*
* @return \WP_REST_Response
*/
public static function get_sync_settings() {
return rest_ensure_response( Settings::get_settings() );
}
/**
* Update Sync settings.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function update_sync_settings( $request ) {
$args = $request->get_params();
$sync_settings = Settings::get_settings();
foreach ( $args as $key => $value ) {
if ( false !== $value ) {
if ( is_numeric( $value ) ) {
$value = (int) $value;
}
// special case for sending empty arrays - a string with value 'empty'.
if ( 'empty' === $value ) {
$value = array();
}
$sync_settings[ $key ] = $value;
}
}
Settings::update_settings( $sync_settings );
// re-fetch so we see what's really being stored.
return rest_ensure_response( Settings::get_settings() );
}
/**
* Retrieve Sync Objects.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function get_sync_objects( $request ) {
$args = $request->get_params();
$module_name = $args['module_name'];
// Verify valid Sync Module.
$sync_module = Modules::get_module( $module_name );
if ( ! $sync_module ) {
return new WP_Error( 'invalid_module', 'You specified an invalid sync module' );
}
Actions::mark_sync_read_only();
$codec = Sender::get_instance()->get_codec();
Settings::set_is_syncing( true );
$objects = $codec->encode( $sync_module->get_objects_by_id( $args['object_type'], $args['object_ids'] ) );
Settings::set_is_syncing( false );
return rest_ensure_response(
array(
'objects' => $objects,
'codec' => $codec->name(),
)
);
}
/**
* Request Sync processing.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function do_sync( $request ) {
$queue_name = self::validate_queue( $request->get_param( 'queue' ) );
if ( is_wp_error( $queue_name ) ) {
return $queue_name;
}
$sender = Sender::get_instance();
$response = $sender->do_sync_for_queue( new Queue( $queue_name ) );
return rest_ensure_response(
array(
'response' => $response,
)
);
}
/**
* Request sync data from specified queue.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function checkout( $request ) {
$args = $request->get_params();
$queue_name = self::validate_queue( $args['queue'] );
if ( is_wp_error( $queue_name ) ) {
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 );
}
// REST Sender.
$sender = new REST_Sender();
if ( 'immediate' === $queue_name ) {
return rest_ensure_response( $sender->immediate_full_sync_pull( $number_of_items ) );
}
return rest_ensure_response( $sender->queue_pull( $queue_name, $number_of_items, $args ) );
}
/**
* Unlock a Sync queue.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function unlock_queue( $request ) {
$queue_name = $request->get_param( 'queue' );
if ( ! in_array( $queue_name, array( 'sync', 'full_sync' ), true ) ) {
return new WP_Error( 'invalid_queue', 'Queue name should be sync or full_sync', 400 );
}
$queue = new Queue( $queue_name );
// False means that there was no lock to delete.
$response = $queue->unlock();
return rest_ensure_response(
array(
'success' => $response,
)
);
}
/**
* Checkin Sync actions.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function close( $request ) {
$request_body = $request->get_params();
$queue_name = self::validate_queue( $request_body['queue'] );
if ( is_wp_error( $queue_name ) ) {
return $queue_name;
}
if ( empty( $request_body['buffer_id'] ) ) {
return new WP_Error( 'missing_buffer_id', 'Please provide a buffer id', 400 );
}
if ( ! is_array( $request_body['item_ids'] ) ) {
return new WP_Error( 'missing_item_ids', 'Please provide a list of item ids in the item_ids argument', 400 );
}
// Limit to A-Z,a-z,0-9,_,- .
$request_body['buffer_id'] = preg_replace( '/[^A-Za-z0-9]/', '', $request_body['buffer_id'] );
$request_body['item_ids'] = array_filter( array_map( array( 'Automattic\Jetpack\Sync\REST_Endpoints', 'sanitize_item_ids' ), $request_body['item_ids'] ) );
$queue = new Queue( $queue_name );
$items = $queue->peek_by_id( $request_body['item_ids'] );
// Update Full Sync Status if queue is "full_sync".
if ( 'full_sync' === $queue_name ) {
$full_sync_module = Modules::get_module( 'full-sync' );
$full_sync_module->update_sent_progress_action( $items );
}
$buffer = new Queue_Buffer( $request_body['buffer_id'], $request_body['item_ids'] );
$response = $queue->close( $buffer, $request_body['item_ids'] );
// Perform another checkout?
if ( isset( $request_body['continue'] ) && $request_body['continue'] ) {
if ( in_array( $queue_name, array( 'full_sync', 'immediate' ), true ) ) {
// Send Full Sync Actions.
Sender::get_instance()->do_full_sync();
} else {
// Send Incremental Sync Actions.
if ( $queue->has_any_items() ) {
Sender::get_instance()->do_sync();
}
}
}
if ( is_wp_error( $response ) ) {
return $response;
}
return rest_ensure_response(
array(
'success' => $response,
'status' => Actions::get_sync_status(),
)
);
}
/**
* Retrieve range of Object Ids for a specified Sync module.
*
* @since 1.23.1
*
* @param \WP_REST_Request $request The request sent to the WP REST API.
*
* @return \WP_REST_Response
*/
public static function get_object_id_range( $request ) {
$module_name = $request->get_param( 'sync_module' );
$batch_size = $request->get_param( 'batch_size' );
if ( ! self::is_valid_sync_module( $module_name ) ) {
return new WP_Error( 'invalid_module', 'This sync module cannot be used to calculate a range.', 400 );
}
$module = Modules::get_module( $module_name );
return rest_ensure_response(
array(
'ranges' => $module->get_min_max_object_ids_for_batches( $batch_size ),
)
);
}
/**
* This endpoint is used by Sync to spawn a
* dedicated Sync request which will trigger Sync to run.
*
* If Dedicated Sync is enabled, this callback should never run as
* processing of Sync actions will occur earlier and exit.
*
* @see Actions::init
* @see Sender::do_dedicated_sync_and_exit
*
* @since $$next_version$$
*
* @return \WP_REST_Response
*/
public static function spawn_sync() {
nocache_headers();
if ( ! Settings::is_dedicated_sync_enabled() ) {
return new WP_Error(
'dedicated_sync_disabled',
'Dedicated Sync flow is disabled.',
array( 'status' => 422 )
);
}
return new WP_Error(
'dedicated_sync_failed',
'Failed to process Dedicated Sync request',
array( 'status' => 500 )
);
}
/**
* Verify that request has default permissions to perform sync actions.
*
* @since 1.23.1
*
* @return bool Whether user has capability 'manage_options' or a blog token is used.
*/
public static function verify_default_permissions() {
if ( current_user_can( 'manage_options' ) || Rest_Authentication::is_signed_with_blog_token() ) {
return true;
}
$error_msg = esc_html__(
'You do not have the correct user permissions to perform this action.
Please contact your site admin if you think this is a mistake.',
'jetpack-sync'
);
return new WP_Error( 'invalid_user_permission_sync', $error_msg, array( 'status' => rest_authorization_required_code() ) );
}
/**
* Validate Queue name.
*
* @param string $value Queue Name.
*
* @return WP_Error
*/
protected static function validate_queue( $value ) {
if ( ! isset( $value ) ) {
return new WP_Error( 'invalid_queue', 'Queue name is required', 400 );
}
if ( ! in_array( $value, array( 'sync', 'full_sync', 'immediate' ), true ) ) {
return new WP_Error( 'invalid_queue', 'Queue name should be sync, full_sync or immediate', 400 );
}
return $value;
}
/**
* Validate name is a valid Sync module.
*
* @param string $module_name Name of Sync Module.
*
* @return bool
*/
protected static function is_valid_sync_module( $module_name ) {
return in_array(
$module_name,
array(
'comments',
'posts',
'terms',
'term_relationships',
'users',
),
true
);
}
/**
* Sanitize Item Ids.
*
* @param string $item Sync item identifier.
*
* @return string|string[]|null
*/
protected static function sanitize_item_ids( $item ) {
// lets not delete any options that don't start with jpsq_sync- .
if ( ! is_string( $item ) || substr( $item, 0, 5 ) !== 'jpsq_' ) {
return null;
}
// Limit to A-Z,a-z,0-9,_,-,. .
return preg_replace( '/[^A-Za-z0-9-_.]/', '', $item );
}
}

View File

@ -0,0 +1,144 @@
<?php
/**
* Sync package.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use WP_Error;
/**
* This class will handle checkout of Sync queues for REST Endpoints.
*
* @since 1.23.1
*/
class REST_Sender {
/**
* Items pending send.
*
* @var array
*/
public $items = array();
/**
* Checkout objects from the queue
*
* @param string $queue_name Name of Queue.
* @param int $number_of_items Number of Items.
* @param array $args arguments.
*
* @return array|WP_Error
*/
public function queue_pull( $queue_name, $number_of_items, $args ) {
$queue = new Queue( $queue_name );
if ( 0 === $queue->size() ) {
return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 );
}
$sender = Sender::get_instance();
// try to give ourselves as much time as possible.
set_time_limit( 0 );
if ( ! empty( $args['pop'] ) ) {
$buffer = new Queue_Buffer( 'pop', $queue->pop( $number_of_items ) );
} else {
// let's delete the checkin state.
if ( $args['force'] ) {
$queue->unlock();
}
$buffer = $this->get_buffer( $queue, $number_of_items );
}
// Check that the $buffer is not checkout out already.
if ( is_wp_error( $buffer ) ) {
return new WP_Error( 'buffer_open', "We couldn't get the buffer it is currently checked out", 400 );
}
if ( ! is_object( $buffer ) ) {
return new WP_Error( 'buffer_non-object', 'Buffer is not an object', 400 );
}
$encode = isset( $args['encode'] ) ? $args['encode'] : true;
Settings::set_is_syncing( true );
list( $items_to_send, $skipped_items_ids ) = $sender->get_items_to_send( $buffer, $encode );
Settings::set_is_syncing( false );
return array(
'buffer_id' => $buffer->id,
'items' => $items_to_send,
'skipped_items' => $skipped_items_ids,
'codec' => $encode ? $sender->get_codec()->name() : null,
'sent_timestamp' => time(),
);
}
/**
* Adds Sync items to local property.
*/
public function jetpack_sync_send_data_listener() {
foreach ( func_get_args()[0] as $key => $item ) {
$this->items[ $key ] = $item;
}
}
/**
* Check out a buffer of full sync actions.
*
* @return array Sync Actions to be returned to requestor
*/
public function immediate_full_sync_pull() {
// try to give ourselves as much time as possible.
set_time_limit( 0 );
$original_send_data_cb = array( 'Automattic\Jetpack\Sync\Actions', 'send_data' );
$temp_send_data_cb = array( $this, 'jetpack_sync_send_data_listener' );
Sender::get_instance()->set_enqueue_wait_time( 0 );
remove_filter( 'jetpack_sync_send_data', $original_send_data_cb );
add_filter( 'jetpack_sync_send_data', $temp_send_data_cb, 10, 6 );
Sender::get_instance()->do_full_sync();
remove_filter( 'jetpack_sync_send_data', $temp_send_data_cb );
add_filter( 'jetpack_sync_send_data', $original_send_data_cb, 10, 6 );
return array(
'items' => $this->items,
'codec' => Sender::get_instance()->get_codec()->name(),
'sent_timestamp' => time(),
'status' => Actions::get_sync_status(),
);
}
/**
* Checkout items out of the sync queue.
*
* @param Queue $queue Sync Queue.
* @param int $number_of_items Number of items to checkout.
*
* @return 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 );
$duration = time() - $start;
while ( is_wp_error( $buffer ) && $duration < $max_duration ) {
sleep( 2 );
$duration = time() - $start;
$buffer = $queue->checkout( $number_of_items );
}
if ( false === $buffer ) {
return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 );
}
return $buffer;
}
}

View File

@ -0,0 +1,971 @@
<?php
/**
* Sync sender.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Constants;
use WP_Error;
/**
* This class grabs pending actions from the queue and sends them
*/
class Sender {
/**
* Name of the option that stores the time of the next sync.
*
* @access public
*
* @var string
*/
const NEXT_SYNC_TIME_OPTION_NAME = 'jetpack_next_sync_time';
/**
* Sync timeout after a WPCOM error.
*
* @access public
*
* @var int
*/
const WPCOM_ERROR_SYNC_DELAY = 60;
/**
* Sync timeout after a queue has been locked.
*
* @access public
*
* @var int
*/
const QUEUE_LOCKED_SYNC_DELAY = 10;
/**
* Maximum bytes to checkout without exceeding the memory limit.
*
* @access private
*
* @var int
*/
private $dequeue_max_bytes;
/**
* Maximum bytes in a single encoded item.
*
* @access private
*
* @var int
*/
private $upload_max_bytes;
/**
* Maximum number of sync items in a single action.
*
* @access private
*
* @var int
*/
private $upload_max_rows;
/**
* Maximum time for perfirming a checkout of items from the queue (in seconds).
*
* @access private
*
* @var int
*/
private $max_dequeue_time;
/**
* How many seconds to wait after sending sync items after exceeding the sync wait threshold (in seconds).
*
* @access private
*
* @var int
*/
private $sync_wait_time;
/**
* How much maximum time to wait for the checkout to finish (in seconds).
*
* @access private
*
* @var int
*/
private $sync_wait_threshold;
/**
* How much maximum time to wait for the sync items to be queued for sending (in seconds).
*
* @access private
*
* @var int
*/
private $enqueue_wait_time;
/**
* Incremental sync queue object.
*
* @access private
*
* @var Automattic\Jetpack\Sync\Queue
*/
private $sync_queue;
/**
* Full sync queue object.
*
* @access private
*
* @var Automattic\Jetpack\Sync\Queue
*/
private $full_sync_queue;
/**
* Codec object for encoding and decoding sync items.
*
* @access private
*
* @var Automattic\Jetpack\Sync\Codec_Interface
*/
private $codec;
/**
* The current user before we change or clear it.
*
* @access private
*
* @var \WP_User
*/
private $old_user;
/**
* Container for the singleton instance of this class.
*
* @access private
* @static
*
* @var Automattic\Jetpack\Sync\Sender
*/
private static $instance;
/**
* Retrieve the singleton instance of this class.
*
* @access public
* @static
*
* @return Sender
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
* This is necessary because you can't use "new" when you declare instance properties >:(
*
* @access protected
* @static
*/
protected function __construct() {
$this->set_defaults();
$this->init();
}
/**
* Initialize the sender.
* Prepares the current user and initializes all sync modules.
*
* @access private
*/
private function init() {
add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_set_user_from_token' ), 1 );
add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_clear_user_from_token' ), 20 );
add_filter( 'jetpack_xmlrpc_unauthenticated_methods', array( $this, 'register_jetpack_xmlrpc_methods' ) );
foreach ( Modules::get_modules() as $module ) {
$module->init_before_send();
}
}
/**
* Detect if this is a XMLRPC request with a valid signature.
* If so, changes the user to the new one.
*
* @access public
*/
public function maybe_set_user_from_token() {
$connection = new Manager();
$verified_user = $connection->verify_xml_rpc_signature();
if ( Constants::is_true( 'XMLRPC_REQUEST' ) &&
! is_wp_error( $verified_user )
&& $verified_user
) {
$old_user = wp_get_current_user();
$this->old_user = isset( $old_user->ID ) ? $old_user->ID : 0;
wp_set_current_user( $verified_user['user_id'] );
}
}
/**
* If we used to have a previous current user, revert back to it.
*
* @access public
*/
public function maybe_clear_user_from_token() {
if ( isset( $this->old_user ) ) {
wp_set_current_user( $this->old_user );
}
}
/**
* Retrieve the next sync time.
*
* @access public
*
* @param string $queue_name Name of the queue.
* @return float Timestamp of the next sync.
*/
public function get_next_sync_time( $queue_name ) {
return (float) get_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name, 0 );
}
/**
* Set the next sync time.
*
* @access public
*
* @param int $time Timestamp of the next sync.
* @param string $queue_name Name of the queue.
* @return boolean True if update was successful, false otherwise.
*/
public function set_next_sync_time( $time, $queue_name ) {
return update_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name, $time, true );
}
/**
* Trigger a full sync.
*
* @access public
*
* @return boolean|WP_Error True if this sync sending was successful, error object otherwise.
*/
public function do_full_sync() {
$sync_module = Modules::get_module( 'full-sync' );
if ( ! $sync_module ) {
return;
}
// Full Sync Disabled.
if ( ! Settings::get_setting( 'full_sync_sender_enabled' ) ) {
return;
}
// Don't sync if request is marked as read only.
if ( Constants::is_true( 'JETPACK_SYNC_READ_ONLY' ) ) {
return new WP_Error( 'jetpack_sync_read_only' );
}
// Sync not started or Sync finished.
$status = $sync_module->get_status();
if ( false === $status['started'] || ( ! empty( $status['started'] ) && ! empty( $status['finished'] ) ) ) {
return false;
}
$this->continue_full_sync_enqueue();
// immediate full sync sends data in continue_full_sync_enqueue.
if ( false === strpos( get_class( $sync_module ), 'Full_Sync_Immediately' ) ) {
return $this->do_sync_and_set_delays( $this->full_sync_queue );
} else {
$status = $sync_module->get_status();
// Sync not started or Sync finished.
if ( false === $status['started'] || ( ! empty( $status['started'] ) && ! empty( $status['finished'] ) ) ) {
return false;
} else {
return true;
}
}
}
/**
* Enqueue the next sync items for sending.
* Will not be done if the current request is a WP import one.
* Will be delayed until the next sync time comes.
*
* @access private
*/
private function continue_full_sync_enqueue() {
if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
return false;
}
if ( $this->get_next_sync_time( 'full-sync-enqueue' ) > microtime( true ) ) {
return false;
}
Modules::get_module( 'full-sync' )->continue_enqueuing();
$this->set_next_sync_time( time() + $this->get_enqueue_wait_time(), 'full-sync-enqueue' );
}
/**
* Trigger incremental sync.
*
* @access public
*
* @return boolean|WP_Error True if this sync sending was successful, error object otherwise.
*/
public function do_sync() {
if ( ! Settings::is_dedicated_sync_enabled() ) {
$result = $this->do_sync_and_set_delays( $this->sync_queue );
} else {
$result = Dedicated_Sender::spawn_sync( $this->sync_queue );
}
return $result;
}
/**
* Trigger incremental sync and early exit on Dedicated Sync request.
*
* @access public
*
* @param bool $do_real_exit If we should exit at the end of the request. We should by default.
* In the context of running this in the REST API, we actually want to return an error.
*
* @return void|WP_Error
*/
public function do_dedicated_sync_and_exit( $do_real_exit = true ) {
nocache_headers();
if ( ! Settings::is_dedicated_sync_enabled() ) {
return new WP_Error( 'dedicated_sync_disabled', 'Dedicated Sync flow is disabled.' );
}
if ( ! Dedicated_Sender::is_dedicated_sync_request() ) {
return new WP_Error( 'non_dedicated_sync_request', 'Not a Dedicated Sync request.' );
}
/**
* Output an `OK` to show that Dedicated Sync is enabled and we can process events.
* This is used to test the feature is working.
*
* @see \Automattic\Jetpack\Sync\Dedicated_Sender::can_spawn_dedicated_sync_request
*/
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo Dedicated_Sender::DEDICATED_SYNC_VALIDATION_STRING;
// Try to disconnect the request as quickly as possible and process things in the background.
$this->fastcgi_finish_request();
// 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 );
// If no errors occurred, re-spawn a dedicated Sync request.
if ( true === $result ) {
Dedicated_Sender::spawn_sync( $this->sync_queue );
}
if ( $do_real_exit ) {
exit;
}
}
/**
* Trigger sync for a certain sync queue.
* Responsible for setting next sync time.
* Will not be delayed if the current request is a WP import one.
* Will be delayed until the next sync time comes.
*
* @access public
*
* @param Automattic\Jetpack\Sync\Queue $queue Queue object.
*
* @return boolean|WP_Error True if this sync sending was successful, error object otherwise.
*/
public function do_sync_and_set_delays( $queue ) {
// Don't sync if importing.
if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
return new WP_Error( 'is_importing' );
}
// Don't sync if request is marked as read only.
if ( Constants::is_true( 'JETPACK_SYNC_READ_ONLY' ) ) {
return new WP_Error( 'jetpack_sync_read_only' );
}
if ( ! Settings::is_sender_enabled( $queue->id ) ) {
return new WP_Error( 'sender_disabled_for_queue_' . $queue->id );
}
// Return early if we've gotten a retry-after header response.
$retry_time = get_option( Actions::RETRY_AFTER_PREFIX . $queue->id );
if ( $retry_time ) {
// If expired update to false but don't send. Send will occurr in new request to avoid race conditions.
if ( microtime( true ) > $retry_time ) {
update_option( Actions::RETRY_AFTER_PREFIX . $queue->id, false, false );
}
return new WP_Error( 'retry_after' );
}
// Don't sync if we are throttled.
if ( $this->get_next_sync_time( $queue->id ) > microtime( true ) ) {
return new WP_Error( 'sync_throttled' );
}
$start_time = microtime( true );
Settings::set_is_syncing( true );
$sync_result = $this->do_sync_for_queue( $queue );
Settings::set_is_syncing( false );
$exceeded_sync_wait_threshold = ( microtime( true ) - $start_time ) > (float) $this->get_sync_wait_threshold();
if ( is_wp_error( $sync_result ) ) {
if ( 'unclosed_buffer' === $sync_result->get_error_code() ) {
$this->set_next_sync_time( time() + self::QUEUE_LOCKED_SYNC_DELAY, $queue->id );
}
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.
$this->set_next_sync_time( time() + $this->get_sync_wait_time(), $queue->id );
}
return $sync_result;
}
/**
* Retrieve the next sync items to send.
*
* @access public
*
* @param (array|Automattic\Jetpack\Sync\Queue_Buffer) $buffer_or_items Queue buffer or array of objects.
* @param boolean $encode Whether to encode the items.
* @return array Sync items to send.
*/
public function get_items_to_send( $buffer_or_items, $encode = true ) {
// Track how long we've been processing so we can avoid request timeouts.
$start_time = microtime( true );
$upload_size = 0;
$items_to_send = array();
$items = is_array( $buffer_or_items ) ? $buffer_or_items : $buffer_or_items->get_items();
if ( ! is_array( $items ) ) {
$items = array();
}
// Set up current screen to avoid errors rendering content.
require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
require_once ABSPATH . 'wp-admin/includes/screen.php';
set_current_screen( 'sync' );
$skipped_items_ids = array();
/**
* We estimate the total encoded size as we go by encoding each item individually.
* This is expensive, but the only way to really know :/
*/
foreach ( $items as $key => $item ) {
// Suspending cache addition help prevent overloading in memory cache of large sites.
wp_suspend_cache_addition( true );
/**
* Modify the data within an action before it is serialized and sent to the server
* For example, during full sync this expands Post ID's into full Post objects,
* so that we don't have to serialize the whole object into the queue.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param array The action parameters
* @param int The ID of the user who triggered the action
*/
$item[1] = apply_filters( 'jetpack_sync_before_send_' . $item[0], $item[1], $item[2] );
wp_suspend_cache_addition( false );
// Serialization usage can lead to empty, null or false action_name. Lets skip as there is no information to send.
if ( empty( $item[0] ) || false === $item[1] ) {
$skipped_items_ids[] = $key;
continue;
}
$encoded_item = $this->codec->encode( $item );
$upload_size += strlen( $encoded_item );
if ( $upload_size > $this->upload_max_bytes && count( $items_to_send ) > 0 ) {
break;
}
$items_to_send[ $key ] = $encode ? $encoded_item : $item;
if ( microtime( true ) - $start_time > $this->max_dequeue_time ) {
break;
}
}
return array( $items_to_send, $skipped_items_ids, $items, microtime( true ) - $start_time );
}
/**
* If supported, flush all response data to the client and finish the request.
* This allows for time consuming tasks to be performed without leaving the connection open.
*
* @access private
*/
private function fastcgi_finish_request() {
if ( function_exists( 'fastcgi_finish_request' ) && version_compare( phpversion(), '7.0.16', '>=' ) ) {
fastcgi_finish_request();
}
}
/**
* Perform sync for a certain sync queue.
*
* @access public
*
* @param Automattic\Jetpack\Sync\Queue $queue Queue object.
*
* @return boolean|WP_Error True if this sync sending was successful, error object otherwise.
*/
public function do_sync_for_queue( $queue ) {
do_action( 'jetpack_sync_before_send_queue_' . $queue->id );
if ( $queue->size() === 0 ) {
return new WP_Error( 'empty_queue_' . $queue->id );
}
/**
* Now that we're sure we are about to sync, try to ignore user abort
* so we can avoid getting into a bad state.
*/
if ( function_exists( 'ignore_user_abort' ) ) {
ignore_user_abort( true );
}
/* Don't make the request block till we finish, if possible. */
if ( Constants::is_true( 'REST_REQUEST' ) || Constants::is_true( 'XMLRPC_REQUEST' ) ) {
$this->fastcgi_finish_request();
}
$checkout_start_time = microtime( true );
$buffer = $queue->checkout_with_memory_limit( $this->dequeue_max_bytes, $this->upload_max_rows );
if ( ! $buffer ) {
// Buffer has no items.
return new WP_Error( 'empty_buffer' );
}
if ( is_wp_error( $buffer ) ) {
return $buffer;
}
$checkout_duration = microtime( true ) - $checkout_start_time;
list( $items_to_send, $skipped_items_ids, $items, $preprocess_duration ) = $this->get_items_to_send( $buffer, true );
if ( ! empty( $items_to_send ) ) {
/**
* Fires when data is ready to send to the server.
* Return false or WP_Error to abort the sync (e.g. if there's an error)
* The items will be automatically re-sent later
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param array $data The action buffer
* @param string $codec The codec name used to encode the data
* @param double $time The current time
* @param string $queue The queue used to send ('sync' or 'full_sync')
* @param float $checkout_duration The duration of the checkout operation.
* @param float $preprocess_duration The duration of the pre-process operation.
* @param int $queue_size The size of the sync queue at the time of processing.
*/
Settings::set_is_sending( true );
$processed_item_ids = apply_filters( 'jetpack_sync_send_data', $items_to_send, $this->codec->name(), microtime( true ), $queue->id, $checkout_duration, $preprocess_duration, $queue->size(), $buffer->id );
Settings::set_is_sending( false );
} else {
$processed_item_ids = $skipped_items_ids;
$skipped_items_ids = array();
}
if ( 'non-blocking' !== $processed_item_ids ) {
if ( ! $processed_item_ids || is_wp_error( $processed_item_ids ) ) {
$checked_in_item_ids = $queue->checkin( $buffer );
if ( is_wp_error( $checked_in_item_ids ) ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'Error checking in buffer: ' . $checked_in_item_ids->get_error_message() );
$queue->force_checkin();
}
if ( is_wp_error( $processed_item_ids ) ) {
return new WP_Error( 'wpcom_error', $processed_item_ids->get_error_code() );
}
// Returning a wpcom_error is a sign to the caller that we should wait a while before syncing again.
return new WP_Error( 'wpcom_error', 'jetpack_sync_send_data_false' );
} else {
// Detect if the last item ID was an error.
$had_wp_error = is_wp_error( end( $processed_item_ids ) );
if ( $had_wp_error ) {
$wp_error = array_pop( $processed_item_ids );
}
// Also checkin any items that were skipped.
if ( count( $skipped_items_ids ) > 0 ) {
$processed_item_ids = array_merge( $processed_item_ids, $skipped_items_ids );
}
$processed_items = array_intersect_key( $items, array_flip( $processed_item_ids ) );
/**
* Allows us to keep track of all the actions that have been sent.
* Allows us to calculate the progress of specific actions.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param array $processed_actions The actions that we send successfully.
*/
do_action( 'jetpack_sync_processed_actions', $processed_items );
$queue->close( $buffer, $processed_item_ids );
// Returning a WP_Error is a sign to the caller that we should wait a while before syncing again.
if ( $had_wp_error ) {
return new WP_Error( 'wpcom_error', $wp_error->get_error_code() );
}
}
}
return true;
}
/**
* Immediately sends a single item without firing or enqueuing it
*
* @param string $action_name The action.
* @param array $data The data associated with the action.
*
* @return Items processed. TODO: this doesn't make much sense anymore, it should probably be just a bool.
*/
public function send_action( $action_name, $data = null ) {
if ( ! Settings::is_sender_enabled( 'full_sync' ) ) {
return array();
}
// Compose the data to be sent.
$action_to_send = $this->create_action_to_send( $action_name, $data );
list( $items_to_send, $skipped_items_ids, $items, $preprocess_duration ) = $this->get_items_to_send( $action_to_send, true ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
Settings::set_is_sending( true );
$processed_item_ids = apply_filters( 'jetpack_sync_send_data', $items_to_send, $this->get_codec()->name(), microtime( true ), 'immediate-send', 0, $preprocess_duration );
Settings::set_is_sending( false );
/**
* Allows us to keep track of all the actions that have been sent.
* Allows us to calculate the progress of specific actions.
*
* @param array $processed_actions The actions that we send successfully.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*/
do_action( 'jetpack_sync_processed_actions', $action_to_send );
return $processed_item_ids;
}
/**
* Create an synthetic action for direct sending to WPCOM during full sync (for example)
*
* @access private
*
* @param string $action_name The action.
* @param array $data The data associated with the action.
* @return array An array of synthetic sync actions keyed by current microtime(true)
*/
private function create_action_to_send( $action_name, $data ) {
return array(
(string) microtime( true ) => array(
$action_name,
$data,
get_current_user_id(),
microtime( true ),
Settings::is_importing(),
),
);
}
/**
* Returns any object that is able to be synced.
*
* @access public
*
* @param array $args the synchronized object parameters.
* @return string Encoded sync object.
*/
public function sync_object( $args ) {
// For example: posts, post, 5.
list( $module_name, $object_type, $id ) = $args;
$sync_module = Modules::get_module( $module_name );
$codec = $this->get_codec();
return $codec->encode( $sync_module->get_object_by_id( $object_type, $id ) );
}
/**
* Register additional sync XML-RPC methods available to Jetpack for authenticated users.
*
* @access public
* @since 1.6.3
* @since-jetpack 7.8.0
*
* @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
* @return array Filtered XML-RPC methods.
*/
public function register_jetpack_xmlrpc_methods( $jetpack_methods ) {
$jetpack_methods['jetpack.syncObject'] = array( $this, 'sync_object' );
return $jetpack_methods;
}
/**
* Get the incremental sync queue object.
*
* @access public
*
* @return Automattic\Jetpack\Sync\Queue Queue object.
*/
public function get_sync_queue() {
return $this->sync_queue;
}
/**
* Get the full sync queue object.
*
* @access public
*
* @return Automattic\Jetpack\Sync\Queue Queue object.
*/
public function get_full_sync_queue() {
return $this->full_sync_queue;
}
/**
* Get the codec object.
*
* @access public
*
* @return Automattic\Jetpack\Sync\Codec_Interface Codec object.
*/
public function get_codec() {
return $this->codec;
}
/**
* Determine the codec object.
* Use gzip deflate if supported.
*
* @access public
*/
public function set_codec() {
if ( function_exists( 'gzinflate' ) ) {
$this->codec = new JSON_Deflate_Array_Codec();
} else {
$this->codec = new Simple_Codec();
}
}
/**
* Compute and send all the checksums.
*
* @access public
*/
public function send_checksum() {
$store = new Replicastore();
do_action( 'jetpack_sync_checksum', $store->checksum_all() );
}
/**
* Reset the incremental sync queue.
*
* @access public
*/
public function reset_sync_queue() {
$this->sync_queue->reset();
}
/**
* Reset the full sync queue.
*
* @access public
*/
public function reset_full_sync_queue() {
$this->full_sync_queue->reset();
}
/**
* Set the maximum bytes to checkout without exceeding the memory limit.
*
* @access public
*
* @param int $size Maximum bytes to checkout.
*/
public function set_dequeue_max_bytes( $size ) {
$this->dequeue_max_bytes = $size;
}
/**
* Set the maximum bytes in a single encoded item.
*
* @access public
*
* @param int $max_bytes Maximum bytes in a single encoded item.
*/
public function set_upload_max_bytes( $max_bytes ) {
$this->upload_max_bytes = $max_bytes;
}
/**
* Set the maximum number of sync items in a single action.
*
* @access public
*
* @param int $max_rows Maximum number of sync items.
*/
public function set_upload_max_rows( $max_rows ) {
$this->upload_max_rows = $max_rows;
}
/**
* Set the sync wait time (in seconds).
*
* @access public
*
* @param int $seconds Sync wait time.
*/
public function set_sync_wait_time( $seconds ) {
$this->sync_wait_time = $seconds;
}
/**
* Get current sync wait time (in seconds).
*
* @access public
*
* @return int Sync wait time.
*/
public function get_sync_wait_time() {
return $this->sync_wait_time;
}
/**
* Set the enqueue wait time (in seconds).
*
* @access public
*
* @param int $seconds Enqueue wait time.
*/
public function set_enqueue_wait_time( $seconds ) {
$this->enqueue_wait_time = $seconds;
}
/**
* Get current enqueue wait time (in seconds).
*
* @access public
*
* @return int Enqueue wait time.
*/
public function get_enqueue_wait_time() {
return $this->enqueue_wait_time;
}
/**
* Set the sync wait threshold (in seconds).
*
* @access public
*
* @param int $seconds Sync wait threshold.
*/
public function set_sync_wait_threshold( $seconds ) {
$this->sync_wait_threshold = $seconds;
}
/**
* Get current sync wait threshold (in seconds).
*
* @access public
*
* @return int Sync wait threshold.
*/
public function get_sync_wait_threshold() {
return $this->sync_wait_threshold;
}
/**
* Set the maximum time for perfirming a checkout of items from the queue (in seconds).
*
* @access public
*
* @param int $seconds Maximum dequeue time.
*/
public function set_max_dequeue_time( $seconds ) {
$this->max_dequeue_time = $seconds;
}
/**
* Initialize the sync queues, codec and set the default settings.
*
* @access public
*/
public function set_defaults() {
$this->sync_queue = new Queue( 'sync' );
$this->full_sync_queue = new Queue( 'full_sync' );
$this->set_codec();
// Saved settings.
Settings::set_importing( null );
$settings = Settings::get_settings();
$this->set_dequeue_max_bytes( $settings['dequeue_max_bytes'] );
$this->set_upload_max_bytes( $settings['upload_max_bytes'] );
$this->set_upload_max_rows( $settings['upload_max_rows'] );
$this->set_sync_wait_time( $settings['sync_wait_time'] );
$this->set_enqueue_wait_time( $settings['enqueue_wait_time'] );
$this->set_sync_wait_threshold( $settings['sync_wait_threshold'] );
$this->set_max_dequeue_time( Defaults::get_max_sync_execution_time() );
}
/**
* Reset sync queues, modules and settings.
*
* @access public
*/
public function reset_data() {
$this->reset_sync_queue();
$this->reset_full_sync_queue();
foreach ( Modules::get_modules() as $module ) {
$module->reset_data();
}
foreach ( array( 'sync', 'full_sync', 'full-sync-enqueue' ) as $queue_name ) {
delete_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name );
}
Settings::reset_data();
}
/**
* Perform cleanup at the event of plugin uninstallation.
*
* @access public
*/
public function uninstall() {
// Lets delete all the other fun stuff like transient and option and the sync queue.
$this->reset_data();
// Delete the full sync status.
delete_option( 'jetpack_full_sync_status' );
// Clear the sync cron.
wp_clear_scheduled_hook( 'jetpack_sync_cron' );
wp_clear_scheduled_hook( 'jetpack_sync_full_cron' );
}
}

View File

@ -0,0 +1,195 @@
<?php
/**
* Sync server.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use WP_Error;
/**
* Simple version of a Jetpack Sync Server - just receives arrays of events and
* issues them locally with the 'jetpack_sync_remote_action' action.
*/
class Server {
/**
* Codec used to decode sync events.
*
* @access private
*
* @var Automattic\Jetpack\Sync\Codec_Interface
*/
private $codec;
/**
* Maximum time for processing sync actions.
*
* @access public
*
* @var int
*/
const MAX_TIME_PER_REQUEST_IN_SECONDS = 15;
/**
* Prefix of the blog lock transient.
*
* @access public
*
* @var string
*/
const BLOG_LOCK_TRANSIENT_PREFIX = 'jp_sync_req_lock_';
/**
* Lifetime of the blog lock transient.
*
* @access public
*
* @var int
*/
const BLOG_LOCK_TRANSIENT_EXPIRY = 60; // Seconds.
/**
* Constructor.
*
* This is necessary because you can't use "new" when you declare instance properties >:(
*
* @access public
*/
public function __construct() {
$this->codec = new JSON_Deflate_Array_Codec();
}
/**
* Set the codec instance.
*
* @access public
*
* @param Automattic\Jetpack\Sync\Codec_Interface $codec Codec instance.
*/
public function set_codec( Codec_Interface $codec ) {
$this->codec = $codec;
}
/**
* Attempt to lock the request when the server receives concurrent requests from the same blog.
*
* @access public
*
* @param int $blog_id ID of the blog.
* @param int $expiry Blog lock transient lifetime.
* @return boolean True if succeeded, false otherwise.
*/
public function attempt_request_lock( $blog_id, $expiry = self::BLOG_LOCK_TRANSIENT_EXPIRY ) {
$transient_name = $this->get_concurrent_request_transient_name( $blog_id );
$locked_time = get_site_transient( $transient_name );
if ( $locked_time ) {
return false;
}
set_site_transient( $transient_name, microtime( true ), $expiry );
return true;
}
/**
* Retrieve the blog lock transient name for a particular blog.
*
* @access public
*
* @param int $blog_id ID of the blog.
* @return string Name of the blog lock transient.
*/
private function get_concurrent_request_transient_name( $blog_id ) {
return self::BLOG_LOCK_TRANSIENT_PREFIX . $blog_id;
}
/**
* Remove the request lock from a particular blog ID.
*
* @access public
*
* @param int $blog_id ID of the blog.
*/
public function remove_request_lock( $blog_id ) {
delete_site_transient( $this->get_concurrent_request_transient_name( $blog_id ) );
}
/**
* Receive and process sync events.
*
* @access public
*
* @param array $data Sync events.
* @param object $token The auth token used to invoke the API.
* @param int $sent_timestamp Timestamp (in seconds) when the actions were transmitted.
* @param string $queue_id ID of the queue from which the event was sent (`sync` or `full_sync`).
* @return array Processed sync events.
*/
public function receive( $data, $token = null, $sent_timestamp = null, $queue_id = null ) {
$start_time = microtime( true );
if ( ! is_array( $data ) ) {
return new WP_Error( 'action_decoder_error', 'Events must be an array' );
}
if ( $token && ! $this->attempt_request_lock( $token->blog_id ) ) {
/**
* Fires when the server receives two concurrent requests from the same blog
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param token The token object of the misbehaving site
*/
do_action( 'jetpack_sync_multi_request_fail', $token );
return new WP_Error( 'concurrent_request_error', 'There is another request running for the same blog ID' );
}
$events = wp_unslash( array_map( array( $this->codec, 'decode' ), $data ) );
$events_processed = array();
/**
* Fires when an array of actions are received from a remote Jetpack site
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param array Array of actions received from the remote site
*/
do_action( 'jetpack_sync_remote_actions', $events, $token );
foreach ( $events as $key => $event ) {
list( $action_name, $args, $user_id, $timestamp, $silent ) = $event;
/**
* Fires when an action is received from a remote Jetpack site
*
* @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
*/
do_action( 'jetpack_sync_remote_action', $action_name, $args, $user_id, $silent, $timestamp, $sent_timestamp, $queue_id, $token );
$events_processed[] = $key;
if ( microtime( true ) - $start_time > self::MAX_TIME_PER_REQUEST_IN_SECONDS ) {
break;
}
}
if ( $token ) {
$this->remove_request_lock( $token->blog_id );
}
return $events_processed;
}
}

View File

@ -0,0 +1,588 @@
<?php
/**
* Sync settings.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* Class to manage the sync settings.
*/
class Settings {
/**
* Prefix, used for the sync settings option names.
*
* @access public
*
* @var string
*/
const SETTINGS_OPTION_PREFIX = 'jetpack_sync_settings_';
/**
* A whitelist of valid settings.
*
* @access public
* @static
*
* @var array
*/
public static $valid_settings = array(
'dequeue_max_bytes' => true,
'upload_max_bytes' => true,
'upload_max_rows' => true,
'sync_wait_time' => true,
'sync_wait_threshold' => true,
'enqueue_wait_time' => true,
'max_queue_size' => true,
'max_queue_lag' => true,
'queue_max_writes_sec' => true,
'post_types_blacklist' => true,
'taxonomies_blacklist' => true,
'disable' => true,
'network_disable' => true,
'render_filtered_content' => true,
'post_meta_whitelist' => true,
'comment_meta_whitelist' => true,
'max_enqueue_full_sync' => true,
'max_queue_size_full_sync' => true,
'sync_via_cron' => true,
'cron_sync_time_limit' => true,
'known_importers' => true,
'term_relationships_full_sync_item_size' => true,
'sync_sender_enabled' => true,
'full_sync_sender_enabled' => true,
'full_sync_send_duration' => true,
'full_sync_limits' => true,
'checksum_disable' => true,
'dedicated_sync_enabled' => true,
);
/**
* Whether WordPress is currently running an import.
*
* @access public
* @static
*
* @var null|boolean
*/
public static $is_importing;
/**
* Whether WordPress is currently running a WP cron request.
*
* @access public
* @static
*
* @var null|boolean
*/
public static $is_doing_cron;
/**
* Whether we're currently syncing.
*
* @access public
* @static
*
* @var null|boolean
*/
public static $is_syncing;
/**
* Whether we're currently sending sync items.
*
* @access public
* @static
*
* @var null|boolean
*/
public static $is_sending;
/**
* Retrieve all settings with their current values.
*
* @access public
* @static
*
* @return array All current settings.
*/
public static function get_settings() {
$settings = array();
foreach ( array_keys( self::$valid_settings ) as $setting ) {
$settings[ $setting ] = self::get_setting( $setting );
}
return $settings;
}
/**
* Fetches the setting. It saves it if the setting doesn't exist, so that it gets
* autoloaded on page load rather than re-queried every time.
*
* @access public
* @static
*
* @param string $setting The setting name.
* @return mixed The setting value.
*/
public static function get_setting( $setting ) {
if ( ! isset( self::$valid_settings[ $setting ] ) ) {
return false;
}
if ( self::is_network_setting( $setting ) ) {
if ( is_multisite() ) {
$value = get_site_option( self::SETTINGS_OPTION_PREFIX . $setting );
} else {
// On single sites just return the default setting.
return Defaults::get_default_setting( $setting );
}
} else {
$value = get_option( self::SETTINGS_OPTION_PREFIX . $setting );
}
if ( false === $value ) { // No default value is set.
$value = Defaults::get_default_setting( $setting );
if ( self::is_network_setting( $setting ) ) {
update_site_option( self::SETTINGS_OPTION_PREFIX . $setting, $value );
} else {
// We set one so that it gets autoloaded.
update_option( self::SETTINGS_OPTION_PREFIX . $setting, $value, true );
}
}
if ( is_numeric( $value ) ) {
$value = (int) $value;
}
$default_array_value = null;
switch ( $setting ) {
case 'post_types_blacklist':
$default_array_value = Defaults::$blacklisted_post_types;
break;
case 'taxonomies_blacklist':
$default_array_value = Defaults::$blacklisted_taxonomies;
break;
case 'post_meta_whitelist':
$default_array_value = Defaults::get_post_meta_whitelist();
break;
case 'comment_meta_whitelist':
$default_array_value = Defaults::get_comment_meta_whitelist();
break;
case 'known_importers':
$default_array_value = Defaults::get_known_importers();
break;
}
if ( $default_array_value ) {
if ( is_array( $value ) ) {
$value = array_unique( array_merge( $value, $default_array_value ) );
} else {
$value = $default_array_value;
}
}
return $value;
}
/**
* Change multiple settings in the same time.
*
* @access public
* @static
*
* @param array $new_settings The new settings.
*/
public static function update_settings( $new_settings ) {
$validated_settings = array_intersect_key( $new_settings, self::$valid_settings );
foreach ( $validated_settings as $setting => $value ) {
if ( self::is_network_setting( $setting ) ) {
if ( is_multisite() && is_main_site() ) {
$updated = update_site_option( self::SETTINGS_OPTION_PREFIX . $setting, $value );
}
} else {
$updated = update_option( self::SETTINGS_OPTION_PREFIX . $setting, $value, true );
}
// If we set the disabled option to true, clear the queues.
if ( ( 'disable' === $setting || 'network_disable' === $setting ) && (bool) $value ) {
$listener = Listener::get_instance();
$listener->get_sync_queue()->reset();
$listener->get_full_sync_queue()->reset();
}
// Do not enable Dedicated Sync if we cannot spawn a Dedicated Sync request.
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 );
}
}
}
}
/**
* Whether the specified setting is a network setting.
*
* @access public
* @static
*
* @param string $setting Setting name.
* @return boolean Whether the setting is a network setting.
*/
public static function is_network_setting( $setting ) {
return strpos( $setting, 'network_' ) === 0;
}
/**
* Returns escaped SQL for blacklisted post types.
* Can be injected directly into a WHERE clause.
*
* @access public
* @static
*
* @return string SQL WHERE clause.
*/
public static function get_blacklisted_post_types_sql() {
return 'post_type NOT IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'post_types_blacklist' ) ) ) . '\')';
}
/**
* Returns escaped values for disallowed post types.
*
* @access public
* @static
*
* @return array Post type filter values
*/
public static function get_disallowed_post_types_structured() {
return array(
'post_type' => array(
'operator' => 'NOT IN',
'values' => array_map( 'esc_sql', self::get_setting( 'post_types_blacklist' ) ),
),
);
}
/**
* Returns escaped SQL for blacklisted taxonomies.
* Can be injected directly into a WHERE clause.
*
* @access public
* @static
*
* @return string SQL WHERE clause.
*/
public static function get_blacklisted_taxonomies_sql() {
return "taxonomy NOT IN ('" . join( "', '", array_map( 'esc_sql', self::get_setting( 'taxonomies_blacklist' ) ) ) . "')";
}
/**
* Returns escaped SQL for blacklisted post meta.
* Can be injected directly into a WHERE clause.
*
* @access public
* @static
*
* @return string SQL WHERE clause.
*/
public static function get_whitelisted_post_meta_sql() {
return 'meta_key IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'post_meta_whitelist' ) ) ) . '\')';
}
/**
* Returns escaped SQL for allowed post meta keys.
*
* @access public
* @static
*
* @return array Meta keys filter values
*/
public static function get_allowed_post_meta_structured() {
return array(
'meta_key' => array(
'operator' => 'IN',
'values' => array_map( 'esc_sql', self::get_setting( 'post_meta_whitelist' ) ),
),
);
}
/**
* Returns structured SQL clause for blacklisted taxonomies.
*
* @access public
* @static
*
* @return array taxonomies filter values
*/
public static function get_blacklisted_taxonomies_structured() {
return array(
'taxonomy' => array(
'operator' => 'NOT IN',
'values' => array_map( 'esc_sql', self::get_setting( 'taxonomies_blacklist' ) ),
),
);
}
/**
* Returns structured SQL clause for allowed taxonomies.
*
* @access public
* @static
*
* @return array taxonomies filter values
*/
public static function get_allowed_taxonomies_structured() {
global $wp_taxonomies;
$allowed_taxonomies = array_keys( $wp_taxonomies );
$allowed_taxonomies = array_diff( $allowed_taxonomies, self::get_setting( 'taxonomies_blacklist' ) );
return array(
'taxonomy' => array(
'operator' => 'IN',
'values' => array_map( 'esc_sql', $allowed_taxonomies ),
),
);
}
/**
* Returns escaped SQL for blacklisted comment meta.
* Can be injected directly into a WHERE clause.
*
* @access public
* @static
*
* @return string SQL WHERE clause.
*/
public static function get_whitelisted_comment_meta_sql() {
return 'meta_key IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'comment_meta_whitelist' ) ) ) . '\')';
}
/**
* Returns SQL-escaped values for allowed post meta keys.
*
* @access public
* @static
*
* @return array Meta keys filter values
*/
public static function get_allowed_comment_meta_structured() {
return array(
'meta_key' => array(
'operator' => 'IN',
'values' => array_map( 'esc_sql', self::get_setting( 'comment_meta_whitelist' ) ),
),
);
}
/**
* Returns SQL-escaped values for allowed order_item meta keys.
*
* @access public
* @static
*
* @return array Meta keys filter values
*/
public static function get_allowed_order_itemmeta_structured() {
// Make sure that we only try to add the properties when the class exists.
if ( ! class_exists( '\Automattic\Jetpack\Sync\Modules\WooCommerce' ) ) {
return array();
}
$values = \Automattic\Jetpack\Sync\Modules\WooCommerce::$order_item_meta_whitelist;
return array(
'meta_key' => array(
'operator' => 'IN',
'values' => array_map( 'esc_sql', $values ),
),
);
}
/**
* Returns escaped SQL for comments, excluding any spam comments.
* Can be injected directly into a WHERE clause.
*
* @access public
* @static
*
* @return string SQL WHERE clause.
*/
public static function get_comments_filter_sql() {
return "comment_approved <> 'spam'";
}
/**
* Delete any settings options and clean up the current settings state.
*
* @access public
* @static
*/
public static function reset_data() {
$valid_settings = self::$valid_settings;
foreach ( $valid_settings as $option => $value ) {
delete_option( self::SETTINGS_OPTION_PREFIX . $option );
}
self::set_importing( null );
self::set_doing_cron( null );
self::set_is_syncing( null );
self::set_is_sending( null );
}
/**
* Set the importing state.
*
* @access public
* @static
*
* @param boolean $is_importing Whether WordPress is currently importing.
*/
public static function set_importing( $is_importing ) {
// Set to NULL to revert to WP_IMPORTING, the standard behavior.
self::$is_importing = $is_importing;
}
/**
* Whether WordPress is currently importing.
*
* @access public
* @static
*
* @return boolean Whether WordPress is currently importing.
*/
public static function is_importing() {
if ( self::$is_importing !== null ) {
return self::$is_importing;
}
return defined( 'WP_IMPORTING' ) && WP_IMPORTING;
}
/**
* Whether sync is enabled.
*
* @access public
* @static
*
* @return boolean Whether sync is enabled.
*/
public static function is_sync_enabled() {
return ! ( self::get_setting( 'disable' ) || self::get_setting( 'network_disable' ) );
}
/**
* Set the WP cron state.
*
* @access public
* @static
*
* @param boolean $is_doing_cron Whether WordPress is currently doing WP cron.
*/
public static function set_doing_cron( $is_doing_cron ) {
// Set to NULL to revert to WP_IMPORTING, the standard behavior.
self::$is_doing_cron = $is_doing_cron;
}
/**
* Whether WordPress is currently doing WP cron.
*
* @access public
* @static
*
* @return boolean Whether WordPress is currently doing WP cron.
*/
public static function is_doing_cron() {
if ( self::$is_doing_cron !== null ) {
return self::$is_doing_cron;
}
return defined( 'DOING_CRON' ) && DOING_CRON;
}
/**
* Whether we are currently syncing.
*
* @access public
* @static
*
* @return boolean Whether we are currently syncing.
*/
public static function is_syncing() {
return (bool) self::$is_syncing || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST );
}
/**
* Set the syncing state.
*
* @access public
* @static
*
* @param boolean $is_syncing Whether we are currently syncing.
*/
public static function set_is_syncing( $is_syncing ) {
self::$is_syncing = $is_syncing;
}
/**
* Whether we are currently sending sync items.
*
* @access public
* @static
*
* @return boolean Whether we are currently sending sync items.
*/
public static function is_sending() {
return (bool) self::$is_sending;
}
/**
* Set the sending state.
*
* @access public
* @static
*
* @param boolean $is_sending Whether we are currently sending sync items.
*/
public static function set_is_sending( $is_sending ) {
self::$is_sending = $is_sending;
}
/**
* Whether should send from the queue
*
* @access public
* @static
*
* @param string $queue_id The queue identifier.
*
* @return boolean Whether sync is enabled.
*/
public static function is_sender_enabled( $queue_id ) {
return (bool) self::get_setting( $queue_id . '_sender_enabled' );
}
/**
* Whether checksums are enabled.
*
* @access public
* @static
*
* @return boolean Whether sync is enabled.
*/
public static function is_checksum_enabled() {
return ! (bool) self::get_setting( 'checksum_disable' );
}
/**
* Whether dedicated Sync flow is enabled.
*
* @access public
* @static
*
* @return boolean Whether dedicated Sync flow is enabled.
*/
public static function is_dedicated_sync_enabled() {
return (bool) self::get_setting( 'dedicated_sync_enabled' );
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* Simple codec for encoding and decoding sync objects.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses base64
* algorithm to compress objects serialized using json_encode.
*/
class Simple_Codec extends JSON_Deflate_Array_Codec {
/**
* Name of the codec.
*
* @access public
*
* @var string
*/
const CODEC_NAME = 'simple';
/**
* Retrieve the name of the codec.
*
* @access public
*
* @return string Name of the codec.
*/
public function name() {
return self::CODEC_NAME;
}
/**
* Encode a sync object.
*
* @access public
*
* @param mixed $object Sync object to encode.
* @return string Encoded sync object.
*/
public function encode( $object ) {
// This is intentionally using base64_encode().
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
return base64_encode( $this->json_serialize( $object ) );
}
/**
* Encode a sync object.
*
* @access public
*
* @param string $input Encoded sync object to decode.
* @return mixed Decoded sync object.
*/
public function decode( $input ) {
// This is intentionally using base64_decode().
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
return $this->json_unserialize( base64_decode( $input ) );
}
}

View File

@ -0,0 +1,152 @@
<?php
/**
* Sync for users.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
use Automattic\Jetpack\Connection\XMLRPC_Async_Call;
use Automattic\Jetpack\Roles;
/**
* Class Users.
*
* Responsible for syncing user data changes.
*/
class Users {
/**
* Roles of all users, indexed by user ID.
*
* @access public
* @static
*
* @var array
*/
public static $user_roles = array();
/**
* Initialize sync for user data changes.
*
* @access public
* @static
* @todo Eventually, connection needs to be instantiated at the top level in the sync package.
*/
public static function init() {
add_action( 'jetpack_user_authorized', array( 'Automattic\\Jetpack\\Sync\\Actions', 'do_initial_sync' ), 10, 0 );
$connection = new Jetpack_Connection();
if ( $connection->has_connected_user() ) {
// Kick off synchronization of user role when it changes.
add_action( 'set_user_role', array( __CLASS__, 'user_role_change' ) );
}
}
/**
* Synchronize connected user role changes.
*
* @access public
* @static
*
* @param int $user_id ID of the user.
*/
public static function user_role_change( $user_id ) {
$connection = new Jetpack_Connection();
if ( $connection->is_user_connected( $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 );
}
}
/**
* Retrieve the role of a user by their ID.
*
* @access public
* @static
*
* @param int $user_id ID of the user.
* @return string Role of the user.
*/
public static function get_role( $user_id ) {
if ( isset( self::$user_roles[ $user_id ] ) ) {
return self::$user_roles[ $user_id ];
}
$current_user_id = get_current_user_id();
wp_set_current_user( $user_id );
$roles = new Roles();
$role = $roles->translate_current_user_to_role();
wp_set_current_user( $current_user_id );
self::$user_roles[ $user_id ] = $role;
return $role;
}
/**
* Retrieve the signed role of a user by their ID.
*
* @access public
* @static
*
* @param int $user_id ID of the user.
* @return string Signed role of the user.
*/
public static function get_signed_role( $user_id ) {
$connection = new Jetpack_Connection();
return $connection->sign_role( self::get_role( $user_id ), $user_id );
}
/**
* Retrieve the signed role and update it in WP.com for that user.
*
* @access public
* @static
*
* @param int $user_id ID of the user.
*/
public static function update_role_on_com( $user_id ) {
$signed_role = self::get_signed_role( $user_id );
XMLRPC_Async_Call::add_call( 'jetpack.updateRole', get_current_user_id(), $user_id, $signed_role );
}
/**
* Choose a new master user if we're demoting the current one.
*
* @access public
* @static
* @todo Disconnect if there is no user with enough capabilities to be the master user.
* @uses \WP_User_Query
*
* @param int $user_id ID of the user.
*/
public static function maybe_demote_master_user( $user_id ) {
$master_user_id = (int) \Jetpack_Options::get_option( 'master_user' );
$role = self::get_role( $user_id );
if ( $user_id === $master_user_id && 'administrator' !== $role ) {
$query = new \WP_User_Query(
array(
'fields' => array( 'ID' ),
'role' => 'administrator',
'orderby' => 'ID',
'exclude' => array( $master_user_id ),
)
);
$new_master = false;
$connection = new Jetpack_Connection();
foreach ( $query->results as $result ) {
$found_user_id = absint( $result->ID );
if ( $found_user_id && $connection->is_user_connected( $found_user_id ) ) {
$new_master = $found_user_id;
break;
}
}
if ( $new_master ) {
\Jetpack_Options::update_option( 'master_user', $new_master );
}
// TODO: else disconnect..?
}
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* Sync utils.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* Class for sync utilities.
*/
class Utils {
/**
* Retrieve the values of sync items.
*
* @access public
* @static
*
* @param array $items Array of sync items.
* @return array Array of sync item values.
*/
public static function get_item_values( $items ) {
return array_map( array( __CLASS__, 'get_item_value' ), $items );
}
/**
* Retrieve the IDs of sync items.
*
* @access public
* @static
*
* @param array $items Array of sync items.
* @return array Array of sync item IDs.
*/
public static function get_item_ids( $items ) {
return array_map( array( __CLASS__, 'get_item_id' ), $items );
}
/**
* Get the value of a sync item.
*
* @access private
* @static
*
* @param array $item Sync item.
* @return mixed Sync item value.
*/
private static function get_item_value( $item ) {
return $item->value;
}
/**
* Get the ID of a sync item.
*
* @access private
* @static
*
* @param array $item Sync item.
* @return int Sync item ID.
*/
private static function get_item_id( $item ) {
return $item->id;
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Interface for encoding and decoding sync objects.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* Very simple interface for encoding and decoding input.
* This is used to provide compression and serialization to messages.
**/
interface Codec_Interface {
/**
* Retrieve the name of the codec.
* We send this with the payload so we can select the appropriate decoder at the other end.
*
* @access public
*
* @return string Name of the codec.
*/
public function name();
/**
* Encode a sync object.
*
* @access public
*
* @param mixed $object Sync object to encode.
* @return string Encoded sync object.
*/
public function encode( $object );
/**
* Encode a sync object.
*
* @access public
*
* @param string $input Encoded sync object to decode.
* @return mixed Decoded sync object.
*/
public function decode( $input );
}

View File

@ -0,0 +1,566 @@
<?php
/**
* Sync architecture prototype.
*
* To run tests: phpunit --testsuite sync --filter New_Sync
*
* @author Dan Walmsley
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync;
/**
* A high-level interface for objects that store synced WordPress data.
* Useful for ensuring that different storage mechanisms implement the
* required semantics for storing all the data that we sync.
*/
interface Replicastore_Interface {
/**
* Empty and reset the replicastore.
*
* @access public
*/
public function reset();
/**
* Ran when full sync has just started.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
*/
public function full_sync_start( $config );
/**
* Ran when full sync has just finished.
*
* @access public
*
* @param string $checksum Deprecated since 7.3.0.
*/
public function full_sync_end( $checksum );
/**
* Retrieve the number of posts with a particular post status within a certain range.
*
* @access public
*
* @todo Prepare the SQL query before executing it.
*
* @param string $status Post status.
* @param int $min_id Minimum post ID.
* @param int $max_id Maximum post ID.
*/
public function post_count( $status = null, $min_id = null, $max_id = null );
/**
* Retrieve the posts with a particular post status.
*
* @access public
*
* @param string $status Post status.
* @param int $min_id Minimum post ID.
* @param int $max_id Maximum post ID.
*/
public function get_posts( $status = null, $min_id = null, $max_id = null );
/**
* Retrieve a post object by the post ID.
*
* @access public
*
* @param int $id Post ID.
*/
public function get_post( $id );
/**
* Update or insert a post.
*
* @access public
*
* @param \WP_Post $post Post object.
* @param bool $silent Whether to perform a silent action.
*/
public function upsert_post( $post, $silent = false );
/**
* Delete a post by the post ID.
*
* @access public
*
* @param int $post_id Post ID.
*/
public function delete_post( $post_id );
/**
* Retrieve the checksum for posts within a range.
*
* @access public
*
* @param int $min_id Minimum post ID.
* @param int $max_id Maximum post ID.
*/
public function posts_checksum( $min_id = null, $max_id = null );
/**
* Retrieve the checksum for post meta within a range.
*
* @access public
*
* @param int $min_id Minimum post meta ID.
* @param int $max_id Maximum post meta ID.
*/
public function post_meta_checksum( $min_id = null, $max_id = null );
/**
* Retrieve the number of comments with a particular comment status within a certain range.
*
* @access public
*
* @param string $status Comment status.
* @param int $min_id Minimum comment ID.
* @param int $max_id Maximum comment ID.
*/
public function comment_count( $status = null, $min_id = null, $max_id = null );
/**
* Retrieve the comments with a particular comment status.
*
* @access public
*
* @param string $status Comment status.
* @param int $min_id Minimum comment ID.
* @param int $max_id Maximum comment ID.
*/
public function get_comments( $status = null, $min_id = null, $max_id = null );
/**
* Retrieve a comment object by the comment ID.
*
* @access public
*
* @param int $id Comment ID.
*/
public function get_comment( $id );
/**
* Update or insert a comment.
*
* @access public
*
* @param \WP_Comment $comment Comment object.
*/
public function upsert_comment( $comment );
/**
* Trash a comment by the comment ID.
*
* @access public
*
* @param int $comment_id Comment ID.
*/
public function trash_comment( $comment_id );
/**
* Mark a comment by the comment ID as spam.
*
* @access public
*
* @param int $comment_id Comment ID.
*/
public function spam_comment( $comment_id );
/**
* Delete a comment by the comment ID.
*
* @access public
*
* @param int $comment_id Comment ID.
*/
public function delete_comment( $comment_id );
/**
* Trash the comments of a post.
*
* @access public
*
* @param int $post_id Post ID.
* @param array $statuses Post statuses.
*/
public function trashed_post_comments( $post_id, $statuses );
/**
* Untrash the comments of a post.
*
* @access public
*
* @param int $post_id Post ID.
*/
public function untrashed_post_comments( $post_id );
/**
* Retrieve the checksum for comments within a range.
*
* @access public
*
* @param int $min_id Minimum comment ID.
* @param int $max_id Maximum comment ID.
*/
public function comments_checksum( $min_id = null, $max_id = null );
/**
* Retrieve the checksum for comment meta within a range.
*
* @access public
*
* @param int $min_id Minimum comment meta ID.
* @param int $max_id Maximum comment meta ID.
*/
public function comment_meta_checksum( $min_id = null, $max_id = null );
/**
* Update the value of an option.
*
* @access public
*
* @param string $option Option name.
* @param mixed $value Option value.
*/
public function update_option( $option, $value );
/**
* Retrieve an option value based on an option name.
*
* @access public
*
* @param string $option Name of option to retrieve.
* @param mixed $default Optional. Default value to return if the option does not exist.
*/
public function get_option( $option, $default = false );
/**
* Remove an option by name.
*
* @access public
*
* @param string $option Name of option to remove.
*/
public function delete_option( $option );
/**
* Change the info of the current theme.
*
* @access public
*
* @param array $theme_info Theme info array.
*/
public function set_theme_info( $theme_info );
/**
* Whether the current theme supports a certain feature.
*
* @access public
*
* @param string $feature Name of the feature.
*/
public function current_theme_supports( $feature );
/**
* Retrieve metadata for the specified object.
*
* @access public
*
* @param string $type Meta type.
* @param int $object_id ID of the object.
* @param string $meta_key Meta key.
* @param bool $single If true, return only the first value of the specified meta_key.
*/
public function get_metadata( $type, $object_id, $meta_key = '', $single = false );
/**
* Stores remote meta key/values alongside an ID mapping key.
*
* @access public
*
* @param string $type Meta type.
* @param int $object_id ID of the object.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value.
* @param int $meta_id ID of the meta.
*/
public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id );
/**
* Delete metadata for the specified object.
*
* @access public
*
* @param string $type Meta type.
* @param int $object_id ID of the object.
* @param array $meta_ids IDs of the meta objects to delete.
*/
public function delete_metadata( $type, $object_id, $meta_ids );
/**
* Delete metadata with a certain key for the specified objects.
*
* @access public
*
* @param string $type Meta type.
* @param array $object_ids IDs of the objects.
* @param string $meta_key Meta key.
*/
public function delete_batch_metadata( $type, $object_ids, $meta_key );
/**
* Retrieve value of a constant based on the constant name.
*
* @access public
*
* @param string $constant Name of constant to retrieve.
*/
public function get_constant( $constant );
/**
* Set the value of a constant.
*
* @access public
*
* @param string $constant Name of constant to retrieve.
* @param mixed $value Value set for the constant.
*/
public function set_constant( $constant, $value );
/**
* Retrieve the number of the available updates of a certain type.
* Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`.
*
* @access public
*
* @param string $type Type of updates to retrieve.
*/
public function get_updates( $type );
/**
* Set the available updates of a certain type.
* Type is one of: `plugins`, `themes`, `wordpress`, `translations`, `total`, `wp_update_version`.
*
* @access public
*
* @param string $type Type of updates to set.
* @param int $updates Total number of updates.
*/
public function set_updates( $type, $updates );
/**
* Retrieve a callable value based on its name.
*
* @access public
*
* @param string $callable Name of the callable to retrieve.
*/
public function get_callable( $callable );
/**
* Update the value of a callable.
*
* @access public
*
* @param string $callable Callable name.
* @param mixed $value Callable value.
*/
public function set_callable( $callable, $value );
/**
* Retrieve a network option value based on a network option name.
*
* @access public
*
* @param string $option Name of network option to retrieve.
*/
public function get_site_option( $option );
/**
* Update the value of a network option.
*
* @access public
*
* @param string $option Network option name.
* @param mixed $value Network option value.
*/
public function update_site_option( $option, $value );
/**
* Remove a network option by name.
*
* @access public
*
* @param string $option Name of option to remove.
*/
public function delete_site_option( $option );
/**
* Retrieve the terms from a particular taxonomy.
*
* @access public
*
* @param string $taxonomy Taxonomy slug.
*/
public function get_terms( $taxonomy );
/**
* Retrieve a particular term.
*
* @access public
*
* @param string $taxonomy Taxonomy slug.
* @param int $term_id ID of the term.
* @param string $term_key ID Field `term_id` or `term_taxonomy_id`.
*/
public function get_term( $taxonomy, $term_id, $term_key = 'term_id' );
/**
* Insert or update a term.
*
* @access public
*
* @param \WP_Term $term_object Term object.
*/
public function update_term( $term_object );
/**
* Delete a term by the term ID and its corresponding taxonomy.
*
* @access public
*
* @param int $term_id Term ID.
* @param string $taxonomy Taxonomy slug.
*/
public function delete_term( $term_id, $taxonomy );
/**
* Retrieve all terms from a taxonomy that are related to an object with a particular ID.
*
* @access public
*
* @param int $object_id Object ID.
* @param string $taxonomy Taxonomy slug.
*/
public function get_the_terms( $object_id, $taxonomy );
/**
* Add/update terms of a particular taxonomy of an object with the specified ID.
*
* @access public
*
* @param int $object_id The object to relate to.
* @param string $taxonomy The context in which to relate the term to the object.
* @param string|int|array $terms A single term slug, single term id, or array of either term slugs or ids.
* @param bool $append Optional. If false will delete difference of terms. Default false.
*/
public function update_object_terms( $object_id, $taxonomy, $terms, $append );
/**
* Remove certain term relationships from the specified object.
*
* @access public
*
* @todo Refactor to not use interpolated values when preparing the SQL query.
*
* @param int $object_id ID of the object.
* @param array $tt_ids Term taxonomy IDs.
*/
public function delete_object_terms( $object_id, $tt_ids );
/**
* Retrieve the number of users.
*
* @access public
*/
public function user_count();
/**
* Retrieve a user object by the user ID.
*
* @access public
*
* @param int $user_id User ID.
*/
public function get_user( $user_id );
/**
* Insert or update a user.
*
* @access public
*
* @param \WP_User $user User object.
*/
public function upsert_user( $user );
/**
* Delete a user.
*
* @access public
*
* @param int $user_id User ID.
*/
public function delete_user( $user_id );
/**
* Update/insert user locale.
*
* @access public
*
* @param int $user_id User ID.
* @param string $locale The user locale.
*/
public function upsert_user_locale( $user_id, $locale );
/**
* Delete user locale.
*
* @access public
*
* @param int $user_id User ID.
*/
public function delete_user_locale( $user_id );
/**
* Retrieve the user locale.
*
* @access public
*
* @param int $user_id User ID.
*/
public function get_user_locale( $user_id );
/**
* Retrieve the allowed mime types for the user.
*
* @access public
*
* @param int $user_id User ID.
*/
public function get_allowed_mime_types( $user_id );
/**
* Retrieve all the checksums we are interested in.
* Currently that is posts, comments, post meta and comment meta.
*
* @access public
*/
public function checksum_all();
/**
* Retrieve the checksum histogram for a specific object type.
*
* @access public
*
* @param string $object_type Object type.
* @param int $buckets Number of buckets to split the objects to.
* @param int $start_id Minimum object ID.
* @param int $end_id Maximum object ID.
*/
public function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null );
}

View File

@ -0,0 +1,98 @@
<?php
/**
* Attachments sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
/**
* Class to handle sync for attachments.
*/
class Attachments extends Module {
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'attachments';
}
/**
* Initialize attachment action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
add_action( 'add_attachment', array( $this, 'process_add' ) );
add_action( 'attachment_updated', array( $this, 'process_update' ), 10, 3 );
add_action( 'jetpack_sync_save_update_attachment', $callable, 10, 2 );
add_action( 'jetpack_sync_save_add_attachment', $callable, 10, 2 );
add_action( 'jetpack_sync_save_attach_attachment', $callable, 10, 2 );
}
/**
* Handle the creation of a new attachment.
*
* @access public
*
* @param int $attachment_id ID of the attachment.
*/
public function process_add( $attachment_id ) {
$attachment = get_post( $attachment_id );
/**
* Fires when the client needs to sync an new attachment
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param int Attachment ID.
* @param \WP_Post Attachment post object.
*/
do_action( 'jetpack_sync_save_add_attachment', $attachment_id, $attachment );
}
/**
* Handle updating an existing attachment.
*
* @access public
*
* @param int $attachment_id Attachment ID.
* @param \WP_Post $attachment_after Attachment post object before the update.
* @param \WP_Post $attachment_before Attachment post object after the update.
*/
public function process_update( $attachment_id, $attachment_after, $attachment_before ) {
// Check whether attachment was added to a post for the first time.
if ( 0 === $attachment_before->post_parent && 0 !== $attachment_after->post_parent ) {
/**
* Fires when an existing attachment is added to a post for the first time
*
* @since 1.6.3
* @since-jetpack 6.6.0
*
* @param int $attachment_id Attachment ID.
* @param \WP_Post $attachment_after Attachment post object after the update.
*/
do_action( 'jetpack_sync_save_attach_attachment', $attachment_id, $attachment_after );
} else {
/**
* Fires when the client needs to sync an updated attachment
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param int $attachment_id Attachment ID.
* @param \WP_Post $attachment_after Attachment post object after the update.
*
* Previously this action was synced using jetpack_sync_save_add_attachment action.
*/
do_action( 'jetpack_sync_save_update_attachment', $attachment_id, $attachment_after );
}
}
}

View File

@ -0,0 +1,642 @@
<?php
/**
* Callables sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use Automattic\Jetpack\Sync\Dedicated_Sender;
use Automattic\Jetpack\Sync\Defaults;
use Automattic\Jetpack\Sync\Functions;
use Automattic\Jetpack\Sync\Settings;
/**
* Class to handle sync for callables.
*/
class Callables extends Module {
/**
* Name of the callables checksum option.
*
* @var string
*/
const CALLABLES_CHECKSUM_OPTION_NAME = 'jetpack_callables_sync_checksum';
/**
* Name of the transient for locking callables.
*
* @var string
*/
const CALLABLES_AWAIT_TRANSIENT_NAME = 'jetpack_sync_callables_await';
/**
* Whitelist for callables we want to sync.
*
* @access private
*
* @var array
*/
private $callable_whitelist;
/**
* For some options, we should always send the change right away!
*
* @access public
*
* @var array
*/
const ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS = array(
'jetpack_active_modules',
'home', // option is home, callable is home_url.
'siteurl',
'jetpack_sync_error_idc',
'paused_plugins',
'paused_themes',
);
const ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS_NEXT_TICK = array(
'stylesheet',
);
/**
* Setting this value to true will make it so that the callables will not be unlocked
* but the lock will be removed after content is send so that callables will be
* sent in the next request.
*
* @var bool
*/
private $force_send_callables_on_next_tick = false;
/**
* For some options, the callable key differs from the option name/key
*
* @access public
*
* @var array
*/
const OPTION_NAMES_TO_CALLABLE_NAMES = array(
// @TODO: Audit the other option names for differences between the option names and callable names.
'home' => 'home_url',
'siteurl' => 'site_url',
'jetpack_active_modules' => 'active_modules',
);
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'functions';
}
/**
* Set module defaults.
* Define the callable whitelist based on whether this is a single site or a multisite installation.
*
* @access public
*/
public function set_defaults() {
if ( is_multisite() ) {
$this->callable_whitelist = array_merge( Defaults::get_callable_whitelist(), Defaults::get_multisite_callable_whitelist() );
} else {
$this->callable_whitelist = Defaults::get_callable_whitelist();
}
$this->force_send_callables_on_next_tick = false; // Resets here as well mostly for tests.
}
/**
* Initialize callables action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
add_action( 'jetpack_sync_callable', $callable, 10, 2 );
add_action( 'current_screen', array( $this, 'set_plugin_action_links' ), 9999 ); // Should happen very late.
foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option ) {
add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable' ) );
add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable' ) );
}
foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS_NEXT_TICK as $option ) {
add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable_next_tick' ) );
add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable_next_tick' ) );
}
// Provide a hook so that hosts can send changes to certain callables right away.
// Especially useful when a host uses constants to change home and siteurl.
add_action( 'jetpack_sync_unlock_sync_callable', array( $this, 'unlock_sync_callable' ) );
// get_plugins and wp_version
// gets fired when new code gets installed, updates etc.
add_action( 'upgrader_process_complete', array( $this, 'unlock_plugin_action_link_and_callables' ) );
add_action( 'update_option_active_plugins', array( $this, 'unlock_plugin_action_link_and_callables' ) );
}
/**
* Initialize callables 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_callables', $callable );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_callables' ) );
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) );
}
/**
* Perform module cleanup.
* Deletes any transients and options that this module uses.
* Usually triggered when uninstalling the plugin.
*
* @access public
*/
public function reset_data() {
delete_option( self::CALLABLES_CHECKSUM_OPTION_NAME );
delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
$url_callables = array( 'home_url', 'site_url', 'main_network_site_url' );
foreach ( $url_callables as $callable ) {
delete_option( Functions::HTTPS_CHECK_OPTION_PREFIX . $callable );
}
}
/**
* Set the callable whitelist.
*
* @access public
*
* @param array $callables The new callables whitelist.
*/
public function set_callable_whitelist( $callables ) {
$this->callable_whitelist = $callables;
}
/**
* Get the callable whitelist.
*
* @access public
*
* @return array The callables whitelist.
*/
public function get_callable_whitelist() {
return $this->callable_whitelist;
}
/**
* Retrieve all callables as per the current callables whitelist.
*
* @access public
*
* @return array All callables.
*/
public function get_all_callables() {
// get_all_callables should run as the master user always.
$current_user_id = get_current_user_id();
wp_set_current_user( \Jetpack_Options::get_option( 'master_user' ) );
$callables = array_combine(
array_keys( $this->get_callable_whitelist() ),
array_map( array( $this, 'get_callable' ), array_values( $this->get_callable_whitelist() ) )
);
wp_set_current_user( $current_user_id );
return $callables;
}
/**
* Invoke a particular callable.
* Used as a wrapper to standartize invocation.
*
* @access private
*
* @param callable $callable Callable to invoke.
* @return mixed Return value of the callable, null if not callable.
*/
private function get_callable( $callable ) {
if ( is_callable( $callable ) ) {
return call_user_func( $callable );
} else {
return null;
}
}
/**
* Enqueue the callable 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* Tells the client to sync all callables to the server
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param boolean Whether to expand callables (should always be true)
*/
do_action( 'jetpack_full_sync_callables', true );
// The number of actions enqueued, and next module state (true == done).
return array( 1, true );
}
/**
* Send the callable actions for full sync.
*
* @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.
*
* @return array This Module Full Sync Status.
*/
public function send_full_sync_actions( $config, $send_until, $status ) { // 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 ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
}
/**
* 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_callables' );
}
/**
* Unlock callables so they would be available for syncing again.
*
* @access public
*/
public function unlock_sync_callable() {
delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
}
/**
* Unlock callables on the next tick.
* Sometime the true callable values are only present on the next tick.
* When switching themes for example.
*
* @access public
*/
public function unlock_sync_callable_next_tick() {
$this->force_send_callables_on_next_tick = true;
}
/**
* Unlock callables and plugin action links.
*
* @access public
*/
public function unlock_plugin_action_link_and_callables() {
delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
delete_transient( 'jetpack_plugin_api_action_links_refresh' );
add_filter( 'jetpack_check_and_send_callables', '__return_true' );
}
/**
* Parse and store the plugin action links if on the plugins page.
*
* @uses \DOMDocument
* @uses libxml_use_internal_errors
* @uses mb_convert_encoding
*
* @access public
*/
public function set_plugin_action_links() {
if (
! class_exists( '\DOMDocument' ) ||
! function_exists( 'libxml_use_internal_errors' ) ||
! function_exists( 'mb_convert_encoding' )
) {
return;
}
$current_screeen = get_current_screen();
$plugins_action_links = array();
// Is the transient lock in place?
$plugins_lock = get_transient( 'jetpack_plugin_api_action_links_refresh', false );
if ( ! empty( $plugins_lock ) && ( isset( $current_screeen->id ) && 'plugins' !== $current_screeen->id ) ) {
return;
}
$plugins = array_keys( Functions::get_plugins() );
foreach ( $plugins as $plugin_file ) {
/**
* Plugins often like to unset things but things break if they are not able to.
*/
$action_links = array(
'deactivate' => '',
'activate' => '',
'details' => '',
'delete' => '',
'edit' => '',
);
/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
$action_links = apply_filters( 'plugin_action_links', $action_links, $plugin_file, null, 'all' );
/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
$action_links = apply_filters( "plugin_action_links_{$plugin_file}", $action_links, $plugin_file, null, 'all' );
// Verify $action_links is still an array to resolve warnings from filters not returning an array.
if ( is_array( $action_links ) ) {
$action_links = array_filter( $action_links );
} else {
$action_links = array();
}
$formatted_action_links = null;
if ( ! empty( $action_links ) && count( $action_links ) > 0 ) {
$dom_doc = new \DOMDocument();
foreach ( $action_links as $action_link ) {
// The @ is not enough to suppress errors when dealing with libxml,
// we have to tell it directly how we want to handle errors.
libxml_use_internal_errors( true );
$dom_doc->loadHTML( mb_convert_encoding( $action_link, 'HTML-ENTITIES', 'UTF-8' ) );
libxml_use_internal_errors( false );
$link_elements = $dom_doc->getElementsByTagName( 'a' );
if ( 0 === $link_elements->length ) {
continue;
}
$link_element = $link_elements->item( 0 );
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( $link_element->hasAttribute( 'href' ) && $link_element->nodeValue ) {
$link_url = trim( $link_element->getAttribute( 'href' ) );
// Add the full admin path to the url if the plugin did not provide it.
$link_url_scheme = wp_parse_url( $link_url, PHP_URL_SCHEME );
if ( empty( $link_url_scheme ) ) {
$link_url = admin_url( $link_url );
}
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$formatted_action_links[ $link_element->nodeValue ] = $link_url;
}
}
}
if ( $formatted_action_links ) {
$plugins_action_links[ $plugin_file ] = $formatted_action_links;
}
}
// Cache things for a long time.
set_transient( 'jetpack_plugin_api_action_links_refresh', time(), DAY_IN_SECONDS );
update_option( 'jetpack_plugin_api_action_links', $plugins_action_links );
}
/**
* Whether a certain callable should be sent.
*
* @access public
*
* @param array $callable_checksums Callable checksums.
* @param string $name Name of the callable.
* @param string $checksum A checksum of the callable.
* @return boolean Whether to send the callable.
*/
public function should_send_callable( $callable_checksums, $name, $checksum ) {
$idc_override_callables = array(
'main_network_site',
'home_url',
'site_url',
);
if ( in_array( $name, $idc_override_callables, true ) && \Jetpack_Options::get_option( 'migrate_for_idc' ) ) {
return true;
}
return ! $this->still_valid_checksum( $callable_checksums, $name, $checksum );
}
/**
* Sync the callables if we're supposed to.
*
* @access public
*/
public function maybe_sync_callables() {
$callables = $this->get_all_callables();
if ( ! apply_filters( 'jetpack_check_and_send_callables', false ) ) {
/**
* Treating Dedicated Sync requests a bit differently from normal. We want to send callables
* normally with all Sync actions, no matter if they were with admin action origin or not,
* since Dedicated Sync runs out of bound and the requests are never coming from an admin.
*/
if ( ! is_admin() && ! Dedicated_Sender::is_dedicated_sync_request() ) {
// If we're not an admin and we're not doing cron and this isn't WP_CLI, don't sync anything.
if ( ! Settings::is_doing_cron() && ! Jetpack_Constants::get_constant( 'WP_CLI' ) ) {
return;
}
// If we're not an admin and we are doing cron, sync the Callables that are always supposed to sync ( See https://github.com/Automattic/jetpack/issues/12924 ).
$callables = $this->get_always_sent_callables();
}
if ( get_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ) ) {
if ( $this->force_send_callables_on_next_tick ) {
$this->unlock_sync_callable();
}
return;
}
}
if ( empty( $callables ) ) {
return;
}
// No need to set the transiant we are trying to remove it anyways.
if ( ! $this->force_send_callables_on_next_tick ) {
set_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME, microtime( true ), Defaults::$default_sync_callables_wait_time );
}
$callable_checksums = (array) \Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() );
$has_changed = false;
// Only send the callables that have changed.
foreach ( $callables as $name => $value ) {
$checksum = $this->get_check_sum( $value );
// Explicitly not using Identical comparison as get_option returns a string.
if ( $value !== null && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) {
// Only send callable if the non sorted checksum also does not match.
if ( $this->should_send_callable( $callable_checksums, $name, $this->get_check_sum( $value, false ) ) ) {
/**
* Tells the client to sync a callable (aka function) to the server
*
* @param string The name of the callable
* @param mixed The value of the callable
*
* @since 1.6.3
* @since-jetpack 4.2.0
*/
do_action( 'jetpack_sync_callable', $name, $value );
}
$callable_checksums[ $name ] = $checksum;
$has_changed = true;
} else {
$callable_checksums[ $name ] = $checksum;
}
}
if ( $has_changed ) {
\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums );
}
if ( $this->force_send_callables_on_next_tick ) {
$this->unlock_sync_callable();
}
}
/**
* Get the callables that should always be sent, e.g. on cron.
*
* @return array Callables that should always be sent
*/
protected function get_always_sent_callables() {
$callables = $this->get_all_callables();
$cron_callables = array();
foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option_name ) {
if ( array_key_exists( $option_name, $callables ) ) {
$cron_callables[ $option_name ] = $callables[ $option_name ];
continue;
}
// Check for the Callable name/key for the option, if different from option name.
if ( array_key_exists( $option_name, self::OPTION_NAMES_TO_CALLABLE_NAMES ) ) {
$callable_name = self::OPTION_NAMES_TO_CALLABLE_NAMES[ $option_name ];
if ( array_key_exists( $callable_name, $callables ) ) {
$cron_callables[ $callable_name ] = $callables[ $callable_name ];
}
}
}
return $cron_callables;
}
/**
* Expand the callables within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The hook parameters.
*/
public function expand_callables( $args ) {
if ( $args[0] ) {
$callables = $this->get_all_callables();
$callables_checksums = array();
foreach ( $callables as $name => $value ) {
$callables_checksums[ $name ] = $this->get_check_sum( $value );
}
\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums );
return $callables;
}
return $args;
}
/**
* Return Total number of objects.
*
* @param array $config Full Sync config.
*
* @return int total
*/
public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return count( $this->get_callable_whitelist() );
}
/**
* Retrieve a set of callables by their IDs.
*
* @access public
*
* @param string $object_type Object type.
* @param array $ids Object IDs.
* @return array Array of objects.
*/
public function get_objects_by_id( $object_type, $ids ) {
if ( empty( $ids ) || empty( $object_type ) || 'callable' !== $object_type ) {
return array();
}
$objects = array();
foreach ( (array) $ids as $id ) {
$object = $this->get_object_by_id( $object_type, $id );
if ( 'CALLABLE-DOES-NOT-EXIST' !== $object ) {
if ( 'all' === $id ) {
// If all was requested it contains all options and can simply be returned.
return $object;
}
$objects[ $id ] = $object;
}
}
return $objects;
}
/**
* Retrieve a callable by its name.
*
* @access public
*
* @param string $object_type Type of the sync object.
* @param string $id ID of the sync object.
* @return mixed Value of Callable.
*/
public function get_object_by_id( $object_type, $id ) {
if ( 'callable' === $object_type ) {
// Only whitelisted options can be returned.
if ( array_key_exists( $id, $this->get_callable_whitelist() ) ) {
// requires master user to be in context.
$current_user_id = get_current_user_id();
wp_set_current_user( \Jetpack_Options::get_option( 'master_user' ) );
$callable = $this->get_callable( $this->callable_whitelist[ $id ] );
wp_set_current_user( $current_user_id );
return $callable;
} elseif ( 'all' === $id ) {
return $this->get_all_callables();
}
}
return 'CALLABLE-DOES-NOT-EXIST';
}
}

View File

@ -0,0 +1,495 @@
<?php
/**
* Comments sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Settings;
/**
* Class to handle sync for comments.
*/
class Comments extends Module {
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'comments';
}
/**
* The id field in the database.
*
* @access public
*
* @return string
*/
public function id_field() {
return 'comment_ID';
}
/**
* The table in the database.
*
* @access public
*
* @return string
*/
public function table_name() {
return 'comments';
}
/**
* Retrieve a comment by its ID.
*
* @access public
*
* @param string $object_type Type of the sync object.
* @param int $id ID of the sync object.
* @return \WP_Comment|bool Filtered \WP_Comment object, or false if the object is not a comment.
*/
public function get_object_by_id( $object_type, $id ) {
$comment_id = (int) $id;
if ( 'comment' === $object_type ) {
$comment = get_comment( $comment_id );
if ( $comment ) {
return $this->filter_comment( $comment );
}
}
return false;
}
/**
* Initialize comments action listeners.
* Also responsible for initializing comment meta listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
add_action( 'wp_insert_comment', $callable, 10, 2 );
add_action( 'deleted_comment', $callable );
add_action( 'trashed_comment', $callable );
add_action( 'spammed_comment', $callable );
add_action( 'trashed_post_comments', $callable, 10, 2 );
add_action( 'untrash_post_comments', $callable );
add_action( 'comment_approved_to_unapproved', $callable );
add_action( 'comment_unapproved_to_approved', $callable );
add_action( 'jetpack_modified_comment_contents', $callable, 10, 2 );
add_action( 'untrashed_comment', $callable, 10, 2 );
add_action( 'unspammed_comment', $callable, 10, 2 );
add_filter( 'wp_update_comment_data', array( $this, 'handle_comment_contents_modification' ), 10, 3 );
// comment actions.
add_filter( 'jetpack_sync_before_enqueue_wp_insert_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
add_filter( 'jetpack_sync_before_enqueue_deleted_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
add_filter( 'jetpack_sync_before_enqueue_trashed_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
add_filter( 'jetpack_sync_before_enqueue_untrashed_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
add_filter( 'jetpack_sync_before_enqueue_spammed_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
add_filter( 'jetpack_sync_before_enqueue_unspammed_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
// comment status transitions.
add_filter( 'jetpack_sync_before_enqueue_comment_approved_to_unapproved', array( $this, 'only_allow_white_listed_comment_type_transitions' ) );
add_filter( 'jetpack_sync_before_enqueue_comment_unapproved_to_approved', array( $this, 'only_allow_white_listed_comment_type_transitions' ) );
// Post Actions.
add_filter( 'jetpack_sync_before_enqueue_trashed_post_comments', array( $this, 'filter_blacklisted_post_types' ) );
add_filter( 'jetpack_sync_before_enqueue_untrash_post_comments', array( $this, 'filter_blacklisted_post_types' ) );
/**
* Even though it's messy, we implement these hooks because
* the edit_comment hook doesn't include the data
* so this saves us a DB read for every comment event.
*/
foreach ( $this->get_whitelisted_comment_types() as $comment_type ) {
foreach ( array( 'unapproved', 'approved' ) as $comment_status ) {
$comment_action_name = "comment_{$comment_status}_{$comment_type}";
add_action( $comment_action_name, $callable, 10, 2 );
}
}
// Listen for meta changes.
$this->init_listeners_for_meta_type( 'comment', $callable );
$this->init_meta_whitelist_handler( 'comment', array( $this, 'filter_meta' ) );
}
/**
* Handler for any comment content updates.
*
* @access public
*
* @param array $new_comment The new, processed comment data.
* @param array $old_comment The old, unslashed comment data.
* @param array $new_comment_with_slashes The new, raw comment data.
* @return array The new, processed comment data.
*/
public function handle_comment_contents_modification( $new_comment, $old_comment, $new_comment_with_slashes ) {
$changes = array();
$content_fields = array(
'comment_author',
'comment_author_email',
'comment_author_url',
'comment_content',
);
foreach ( $content_fields as $field ) {
if ( $new_comment_with_slashes[ $field ] !== $old_comment[ $field ] ) {
$changes[ $field ] = array( $new_comment[ $field ], $old_comment[ $field ] );
}
}
if ( ! empty( $changes ) ) {
/**
* Signals to the sync listener that this comment's contents were modified and a sync action
* reflecting the change(s) to the content should be sent
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param int $new_comment['comment_ID'] ID of comment whose content was modified
* @param mixed $changes Array of changed comment fields with before and after values
*/
do_action( 'jetpack_modified_comment_contents', $new_comment['comment_ID'], $changes );
}
return $new_comment;
}
/**
* Initialize comments 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_comments', $callable ); // Also send comments meta.
}
/**
* Gets a filtered list of comment types that sync can hook into.
*
* @access public
*
* @return array Defaults to [ '', 'trackback', 'pingback' ].
*/
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' )
);
}
/**
* Prevents any comment types that are not in the whitelist from being enqueued and sent to WordPress.com.
*
* @param array $args Arguments passed to wp_insert_comment, deleted_comment, spammed_comment, etc.
*
* @return bool or array $args Arguments passed to wp_insert_comment, deleted_comment, spammed_comment, etc.
*/
public function only_allow_white_listed_comment_types( $args ) {
$comment = false;
if ( isset( $args[1] ) ) {
// comment object is available.
$comment = $args[1];
} elseif ( is_numeric( $args[0] ) ) {
// comment_id is available.
$comment = get_comment( $args[0] );
}
if (
isset( $comment->comment_type )
&& ! in_array( $comment->comment_type, $this->get_whitelisted_comment_types(), true )
) {
return false;
}
return $args;
}
/**
* Filter all blacklisted post types.
*
* @param array $args Hook arguments.
* @return array|false Hook arguments, or false if the post type is a blacklisted one.
*/
public function filter_blacklisted_post_types( $args ) {
$post_id = $args[0];
$posts_module = Modules::get_module( 'posts' );
if ( false !== $posts_module && ! $posts_module->is_post_type_allowed( $post_id ) ) {
return false;
}
return $args;
}
/**
* Prevents any comment types that are not in the whitelist from being enqueued and sent to WordPress.com.
*
* @param array $args Arguments passed to wp_{old_status}_to_{new_status}.
*
* @return bool or array $args Arguments passed to wp_{old_status}_to_{new_status}
*/
public function only_allow_white_listed_comment_type_transitions( $args ) {
$comment = $args[0];
if ( ! in_array( $comment->comment_type, $this->get_whitelisted_comment_types(), true ) ) {
return false;
}
return $args;
}
/**
* Whether a comment type is allowed.
* A comment type is allowed if it's present in the comment type whitelist.
*
* @param int $comment_id ID of the comment.
* @return boolean Whether the comment type is allowed.
*/
public function is_comment_type_allowed( $comment_id ) {
$comment = get_comment( $comment_id );
if ( isset( $comment->comment_type ) ) {
return in_array( $comment->comment_type, $this->get_whitelisted_comment_types(), true );
}
return false;
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_wp_insert_comment', array( $this, 'expand_wp_insert_comment' ) );
foreach ( $this->get_whitelisted_comment_types() as $comment_type ) {
foreach ( array( 'unapproved', 'approved' ) as $comment_status ) {
$comment_action_name = "comment_{$comment_status}_{$comment_type}";
add_filter(
'jetpack_sync_before_send_' . $comment_action_name,
array(
$this,
'expand_wp_insert_comment',
)
);
}
}
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_comments', array( $this, 'expand_comment_ids' ) );
}
/**
* Enqueue the comments 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 ) {
global $wpdb;
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_comments', $wpdb->comments, 'comment_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 $wpdb->comments";
$where_sql = $this->get_where_sql( $config );
if ( $where_sql ) {
$query .= ' WHERE ' . $where_sql;
}
// TODO: Call $wpdb->prepare on the following query.
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
/**
* Retrieve the WHERE SQL clause based on the module config.
*
* @access public
*
* @param array $config Full sync configuration for this sync 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 ) ) {
return 'comment_ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
return '1=1';
}
/**
* 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_comments' );
}
/**
* Count all the actions that are going to be sent.
*
* @access public
*
* @param array $action_names Names of all the actions that will be sent.
* @return int Number of actions.
*/
public function count_full_sync_actions( $action_names ) {
return $this->count_actions( $action_names, array( 'jetpack_full_sync_comments' ) );
}
/**
* Expand the comment status change before the data is serialized and sent to the server.
*
* @access public
* @todo This is not used currently - let's implement it.
*
* @param array $args The hook parameters.
* @return array The expanded hook parameters.
*/
public function expand_wp_comment_status_change( $args ) {
return array( $args[0], $this->filter_comment( $args[1] ) );
}
/**
* Expand the comment creation before the data is serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array The expanded hook parameters.
*/
public function expand_wp_insert_comment( $args ) {
return array( $args[0], $this->filter_comment( $args[1] ) );
}
/**
* Filter a comment object to the fields we need.
*
* @access public
*
* @param \WP_Comment $comment The unfiltered comment object.
* @return \WP_Comment Filtered comment object.
*/
public function filter_comment( $comment ) {
/**
* Filters whether to prevent sending comment data to .com
*
* Passing true to the filter will prevent the comment data from being sent
* to the WordPress.com.
* Instead we pass data that will still enable us to do a checksum against the
* Jetpacks data but will prevent us from displaying the data on in the API as well as
* other services.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param boolean false prevent post data from bing synced to WordPress.com
* @param mixed $comment WP_COMMENT object
*/
if ( apply_filters( 'jetpack_sync_prevent_sending_comment_data', false, $comment ) ) {
$blocked_comment = new \stdClass();
$blocked_comment->comment_ID = $comment->comment_ID;
$blocked_comment->comment_date = $comment->comment_date;
$blocked_comment->comment_date_gmt = $comment->comment_date_gmt;
$blocked_comment->comment_approved = 'jetpack_sync_blocked';
return $blocked_comment;
}
return $comment;
}
/**
* Whether a certain comment meta key is whitelisted for sync.
*
* @access public
*
* @param string $meta_key Comment meta key.
* @return boolean Whether the meta key is whitelisted.
*/
public function is_whitelisted_comment_meta( $meta_key ) {
return in_array( $meta_key, Settings::get_setting( 'comment_meta_whitelist' ), true );
}
/**
* Handler for filtering out non-whitelisted comment meta.
*
* @access public
*
* @param array $args Hook args.
* @return array|boolean False if not whitelisted, the original hook args otherwise.
*/
public function filter_meta( $args ) {
if ( $this->is_comment_type_allowed( $args[1] ) && $this->is_whitelisted_comment_meta( $args[2] ) ) {
return $args;
}
return false;
}
/**
* Expand the comment IDs to comment objects and meta before being serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array The expanded hook parameters.
*/
public function expand_comment_ids( $args ) {
list( $comment_ids, $previous_interval_end ) = $args;
$comments = get_comments(
array(
'include_unapproved' => true,
'comment__in' => $comment_ids,
'orderby' => 'comment_ID',
'order' => 'DESC',
)
);
return array(
$comments,
$this->get_metadata( $comment_ids, 'comment', Settings::get_setting( 'comment_meta_whitelist' ) ),
$previous_interval_end,
);
}
}

View File

@ -0,0 +1,339 @@
<?php
/**
* Constants sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Defaults;
/**
* Class to handle sync for constants.
*/
class Constants extends Module {
/**
* Name of the constants checksum option.
*
* @var string
*/
const CONSTANTS_CHECKSUM_OPTION_NAME = 'jetpack_constants_sync_checksum';
/**
* Name of the transient for locking constants.
*
* @var string
*/
const CONSTANTS_AWAIT_TRANSIENT_NAME = 'jetpack_sync_constants_await';
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'constants';
}
/**
* Initialize constants action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
add_action( 'jetpack_sync_constant', $callable, 10, 2 );
}
/**
* Initialize constants 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_constants', $callable );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_constants' ) );
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_constants', array( $this, 'expand_constants' ) );
}
/**
* Perform module cleanup.
* Deletes any transients and options that this module uses.
* Usually triggered when uninstalling the plugin.
*
* @access public
*/
public function reset_data() {
delete_option( self::CONSTANTS_CHECKSUM_OPTION_NAME );
delete_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME );
}
/**
* Set the constants whitelist.
*
* @access public
* @todo We don't seem to use this one. Should we remove it?
*
* @param array $constants The new constants whitelist.
*/
public function set_constants_whitelist( $constants ) {
$this->constants_whitelist = $constants;
}
/**
* Get the constants whitelist.
*
* @access public
*
* @return array The constants whitelist.
*/
public function get_constants_whitelist() {
return Defaults::get_constants_whitelist();
}
/**
* Enqueue the constants 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* Tells the client to sync all constants to the server
*
* @param boolean Whether to expand constants (should always be true)
*
* @since 1.6.3
* @since-jetpack 4.2.0
*/
do_action( 'jetpack_full_sync_constants', true );
// The number of actions enqueued, and next module state (true == done).
return array( 1, true );
}
/**
* Send the constants actions for full sync.
*
* @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 $state This module Full Sync status.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // 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 ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
*
* @return array Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
}
/**
* 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_constants' );
}
/**
* Sync the constants if we're supposed to.
*
* @access public
*/
public function maybe_sync_constants() {
if ( get_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME ) ) {
return;
}
set_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME, microtime( true ), Defaults::$default_sync_constants_wait_time );
$constants = $this->get_all_constants();
if ( empty( $constants ) ) {
return;
}
$constants_checksums = (array) get_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, array() );
foreach ( $constants as $name => $value ) {
$checksum = $this->get_check_sum( $value );
// Explicitly not using Identical comparison as get_option returns a string.
if ( ! $this->still_valid_checksum( $constants_checksums, $name, $checksum ) && $value !== null ) {
/**
* Tells the client to sync a constant to the server
*
* @param string The name of the constant
* @param mixed The value of the constant
*
* @since 1.6.3
* @since-jetpack 4.2.0
*/
do_action( 'jetpack_sync_constant', $name, $value );
$constants_checksums[ $name ] = $checksum;
} else {
$constants_checksums[ $name ] = $checksum;
}
}
update_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, $constants_checksums );
}
/**
* Retrieve all constants as per the current constants whitelist.
* Public so that we don't have to store an option for each constant.
*
* @access public
*
* @return array All constants.
*/
public function get_all_constants() {
$constants_whitelist = $this->get_constants_whitelist();
return array_combine(
$constants_whitelist,
array_map( array( $this, 'get_constant' ), $constants_whitelist )
);
}
/**
* Retrieve the value of a constant.
* Used as a wrapper to standartize access to constants.
*
* @access private
*
* @param string $constant Constant name.
*
* @return mixed Return value of the constant.
*/
private function get_constant( $constant ) {
return ( defined( $constant ) ) ?
constant( $constant )
: null;
}
/**
* Expand the constants within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
*
* @return array $args The hook parameters.
*/
public function expand_constants( $args ) {
if ( $args[0] ) {
$constants = $this->get_all_constants();
$constants_checksums = array();
foreach ( $constants as $name => $value ) {
$constants_checksums[ $name ] = $this->get_check_sum( $value );
}
update_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, $constants_checksums );
return $constants;
}
return $args;
}
/**
* Return Total number of objects.
*
* @param array $config Full Sync config.
*
* @return int total
*/
public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return count( $this->get_constants_whitelist() );
}
/**
* Retrieve a set of constants by their IDs.
*
* @access public
*
* @param string $object_type Object type.
* @param array $ids Object IDs.
* @return array Array of objects.
*/
public function get_objects_by_id( $object_type, $ids ) {
if ( empty( $ids ) || empty( $object_type ) || 'constant' !== $object_type ) {
return array();
}
$objects = array();
foreach ( (array) $ids as $id ) {
$object = $this->get_object_by_id( $object_type, $id );
if ( 'all' === $id ) {
// If all was requested it contains all options and can simply be returned.
return $object;
}
$objects[ $id ] = $object;
}
return $objects;
}
/**
* Retrieve a constant by its name.
*
* @access public
*
* @param string $object_type Type of the sync object.
* @param string $id ID of the sync object.
* @return mixed Value of Constant.
*/
public function get_object_by_id( $object_type, $id ) {
if ( 'constant' === $object_type ) {
// Only whitelisted constants can be returned.
if ( in_array( $id, $this->get_constants_whitelist(), true ) ) {
return $this->get_constant( $id );
} elseif ( 'all' === $id ) {
return $this->get_all_constants();
}
}
return false;
}
}

View File

@ -0,0 +1,470 @@
<?php
/**
* Full sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Actions;
use Automattic\Jetpack\Sync\Defaults;
use Automattic\Jetpack\Sync\Lock;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Settings;
/**
* This class does a full resync of the database by
* sending an outbound action for every single object
* that we care about.
*/
class Full_Sync_Immediately extends Module {
/**
* Prefix of the full sync status option name.
*
* @var string
*/
const STATUS_OPTION = 'jetpack_sync_full_status';
/**
* Sync Lock name.
*
* @var string
*/
const LOCK_NAME = 'full_sync';
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'full-sync';
}
/**
* Initialize action listeners for full sync.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_full_sync_listeners( $callable ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
}
/**
* Start a full sync.
*
* @access public
*
* @param array $full_sync_config Full sync configuration.
*
* @return bool Always returns true at success.
*/
public function start( $full_sync_config = 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' );
}
// Remove all evidence of previous full sync items and status.
$this->reset_data();
if ( ! is_array( $full_sync_config ) ) {
$full_sync_config = Defaults::$default_full_sync_config;
if ( is_multisite() ) {
$full_sync_config['network_options'] = 1;
}
}
if ( isset( $full_sync_config['users'] ) && 'initial' === $full_sync_config['users'] ) {
$full_sync_config['users'] = Modules::get_module( 'users' )->get_initial_sync_user_config();
}
$this->update_status(
array(
'started' => time(),
'config' => $full_sync_config,
'progress' => $this->get_initial_progress( $full_sync_config ),
)
);
$range = $this->get_content_range( $full_sync_config );
/**
* 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 array $empty The modules with no items to sync during a full sync.
*
* @since 1.6.3
* @since-jetpack 4.2.0
* @since-jetpack 7.3.0 Added $range arg.
* @since-jetpack 7.4.0 Added $empty arg.
*/
do_action( 'jetpack_full_sync_start', $full_sync_config, $range );
$this->send_action( 'jetpack_full_sync_start', array( $full_sync_config, $range ) );
return true;
}
/**
* Whether full sync has started.
*
* @access public
*
* @return boolean
*/
public function is_started() {
return (bool) $this->get_status()['started'];
}
/**
* Retrieve the status of the current full sync.
*
* @access public
*
* @return array Full sync status.
*/
public function get_status() {
$default = array(
'started' => false,
'finished' => false,
'progress' => array(),
'config' => array(),
);
return wp_parse_args( \Jetpack_Options::get_raw_option( self::STATUS_OPTION ), $default );
}
/**
* Returns the progress percentage of a full sync.
*
* @access public
*
* @return int|null
*/
public function get_sync_progress_percentage() {
if ( ! $this->is_started() || $this->is_finished() ) {
return null;
}
$status = $this->get_status();
if ( empty( $status['progress'] ) ) {
return null;
}
$total_items = array_reduce(
array_values( $status['progress'] ),
function ( $sum, $sync_item ) {
return isset( $sync_item['total'] ) ? ( $sum + (int) $sync_item['total'] ) : $sum;
},
0
);
$total_sent = array_reduce(
array_values( $status['progress'] ),
function ( $sum, $sync_item ) {
return isset( $sync_item['sent'] ) ? ( $sum + (int) $sync_item['sent'] ) : $sum;
},
0
);
return floor( ( $total_sent / $total_items ) * 100 );
}
/**
* Whether full sync has finished.
*
* @access public
*
* @return boolean
*/
public function is_finished() {
return (bool) $this->get_status()['finished'];
}
/**
* Clear all the full sync data.
*
* @access public
*/
public function reset_data() {
$this->clear_status();
( new Lock() )->remove( self::LOCK_NAME, true );
}
/**
* Clear all the full sync status options.
*
* @access public
*/
public function clear_status() {
\Jetpack_Options::delete_raw_option( self::STATUS_OPTION );
}
/**
* Updates the status of the current full sync.
*
* @access public
*
* @param array $values New values to set.
*
* @return bool True if success.
*/
public function update_status( $values ) {
return $this->set_status( wp_parse_args( $values, $this->get_status() ) );
}
/**
* Retrieve the status of the current full sync.
*
* @param array $values New values to set.
*
* @access public
*
* @return boolean Full sync status.
*/
public function set_status( $values ) {
return \Jetpack_Options::update_raw_option( self::STATUS_OPTION, $values );
}
/**
* Given an initial Full Sync configuration get the initial status.
*
* @param array $full_sync_config Full sync configuration.
*
* @return array Initial Sent status.
*/
public function get_initial_progress( $full_sync_config ) {
// Set default configuration, calculate totals, and save configuration if totals > 0.
$status = array();
foreach ( $full_sync_config as $name => $config ) {
$module = Modules::get_module( $name );
if ( ! $module ) {
continue;
}
$status[ $name ] = array(
'total' => $module->total( $config ),
'sent' => 0,
'finished' => false,
);
}
return $status;
}
/**
* Get the range for content (posts and comments) to sync.
*
* @access private
*
* @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' );
}
return $range;
}
/**
* Get the range (min ID, max ID and total items) of items to sync.
*
* @access public
*
* @param string $type Type of sync item to get the range for.
*
* @return array Array of min ID, max ID and total items in the range.
*/
public function get_range( $type ) {
global $wpdb;
if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
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;
}
// TODO: Call $wpdb->prepare on the following query.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" );
if ( isset( $results[0] ) ) {
return $results[0];
}
return array();
}
/**
* Continue sending instead of enqueueing.
*
* @access public
*/
public function continue_enqueuing() {
$this->continue_sending();
}
/**
* Continue sending.
*
* @access public
*/
public function continue_sending() {
// Return early if Full Sync is not running.
if ( ! $this->is_started() || $this->get_status()['finished'] ) {
return;
}
// Return early if we've gotten a retry-after header response.
$retry_time = get_option( Actions::RETRY_AFTER_PREFIX . 'immediate-send' );
if ( $retry_time ) {
// If expired delete but don't send. Send will occurr in new request to avoid race conditions.
if ( microtime( true ) > $retry_time ) {
update_option( Actions::RETRY_AFTER_PREFIX . 'immediate-send', false, false );
}
return false;
}
// Obtain send Lock.
$lock = new Lock();
$lock_expiration = $lock->attempt( self::LOCK_NAME );
// Return if unable to obtain lock.
if ( false === $lock_expiration ) {
return;
}
// Send Full Sync actions.
$success = $this->send();
// Remove lock.
if ( $success ) {
$lock->remove( self::LOCK_NAME, $lock_expiration );
}
}
/**
* Immediately send the next items to full sync.
*
* @access public
*/
public function send() {
$config = $this->get_status()['config'];
$max_duration = Settings::get_setting( 'full_sync_send_duration' );
$send_until = microtime( true ) + $max_duration;
$progress = $this->get_status()['progress'];
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;
}
}
$this->send_full_sync_end();
$this->update_status( array( 'progress' => $progress ) );
return true;
}
/**
* Get Modules that are configured to Full Sync and haven't finished sending
*
* @return array
*/
public function get_remaining_modules_to_send() {
$status = $this->get_status();
return array_filter(
Modules::get_modules(),
/**
* Select configured and not finished modules.
*
* @return bool
* @var $module Module
*/
function ( $module ) use ( $status ) {
// Skip module if not configured for this sync or module is done.
if ( ! isset( $status['config'][ $module->name() ] ) ) {
return false;
}
if ( ! $status['config'][ $module->name() ] ) {
return false;
}
if ( isset( $status['progress'][ $module->name() ]['finished'] ) ) {
if ( true === $status['progress'][ $module->name() ]['finished'] ) {
return false;
}
}
return true;
}
);
}
/**
* Send 'jetpack_full_sync_end' and update 'finished' status.
*
* @access public
*/
public function send_full_sync_end() {
$range = $this->get_content_range();
/**
* Fires when a full sync ends. This action is serialized
* and sent to the server.
*
* @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
* @param array $range Range of the sync items, containing min and max IDs for some item types.
*
* @since 1.6.3
* @since-jetpack 4.2.0
* @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() ) );
}
/**
* Empty Function as we don't close buffers on Immediate Full Sync.
*
* @param array $actions an array of actions, ignored for queueless sync.
*/
public function update_sent_progress_action( $actions ) { } // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
}

View File

@ -0,0 +1,730 @@
<?php
/**
* Full sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Listener;
use Automattic\Jetpack\Sync\Lock;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Queue;
use Automattic\Jetpack\Sync\Settings;
/**
* This class does a full resync of the database by
* enqueuing an outbound action for every single object
* that we care about.
*
* This class, and its related class Jetpack_Sync_Module, contain a few non-obvious optimisations that should be explained:
* - we fire an action called jetpack_full_sync_start so that WPCOM can erase the contents of the cached database
* - for each object type, we page through the object IDs and enqueue them by firing some monitored actions
* - we load the full objects for those IDs in chunks of Jetpack_Sync_Module::ARRAY_CHUNK_SIZE (to reduce the number of MySQL calls)
* - we fire a trigger for the entire array which the Automattic\Jetpack\Sync\Listener then serializes and queues.
*/
class Full_Sync extends Module {
/**
* Prefix of the full sync status option name.
*
* @var string
*/
const STATUS_OPTION_PREFIX = 'jetpack_sync_full_';
/**
* Enqueue Lock name.
*
* @var string
*/
const ENQUEUE_LOCK_NAME = 'full_sync_enqueue';
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'full-sync';
}
/**
* Initialize action listeners for full sync.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_full_sync_listeners( $callable ) {
// Synthetic actions for full sync.
add_action( 'jetpack_full_sync_start', $callable, 10, 3 );
add_action( 'jetpack_full_sync_end', $callable, 10, 2 );
add_action( 'jetpack_full_sync_cancelled', $callable );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
// This is triggered after actions have been processed on the server.
add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) );
}
/**
* Start a full sync.
*
* @access public
*
* @param array $module_configs Full sync configuration for all sync modules.
* @return bool Always returns true at success.
*/
public function start( $module_configs = null ) {
$was_already_running = $this->is_started() && ! $this->is_finished();
// Remove all evidence of previous full sync items and status.
$this->reset_data();
if ( $was_already_running ) {
/**
* 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_option( 'started', time() );
$this->update_status_option( 'params', $module_configs );
$enqueue_status = array();
$full_sync_config = array();
$include_empty = false;
$empty = array();
// Default value is full sync.
if ( ! is_array( $module_configs ) ) {
$module_configs = array();
$include_empty = true;
foreach ( Modules::get_modules() as $module ) {
$module_configs[ $module->name() ] = true;
}
}
// Set default configuration, calculate totals, and save configuration if totals > 0.
foreach ( Modules::get_modules() as $module ) {
$module_name = $module->name();
$module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false;
if ( ! $module_config ) {
continue;
}
if ( 'users' === $module_name && 'initial' === $module_config ) {
$module_config = $module->get_initial_sync_user_config();
}
$enqueue_status[ $module_name ] = false;
$total_items = $module->estimate_full_sync_actions( $module_config );
// If there's information to process, configure this module.
if ( $total_items !== null && $total_items > 0 ) {
$full_sync_config[ $module_name ] = $module_config;
$enqueue_status[ $module_name ] = array(
$total_items, // Total.
0, // Queued.
false, // Current state.
);
} elseif ( $include_empty && 0 === $total_items ) {
$empty[ $module_name ] = true;
}
}
$this->set_config( $full_sync_config );
$this->set_enqueue_status( $enqueue_status );
$range = $this->get_content_range( $full_sync_config );
/**
* Fires when a full sync begins. This action is serialized
* and sent to the server so that it knows a full sync is coming.
*
* @since 1.6.3
* @since-jetpack 4.2.0
* @since-jetpack 7.3.0 Added $range arg.
* @since-jetpack 7.4.0 Added $empty arg.
*
* @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 array $empty The modules with no items to sync during a full sync.
*/
do_action( 'jetpack_full_sync_start', $full_sync_config, $range, $empty );
$this->continue_enqueuing( $full_sync_config );
return true;
}
/**
* Enqueue the next items to sync.
*
* @access public
*
* @param array $configs Full sync configuration for all sync modules.
*/
public function continue_enqueuing( $configs = null ) {
// Return early if not in progress.
if ( ! $this->get_status_option( 'started' ) || $this->get_status_option( 'queue_finished' ) ) {
return;
}
// Attempt to obtain lock.
$lock = new Lock();
$lock_expiration = $lock->attempt( self::ENQUEUE_LOCK_NAME );
// Return if unable to obtain lock.
if ( false === $lock_expiration ) {
return;
}
// enqueue full sync actions.
$this->enqueue( $configs );
// Remove lock.
$lock->remove( self::ENQUEUE_LOCK_NAME, $lock_expiration );
}
/**
* Get Modules that are configured to Full Sync and haven't finished enqueuing
*
* @param array $configs Full sync configuration for all sync modules.
*
* @return array
*/
public function get_remaining_modules_to_enqueue( $configs ) {
$enqueue_status = $this->get_enqueue_status();
return array_filter(
Modules::get_modules(),
/**
* Select configured and not finished modules.
*
* @var $module Module
* @return bool
*/
function ( $module ) use ( $configs, $enqueue_status ) {
// Skip module if not configured for this sync or module is done.
if ( ! isset( $configs[ $module->name() ] ) ) {
return false;
}
if ( ! $configs[ $module->name() ] ) {
return false;
}
if ( isset( $enqueue_status[ $module->name() ][2] ) ) {
if ( true === $enqueue_status[ $module->name() ][2] ) {
return false;
}
}
return true;
}
);
}
/**
* Enqueue the next items to sync.
*
* @access public
*
* @param array $configs Full sync configuration for all sync modules.
*/
public function enqueue( $configs = null ) {
if ( ! $configs ) {
$configs = $this->get_config();
}
$enqueue_status = $this->get_enqueue_status();
$full_sync_queue = new Queue( 'full_sync' );
$available_queue_slots = Settings::get_setting( 'max_queue_size_full_sync' ) - $full_sync_queue->size();
if ( $available_queue_slots <= 0 ) {
return;
}
$remaining_items_to_enqueue = min( Settings::get_setting( 'max_enqueue_full_sync' ), $available_queue_slots );
/**
* If a module exits early (e.g. because it ran out of full sync queue slots, or we ran out of request time)
* then it should exit early
*/
foreach ( $this->get_remaining_modules_to_enqueue( $configs ) as $module ) {
list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module->name() ], $remaining_items_to_enqueue, $enqueue_status[ $module->name() ][2] );
$enqueue_status[ $module->name() ][2] = $next_enqueue_state;
// If items were processed, subtract them from the limit.
if ( $items_enqueued !== null && $items_enqueued > 0 ) {
$enqueue_status[ $module->name() ][1] += $items_enqueued;
$remaining_items_to_enqueue -= $items_enqueued;
}
if ( 0 >= $remaining_items_to_enqueue || true !== $next_enqueue_state ) {
$this->set_enqueue_status( $enqueue_status );
return;
}
}
$this->queue_full_sync_end( $configs );
$this->set_enqueue_status( $enqueue_status );
}
/**
* Enqueue 'jetpack_full_sync_end' and update 'queue_finished' status.
*
* @access public
*
* @param array $configs Full sync configuration for all sync modules.
*/
public function queue_full_sync_end( $configs ) {
$range = $this->get_content_range( $configs );
/**
* Fires when a full sync ends. This action is serialized
* and sent to the server.
*
* @since 1.6.3
* @since-jetpack 4.2.0
* @since-jetpack 7.3.0 Added $range arg.
*
* @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
* @param array $range Range of the sync items, containing min and max IDs for some item types.
*/
do_action( 'jetpack_full_sync_end', '', $range );
// Setting autoload to true means that it's faster to check whether we should continue enqueuing.
$this->update_status_option( 'queue_finished', time(), true );
}
/**
* Get the range (min ID, max ID and total items) of items to sync.
*
* @access public
*
* @param string $type Type of sync item to get the range for.
* @return array Array of min ID, max ID and total items in the range.
*/
public function get_range( $type ) {
global $wpdb;
if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
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;
}
// TODO: Call $wpdb->prepare on the following query.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" );
if ( isset( $results[0] ) ) {
return $results[0];
}
return array();
}
/**
* Get the range for content (posts and comments) to sync.
*
* @access private
*
* @param array $config Full sync configuration for this all sync modules.
* @return array Array of range (min ID, max ID, total items) for all content types.
*/
private function get_content_range( $config ) {
$range = array();
// Only when we are sending the whole range do we want to send also the range.
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' );
}
return $range;
}
/**
* Update the progress after sync modules actions have been processed on the server.
*
* @access public
*
* @param array $actions Actions that have been processed on the server.
*/
public function update_sent_progress_action( $actions ) {
// Quick way to map to first items with an array of arrays.
$actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) );
// Total item counts for each action.
$actions_with_total_counts = $this->get_actions_totals( $actions );
if ( ! $this->is_started() || $this->is_finished() ) {
return;
}
if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
$this->update_status_option( 'send_started', time() );
}
foreach ( Modules::get_modules() as $module ) {
$module_actions = $module->get_full_sync_actions();
$status_option_name = "{$module->name()}_sent";
$total_option_name = "{$status_option_name}_total";
$items_sent = $this->get_status_option( $status_option_name, 0 );
$items_sent_total = $this->get_status_option( $total_option_name, 0 );
foreach ( $module_actions as $module_action ) {
if ( isset( $actions_with_counts[ $module_action ] ) ) {
$items_sent += $actions_with_counts[ $module_action ];
}
if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) {
$items_sent_total += $actions_with_total_counts[ $module_action ];
}
}
if ( $items_sent > 0 ) {
$this->update_status_option( $status_option_name, $items_sent );
}
if ( 0 !== $items_sent_total ) {
$this->update_status_option( $total_option_name, $items_sent_total );
}
}
if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) {
$this->update_status_option( 'finished', time() );
}
}
/**
* Returns the progress percentage of a full sync.
*
* @access public
*
* @return int|null
*/
public function get_sync_progress_percentage() {
if ( ! $this->is_started() || $this->is_finished() ) {
return null;
}
$status = $this->get_status();
if ( ! $status['queue'] || ! $status['sent'] || ! $status['total'] ) {
return null;
}
$queued_multiplier = 0.1;
$sent_multiplier = 0.9;
$count_queued = array_reduce(
$status['queue'],
function ( $sum, $value ) {
return $sum + $value;
},
0
);
$count_sent = array_reduce(
$status['sent'],
function ( $sum, $value ) {
return $sum + $value;
},
0
);
$count_total = array_reduce(
$status['total'],
function ( $sum, $value ) {
return $sum + $value;
},
0
);
$percent_queued = ( $count_queued / $count_total ) * $queued_multiplier * 100;
$percent_sent = ( $count_sent / $count_total ) * $sent_multiplier * 100;
return ceil( $percent_queued + $percent_sent );
}
/**
* Get the name of the action for an item in the sync queue.
*
* @access public
*
* @param array $queue_item Item of the sync queue.
* @return string|boolean Name of the action, false if queue item is invalid.
*/
public function get_action_name( $queue_item ) {
if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) {
return $queue_item[0];
}
return false;
}
/**
* Retrieve the total number of items we're syncing in a particular queue item (action).
* `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]`
* represents the first (and only) chunk of items to sync in that action.
*
* @access public
*
* @param array $queue_item Item of the sync queue that corresponds to a particular action.
* @return int Total number of items in the action.
*/
public function get_action_totals( $queue_item ) {
if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) {
if ( is_array( $queue_item[1][0] ) ) {
// Let's count the items we sync in this action.
return count( $queue_item[1][0] );
}
// -1 indicates that this action syncs all items by design.
return -1;
}
return 0;
}
/**
* Retrieve the total number of items for a set of actions, grouped by action name.
*
* @access public
*
* @param array $actions An array of actions.
* @return array An array, representing the total number of items, grouped per action.
*/
public function get_actions_totals( $actions ) {
$totals = array();
foreach ( $actions as $action ) {
$name = $this->get_action_name( $action );
$action_totals = $this->get_action_totals( $action );
if ( ! isset( $totals[ $name ] ) ) {
$totals[ $name ] = 0;
}
$totals[ $name ] += $action_totals;
}
return $totals;
}
/**
* Whether full sync has started.
*
* @access public
*
* @return boolean
*/
public function is_started() {
return (bool) $this->get_status_option( 'started' );
}
/**
* Whether full sync has finished.
*
* @access public
*
* @return boolean
*/
public function is_finished() {
return (bool) $this->get_status_option( 'finished' );
}
/**
* Retrieve the status of the current full sync.
*
* @access public
*
* @return array Full sync status.
*/
public function get_status() {
$status = array(
'started' => $this->get_status_option( 'started' ),
'queue_finished' => $this->get_status_option( 'queue_finished' ),
'send_started' => $this->get_status_option( 'send_started' ),
'finished' => $this->get_status_option( 'finished' ),
'sent' => array(),
'sent_total' => array(),
'queue' => array(),
'config' => $this->get_status_option( 'params' ),
'total' => array(),
);
$enqueue_status = $this->get_enqueue_status();
foreach ( Modules::get_modules() as $module ) {
$name = $module->name();
if ( ! isset( $enqueue_status[ $name ] ) ) {
continue;
}
list( $total, $queued ) = $enqueue_status[ $name ];
if ( $total ) {
$status['total'][ $name ] = $total;
}
if ( $queued ) {
$status['queue'][ $name ] = $queued;
}
$sent = $this->get_status_option( "{$name}_sent" );
if ( $sent ) {
$status['sent'][ $name ] = $sent;
}
$sent_total = $this->get_status_option( "{$name}_sent_total" );
if ( $sent_total ) {
$status['sent_total'][ $name ] = $sent_total;
}
}
return $status;
}
/**
* Clear all the full sync status options.
*
* @access public
*/
public function clear_status() {
$prefix = self::STATUS_OPTION_PREFIX;
\Jetpack_Options::delete_raw_option( "{$prefix}_started" );
\Jetpack_Options::delete_raw_option( "{$prefix}_params" );
\Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" );
\Jetpack_Options::delete_raw_option( "{$prefix}_send_started" );
\Jetpack_Options::delete_raw_option( "{$prefix}_finished" );
$this->delete_enqueue_status();
foreach ( Modules::get_modules() as $module ) {
\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" );
\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" );
}
}
/**
* Clear all the full sync data.
*
* @access public
*/
public function reset_data() {
$this->clear_status();
$this->delete_config();
( new Lock() )->remove( self::ENQUEUE_LOCK_NAME, true );
$listener = Listener::get_instance();
$listener->get_full_sync_queue()->reset();
}
/**
* Get the value of a full sync status option.
*
* @access private
*
* @param string $name Name of the option.
* @param mixed $default Default value of the option.
* @return mixed Option value.
*/
private function get_status_option( $name, $default = null ) {
$value = \Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default );
return is_numeric( $value ) ? (int) $value : $value;
}
/**
* Update the value of a full sync status option.
*
* @access private
*
* @param string $name Name of the option.
* @param mixed $value Value of the option.
* @param boolean $autoload Whether the option should be autoloaded at the beginning of the request.
*/
private function update_status_option( $name, $value, $autoload = false ) {
\Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload );
}
/**
* Set the full sync enqueue status.
*
* @access private
*
* @param array $new_status The new full sync enqueue status.
*/
private function set_enqueue_status( $new_status ) {
\Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status );
}
/**
* Delete full sync enqueue status.
*
* @access private
*
* @return boolean Whether the status was deleted.
*/
private function delete_enqueue_status() {
return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' );
}
/**
* Retrieve the current full sync enqueue status.
*
* @access private
*
* @return array Full sync enqueue status.
*/
public function get_enqueue_status() {
return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' );
}
/**
* Set the full sync enqueue configuration.
*
* @access private
*
* @param array $config The new full sync enqueue configuration.
*/
private function set_config( $config ) {
\Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config );
}
/**
* Delete full sync configuration.
*
* @access private
*
* @return boolean Whether the configuration was deleted.
*/
private function delete_config() {
return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' );
}
/**
* Retrieve the current full sync enqueue config.
*
* @access private
*
* @return array Full sync enqueue config.
*/
private function get_config() {
return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' );
}
}

View File

@ -0,0 +1,220 @@
<?php
/**
* Import sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Settings;
/**
* Class to handle sync for imports.
*/
class Import extends Module {
/**
* Tracks which actions have already been synced for the import
* to prevent the same event from being triggered a second time.
*
* @var array
*/
private $synced_actions = array();
/**
* A mapping of action types to sync action name.
* Keys are the name of the import action.
* Values are the resulting sync action.
*
* Note: import_done and import_end both intentionally map to
* jetpack_sync_import_end, as they both track the same type of action,
* the successful completion of an import. Different import plugins use
* differently named actions, and this is an attempt to consolidate.
*
* @var array
*/
private static $import_sync_action_map = array(
'import_start' => 'jetpack_sync_import_start',
'import_done' => 'jetpack_sync_import_end',
'import_end' => 'jetpack_sync_import_end',
);
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'import';
}
/**
* Initialize imports action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
add_action( 'export_wp', $callable );
add_action( 'jetpack_sync_import_start', $callable, 10, 2 );
add_action( 'jetpack_sync_import_end', $callable, 10, 2 );
// WordPress.
add_action( 'import_start', array( $this, 'sync_import_action' ) );
// Movable type, RSS, Livejournal.
add_action( 'import_done', array( $this, 'sync_import_action' ) );
// WordPress, Blogger, Livejournal, woo tax rate.
add_action( 'import_end', array( $this, 'sync_import_action' ) );
}
/**
* Set module defaults.
* Define an empty list of synced actions for us to fill later.
*
* @access public
*/
public function set_defaults() {
$this->synced_actions = array();
}
/**
* Generic handler for import actions.
*
* @access public
*
* @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'.
*/
public function sync_import_action( $importer ) {
$import_action = current_filter();
// Map action to event name.
$sync_action = self::$import_sync_action_map[ $import_action ];
// Only sync each action once per import.
if ( array_key_exists( $sync_action, $this->synced_actions ) && $this->synced_actions[ $sync_action ] ) {
return;
}
// Mark this action as synced.
$this->synced_actions[ $sync_action ] = true;
// Prefer self-reported $importer value.
if ( ! $importer ) {
// Fall back to inferring by calling class name.
$importer = self::get_calling_importer_class();
}
// Get $importer from known_importers.
$known_importers = Settings::get_setting( 'known_importers' );
if ( is_string( $importer ) && isset( $known_importers[ $importer ] ) ) {
$importer = $known_importers[ $importer ];
}
$importer_name = $this->get_importer_name( $importer );
switch ( $sync_action ) {
case 'jetpack_sync_import_start':
/**
* Used for syncing the start of an import
*
* @since 1.6.3
* @since-jetpack 7.3.0
*
* @module sync
*
* @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'.
* @param string $importer_name The name reported by the importer, or 'Unknown Importer'.
*/
do_action( 'jetpack_sync_import_start', $importer, $importer_name );
break;
case 'jetpack_sync_import_end':
/**
* Used for syncing the end of an import
*
* @since 1.6.3
* @since-jetpack 7.3.0
*
* @module sync
*
* @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'.
* @param string $importer_name The name reported by the importer, or 'Unknown Importer'.
*/
do_action( 'jetpack_sync_import_end', $importer, $importer_name );
break;
}
}
/**
* Retrieve the name of the importer.
*
* @access private
*
* @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'.
* @return string Name of the importer, or "Unknown Importer" if importer is unknown.
*/
private function get_importer_name( $importer ) {
$importers = get_importers();
return isset( $importers[ $importer ] ) ? $importers[ $importer ][0] : 'Unknown Importer';
}
/**
* Determine the class that extends `WP_Importer` which is responsible for
* the current action. Designed to be used within an action handler.
*
* @access private
* @static
*
* @return string The name of the calling class, or 'unknown'.
*/
private static function get_calling_importer_class() {
// If WP_Importer doesn't exist, neither will any importer that extends it.
if ( ! class_exists( 'WP_Importer', false ) ) {
return 'unknown';
}
$action = current_filter();
$backtrace = debug_backtrace( false ); //phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.debug_backtrace_optionsFound,WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$do_action_pos = -1;
$backtrace_len = count( $backtrace );
for ( $i = 0; $i < $backtrace_len; $i++ ) {
// Find the location in the stack of the calling action.
if ( 'do_action' === $backtrace[ $i ]['function'] && $action === $backtrace[ $i ]['args'][0] ) {
$do_action_pos = $i;
break;
}
}
// If the action wasn't called, the calling class is unknown.
if ( -1 === $do_action_pos ) {
return 'unknown';
}
// Continue iterating the stack looking for a caller that extends WP_Importer.
for ( $i = $do_action_pos + 1; $i < $backtrace_len; $i++ ) {
// If there is no class on the trace, continue.
if ( ! isset( $backtrace[ $i ]['class'] ) ) {
continue;
}
$class_name = $backtrace[ $i ]['class'];
// Check if the class extends WP_Importer.
if ( class_exists( $class_name, false ) ) {
$parents = class_parents( $class_name, false );
if ( $parents && in_array( 'WP_Importer', $parents, true ) ) {
return $class_name;
}
}
}
// If we've exhausted the stack without a match, the calling class is unknown.
return 'unknown';
}
}

View File

@ -0,0 +1,146 @@
<?php
/**
* Menus sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
/**
* Class to handle sync for menus.
*/
class Menus extends Module {
/**
* Navigation menu items that were added but not synced yet.
*
* @access private
*
* @var array
*/
private $nav_items_just_added = array();
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'menus';
}
/**
* Initialize menus action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
add_action( 'wp_create_nav_menu', $callable, 10, 2 );
add_action( 'wp_update_nav_menu', array( $this, 'update_nav_menu' ), 10, 2 );
add_action( 'wp_add_nav_menu_item', array( $this, 'update_nav_menu_add_item' ), 10, 3 );
add_action( 'wp_update_nav_menu_item', array( $this, 'update_nav_menu_update_item' ), 10, 3 );
add_action( 'post_updated', array( $this, 'remove_just_added_menu_item' ), 10, 2 );
add_action( 'jetpack_sync_updated_nav_menu', $callable, 10, 2 );
add_action( 'jetpack_sync_updated_nav_menu_add_item', $callable, 10, 4 );
add_action( 'jetpack_sync_updated_nav_menu_update_item', $callable, 10, 4 );
add_action( 'delete_nav_menu', $callable, 10, 3 );
}
/**
* Nav menu update handler.
*
* @access public
*
* @param int $menu_id ID of the menu.
* @param array $menu_data An array of menu data.
*/
public function update_nav_menu( $menu_id, $menu_data = array() ) {
if ( empty( $menu_data ) ) {
return;
}
/**
* Helps sync log that a nav menu was updated.
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param int $menu_id ID of the menu.
* @param array $menu_data An array of menu data.
*/
do_action( 'jetpack_sync_updated_nav_menu', $menu_id, $menu_data );
}
/**
* Nav menu item addition handler.
*
* @access public
*
* @param int $menu_id ID of the menu.
* @param int $nav_item_id ID of the new menu item.
* @param array $nav_item_args Arguments used to add the menu item.
*/
public function update_nav_menu_add_item( $menu_id, $nav_item_id, $nav_item_args ) {
$menu_data = wp_get_nav_menu_object( $menu_id );
$this->nav_items_just_added[] = $nav_item_id;
/**
* Helps sync log that a new menu item was added.
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param int $menu_id ID of the menu.
* @param array $menu_data An array of menu data.
* @param int $nav_item_id ID of the new menu item.
* @param array $nav_item_args Arguments used to add the menu item.
*/
do_action( 'jetpack_sync_updated_nav_menu_add_item', $menu_id, $menu_data, $nav_item_id, $nav_item_args );
}
/**
* Nav menu item update handler.
*
* @access public
*
* @param int $menu_id ID of the menu.
* @param int $nav_item_id ID of the new menu item.
* @param array $nav_item_args Arguments used to update the menu item.
*/
public function update_nav_menu_update_item( $menu_id, $nav_item_id, $nav_item_args ) {
if ( in_array( $nav_item_id, $this->nav_items_just_added, true ) ) {
return;
}
$menu_data = wp_get_nav_menu_object( $menu_id );
/**
* Helps sync log that an update to the menu item happened.
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param int $menu_id ID of the menu.
* @param array $menu_data An array of menu data.
* @param int $nav_item_id ID of the new menu item.
* @param array $nav_item_args Arguments used to update the menu item.
*/
do_action( 'jetpack_sync_updated_nav_menu_update_item', $menu_id, $menu_data, $nav_item_id, $nav_item_args );
}
/**
* Remove menu items that have already been saved from the "just added" list.
*
* @access public
*
* @param int $nav_item_id ID of the new menu item.
* @param \WP_Post $post_after Nav menu item post object after the update.
*/
public function remove_just_added_menu_item( $nav_item_id, $post_after ) {
if ( 'nav_menu_item' !== $post_after->post_type ) {
return;
}
$this->nav_items_just_added = array_diff( $this->nav_items_just_added, array( $nav_item_id ) );
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* Meta sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
/**
* Class to handle sync for meta.
*/
class Meta extends Module {
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'meta';
}
/**
* This implementation of get_objects_by_id() is a bit hacky since we're not passing in an array of meta IDs,
* but instead an array of post or comment IDs for which to retrieve meta for. On top of that,
* we also pass in an associative array where we expect there to be 'meta_key' and 'ids' keys present.
*
* This seemed to be required since if we have missing meta on WP.com and need to fetch it, we don't know what
* the meta key is, but we do know that we have missing meta for a given post or comment.
*
* @todo Refactor the $wpdb->prepare call to use placeholders.
*
* @param string $object_type The type of object for which we retrieve meta. Either 'post' or 'comment'.
* @param array $config Must include 'meta_key' and 'ids' keys.
*
* @return array
*/
public function get_objects_by_id( $object_type, $config ) {
$table = _get_meta_table( $object_type );
if ( ! $table ) {
return array();
}
if ( ! is_array( $config ) ) {
return array();
}
$meta_objects = array();
foreach ( $config as $item ) {
$meta = null;
if ( isset( $item['id'] ) && isset( $item['meta_key'] ) ) {
$meta = $this->get_object_by_id( $object_type, (int) $item['id'], (string) $item['meta_key'] );
}
$meta_objects[ $item['id'] . '-' . $item['meta_key'] ] = $meta;
}
return $meta_objects;
}
/**
* Get a single Meta Result.
*
* @param string $object_type post, comment, term, user.
* @param null $id Object ID.
* @param null $meta_key Meta Key.
*
* @return mixed|null
*/
public function get_object_by_id( $object_type, $id = null, $meta_key = null ) {
global $wpdb;
if ( ! is_int( $id ) || ! is_string( $meta_key ) ) {
return null;
}
$table = _get_meta_table( $object_type );
$object_id_column = $object_type . '_id';
// Sanitize so that the array only has integer values.
$meta = $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT * FROM {$table} WHERE {$object_id_column} = %d AND meta_key = %s",
$id,
$meta_key
),
ARRAY_A
);
$meta_objects = null;
if ( ! is_wp_error( $meta ) && ! empty( $meta ) ) {
foreach ( $meta as $meta_entry ) {
if ( 'post' === $object_type && strlen( $meta_entry['meta_value'] ) >= Posts::MAX_POST_META_LENGTH ) {
$meta_entry['meta_value'] = '';
}
$meta_objects[] = array(
'meta_type' => $object_type,
'meta_id' => $meta_entry['meta_id'],
'meta_key' => $meta_key,
'meta_value' => $meta_entry['meta_value'],
'object_id' => $meta_entry[ $object_id_column ],
);
}
}
return $meta_objects;
}
}

View File

@ -0,0 +1,604 @@
<?php
/**
* A base abstraction of a sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Functions;
use Automattic\Jetpack\Sync\Listener;
use Automattic\Jetpack\Sync\Replicastore;
use Automattic\Jetpack\Sync\Sender;
use Automattic\Jetpack\Sync\Settings;
/**
* Basic methods implemented by Jetpack Sync extensions.
*
* @abstract
*/
abstract class Module {
/**
* Number of items per chunk when grouping objects for performance reasons.
*
* @access public
*
* @var int
*/
const ARRAY_CHUNK_SIZE = 10;
/**
* Sync module name.
*
* @access public
*
* @return string
*/
abstract public function name();
/**
* The id field in the database.
*
* @access public
*
* @return string
*/
public function id_field() {
return 'ID';
}
/**
* The table in the database.
*
* @access public
*
* @return string|bool
*/
public function table_name() {
return false;
}
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* Retrieve a sync object by its ID.
*
* @access public
*
* @param string $object_type Type of the sync object.
* @param int $id ID of the sync object.
* @return mixed Object, or false if the object is invalid.
*/
public function get_object_by_id( $object_type, $id ) {
return false;
}
/**
* Initialize callables action listeners.
* Override these to set up listeners and set/reset data/defaults.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
}
/**
* Initialize module action listeners for full sync.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_full_sync_listeners( $callable ) {
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
}
/**
* Set module defaults.
*
* @access public
*/
public function set_defaults() {
}
/**
* Perform module cleanup.
* Usually triggered when uninstalling the plugin.
*
* @access public
*/
public function reset_data() {
}
/**
* Enqueue the module 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 ) {
// In subclasses, return the number of actions enqueued, and next module state (true == done).
return array( null, true );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array 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.
return null;
}
// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* 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();
}
/**
* Get the number of actions that we care about.
*
* @access protected
*
* @param array $action_names Action names we're interested in.
* @param array $actions_to_count Unfiltered list of actions we want to count.
* @return array Number of actions that we're interested in.
*/
protected function count_actions( $action_names, $actions_to_count ) {
return count( array_intersect( $action_names, $actions_to_count ) );
}
/**
* Calculate the checksum of one or more values.
*
* @access protected
*
* @param mixed $values Values to calculate checksum for.
* @param bool $sort If $values should have ksort called on it.
* @return int The checksum.
*/
protected function get_check_sum( $values, $sort = true ) {
// Associative array order changes the generated checksum value.
if ( $sort && is_array( $values ) ) {
$this->recursive_ksort( $values );
}
return crc32( wp_json_encode( Functions::json_wrap( $values ) ) );
}
/**
* Recursively call ksort on an Array
*
* @param array $values Array.
*/
private function recursive_ksort( &$values ) {
ksort( $values );
foreach ( $values as &$value ) {
if ( is_array( $value ) ) {
$this->recursive_ksort( $value );
}
}
}
/**
* Whether a particular checksum in a set of checksums is valid.
*
* @access protected
*
* @param array $sums_to_check Array of checksums.
* @param string $name Name of the checksum.
* @param int $new_sum Checksum to compare against.
* @return boolean Whether the checksum is valid.
*/
protected function still_valid_checksum( $sums_to_check, $name, $new_sum ) {
if ( isset( $sums_to_check[ $name ] ) && $sums_to_check[ $name ] === $new_sum ) {
return true;
}
return false;
}
/**
* Enqueue all items of a sync type as an action.
*
* @access protected
*
* @param string $action_name Name of the action.
* @param string $table_name Name of the database table.
* @param string $id_field Name of the ID field in the database.
* @param string $where_sql The SQL WHERE clause to filter to the desired items.
* @param int $max_items_to_enqueue Maximum number of items to enqueue in the same time.
* @param boolean $state Whether enqueueing has finished.
* @return array Array, containing the number of chunks and TRUE, indicating enqueueing has finished.
*/
protected function enqueue_all_ids_as_action( $action_name, $table_name, $id_field, $where_sql, $max_items_to_enqueue, $state ) {
global $wpdb;
if ( ! $where_sql ) {
$where_sql = '1 = 1';
}
$items_per_page = 1000;
$page = 1;
$chunk_count = 0;
$previous_interval_end = $state ? $state : '~0';
$listener = Listener::get_instance();
// Count down from max_id to min_id so we get newest posts/comments/etc first.
// phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
while ( $ids = $wpdb->get_col( "SELECT {$id_field} FROM {$table_name} WHERE {$where_sql} AND {$id_field} < {$previous_interval_end} ORDER BY {$id_field} DESC LIMIT {$items_per_page}" ) ) {
// Request posts in groups of N for efficiency.
$chunked_ids = array_chunk( $ids, self::ARRAY_CHUNK_SIZE );
// If we hit our row limit, process and return.
if ( $chunk_count + count( $chunked_ids ) >= $max_items_to_enqueue ) {
$remaining_items_count = $max_items_to_enqueue - $chunk_count;
$remaining_items = array_slice( $chunked_ids, 0, $remaining_items_count );
$remaining_items_with_previous_interval_end = $this->get_chunks_with_preceding_end( $remaining_items, $previous_interval_end );
$listener->bulk_enqueue_full_sync_actions( $action_name, $remaining_items_with_previous_interval_end );
$last_chunk = end( $remaining_items );
return array( $remaining_items_count + $chunk_count, end( $last_chunk ) );
}
$chunked_ids_with_previous_end = $this->get_chunks_with_preceding_end( $chunked_ids, $previous_interval_end );
$listener->bulk_enqueue_full_sync_actions( $action_name, $chunked_ids_with_previous_end );
$chunk_count += count( $chunked_ids );
$page++;
// The $ids are ordered in descending order.
$previous_interval_end = end( $ids );
}
if ( $wpdb->last_error ) {
// return the values that were passed in so all these chunks get retried.
return array( $max_items_to_enqueue, $state );
}
return array( $chunk_count, true );
}
/**
* Given the Module Full Sync 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|object|null
*/
public function get_next_chunk( $config, $status, $chunk_size ) {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
global $wpdb;
return $wpdb->get_col(
<<<SQL
SELECT {$this->id_field()}
FROM {$wpdb->{$this->table_name()}}
WHERE {$this->get_where_sql( $config )}
AND {$this->id_field()} < {$status['last_sent']}
ORDER BY {$this->id_field()}
DESC LIMIT {$chunk_size}
SQL
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Return the initial last sent object.
*
* @return string|array initial status.
*/
public function get_initial_last_sent() {
return '~0';
}
/**
* Immediately send all items of a sync type as an action.
*
* @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.
*
* @return array Status, the module full sync status updated.
*/
public function send_full_sync_actions( $config, $status, $send_until ) {
global $wpdb;
if ( empty( $status['last_sent'] ) ) {
$status['last_sent'] = $this->get_initial_last_sent();
}
$limits = Settings::get_setting( 'full_sync_limits' )[ $this->name() ];
$chunks_sent = 0;
// phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while ( $objects = $this->get_next_chunk( $config, $status, $limits['chunk_size'] ) ) {
if ( $chunks_sent++ === $limits['max_chunks'] || microtime( true ) >= $send_until ) {
return $status;
}
$result = $this->send_action( 'jetpack_full_sync_' . $this->name(), array( $objects, $status['last_sent'] ) );
if ( is_wp_error( $result ) || $wpdb->last_error ) {
$status['error'] = true;
return $status;
}
// The $ids are ordered in descending order.
$status['last_sent'] = end( $objects );
$status['sent'] += count( $objects );
}
if ( ! $wpdb->last_error ) {
$status['finished'] = true;
}
return $status;
}
/**
* Immediately sends a single item without firing or enqueuing it
*
* @param string $action_name The action.
* @param array $data The data associated with the action.
*/
public function send_action( $action_name, $data = null ) {
$sender = Sender::get_instance();
return $sender->send_action( $action_name, $data );
}
/**
* Retrieve chunk IDs with previous interval end.
*
* @access protected
*
* @param array $chunks All remaining items.
* @param int $previous_interval_end The last item from the previous interval.
* @return array Chunk IDs with the previous interval end.
*/
protected function get_chunks_with_preceding_end( $chunks, $previous_interval_end ) {
$chunks_with_ends = array();
foreach ( $chunks as $chunk ) {
$chunks_with_ends[] = array(
'ids' => $chunk,
'previous_end' => $previous_interval_end,
);
// Chunks are ordered in descending order.
$previous_interval_end = end( $chunk );
}
return $chunks_with_ends;
}
/**
* Get metadata of a particular object type within the designated meta key whitelist.
*
* @access protected
*
* @todo Refactor to use $wpdb->prepare() on the SQL query.
*
* @param array $ids Object IDs.
* @param string $meta_type Meta type.
* @param array $meta_key_whitelist Meta key whitelist.
* @return array Unserialized meta values.
*/
protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) {
global $wpdb;
$table = _get_meta_table( $meta_type );
$id = $meta_type . '_id';
if ( ! $table ) {
return array();
}
$private_meta_whitelist_sql = "'" . implode( "','", array_map( 'esc_sql', $meta_key_whitelist ) ) . "'";
return array_map(
array( $this, 'unserialize_meta' ),
$wpdb->get_results(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
"SELECT $id, meta_key, meta_value, meta_id FROM $table WHERE $id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )' .
" AND meta_key IN ( $private_meta_whitelist_sql ) ",
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
OBJECT
)
);
}
/**
* Initialize listeners for the particular meta type.
*
* @access public
*
* @param string $meta_type Meta type.
* @param callable $callable Action handler callable.
*/
public function init_listeners_for_meta_type( $meta_type, $callable ) {
add_action( "added_{$meta_type}_meta", $callable, 10, 4 );
add_action( "updated_{$meta_type}_meta", $callable, 10, 4 );
add_action( "deleted_{$meta_type}_meta", $callable, 10, 4 );
}
/**
* Initialize meta whitelist handler for the particular meta type.
*
* @access public
*
* @param string $meta_type Meta type.
* @param callable $whitelist_handler Action handler callable.
*/
public function init_meta_whitelist_handler( $meta_type, $whitelist_handler ) {
add_filter( "jetpack_sync_before_enqueue_added_{$meta_type}_meta", $whitelist_handler );
add_filter( "jetpack_sync_before_enqueue_updated_{$meta_type}_meta", $whitelist_handler );
add_filter( "jetpack_sync_before_enqueue_deleted_{$meta_type}_meta", $whitelist_handler );
}
/**
* Retrieve the term relationships for the specified object IDs.
*
* @access protected
*
* @todo This feels too specific to be in the abstract sync Module class. Move it?
*
* @param array $ids Object IDs.
* @return array Term relationships - object ID and term taxonomy ID pairs.
*/
protected function get_term_relationships( $ids ) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )', OBJECT );
}
/**
* Unserialize the value of a meta object, if necessary.
*
* @access public
*
* @param object $meta Meta object.
* @return object Meta object with possibly unserialized value.
*/
public function unserialize_meta( $meta ) {
$meta->meta_value = maybe_unserialize( $meta->meta_value );
return $meta;
}
/**
* Retrieve a set of objects by their IDs.
*
* @access public
*
* @param string $object_type Object type.
* @param array $ids Object IDs.
* @return array Array of objects.
*/
public function get_objects_by_id( $object_type, $ids ) {
if ( empty( $ids ) || empty( $object_type ) ) {
return array();
}
$objects = array();
foreach ( (array) $ids as $id ) {
$object = $this->get_object_by_id( $object_type, $id );
// Only add object if we have the object.
if ( $object ) {
$objects[ $id ] = $object;
}
}
return $objects;
}
/**
* Gets a list of minimum and maximum object ids for each batch based on the given batch size.
*
* @access public
*
* @param int $batch_size The batch size for objects.
* @param string|bool $where_sql The sql where clause minus 'WHERE', or false if no where clause is needed.
*
* @return array|bool An array of min and max ids for each batch. FALSE if no table can be found.
*/
public function get_min_max_object_ids_for_batches( $batch_size, $where_sql = false ) {
global $wpdb;
if ( ! $this->table_name() ) {
return false;
}
$results = array();
$table = $wpdb->{$this->table_name()};
$current_max = 0;
$current_min = 1;
$id_field = $this->id_field();
$replicastore = new Replicastore();
$total = $replicastore->get_min_max_object_id(
$id_field,
$table,
$where_sql,
false
);
while ( $total->max > $current_max ) {
$where = $where_sql ?
$where_sql . " AND $id_field > $current_max" :
"$id_field > $current_max";
$result = $replicastore->get_min_max_object_id(
$id_field,
$table,
$where,
$batch_size
);
if ( empty( $result->min ) && empty( $result->max ) ) {
// Our query produced no min and max. We can assume the min from the previous query,
// and the total max we found in the initial query.
$current_max = (int) $total->max;
$result = (object) array(
'min' => $current_min,
'max' => $current_max,
);
} else {
$current_min = (int) $result->min;
$current_max = (int) $result->max;
}
$results[] = $result;
}
return $results;
}
/**
* Return Total number of objects.
*
* @param array $config Full Sync config.
*
* @return int total
*/
public function total( $config ) {
global $wpdb;
$table = $wpdb->{$this->table_name()};
$where = $this->get_where_sql( $config );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE $where" );
}
/**
* Retrieve the WHERE SQL clause based on the module config.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
*/
public function get_where_sql( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return '1=1';
}
}

View File

@ -0,0 +1,252 @@
<?php
/**
* Network Options sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Defaults;
/**
* Class to handle sync for network options.
*/
class Network_Options extends Module {
/**
* Whitelist for network options we want to sync.
*
* @access private
*
* @var array
*/
private $network_options_whitelist;
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'network_options';
}
/**
* Initialize network options action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
// Multi site network options.
add_action( 'add_site_option', $callable, 10, 2 );
add_action( 'update_site_option', $callable, 10, 3 );
add_action( 'delete_site_option', $callable, 10, 1 );
$whitelist_network_option_handler = array( $this, 'whitelist_network_options' );
add_filter( 'jetpack_sync_before_enqueue_delete_site_option', $whitelist_network_option_handler );
add_filter( 'jetpack_sync_before_enqueue_add_site_option', $whitelist_network_option_handler );
add_filter( 'jetpack_sync_before_enqueue_update_site_option', $whitelist_network_option_handler );
}
/**
* Initialize network options 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_network_options', $callable );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
// Full sync.
add_filter(
'jetpack_sync_before_send_jetpack_full_sync_network_options',
array(
$this,
'expand_network_options',
)
);
}
/**
* Set module defaults.
* Define the network options whitelist based on the default one.
*
* @access public
*/
public function set_defaults() {
$this->network_options_whitelist = Defaults::$default_network_options_whitelist;
}
/**
* Enqueue the network options 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* Tells the client to sync all options to the server
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param boolean Whether to expand options (should always be true)
*/
do_action( 'jetpack_full_sync_network_options', true );
// The number of actions enqueued, and next module state (true == done).
return array( 1, true );
}
/**
* Send the network options actions for full sync.
*
* @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 $state This module Full Sync status.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // 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 ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
}
/**
* 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_network_options' );
}
/**
* Retrieve all network options as per the current network options whitelist.
*
* @access public
*
* @return array All network options.
*/
public function get_all_network_options() {
$options = array();
foreach ( $this->network_options_whitelist as $option ) {
$options[ $option ] = get_site_option( $option );
}
return $options;
}
/**
* Set the network options whitelist.
*
* @access public
*
* @param array $options The new network options whitelist.
*/
public function set_network_options_whitelist( $options ) {
$this->network_options_whitelist = $options;
}
/**
* Get the network options whitelist.
*
* @access public
*
* @return array The network options whitelist.
*/
public function get_network_options_whitelist() {
return $this->network_options_whitelist;
}
/**
* Reject non-whitelisted network options.
*
* @access public
*
* @param array $args The hook parameters.
* @return array|false $args The hook parameters, false if not a whitelisted network option.
*/
public function whitelist_network_options( $args ) {
if ( ! $this->is_whitelisted_network_option( $args[0] ) ) {
return false;
}
return $args;
}
/**
* Whether the option is a whitelisted network option.
*
* @access public
*
* @param string $option Option name.
* @return boolean True if this is a whitelisted network option.
*/
public function is_whitelisted_network_option( $option ) {
return in_array( $option, $this->network_options_whitelist, true );
}
/**
* Expand the network options within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The hook parameters.
*/
public function expand_network_options( $args ) {
if ( $args[0] ) {
return $this->get_all_network_options();
}
return $args;
}
/**
* Return Total number of objects.
*
* @param array $config Full Sync config.
*
* @return int total
*/
public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return count( $this->network_options_whitelist );
}
}

View File

@ -0,0 +1,481 @@
<?php
/**
* Options sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Defaults;
use Automattic\Jetpack\Sync\Settings;
/**
* Class to handle sync for options.
*/
class Options extends Module {
/**
* Whitelist for options we want to sync.
*
* @access private
*
* @var array
*/
private $options_whitelist;
/**
* Contentless options we want to sync.
*
* @access private
*
* @var array
*/
private $options_contentless;
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'options';
}
/**
* Initialize options action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
// Options.
add_action( 'added_option', $callable, 10, 2 );
add_action( 'updated_option', $callable, 10, 3 );
add_action( 'deleted_option', $callable, 10, 1 );
// Sync Core Icon: Detect changes in Core's Site Icon and make it syncable.
add_action( 'add_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) );
add_action( 'update_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) );
add_action( 'delete_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) );
// Handle deprecated options.
add_filter( 'jetpack_options_whitelist', array( $this, 'add_deprecated_options' ) );
$whitelist_option_handler = array( $this, 'whitelist_options' );
add_filter( 'jetpack_sync_before_enqueue_deleted_option', $whitelist_option_handler );
add_filter( 'jetpack_sync_before_enqueue_added_option', $whitelist_option_handler );
add_filter( 'jetpack_sync_before_enqueue_updated_option', $whitelist_option_handler );
}
/**
* Initialize options 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_options', $callable );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_options', array( $this, 'expand_options' ) );
}
/**
* Set module defaults.
* Define the options whitelist and contentless options.
*
* @access public
*/
public function set_defaults() {
$this->update_options_whitelist();
$this->update_options_contentless();
}
/**
* Set module defaults at a later time.
*
* @access public
*/
public function set_late_default() {
/** This filter is already documented in json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php */
$late_options = apply_filters( 'jetpack_options_whitelist', array() );
if ( ! empty( $late_options ) && is_array( $late_options ) ) {
$this->options_whitelist = array_merge( $this->options_whitelist, $late_options );
}
}
/**
* Add old deprecated options to the list of options to keep in sync.
*
* @since 1.14.0
*
* @access public
*
* @param array $options The default list of site options.
*/
public function add_deprecated_options( $options ) {
global $wp_version;
$deprecated_options = array(
'blacklist_keys' => '5.5-alpha', // Replaced by disallowed_keys.
'comment_whitelist' => '5.5-alpha', // Replaced by comment_previously_approved.
);
foreach ( $deprecated_options as $option => $version ) {
if ( version_compare( $wp_version, $version, '<=' ) ) {
$options[] = $option;
}
}
return $options;
}
/**
* Enqueue the options 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* Tells the client to sync all options to the server
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param boolean Whether to expand options (should always be true)
*/
do_action( 'jetpack_full_sync_options', true );
// The number of actions enqueued, and next module state (true == done).
return array( 1, true );
}
/**
* Send the options actions for full sync.
*
* @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 $state This module Full Sync status.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // 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 ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
}
/**
* 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
}
/**
* 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_options' );
}
/**
* Retrieve all options as per the current options whitelist.
* Public so that we don't have to store so much data all the options twice.
*
* @access public
*
* @return array All options.
*/
public function get_all_options() {
$options = array();
$random_string = wp_generate_password();
foreach ( $this->options_whitelist as $option ) {
if ( 0 === strpos( $option, Settings::SETTINGS_OPTION_PREFIX ) ) {
$option_value = Settings::get_setting( str_replace( Settings::SETTINGS_OPTION_PREFIX, '', $option ) );
$options[ $option ] = $option_value;
} else {
$option_value = get_option( $option, $random_string );
if ( $option_value !== $random_string ) {
$options[ $option ] = $option_value;
}
}
}
// Add theme mods.
$theme_mods_option = 'theme_mods_' . get_option( 'stylesheet' );
$theme_mods_value = get_option( $theme_mods_option, $random_string );
if ( $theme_mods_value === $random_string ) {
return $options;
}
$this->filter_theme_mods( $theme_mods_value );
$options[ $theme_mods_option ] = $theme_mods_value;
return $options;
}
/**
* Update the options whitelist to the default one.
*
* @access public
*/
public function update_options_whitelist() {
$this->options_whitelist = Defaults::get_options_whitelist();
}
/**
* Set the options whitelist.
*
* @access public
*
* @param array $options The new options whitelist.
*/
public function set_options_whitelist( $options ) {
$this->options_whitelist = $options;
}
/**
* Get the options whitelist.
*
* @access public
*
* @return array The options whitelist.
*/
public function get_options_whitelist() {
return $this->options_whitelist;
}
/**
* Update the contentless options to the defaults.
*
* @access public
*/
public function update_options_contentless() {
$this->options_contentless = Defaults::get_options_contentless();
}
/**
* Get the contentless options.
*
* @access public
*
* @return array Array of the contentless options.
*/
public function get_options_contentless() {
return $this->options_contentless;
}
/**
* Reject any options that aren't whitelisted or contentless.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The hook parameters.
*/
public function whitelist_options( $args ) {
// Reject non-whitelisted options.
if ( ! $this->is_whitelisted_option( $args[0] ) ) {
return false;
}
// Filter our weird array( false ) value for theme_mods_*.
if ( 'theme_mods_' === substr( $args[0], 0, 11 ) ) {
$this->filter_theme_mods( $args[1] );
if ( isset( $args[2] ) ) {
$this->filter_theme_mods( $args[2] );
}
}
// Set value(s) of contentless option to empty string(s).
if ( $this->is_contentless_option( $args[0] ) ) {
// Create a new array matching length of $args, containing empty strings.
$empty = array_fill( 0, count( $args ), '' );
$empty[0] = $args[0];
return $empty;
}
return $args;
}
/**
* Whether a certain option is whitelisted for sync.
*
* @access public
*
* @param string $option Option name.
* @return boolean Whether the option is whitelisted.
*/
public function is_whitelisted_option( $option ) {
return in_array( $option, $this->options_whitelist, true ) || 'theme_mods_' === substr( $option, 0, 11 );
}
/**
* Whether a certain option is a contentless one.
*
* @access private
*
* @param string $option Option name.
* @return boolean Whether the option is contentless.
*/
private function is_contentless_option( $option ) {
return in_array( $option, $this->options_contentless, true );
}
/**
* Filters out falsy values from theme mod options.
*
* @access private
*
* @param array $value Option value.
*/
private function filter_theme_mods( &$value ) {
if ( is_array( $value ) && isset( $value[0] ) ) {
unset( $value[0] );
}
}
/**
* Handle changes in the core site icon and sync them.
*
* @access public
*/
public function jetpack_sync_core_icon() {
$url = get_site_icon_url();
$jetpack_url = \Jetpack_Options::get_option( 'site_icon_url' );
if ( defined( 'JETPACK__PLUGIN_DIR' ) ) {
if ( ! function_exists( 'jetpack_site_icon_url' ) ) {
require_once JETPACK__PLUGIN_DIR . 'modules/site-icon/site-icon-functions.php';
}
$jetpack_url = jetpack_site_icon_url();
}
// If there's a core icon, maybe update the option. If not, fall back to Jetpack's.
if ( ! empty( $url ) && $jetpack_url !== $url ) {
// This is the option that is synced with dotcom.
\Jetpack_Options::update_option( 'site_icon_url', $url );
} elseif ( empty( $url ) ) {
\Jetpack_Options::delete_option( 'site_icon_url' );
}
}
/**
* Expand all options within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The hook parameters.
*/
public function expand_options( $args ) {
if ( $args[0] ) {
return $this->get_all_options();
}
return $args;
}
/**
* Return Total number of objects.
*
* @param array $config Full Sync config.
*
* @return int total
*/
public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return count( Defaults::get_options_whitelist() );
}
/**
* Retrieve a set of options by their IDs.
*
* @access public
*
* @param string $object_type Object type.
* @param array $ids Object IDs.
* @return array Array of objects.
*/
public function get_objects_by_id( $object_type, $ids ) {
if ( empty( $ids ) || empty( $object_type ) || 'option' !== $object_type ) {
return array();
}
$objects = array();
foreach ( (array) $ids as $id ) {
$object = $this->get_object_by_id( $object_type, $id );
// Only add object if we have the object.
if ( 'OPTION-DOES-NOT-EXIST' !== $object ) {
if ( 'all' === $id ) {
// If all was requested it contains all options and can simply be returned.
return $object;
}
$objects[ $id ] = $object;
}
}
return $objects;
}
/**
* Retrieve an option by its name.
*
* @access public
*
* @param string $object_type Type of the sync object.
* @param string $id ID of the sync object.
* @return mixed Value of Option or 'OPTION-DOES-NOT-EXIST' if not found.
*/
public function get_object_by_id( $object_type, $id ) {
if ( 'option' === $object_type ) {
// Utilize Random string as default value to distinguish between false and not exist.
$random_string = wp_generate_password();
// Only whitelisted options can be returned.
if ( in_array( $id, $this->options_whitelist, true ) ) {
if ( 0 === strpos( $id, Settings::SETTINGS_OPTION_PREFIX ) ) {
$option_value = Settings::get_setting( str_replace( Settings::SETTINGS_OPTION_PREFIX, '', $id ) );
return $option_value;
} else {
$option_value = get_option( $id, $random_string );
if ( $option_value !== $random_string ) {
return $option_value;
}
}
} elseif ( 'all' === $id ) {
return $this->get_all_options();
}
}
return 'OPTION-DOES-NOT-EXIST';
}
}

View File

@ -0,0 +1,420 @@
<?php
/**
* Plugins sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
/**
* Class to handle sync for plugins.
*/
class Plugins extends Module {
/**
* Action handler callable.
*
* @access private
*
* @var callable
*/
private $action_handler;
/**
* Information about plugins we store temporarily.
*
* @access private
*
* @var array
*/
private $plugin_info = array();
/**
* List of all plugins in the installation.
*
* @access private
*
* @var array
*/
private $plugins = array();
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'plugins';
}
/**
* Initialize plugins action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
$this->action_handler = $callable;
add_action( 'deleted_plugin', array( $this, 'deleted_plugin' ), 10, 2 );
add_action( 'activated_plugin', $callable, 10, 2 );
add_action( 'deactivated_plugin', $callable, 10, 2 );
add_action( 'delete_plugin', array( $this, 'delete_plugin' ) );
add_filter( 'upgrader_pre_install', array( $this, 'populate_plugins' ), 10, 1 );
add_action( 'upgrader_process_complete', array( $this, 'on_upgrader_completion' ), 10, 2 );
add_action( 'jetpack_plugin_installed', $callable, 10, 1 );
add_action( 'jetpack_plugin_update_failed', $callable, 10, 4 );
add_action( 'jetpack_plugins_updated', $callable, 10, 2 );
add_action( 'admin_action_update', array( $this, 'check_plugin_edit' ) );
add_action( 'jetpack_edited_plugin', $callable, 10, 2 );
add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'plugin_edit_ajax' ), 0 );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_activated_plugin', array( $this, 'expand_plugin_data' ) );
add_filter( 'jetpack_sync_before_send_deactivated_plugin', array( $this, 'expand_plugin_data' ) );
// Note that we don't simply 'expand_plugin_data' on the 'delete_plugin' action here because the plugin file is deleted when that action finishes.
}
/**
* Fetch and populate all current plugins before upgrader installation.
*
* @access public
*
* @param bool|WP_Error $response Install response, true if successful, WP_Error if not.
*/
public function populate_plugins( $response ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$this->plugins = get_plugins();
return $response;
}
/**
* Handler for the upgrader success finishes.
*
* @access public
*
* @param \WP_Upgrader $upgrader Upgrader instance.
* @param array $details Array of bulk item update data.
*/
public function on_upgrader_completion( $upgrader, $details ) {
if ( ! isset( $details['type'] ) ) {
return;
}
if ( 'plugin' !== $details['type'] ) {
return;
}
if ( ! isset( $details['action'] ) ) {
return;
}
$plugins = ( isset( $details['plugins'] ) ? $details['plugins'] : null );
if ( empty( $plugins ) ) {
$plugins = ( isset( $details['plugin'] ) ? array( $details['plugin'] ) : null );
}
// For plugin installer.
if ( empty( $plugins ) && method_exists( $upgrader, 'plugin_info' ) ) {
$plugins = array( $upgrader->plugin_info() );
}
if ( empty( $plugins ) ) {
return; // We shouldn't be here.
}
switch ( $details['action'] ) {
case 'update':
$state = array(
'is_autoupdate' => Jetpack_Constants::is_true( 'JETPACK_PLUGIN_AUTOUPDATE' ),
);
$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'], $state );
}
return;
}
/**
* Sync that a plugin update
*
* @since 1.6.3
* @since-jetpack 5.8.0
*
* @module sync
*
* @param array () $plugin, Plugin Data
*/
do_action( 'jetpack_plugins_updated', array_map( array( $this, 'get_plugin_info' ), $plugins ), $state );
break;
case 'install':
}
if ( 'install' === $details['action'] ) {
/**
* 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 ) );
return;
}
}
/**
* Retrieve the plugin information by a plugin slug.
*
* @access private
*
* @param string $slug Plugin slug.
* @return array Plugin information.
*/
private function get_plugin_info( $slug ) {
$plugins = get_plugins(); // Get the most up to date info.
if ( isset( $plugins[ $slug ] ) ) {
return array_merge( array( 'slug' => $slug ), $plugins[ $slug ] );
}
// Try grabbing the info from before the update.
return isset( $this->plugins[ $slug ] ) ? array_merge( array( 'slug' => $slug ), $this->plugins[ $slug ] ) : array( 'slug' => $slug );
}
/**
* Retrieve upgrade errors.
*
* @access private
*
* @param \Automatic_Upgrader_Skin|\WP_Upgrader_Skin $skin The upgrader skin being used.
* @return array|boolean Error on error, false otherwise.
*/
private function get_errors( $skin ) {
$errors = method_exists( $skin, 'get_errors' ) ? $skin->get_errors() : null;
if ( is_wp_error( $errors ) ) {
$error_code = $errors->get_error_code();
if ( ! empty( $error_code ) ) {
return array(
'code' => $error_code,
'message' => $errors->get_error_message(),
);
}
}
if ( isset( $skin->result ) ) {
$errors = $skin->result;
if ( is_wp_error( $errors ) ) {
return array(
'code' => $errors->get_error_code(),
'message' => $errors->get_error_message(),
);
}
if ( empty( $skin->result ) ) {
return array(
'code' => 'unknown',
'message' => __( 'Unknown Plugin Update Failure', 'jetpack-sync' ),
);
}
}
return false;
}
/**
* Handle plugin edit in the administration.
*
* @access public
*
* @todo The `admin_action_update` hook is called only for logged in users, but maybe implement nonce verification?
*/
public function check_plugin_edit() {
$screen = get_current_screen();
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( 'plugin-editor' !== $screen->base || ! isset( $_POST['newcontent'] ) || ! isset( $_POST['plugin'] ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated manually just after.
$plugin = wp_unslash( $_POST['plugin'] );
$plugins = get_plugins();
if ( ! isset( $plugins[ $plugin ] ) ) {
return;
}
/**
* Helps Sync log that a plugin was edited
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param string $plugin, Plugin slug
* @param mixed $plugins[ $plugin ], Array of plugin data
*/
do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] );
}
/**
* Handle plugin ajax edit in the administration.
*
* @access public
*
* @todo Update this method to use WP_Filesystem instead of fopen/fclose.
*/
public function plugin_edit_ajax() {
// This validation is based on wp_edit_theme_plugin_file().
$args = wp_unslash( $_POST );
if ( empty( $args['file'] ) ) {
return;
}
$file = $args['file'];
if ( 0 !== validate_file( $file ) ) {
return;
}
if ( ! isset( $args['newcontent'] ) ) {
return;
}
if ( ! isset( $args['nonce'] ) ) {
return;
}
if ( empty( $args['plugin'] ) ) {
return;
}
$plugin = $args['plugin'];
if ( ! current_user_can( 'edit_plugins' ) ) {
return;
}
if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) {
return;
}
$plugins = get_plugins();
if ( ! array_key_exists( $plugin, $plugins ) ) {
return;
}
if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) {
return;
}
$real_file = WP_PLUGIN_DIR . '/' . $file;
if ( ! is_writeable( $real_file ) ) {
return;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
$file_pointer = fopen( $real_file, 'w+' );
if ( false === $file_pointer ) {
return;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
fclose( $file_pointer );
/**
* This action is documented already in this file
*/
do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] );
}
/**
* Handle plugin deletion.
*
* @access public
*
* @param string $plugin_path Path to the plugin main file.
*/
public function delete_plugin( $plugin_path ) {
$full_plugin_path = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_path;
// Checking for file existence because some sync plugin module tests simulate plugin installation and deletion without putting file on disk.
if ( file_exists( $full_plugin_path ) ) {
$all_plugin_data = get_plugin_data( $full_plugin_path );
$data = array(
'name' => $all_plugin_data['Name'],
'version' => $all_plugin_data['Version'],
);
} else {
$data = array(
'name' => $plugin_path,
'version' => 'unknown',
);
}
$this->plugin_info[ $plugin_path ] = $data;
}
/**
* Invoked after plugin deletion.
*
* @access public
*
* @param string $plugin_path Path to the plugin main file.
* @param boolean $is_deleted Whether the plugin was deleted successfully.
*/
public function deleted_plugin( $plugin_path, $is_deleted ) {
call_user_func( $this->action_handler, $plugin_path, $is_deleted, $this->plugin_info[ $plugin_path ] );
unset( $this->plugin_info[ $plugin_path ] );
}
/**
* Expand the plugins within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The expanded hook parameters.
*/
public function expand_plugin_data( $args ) {
$plugin_path = $args[0];
$plugin_data = array();
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
if ( isset( $all_plugins[ $plugin_path ] ) ) {
$all_plugin_data = $all_plugins[ $plugin_path ];
$plugin_data['name'] = $all_plugin_data['Name'];
$plugin_data['version'] = $all_plugin_data['Version'];
}
return array(
$args[0],
$args[1],
$plugin_data,
);
}
}

View File

@ -0,0 +1,776 @@
<?php
/**
* Posts sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use Automattic\Jetpack\Roles;
use Automattic\Jetpack\Sync\Settings;
/**
* Class to handle sync for posts.
*/
class Posts extends Module {
/**
* The post IDs of posts that were just published but not synced yet.
*
* @access private
*
* @var array
*/
private $just_published = array();
/**
* The previous status of posts that we use for calculating post status transitions.
*
* @access private
*
* @var array
*/
private $previous_status = array();
/**
* Action handler callable.
*
* @access private
*
* @var callable
*/
private $action_handler;
/**
* Import end.
*
* @access private
*
* @todo This appears to be unused - let's remove it.
*
* @var boolean
*/
private $import_end = false;
/**
* Max bytes allowed for post_content => length.
* Current Setting : 5MB.
*
* @access public
*
* @var int
*/
const MAX_POST_CONTENT_LENGTH = 5000000;
/**
* Max bytes allowed for post meta_value => length.
* Current Setting : 2MB.
*
* @access public
*
* @var int
*/
const MAX_POST_META_LENGTH = 2000000;
/**
* Default previous post state.
* Used for default previous post status.
*
* @access public
*
* @var string
*/
const DEFAULT_PREVIOUS_STATE = 'new';
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'posts';
}
/**
* The table in the database.
*
* @access public
*
* @return string
*/
public function table_name() {
return 'posts';
}
/**
* Retrieve a post by its ID.
*
* @access public
*
* @param string $object_type Type of the sync object.
* @param int $id ID of the sync object.
* @return \WP_Post|bool Filtered \WP_Post object, or false if the object is not a post.
*/
public function get_object_by_id( $object_type, $id ) {
if ( 'post' === $object_type ) {
$post = get_post( (int) $id );
if ( $post ) {
return $this->filter_post_content_and_add_links( $post );
}
}
return false;
}
/**
* Initialize posts action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
$this->action_handler = $callable;
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 );
add_action( 'deleted_post', $callable, 10 );
add_action( 'jetpack_published_post', $callable, 10, 2 );
add_filter( 'jetpack_sync_before_enqueue_deleted_post', array( $this, 'filter_blacklisted_post_types_deleted' ) );
add_action( 'transition_post_status', array( $this, 'save_published' ), 10, 3 );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_post', array( $this, 'filter_blacklisted_post_types' ) );
// Listen for meta changes.
$this->init_listeners_for_meta_type( 'post', $callable );
$this->init_meta_whitelist_handler( 'post', array( $this, 'filter_meta' ) );
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 );
}
/**
* Before Akismet's daily cleanup of spam detection metadata.
*
* @access public
*
* @param array $feedback_ids IDs of feedback posts.
*/
public function daily_akismet_meta_cleanup_before( $feedback_ids ) {
remove_action( 'deleted_post_meta', $this->action_handler );
if ( ! is_array( $feedback_ids ) || count( $feedback_ids ) < 1 ) {
return;
}
$ids_chunks = array_chunk( $feedback_ids, 100, false );
foreach ( $ids_chunks as $chunk ) {
/**
* Used for syncing deletion of batch post meta
*
* @since 1.6.3
* @since-jetpack 6.1.0
*
* @module sync
*
* @param array $feedback_ids feedback post IDs
* @param string $meta_key to be deleted
*/
do_action( 'jetpack_post_meta_batch_delete', $chunk, '_feedback_akismet_values' );
}
}
/**
* After Akismet's daily cleanup of spam detection metadata.
*
* @access public
*
* @param array $feedback_ids IDs of feedback posts.
*/
public function daily_akismet_meta_cleanup_after( $feedback_ids ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
add_action( 'deleted_post_meta', $this->action_handler );
}
/**
* Initialize posts 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_posts', $callable ); // Also sends post meta.
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_sync_save_post', array( $this, 'expand_jetpack_sync_save_post' ) );
// 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_deleted_post_meta', array( $this, 'trim_post_meta' ) );
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'expand_post_ids' ) );
}
/**
* Enqueue the posts 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 ) {
global $wpdb;
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_posts', $wpdb->posts, 'ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @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.
*/
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM $wpdb->posts WHERE " . $this->get_where_sql( $config );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
/**
* Retrieve the WHERE SQL clause based on the module config.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
*/
public function get_where_sql( $config ) {
$where_sql = Settings::get_blacklisted_post_types_sql();
// Config is a list of post IDs to sync.
if ( is_array( $config ) ) {
$where_sql .= ' AND ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
return $where_sql;
}
/**
* 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_posts' );
}
/**
* Filter meta arguments so that we don't sync meta_values over MAX_POST_META_LENGTH.
*
* @param array $args action arguments.
*
* @return array filtered action arguments.
*/
public function trim_post_meta( $args ) {
list( $meta_id, $object_id, $meta_key, $meta_value ) = $args;
// 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_POST_META_LENGTH ) {
$meta_value = '';
}
return array( $meta_id, $object_id, $meta_key, $meta_value );
}
/**
* Process content before send.
*
* @param array $args Arguments of the `wp_insert_post` hook.
*
* @return array
*/
public function expand_jetpack_sync_save_post( $args ) {
list( $post_id, $post, $update, $previous_state ) = $args;
return array( $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state );
}
/**
* Filter all blacklisted post types.
*
* @param array $args Hook arguments.
* @return array|false Hook arguments, or false if the post type is a blacklisted one.
*/
public function filter_blacklisted_post_types_deleted( $args ) {
// 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] ) ) {
return false;
}
return $args;
}
/**
* Filter all blacklisted post types.
*
* @param array $args Hook arguments.
* @return array|false Hook arguments, or false if the post type is a blacklisted one.
*/
public function filter_blacklisted_post_types( $args ) {
$post = $args[1];
if ( in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true ) ) {
return false;
}
return $args;
}
/**
* Filter all meta that is not blacklisted, or is stored for a disallowed post type.
*
* @param array $args Hook arguments.
* @return array|false Hook arguments, or false if meta was filtered.
*/
public function filter_meta( $args ) {
if ( $this->is_post_type_allowed( $args[1] ) && $this->is_whitelisted_post_meta( $args[2] ) ) {
return $args;
}
return false;
}
/**
* Whether a post meta key is whitelisted.
*
* @param string $meta_key Meta key.
* @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 ) || ( 0 === strpos( $meta_key, '_wpas_skip_' ) );
}
/**
* Whether a post type is allowed.
* A post type will be disallowed if it's present in the post type blacklist.
*
* @param int $post_id ID of the post.
* @return boolean Whether the post type is allowed.
*/
public function is_post_type_allowed( $post_id ) {
$post = get_post( (int) $post_id );
if ( isset( $post->post_type ) ) {
return ! in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true );
}
return false;
}
/**
* Remove the embed shortcode.
*
* @global $wp_embed
*/
public function remove_embed() {
global $wp_embed;
remove_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 );
// remove the embed shortcode since we would do the part later.
remove_shortcode( 'embed' );
// Attempts to embed all URLs in a post.
remove_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 );
}
/**
* Add the embed shortcode.
*
* @global $wp_embed
*/
public function add_embed() {
global $wp_embed;
add_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 );
// Shortcode placeholder for strip_shortcodes().
add_shortcode( 'embed', '__return_false' );
// Attempts to embed all URLs in a post.
add_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 );
}
/**
* Expands wp_insert_post to include filtered content
*
* @param \WP_Post $post_object Post object.
*/
public function filter_post_content_and_add_links( $post_object ) {
global $post;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = $post_object;
// Return non existant post.
$post_type = get_post_type_object( $post->post_type );
if ( empty( $post_type ) || ! is_object( $post_type ) ) {
$non_existant_post = new \stdClass();
$non_existant_post->ID = $post->ID;
$non_existant_post->post_modified = $post->post_modified;
$non_existant_post->post_modified_gmt = $post->post_modified_gmt;
$non_existant_post->post_status = 'jetpack_sync_non_registered_post_type';
$non_existant_post->post_type = $post->post_type;
return $non_existant_post;
}
/**
* Filters whether to prevent sending post data to .com
*
* Passing true to the filter will prevent the post data from being sent
* to the WordPress.com.
* Instead we pass data that will still enable us to do a checksum against the
* Jetpacks data but will prevent us from displaying the data on in the API as well as
* other services.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param boolean false prevent post data from being synced to WordPress.com
* @param mixed $post \WP_Post object
*/
if ( apply_filters( 'jetpack_sync_prevent_sending_post_data', false, $post ) ) {
// We only send the bare necessary object to be able to create a checksum.
$blocked_post = new \stdClass();
$blocked_post->ID = $post->ID;
$blocked_post->post_modified = $post->post_modified;
$blocked_post->post_modified_gmt = $post->post_modified_gmt;
$blocked_post->post_status = 'jetpack_sync_blocked';
$blocked_post->post_type = $post->post_type;
return $blocked_post;
}
// lets not do oembed just yet.
$this->remove_embed();
if ( 0 < strlen( $post->post_password ) ) {
$post->post_password = 'auto-' . wp_generate_password( 10, false );
}
// Explicitly omit post_content when it exceeds limit.
// Large content will cause OOM issues and break Sync.
if ( strlen( $post->post_content ) >= self::MAX_POST_CONTENT_LENGTH ) {
$post->post_content = '';
}
/** This filter is already documented in core. wp-includes/post-template.php */
if ( Settings::get_setting( 'render_filtered_content' ) && $post_type->public ) {
global $shortcode_tags;
/**
* Filter prevents some shortcodes from expanding.
*
* Since we can can expand some type of shortcode better on the .com side and make the
* expansion more relevant to contexts. For example [galleries] and subscription emails
*
* @since 1.6.3
* @since-jetpack 4.5.0
*
* @param array of shortcode tags to remove.
*/
$shortcodes_to_remove = apply_filters(
'jetpack_sync_do_not_expand_shortcodes',
array(
'gallery',
'slideshow',
)
);
$removed_shortcode_callbacks = array();
foreach ( $shortcodes_to_remove as $shortcode ) {
if ( isset( $shortcode_tags[ $shortcode ] ) ) {
$removed_shortcode_callbacks[ $shortcode ] = $shortcode_tags[ $shortcode ];
}
}
array_map( 'remove_shortcode', array_keys( $removed_shortcode_callbacks ) );
$post->post_content_filtered = apply_filters( 'the_content', $post->post_content );
$post->post_excerpt_filtered = apply_filters( 'the_excerpt', $post->post_excerpt );
foreach ( $removed_shortcode_callbacks as $shortcode => $callback ) {
add_shortcode( $shortcode, $callback );
}
}
$this->add_embed();
if ( has_post_thumbnail( $post->ID ) ) {
$image_attributes = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' );
if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) {
$post->featured_image = $image_attributes[0];
}
}
$post->permalink = get_permalink( $post->ID );
$post->shortlink = wp_get_shortlink( $post->ID );
if ( function_exists( 'amp_get_permalink' ) ) {
$post->amp_permalink = amp_get_permalink( $post->ID );
}
return $post;
}
/**
* Handle transition from another post status to a published one.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param \WP_Post $post Post object.
*/
public function save_published( $new_status, $old_status, $post ) {
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
$this->just_published[ $post->ID ] = true;
}
$this->previous_status[ $post->ID ] = $old_status;
}
/**
* When publishing or updating a post, the Gutenberg editor sends two requests:
* 1. sent to WP REST API endpoint `wp-json/wp/v2/posts/$id`
* 2. sent to wp-admin/post.php `?post=$id&action=edit&classic-editor=1&meta_box=1`
*
* The 2nd request is to update post meta, which is not supported on WP REST API.
* When syncing post data, we will include if this was a meta box update.
*
* @todo Implement nonce verification.
*
* @return boolean Whether this is a Gutenberg meta box update.
*/
public function is_gutenberg_meta_box_update() {
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
return (
isset( $_POST['action'], $_GET['classic-editor'], $_GET['meta_box'] ) &&
'editpost' === $_POST['action'] &&
'1' === $_GET['classic-editor'] &&
'1' === $_GET['meta_box']
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
);
}
/**
* Handler for the wp_insert_post hook.
* Called upon creation of a new post.
*
* @param int $post_ID Post ID.
* @param \WP_Post $post Post object.
* @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 ) {
return;
}
// Workaround for https://github.com/woocommerce/woocommerce/issues/18007.
if ( $post && 'shop_order' === $post->post_type ) {
$post = get_post( $post_ID );
}
$previous_status = isset( $this->previous_status[ $post_ID ] ) ? $this->previous_status[ $post_ID ] : self::DEFAULT_PREVIOUS_STATE;
$just_published = isset( $this->just_published[ $post_ID ] ) ? $this->just_published[ $post_ID ] : false;
$state = array(
'is_auto_save' => (bool) Jetpack_Constants::get_constant( 'DOING_AUTOSAVE' ),
'previous_status' => $previous_status,
'just_published' => $just_published,
'is_gutenberg_meta_box_update' => $this->is_gutenberg_meta_box_update(),
);
/**
* Filter that is used to add to the post flags ( meta data ) when a post gets published
*
* @since 1.6.3
* @since-jetpack 5.8.0
*
* @param int $post_ID the post ID
* @param mixed $post \WP_Post object
* @param bool $update Whether this is an existing post being updated or not.
* @param mixed $state state
*
* @module sync
*/
do_action( 'jetpack_sync_save_post', $post_ID, $post, $update, $state );
unset( $this->previous_status[ $post_ID ] );
}
/**
* Handler for the wp_after_insert_post hook.
* Called after creation/update of a new post.
*
* @param int $post_ID Post ID.
* @param \WP_Post $post Post object.
**/
public function wp_after_insert_post( $post_ID, $post ) {
if ( ! is_numeric( $post_ID ) || $post === null ) {
return;
}
// Workaround for https://github.com/woocommerce/woocommerce/issues/18007.
if ( $post && 'shop_order' === $post->post_type ) {
$post = get_post( $post_ID );
}
$this->send_published( $post_ID, $post );
}
/**
* Send a published post for sync.
*
* @param int $post_ID Post ID.
* @param \WP_Post $post Post object.
*/
public function send_published( $post_ID, $post ) {
if ( ! isset( $this->just_published[ $post_ID ] ) ) {
return;
}
// Post revisions cause race conditions where this send_published add the action before the actual post gets synced.
if ( wp_is_post_autosave( $post ) || wp_is_post_revision( $post ) ) {
return;
}
$post_flags = array(
'post_type' => $post->post_type,
);
$author_user_object = get_user_by( 'id', $post->post_author );
if ( $author_user_object ) {
$roles = new Roles();
$post_flags['author'] = array(
'id' => $post->post_author,
'wpcom_user_id' => get_user_meta( $post->post_author, 'wpcom_user_id', true ),
'display_name' => $author_user_object->display_name,
'email' => $author_user_object->user_email,
'translated_role' => $roles->translate_user_to_role( $author_user_object ),
);
}
/**
* Filter that is used to add to the post flags ( meta data ) when a post gets published
*
* @since 1.6.3
* @since-jetpack 4.4.0
*
* @param mixed array post flags that are added to the post
* @param mixed $post \WP_Post object
*/
$flags = apply_filters( 'jetpack_published_post_flags', $post_flags, $post );
// Only Send Pulished Post event if post_type is not blacklisted.
if ( ! in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true ) ) {
// Refreshing the post in the cache site before triggering the publish event.
// The true parameter means that it's an update action, not create action.
$this->wp_insert_post( $post_ID, $post, true );
/**
* Action that gets synced when a post type gets published.
*
* @since 1.6.3
* @since-jetpack 4.4.0
*
* @param int $post_ID
* @param mixed array $flags post flags that are added to the post
*/
do_action( 'jetpack_published_post', $post_ID, $flags );
}
unset( $this->just_published[ $post_ID ] );
/**
* Send additional sync action for Activity Log when post is a Customizer publish
*/
if ( 'customize_changeset' === $post->post_type ) {
$post_content = json_decode( $post->post_content, true );
foreach ( $post_content as $key => $value ) {
// Skip if it isn't a widget.
if ( 'widget_' !== substr( $key, 0, strlen( 'widget_' ) ) ) {
continue;
}
// Change key from "widget_archives[2]" to "archives-2".
$key = str_replace( 'widget_', '', $key );
$key = str_replace( '[', '-', $key );
$key = str_replace( ']', '', $key );
global $wp_registered_widgets;
if ( isset( $wp_registered_widgets[ $key ] ) ) {
$widget_data = array(
'name' => $wp_registered_widgets[ $key ]['name'],
'id' => $key,
'title' => $value['value']['title'],
);
do_action( 'jetpack_widget_edited', $widget_data );
}
}
}
}
/**
* Expand post IDs to post objects within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The expanded hook parameters.
*/
public function expand_post_ids( $args ) {
list( $post_ids, $previous_interval_end) = $args;
$posts = array_filter( array_map( array( 'WP_Post', 'get_instance' ), $post_ids ) );
$posts = array_map( array( $this, 'filter_post_content_and_add_links' ), $posts );
$posts = array_values( $posts ); // Reindex in case posts were deleted.
return array(
$posts,
$this->get_metadata( $post_ids, 'post', Settings::get_setting( 'post_meta_whitelist' ) ),
$this->get_term_relationships( $post_ids ),
$previous_interval_end,
);
}
/**
* Gets a list of minimum and maximum object ids for each batch based on the given batch size.
*
* @access public
*
* @param int $batch_size The batch size for objects.
* @param string|bool $where_sql The sql where clause minus 'WHERE', or false if no where clause is needed.
*
* @return array|bool An array of min and max ids for each batch. FALSE if no table can be found.
*/
public function get_min_max_object_ids_for_batches( $batch_size, $where_sql = false ) {
return parent::get_min_max_object_ids_for_batches( $batch_size, $this->get_where_sql( $where_sql ) );
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Protect sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
/**
* Class to handle sync for Protect.
* Logs BruteProtect failed logins via sync.
*/
class Protect extends Module {
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'protect';
}
/**
* Initialize Protect action listeners.
*
* @access public
*
* @param callable $callback Action handler callable.
*/
public function init_listeners( $callback ) {
add_action( 'jpp_log_failed_attempt', array( $this, 'maybe_log_failed_login_attempt' ) );
add_action( 'jetpack_valid_failed_login_attempt', $callback );
}
/**
* Maybe log a failed login attempt.
*
* @access public
*
* @param array $failed_attempt Failed attempt data.
*/
public function maybe_log_failed_login_attempt( $failed_attempt ) {
$protect = \Jetpack_Protect_Module::instance();
if ( $protect->has_login_ability() && ! Jetpack_Constants::is_true( 'XMLRPC_REQUEST' ) ) {
do_action( 'jetpack_valid_failed_login_attempt', $failed_attempt );
}
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* Stats sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Heartbeat;
/**
* Class to handle sync for stats.
*/
class Stats extends Module {
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'stats';
}
/**
* Initialize stats action listeners.
*
* @access public
*
* @param callable $callback Action handler callable.
*/
public function init_listeners( $callback ) {
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.
*
* @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' ) );
}
/**
* Retrieve the stats data for the site.
*
* @access public
*
* @return array Stats data.
*/
public function add_stats() {
return array( Heartbeat::generate_stats_array() );
}
}

View File

@ -0,0 +1,244 @@
<?php
/**
* Term relationships sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Listener;
use Automattic\Jetpack\Sync\Settings;
/**
* Class to handle sync for term relationships.
*/
class Term_Relationships extends Module {
/**
* Max terms to return in one single query
*
* @access public
*
* @const int
*/
const QUERY_LIMIT = 1000;
/**
* Max value for a signed INT in MySQL - https://dev.mysql.com/doc/refman/8.0/en/integer-types.html
*
* @access public
*
* @const int
*/
const MAX_INT = 2147483647;
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'term_relationships';
}
/**
* The id field in the database.
*
* @access public
*
* @return string
*/
public function id_field() {
return 'object_id';
}
/**
* The table in the database.
*
* @access public
*
* @return string
*/
public function table_name() {
return 'term_relationships';
}
/**
* Initialize term relationships 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_term_relationships', $callable, 10, 2 );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_term_relationships', array( $this, 'expand_term_relationships' ) );
}
/**
* Enqueue the term relationships 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 object $last_object_enqueued Last object enqueued.
*
* @return array Number of actions enqueued, and next module state.
* @todo This method has similarities with Automattic\Jetpack\Sync\Modules\Module::enqueue_all_ids_as_action. Refactor to keep DRY.
* @see Automattic\Jetpack\Sync\Modules\Module::enqueue_all_ids_as_action
*/
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $last_object_enqueued ) {
global $wpdb;
$term_relationships_full_sync_item_size = Settings::get_setting( 'term_relationships_full_sync_item_size' );
$limit = min( $max_items_to_enqueue * $term_relationships_full_sync_item_size, self::QUERY_LIMIT );
$items_enqueued_count = 0;
$last_object_enqueued = $last_object_enqueued ? $last_object_enqueued : array(
'object_id' => self::MAX_INT,
'term_taxonomy_id' => self::MAX_INT,
);
while ( $limit > 0 ) {
/*
* SELECT object_id, term_taxonomy_id
* FROM $wpdb->term_relationships
* WHERE ( object_id = 11 AND term_taxonomy_id < 14 ) OR ( object_id < 11 )
* ORDER BY object_id DESC, term_taxonomy_id DESC LIMIT 1000
*/
$objects = $wpdb->get_results( $wpdb->prepare( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships WHERE ( object_id = %d AND term_taxonomy_id < %d ) OR ( object_id < %d ) ORDER BY object_id DESC, term_taxonomy_id DESC LIMIT %d", $last_object_enqueued['object_id'], $last_object_enqueued['term_taxonomy_id'], $last_object_enqueued['object_id'], $limit ), ARRAY_A );
// Request term relationships in groups of N for efficiency.
$objects_count = count( $objects );
if ( ! count( $objects ) ) {
return array( $items_enqueued_count, true );
}
$items = array_chunk( $objects, $term_relationships_full_sync_item_size );
$last_object_enqueued = $this->bulk_enqueue_full_sync_term_relationships( $items, $last_object_enqueued );
$items_enqueued_count += count( $items );
$limit = min( $limit - $objects_count, self::QUERY_LIMIT );
}
// We need to do this extra check in case $max_items_to_enqueue * $term_relationships_full_sync_item_size == relationships objects left.
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE ( object_id = %d AND term_taxonomy_id < %d ) OR ( object_id < %d ) ORDER BY object_id DESC, term_taxonomy_id DESC LIMIT %d", $last_object_enqueued['object_id'], $last_object_enqueued['term_taxonomy_id'], $last_object_enqueued['object_id'], 1 ) );
if ( 0 === (int) $count ) {
return array( $items_enqueued_count, true );
}
return array( $items_enqueued_count, $last_object_enqueued );
}
/**
* Return the initial last sent object.
*
* @return string|array initial status.
*/
public function get_initial_last_sent() {
return array(
'object_id' => self::MAX_INT,
'term_taxonomy_id' => self::MAX_INT,
);
}
/**
* Given the Module Full Sync 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|object|null
*/
public function get_next_chunk( $config, $status, $chunk_size ) {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT object_id, term_taxonomy_id
FROM $wpdb->term_relationships
WHERE ( object_id = %d AND term_taxonomy_id < %d ) OR ( object_id < %d )
ORDER BY object_id DESC, term_taxonomy_id
DESC LIMIT %d",
$status['last_sent']['object_id'],
$status['last_sent']['term_taxonomy_id'],
$status['last_sent']['object_id'],
$chunk_size
),
ARRAY_A
);
}
/**
*
* Enqueue all $items within `jetpack_full_sync_term_relationships` actions.
*
* @param array $items Groups of objects to sync.
* @param array $previous_interval_end Last item enqueued.
*
* @return array Last enqueued object.
*/
public function bulk_enqueue_full_sync_term_relationships( $items, $previous_interval_end ) {
$listener = Listener::get_instance();
$items_with_previous_interval_end = $this->get_chunks_with_preceding_end( $items, $previous_interval_end );
$listener->bulk_enqueue_full_sync_actions( 'jetpack_full_sync_term_relationships', $items_with_previous_interval_end );
$last_item = end( $items );
return end( $last_item );
}
/**
* 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
global $wpdb;
$query = "SELECT COUNT(*) FROM $wpdb->term_relationships";
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$count = $wpdb->get_var( $query );
return (int) ceil( $count / Settings::get_setting( 'term_relationships_full_sync_item_size' ) );
}
/**
* 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_term_relationships' );
}
/**
* Expand the term relationships within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The expanded hook parameters.
*/
public function expand_term_relationships( $args ) {
list( $term_relationships, $previous_end ) = $args;
return array(
'term_relationships' => $term_relationships,
'previous_end' => $previous_end,
);
}
}

View File

@ -0,0 +1,314 @@
<?php
/**
* Terms sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Defaults;
use Automattic\Jetpack\Sync\Settings;
/**
* Class to handle sync for terms.
*/
class Terms extends Module {
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'terms';
}
/**
* The id field in the database.
*
* @access public
*
* @return string
*/
public function id_field() {
return 'term_taxonomy_id';
}
/**
* The table in the database.
*
* @access public
*
* @return string
*/
public function table_name() {
return 'term_taxonomy';
}
/**
* Allows WordPress.com servers to retrieve term-related objects via the sync API.
*
* @param string $object_type The type of object.
* @param int $id The id of the object.
*
* @return bool|object A WP_Term object, or a row from term_taxonomy table depending on object type.
*/
public function get_object_by_id( $object_type, $id ) {
global $wpdb;
$object = false;
if ( 'term' === $object_type ) {
$object = get_term( (int) $id );
if ( is_wp_error( $object ) && $object->get_error_code() === 'invalid_taxonomy' ) {
// Fetch raw term.
$columns = implode( ', ', array_unique( array_merge( Defaults::$default_term_checksum_columns, array( 'term_group' ) ) ) );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$object = $wpdb->get_row( $wpdb->prepare( "SELECT $columns FROM $wpdb->terms WHERE term_id = %d", $id ) );
}
}
if ( 'term_taxonomy' === $object_type ) {
$columns = implode( ', ', array_unique( array_merge( Defaults::$default_term_taxonomy_checksum_columns, array( 'description' ) ) ) );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$object = $wpdb->get_row( $wpdb->prepare( "SELECT $columns FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $id ) );
}
if ( 'term_relationships' === $object_type ) {
$columns = implode( ', ', Defaults::$default_term_relationships_checksum_columns );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$objects = $wpdb->get_results( $wpdb->prepare( "SELECT $columns FROM $wpdb->term_relationships WHERE object_id = %d", $id ) );
$object = (object) array(
'object_id' => $id,
'relationships' => array_map( array( $this, 'expand_terms_for_relationship' ), $objects ),
);
}
return $object ? $object : false;
}
/**
* Initialize terms action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
add_action( 'created_term', array( $this, 'save_term_handler' ), 10, 3 );
add_action( 'edited_term', array( $this, 'save_term_handler' ), 10, 3 );
add_action( 'jetpack_sync_save_term', $callable );
add_action( 'jetpack_sync_add_term', $callable );
add_action( 'delete_term', $callable, 10, 4 );
add_action( 'set_object_terms', $callable, 10, 6 );
add_action( 'deleted_term_relationships', $callable, 10, 2 );
add_filter( 'jetpack_sync_before_enqueue_set_object_terms', array( $this, 'filter_set_object_terms_no_update' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_term', array( $this, 'filter_blacklisted_taxonomies' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_term', array( $this, 'filter_blacklisted_taxonomies' ) );
}
/**
* Initialize terms 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_terms', $callable, 10, 2 );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_terms', array( $this, 'expand_term_taxonomy_id' ) );
}
/**
* Enqueue the terms 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 ) {
global $wpdb;
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_terms', $wpdb->term_taxonomy, 'term_taxonomy_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
/**
* Retrieve the WHERE SQL clause based on the module config.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
*/
public function get_where_sql( $config ) {
$where_sql = Settings::get_blacklisted_taxonomies_sql();
if ( is_array( $config ) ) {
$where_sql .= ' AND term_taxonomy_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
return $where_sql;
}
/**
* 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 $wpdb->term_taxonomy";
$where_sql = $this->get_where_sql( $config );
if ( $where_sql ) {
$query .= ' WHERE ' . $where_sql;
}
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
/**
* 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_terms' );
}
/**
* Handler for creating and updating terms.
*
* @access public
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy slug.
*/
public function save_term_handler( $term_id, $tt_id, $taxonomy ) {
if ( class_exists( '\\WP_Term' ) ) {
$term_object = \WP_Term::get_instance( $term_id, $taxonomy );
} else {
$term_object = get_term_by( 'id', $term_id, $taxonomy );
}
$current_filter = current_filter();
if ( 'created_term' === $current_filter ) {
/**
* Fires when the client needs to add a new term
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param object the Term object
*/
do_action( 'jetpack_sync_add_term', $term_object );
return;
}
/**
* Fires when the client needs to update a term
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param object the Term object
*/
do_action( 'jetpack_sync_save_term', $term_object );
}
/**
* Filter blacklisted taxonomies.
*
* @access public
*
* @param array $args Hook args.
* @return array|boolean False if not whitelisted, the original hook args otherwise.
*/
public function filter_blacklisted_taxonomies( $args ) {
$term = $args[0];
if ( in_array( $term->taxonomy, Settings::get_setting( 'taxonomies_blacklist' ), true ) ) {
return false;
}
return $args;
}
/**
* Filter out set_object_terms actions where the terms have not changed.
*
* @param array $args Hook args.
* @return array|boolean False if no change in terms, the original hook args otherwise.
*/
public function filter_set_object_terms_no_update( $args ) {
// There is potential for other plugins to modify args, therefore lets validate # of and types.
// $args[2] is $tt_ids, $args[5] is $old_tt_ids see wp-includes/taxonomy.php L2740.
if ( 6 === count( $args ) && is_array( $args[2] ) && is_array( $args[5] ) ) {
if ( empty( array_diff( $args[2], $args[5] ) ) && empty( array_diff( $args[5], $args[2] ) ) ) {
return false;
}
}
return $args;
}
/**
* Expand the term taxonomy IDs to terms within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The expanded hook parameters.
*/
public function expand_term_taxonomy_id( $args ) {
list( $term_taxonomy_ids, $previous_end ) = $args;
return array(
'terms' => get_terms(
array(
'hide_empty' => false,
'term_taxonomy_id' => $term_taxonomy_ids,
'orderby' => 'term_taxonomy_id',
'order' => 'DESC',
)
),
'previous_end' => $previous_end,
);
}
/**
* Gets a term object based on a given row from the term_relationships database table.
*
* @access public
*
* @param object $relationship A row object from the term_relationships table.
* @return object|bool A term object, or false if term taxonomy doesn't exist.
*/
public function expand_terms_for_relationship( $relationship ) {
return get_term_by( 'term_taxonomy_id', $relationship->term_taxonomy_id );
}
}

View File

@ -0,0 +1,877 @@
<?php
/**
* Themes sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
/**
* Class to handle sync for themes.
*/
class Themes extends Module {
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'themes';
}
/**
* Initialize themes action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
add_action( 'switch_theme', array( $this, 'sync_theme_support' ), 10, 3 );
add_action( 'jetpack_sync_current_theme_support', $callable, 10, 2 );
add_action( 'upgrader_process_complete', array( $this, 'check_upgrader' ), 10, 2 );
add_action( 'jetpack_installed_theme', $callable, 10, 2 );
add_action( 'jetpack_updated_themes', $callable, 10, 2 );
add_filter( 'wp_redirect', array( $this, 'detect_theme_edit' ) );
add_action( 'jetpack_edited_theme', $callable, 10, 2 );
add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'theme_edit_ajax' ), 0 );
add_action( 'update_site_option_allowedthemes', array( $this, 'sync_network_allowed_themes_change' ), 10, 4 );
add_action( 'jetpack_network_disabled_themes', $callable, 10, 2 );
add_action( 'jetpack_network_enabled_themes', $callable, 10, 2 );
// Theme deletions.
add_action( 'deleted_theme', array( $this, 'detect_theme_deletion' ), 10, 2 );
add_action( 'jetpack_deleted_theme', $callable, 10, 2 );
// Sidebar updates.
add_action( 'update_option_sidebars_widgets', array( $this, 'sync_sidebar_widgets_actions' ), 10, 2 );
add_action( 'jetpack_widget_added', $callable, 10, 4 );
add_action( 'jetpack_widget_removed', $callable, 10, 4 );
add_action( 'jetpack_widget_moved_to_inactive', $callable, 10, 2 );
add_action( 'jetpack_cleared_inactive_widgets', $callable );
add_action( 'jetpack_widget_reordered', $callable, 10, 2 );
add_filter( 'widget_update_callback', array( $this, 'sync_widget_edit' ), 10, 4 );
add_action( 'jetpack_widget_edited', $callable );
}
/**
* Sync handler for a widget edit.
*
* @access public
*
* @todo Implement nonce verification
*
* @param array $instance The current widget instance's settings.
* @param array $new_instance Array of new widget settings.
* @param array $old_instance Array of old widget settings.
* @param \WP_Widget $widget_object The current widget instance.
* @return array The current widget instance's settings.
*/
public function sync_widget_edit( $instance, $new_instance, $old_instance, $widget_object ) {
if ( empty( $old_instance ) ) {
return $instance;
}
// Don't trigger sync action if this is an ajax request, because Customizer makes them during preview before saving changes.
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_POST['customized'] ) ) {
return $instance;
}
$widget = array(
'name' => $widget_object->name,
'id' => $widget_object->id,
'title' => isset( $new_instance['title'] ) ? $new_instance['title'] : '',
);
/**
* Trigger action to alert $callable sync listener that a widget was edited.
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param string $widget_name , Name of edited widget
*/
do_action( 'jetpack_widget_edited', $widget );
return $instance;
}
/**
* Sync handler for network allowed themes change.
*
* @access public
*
* @param string $option Name of the network option.
* @param mixed $value Current value of the network option.
* @param mixed $old_value Old value of the network option.
* @param int $network_id ID of the network.
*/
public function sync_network_allowed_themes_change( $option, $value, $old_value, $network_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$all_enabled_theme_slugs = array_keys( $value );
if ( count( $old_value ) > count( $value ) ) {
// Suppress jetpack_network_disabled_themes sync action when theme is deleted.
$delete_theme_call = $this->get_delete_theme_call();
if ( ! empty( $delete_theme_call ) ) {
return;
}
$newly_disabled_theme_names = array_keys( array_diff_key( $old_value, $value ) );
$newly_disabled_themes = $this->get_theme_details_for_slugs( $newly_disabled_theme_names );
/**
* Trigger action to alert $callable sync listener that network themes were disabled.
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param mixed $newly_disabled_themes, Array of info about network disabled themes
* @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes
*/
do_action( 'jetpack_network_disabled_themes', $newly_disabled_themes, $all_enabled_theme_slugs );
return;
}
$newly_enabled_theme_names = array_keys( array_diff_key( $value, $old_value ) );
$newly_enabled_themes = $this->get_theme_details_for_slugs( $newly_enabled_theme_names );
/**
* Trigger action to alert $callable sync listener that network themes were enabled
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param mixed $newly_enabled_themes , Array of info about network enabled themes
* @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes
*/
do_action( 'jetpack_network_enabled_themes', $newly_enabled_themes, $all_enabled_theme_slugs );
}
/**
* Retrieve details for one or more themes by their slugs.
*
* @access private
*
* @param array $theme_slugs Theme slugs.
* @return array Details for the themes.
*/
private function get_theme_details_for_slugs( $theme_slugs ) {
$theme_data = array();
foreach ( $theme_slugs as $slug ) {
$theme = wp_get_theme( $slug );
$theme_data[ $slug ] = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
'slug' => $slug,
);
}
return $theme_data;
}
/**
* Detect a theme edit during a redirect.
*
* @access public
*
* @param string $redirect_url Redirect URL.
* @return string Redirect URL.
*/
public function detect_theme_edit( $redirect_url ) {
$url = wp_parse_url( admin_url( $redirect_url ) );
$theme_editor_url = wp_parse_url( admin_url( 'theme-editor.php' ) );
if ( $theme_editor_url['path'] !== $url['path'] ) {
return $redirect_url;
}
$query_params = array();
wp_parse_str( $url['query'], $query_params );
if (
! isset( $_POST['newcontent'] ) ||
! isset( $query_params['file'] ) ||
! isset( $query_params['theme'] ) ||
! isset( $query_params['updated'] )
) {
return $redirect_url;
}
$theme = wp_get_theme( $query_params['theme'] );
$theme_data = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
);
/**
* Trigger action to alert $callable sync listener that a theme was edited.
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param string $query_params['theme'], Slug of edited theme
* @param string $theme_data, Information about edited them
*/
do_action( 'jetpack_edited_theme', $query_params['theme'], $theme_data );
return $redirect_url;
}
/**
* Handler for AJAX theme editing.
*
* @todo Refactor to use WP_Filesystem instead of fopen()/fclose().
*/
public function theme_edit_ajax() {
$args = wp_unslash( $_POST );
if ( empty( $args['theme'] ) ) {
return;
}
if ( empty( $args['file'] ) ) {
return;
}
$file = $args['file'];
if ( 0 !== validate_file( $file ) ) {
return;
}
if ( ! isset( $args['newcontent'] ) ) {
return;
}
if ( ! isset( $args['nonce'] ) ) {
return;
}
$stylesheet = $args['theme'];
if ( 0 !== validate_file( $stylesheet ) ) {
return;
}
if ( ! current_user_can( 'edit_themes' ) ) {
return;
}
$theme = wp_get_theme( $stylesheet );
if ( ! $theme->exists() ) {
return;
}
if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $stylesheet . '_' . $file ) ) {
return;
}
if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) {
return;
}
$editable_extensions = wp_get_theme_file_editable_extensions( $theme );
$allowed_files = array();
foreach ( $editable_extensions as $type ) {
switch ( $type ) {
case 'php':
$allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) );
break;
case 'css':
$style_files = $theme->get_files( 'css', -1 );
$allowed_files['style.css'] = $style_files['style.css'];
$allowed_files = array_merge( $allowed_files, $style_files );
break;
default:
$allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) );
break;
}
}
$real_file = $theme->get_stylesheet_directory() . '/' . $file;
if ( 0 !== validate_file( $real_file, $allowed_files ) ) {
return;
}
// Ensure file is real.
if ( ! is_file( $real_file ) ) {
return;
}
// Ensure file extension is allowed.
$extension = null;
if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) {
$extension = strtolower( $matches[1] );
if ( ! in_array( $extension, $editable_extensions, true ) ) {
return;
}
}
if ( ! is_writeable( $real_file ) ) {
return;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
$file_pointer = fopen( $real_file, 'w+' );
if ( false === $file_pointer ) {
return;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
fclose( $file_pointer );
$theme_data = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
);
/**
* This action is documented already in this file.
*/
do_action( 'jetpack_edited_theme', $stylesheet, $theme_data );
}
/**
* Detect a theme deletion.
*
* @access public
*
* @param string $stylesheet Stylesheet of the theme to delete.
* @param bool $deleted Whether the theme deletion was successful.
*/
public function detect_theme_deletion( $stylesheet, $deleted ) {
$theme = wp_get_theme( $stylesheet );
$theme_data = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
'slug' => $stylesheet,
);
if ( $deleted ) {
/**
* Signals to the sync listener that a theme was deleted and a sync action
* reflecting the deletion and theme slug should be sent
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param string $stylesheet Theme slug
* @param array $theme_data Theme info Since 5.3
*/
do_action( 'jetpack_deleted_theme', $stylesheet, $theme_data );
}
}
/**
* Handle an upgrader completion action.
*
* @access public
*
* @param \WP_Upgrader $upgrader The upgrader instance.
* @param array $details Array of bulk item update data.
*/
public function check_upgrader( $upgrader, $details ) {
if ( ! isset( $details['type'] ) ||
'theme' !== $details['type'] ||
is_wp_error( $upgrader->skin->result ) ||
! method_exists( $upgrader, 'theme_info' )
) {
return;
}
if ( 'install' === $details['action'] ) {
$theme = $upgrader->theme_info();
if ( ! $theme instanceof \WP_Theme ) {
return;
}
$theme_info = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
);
/**
* Signals to the sync listener that a theme was installed and a sync action
* reflecting the installation and the theme info should be sent
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param string $theme->theme_root Text domain of the theme
* @param mixed $theme_info Array of abbreviated theme info
*/
do_action( 'jetpack_installed_theme', $theme->stylesheet, $theme_info );
}
if ( 'update' === $details['action'] ) {
$themes = array();
if ( empty( $details['themes'] ) && isset( $details['theme'] ) ) {
$details['themes'] = array( $details['theme'] );
}
foreach ( $details['themes'] as $theme_slug ) {
$theme = wp_get_theme( $theme_slug );
if ( ! $theme instanceof \WP_Theme ) {
continue;
}
$themes[ $theme_slug ] = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
'stylesheet' => $theme->stylesheet,
);
}
if ( empty( $themes ) ) {
return;
}
/**
* Signals to the sync listener that one or more themes was updated and a sync action
* reflecting the update and the theme info should be sent
*
* @since 1.6.3
* @since-jetpack 6.2.0
*
* @param mixed $themes Array of abbreviated theme info
*/
do_action( 'jetpack_updated_themes', $themes );
}
}
/**
* Initialize themes 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_theme_data', $callable );
}
/**
* Handle a theme switch.
*
* @access public
*
* @param string $new_name Name of the new theme.
* @param \WP_Theme $new_theme The new theme.
* @param \WP_Theme $old_theme The previous theme.
*/
public function sync_theme_support( $new_name, $new_theme = null, $old_theme = null ) {
$previous_theme = $this->get_theme_info( $old_theme );
/**
* Fires when the client needs to sync theme support info
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param array the theme support array
* @param array the previous theme since Jetpack 6.5.0
*/
do_action( 'jetpack_sync_current_theme_support', $this->get_theme_info(), $previous_theme );
}
/**
* Enqueue the themes 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* Tells the client to sync all theme data to the server
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param boolean Whether to expand theme data (should always be true)
*/
do_action( 'jetpack_full_sync_theme_data', true );
// The number of actions enqueued, and next module state (true == done).
return array( 1, true );
}
/**
* Send the themes actions for full sync.
*
* @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 $state This module Full Sync status.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // 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 ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_theme_data', array( $this, 'expand_theme_data' ) );
}
/**
* 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_theme_data' );
}
/**
* Expand the theme within a hook before it is serialized and sent to the server.
*
* @access public
*
* @return array Theme data.
*/
public function expand_theme_data() {
return array( $this->get_theme_info() );
}
/**
* Retrieve the name of the widget by the widget ID.
*
* @access public
* @global $wp_registered_widgets
*
* @param string $widget_id Widget ID.
* @return string Name of the widget, or null if not found.
*/
public function get_widget_name( $widget_id ) {
global $wp_registered_widgets;
return ( isset( $wp_registered_widgets[ $widget_id ] ) ? $wp_registered_widgets[ $widget_id ]['name'] : null );
}
/**
* Retrieve the name of the sidebar by the sidebar ID.
*
* @access public
* @global $wp_registered_sidebars
*
* @param string $sidebar_id Sidebar ID.
* @return string Name of the sidebar, or null if not found.
*/
public function get_sidebar_name( $sidebar_id ) {
global $wp_registered_sidebars;
return ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ? $wp_registered_sidebars[ $sidebar_id ]['name'] : null );
}
/**
* Sync addition of widgets to a sidebar.
*
* @access public
*
* @param array $new_widgets New widgets.
* @param array $old_widgets Old widgets.
* @param string $sidebar Sidebar ID.
* @return array All widgets that have been moved to the sidebar.
*/
public function sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar ) {
$added_widgets = array_diff( $new_widgets, $old_widgets );
if ( empty( $added_widgets ) ) {
return array();
}
$moved_to_sidebar = array();
$sidebar_name = $this->get_sidebar_name( $sidebar );
// Don't sync jetpack_widget_added if theme was switched.
if ( $this->is_theme_switch() ) {
return array();
}
foreach ( $added_widgets as $added_widget ) {
$moved_to_sidebar[] = $added_widget;
$added_widget_name = $this->get_widget_name( $added_widget );
/**
* Helps Sync log that a widget got added
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param string $sidebar, Sidebar id got changed
* @param string $added_widget, Widget id got added
* @param string $sidebar_name, Sidebar id got changed Since 5.0.0
* @param string $added_widget_name, Widget id got added Since 5.0.0
*/
do_action( 'jetpack_widget_added', $sidebar, $added_widget, $sidebar_name, $added_widget_name );
}
return $moved_to_sidebar;
}
/**
* Sync removal of widgets from a sidebar.
*
* @access public
*
* @param array $new_widgets New widgets.
* @param array $old_widgets Old widgets.
* @param string $sidebar Sidebar ID.
* @param array $inactive_widgets Current inactive widgets.
* @return array All widgets that have been moved to inactive.
*/
public function sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $inactive_widgets ) {
$removed_widgets = array_diff( $old_widgets, $new_widgets );
if ( empty( $removed_widgets ) ) {
return array();
}
$moved_to_inactive = array();
$sidebar_name = $this->get_sidebar_name( $sidebar );
foreach ( $removed_widgets as $removed_widget ) {
// Lets check if we didn't move the widget to in_active_widgets.
if ( isset( $inactive_widgets ) && ! in_array( $removed_widget, $inactive_widgets, true ) ) {
$removed_widget_name = $this->get_widget_name( $removed_widget );
/**
* Helps Sync log that a widgte got removed
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param string $sidebar, Sidebar id got changed
* @param string $removed_widget, Widget id got removed
* @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0
* @param string $removed_widget_name, Name of the widget that got removed Since 5.0.0
*/
do_action( 'jetpack_widget_removed', $sidebar, $removed_widget, $sidebar_name, $removed_widget_name );
} else {
$moved_to_inactive[] = $removed_widget;
}
}
return $moved_to_inactive;
}
/**
* Sync a reorder of widgets within a sidebar.
*
* @access public
*
* @todo Refactor serialize() to a json_encode().
*
* @param array $new_widgets New widgets.
* @param array $old_widgets Old widgets.
* @param string $sidebar Sidebar ID.
*/
public function sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar ) {
$added_widgets = array_diff( $new_widgets, $old_widgets );
if ( ! empty( $added_widgets ) ) {
return;
}
$removed_widgets = array_diff( $old_widgets, $new_widgets );
if ( ! empty( $removed_widgets ) ) {
return;
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
if ( serialize( $old_widgets ) !== serialize( $new_widgets ) ) {
$sidebar_name = $this->get_sidebar_name( $sidebar );
/**
* Helps Sync log that a sidebar id got reordered
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param string $sidebar, Sidebar id got changed
* @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0
*/
do_action( 'jetpack_widget_reordered', $sidebar, $sidebar_name );
}
}
/**
* Handle the update of the sidebars and widgets mapping option.
*
* @access public
*
* @param mixed $old_value The old option value.
* @param mixed $new_value The new option value.
*/
public function sync_sidebar_widgets_actions( $old_value, $new_value ) {
// Don't really know how to deal with different array_values yet.
if (
( isset( $old_value['array_version'] ) && 3 !== $old_value['array_version'] ) ||
( isset( $new_value['array_version'] ) && 3 !== $new_value['array_version'] )
) {
return;
}
$moved_to_inactive_ids = array();
$moved_to_sidebar = array();
foreach ( $new_value as $sidebar => $new_widgets ) {
if ( in_array( $sidebar, array( 'array_version', 'wp_inactive_widgets' ), true ) ) {
continue;
}
$old_widgets = isset( $old_value[ $sidebar ] )
? $old_value[ $sidebar ]
: array();
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_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 );
$moved_to_sidebar = array_merge( $moved_to_sidebar, $moved_to_sidebar_recently );
$this->sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar );
}
// Don't sync either jetpack_widget_moved_to_inactive or jetpack_cleared_inactive_widgets if theme was switched.
if ( $this->is_theme_switch() ) {
return;
}
// Treat inactive sidebar a bit differently.
if ( ! empty( $moved_to_inactive_ids ) ) {
$moved_to_inactive_name = array_map( array( $this, 'get_widget_name' ), $moved_to_inactive_ids );
/**
* Helps Sync log that a widgets IDs got moved to in active
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param array $moved_to_inactive_ids, Array of widgets id that moved to inactive id got changed
* @param array $moved_to_inactive_names, Array of widgets names that moved to inactive id got changed Since 5.0.0
*/
do_action( 'jetpack_widget_moved_to_inactive', $moved_to_inactive_ids, $moved_to_inactive_name );
} elseif ( empty( $moved_to_sidebar ) && empty( $new_value['wp_inactive_widgets'] ) && ! empty( $old_value['wp_inactive_widgets'] ) ) {
/**
* Helps Sync log that a got cleared from inactive.
*
* @since 1.6.3
* @since-jetpack 4.9.0
*/
do_action( 'jetpack_cleared_inactive_widgets' );
}
}
/**
* Retrieve the theme data for the current or a specific theme.
*
* @access private
*
* @param \WP_Theme $theme Theme object. Optional, will default to the current theme.
*
* @return array Theme data.
*/
private function get_theme_info( $theme = null ) {
$theme_support = array();
// We are trying to get the current theme info.
if ( null === $theme ) {
$theme = wp_get_theme();
}
$theme_support['name'] = $theme->get( 'Name' );
$theme_support['version'] = $theme->get( 'Version' );
$theme_support['slug'] = $theme->get_stylesheet();
$theme_support['uri'] = $theme->get( 'ThemeURI' );
return $theme_support;
}
/**
* Whether we've deleted a theme in the current request.
*
* @access private
*
* @return boolean True if this is a theme deletion request, false otherwise.
*/
private function get_delete_theme_call() {
// Intentional usage of `debug_backtrace()` for production needs.
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$backtrace = debug_backtrace();
$delete_theme_call = null;
foreach ( $backtrace as $call ) {
if ( isset( $call['function'] ) && 'delete_theme' === $call['function'] ) {
$delete_theme_call = $call;
break;
}
}
return $delete_theme_call;
}
/**
* Whether we've switched to another theme in the current request.
*
* @access private
*
* @return boolean True if this is a theme switch request, false otherwise.
*/
private function is_theme_switch() {
return did_action( 'after_switch_theme' );
}
/**
* Return Total number of objects.
*
* @param array $config Full Sync config.
*
* @return int total
*/
public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
}
/**
* Retrieve a set of constants by their IDs.
*
* @access public
*
* @param string $object_type Object type.
* @param array $ids Object IDs.
* @return array Array of objects.
*/
public function get_objects_by_id( $object_type, $ids ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( 'theme-info' !== $object_type ) {
return array();
}
return array( $this->get_theme_info() );
}
}

View File

@ -0,0 +1,585 @@
<?php
/**
* Updates sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
/**
* Class to handle sync for updates.
*/
class Updates extends Module {
/**
* Name of the updates checksum option.
*
* @var string
*/
const UPDATES_CHECKSUM_OPTION_NAME = 'jetpack_updates_sync_checksum';
/**
* WordPress Version.
*
* @access private
*
* @var string
*/
private $old_wp_version = null;
/**
* The current updates.
*
* @access private
*
* @var array
*/
private $updates = array();
/**
* Set module defaults.
*
* @access public
*/
public function set_defaults() {
$this->updates = array();
}
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'updates';
}
/**
* Initialize updates action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
global $wp_version;
$this->old_wp_version = $wp_version;
add_action( 'set_site_transient_update_plugins', array( $this, 'validate_update_change' ), 10, 3 );
add_action( 'set_site_transient_update_themes', array( $this, 'validate_update_change' ), 10, 3 );
add_action( 'set_site_transient_update_core', array( $this, 'validate_update_change' ), 10, 3 );
add_action( 'jetpack_update_plugins_change', $callable );
add_action( 'jetpack_update_themes_change', $callable );
add_action( 'jetpack_update_core_change', $callable );
add_filter(
'jetpack_sync_before_enqueue_jetpack_update_plugins_change',
array(
$this,
'filter_update_keys',
),
10,
2
);
add_filter(
'jetpack_sync_before_enqueue_upgrader_process_complete',
array(
$this,
'filter_upgrader_process_complete',
),
10,
2
);
add_action( 'automatic_updates_complete', $callable );
if ( is_multisite() ) {
add_filter( 'pre_update_site_option_wpmu_upgrade_site', array( $this, 'update_core_network_event' ), 10, 2 );
add_action( 'jetpack_sync_core_update_network', $callable, 10, 3 );
}
// Send data when update completes.
add_action( '_core_updated_successfully', array( $this, 'update_core' ) );
add_action( 'jetpack_sync_core_reinstalled_successfully', $callable );
add_action( 'jetpack_sync_core_autoupdated_successfully', $callable, 10, 2 );
add_action( 'jetpack_sync_core_updated_successfully', $callable, 10, 2 );
}
/**
* Initialize updates 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_updates', $callable );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_updates', array( $this, 'expand_updates' ) );
add_filter( 'jetpack_sync_before_send_jetpack_update_themes_change', array( $this, 'expand_themes' ) );
}
/**
* Handle a core network update.
*
* @access public
*
* @param int $wp_db_version Current version of the WordPress database.
* @param int $old_wp_db_version Old version of the WordPress database.
* @return int Current version of the WordPress database.
*/
public function update_core_network_event( $wp_db_version, $old_wp_db_version ) {
global $wp_version;
/**
* Sync event for when core wp network updates to a new db version
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param int $wp_db_version the latest wp_db_version
* @param int $old_wp_db_version previous wp_db_version
* @param string $wp_version the latest wp_version
*/
do_action( 'jetpack_sync_core_update_network', $wp_db_version, $old_wp_db_version, $wp_version );
return $wp_db_version;
}
/**
* Handle a core update.
*
* @access public
*
* @todo Implement nonce or refactor to use `admin_post_{$action}` hooks instead.
*
* @param string $new_wp_version The new WP core version.
*/
public function update_core( $new_wp_version ) {
global $pagenow;
// // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['action'] ) && 'do-core-reinstall' === $_GET['action'] ) {
/**
* Sync event that fires when core reinstall was successful
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param string $new_wp_version the updated WordPress version
*/
do_action( 'jetpack_sync_core_reinstalled_successfully', $new_wp_version );
return;
}
// Core was autoupdated.
if (
'update-core.php' !== $pagenow &&
! Jetpack_Constants::is_true( 'REST_API_REQUEST' ) // WP.com rest api calls should never be marked as a core autoupdate.
) {
/**
* Sync event that fires when core autoupdate was successful
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param string $new_wp_version the updated WordPress version
* @param string $old_wp_version the previous WordPress version
*/
do_action( 'jetpack_sync_core_autoupdated_successfully', $new_wp_version, $this->old_wp_version );
return;
}
/**
* Sync event that fires when core update was successful
*
* @since 1.6.3
* @since-jetpack 5.0.0
*
* @param string $new_wp_version the updated WordPress version
* @param string $old_wp_version the previous WordPress version
*/
do_action( 'jetpack_sync_core_updated_successfully', $new_wp_version, $this->old_wp_version );
}
/**
* Retrieve the checksum for an update.
*
* @access public
*
* @param object $update The update object.
* @param string $transient The transient we're retrieving a checksum for.
* @return int The checksum.
*/
public function get_update_checksum( $update, $transient ) {
$updates = array();
$no_updated = array();
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 );
}
}
}
if ( ! empty( $update->no_update ) ) {
$no_updated = array_keys( $update->no_update );
}
if ( ! isset( $no_updated['jetpack/jetpack.php'] ) && isset( $updates['jetpack/jetpack.php'] ) ) {
return false;
}
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'] );
}
}
}
if ( ! empty( $update->checked ) ) {
$no_updated = $update->checked;
}
break;
case 'update_core':
if ( ! empty( $update->updates ) && is_array( $update->updates ) ) {
foreach ( $update->updates as $response ) {
if ( ! empty( $response->response ) && 'latest' === $response->response ) {
continue;
}
if ( ! empty( $response->response ) && isset( $response->packages->full ) ) {
$updates[] = array( $response->response => $response->packages->full );
}
}
}
if ( ! empty( $update->version_checked ) ) {
$no_updated = $update->version_checked;
}
if ( empty( $updates ) ) {
return false;
}
break;
}
if ( empty( $updates ) && empty( $no_updated ) ) {
return false;
}
return $this->get_check_sum( array( $no_updated, $updates ) );
}
/**
* Validate a change coming from an update before sending for sync.
*
* @access public
*
* @param mixed $value Site transient value.
* @param int $expiration Time until transient expiration in seconds.
* @param string $transient Transient name.
*/
public function validate_update_change( $value, $expiration, $transient ) {
$new_checksum = $this->get_update_checksum( $value, $transient );
if ( false === $new_checksum ) {
return;
}
$checksums = get_option( self::UPDATES_CHECKSUM_OPTION_NAME, array() );
if ( isset( $checksums[ $transient ] ) && $checksums[ $transient ] === $new_checksum ) {
return;
}
$checksums[ $transient ] = $new_checksum;
update_option( self::UPDATES_CHECKSUM_OPTION_NAME, $checksums );
if ( 'update_core' === $transient ) {
/**
* Trigger a change to core update that we want to sync.
*
* @since 1.6.3
* @since-jetpack 5.1.0
*
* @param array $value Contains info that tells us what needs updating.
*/
do_action( 'jetpack_update_core_change', $value );
return;
}
if ( empty( $this->updates ) ) {
// Lets add the shutdown method once and only when the updates move from empty to filled with something.
add_action( 'shutdown', array( $this, 'sync_last_event' ), 9 );
}
if ( ! isset( $this->updates[ $transient ] ) ) {
$this->updates[ $transient ] = array();
}
$this->updates[ $transient ][] = $value;
}
/**
* Sync the last update only.
*
* @access public
*/
public function sync_last_event() {
foreach ( $this->updates as $transient => $values ) {
$value = end( $values ); // Only send over the last value.
/**
* Trigger a change to a specific update that we want to sync.
* Triggers one of the following actions:
* - jetpack_{$transient}_change
* - jetpack_update_plugins_change
* - jetpack_update_themes_change
*
* @since 1.6.3
* @since-jetpack 5.1.0
*
* @param array $value Contains info that tells us what needs updating.
*/
do_action( "jetpack_{$transient}_change", $value );
}
}
/**
* Enqueue the updates 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/**
* Tells the client to sync all updates to the server
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param boolean Whether to expand updates (should always be true)
*/
do_action( 'jetpack_full_sync_updates', true );
// The number of actions enqueued, and next module state (true == done).
return array( 1, true );
}
/**
* Send the updates actions for full sync.
*
* @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 $state This module Full Sync status.
*
* @return array This module Full Sync status.
*/
public function send_full_sync_actions( $config, $send_until, $state ) { // 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 ) );
// The number of actions enqueued, and next module state (true == done).
return array( 'finished' => true );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return array Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 1;
}
/**
* 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_updates' );
}
/**
* Retrieve all updates that we're interested in.
*
* @access public
*
* @return array All updates.
*/
public function get_all_updates() {
return array(
'core' => get_site_transient( 'update_core' ),
'plugins' => get_site_transient( 'update_plugins' ),
'themes' => get_site_transient( 'update_themes' ),
);
}
/**
* Remove unnecessary keys from synced updates data.
*
* @access public
*
* @param array $args Hook arguments.
* @return array $args Hook arguments.
*/
public function filter_update_keys( $args ) {
$updates = $args[0];
if ( isset( $updates->no_update ) ) {
unset( $updates->no_update );
}
return $args;
}
/**
* Filter out upgrader object from the completed upgrader action args.
*
* @access public
*
* @param array $args Hook arguments.
* @return array $args Filtered hook arguments.
*/
public function filter_upgrader_process_complete( $args ) {
array_shift( $args );
return $args;
}
/**
* Expand the updates within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The hook parameters.
*/
public function expand_updates( $args ) {
if ( $args[0] ) {
return $this->get_all_updates();
}
return $args;
}
/**
* Expand the themes within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook parameters.
* @return array $args The hook parameters.
*/
public function expand_themes( $args ) {
if ( ! isset( $args[0], $args[0]->response ) ) {
return $args;
}
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 );
return $args;
}
foreach ( $args[0]->response as $stylesheet => &$theme_data ) {
$theme = wp_get_theme( $stylesheet );
$theme_data['name'] = $theme->name;
}
return $args;
}
/**
* Perform module cleanup.
* Deletes any transients and options that this module uses.
* Usually triggered when uninstalling the plugin.
*
* @access public
*/
public function reset_data() {
delete_option( self::UPDATES_CHECKSUM_OPTION_NAME );
}
/**
* Return Total number of objects.
*
* @param array $config Full Sync config.
*
* @return int total
*/
public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return 3;
}
/**
* Retrieve a set of updates by their IDs.
*
* @access public
*
* @param string $object_type Object type.
* @param array $ids Object IDs.
* @return array Array of objects.
*/
public function get_objects_by_id( $object_type, $ids ) {
if ( empty( $ids ) || empty( $object_type ) || 'update' !== $object_type ) {
return array();
}
$objects = array();
foreach ( (array) $ids as $id ) {
$object = $this->get_object_by_id( $object_type, $id );
if ( 'all' === $id ) {
// If all was requested it contains all updates and can simply be returned.
return $object;
}
$objects[ $id ] = $object;
}
return $objects;
}
/**
* Retrieve a update by its id.
*
* @access public
*
* @param string $object_type Type of the sync object.
* @param string $id ID of the sync object.
* @return mixed Value of Update.
*/
public function get_object_by_id( $object_type, $id ) {
if ( 'update' === $object_type ) {
// Only whitelisted constants can be returned.
if ( in_array( $id, array( 'core', 'plugins', 'themes' ), true ) ) {
return get_site_transient( 'update_' . $id );
} elseif ( 'all' === $id ) {
return $this->get_all_updates();
}
}
return false;
}
}

View File

@ -0,0 +1,871 @@
<?php
/**
* Users sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use Automattic\Jetpack\Password_Checker;
use Automattic\Jetpack\Sync\Defaults;
/**
* Class to handle sync for users.
*/
class Users extends Module {
/**
* Maximum number of users to sync initially.
*
* @var int
*/
const MAX_INITIAL_SYNC_USERS = 100;
/**
* User flags we care about.
*
* @access protected
*
* @var array
*/
protected $flags = array();
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'users';
}
/**
* The table in the database.
*
* @access public
*
* @return string
*/
public function table_name() {
return 'usermeta';
}
/**
* The id field in the database.
*
* @access public
*
* @return string
*/
public function id_field() {
return 'user_id';
}
/**
* Retrieve a user by its ID.
* This is here to support the backfill API.
*
* @access public
*
* @param string $object_type Type of the sync object.
* @param int $id ID of the sync object.
* @return \WP_User|bool Filtered \WP_User object, or false if the object is not a user.
*/
public function get_object_by_id( $object_type, $id ) {
if ( 'user' === $object_type ) {
$user = get_user_by( 'id', (int) $id );
if ( $user ) {
return $this->sanitize_user_and_expand( $user );
}
}
return false;
}
/**
* Initialize users action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
// Users.
add_action( 'user_register', array( $this, 'user_register_handler' ) );
add_action( 'profile_update', array( $this, 'save_user_handler' ), 10, 2 );
add_action( 'add_user_to_blog', array( $this, 'add_user_to_blog_handler' ) );
add_action( 'jetpack_sync_add_user', $callable, 10, 2 );
add_action( 'jetpack_sync_register_user', $callable, 10, 2 );
add_action( 'jetpack_sync_save_user', $callable, 10, 2 );
add_action( 'jetpack_sync_user_locale', $callable, 10, 2 );
add_action( 'jetpack_sync_user_locale_delete', $callable, 10, 1 );
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( 'jetpack_removed_user_from_blog', $callable, 10, 2 );
// User roles.
add_action( 'add_user_role', array( $this, 'save_user_role_handler' ), 10, 2 );
add_action( 'set_user_role', array( $this, 'save_user_role_handler' ), 10, 3 );
add_action( 'remove_user_role', array( $this, 'save_user_role_handler' ), 10, 2 );
// User capabilities.
add_action( 'added_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
add_action( 'updated_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
add_action( 'deleted_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
// User authentication.
add_filter( 'authenticate', array( $this, 'authenticate_handler' ), 1000, 3 );
add_action( 'wp_login', array( $this, 'wp_login_handler' ), 10, 2 );
add_action( 'jetpack_wp_login', $callable, 10, 3 );
add_action( 'wp_logout', $callable, 10, 0 );
add_action( 'wp_masterbar_logout', $callable, 10, 1 );
// Add on init.
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_user', array( $this, 'expand_action' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_register_user', array( $this, 'expand_action' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_user', array( $this, 'expand_action' ) );
}
/**
* Initialize users 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_users', $callable );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_wp_login', array( $this, 'expand_login_username' ), 10, 1 );
add_filter( 'jetpack_sync_before_send_wp_logout', array( $this, 'expand_logout_username' ), 10, 2 );
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_users', array( $this, 'expand_users' ) );
}
/**
* Retrieve a user by a user ID or object.
*
* @access private
*
* @param mixed $user User object or ID.
* @return \WP_User User object, or `null` if user invalid/not found.
*/
private function get_user( $user ) {
if ( is_numeric( $user ) ) {
$user = get_user_by( 'id', $user );
}
if ( $user instanceof \WP_User ) {
return $user;
}
return null;
}
/**
* Sanitize a user object.
* Removes the password from the user object because we don't want to sync it.
*
* @access public
*
* @todo Refactor `serialize`/`unserialize` to `wp_json_encode`/`wp_json_decode`.
*
* @param \WP_User $user User object.
* @return \WP_User Sanitized user object.
*/
public function sanitize_user( $user ) {
$user = $this->get_user( $user );
// This creates a new user object and stops the passing of the object by reference.
// // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize, WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
$user = unserialize( serialize( $user ) );
if ( is_object( $user ) && is_object( $user->data ) ) {
unset( $user->data->user_pass );
}
return $user;
}
/**
* Expand a particular user.
*
* @access public
*
* @param \WP_User $user User object.
* @return \WP_User Expanded user object.
*/
public function expand_user( $user ) {
if ( ! is_object( $user ) ) {
return null;
}
$user->allowed_mime_types = get_allowed_mime_types( $user );
$user->allcaps = $this->get_real_user_capabilities( $user );
// Only set the user locale if it is different from the site locale.
if ( get_locale() !== get_user_locale( $user->ID ) ) {
$user->locale = get_user_locale( $user->ID );
}
return $user;
}
/**
* Retrieve capabilities we care about for a particular user.
*
* @access public
*
* @param \WP_User $user User object.
* @return array User capabilities.
*/
public function get_real_user_capabilities( $user ) {
$user_capabilities = array();
if ( is_wp_error( $user ) ) {
return $user_capabilities;
}
foreach ( Defaults::get_capabilities_whitelist() as $capability ) {
if ( user_can( $user, $capability ) ) {
$user_capabilities[ $capability ] = true;
}
}
return $user_capabilities;
}
/**
* Retrieve, expand and sanitize a user.
* Can be directly used in the sync user action handlers.
*
* @access public
*
* @param mixed $user User ID or user object.
* @return \WP_User Expanded and sanitized user object.
*/
public function sanitize_user_and_expand( $user ) {
$user = $this->get_user( $user );
$user = $this->expand_user( $user );
return $this->sanitize_user( $user );
}
/**
* Expand the user within a hook before it is serialized and sent to the server.
*
* @access public
*
* @param array $args The hook arguments.
* @return array $args The hook arguments.
*/
public function expand_action( $args ) {
// The first argument is always the user.
list( $user ) = $args;
if ( $user ) {
$args[0] = $this->sanitize_user_and_expand( $user );
return $args;
}
return false;
}
/**
* Expand the user username at login before being sent to the server.
*
* @access public
*
* @param array $args The hook arguments.
* @return array $args Expanded hook arguments.
*/
public function expand_login_username( $args ) {
list( $login, $user, $flags ) = $args;
$user = $this->sanitize_user( $user );
return array( $login, $user, $flags );
}
/**
* Expand the user username at logout before being sent to the server.
*
* @access public
*
* @param array $args The hook arguments.
* @param int $user_id ID of the user.
* @return array $args Expanded hook arguments.
*/
public function expand_logout_username( $args, $user_id ) {
$user = get_userdata( $user_id );
$user = $this->sanitize_user( $user );
$login = '';
if ( is_object( $user ) && is_object( $user->data ) ) {
$login = $user->data->user_login;
}
// If we don't have a user here lets not send anything.
if ( empty( $login ) ) {
return false;
}
return array( $login, $user );
}
/**
* Additional processing is needed for wp_login so we introduce this wrapper handler.
*
* @access public
*
* @param string $user_login The user login.
* @param \WP_User $user The user object.
*/
public function wp_login_handler( $user_login, $user ) {
/**
* Fires when a user is logged into a site.
*
* @since 1.6.3
* @since-jetpack 7.2.0
*
* @param int $user_id The user ID.
* @param \WP_User $user The User Object of the user that currently logged in.
* @param array $params Any Flags that have been added during login.
*/
do_action( 'jetpack_wp_login', $user->ID, $user, $this->get_flags( $user->ID ) );
$this->clear_flags( $user->ID );
}
/**
* A hook for the authenticate event that checks the password strength.
*
* @access public
*
* @param \WP_Error|\WP_User $user The user object, or an error.
* @param string $username The username.
* @param string $password The password used to authenticate.
* @return \WP_Error|\WP_User the same object that was passed into the function.
*/
public function authenticate_handler( $user, $username, $password ) {
// In case of cookie authentication we don't do anything here.
if ( empty( $password ) ) {
return $user;
}
// We are only interested in successful authentication events.
if ( is_wp_error( $user ) || ! ( $user instanceof \WP_User ) ) {
return $user;
}
$password_checker = new Password_Checker( $user->ID );
$test_results = $password_checker->test( $password, true );
// If the password passes tests, we don't do anything.
if ( empty( $test_results['test_results']['failed'] ) ) {
return $user;
}
$this->add_flags(
$user->ID,
array(
'warning' => 'The password failed at least one strength test.',
'failures' => $test_results['test_results']['failed'],
)
);
return $user;
}
/**
* Handler for after the user is deleted.
*
* @access public
*
* @param int $deleted_user_id ID of the deleted user.
* @param int $reassigned_user_id ID of the user the deleted user's posts are reassigned to (if any).
*/
public function deleted_user_handler( $deleted_user_id, $reassigned_user_id = '' ) {
$is_multisite = is_multisite();
/**
* Fires when a user is deleted on a site
*
* @since 1.6.3
* @since-jetpack 5.4.0
*
* @param int $deleted_user_id - ID of the deleted user.
* @param int $reassigned_user_id - ID of the user the deleted user's posts are reassigned to (if any).
* @param bool $is_multisite - Whether this site is a multisite installation.
*/
do_action( 'jetpack_deleted_user', $deleted_user_id, $reassigned_user_id, $is_multisite );
}
/**
* Handler for user registration.
*
* @access public
*
* @param int $user_id ID of the deleted user.
*/
public function user_register_handler( $user_id ) {
// Ensure we only sync users who are members of the current blog.
if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
return;
}
if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
$this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
}
/**
* Fires when a new user is registered on a site
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param object The WP_User object
*/
do_action( 'jetpack_sync_register_user', $user_id, $this->get_flags( $user_id ) );
$this->clear_flags( $user_id );
}
/**
* Handler for user addition to the current blog.
*
* @access public
*
* @param int $user_id ID of the user.
*/
public function add_user_to_blog_handler( $user_id ) {
// Ensure we only sync users who are members of the current blog.
if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
return;
}
if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
$this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
}
/**
* Fires when a user is added on a site
*
* @since 1.6.3
* @since-jetpack 4.9.0
*
* @param object The WP_User object
*/
do_action( 'jetpack_sync_add_user', $user_id, $this->get_flags( $user_id ) );
$this->clear_flags( $user_id );
}
/**
* Handler for user save.
*
* @access public
*
* @param int $user_id ID of the user.
* @param \WP_User $old_user_data User object before the changes.
*/
public function save_user_handler( $user_id, $old_user_data = null ) {
// Ensure we only sync users who are members of the current blog.
if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
return;
}
$user = get_user_by( 'id', $user_id );
// Older versions of WP don't pass the old_user_data in ->data.
if ( isset( $old_user_data->data ) ) {
$old_user = $old_user_data->data;
} else {
$old_user = $old_user_data;
}
if ( null !== $old_user && $user->user_pass !== $old_user->user_pass ) {
$this->flags[ $user_id ]['password_changed'] = true;
}
if ( null !== $old_user && $user->data->user_email !== $old_user->user_email ) {
/**
* The '_new_email' user meta is deleted right after the call to wp_update_user
* that got us to this point so if it's still set then this was a user confirming
* their new email address.
*/
if ( 1 === (int) get_user_meta( $user->ID, '_new_email', true ) ) {
$this->flags[ $user_id ]['email_changed'] = true;
}
}
/**
* Fires when the client needs to sync an updated user.
*
* @since 1.6.3
* @since-jetpack 4.2.0
*
* @param \WP_User The WP_User object
* @param array State - New since 5.8.0
*/
do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
$this->clear_flags( $user_id );
}
/**
* Handler for user role change.
*
* @access public
*
* @param int $user_id ID of the user.
* @param string $role New user role.
* @param array $old_roles Previous user roles.
*/
public function save_user_role_handler( $user_id, $role, $old_roles = null ) {
$this->add_flags(
$user_id,
array(
'role_changed' => true,
'previous_role' => $old_roles,
)
);
// 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() ) {
return;
}
/**
* This action is documented already in this file
*/
do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
$this->clear_flags( $user_id );
}
/**
* Retrieve current flags for a particular user.
*
* @access public
*
* @param int $user_id ID of the user.
* @return array Current flags of the user.
*/
public function get_flags( $user_id ) {
if ( isset( $this->flags[ $user_id ] ) ) {
return $this->flags[ $user_id ];
}
return array();
}
/**
* Clear the flags of a particular user.
*
* @access public
*
* @param int $user_id ID of the user.
*/
public function clear_flags( $user_id ) {
if ( isset( $this->flags[ $user_id ] ) ) {
unset( $this->flags[ $user_id ] );
}
}
/**
* Add flags to a particular user.
*
* @access public
*
* @param int $user_id ID of the user.
* @param array $flags New flags to add for the user.
*/
public function add_flags( $user_id, $flags ) {
$this->flags[ $user_id ] = wp_parse_args( $flags, $this->get_flags( $user_id ) );
}
/**
* Save the user meta, if we're interested in it.
* Also uses the time to add flags for the user.
*
* @access public
*
* @param int $meta_id ID of the meta object.
* @param int $user_id ID of the user.
* @param string $meta_key Meta key.
* @param mixed $value Meta value.
*/
public function maybe_save_user_meta( $meta_id, $user_id, $meta_key, $value ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( 'locale' === $meta_key ) {
$this->add_flags( $user_id, array( 'locale_changed' => true ) );
}
$user = get_user_by( 'id', $user_id );
if ( isset( $user->cap_key ) && $meta_key === $user->cap_key ) {
$this->add_flags( $user_id, array( 'capabilities_changed' => true ) );
}
if ( $this->is_create_user() || $this->is_add_user_to_blog() || $this->is_delete_user() ) {
return;
}
if ( isset( $this->flags[ $user_id ] ) ) {
/**
* This action is documented already in this file
*/
do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
}
}
/**
* Enqueue the users 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 ) {
global $wpdb;
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_users', $wpdb->usermeta, 'user_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @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.
*/
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM $wpdb->usermeta";
$where_sql = $this->get_where_sql( $config );
if ( $where_sql ) {
$query .= ' WHERE ' . $where_sql;
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
/**
* Retrieve the WHERE SQL clause based on the module config.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
*/
public function get_where_sql( $config ) {
global $wpdb;
$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 ) ) {
$query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
return $query;
}
/**
* 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_users' );
}
/**
* Retrieve initial sync user config.
*
* @access public
*
* @todo Refactor the SQL query to call $wpdb->prepare() before execution.
*
* @return array|boolean IDs of users to initially sync, or false if tbe number of users exceed the maximum.
*/
public function get_initial_sync_user_config() {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$user_ids = $wpdb->get_col( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = '{$wpdb->prefix}user_level' AND meta_value > 0 LIMIT " . ( self::MAX_INITIAL_SYNC_USERS + 1 ) );
if ( count( $user_ids ) <= self::MAX_INITIAL_SYNC_USERS ) {
return $user_ids;
} else {
return false;
}
}
/**
* Expand the users within a hook before they are serialized and sent to the server.
*
* @access public
*
* @param array $args The hook arguments.
* @return array $args The hook arguments.
*/
public function expand_users( $args ) {
list( $user_ids, $previous_end ) = $args;
return array(
'users' => array_map(
array( $this, 'sanitize_user_and_expand' ),
get_users(
array(
'include' => $user_ids,
'orderby' => 'ID',
'order' => 'DESC',
)
)
),
'previous_end' => $previous_end,
);
}
/**
* Handler for user removal from a particular blog.
*
* @access public
*
* @param int $user_id ID of the user.
* @param int $blog_id ID of the blog.
*/
public function remove_user_from_blog_handler( $user_id, $blog_id ) { // 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();
// 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.
/**
* Fires when a user is removed from a blog on a multisite installation
*
* @since 1.6.3
* @since-jetpack 5.4.0
*
* @param int $user_id - ID of the removed user
* @param int $reassigned_user_id - ID of the user the removed user's posts are reassigned to (if any).
*/
do_action( 'jetpack_removed_user_from_blog', $user_id, $reassigned_user_id );
}
/**
* Whether we're adding a new user to a blog in this request.
*
* @access protected
*
* @return boolean
*/
protected function is_add_new_user_to_blog() {
return $this->is_function_in_backtrace( 'add_new_user_to_blog' );
}
/**
* Whether we're adding an existing user to a blog in this request.
*
* @access protected
*
* @return boolean
*/
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(
'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).
*
* @return int ID of the user that got reassigned as the author of the posts.
*/
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];
}
}
return false;
}
/**
* Checks if one or more function names is in debug_backtrace.
*
* @access protected
*
* @param array|string $names Mixed string name of function or array of string names of functions.
* @return bool
*/
protected function is_function_in_backtrace( $names ) {
$backtrace = debug_backtrace( false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
if ( ! is_array( $names ) ) {
$names = array( $names );
}
$names_as_keys = array_flip( $names );
// Do check in constant O(1) time for PHP5.5+.
if ( function_exists( 'array_column' ) ) {
$backtrace_functions = array_column( $backtrace, 'function' ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_columnFound
$backtrace_functions_as_keys = array_flip( $backtrace_functions );
$intersection = array_intersect_key( $backtrace_functions_as_keys, $names_as_keys );
return ! empty( $intersection );
}
// Do check in linear O(n) time for < PHP5.5 ( using isset at least prevents O(n^2) ).
foreach ( $backtrace as $call ) {
if ( isset( $names_as_keys[ $call['function'] ] ) ) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,613 @@
<?php
/**
* WooCommerce sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use WP_Error;
/**
* Class to handle sync for WooCommerce.
*/
class WooCommerce extends Module {
/**
* Whitelist for order item meta we are interested to sync.
*
* @access private
*
* @var array
*/
public static $order_item_meta_whitelist = array(
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-product-store.php#L20 .
'_product_id',
'_variation_id',
'_qty',
// Tax ones also included in below class
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-fee-data-store.php#L20 .
'_tax_class',
'_tax_status',
'_line_subtotal',
'_line_subtotal_tax',
'_line_total',
'_line_tax',
'_line_tax_data',
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-shipping-data-store.php#L20 .
'method_id',
'cost',
'total_tax',
'taxes',
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-tax-data-store.php#L20 .
'rate_id',
'label',
'compound',
'tax_amount',
'shipping_tax_amount',
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-coupon-data-store.php .
'discount_amount',
'discount_amount_tax',
);
/**
* Name of the order item database table.
*
* @access private
*
* @var string
*/
private $order_item_table_name;
/**
* The table in the database.
*
* @access public
*
* @return string
*/
public function table_name() {
return $this->order_item_table_name;
}
/**
* Constructor.
*
* @global $wpdb
*
* @todo Should we refactor this to use $this->set_defaults() instead?
*/
public function __construct() {
global $wpdb;
$this->order_item_table_name = $wpdb->prefix . 'woocommerce_order_items';
// Options, constants and post meta whitelists.
add_filter( 'jetpack_sync_options_whitelist', array( $this, 'add_woocommerce_options_whitelist' ), 10 );
add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_woocommerce_constants_whitelist' ), 10 );
add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_meta_whitelist' ), 10 );
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.
add_filter( 'jetpack_sync_prevent_sending_comment_data', array( $this, 'filter_action_scheduler_comments' ), 10, 2 );
}
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'woocommerce';
}
/**
* Initialize WooCommerce action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
// Attributes.
add_action( 'woocommerce_attribute_added', $callable, 10, 2 );
add_action( 'woocommerce_attribute_updated', $callable, 10, 3 );
add_action( 'woocommerce_attribute_deleted', $callable, 10, 3 );
// Orders.
add_action( 'woocommerce_new_order', $callable, 10, 1 );
add_action( 'woocommerce_order_status_changed', $callable, 10, 3 );
add_action( 'woocommerce_payment_complete', $callable, 10, 1 );
// 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 );
$this->init_listeners_for_meta_type( 'order_item', $callable );
// Payment tokens.
add_action( 'woocommerce_new_payment_token', $callable, 10, 1 );
add_action( 'woocommerce_payment_token_deleted', $callable, 10, 2 );
add_action( 'woocommerce_payment_token_updated', $callable, 10, 1 );
$this->init_listeners_for_meta_type( 'payment_token', $callable );
// Product downloads.
add_action( 'woocommerce_downloadable_product_download_log_insert', $callable, 10, 1 );
add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 );
// Tax rates.
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 );
// Webhooks.
add_action( 'woocommerce_new_webhook', $callable, 10, 1 );
add_action( 'woocommerce_webhook_deleted', $callable, 10, 2 );
add_action( 'woocommerce_webhook_updated', $callable, 10, 1 );
}
/**
* Initialize WooCommerce 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_order_items', $callable ); // Also sends post meta.
}
/**
* 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_order_items' );
}
/**
* 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_order_items', array( $this, 'expand_order_item_ids' ) );
}
/**
* Expand the order items properly.
*
* @access public
*
* @param array $args The hook arguments.
* @return array $args The hook arguments.
*/
public function filter_order_item( $args ) {
// Make sure we always have all the data - prior to WooCommerce 3.0 we only have the user supplied data in the second argument and not the full details.
$args[1] = $this->build_order_item( $args[0] );
return $args;
}
/**
* Expand order item IDs to order items and their meta.
*
* @access public
*
* @todo Refactor table name to use a $wpdb->prepare placeholder.
*
* @param array $args The hook arguments.
* @return array $args Expanded order items with meta.
*/
public function expand_order_item_ids( $args ) {
$order_item_ids = $args[0];
global $wpdb;
$order_item_ids_sql = implode( ', ', array_map( 'intval', $order_item_ids ) );
$order_items = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT * FROM $this->order_item_table_name WHERE order_item_id IN ( $order_item_ids_sql )"
);
return array(
$order_items,
$this->get_metadata( $order_item_ids, 'order_item', static::$order_item_meta_whitelist ),
);
}
/**
* Extract the full order item from the database by its ID.
*
* @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 ) );
}
/**
* Enqueue the WooCommerce 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_order_items', $this->order_item_table_name, 'order_item_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @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.
*/
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM $this->order_item_table_name WHERE " . $this->get_where_sql( $config );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
/**
* Retrieve the WHERE SQL clause based on the module config.
*
* @access private
*
* @param array $config Full sync configuration for this sync module.
* @return string WHERE SQL clause.
*/
public function get_where_sql( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return '1=1';
}
/**
* Add WooCommerce options to the options whitelist.
*
* @param array $list Existing options whitelist.
* @return array Updated options whitelist.
*/
public function add_woocommerce_options_whitelist( $list ) {
return array_merge( $list, self::$wc_options_whitelist );
}
/**
* Add WooCommerce constants to the constants whitelist.
*
* @param array $list Existing constants whitelist.
* @return array Updated constants whitelist.
*/
public function add_woocommerce_constants_whitelist( $list ) {
return array_merge( $list, self::$wc_constants_whitelist );
}
/**
* Add WooCommerce post meta to the post meta whitelist.
*
* @param array $list Existing post meta whitelist.
* @return array Updated post meta whitelist.
*/
public function add_woocommerce_post_meta_whitelist( $list ) {
return array_merge( $list, self::$wc_post_meta_whitelist );
}
/**
* Add WooCommerce comment meta to the comment meta whitelist.
*
* @param array $list Existing comment meta whitelist.
* @return array Updated comment meta whitelist.
*/
public function add_woocommerce_comment_meta_whitelist( $list ) {
return array_merge( $list, self::$wc_comment_meta_whitelist );
}
/**
* Adds 'revew' to the list of comment types so Sync will listen for status changes on 'reviews'.
*
* @access public
*
* @param array $comment_types The list of comment types prior to this filter.
* return array The list of comment types with 'review' added.
*/
public function add_review_comment_types( $comment_types ) {
if ( is_array( $comment_types ) ) {
$comment_types[] = 'review';
}
return $comment_types;
}
/**
* Stop comments from the Action Scheduler from being synced.
* https://github.com/woocommerce/woocommerce/tree/e7762627c37ec1f7590e6cac4218ba0c6a20024d/includes/libraries/action-scheduler
*
* @since 1.6.3
* @since-jetpack 7.7.0
*
* @param boolean $can_sync Should we prevent comment data from bing synced to WordPress.com.
* @param mixed $comment WP_COMMENT object.
*
* @return bool
*/
public function filter_action_scheduler_comments( $can_sync, $comment ) {
if ( isset( $comment->comment_agent ) && 'ActionScheduler' === $comment->comment_agent ) {
return true;
}
return $can_sync;
}
/**
* Whitelist for options we are interested to sync.
*
* @access private
* @static
*
* @var array
*/
private static $wc_options_whitelist = array(
'woocommerce_currency',
'woocommerce_db_version',
'woocommerce_weight_unit',
'woocommerce_version',
'woocommerce_unforce_ssl_checkout',
'woocommerce_tax_total_display',
'woocommerce_tax_round_at_subtotal',
'woocommerce_tax_display_shop',
'woocommerce_tax_display_cart',
'woocommerce_prices_include_tax',
'woocommerce_price_thousand_sep',
'woocommerce_price_num_decimals',
'woocommerce_price_decimal_sep',
'woocommerce_notify_low_stock',
'woocommerce_notify_low_stock_amount',
'woocommerce_notify_no_stock',
'woocommerce_notify_no_stock_amount',
'woocommerce_manage_stock',
'woocommerce_force_ssl_checkout',
'woocommerce_hide_out_of_stock_items',
'woocommerce_file_download_method',
'woocommerce_enable_signup_and_login_from_checkout',
'woocommerce_enable_shipping_calc',
'woocommerce_enable_review_rating',
'woocommerce_enable_guest_checkout',
'woocommerce_enable_coupons',
'woocommerce_enable_checkout_login_reminder',
'woocommerce_enable_ajax_add_to_cart',
'woocommerce_dimension_unit',
'woocommerce_default_country',
'woocommerce_default_customer_address',
'woocommerce_currency_pos',
'woocommerce_api_enabled',
'woocommerce_allow_tracking',
'woocommerce_task_list_hidden',
'woocommerce_onboarding_profile',
);
/**
* Whitelist for constants we are interested to sync.
*
* @access private
* @static
*
* @var array
*/
private static $wc_constants_whitelist = array(
// WooCommerce constants.
'WC_PLUGIN_FILE',
'WC_ABSPATH',
'WC_PLUGIN_BASENAME',
'WC_VERSION',
'WOOCOMMERCE_VERSION',
'WC_ROUNDING_PRECISION',
'WC_DISCOUNT_ROUNDING_MODE',
'WC_TAX_ROUNDING_MODE',
'WC_DELIMITER',
'WC_LOG_DIR',
'WC_SESSION_CACHE_GROUP',
'WC_TEMPLATE_DEBUG_MODE',
);
/**
* Whitelist for post meta we are interested to sync.
*
* @access private
* @static
*
* @var array
*/
private static $wc_post_meta_whitelist = array(
// WooCommerce products.
// See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-product-data-store-cpt.php#L21 .
'_visibility',
'_sku',
'_price',
'_regular_price',
'_sale_price',
'_sale_price_dates_from',
'_sale_price_dates_to',
'total_sales',
'_tax_status',
'_tax_class',
'_manage_stock',
'_backorders',
'_sold_individually',
'_weight',
'_length',
'_width',
'_height',
'_upsell_ids',
'_crosssell_ids',
'_purchase_note',
'_default_attributes',
'_product_attributes',
'_virtual',
'_downloadable',
'_download_limit',
'_download_expiry',
'_featured',
'_downloadable_files',
'_wc_rating_count',
'_wc_average_rating',
'_wc_review_count',
'_variation_description',
'_thumbnail_id',
'_file_paths',
'_product_image_gallery',
'_product_version',
'_wp_old_slug',
// Woocommerce orders.
// See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L27 .
'_order_key',
'_order_currency',
// '_billing_first_name', do not sync these as they contain personal data
// '_billing_last_name',
// '_billing_company',
// '_billing_address_1',
// '_billing_address_2',
'_billing_city',
'_billing_state',
'_billing_postcode',
'_billing_country',
// '_billing_email', do not sync these as they contain personal data.
// '_billing_phone',
// '_shipping_first_name',
// '_shipping_last_name',
// '_shipping_company',
// '_shipping_address_1',
// '_shipping_address_2',
'_shipping_city',
'_shipping_state',
'_shipping_postcode',
'_shipping_country',
'_completed_date',
'_paid_date',
'_cart_discount',
'_cart_discount_tax',
'_order_shipping',
'_order_shipping_tax',
'_order_tax',
'_order_total',
'_payment_method',
'_payment_method_title',
// '_transaction_id', do not sync these as they contain personal data.
// '_customer_ip_address',
// '_customer_user_agent',
'_created_via',
'_order_version',
'_prices_include_tax',
'_date_completed',
'_date_paid',
'_payment_tokens',
'_billing_address_index',
'_shipping_address_index',
'_recorded_sales',
'_recorded_coupon_usage_counts',
// See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L539 .
'_download_permissions_granted',
// See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L594 .
'_order_stock_reduced',
// Woocommerce order refunds.
// See https://github.com/woocommerce/woocommerce/blob/b8a2815ae546c836467008739e7ff5150cb08e93/includes/data-stores/class-wc-order-refund-data-store-cpt.php#L20 .
'_order_currency',
'_refund_amount',
'_refunded_by',
'_refund_reason',
'_order_shipping',
'_order_shipping_tax',
'_order_tax',
'_order_total',
'_order_version',
'_prices_include_tax',
'_payment_tokens',
);
/**
* Whitelist for comment meta we are interested to sync.
*
* @access private
* @static
*
* @var array
*/
private static $wc_comment_meta_whitelist = array(
'rating',
);
/**
* 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 ) {
switch ( $object_type ) {
case 'order_item':
return $this->get_order_item_by_ids( $ids );
}
return new WP_Error( 'unsupported_object_type', 'Unsupported object type' );
}
/**
* Returns a list of order_item objects by their IDs.
*
* @param array $ids List of order_item IDs to fetch.
*
* @access public
*
* @return array|object|null
*/
public function get_order_item_by_ids( $ids ) {
global $wpdb;
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();
}
// Prepare the placeholders for the prepared query below.
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
$query = "SELECT * FROM {$this->order_item_table_name} WHERE order_item_id IN ( $placeholders )";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return $wpdb->get_results( $wpdb->prepare( $query, $ids ), ARRAY_A );
}
}

View File

@ -0,0 +1,156 @@
<?php
/**
* WP_Super_Cache sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
/**
* Class to handle sync for WP_Super_Cache.
*/
class WP_Super_Cache extends Module {
/**
* Constructor.
*
* @todo Should we refactor this to use $this->set_defaults() instead?
*/
public function __construct() {
add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_wp_super_cache_constants_whitelist' ), 10 );
add_filter( 'jetpack_sync_callable_whitelist', array( $this, 'add_wp_super_cache_callable_whitelist' ), 10 );
}
/**
* Whitelist for constants we are interested to sync.
*
* @access public
* @static
*
* @var array
*/
public static $wp_super_cache_constants = array(
'WPLOCKDOWN',
'WPSC_DISABLE_COMPRESSION',
'WPSC_DISABLE_LOCKING',
'WPSC_DISABLE_HTACCESS_UPDATE',
'ADVANCEDCACHEPROBLEM',
);
/**
* Container for the whitelist for WP_Super_Cache callables we are interested to sync.
*
* @access public
* @static
*
* @var array
*/
public static $wp_super_cache_callables = array(
'wp_super_cache_globals' => array( __CLASS__, 'get_wp_super_cache_globals' ),
);
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'wp-super-cache';
}
/**
* Retrieve all WP_Super_Cache callables we are interested to sync.
*
* @access public
*
* @global $wp_cache_mod_rewrite;
* @global $cache_enabled;
* @global $super_cache_enabled;
* @global $ossdlcdn;
* @global $cache_rebuild_files;
* @global $wp_cache_mobile;
* @global $wp_super_cache_late_init;
* @global $wp_cache_anon_only;
* @global $wp_cache_not_logged_in;
* @global $wp_cache_clear_on_post_edit;
* @global $wp_cache_mobile_enabled;
* @global $wp_super_cache_debug;
* @global $cache_max_time;
* @global $wp_cache_refresh_single_only;
* @global $wp_cache_mfunc_enabled;
* @global $wp_supercache_304;
* @global $wp_cache_no_cache_for_get;
* @global $wp_cache_mutex_disabled;
* @global $cache_jetpack;
* @global $cache_domain_mapping;
*
* @return array All WP_Super_Cache callables.
*/
public static function get_wp_super_cache_globals() {
global $wp_cache_mod_rewrite;
global $cache_enabled;
global $super_cache_enabled;
global $ossdlcdn;
global $cache_rebuild_files;
global $wp_cache_mobile;
global $wp_super_cache_late_init;
global $wp_cache_anon_only;
global $wp_cache_not_logged_in;
global $wp_cache_clear_on_post_edit;
global $wp_cache_mobile_enabled;
global $wp_super_cache_debug;
global $cache_max_time;
global $wp_cache_refresh_single_only;
global $wp_cache_mfunc_enabled;
global $wp_supercache_304;
global $wp_cache_no_cache_for_get;
global $wp_cache_mutex_disabled;
global $cache_jetpack;
global $cache_domain_mapping;
return array(
'wp_cache_mod_rewrite' => $wp_cache_mod_rewrite,
'cache_enabled' => $cache_enabled,
'super_cache_enabled' => $super_cache_enabled,
'ossdlcdn' => $ossdlcdn,
'cache_rebuild_files' => $cache_rebuild_files,
'wp_cache_mobile' => $wp_cache_mobile,
'wp_super_cache_late_init' => $wp_super_cache_late_init,
'wp_cache_anon_only' => $wp_cache_anon_only,
'wp_cache_not_logged_in' => $wp_cache_not_logged_in,
'wp_cache_clear_on_post_edit' => $wp_cache_clear_on_post_edit,
'wp_cache_mobile_enabled' => $wp_cache_mobile_enabled,
'wp_super_cache_debug' => $wp_super_cache_debug,
'cache_max_time' => $cache_max_time,
'wp_cache_refresh_single_only' => $wp_cache_refresh_single_only,
'wp_cache_mfunc_enabled' => $wp_cache_mfunc_enabled,
'wp_supercache_304' => $wp_supercache_304,
'wp_cache_no_cache_for_get' => $wp_cache_no_cache_for_get,
'wp_cache_mutex_disabled' => $wp_cache_mutex_disabled,
'cache_jetpack' => $cache_jetpack,
'cache_domain_mapping' => $cache_domain_mapping,
);
}
/**
* Add WP_Super_Cache constants to the constants whitelist.
*
* @param array $list Existing constants whitelist.
* @return array Updated constants whitelist.
*/
public function add_wp_super_cache_constants_whitelist( $list ) {
return array_merge( $list, self::$wp_super_cache_constants );
}
/**
* Add WP_Super_Cache callables to the callables whitelist.
*
* @param array $list Existing callables whitelist.
* @return array Updated callables whitelist.
*/
public function add_wp_super_cache_callable_whitelist( $list ) {
return array_merge( $list, self::$wp_super_cache_callables );
}
}

View File

@ -0,0 +1,208 @@
<?php
/**
* Table Checksums Class.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Replicastore;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Sync;
use Automattic\Jetpack\Sync\Modules;
use WP_Error;
use WP_User_Query;
/**
* Class to handle Table Checksums for the User Meta table.
*/
class Table_Checksum_Usermeta extends Table_Checksum_Users {
/**
* Calculate the checksum based on provided range and filters.
*
* @param int|null $range_from The start of the range.
* @param int|null $range_to The end of the range.
* @param array|null $filter_values Additional filter values. Not used at the moment.
* @param bool $granular_result If the returned result should be granular or only the checksum.
* @param bool $simple_return_value If we want to use a simple return value for non-granular results (return only the checksum, without wrappers).
*
* @return array|mixed|object|WP_Error|null
*/
public function calculate_checksum( $range_from = null, $range_to = null, $filter_values = null, $granular_result = false, $simple_return_value = true ) {
if ( ! Sync\Settings::is_checksum_enabled() ) {
return new WP_Error( 'checksum_disabled', 'Checksums are currently disabled.' );
}
/**
* First we need to fetch the user IDs for the users that we want to include in the range.
*
* To keep things a bit simple and avoid filtering issues, let's reuse the `build_filter_statement` that already
* exists. Unfortunately we don't
*/
global $wpdb;
// This call depends on the `range_field` pointing to the `ID` field of the `users` table. Currently, "ID".
$range_filter_statement = $this->build_filter_statement( $range_from, $range_to );
$query = "
SELECT
DISTINCT {$this->table}.{$this->range_field}
FROM
{$this->table}
JOIN {$wpdb->usermeta} as um_table ON um_table.user_id = {$this->table}.ID
WHERE
{$range_filter_statement}
AND um_table.meta_key = '{$wpdb->prefix}user_level'
AND um_table.meta_value > 0
";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$user_ids = $wpdb->get_col( $query );
// Chunk the array down to make sure we don't overload the database with queries that are too large.
$chunked_user_ids = array_chunk( $user_ids, 500 );
$checksum_entries = array();
foreach ( $chunked_user_ids as $user_ids_chunk ) {
$user_objects = $this->get_user_objects_by_ids( $user_ids_chunk );
foreach ( $user_objects as $user_object ) {
// expand and sanitize desired meta based on WP.com logic.
$user_object = $this->expand_and_sanitize_user_meta( $user_object );
// Generate checksum entry based on the serialized value if not empty.
$checksum_entry = 0;
if ( ! empty( $user_object->roles ) ) {
$checksum_entry = crc32( implode( '#', array( $this->salt, 'roles', maybe_serialize( $user_object->roles ) ) ) );
}
// Meta only persisted if user is connected to WP.com.
if ( ( new Manager( 'jetpack' ) )->is_user_connected( $user_object->ID ) ) {
if ( ! empty( $user_object->allcaps ) ) {
$checksum_entry += crc32(
implode(
'#',
array(
$this->salt,
'capabilities',
maybe_serialize( $user_object->allcaps ),
)
)
);
}
// Explicitly check that locale is not same as site locale.
if ( ! empty( $user_object->locale ) && get_locale() !== $user_object->locale ) {
$checksum_entry += crc32(
implode(
'#',
array(
$this->salt,
'locale',
maybe_serialize( $user_object->locale ),
)
)
);
}
if ( ! empty( $user_object->allowed_mime_types ) ) {
$checksum_entry += crc32(
implode(
'#',
array(
$this->salt,
'allowed_mime_types',
maybe_serialize( $user_object->allowed_mime_types ),
)
)
);
}
}
$checksum_entries[ $user_object->ID ] = '' . $checksum_entry;
}
}
// Non-granular results need only to sum the different entries.
if ( ! $granular_result ) {
$checksum_sum = 0;
foreach ( $checksum_entries as $entry ) {
$checksum_sum += intval( $entry );
}
if ( $simple_return_value ) {
return '' . $checksum_sum;
}
return array(
'range' => $range_from . '-' . $range_to,
'checksum' => '' . $checksum_sum,
);
}
// Granular results.
$response = $checksum_entries;
// Sort the return value for easier comparisons and code flows further down the line.
ksort( $response );
return $response;
}
/**
* Expand the User Object with additional meta santized by WP.com logic.
*
* @param mixed $user_object User Object from WP_User_Query.
*
* @return mixed $user_object expanded User Object.
*/
protected function expand_and_sanitize_user_meta( $user_object ) {
$user_module = Modules::get_module( 'users' );
// Expand User Objects based on Sync logic.
$user_object = $user_module->expand_user( $user_object );
// Sanitize location.
if ( ! empty( $user_object->locale ) ) {
$user_object->locale = wp_strip_all_tags( $user_object->locale, true );
}
// Sanitize allcaps.
if ( ! empty( $user_object->allcaps ) ) {
$user_object->allcaps = array_map(
function ( $cap ) {
return (bool) $cap;
},
$user_object->allcaps
);
}
// Sanitize allowed_mime_types.
$allowed_mime_types = $user_object->allowed_mime_types;
foreach ( $allowed_mime_types as $allowed_mime_type_short => $allowed_mime_type_long ) {
$allowed_mime_type_short = wp_strip_all_tags( (string) $allowed_mime_type_short, true );
$allowed_mime_type_long = wp_strip_all_tags( (string) $allowed_mime_type_long, true );
$allowed_mime_types[ $allowed_mime_type_short ] = $allowed_mime_type_long;
}
$user_object->allowed_mime_types = $allowed_mime_types;
// Sanitize roles.
if ( is_array( $user_object->roles ) ) {
$user_object->roles = array_map( 'sanitize_text_field', $user_object->roles );
}
return $user_object;
}
/**
* Gets a list of `WP_User` objects by their IDs
*
* @param array $ids List of IDs to fetch.
*
* @return array
*/
protected function get_user_objects_by_ids( $ids ) {
$user_query = new WP_User_Query( array( 'include' => $ids ) );
return $user_query->get_results();
}
}

View File

@ -0,0 +1,184 @@
<?php
/**
* Table Checksums Class.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Replicastore;
/**
* Class to handle Table Checksums for the Users table.
*/
class Table_Checksum_Users extends Table_Checksum {
/**
* Returns the checksum query. All validation of fields and configurations are expected to occur prior to usage.
*
* @param int|null $range_from The start of the range.
* @param int|null $range_to The end of the range.
* @param array|null $filter_values Additional filter values. Not used at the moment.
* @param bool $granular_result If the function should return a granular result.
*
* @return string
*
* @throws Exception Throws an exception if validation fails in the internal function calls.
*/
protected function build_checksum_query( $range_from = null, $range_to = null, $filter_values = null, $granular_result = false ) {
global $wpdb;
// Escape the salt.
$salt = $wpdb->prepare( '%s', $this->salt );
// Prepare the compound key.
$key_fields = array();
// Prefix the fields with the table name, to avoid clashes in queries with sub-queries (e.g. meta tables).
foreach ( $this->key_fields as $field ) {
$key_fields[] = $this->table . '.' . $field;
}
$key_fields = implode( ',', $key_fields );
// Prepare the checksum fields.
$checksum_fields = array();
// Prefix the fields with the table name, to avoid clashes in queries with sub-queries (e.g. meta tables).
foreach ( $this->checksum_fields as $field ) {
$checksum_fields[] = $this->table . '.' . $field;
}
// Apply latin1 conversion if enabled.
if ( $this->perform_text_conversion ) {
// Convert text fields to allow for encoding discrepancies as WP.com is latin1.
foreach ( $this->checksum_text_fields as $field ) {
$checksum_fields[] = 'CONVERT(' . $this->table . '.' . $field . ' using latin1 )';
}
} else {
// Conversion disabled, default to table prefixing.
foreach ( $this->checksum_text_fields as $field ) {
$checksum_fields[] = $this->table . '.' . $field;
}
}
$checksum_fields_string = implode( ',', array_merge( $checksum_fields, array( $salt ) ) );
$additional_fields = '';
if ( $granular_result ) {
// TODO uniq the fields as sometimes(most) range_index is the key and there's no need to select the same field twice.
$additional_fields = "
{$this->table}.{$this->range_field} as range_index,
{$key_fields},
";
}
$filter_stamenet = $this->build_filter_statement( $range_from, $range_to, $filter_values );
// usermeta join to limit on user_level.
$join_statement = "JOIN {$wpdb->usermeta} as um_table ON um_table.user_id = {$this->table}.ID";
$query = "
SELECT
{$additional_fields}
SUM(
CRC32(
CONCAT_WS( '#', {$salt}, {$checksum_fields_string} )
)
) AS checksum
FROM
{$this->table}
{$join_statement}
WHERE
{$filter_stamenet}
AND um_table.meta_key = '{$wpdb->prefix}user_level'
AND um_table.meta_value > 0
";
/**
* We need the GROUP BY only for compound keys.
*/
if ( $granular_result ) {
$query .= "
GROUP BY {$key_fields}
LIMIT 9999999
";
}
return $query;
}
/**
* Obtain the min-max values (edges) of the range.
*
* @param int|null $range_from The start of the range.
* @param int|null $range_to The end of the range.
* @param int|null $limit How many values to return.
*
* @return array|object|void
* @throws Exception Throws an exception if validation fails on the internal function calls.
*/
public function get_range_edges( $range_from = null, $range_to = null, $limit = null ) {
global $wpdb;
$this->validate_fields( array( $this->range_field ) );
// `trim()` to make sure we don't add the statement if it's empty.
$filters = trim( $this->build_filter_statement( $range_from, $range_to ) );
$filter_statement = '';
if ( ! empty( $filters ) ) {
$filter_statement = "
JOIN {$wpdb->usermeta} as um_table ON um_table.user_id = {$this->table}.ID
WHERE
{$filters}
AND um_table.meta_key = '{$wpdb->prefix}user_level'
AND um_table.meta_value > 0
";
}
$query = "
SELECT
MIN({$this->range_field}) as min_range,
MAX({$this->range_field}) as max_range,
COUNT( {$this->range_field} ) as item_count
FROM
";
/**
* If `$limit` is not specified, we can directly use the table.
*/
if ( ! $limit ) {
$query .= "
{$this->table}
{$filter_statement}
";
} else {
/**
* If there is `$limit` specified, we can't directly use `MIN/MAX()` as they don't work with `LIMIT`.
* That's why we will alter the query for this case.
*/
$limit = intval( $limit );
$query .= "
(
SELECT
{$this->range_field}
FROM
{$this->table}
{$filter_statement}
ORDER BY
{$this->range_field} ASC
LIMIT {$limit}
) as ids_query
";
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->get_row( $query, ARRAY_A );
if ( ! $result || ! is_array( $result ) ) {
throw new Exception( 'Unable to get range edges' );
}
return $result;
}
}

View File

@ -0,0 +1,856 @@
<?php
/**
* Table Checksums Class.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Replicastore;
use Automattic\Jetpack\Sync;
use Exception;
use WP_Error;
// TODO add rest endpoints to work with this, hopefully in the same folder.
/**
* Class to handle Table Checksums.
*/
class Table_Checksum {
/**
* Table to be checksummed.
*
* @var string
*/
public $table = '';
/**
* Table Checksum Configuration.
*
* @var array
*/
public $table_configuration = array();
/**
* Perform Text Conversion to latin1.
*
* @var boolean
*/
protected $perform_text_conversion = false;
/**
* Field to be used for range queries.
*
* @var string
*/
public $range_field = '';
/**
* ID Field(s) to be used.
*
* @var array
*/
public $key_fields = array();
/**
* Field(s) to be used in generating the checksum value.
*
* @var array
*/
public $checksum_fields = array();
/**
* Field(s) to be used in generating the checksum value that need latin1 conversion.
*
* @var array
*/
public $checksum_text_fields = array();
/**
* Default filter values for the table
*
* @var array
*/
public $filter_values = array();
/**
* SQL Query to be used to filter results (allow/disallow).
*
* @var string
*/
public $additional_filter_sql = '';
/**
* Default Checksum Table Configurations.
*
* @var array
*/
public $default_tables = array();
/**
* Salt to be used when generating checksum.
*
* @var string
*/
public $salt = '';
/**
* Tables which are allowed to be checksummed.
*
* @var string
*/
public $allowed_tables = array();
/**
* If the table has a "parent" table that it's related to.
*
* @var mixed|null
*/
protected $parent_table = null;
/**
* What field to use for the parent table join, if it has a "parent" table.
*
* @var mixed|null
*/
protected $parent_join_field = null;
/**
* What field to use for the table join, if it has a "parent" table.
*
* @var mixed|null
*/
protected $table_join_field = null;
/**
* Some tables might not exist on the remote, and we want to verify they exist, before trying to query them.
*
* @var callable
*/
protected $is_table_enabled_callback = false;
/**
* Table_Checksum constructor.
*
* @param string $table The table to calculate checksums for.
* @param string $salt Optional salt to add to the checksum.
* @param boolean $perform_text_conversion If text fields should be latin1 converted.
*
* @throws Exception Throws exception from inner functions.
*/
public function __construct( $table, $salt = null, $perform_text_conversion = false ) {
if ( ! Sync\Settings::is_checksum_enabled() ) {
throw new Exception( 'Checksums are currently disabled.' );
}
$this->salt = $salt;
$this->default_tables = $this->get_default_tables();
$this->perform_text_conversion = $perform_text_conversion;
// TODO change filters to allow the array format.
// TODO add get_fields or similar method to get things out of the table.
// TODO extract this configuration in a better way, still make it work with `$wpdb` names.
// TODO take over the replicastore functions and move them over to this class.
// TODO make the API work.
$this->allowed_tables = apply_filters( 'jetpack_sync_checksum_allowed_tables', $this->default_tables );
$this->table = $this->validate_table_name( $table );
$this->table_configuration = $this->allowed_tables[ $table ];
$this->prepare_fields( $this->table_configuration );
// Run any callbacks to check if a table is enabled or not.
if (
is_callable( $this->is_table_enabled_callback )
&& ! call_user_func( $this->is_table_enabled_callback, $table )
) {
throw new Exception( "Unable to use table name: $table" );
}
}
/**
* Get Default Table configurations.
*
* @return array
*/
protected function get_default_tables() {
global $wpdb;
return array(
'posts' => array(
'table' => $wpdb->posts,
'range_field' => 'ID',
'key_fields' => array( 'ID' ),
'checksum_fields' => array( 'post_modified_gmt' ),
'filter_values' => Sync\Settings::get_disallowed_post_types_structured(),
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'posts' );
},
),
'postmeta' => array(
'table' => $wpdb->postmeta,
'range_field' => 'post_id',
'key_fields' => array( 'post_id', 'meta_key' ),
'checksum_text_fields' => array( 'meta_key', 'meta_value' ),
'filter_values' => Sync\Settings::get_allowed_post_meta_structured(),
'parent_table' => 'posts',
'parent_join_field' => 'ID',
'table_join_field' => 'post_id',
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'posts' );
},
),
'comments' => array(
'table' => $wpdb->comments,
'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' )
),
),
'comment_approved' => array(
'operator' => 'NOT IN',
'values' => array( 'spam' ),
),
),
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'comments' );
},
),
'commentmeta' => array(
'table' => $wpdb->commentmeta,
'range_field' => 'comment_id',
'key_fields' => array( 'comment_id', 'meta_key' ),
'checksum_text_fields' => array( 'meta_key', 'meta_value' ),
'filter_values' => Sync\Settings::get_allowed_comment_meta_structured(),
'parent_table' => 'comments',
'parent_join_field' => 'comment_ID',
'table_join_field' => 'comment_id',
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'comments' );
},
),
'terms' => array(
'table' => $wpdb->terms,
'range_field' => 'term_id',
'key_fields' => array( 'term_id' ),
'checksum_fields' => array( 'term_id' ),
'checksum_text_fields' => array( 'name', 'slug' ),
'parent_table' => 'term_taxonomy',
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'terms' );
},
),
'termmeta' => array(
'table' => $wpdb->termmeta,
'range_field' => 'term_id',
'key_fields' => array( 'term_id', 'meta_key' ),
'checksum_text_fields' => array( 'meta_key', 'meta_value' ),
'parent_table' => 'term_taxonomy',
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'terms' );
},
),
'term_relationships' => array(
'table' => $wpdb->term_relationships,
'range_field' => 'object_id',
'key_fields' => array( 'object_id' ),
'checksum_fields' => array( 'object_id', 'term_taxonomy_id' ),
'parent_table' => 'term_taxonomy',
'parent_join_field' => 'term_taxonomy_id',
'table_join_field' => 'term_taxonomy_id',
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'terms' );
},
),
'term_taxonomy' => array(
'table' => $wpdb->term_taxonomy,
'range_field' => 'term_taxonomy_id',
'key_fields' => array( 'term_taxonomy_id' ),
'checksum_fields' => array( 'term_taxonomy_id', 'term_id', 'parent' ),
'checksum_text_fields' => array( 'taxonomy', 'description' ),
'filter_values' => Sync\Settings::get_allowed_taxonomies_structured(),
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'terms' );
},
),
'links' => $wpdb->links, // TODO describe in the array format or add exceptions.
'options' => $wpdb->options, // TODO describe in the array format or add exceptions.
'woocommerce_order_items' => array(
'table' => "{$wpdb->prefix}woocommerce_order_items",
'range_field' => 'order_item_id',
'key_fields' => array( 'order_item_id' ),
'checksum_fields' => array( 'order_id' ),
'checksum_text_fields' => array( 'order_item_name', 'order_item_type' ),
'is_table_enabled_callback' => array( $this, 'enable_woocommerce_tables' ),
),
'woocommerce_order_itemmeta' => array(
'table' => "{$wpdb->prefix}woocommerce_order_itemmeta",
'range_field' => 'order_item_id',
'key_fields' => array( 'order_item_id', 'meta_key' ),
'checksum_text_fields' => array( 'meta_key', 'meta_value' ),
'filter_values' => Sync\Settings::get_allowed_order_itemmeta_structured(),
'parent_table' => 'woocommerce_order_items',
'parent_join_field' => 'order_item_id',
'table_join_field' => 'order_item_id',
'is_table_enabled_callback' => array( $this, 'enable_woocommerce_tables' ),
),
'users' => array(
'table' => $wpdb->users,
'range_field' => 'ID',
'key_fields' => array( 'ID' ),
'checksum_text_fields' => array( 'user_login', 'user_nicename', 'user_email', 'user_url', 'user_registered', 'user_status', 'display_name' ),
'filter_values' => array(),
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'users' );
},
),
/**
* Usermeta is a special table, as it needs to use a custom override flow,
* as the user roles, capabilities, locale, mime types can be filtered by plugins.
* This prevents us from doing a direct comparison in the database.
*/
'usermeta' => array(
'table' => $wpdb->users,
/**
* Range field points to ID, which in this case is the `WP_User` ID,
* since we're querying the whole WP_User objects, instead of meta entries in the DB.
*/
'range_field' => 'ID',
'key_fields' => array(),
'checksum_fields' => array(),
'is_table_enabled_callback' => function () {
return false !== Sync\Modules::get_module( 'users' );
},
),
);
}
/**
* Prepare field params based off provided configuration.
*
* @param array $table_configuration The table configuration array.
*/
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->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;
}
/**
* Verify provided table name is valid for checksum processing.
*
* @param string $table Table name to validate.
*
* @return mixed|string
* @throws Exception Throw an exception on validation failure.
*/
protected function validate_table_name( $table ) {
if ( empty( $table ) ) {
throw new Exception( 'Invalid table name: empty' );
}
if ( ! array_key_exists( $table, $this->allowed_tables ) ) {
throw new Exception( "Invalid table name: $table not allowed" );
}
return $this->allowed_tables[ $table ]['table'];
}
/**
* Verify provided fields are proper names.
*
* @param array $fields Array of field names to validate.
*
* @throws Exception Throw an exception on failure to validate.
*/
protected function validate_fields( $fields ) {
foreach ( $fields as $field ) {
if ( ! preg_match( '/^[0-9,a-z,A-Z$_]+$/i', $field ) ) {
throw new Exception( "Invalid field name: $field is not allowed" );
}
// TODO other verifications of the field names.
}
}
/**
* Verify the fields exist in the table.
*
* @param array $fields Array of fields to validate.
*
* @return bool
* @throws Exception Throw an exception on failure to validate.
*/
protected function validate_fields_against_table( $fields ) {
global $wpdb;
$valid_fields = array();
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$result = $wpdb->get_results( "SHOW COLUMNS FROM {$this->table}", ARRAY_A );
foreach ( $result as $result_row ) {
$valid_fields[] = $result_row['Field'];
}
// Check if the fields are actually contained in the table.
foreach ( $fields as $field_to_check ) {
if ( ! in_array( $field_to_check, $valid_fields, true ) ) {
throw new Exception( "Invalid field name: field '{$field_to_check}' doesn't exist in table {$this->table}" );
}
}
return true;
}
/**
* Verify the configured fields.
*
* @throws Exception Throw an exception on failure to validate in the internal functions.
*/
protected function validate_input() {
$fields = array_merge( array( $this->range_field ), $this->key_fields, $this->checksum_fields, $this->checksum_text_fields );
$this->validate_fields( $fields );
$this->validate_fields_against_table( $fields );
}
/**
* Prepare filter values as SQL statements to be added to the other filters.
*
* @param array $filter_values The filter values array.
* @param string $table_prefix If the values are going to be used in a sub-query, add a prefix with the table alias.
*
* @return array|null
*/
protected function prepare_filter_values_as_sql( $filter_values = array(), $table_prefix = '' ) {
global $wpdb;
if ( ! is_array( $filter_values ) ) {
return null;
}
$result = array();
foreach ( $filter_values as $field => $filter ) {
$key = ( ! empty( $table_prefix ) ? $table_prefix : $this->table ) . '.' . $field;
switch ( $filter['operator'] ) {
case 'IN':
case 'NOT IN':
$values_placeholders = implode( ',', array_fill( 0, count( $filter['values'] ), '%s' ) );
$statement = "{$key} {$filter['operator']} ( $values_placeholders )";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$prepared_statement = $wpdb->prepare( $statement, $filter['values'] );
$result[] = $prepared_statement;
break;
}
}
return $result;
}
/**
* Build the filter query baased off range fields and values and the additional sql.
*
* @param int|null $range_from Start of the range.
* @param int|null $range_to End of the range.
* @param array|null $filter_values Additional filter values. Not used at the moment.
* @param string $table_prefix Table name to be prefixed to the columns. Used in sub-queries where columns can clash.
*
* @return string
*/
public function build_filter_statement( $range_from = null, $range_to = null, $filter_values = null, $table_prefix = '' ) {
global $wpdb;
// If there is a field prefix that we want to use with table aliases.
$parent_prefix = ( ! empty( $table_prefix ) ? $table_prefix : $this->table ) . '.';
/**
* Prepare the ranges.
*/
$filter_array = array( '1 = 1' );
if ( null !== $range_from ) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$filter_array[] = $wpdb->prepare( "{$parent_prefix}{$this->range_field} >= %d", array( intval( $range_from ) ) );
}
if ( null !== $range_to ) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$filter_array[] = $wpdb->prepare( "{$parent_prefix}{$this->range_field} <= %d", array( intval( $range_to ) ) );
}
/**
* End prepare the ranges.
*/
/**
* Prepare data filters.
*/
// Default filters.
if ( $this->filter_values ) {
$prepared_values_statements = $this->prepare_filter_values_as_sql( $this->filter_values, $table_prefix );
if ( $prepared_values_statements ) {
$filter_array = array_merge( $filter_array, $prepared_values_statements );
}
}
// Additional filters.
if ( ! empty( $filter_values ) ) {
// Prepare filtering.
$prepared_values_statements = $this->prepare_filter_values_as_sql( $filter_values, $table_prefix );
if ( $prepared_values_statements ) {
$filter_array = array_merge( $filter_array, $prepared_values_statements );
}
}
// Add any additional filters via direct SQL statement.
// Currently used only because we haven't converted all filtering to happen via `filter_values`.
// This SQL is NOT prefixed and column clashes can occur when used in sub-queries.
if ( $this->additional_filter_sql ) {
$filter_array[] = $this->additional_filter_sql;
}
/**
* End prepare data filters.
*/
return implode( ' AND ', $filter_array );
}
/**
* Returns the checksum query. All validation of fields and configurations are expected to occur prior to usage.
*
* @param int|null $range_from The start of the range.
* @param int|null $range_to The end of the range.
* @param array|null $filter_values Additional filter values. Not used at the moment.
* @param bool $granular_result If the function should return a granular result.
*
* @return string
*
* @throws Exception Throws an exception if validation fails in the internal function calls.
*/
protected function build_checksum_query( $range_from = null, $range_to = null, $filter_values = null, $granular_result = false ) {
global $wpdb;
// Escape the salt.
$salt = $wpdb->prepare( '%s', $this->salt );
// Prepare the compound key.
$key_fields = array();
// Prefix the fields with the table name, to avoid clashes in queries with sub-queries (e.g. meta tables).
foreach ( $this->key_fields as $field ) {
$key_fields[] = $this->table . '.' . $field;
}
$key_fields = implode( ',', $key_fields );
// Prepare the checksum fields.
$checksum_fields = array();
// Prefix the fields with the table name, to avoid clashes in queries with sub-queries (e.g. meta tables).
foreach ( $this->checksum_fields as $field ) {
$checksum_fields[] = $this->table . '.' . $field;
}
// Apply latin1 conversion if enabled.
if ( $this->perform_text_conversion ) {
// Convert text fields to allow for encoding discrepancies as WP.com is latin1.
foreach ( $this->checksum_text_fields as $field ) {
$checksum_fields[] = 'CONVERT(' . $this->table . '.' . $field . ' using latin1 )';
}
} else {
// Conversion disabled, default to table prefixing.
foreach ( $this->checksum_text_fields as $field ) {
$checksum_fields[] = $this->table . '.' . $field;
}
}
$checksum_fields_string = implode( ',', array_merge( $checksum_fields, array( $salt ) ) );
$additional_fields = '';
if ( $granular_result ) {
// TODO uniq the fields as sometimes(most) range_index is the key and there's no need to select the same field twice.
$additional_fields = "
{$this->table}.{$this->range_field} as range_index,
{$key_fields},
";
}
$filter_stamenet = $this->build_filter_statement( $range_from, $range_to, $filter_values );
$join_statement = '';
if ( $this->parent_table ) {
$parent_table_obj = new Table_Checksum( $this->parent_table );
$parent_filter_query = $parent_table_obj->build_filter_statement( null, null, null, 'parent_table' );
// It is possible to have the GROUP By cause multiple rows to be returned for the same row for term_taxonomy.
// To get distinct entries we use a correlatd subquery back on the parent table using the primary key.
$additional_unique_clause = '';
if ( 'term_taxonomy' === $this->parent_table ) {
$additional_unique_clause = "
AND parent_table.{$parent_table_obj->range_field} = (
SELECT min( parent_table_cs.{$parent_table_obj->range_field} )
FROM {$parent_table_obj->table} as parent_table_cs
WHERE parent_table_cs.{$this->parent_join_field} = {$this->table}.{$this->table_join_field}
)
";
}
$join_statement = "
INNER JOIN {$parent_table_obj->table} as parent_table
ON (
{$this->table}.{$this->table_join_field} = parent_table.{$this->parent_join_field}
AND {$parent_filter_query}
$additional_unique_clause
)
";
}
$query = "
SELECT
{$additional_fields}
SUM(
CRC32(
CONCAT_WS( '#', {$salt}, {$checksum_fields_string} )
)
) AS checksum
FROM
{$this->table}
{$join_statement}
WHERE
{$filter_stamenet}
";
/**
* We need the GROUP BY only for compound keys.
*/
if ( $granular_result ) {
$query .= "
GROUP BY {$key_fields}
LIMIT 9999999
";
}
return $query;
}
/**
* Obtain the min-max values (edges) of the range.
*
* @param int|null $range_from The start of the range.
* @param int|null $range_to The end of the range.
* @param int|null $limit How many values to return.
*
* @return array|object|void
* @throws Exception Throws an exception if validation fails on the internal function calls.
*/
public function get_range_edges( $range_from = null, $range_to = null, $limit = null ) {
global $wpdb;
$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.
$filter_values = $this->filter_values;
if ( 'postmeta' === $this->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 ( 'postmeta' === $this->table ) {
$this->filter_values = $filter_values;
}
$filter_statement = '';
if ( ! empty( $filters ) ) {
$filter_statement = "
WHERE
{$filters}
";
}
// Only make the distinct count when we know there can be multiple entries for the range column.
$distinct_count = '';
if ( count( $this->key_fields ) > 1 || $wpdb->terms === $this->table || $wpdb->term_relationships === $this->table ) {
$distinct_count = 'DISTINCT';
}
$query = "
SELECT
MIN({$this->range_field}) as min_range,
MAX({$this->range_field}) as max_range,
COUNT( {$distinct_count} {$this->range_field}) as item_count
FROM
";
/**
* If `$limit` is not specified, we can directly use the table.
*/
if ( ! $limit ) {
$query .= "
{$this->table}
{$filter_statement}
";
} else {
/**
* If there is `$limit` specified, we can't directly use `MIN/MAX()` as they don't work with `LIMIT`.
* That's why we will alter the query for this case.
*/
$limit = intval( $limit );
$query .= "
(
SELECT
{$distinct_count} {$this->range_field}
FROM
{$this->table}
{$filter_statement}
ORDER BY
{$this->range_field} ASC
LIMIT {$limit}
) as ids_query
";
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->get_row( $query, ARRAY_A );
if ( ! $result || ! is_array( $result ) ) {
throw new Exception( 'Unable to get range edges' );
}
return $result;
}
/**
* Update the results to have key/checksum format.
*
* @param array $results Prepare the results for output of granular results.
*/
protected function prepare_results_for_output( &$results ) {
// get the compound key.
// only return range and compound key for granular results.
$return_value = array();
foreach ( $results as &$result ) {
// Working on reference to save memory here.
$key = array();
foreach ( $this->key_fields as $field ) {
$key[] = $result[ $field ];
}
$return_value[ implode( '-', $key ) ] = $result['checksum'];
}
return $return_value;
}
/**
* Calculate the checksum based on provided range and filters.
*
* @param int|null $range_from The start of the range.
* @param int|null $range_to The end of the range.
* @param array|null $filter_values Additional filter values. Not used at the moment.
* @param bool $granular_result If the returned result should be granular or only the checksum.
* @param bool $simple_return_value If we want to use a simple return value for non-granular results (return only the checksum, without wrappers).
*
* @return array|mixed|object|WP_Error|null
*/
public function calculate_checksum( $range_from = null, $range_to = null, $filter_values = null, $granular_result = false, $simple_return_value = true ) {
if ( ! Sync\Settings::is_checksum_enabled() ) {
return new WP_Error( 'checksum_disabled', 'Checksums are currently disabled.' );
}
try {
$this->validate_input();
} catch ( Exception $ex ) {
return new WP_Error( 'invalid_input', $ex->getMessage() );
}
$query = $this->build_checksum_query( $range_from, $range_to, $filter_values, $granular_result );
global $wpdb;
if ( ! $granular_result ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->get_row( $query, ARRAY_A );
if ( ! is_array( $result ) ) {
return new WP_Error( 'invalid_query', "Result wasn't an array" );
}
if ( $simple_return_value ) {
return $result['checksum'];
}
return array(
'range' => $range_from . '-' . $range_to,
'checksum' => $result['checksum'],
);
} else {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->get_results( $query, ARRAY_A );
return $this->prepare_results_for_output( $result );
}
}
/**
* Make sure the WooCommerce tables should be enabled for Checksum/Fix.
*
* @return bool
*/
protected function enable_woocommerce_tables() {
/**
* On WordPress.com, we can't directly check if the site has support for WooCommerce.
* Having the option to override the functionality here helps with syncing WooCommerce tables.
*
* @since 10.1
*
* @param bool If we should we force-enable WooCommerce tables support.
*/
$force_woocommerce_support = apply_filters( 'jetpack_table_checksum_force_enable_woocommerce', false );
// If we're forcing WooCommerce tables support, there's no need to check further.
// This is used on WordPress.com.
if ( $force_woocommerce_support ) {
return true;
}
// No need to proceed if WooCommerce is not available.
if ( ! class_exists( 'WooCommerce' ) ) {
return false;
}
// TODO more checks if needed. Probably query the DB to make sure the tables exist.
return true;
}
}