s $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 ); } } } } /** * Add term relationships to post objects within a hook before they are serialized and sent to the server. * This is used in Full Sync Immediately * * @access public * * @param array $args The hook parameters. * @return array $args The expanded hook parameters. */ public function add_term_relationships( $args ) { list( $filtered_posts, $previous_interval_end ) = $args; list( $filtered_post_ids, $filtered_posts, $filtered_posts_metadata ) = $filtered_posts; return array( $filtered_posts, $filtered_posts_metadata, $this->get_term_relationships( $filtered_post_ids ), $previous_interval_end, ); } /** * Expand post IDs to post objects within a hook before they are serialized and sent to the server. * This is used in Legacy Full Sync * * @access public * * @param array $args The hook parameters. * @return array $args The expanded hook parameters. */ public function expand_posts_with_metadata_and_terms( $args ) { list( $post_ids, $previous_interval_end ) = $args; $posts = $this->expand_posts( $post_ids ); $posts_metadata = $this->get_metadata( $post_ids, 'post', Settings::get_setting( 'post_meta_whitelist' ) ); $term_relationships = $this->get_term_relationships( $post_ids ); return array( $posts, $posts_metadata, $term_relationships, $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 ) ); } /** * Given the Module Configuration and Status return the next chunk of items to send. * This function also expands the posts and metadata and filters them based on the maximum size constraints. * * @param array $config This module Full Sync configuration. * @param array $status This module Full Sync status. * @param int $chunk_size Chunk size. * * @return array */ public function get_next_chunk( $config, $status, $chunk_size ) { $post_ids = parent::get_next_chunk( $config, $status, $chunk_size ); if ( empty( $post_ids ) ) { return array(); } $posts = $this->expand_posts( $post_ids ); $posts_metadata = $this->get_metadata( $post_ids, 'post', Settings::get_setting( 'post_meta_whitelist' ) ); // Filter posts and metadata based on maximum size constraints. list( $filtered_post_ids, $filtered_posts, $filtered_posts_metadata ) = $this->filter_posts_and_metadata_max_size( $posts, $posts_metadata ); return array( $filtered_post_ids, $filtered_posts, $filtered_posts_metadata, ); } /** * Expand posts. * * @param array $post_ids Post IDs. * * @return array Expanded posts. */ private function expand_posts( $post_ids ) { $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 $posts; } /** * Filters posts and metadata based on maximum size constraints. * It always allows the first post with its metadata even if they exceed the limit, otherwise they will never be synced. * * @access public * * @param array $posts The array of posts to filter. * @param array $metadata The array of metadata to filter. * @return array An array containing the filtered post IDs, filtered posts, and filtered metadata. */ public function filter_posts_and_metadata_max_size( $posts, $metadata ) { $filtered_posts = array(); $filtered_metadata = array(); $filtered_post_ids = array(); $current_size = 0; foreach ( $posts as $post ) { $post_content_size = isset( $post->post_content ) ? strlen( $post->post_content ) : 0; $current_metadata = array(); $metadata_size = 0; foreach ( $metadata as $key => $metadata_item ) { if ( (int) $metadata_item->post_id === $post->ID ) { // Trimming metadata if it exceeds limit. Similar to trim_post_meta. $metadata_item_size = strlen( maybe_serialize( $metadata_item->meta_value ) ); if ( $metadata_item_size >= self::MAX_POST_META_LENGTH ) { $metadata_item->meta_value = ''; } $current_metadata[] = $metadata_item; $metadata_size += $metadata_item_size >= self::MAX_POST_META_LENGTH ? 0 : $metadata_item_size; if ( ! empty( $filtered_post_ids ) && ( $current_size + $post_content_size + $metadata_size ) > ( self::MAX_SIZE_FULL_SYNC ) ) { break 2; // Break both foreach loops. } unset( $metadata[ $key ] ); } } // Always allow the first post with its metadata. if ( empty( $filtered_post_ids ) || ( $current_size + $post_content_size + $metadata_size ) <= ( self::MAX_SIZE_FULL_SYNC ) ) { $filtered_post_ids[] = strval( $post->ID ); $filtered_posts[] = $post; $filtered_metadata = array_merge( $filtered_metadata, $current_metadata ); $current_size += $post_content_size + $metadata_size; } else { break; } } return array( $filtered_post_ids, $filtered_posts, $filtered_metadata, ); } /** * Set the status of the full sync action based on the objects that were sent. * * @access public * * @param array $status This module Full Sync status. * @param array $objects This module Full Sync objects. * * @return array The updated status. */ public function set_send_full_sync_actions_status( $status, $objects ) { $status['last_sent'] = end( $objects[0] ); $status['sent'] += count( $objects[0] ); return $status; } }