Файловый менеджер - Редактировать - /home/bean7936/perfect-community.com/442aa3/modules.tar
Назад
videopress/class.videopress-edit-attachment.php 0000644 00000035154 15174711637 0016024 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Status; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VideoPress edit attachment screen * * @since 4.1 */ class VideoPress_Edit_Attachment { /** * Singleton method to initialize the object only once. * * @return VideoPress_Edit_Attachment */ public static function init() { static $instance = null; if ( ! $instance ) { $instance = new VideoPress_Edit_Attachment(); } return $instance; } /** * VideoPress_Edit_Attachment constructor. * * Adds in appropriate actions for attachment fields editor, meta boxes and saving. */ public function __construct() { add_filter( 'attachment_fields_to_edit', array( $this, 'fields_to_edit' ), 10, 2 ); add_filter( 'attachment_fields_to_save', array( $this, 'save_fields' ), 10, 2 ); add_filter( 'wp_ajax_save-attachment', array( $this, 'save_fields' ), -1 ); add_filter( 'wp_ajax_save-attachment-compat', array( $this, 'save_fields' ), -1 ); add_action( 'add_meta_boxes', array( $this, 'configure_meta_boxes' ), 10, 2 ); } /** * Add VideoPress meta box. * * @param string $post_type Post type. * @param object $post Post object. */ public function configure_meta_boxes( $post_type = 'unknown', $post = null ) { if ( null === $post ) { $post = (object) array( 'ID' => 0 ); } if ( 'attachment' !== $post_type ) { return; } // If this has not been processed by videopress, we can skip the rest. if ( ! is_videopress_attachment( $post->ID ) ) { return; } add_meta_box( 'videopress-media-info', __( 'VideoPress Information', 'jetpack' ), array( $this, 'videopress_information_box' ), 'attachment', 'side', 'core' ); } /** * Filter attachment fields data to save. * * @param array $post Post data. * @param array|null $attachment Attachment metadata. * * @return array */ public function save_fields( $post, $attachment = null ) { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification already done by core. if ( null === $attachment && isset( $_POST['attachment'] ) ) { $attachment = filter_var( wp_unslash( $_POST['attachment'] ) ); } if ( ! isset( $attachment['is_videopress_attachment'] ) || 'yes' !== $attachment['is_videopress_attachment'] ) { return $post; } // If this has not been processed by videopress, we can skip the rest. if ( ! is_videopress_attachment( $post['ID'] ) ) { $post['errors']['videopress']['errors'][] = __( 'The media you are trying to update is not processed by VideoPress.', 'jetpack' ); return $post; } $post_title = isset( $_POST['post_title'] ) ? sanitize_text_field( wp_unslash( $_POST['post_title'] ) ) : null; $post_excerpt = isset( $_POST['post_excerpt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['post_excerpt'] ) ) : null; $rating = isset( $attachment['rating'] ) ? $attachment['rating'] : null; $display_embed = isset( $attachment['display_embed'] ) ? $attachment['display_embed'] : 0; $allow_download = isset( $attachment['allow_download'] ) ? $attachment['allow_download'] : 0; $privacy_setting = isset( $attachment['privacy_setting'] ) ? $attachment['privacy_setting'] : VIDEOPRESS_PRIVACY::SITE_DEFAULT; $result = Videopress_Attachment_Metadata::persist_metadata( $post['ID'], get_post_meta( $post['ID'], 'videopress_guid', true ), $post_title, null, // @todo: Check why we haven't sent the caption in the first place. $post_excerpt, $rating, $this->normalize_checkbox_value( $display_embed ), $this->normalize_checkbox_value( $allow_download ), $privacy_setting ); if ( is_wp_error( $result ) ) { $post['errors']['videopress']['errors'][] = $result->get_error_message(); return $post; } return $post; // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Convert the string values of a checkbox option to the format that they will be stored in db. * * @param string $value The denormalized version. * * @return int */ private function normalize_checkbox_value( $value ) { return 'on' === $value ? 1 : 0; } /** * Get the upload api path. * * @param string $guid The guid of the video. * @return string */ public function make_video_api_path( $guid ) { return sprintf( '%s/rest/v%s/videos/%s', JETPACK__WPCOM_JSON_API_BASE, Client::WPCOM_JSON_API_VERSION, $guid ); } /** * Creates an array of video fields to edit based on transcoded videos. * * @param array $fields video fields of interest. * @param stdClass $post Post object. * @return array modified version of video fields for administrative interface display */ public function fields_to_edit( $fields, $post ) { $post_id = absint( $post->ID ); $meta = wp_get_attachment_metadata( $post_id ); // If this has not been processed by videopress, we can skip the rest. if ( ! is_videopress_attachment( $post_id ) || ! isset( $meta['videopress'] ) ) { return $fields; } $info = (object) $meta['videopress']; $file_statuses = isset( $meta['file_statuses'] ) ? $meta['file_statuses'] : array(); $guid = get_post_meta( $post_id, 'videopress_guid', true ); unset( $fields['url'] ); unset( $fields['post_content'] ); // If a video isn't attached to any specific post, manually add a post ID. if ( ! isset( $info->post_id ) ) { $info->post_id = 0; } if ( isset( $file_statuses['ogg'] ) && 'done' === $file_statuses['ogg'] ) { $v_name = preg_replace( '/\.\w+/', '', basename( $info->path ) ); $video_name = $v_name . '_fmt1.ogv'; $ogg_url = videopress_cdn_file_url( $guid, $video_name ); $fields['video-ogg'] = array( 'label' => __( 'Ogg File URL', 'jetpack' ), 'input' => 'html', 'html' => "<input type='text' class='urlfield' readonly='readonly' name='attachments[$post_id][oggurl]' value='" . esc_url( $ogg_url, array( 'http', 'https' ) ) . "' />", 'helps' => __( 'Location of the Ogg video file.', 'jetpack' ), ); } $fields['post_title']['helps'] = __( 'Title will appear on the first frame of your video', 'jetpack' ); $fields['post_excerpt']['label'] = _x( 'Description', 'A header for the short description display', 'jetpack' ); $fields['post_excerpt']['input'] = 'textarea'; $fields['post_excerpt']['value'] = ! empty( $info->description ) ? $info->description : ''; $fields['is_videopress_attachment'] = array( 'input' => 'hidden', 'value' => 'yes', ); $fields['videopress_shortcode'] = array( 'label' => _x( 'Shortcode', 'A header for the shortcode display', 'jetpack' ), 'input' => 'html', 'html' => "<input type=\"text\" name=\"videopress_shortcode\" value=\"[videopress {$guid}]\" readonly=\"readonly\"/>", 'show_in_modal' => true, 'show_in_edit' => false, ); $fields['display_embed'] = array( 'label' => _x( 'Share', 'A header for the video sharing options area', 'jetpack' ), 'input' => 'html', 'html' => $this->display_embed_choice( $info ), ); $fields['allow_download'] = array( 'label' => _x( 'Download', 'A header for the video allow download option area', 'jetpack' ), 'input' => 'html', 'html' => $this->display_download_choice( $info ), ); $fields['video-rating'] = array( 'label' => _x( 'Rating', 'A header for the video rating area', 'jetpack' ), 'input' => 'html', 'html' => $this->display_rating( $info ), ); $fields['privacy_setting'] = array( 'label' => _x( 'Privacy Setting', 'A header for the video privacy setting area.', 'jetpack' ), 'input' => 'html', 'html' => $this->display_privacy_setting( $info ), ); return $fields; } /** * Meta box output. * * @param stdClass $post Post object. */ public function videopress_information_box( $post ) { $post_id = absint( $post->ID ); $guid = get_post_meta( $post_id, 'videopress_guid', true ); // If this has not been processed by videopress, we can skip the rest. if ( ! is_videopress_attachment( $post_id ) || empty( $guid ) ) { return; } $meta = wp_get_attachment_metadata( $post_id ); $info = (object) $meta['videopress']; $is_public = VIDEOPRESS_PRIVACY::IS_PUBLIC === $info->privacy_setting || ( VIDEOPRESS_PRIVACY::SITE_DEFAULT === $info->privacy_setting && ! ( new Status() )->is_private_site() ); /* Translators: %s is the video title */ $alt_text = sprintf( __( 'Poster image for video: %s', 'jetpack' ), get_the_title( $post_id ) ); ?> <p class="post-attributes-label-wrapper"> <label class="post-attributes-label" for="videopress-shortcode"><?php esc_html_e( 'Shortcode', 'jetpack' ); ?></label> </p> <input type="text" class="widefat" id="videopress-shortcode" readonly="readonly" value="<?php echo esc_attr( "[videopress $guid]" ); ?>" onclick="this.focus();this.select();" /> <p class="post-attributes-label-wrapper"> <label class="post-attributes-label"><?php esc_html_e( 'URL', 'jetpack' ); ?></label> </p> <?php printf( '<a href="%1$s">%1$s</a>', esc_url( videopress_build_url( $guid ) ) ); ?> <p class="post-attributes-label-wrapper"> <label class="post-attributes-label"><?php esc_html_e( 'Poster', 'jetpack' ); ?></label> </p> <?php if ( ! empty( $info->poster ) ) : ?> <?php if ( $is_public ) : ?> <img src="<?php echo esc_url( $info->poster ); ?>" width="100%" alt="<?php echo esc_attr( $alt_text ); ?>" /> <?php else : ?> <img id="videopress-poster-<?php echo esc_attr( $guid ); ?>" data-poster="<?php echo esc_url( $info->poster ); ?>" data-guid="<?php echo esc_attr( $guid ); ?>" width="100%" alt="<?php echo esc_attr( $alt_text ); ?>" style="display:none;" src="" /> <em class="videopress-poster-loading" data-error="<?php esc_attr_e( 'Poster unavailable.', 'jetpack' ); ?>"><?php esc_html_e( 'Loading…', 'jetpack' ); ?></em> <script> ( function() { var img = document.getElementById( 'videopress-poster-<?php echo esc_attr( $guid ); ?>' ); var loading = img ? img.nextElementSibling : null; if ( ! img || ! loading || ! window.videopressAjax ) { return; } fetch( window.videopressAjax.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: new URLSearchParams( { action: 'videopress-get-playback-jwt', guid: img.dataset.guid, post_id: window.videopressAjax.post_id || 0 } ) } ) .then( function( response ) { return response.json(); } ) .then( function( data ) { if ( data.success && data.data.jwt ) { img.src = img.dataset.poster + '?metadata_token=' + data.data.jwt; img.style.display = ''; loading.style.display = 'none'; } else { loading.textContent = loading.dataset.error; } } ) .catch( function() { loading.textContent = loading.dataset.error; } ); } )(); </script> <?php endif; ?> <?php else : ?> <em><?php esc_html_e( 'Processing…', 'jetpack' ); ?></em> <?php endif; } /** * Creates a checkbox and a label for a video option. * * @param string $id the checkbox id. * @param string $name the checkbox name. * @param string $label the label text. * @param bool $is_checked if the checkbox should be checked. * * @return string the generated HTML */ protected function create_checkbox_for_option( $id, $name, $label, $is_checked ) { $html = "<label for='$id'><input type='checkbox' name='$name' id='$id'"; if ( $is_checked ) { $html .= ' checked="checked"'; } $html .= " />$label</label>"; return $html; } /** * Build HTML to display a form checkbox for embedcode display preference * * @param object $info Database row from the videos table. * @return string Input element of type checkbox set to checked state based on stored embed preference. */ protected function display_embed_choice( $info ) { return $this->create_checkbox_for_option( "attachments-{$info->post_id}-displayembed", "attachments[{$info->post_id}][display_embed]", __( 'Display share menu and allow viewers to copy a link or embed this video', 'jetpack' ), isset( $info->display_embed ) ? $info->display_embed : 0 ); } /** * Build HTML to display a form checkbox for the "allow download" video option * * @param object $info database row from the videos table. * @return string input element of type checkbox with checked state matching the download preference */ protected function display_download_choice( $info ) { return $this->create_checkbox_for_option( "attachments-{$info->post_id}-allowdownload", "attachments[{$info->post_id}][allow_download]", __( 'Display download option and allow viewers to download this video', 'jetpack' ), isset( $info->allow_download ) && $info->allow_download ); } /** * Build HTML to display a form input radio button for video ratings * * @param object $info Database row from the videos table. * * @return string Input Elements of type radio with existing stored value selected. */ protected function display_privacy_setting( $info ) { $privacy_settings = array( VIDEOPRESS_PRIVACY::SITE_DEFAULT => __( 'Site Default', 'jetpack' ), VIDEOPRESS_PRIVACY::IS_PUBLIC => __( 'Public', 'jetpack' ), VIDEOPRESS_PRIVACY::IS_PRIVATE => __( 'Private', 'jetpack' ), ); $displayed_privacy_setting = intval( isset( $info->privacy_setting ) ? $info->privacy_setting : VIDEOPRESS_PRIVACY::SITE_DEFAULT ); $out = "<select name='attachments[{$info->post_id}][privacy_setting]'>"; foreach ( $privacy_settings as $r => $label ) { $out .= "<option value=\"$r\""; if ( $r === $displayed_privacy_setting ) { $out .= ' selected'; } $out .= ">$label</option>"; } $out .= '</select>'; return $out; } /** * Build HTML to display a form input radio button for video ratings * * @param object $info Database row from the videos table. * @return string Input elements of type radio with existing stored value selected. */ protected function display_rating( $info ) { $out = ''; $ratings = array( 'G' => 'G', 'PG-13' => 'PG-13', 'R-17' => 'R', ); $displayed_rating = isset( $info->rating ) ? $info->rating : null; // X-18 was previously supported but is now removed to better comply with our TOS. if ( 'X-18' === $displayed_rating ) { $displayed_rating = 'R-17'; } foreach ( $ratings as $r => $label ) { $id = "attachments-{$info->post_id}-rating-$r"; $out .= "<label for=\"$id\"><input type=\"radio\" name=\"attachments[{$info->post_id}][rating]\" id=\"$id\" value=\"$r\""; if ( $displayed_rating === $r ) { $out .= ' checked="checked"'; } $out .= " />$label</label>"; unset( $id ); } return $out; } } // Let's start this thing up. VideoPress_Edit_Attachment::init(); videopress/class.videopress-gutenberg.php 0000644 00000014245 15174711637 0014731 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Block Editor functionality for VideoPress users. * * @package automattic/jetpack */ use Automattic\Jetpack\Assets; use Automattic\Jetpack\Blocks; use Automattic\Jetpack\Current_Plan as Jetpack_Plan; use Automattic\Jetpack\VideoPress\Block_Replacement; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Register a VideoPress extension to replace the default Core Video block. */ class VideoPress_Gutenberg { /** * Singleton */ public static function init() { static $instance = false; if ( ! $instance ) { $instance = new VideoPress_Gutenberg(); } return $instance; } /** * VideoPress_Gutenberg constructor. * * Initialize the VideoPress Gutenberg extension */ private function __construct() { // Run late to avoid race condition with other plugins that register the video block // Jetpack's jetpack_register_block function bails if the block is already registered add_action( 'init', array( $this, 'register_video_block_with_videopress' ), 99 ); add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'set_extension_availability' ) ); add_action( 'enqueue_block_editor_assets', array( $this, 'override_video_upload' ) ); add_action( 'enqueue_block_editor_assets', array( $this, 'add_resumable_upload_support' ) ); Block_Replacement::init(); } /** * Get site's ID. * * @return int $blog_id Site ID. */ private static function get_blog_id() { if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { return get_current_blog_id(); } elseif ( method_exists( 'Jetpack', 'is_active' ) && Jetpack::is_active() ) { /** * We're intentionally not using `get_current_blog_id` because it was returning unexpected values. * * @see https://github.com/Automattic/jetpack/pull/11193#issuecomment-457883886 * @see https://github.com/Automattic/jetpack/pull/11193/commits/215cf789f3d8bd03ff9eb1bbdb693acb8831d273 */ return Jetpack_Options::get_option( 'id' ); } return null; } /** * Used to check whether VideoPress is enabled for given site. * * @todo Create a global `jetpack_check_module_availability( $module )` helper so we can re-use it on other modules. * This global helper should be created in a file synced with WordPress.com so we can use it there too. * @see https://github.com/Automattic/jetpack/pull/11321#discussion_r255477815 * * @return array Associative array indicating if the module is available (key `available`) and the reason why it is * unavailable (key `unavailable_reason`) */ public function check_videopress_availability() { if ( ! Jetpack_Plan::supports( 'videopress' ) ) { return array( 'available' => false, 'unavailable_reason' => 'missing_plan', ); } if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) { if ( Jetpack::is_connection_ready() ) { if ( ! Jetpack::is_module_active( 'videopress' ) ) { return array( 'available' => false, 'unavailable_reason' => 'missing_module', ); } } else { return array( 'available' => false, 'unavailable_reason' => 'unknown', ); } } return array( 'available' => true ); } /** * Set the Jetpack Gutenberg extension availability. */ public function set_extension_availability() { $availability = $this->check_videopress_availability(); if ( $availability['available'] ) { Jetpack_Gutenberg::set_extension_available( 'videopress' ); } else { Jetpack_Gutenberg::set_extension_unavailable( 'videopress', $availability['unavailable_reason'] ); } } /** * Register the core video block as a dynamic block. * * It defines a server-side rendering that adds VideoPress support to the core video block. */ public function register_video_block_with_videopress() { Blocks::jetpack_register_block( 'core/video', array( 'render_callback' => array( $this, 'render_video_block_with_videopress' ), ) ); } /** * Render the core video block replacing the src attribute with the VideoPress URL * * @param array $attributes Array containing the video block attributes. * @param string $content String containing the video block content. * * @return string */ public function render_video_block_with_videopress( $attributes, $content ) { if ( ! isset( $attributes['id'] ) || isset( $attributes['guid'] ) ) { return $content; } $blog_id = self::get_blog_id(); if ( ! isset( $blog_id ) ) { return $content; } $post_id = absint( $attributes['id'] ); $videopress_id = video_get_info_by_blogpostid( $blog_id, $post_id )->guid; $videopress_data = videopress_get_video_details( $videopress_id ); if ( empty( $videopress_data->file_url_base->https ) || empty( $videopress_data->files->hd->mp4 ) ) { return $content; } $videopress_url = $videopress_data->file_url_base->https . $videopress_data->files->hd->mp4; $pattern = '/(\s)src=([\'"])(?:(?!\2).)+?\2/'; return preg_replace( $pattern, sprintf( '\1src="%1$s"', esc_url_raw( $videopress_url ) ), $content, 1 ); } /** * Temporary method to enable resumable uploads for testing by Automatticians */ public function add_resumable_upload_support() { wp_enqueue_script( 'videopress-add-resumable-upload-support', plugins_url( 'js/videopress-add-resumable-upload-support.js', __FILE__ ), null, '1', false ); } /** * Replaces the video uploaded in the block editor. * * Enqueues a script that registers an API fetch middleware replacing the video uploads in Gutenberg so they are * uploaded against the WP.com API media endpoint and thus transcoded by VideoPress. */ public function override_video_upload() { // Bail if Jetpack is not connected or VideoPress module is not active. if ( ! Jetpack::is_connection_ready() || ! Jetpack::is_module_active( 'videopress' ) ) { return; } wp_enqueue_script( 'jetpack-videopress-gutenberg-override-video-upload', Assets::get_file_url_for_environment( '_inc/build/videopress/js/gutenberg-video-upload.min.js', 'modules/videopress/js/gutenberg-video-upload.js' ), array( 'wp-api-fetch', 'wp-polyfill' ), JETPACK__VERSION, false ); } } VideoPress_Gutenberg::init(); videopress/class.videopress-xmlrpc.php 0000644 00000001107 15174711637 0014245 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\VideoPress\XMLRPC; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VideoPress playback module markup generator. * * @since 1.3 */ class VideoPress_XMLRPC { /** * Initialize the XMLRPC and get back a singleton instance. * * @return XMLRPC * @deprecated 11.2 * @see Automattic\Jetpack\VideoPress\XMLRPC::init */ public static function init() { _deprecated_function( __METHOD__, 'jetpack-11.2', 'Automattic\Jetpack\VideoPress\XMLRPC' ); return XMLRPC::init(); } } videopress/videopress-admin.css 0000644 00000003270 15174711637 0012730 0 ustar 00 /** * VideoPress admin media styles */ .videopress-modal-backdrop { background: #000; opacity: 0.7; position: absolute; top: 0; width: 100%; height: 100%; overflow: hidden; z-index: 100; } .videopress-modal { padding: 10px 20px; background: #fff; position: absolute; top: 0; width: 440px; overflow: hidden; left: 50%; margin-left: -220px; z-index: 101; box-shadow: 2px 2px 5px 2px rgba(0, 0, 0, 0.5); border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; } .videopress-modal .submit { text-align: right; padding: 10px 0 5px; } .videopress-preview { display: block; float: right; width: 65%; margin-top: 18px; background: #000; min-height: 97px; text-decoration: none; } .vp-preview span.videopress-preview-unavailable { width: 65%; float: right; text-align: left; margin-right: 0; } .videopress-preview img { float: left; width: 100%; } .videopress-preview span { display: block; padding-top: 40px; color: #fff !important; text-align: center; } .vp-setting .help { margin: 0 0 4px 35%; } .media-sidebar .vp-setting input[type="checkbox"] { float: left; margin-top: 10px; } .vp-setting label { float: left; margin: 8px 8px 0 5px; max-width: 135px; } .vp-setting input[type="radio"] { float: left; margin-top: 9px; width: auto; } .vp-preview span { margin-top: 18px; } .uploader-videopress { margin: 16px; } .uploader-videopress .videopress-errors div { margin: 16px 0; } .compat-field-video-rating input[type="radio"], .compat-field-display_embed input[type="checkbox"], .compat-field-allow_download input[type="checkbox"] { margin-top: -1px !important; margin-right: 5px !important; margin-left: 5px !important; vertical-align: middle; } videopress/editor-media-view.php 0000644 00000021253 15174711637 0012772 0 ustar 00 <?php /** * VideoPress admin media-view functions. * * @package automattic/jetpack */ use Automattic\Jetpack\Assets; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * WordPress Shortcode Editor View JS Code */ function videopress_handle_editor_view_js() { $content_width = Jetpack::get_content_width(); $current_screen = get_current_screen(); if ( ! isset( $current_screen->id ) || $current_screen->base !== 'post' ) { return; } add_action( 'admin_print_footer_scripts', 'videopress_editor_view_js_templates' ); wp_enqueue_style( 'videopress-editor-ui', plugins_url( 'css/editor.css', __FILE__ ), array(), JETPACK__VERSION ); wp_enqueue_script( 'videopress-editor-view', Assets::get_file_url_for_environment( '_inc/build/videopress/js/editor-view.min.js', 'modules/videopress/js/editor-view.js' ), array( 'wp-util', 'jquery' ), JETPACK__VERSION, true ); wp_localize_script( 'videopress-editor-view', 'vpEditorView', array( 'home_url_host' => wp_parse_url( home_url(), PHP_URL_HOST ), 'min_content_width' => VIDEOPRESS_MIN_WIDTH, 'content_width' => $content_width ? (int) $content_width : VIDEOPRESS_DEFAULT_WIDTH, 'modal_labels' => array( 'title' => esc_html__( 'VideoPress Shortcode', 'jetpack' ), 'guid' => esc_html__( 'Video ID', 'jetpack' ), 'w' => esc_html__( 'Video Width', 'jetpack' ), 'w_unit' => esc_html__( 'pixels', 'jetpack' ), /* Translators: example of usage of this is "Start Video After 10 seconds" */ 'at' => esc_html__( 'Start Video After', 'jetpack' ), 'at_unit' => esc_html__( 'seconds', 'jetpack' ), 'hd' => esc_html__( 'High definition on by default', 'jetpack' ), 'permalink' => esc_html__( 'Link the video title to its URL on VideoPress.com', 'jetpack' ), 'autoplay' => esc_html__( 'Autoplay video on page load', 'jetpack' ), 'loop' => esc_html__( 'Loop video playback', 'jetpack' ), 'freedom' => esc_html__( 'Use only Open Source codecs (may degrade performance)', 'jetpack' ), 'flashonly' => esc_html__( 'Use legacy Flash Player (not recommended)', 'jetpack' ), ), ) ); add_editor_style( plugins_url( 'css/videopress-editor-style.css', __FILE__ ) ); } add_action( 'admin_notices', 'videopress_handle_editor_view_js' ); /** * WordPress Editor Views */ function videopress_editor_view_js_templates() { /** * This template uses the following parameters, and displays the video as an iframe: * - data.guid // The guid of the video. * - data.width // The width of the iframe. * - data.height // The height of the iframe. * - data.urlargs // Arguments serialized into a get string. * * In addition, the calling script will need to ensure that the following * JS file is added to the header of the editor iframe: * - https://s0.wp.com/wp-content/plugins/video/assets/js/next/videopress-iframe.js */ ?> <script type="text/html" id="tmpl-videopress_iframe_vnext"> <div class="tmpl-videopress_iframe_next" style="max-height:{{ data.height }}px;"> <div class="videopress-editor-wrapper" style="padding-top:{{ data.ratio }}%;"> <iframe style="display: block; max-width: 100%; max-height: 100%;" width="{{ data.width }}" height="{{ data.height }}" src="https://videopress.com/embed/{{ data.guid }}?{{ data.urlargs }}" frameborder='0' allowfullscreen></iframe> </div> </div> </script> <?php } /*************************************************\ | This is the chunk that handles overriding core | | media stuff so VideoPress can display natively. | \*/ /** * Media Grid: * Filter out any videopress video posters that we've downloaded, * so that they don't seem to display twice. * * @param array $args Query variables. * @deprecated 11.4 */ function videopress_ajax_query_attachments_args( $args ) { _deprecated_function( __METHOD__, 'jetpack-11.4' ); return Automattic\Jetpack\VideoPress\Attachment_Handler::ajax_query_attachments_args( $args ); } /** * Media List: * Do the same as `videopress_ajax_query_attachments_args()` but for the list view. * * @param array $query WP_Query instance. * @deprecated 11.4 */ function videopress_media_list_table_query( $query ) { _deprecated_function( __METHOD__, 'jetpack-11.4' ); return Automattic\Jetpack\VideoPress\Attachment_Handler::media_list_table_query( $query ); } /** * Make sure that any Video that has a VideoPress GUID passes that data back. * * @param WP_Post $post Attachment object. * @deprecated 11.4 */ function videopress_prepare_attachment_for_js( $post ) { _deprecated_function( __METHOD__, 'jetpack-11.4' ); return Automattic\Jetpack\VideoPress\Attachment_Handler::prepare_attachment_for_js( $post ); } /** * Wherever the Media Modal is deployed, also deploy our overrides. */ function add_videopress_media_overrides() { add_action( 'admin_print_footer_scripts', 'videopress_override_media_templates', 11 ); } add_action( 'wp_enqueue_media', 'add_videopress_media_overrides' ); /** * Our video overrides! * * We have a template for the iframe to get injected. */ function videopress_override_media_templates() { ?> <script type="text/html" id="tmpl-videopress_iframe_vnext"> <iframe class="videopress-iframe" style="display: block; max-width: 100%; max-height: 100%;" width="{{ data.width }}" height="{{ data.height }}" src="https://videopress.com/embed/{{ data.guid }}?{{ data.urlargs }}" frameborder='0' allowfullscreen></iframe> </script> <script> (function( media ){ // This handles the media library modal attachment details display. if ( 'undefined' !== typeof media.view.Attachment.Details.TwoColumn ) { var TwoColumn = media.view.Attachment.Details.TwoColumn, old_render = TwoColumn.prototype.render, vp_template = wp.template('videopress_iframe_vnext'); TwoColumn.prototype.render = function() { // Have the original renderer run first. old_render.apply( this, arguments ); // Now our stuff! if ( 'video' === this.model.get('type') ) { if ( this.model.get('videopress_guid') ) { this.$('.attachment-media-view .thumbnail-video').html( vp_template( { guid : this.model.get('videopress_guid'), width : this.model.get('width') > 0 ? this.model.get('width') : '100%', height : this.model.get('height') > 0 ? this.model.get('height') : '100%' })); } } }; } else { /* console.log( 'media.view.Attachment.Details.TwoColumn undefined' ); */ } // This handles the recreating of the core video shortcode when editing the mce embed. if ( 'undefined' !== typeof media.video ) { media.video.defaults.videopress_guid = ''; // For some reason, even though we're not currently changing anything, the following proxy // function is necessary to include the above default `videopress_guid` param. ¯\_(ツ)_/¯ var old_video_shortcode = media.video.shortcode; media.video.shortcode = function( model ) { // model.videopress_guid = 'FOOBAR'; return old_video_shortcode( model ); }; } else { /* console.log( 'media.video undefined' ); */ } // override the media modal in order to extend the escape method to unload the player on hide var BaseMediaModal = wp.media.view.Modal; wp.media.view.Modal = BaseMediaModal.extend( { escape: function () { BaseMediaModal.prototype.escape.apply( this ); var playerIframes = document.getElementsByClassName( "videopress-iframe" ); if ( playerIframes.length && playerIframes[0].parentElement ) { playerIframes[0].parentElement.removeChild( playerIframes[0] ); } } } ); })( wp.media ); </script> <?php } /** * Properly inject VideoPress data into Core shortcodes, and * generate videopress shortcodes for purely remote videos. * * @param string $html HTML markup for a media item sent to the editor. * @param int $id Attachment ID. * @param array $attachment Attachment metadata. */ function videopress_media_send_to_editor( $html, $id, $attachment ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $videopress_guid = get_post_meta( $id, 'videopress_guid', true ); if ( $videopress_guid && videopress_is_valid_guid( $videopress_guid ) ) { if ( str_starts_with( $html, '[video ' ) ) { $html = sprintf( '[videopress %1$s]', esc_attr( $videopress_guid ) ); } elseif ( str_starts_with( $html, '<a href=' ) ) { // We got here because `wp_attachment_is()` returned false for // video, because there isn't a local copy of the file. $html = sprintf( '[videopress %1$s]', esc_attr( $videopress_guid ) ); } } elseif ( videopress_is_attachment_without_guid( $id ) ) { $html = sprintf( '[videopress postid=%d]', $id ); } return $html; } add_filter( 'media_send_to_editor', 'videopress_media_send_to_editor', 10, 3 ); videopress/css/videopress-editor-style-rtl.css 0000644 00000000560 15174711637 0015652 0 ustar 00 /** * VideoPress styles for Editor */ .videopress-editor-wrapper { position: relative; max-width: 100%; padding: 56.25% 0 0; height: 0; overflow: hidden; } .tmpl-videopress_iframe_next iframe { position: absolute; top: 0; right: 0; max-width: 100%; max-height: 100%; } body.rtl .tmpl-videopress_iframe_next iframe { right: auto; left: 0; } videopress/css/videopress-editor-style.css 0000644 00000000543 15174711637 0015054 0 ustar 00 /** * VideoPress styles for Editor */ .videopress-editor-wrapper { position: relative; max-width: 100%; padding: 56.25% 0 0; height: 0; overflow: hidden; } .tmpl-videopress_iframe_next iframe { position: absolute; top: 0; left: 0; max-width: 100%; max-height: 100%; } body.rtl .tmpl-videopress_iframe_next iframe { left: auto; right: 0; } videopress/css/editor.min.css 0000644 00000001731 15174711637 0012317 0 ustar 00 .mce-videopress-field-flashonly,.mce-videopress-field-freedom,.mce-videopress-field-guid{display:none}.mce-videopress-checkbox .mce-checkbox{left:120px!important;width:100%!important}.mce-videopress-checkbox .mce-label{left:150px!important}.mce-videopress-checkbox .mce-label-unit{left:210px;position:absolute;top:5px}.mce-videopress-checkbox i.mce-i-checkbox{background-color:#fff;color:#4f94d4}.mce-videopress-checkbox .mce-i-checkbox:before{display:inline-block;font:400 21px/1 dashicons;vertical-align:middle;width:16px;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f147";margin:-3px 0 0 -3px}.mce-videopress-checkbox .mce-i-checkbox.mce-checked:before{content:"\f147"}div[class*=mce-videopress-field] input[type=number]{left:120px!important;width:70px!important}.mce-videopress-field-at .mce-label,.mce-videopress-field-w .mce-label{text-align:right;width:115px!important}.mce-videopress-field-unit{left:210px;position:absolute;top:5px} videopress/css/videopress-editor-style-rtl.min.css 0000644 00000000412 15174711637 0016430 0 ustar 00 .videopress-editor-wrapper{height:0;max-width:100%;overflow:hidden;padding:56.25% 0 0;position:relative}.tmpl-videopress_iframe_next iframe{max-height:100%;max-width:100%;position:absolute;right:0;top:0}body.rtl .tmpl-videopress_iframe_next iframe{left:0;right:auto} videopress/css/editor-rtl.css 0000644 00000002355 15174711637 0012337 0 ustar 00 /* VideoPress Settings Modal style overrides */ .mce-videopress-field-guid, .mce-videopress-field-freedom, .mce-videopress-field-flashonly { display: none; } .mce-videopress-checkbox .mce-checkbox { right: 120px !important; width: 100% !important; /* assigning a full width so the label area is clickable */ } .mce-videopress-checkbox .mce-label { right: 150px !important; } .mce-videopress-checkbox .mce-label-unit { position: absolute; right: 210px; top: 5px; } .mce-videopress-checkbox i.mce-i-checkbox { background-color: #fff; color: #4f94d4; } .mce-videopress-checkbox .mce-i-checkbox::before { display: inline-block; vertical-align: middle; width: 16px; font: 400 21px/1 dashicons; speak: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; margin: -3px -3px 0 0; content: "\f147"; } .mce-videopress-checkbox .mce-i-checkbox.mce-checked::before { content: "\f147"; } div[class*=mce-videopress-field] input[type=number] { width: 70px !important; right: 120px !important; } .mce-videopress-field-w .mce-label, .mce-videopress-field-at .mce-label { width: 115px !important; text-align: left; } .mce-videopress-field-unit { position: absolute; right: 210px; top: 5px; } videopress/css/editor-rtl.min.css 0000644 00000001735 15174711637 0013122 0 ustar 00 .mce-videopress-field-flashonly,.mce-videopress-field-freedom,.mce-videopress-field-guid{display:none}.mce-videopress-checkbox .mce-checkbox{right:120px!important;width:100%!important}.mce-videopress-checkbox .mce-label{right:150px!important}.mce-videopress-checkbox .mce-label-unit{position:absolute;right:210px;top:5px}.mce-videopress-checkbox i.mce-i-checkbox{background-color:#fff;color:#4f94d4}.mce-videopress-checkbox .mce-i-checkbox:before{display:inline-block;font:400 21px/1 dashicons;vertical-align:middle;width:16px;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f147";margin:-3px -3px 0 0}.mce-videopress-checkbox .mce-i-checkbox.mce-checked:before{content:"\f147"}div[class*=mce-videopress-field] input[type=number]{right:120px!important;width:70px!important}.mce-videopress-field-at .mce-label,.mce-videopress-field-w .mce-label{text-align:left;width:115px!important}.mce-videopress-field-unit{position:absolute;right:210px;top:5px} videopress/css/videopress-editor-style.min.css 0000644 00000000411 15174711637 0015630 0 ustar 00 .videopress-editor-wrapper{height:0;max-width:100%;overflow:hidden;padding:56.25% 0 0;position:relative}.tmpl-videopress_iframe_next iframe{left:0;max-height:100%;max-width:100%;position:absolute;top:0}body.rtl .tmpl-videopress_iframe_next iframe{left:auto;right:0} videopress/css/editor.css 0000644 00000002323 15174711637 0011533 0 ustar 00 /* VideoPress Settings Modal style overrides */ .mce-videopress-field-guid, .mce-videopress-field-freedom, .mce-videopress-field-flashonly { display: none; } .mce-videopress-checkbox .mce-checkbox { left: 120px !important; width: 100% !important; /* assigning a full width so the label area is clickable */ } .mce-videopress-checkbox .mce-label { left: 150px !important; } .mce-videopress-checkbox .mce-label-unit { position: absolute; left: 210px; top: 5px; } .mce-videopress-checkbox i.mce-i-checkbox { background-color: #fff; color: #4f94d4; } .mce-videopress-checkbox .mce-i-checkbox::before { display: inline-block; vertical-align: middle; width: 16px; font: 400 21px/1 dashicons; speak: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; margin: -3px 0 0 -3px; content: "\f147"; } .mce-videopress-checkbox .mce-i-checkbox.mce-checked::before { content: "\f147"; } div[class*="mce-videopress-field"] input[type="number"] { width: 70px !important; left: 120px !important; } .mce-videopress-field-w .mce-label, .mce-videopress-field-at .mce-label { width: 115px !important; text-align: right; } .mce-videopress-field-unit { position: absolute; left: 210px; top: 5px; } videopress/videopress-admin-rtl.css 0000644 00000003351 15174711637 0013527 0 ustar 00 /** * VideoPress admin media styles */ .videopress-modal-backdrop { background: #000; opacity: 0.7; position: absolute; top: 0; width: 100%; height: 100%; overflow: hidden; z-index: 100; } .videopress-modal { padding: 10px 20px; background: #fff; position: absolute; top: 0; width: 440px; overflow: hidden; right: 50%; margin-right: -220px; z-index: 101; box-shadow: -2px 2px 5px 2px rgba(0, 0, 0, 0.5); border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } .videopress-modal .submit { text-align: left; padding: 10px 0 5px; } .videopress-preview { display: block; float: left; width: 65%; margin-top: 18px; background: #000; min-height: 97px; text-decoration: none; } .vp-preview span.videopress-preview-unavailable { width: 65%; float: left; text-align: right; margin-left: 0; } .videopress-preview img { float: right; width: 100%; } .videopress-preview span { display: block; padding-top: 40px; color: #fff !important; text-align: center; } .vp-setting .help { margin: 0 35% 4px 0; } .media-sidebar .vp-setting input[type=checkbox] { float: right; margin-top: 10px; } .vp-setting label { float: right; margin: 8px 5px 0 8px; max-width: 135px; } .vp-setting input[type=radio] { float: right; margin-top: 9px; width: auto; } .vp-preview span { margin-top: 18px; } .uploader-videopress { margin: 16px; } .uploader-videopress .videopress-errors div { margin: 16px 0; } .compat-field-video-rating input[type=radio], .compat-field-display_embed input[type=checkbox], .compat-field-allow_download input[type=checkbox] { margin-top: -1px !important; margin-left: 5px !important; margin-right: 5px !important; vertical-align: middle; } videopress/class.videopress-video.php 0000644 00000021707 15174711637 0014056 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VideoPress video object retrieved from VideoPress servers and parsed. * * @since 1.3 */ class VideoPress_Video { /** * VideoPress version. * * @var int */ public $version = 3; /** * Manifest version returned by remote service. * * @var string * @since 1.3 */ const manifest_version = '1.5'; // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase /** * Expiration of the video expressed in Unix time * * @var int * @since 1.3 */ public $expires; /** * VideoPress unique identifier * * @var string * @since 1.3 */ public $guid; /** * WordPress.com blog identifier * * @var int * @since 1.5 */ public $blog_id; /** * Remote blog attachment identifier * * @var int * @since 1.5 */ public $post_id; /** * Maximum desired width. * * @var int * @since 1.3 */ public $maxwidth; /** * Video width calculated based on original video dimensions and the requested maxwidth * * @var int * @since 1.3 */ public $calculated_width; /** * Video height calculated based on original video dimensions and the requested maxwidth * * @var int * @since 1.3 */ public $calculated_height; /** * Video title * * @var string * @since 1.3 */ public $title; /** * Video description * * @var string * @since 4.4 */ public $description; /** * Directionality of title text. ltr or rtl * * @var string * @since 1.3 */ public $text_direction; /** * Text and audio language as ISO 639-2 language code * * @var string * @since 1.3 */ public $language; /** * Video duration in whole seconds * * @var int * @since 1.3 */ public $duration; /** * Recommended minimum age of the viewer. * * @var int * @since 1.3 */ public $age_rating; /** * Video author has restricted video embedding or sharing * * @var bool * @since 1.3 */ public $restricted_embed; /** * Poster frame image URI for the given video guid and calculated dimensions. * * @var string * @since 1.3 */ public $poster_frame_uri; /** * Video files associated with the given guid for the calculated dimensions. * * @var stdClass * @since 1.3 */ public $videos; /** * Video player information * * @var stdClass * @since 1.3 */ public $players; /** * Video player skinning preferences including background color and watermark * * @var array * @since 1.5 */ public $skin; /** * Closed captions if available for the given video. Associative array of ISO 639-2 language code and a WebVTT URI * * @var array * @since 1.5 */ public $captions; /** * Error data. * * @var mixed */ public $error; /** * Setup the object. * Request video information from VideoPress servers and process the response. * * @since 1.3 * @param string $guid VideoPress unique identifier. * @param int $maxwidth maximum requested video width. final width and height are calculated on VideoPress servers based on the aspect ratio of the original video upload. */ public function __construct( $guid, $maxwidth = 640 ) { $this->guid = $guid; $maxwidth = absint( $maxwidth ); if ( $maxwidth > 0 ) { $this->maxwidth = $maxwidth; } $data = $this->get_data(); if ( is_wp_error( $data ) || empty( $data ) ) { /** This filter is documented in modules/videopress/class.videopress-player.php */ if ( ! apply_filters( 'jetpack_videopress_use_legacy_player', false ) ) { // Unlike the Flash player, the new player does it's own error checking, age gate, etc. $data = (object) array( 'guid' => $guid, 'width' => $maxwidth, 'height' => $maxwidth / 16 * 9, ); } else { $this->error = $data; return; } } if ( isset( $data->blog_id ) ) { $this->blog_id = absint( $data->blog_id ); } if ( isset( $data->post_id ) ) { $this->post_id = absint( $data->post_id ); } if ( isset( $data->title ) && $data->title !== '' ) { $this->title = trim( str_replace( ' ', ' ', $data->title ) ); } if ( isset( $data->description ) && $data->description !== '' ) { $this->description = trim( $data->description ); } if ( isset( $data->text_direction ) && $data->text_direction === 'rtl' ) { $this->text_direction = 'rtl'; } else { $this->text_direction = 'ltr'; } if ( isset( $data->language ) ) { $this->language = $data->language; } if ( isset( $data->duration ) && $data->duration > 0 ) { $this->duration = absint( $data->duration ); } if ( isset( $data->width ) && $data->width > 0 ) { $this->calculated_width = absint( $data->width ); } if ( isset( $data->height ) && $data->height > 0 ) { $this->calculated_height = absint( $data->height ); } if ( isset( $data->age_rating ) ) { $this->age_rating = absint( $this->age_rating ); } if ( isset( $data->restricted_embed ) && $data->restricted_embed === true ) { $this->restricted_embed = true; } else { $this->restricted_embed = false; } if ( isset( $data->posterframe ) && $data->posterframe !== '' ) { $this->poster_frame_uri = esc_url_raw( $data->posterframe, array( 'http', 'https' ) ); } if ( isset( $data->mp4 ) || isset( $data->ogv ) ) { $this->videos = new stdClass(); if ( isset( $data->mp4 ) ) { $this->videos->mp4 = $data->mp4; } if ( isset( $data->ogv ) ) { $this->videos->ogv = $data->ogv; } } if ( isset( $data->swf ) ) { if ( ! isset( $this->players ) ) { $this->players = new stdClass(); } $this->players->swf = $data->swf; } if ( isset( $data->skin ) ) { $this->skin = $data->skin; } if ( isset( $data->captions ) ) { $this->captions = (array) $data->captions; } } /** * Convert an Expires HTTP header value into Unix time for use in WP Cache. * * @since 1.3 * @param string $expires_header Expires header value. * @return int|bool Unix time or false */ public static function calculate_expiration( $expires_header ) { if ( empty( $expires_header ) || ! is_string( $expires_header ) ) { return false; } $expires_date = DateTime::createFromFormat( 'D, d M Y H:i:s T', $expires_header, new DateTimeZone( 'UTC' ) ); if ( $expires_date instanceof DateTime ) { return date_format( $expires_date, 'U' ); } return false; } /** * Extract the site's host domain for statistics and comparison against an allowed site list in the case of restricted embeds. * * @since 1.2 * @param string $url absolute URL. * @return bool|string host component of the URL, or false if none found. */ public static function hostname( $url ) { return wp_parse_url( esc_url_raw( $url ), PHP_URL_HOST ); } /** * Request data from WordPress.com for the given guid, maxwidth, and calculated blog hostname. * * @since 1.3 * @return stdClass|WP_Error parsed JSON response or WP_Error if request unsuccessful */ private function get_data() { global $wp_version; $domain = self::hostname( home_url() ); $request_params = array( 'guid' => $this->guid, 'domain' => $domain, ); if ( isset( $this->maxwidth ) && $this->maxwidth > 0 ) { $request_params['maxwidth'] = $this->maxwidth; } $url = 'https://v.wordpress.com/data/wordpress.json'; $response = wp_remote_get( add_query_arg( $request_params, $url ), array( 'redirection' => 1, 'user-agent' => 'VideoPress plugin ' . $this->version . '; WordPress ' . $wp_version . ' (' . home_url( '/' ) . ')', ) ); unset( $request_params ); unset( $url ); $response_body = wp_remote_retrieve_body( $response ); $response_code = absint( wp_remote_retrieve_response_code( $response ) ); if ( is_wp_error( $response ) ) { return $response; } elseif ( $response_code === 400 ) { return new WP_Error( 'bad_config', __( 'The VideoPress plugin could not communicate with the VideoPress servers. This error is most likely caused by a misconfigured plugin. Please reinstall or upgrade.', 'jetpack' ) ); } elseif ( $response_code === 403 ) { /* translators: %s URL of site trying to embed a VideoPress video */ return new WP_Error( 'http_forbidden', '<p>' . sprintf( __( '<strong>%s</strong> is not an allowed embed site.', 'jetpack' ), esc_html( $domain ) ) . '</p><p>' . __( 'Publisher limits playback of video embeds.', 'jetpack' ) . '</p>' ); } elseif ( $response_code === 404 ) { /* translators: %s VideoPress object identifier */ return new WP_Error( 'http_not_found', '<p>' . sprintf( __( 'No data found for VideoPress identifier: <strong>%s</strong>.', 'jetpack' ), $this->guid ) . '</p>' ); } elseif ( $response_code !== 200 || empty( $response_body ) ) { return; } else { $expires_header = wp_remote_retrieve_header( $response, 'Expires' ); if ( ! empty( $expires_header ) ) { $expires = self::calculate_expiration( $expires_header ); if ( ! empty( $expires ) ) { $this->expires = $expires; } } return json_decode( $response_body ); } } } videopress/class-videopress-attachment-metadata.php 0000644 00000014541 15174711637 0016653 0 ustar 00 <?php /** * Handle the VideoPress metadata properties. * * @package Jetpack */ use Automattic\Jetpack\Connection\Client; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Class Videopress_Attachment_Metadata */ class Videopress_Attachment_Metadata { /** * Persist the VideoPress metadata information, including rating and display_embed. * * @param string|int $post_id The post id. * @param string $guid VideoPress Guid. * @param string $post_title The post title. * @param string $caption Video caption. * @param string $post_excerpt The post excerpt. * @param string $rating The rating. * @param int $display_embed The display_embed. * @param int $allow_download Allow video downloads. * @param int $privacy_setting The video privacy setting. * * @return bool|\WP_Error */ public static function persist_metadata( $post_id, $guid, $post_title, $caption, $post_excerpt, $rating, $display_embed, $allow_download, $privacy_setting ) { $post_id = absint( $post_id ); $args = array( 'method' => 'POST', 'headers' => array( 'content-type' => 'application/json' ), ); // Keep null values to avoid accidental unset. $display_embed = null === $display_embed ? null : (int) $display_embed; $allow_download = null === $allow_download ? null : (int) $allow_download; $privacy_setting = null === $privacy_setting ? null : (int) $privacy_setting; $values = self::build_wpcom_api_request_values( $post_title, $caption, $post_excerpt, $rating, $display_embed, $allow_download, $privacy_setting ); $endpoint = 'videos'; $values['guid'] = $guid; $result = Client::wpcom_json_api_request_as_blog( $endpoint, '2', $args, wp_json_encode( $values, JSON_UNESCAPED_SLASHES ), 'wpcom' ); $validated_result = self::validate_result( $result ); if ( true !== $validated_result ) { return $validated_result; } // If we are in WPCOM, then we don't need to make anything else since we've already updated the video information. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { return true; } $meta = wp_get_attachment_metadata( $post_id ); if ( isset( $values['display_embed'] ) ) { $meta['videopress']['display_embed'] = (bool) $values['display_embed']; // convert it to bool since that's how we store it on wp-admin side. } if ( isset( $values['allow_download'] ) ) { $meta['videopress']['allow_download'] = (bool) $values['allow_download']; } if ( isset( $values['rating'] ) ) { $meta['videopress']['rating'] = $values['rating']; } if ( isset( $values['privacy_setting'] ) ) { $meta['videopress']['privacy_setting'] = $values['privacy_setting']; } wp_update_attachment_metadata( $post_id, $meta ); return true; } /** * Check if the given media item is a VideoPress file. * * @param stdClass $item The media item. * * @return bool */ public static function is_videopress_media( $item ) { if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { return str_starts_with( $item->mime_type, 'video/' ); } // Else, we are in Jetpack and we need to check if the video is video/videopress. return 'video/videopress' === $item->mime_type; } /** * Check if display_embed has valid values. * * @param mixed $display_embed The input display embed. * * @return bool */ private static function is_display_embed_valid( $display_embed ) { return in_array( $display_embed, array( 0, 1 ), true ); } /** * Check if allow_download has valid values * * @param mixed $allow_download The value to test. * @return bool */ private static function is_allow_download_valid( $allow_download ) { return in_array( $allow_download, array( 0, 1 ), true ); } /** * Check if privacy_setting has valid values * * @param mixed $privacy_setting The value to test. * @return bool */ private static function is_privacy_setting_valid( $privacy_setting ) { return in_array( $privacy_setting, array( VIDEOPRESS_PRIVACY::IS_PUBLIC, VIDEOPRESS_PRIVACY::IS_PRIVATE, VIDEOPRESS_PRIVACY::SITE_DEFAULT ), true ); } /** * Validate the response received from WPCOM. * * @param array|\WP_Error $result The result returned by the client. */ private static function validate_result( $result ) { $response_code = isset( $result['response']['code'] ) ? $result['response']['code'] : 500; // When Client::wpcom_json_api_request_as_blog is called in WPCOM, bad response codes are not converted to WP_Error. // Because of this, we need to manually check the response code to check if the direct API call is 200 (OK). if ( 200 === $response_code && ! is_wp_error( $result ) ) { return true; } $error_message = __( 'There was an issue saving your updates to the VideoPress service. Please try again later.', 'jetpack' ); $error_code = $response_code; if ( is_wp_error( $result ) ) { $error_code = $result->get_error_code(); } return new \WP_Error( $error_code, $error_message ); } /** * Build the request values that will be passed to the WPCOM API. * * @param string $post_title The video title. * @param string $caption The video caption. * @param string $post_excerpt The except. * @param string $rating The video rating. * @param string $display_embed The video display_embed. * @param int $allow_download The video allow_download. * @param int $privacy_setting The video privacy setting. * * @return array */ private static function build_wpcom_api_request_values( $post_title, $caption, $post_excerpt, $rating, $display_embed, $allow_download, $privacy_setting ) { $values = array(); // Add the video title & description in, so that we save it properly. if ( isset( $post_title ) ) { $values['title'] = trim( wp_strip_all_tags( $post_title ) ); } if ( isset( $caption ) ) { $values['caption'] = trim( wp_strip_all_tags( $caption ) ); } if ( isset( $post_excerpt ) ) { $values['description'] = trim( wp_strip_all_tags( $post_excerpt ) ); } if ( isset( $rating ) ) { $values['rating'] = $rating; } if ( self::is_display_embed_valid( $display_embed ) ) { $values['display_embed'] = $display_embed; } if ( self::is_allow_download_valid( $allow_download ) ) { $values['allow_download'] = $allow_download; } if ( self::is_privacy_setting_valid( $privacy_setting ) ) { $values['privacy_setting'] = $privacy_setting; } return $values; } } videopress/class.videopress-options.php 0000644 00000002517 15174711637 0014441 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * VideoPress Options * * @package automattic/jetpack */ use Automattic\Jetpack\VideoPress\Options as Package_Options; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VideoPress Options class. * * @deprecated 11.2 */ class VideoPress_Options { /** * Option name. * * @var string $option_name The 'videopress' option name * @deprecated 11.2 */ public static $option_name = 'videopress'; /** * VideoPress Options. * * @var array $options An array of associated VideoPress options (default empty) * @deprecated 11.2 */ protected static $options = array(); /** * Get VideoPress options * * @return array An array of VideoPress options. * @deprecated 11.2 */ public static function get_options() { _deprecated_function( __METHOD__, 'jetpack-11.2' ); return Package_Options::get_options(); } /** * Update VideoPress options * * @param mixed $options VideoPress options. */ public static function update_options( $options ) { _deprecated_function( __METHOD__, 'jetpack-11.2' ); return Package_Options::update_options( $options ); } /** * Runs when the VideoPress module is deactivated. */ public static function delete_options() { _deprecated_function( __METHOD__, 'jetpack-11.2' ); return Package_Options::delete_options(); } } videopress/class.jetpack-videopress.php 0000644 00000024445 15174711637 0014373 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\Assets; use Automattic\Jetpack\VideoPress\Attachment_Handler; use Automattic\Jetpack\VideoPress\Jwt_Token_Bridge; use Automattic\Jetpack\VideoPress\Options as VideoPress_Options; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VideoPress in Jetpack */ class Jetpack_VideoPress { /** * Module name. * * @var string */ public $module = 'videopress'; /** * Singleton */ public static function init() { static $instance = false; if ( ! $instance ) { $instance = new Jetpack_VideoPress(); } return $instance; } /** * Jetpack_VideoPress constructor. * * Sets up the initializer and makes sure that videopress activates and deactivates properly. */ private function __construct() { add_action( 'init', array( $this, 'on_init' ) ); add_action( 'jetpack_deactivate_module_videopress', array( $this, 'jetpack_module_deactivated' ) ); } /** * Fires on init */ public function on_init() { add_action( 'wp_enqueue_media', array( $this, 'enqueue_admin_scripts' ) ); add_filter( 'plupload_default_settings', array( $this, 'videopress_pluploder_config' ) ); add_action( 'admin_print_footer_scripts', array( $this, 'print_in_footer_open_media_add_new' ) ); add_action( 'admin_head', array( $this, 'enqueue_admin_styles' ) ); VideoPress_Scheduler::init(); if ( $this->is_videopress_enabled() ) { add_action( 'admin_notices', array( $this, 'media_new_page_admin_notice' ) ); } } /** * Enqueues the jwt bridge script. * * @deprecated 11.3 */ public function enqueue_jwt_token_bridge() { _deprecated_function( __METHOD__, 'jetpack-11.3', 'Automattic\Jetpack\VideoPress\Jwt_Token_Bridge::enqueue_jwt_token_bridge' ); return Jwt_Token_Bridge::enqueue_jwt_token_bridge(); } /** * The media-new.php page isn't supported for uploading to VideoPress. * * There is either a technical reason for this (bulk uploader isn't overridable), * or it is an intentional way to give site owners an option for uploading videos that bypass VideoPress. */ public function media_new_page_admin_notice() { global $pagenow; if ( 'media-new.php' !== $pagenow ) { return; } $message = sprintf( wp_kses( /* translators: %s is the url to the Media Library */ __( 'VideoPress uploads are not supported here. To upload to VideoPress, add your videos from the <a href="%s">Media Library</a> or the block editor using the Video block.', 'jetpack' ), array( 'a' => array( 'href' => array() ) ) ), esc_url( admin_url( 'upload.php?mode=grid&action=add-new' ) ) ); wp_admin_notice( $message, array( 'type' => 'warning', 'dismissible' => true, ) ); } /** * Runs when the VideoPress module is deactivated. */ public function jetpack_module_deactivated() { VideoPress_Options::delete_options(); } /** * Similar to current_user_can, but internal to VideoPress. * * @param string $cap Capability name. * @param int $user_id User ID. * @return bool Returns true if the given VideoPress capability is allowed by the given user. */ public function can( $cap, $user_id = false ) { if ( ! $user_id ) { $user_id = get_current_user_id(); } // Connection owners are allowed to do all the things. if ( Jetpack::connection()->is_connection_owner( $user_id ) ) { return true; } // Additional and internal caps checks if ( ! user_can( $user_id, 'upload_files' ) ) { return false; } if ( 'edit_videos' === $cap && ! user_can( $user_id, 'edit_others_posts' ) ) { return false; } if ( 'delete_videos' === $cap && ! user_can( $user_id, 'delete_others_posts' ) ) { return false; } return true; } /** * Register and enqueue VideoPress admin styles. */ public function enqueue_admin_styles() { wp_register_style( 'videopress-admin', plugins_url( 'videopress-admin.css', __FILE__ ), array(), JETPACK__VERSION ); wp_enqueue_style( 'videopress-admin' ); } /** * Attempts to delete a VideoPress video from wp.com. * Will block the deletion from continuing if certain errors return from the wp.com API. * * @param Boolean $delete if the deletion should occur or not (unused). * @param WP_Post $post the post object. * * @deprecated 11.3 * * @return null|WP_Error|Boolean null if deletion should continue. */ public function delete_video_wpcom( $delete, $post ) { _deprecated_function( __METHOD__, 'jetpack-11.3', 'Automattic\Jetpack\VideoPress\Attachment_Handler::delete_video_wpcom' ); return Attachment_Handler::delete_video_wpcom( $delete, $post ); } /** * Register VideoPress admin scripts. */ public function enqueue_admin_scripts() { if ( did_action( 'videopress_enqueue_admin_scripts' ) ) { return; } if ( $this->should_override_media_uploader() ) { wp_enqueue_script( 'videopress-plupload', Assets::get_file_url_for_environment( '_inc/build/videopress/js/videopress-plupload.min.js', 'modules/videopress/js/videopress-plupload.js' ), array( 'jquery', 'wp-plupload', ), JETPACK__VERSION, true ); wp_enqueue_script( 'videopress-uploader', Assets::get_file_url_for_environment( '_inc/build/videopress/js/videopress-uploader.min.js', 'modules/videopress/js/videopress-uploader.js' ), array( 'videopress-plupload', ), JETPACK__VERSION, true ); wp_enqueue_script( 'media-video-widget-extensions', Assets::get_file_url_for_environment( '_inc/build/videopress/js/media-video-widget-extensions.min.js', 'modules/videopress/js/media-video-widget-extensions.js' ), array(), JETPACK__VERSION, true ); } /** * Fires after VideoPress scripts are enqueued in the dashboard. * * @since 2.5.0 */ do_action( 'videopress_enqueue_admin_scripts' ); } /** * Returns the VideoPress URL for the give post id, otherwise returns the provided default. * * This is an attachment-based filter handler. * * @deprecated 11.3 * * @param string $default The default return value if post id is not a VideoPress video. * @param int $post_id The post id for the current attachment. */ public function maybe_get_attached_url_for_videopress( $default, $post_id ) { _deprecated_function( __METHOD__, 'jetpack-11.3', 'Automattic\Jetpack\VideoPress\Attachment_Handler::maybe_get_attached_url_for_videopress' ); return Attachment_Handler::maybe_get_attached_url_for_videopress( $default, $post_id ); } /** * Modify the default plupload config to turn on VideoPress specific filters. * * @param array $config The plupload config. */ public function videopress_pluploder_config( $config ) { if ( ! isset( $config['filters']['max_file_size'] ) ) { $config['filters']['max_file_size'] = wp_max_upload_size() . 'b'; } $config['filters']['videopress_check_uploads'] = $config['filters']['max_file_size']; // We're doing our own check in the videopress_check_uploads filter. unset( $config['filters']['max_file_size'] ); return $config; } /** * Helper function to determine if the media uploader should be overridden. * * The rules are simple, only try to load the script when on the edit post or new post pages. * * @return bool */ protected function should_override_media_uploader() { global $pagenow; // Only load in the admin if ( ! is_admin() ) { return false; } $acceptable_pages = array( 'post-new.php', 'post.php', 'upload.php', 'customize.php', ); // Only load on the post, new post, or upload pages. if ( ! in_array( $pagenow, $acceptable_pages, true ) ) { return false; } return $this->is_videopress_enabled(); } /** * Detects if VideoPress is enabled. * * @return bool */ protected function is_videopress_enabled() { $options = VideoPress_Options::get_options(); return $options['shadow_blog_id'] > 0; } /** * A work-around / hack to make it possible to go to the media library with the add new box open. * * @return bool */ public function print_in_footer_open_media_add_new() { global $pagenow; // Only load in the admin if ( ! is_admin() ) { return false; } if ( $pagenow !== 'upload.php' ) { return false; } if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'add-new' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return false; } ?> <script type="text/javascript"> ( function( $ ) { window.setTimeout( function() { $('#wp-media-grid .page-title-action').click(); }, 500 ); }( jQuery ) ); </script> <?php } /** * Makes sure that all video mimes are added in, as multi site installs can remove them. * * @deprecated 11.3 * * @param array $existing_mimes Mime types to extend/filter. * @return array */ public function add_video_upload_mimes( $existing_mimes = array() ) { _deprecated_function( __METHOD__, 'jetpack-11.3', 'Automattic\Jetpack\VideoPress\Attachment_Handler::add_video_upload_mimes' ); return Attachment_Handler::add_video_upload_mimes( $existing_mimes ); } /** * Filter designed to get rid of non video mime types. * * @deprecated 11.3 * * @param string $value Mime type to filter. * @return int */ public function filter_video_mimes( $value ) { _deprecated_function( __METHOD__, 'jetpack-11.3', 'Automattic\Jetpack\VideoPress\Attachment_Handler::filter_video_mimes' ); return Attachment_Handler::filter_video_mimes( $value ); } /** * Filter the mime type icon. * * @param string $icon Icon path. * @param string $mime Mime type. * @param int $post_id Post ID. * * @deprecated 11.3 * * @return string */ public function wp_mime_type_icon( $icon, $mime, $post_id ) { _deprecated_function( __METHOD__, 'jetpack-11.3', 'Automattic\Jetpack\VideoPress\Attachment_Handler::wp_mime_type_icon' ); return Attachment_Handler::wp_mime_type_icon( $icon, $mime, $post_id ); } /** * Filter the list of supported video formats. * * @param array $extensions Supported video formats. * * @deprecated 11.3 * * @return array */ public function add_videopress_extenstion( $extensions ) { _deprecated_function( __METHOD__, 'jetpack-11.3', 'Automattic\Jetpack\VideoPress\Attachment_Handler::add_videopress_extenstion' ); return Attachment_Handler::add_videopress_extenstion( $extensions ); } } // Initialize the module. Jetpack_VideoPress::init(); videopress/class.videopress-cli.php 0000644 00000012411 15174711637 0013507 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * VideoPress CLI * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } if ( defined( 'WP_CLI' ) && WP_CLI ) { /** * VideoPress command line utilities. */ class VideoPress_CLI extends WP_CLI_Command { /** * Import a VideoPress Video * * ## OPTIONS * * <guid>: Import the video with the specified guid * * ## EXAMPLES * * wp videopress import kUJmAcSf * * @param array $args CLI arguments. */ public function import( $args ) { $guid = $args[0]; $attachment_id = create_local_media_library_for_videopress_guid( $guid ); if ( $attachment_id && ! is_wp_error( $attachment_id ) ) { /* translators: %d: attachment id */ WP_CLI::success( sprintf( __( 'The video has been imported as Attachment ID %d', 'jetpack' ), $attachment_id ) ); } else { WP_CLI::error( __( 'An error has been encountered.', 'jetpack' ) ); } } /** * Manually runs the job to cleanup videos from the media library that failed during the upload process. * * ## EXAMPLES * * wp videopress cleanup_videos */ public function cleanup_videos() { $num_cleaned = videopress_cleanup_media_library(); /* translators: %d: number of videos cleaned */ WP_CLI::success( sprintf( _n( 'Cleaned up %d video.', 'Cleaned up a total of %d videos.', $num_cleaned, 'jetpack' ), $num_cleaned ) ); } /** * List out all of the crons that can be run. * * ## EXAMPLES * * wp videopress list_crons */ public function list_crons() { $scheduler = VideoPress_Scheduler::init(); $crons = $scheduler->get_crons(); $crons_count = is_countable( $crons ) ? count( $crons ) : 0; $schedules = wp_get_schedules(); if ( $crons_count === 0 ) { WP_CLI::success( __( 'Found no available cron jobs.', 'jetpack' ) ); } else { /* translators: %d is the number of crons */ WP_CLI::success( sprintf( _n( 'Found %d available cron job.', 'Found %d available cron jobs.', $crons_count, 'jetpack' ), $crons_count ) ); } foreach ( $crons as $cron_name => $cron ) { $interval = isset( $schedules[ $cron['interval'] ]['display'] ) ? $schedules[ $cron['interval'] ]['display'] : $cron['interval']; $runs_next = $scheduler->check_cron( $cron_name ); $status = $runs_next ? sprintf( 'Scheduled - Runs Next at %s GMT', gmdate( 'Y-m-d H:i:s', $runs_next ) ) : 'Not Scheduled'; WP_CLI::log( 'Name: ' . $cron_name ); WP_CLI::log( 'Method: ' . $cron['method'] ); WP_CLI::log( 'Interval: ' . $interval ); WP_CLI::log( 'Status: ' . $status ); } } /** * Checks for the current status of a cron job. * * ## OPTIONS * * <cron_name>: The name of the cron job to check * * ## EXAMPLES * * wp videopress cron_status cleanup * * @param array $args CLI args. */ public function cron_status( $args ) { if ( ! isset( $args[0] ) ) { return WP_CLI::error( __( 'You need to provide the name of the cronjob to schedule.', 'jetpack' ) ); } $scheduler = VideoPress_Scheduler::init(); if ( ! $scheduler->is_cron_valid( $args[0] ) ) { /* translators: name of a cron job */ WP_CLI::error( sprintf( __( 'There is no cron named %s.', 'jetpack' ), $args[0] ) ); } $time = $scheduler->check_cron( $args[0] ); if ( ! $time ) { WP_CLI::success( __( 'The cron is not scheduled to run.', 'jetpack' ) ); } else { /* translators: date/time */ WP_CLI::success( sprintf( __( 'Cron will run at: %s GMT', 'jetpack' ), gmdate( 'Y-m-d H:i:s', $time ) ) ); } } /** * Actives the given cron job * * ## OPTIONS * * <cron_name>: The name of the cron job to check * * ## EXAMPLES * * wp videopress activate_cron cleanup * * @param array $args CLI args. */ public function activate_cron( $args ) { if ( ! isset( $args[0] ) ) { WP_CLI::error( __( 'You need to provide the name of the cronjob to schedule.', 'jetpack' ) ); } $scheduler = VideoPress_Scheduler::init(); if ( ! $scheduler->is_cron_valid( $args[0] ) ) { /* translators: name of a cron job */ WP_CLI::error( sprintf( __( 'There is no cron named %s.', 'jetpack' ), $args[0] ) ); } $scheduler->activate_cron( $args[0] ); /* translators: name of a cron job */ WP_CLI::success( sprintf( __( 'The cron named `%s` was scheduled.', 'jetpack' ), $args[0] ) ); } /** * Actives the given cron job * * ## OPTIONS * * <cron_name>: The name of the cron job to check * * ## EXAMPLES * * wp videopress deactivate_cron cleanup * * @param array $args CLI args. */ public function deactivate_cron( $args ) { if ( ! isset( $args[0] ) ) { WP_CLI::error( __( 'You need to provide the name of the cronjob to schedule.', 'jetpack' ) ); } $scheduler = VideoPress_Scheduler::init(); if ( ! $scheduler->is_cron_valid( $args[0] ) ) { /* translators: name of a cron job */ WP_CLI::error( sprintf( __( 'There is no cron named %s.', 'jetpack' ), $args[0] ) ); } $scheduler->deactivate_cron( $args[0] ); /* translators: name of a cron job */ WP_CLI::success( sprintf( __( 'The cron named `%s` was removed from the schedule.', 'jetpack' ), $args[0] ) ); } } WP_CLI::add_command( 'videopress', 'VideoPress_CLI' ); } videopress/shortcode.php 0000644 00000022362 15174711637 0011453 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VideoPress Shortcode Handler * * This file may or may not be included from the Jetpack VideoPress module. */ class VideoPress_Shortcode { /** * Singleton VideoPress_Shortcode instance. * * @var VideoPress_Shortcode */ protected static $instance; /** * VideoPress_Shortcode constructor. */ protected function __construct() { // Only add the shortcode if it hasn't already been added by the standalone VideoPress plugin. if ( ! shortcode_exists( 'videopress' ) ) { add_shortcode( 'videopress', array( $this, 'shortcode_callback' ) ); add_shortcode( 'wpvideo', array( $this, 'shortcode_callback' ) ); add_filter( 'wp_video_shortcode_override', array( $this, 'video_shortcode_override' ), 10, 4 ); } $this->add_video_embed_hander(); } /** * VideoPress_Shortcode initialization. * * @return VideoPress_Shortcode */ public static function initialize() { if ( ! isset( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Translate a 'videopress' or 'wpvideo' shortcode and arguments into a video player display. * * Expected input formats: * * [videopress OcobLTqC] * [wpvideo OcobLTqC] * * @link https://codex.wordpress.org/Shortcode_API Shortcode API * @param array $attr shortcode attributes. * @return string HTML markup or blank string on fail */ public function shortcode_callback( $attr ) { global $content_width; /** * We only accept GUIDs as a first unnamed argument. */ $guid = isset( $attr[0] ) ? $attr[0] : null; if ( isset( $attr['postid'] ) ) { $guid = get_post_meta( $attr['postid'], 'videopress_guid', true ); } /** * Make sure the GUID passed in matches how actual GUIDs are formatted. */ if ( ! videopress_is_valid_guid( $guid ) ) { return ''; } /** * Set the defaults */ $defaults = array( 'w' => 0, // Width of the video player, in pixels 'at' => 0, // How many seconds in to initially seek to 'hd' => true, // Whether to display a high definition version 'loop' => false, // Whether to loop the video repeatedly 'freedom' => false, // Whether to use only free/libre codecs 'autoplay' => false, // Whether to autoplay the video on load 'permalink' => true, // Whether to display the permalink to the video 'flashonly' => false, // Whether to support the Flash player exclusively 'defaultlangcode' => false, // Default language code 'cover' => true, // Whether to scale the video to its container. 'muted' => false, // Whether the video should start without sound. 'controls' => true, // Whether the video should display controls. 'playsinline' => false, // Whether the video should be allowed to play inline (for browsers that support this). 'useaveragecolor' => false, // Whether the video should use the seekbar automatic average color. 'preloadcontent' => 'metadata', // Setting for how the browser should preload the video (none, metadata, auto). ); // Make sure "false" will be actually false. foreach ( $attr as $key => $value ) { if ( is_string( $value ) && 'false' === strtolower( $value ) ) { $attr[ $key ] = false; } } if ( isset( $attr['preload'] ) ) { $attr['preloadcontent'] = $attr['preload']; } $attr = shortcode_atts( $defaults, $attr, 'videopress' ); /** * Cast the attributes, post-input. */ $attr['width'] = absint( $attr['w'] ); $attr['hd'] = (bool) $attr['hd']; $attr['freedom'] = (bool) $attr['freedom']; /** * If the provided width is less than the minimum allowed * width, or greater than `$content_width` ignore. */ if ( $attr['width'] < VIDEOPRESS_MIN_WIDTH ) { $attr['width'] = 0; } elseif ( isset( $content_width ) && $content_width > VIDEOPRESS_MIN_WIDTH && $attr['width'] > $content_width ) { $attr['width'] = 0; } /** * If there was an invalid or unspecified width, set the width equal to the theme's `$content_width`. */ if ( 0 === $attr['width'] && isset( $content_width ) && $content_width >= VIDEOPRESS_MIN_WIDTH ) { $attr['width'] = (int) $content_width; } /** * If the width isn't an even number, reduce it by one (making it even). */ if ( 1 === ( $attr['width'] % 2 ) ) { --$attr['width']; } /** * Filter the default VideoPress shortcode options. * * @module videopress * * @since 2.5.0 * * @param array $args Array of VideoPress shortcode options. */ $options = apply_filters( 'videopress_shortcode_options', array( 'at' => (int) $attr['at'], 'hd' => $attr['hd'], 'cover' => (bool) $attr['cover'], 'loop' => $attr['loop'], 'freedom' => $attr['freedom'], 'autoplay' => $attr['autoplay'], 'permalink' => $attr['permalink'], 'force_flash' => (bool) $attr['flashonly'], 'defaultlangcode' => $attr['defaultlangcode'], 'forcestatic' => false, // This used to be a displayed option, but now is only. 'muted' => $attr['muted'], 'controls' => $attr['controls'], 'playsinline' => $attr['playsinline'], 'useAverageColor' => (bool) $attr['useaveragecolor'], // The casing is intentional, shortcode params are lowercase, but player expects useAverageColor 'preloadContent' => $attr['preloadcontent'], // The casing is intentional, shortcode params are lowercase, but player expects preloadContent // accessible via the `videopress_shortcode_options` filter. ) ); // Register VideoPress scripts wp_register_script( 'videopress', 'https://v0.wordpress.com/js/videopress.js', array( 'jquery', 'swfobject' ), '1.09', false ); require_once __DIR__ . '/class.videopress-video.php'; require_once __DIR__ . '/class.videopress-player.php'; $player = new VideoPress_Player( $guid, $attr['width'], $options ); if ( is_feed() ) { return $player->as_xml(); } else { return $player->as_html(); } } /** * Override the standard video short tag to also process videopress files as well. * * This will, parse the src given, and if it is a videopress file, it will parse as the * VideoPress shortcode instead. * * @param string $html Empty variable to be replaced with shortcode markup. * @param array $attr Attributes of the video shortcode. * @param string $content Video shortcode content. * @param int $instance Unique numeric ID of this video shortcode instance. * * @return string */ public function video_shortcode_override( $html, $attr, $content, $instance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $videopress_guid = null; if ( isset( $attr['videopress_guid'] ) ) { $videopress_guid = $attr['videopress_guid']; } else { // Handle the different possible url attributes $url_keys = array( 'src', 'mp4' ); foreach ( $url_keys as $key ) { if ( isset( $attr[ $key ] ) ) { $url = $attr[ $key ]; // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText if ( preg_match( '@videos.(videopress\.com|files\.wordpress\.com)/([a-z0-9]{8})/@i', $url, $matches ) ) { $videopress_guid = $matches[2]; } // Also test for videopress oembed url, which is used by the Video Media Widget. if ( ! $videopress_guid && preg_match( '@https://videopress.com/v/([a-z0-9]{8})@i', $url, $matches ) ) { $videopress_guid = $matches[1]; } // Also test for old v.wordpress.com oembed URL. if ( ! $videopress_guid && preg_match( '|^https?://v\.wordpress\.com/([a-zA-Z\d]{8})(.+)?$|i', $url, $matches ) ) { // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText $videopress_guid = $matches[1]; } break; } } } if ( $videopress_guid ) { $videopress_attr = array( $videopress_guid ); if ( isset( $attr['width'] ) ) { $videopress_attr['w'] = (int) $attr['width']; } if ( isset( $attr['muted'] ) ) { $videopress_attr['muted'] = $attr['muted']; } if ( isset( $attr['autoplay'] ) ) { $videopress_attr['autoplay'] = $attr['autoplay']; } if ( isset( $attr['loop'] ) ) { $videopress_attr['loop'] = $attr['loop']; } // The core video block doesn't support the cover attribute, setting it to false for consistency. $videopress_attr['cover'] = false; // Then display the VideoPress version of the stored GUID! return $this->shortcode_callback( $videopress_attr ); } return ''; } /** * Register a VideoPress handler for direct links to .mov files (and potential other non-handled types later). */ public function add_video_embed_hander() { // These are the video extensions that VideoPress can transcode and considers video as well (even if core does not). $extensions = array( 'mov' ); $override_extensions = implode( '|', $extensions ); $regex = "#^https?://videos.(videopress.com|files.wordpress.com)/.+?.($override_extensions)$#i"; /** This filter is already documented in core/wp-includes/embed.php */ $filter = apply_filters( 'wp_video_embed_handler', 'wp_embed_handler_video' ); wp_embed_register_handler( 'video', $regex, $filter, 10 ); } } VideoPress_Shortcode::initialize(); videopress/videopress-admin-rtl.min.css 0000644 00000002612 15174711637 0014310 0 ustar 00 .videopress-modal-backdrop{background:#000;height:100%;opacity:.7;overflow:hidden;position:absolute;top:0;width:100%;z-index:100}.videopress-modal{background:#fff;border-bottom-left-radius:2px;border-bottom-right-radius:2px;box-shadow:-2px 2px 5px 2px #00000080;margin-right:-220px;overflow:hidden;padding:10px 20px;position:absolute;right:50%;top:0;width:440px;z-index:101}.videopress-modal .submit{padding:10px 0 5px;text-align:left}.videopress-preview{background:#000;display:block;float:left;margin-top:18px;min-height:97px;text-decoration:none;width:65%}.vp-preview span.videopress-preview-unavailable{float:left;margin-left:0;text-align:right;width:65%}.videopress-preview img{float:right;width:100%}.videopress-preview span{color:#fff!important;display:block;padding-top:40px;text-align:center}.vp-setting .help{margin:0 35% 4px 0}.media-sidebar .vp-setting input[type=checkbox]{float:right;margin-top:10px}.vp-setting label{float:right;margin:8px 5px 0 8px;max-width:135px}.vp-setting input[type=radio]{float:right;margin-top:9px;width:auto}.vp-preview span{margin-top:18px}.uploader-videopress{margin:16px}.uploader-videopress .videopress-errors div{margin:16px 0}.compat-field-allow_download input[type=checkbox],.compat-field-display_embed input[type=checkbox],.compat-field-video-rating input[type=radio]{margin-left:5px!important;margin-right:5px!important;margin-top:-1px!important;vertical-align:middle} videopress/class.videopress-player.php 0000644 00000106653 15174711637 0014250 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName use Automattic\Jetpack\VideoPress\Jwt_Token_Bridge; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VideoPress playback module markup generator. * * @since 1.3 */ class VideoPress_Player { /** * Video data for the requested guid and maximum width * * @since 1.3 * @var VideoPress_Video */ protected $video; /** * DOM identifier of the video container * * @var string * @since 1.3 */ protected $video_container_id; /** * DOM identifier of the video element (video, object, embed) * * @var string * @since 1.3 */ protected $video_id; /** * Array of playback options: force_flash or freedom * * @var array * @since 1.3 */ protected $options; /** * Array of video GUIDs shown and their counts, * moved from the old VideoPress class. * * @var array */ public static $shown = array(); /** * Fallback video title. * * @var ?string */ protected $title; /** * Initiate a player object based on shortcode values and possible blog-level option overrides * * @since 1.3 * @param string $guid VideoPress unique identifier. * @param int $maxwidth Maximum desired width of the video player if specified. * @param array $options Player customizations. */ public function __construct( $guid, $maxwidth = 0, $options = array() ) { if ( empty( self::$shown[ $guid ] ) ) { self::$shown[ $guid ] = 0; } ++self::$shown[ $guid ]; $this->video_container_id = 'v-' . $guid . '-' . self::$shown[ $guid ]; $this->video_id = $this->video_container_id . '-video'; if ( is_array( $options ) ) { $this->options = $options; } else { $this->options = array(); } // set up the video $cache_key = null; // disable cache in debug mode if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) { $cached_video = null; } else { $cache_key_pieces = array( 'video' ); if ( is_multisite() && is_subdomain_install() ) { $cache_key_pieces[] = get_current_blog_id(); } $cache_key_pieces[] = $guid; if ( $maxwidth > 0 ) { $cache_key_pieces[] = $maxwidth; } if ( is_ssl() ) { $cache_key_pieces[] = 'ssl'; } $cache_key = implode( '-', $cache_key_pieces ); unset( $cache_key_pieces ); $cached_video = wp_cache_get( $cache_key, 'video' ); } if ( empty( $cached_video ) ) { $video = new VideoPress_Video( $guid, $maxwidth ); if ( isset( $video->error ) ) { $this->video = $video->error; return; } elseif ( is_wp_error( $video ) ) { $this->video = $video; return; } $this->video = $video; unset( $video ); if ( ! defined( 'WP_DEBUG' ) || WP_DEBUG !== true ) { $expire = 3600; if ( isset( $this->video->expires ) && is_int( $this->video->expires ) ) { $expires_diff = time() - $this->video->expires; if ( $expires_diff > 0 && $expires_diff < 86400 ) { // allowed range: 1 second to 1 day $expire = $expires_diff; } unset( $expires_diff ); } wp_cache_set( $cache_key, serialize( $this->video ), 'video', $expire ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize unset( $expire ); } } else { $this->video = unserialize( $cached_video ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Make sure to unserialize as VideoPress_Video class. } unset( $cache_key ); unset( $cached_video ); } /** * Wrap output in a VideoPress player container. * * @since 1.3 * @param string $content HTML string. * @return string HTML string or blank string if nothing to wrap. */ private function html_wrapper( $content ) { if ( empty( $content ) ) { return ''; } else { return '<div id="' . esc_attr( $this->video_container_id ) . '" class="video-player">' . $content . '</div>'; } } /** * Output content suitable for a feed reader displaying RSS or Atom feeds * We do not display error messages in the feed view due to caching concerns. * Flash content presented using <embed> markup for feed reader compatibility. * * @since 1.3 * @return string HTML string or empty string if error */ public function as_xml() { if ( empty( $this->video ) || is_wp_error( $this->video ) ) { return ''; } if ( isset( $this->options['force_flash'] ) && true === $this->options['force_flash'] ) { $content = $this->flash_embed(); } else { $content = $this->html5_static(); } return $this->html_wrapper( $content ); } /** * Video player markup for best matching the current request and publisher options * * @since 1.3 * @return string HTML markup string or empty string if no video property found */ public function as_html() { if ( empty( $this->video ) ) { $content = ''; } elseif ( is_wp_error( $this->video ) ) { $content = $this->error_message( $this->video ); } elseif ( isset( $this->options['force_flash'] ) && true === $this->options['force_flash'] ) { $content = $this->flash_object(); } elseif ( isset( $this->video->restricted_embed ) && true === $this->video->restricted_embed ) { if ( $this->options['forcestatic'] ) { $content = $this->flash_object(); } else { $content = $this->html5_dynamic(); } } elseif ( isset( $this->options['freedom'] ) && true === $this->options['freedom'] ) { $content = $this->html5_static(); } else { $content = $this->html5_dynamic(); } return $this->html_wrapper( $content ); } /** * Display an error message to users capable of doing something about the error * * @since 1.3 * @uses current_user_can() to test if current user has edit_posts capability. * @param WP_Error $error WordPress error. * @return string HTML string */ private function error_message( $error ) { if ( ! current_user_can( 'edit_posts' ) || empty( $error ) ) { return ''; } $html = '<div class="videopress-error" style="background-color:rgb(255,0,0);color:rgb(255,255,255);font-family:font-family:\'Helvetica Neue\',Arial,Helvetica,\'Nimbus Sans L\',sans-serif;font-size:140%;min-height:10em;padding-top:1.5em;padding-bottom:1.5em">'; /* translators: %s is 'VideoPress' */ $html .= '<h1 style="font-size:180%;font-style:bold;line-height:130%;text-decoration:underline">' . esc_html( sprintf( __( '%s Error', 'jetpack' ), 'VideoPress' ) ) . '</h1>'; foreach ( $error->get_error_messages() as $message ) { $html .= $message; } $html .= '</div>'; return $html; } /** * Rating agencies and industry associations require a potential viewer verify their age before a video or its poster frame are displayed. * Content rated for audiences 17 years of age or older requires such verification across multiple rating agencies and industry associations * * @since 1.3 * @return bool true if video requires the viewer verify they are 17 years of age or older */ private function age_gate_required() { if ( isset( $this->video->age_rating ) && $this->video->age_rating >= 17 ) { return true; } else { return false; } } /** * Select a date of birth using HTML form elements. * * @since 1.5 * @return string HTML markup */ private function html_age_gate() { global $wp_locale; $text_align = 'left'; if ( $this->video->text_direction === 'rtl' ) { $text_align = 'right'; } $html = '<div class="videopress-age-gate" style="margin:0 60px">'; $html .= '<p class="instructions" style="color:rgb(255, 255, 255);font-size:21px;padding-top:60px;padding-bottom:20px;text-align:' . $text_align . '">' . esc_html( __( 'This video is intended for mature audiences.', 'jetpack' ) ) . '<br />' . esc_html( __( 'Please verify your birthday.', 'jetpack' ) ) . '</p>'; $html .= '<fieldset id="birthday" style="border:0 none;text-align:' . $text_align . ';padding:0;">'; $inputs_style = 'border:1px solid #444;margin-'; if ( $this->video->text_direction === 'rtl' ) { $inputs_style .= 'left'; } else { $inputs_style .= 'right'; } $inputs_style .= ':10px;background-color:rgb(0, 0, 0);font-size:14px;color:rgb(255,255,255);padding:4px 6px;line-height: 2em;vertical-align: middle'; /** * Display a list of months in the Gregorian calendar. * Set values to 0-based to match JavaScript Date. * * @link https://developer.mozilla.org/en/JavaScript/Reference/global_objects/date Mozilla JavaScript Reference: Date */ $html .= '<select name="month" style="' . $inputs_style . '">'; for ( $i = 0; $i < 12; $i++ ) { $html .= '<option value="' . esc_attr( $i ) . '">' . esc_html( $wp_locale->get_month( $i + 1 ) ) . '</option>'; } $html .= '</select>'; /** * Todo: numdays variance by month. */ $html .= '<select name="day" style="' . $inputs_style . '">'; for ( $i = 1; $i < 32; $i++ ) { $html .= '<option>' . $i . '</option>'; } $html .= '</select>'; /** * Current record for human life is 122. Go back 130 years and no one is left out. * Don't ask infants younger than 2 for their birthday * Default to 13 */ $html .= '<select name="year" style="' . $inputs_style . '">'; $start_year = gmdate( 'Y' ) - 2; $default_year = $start_year - 11; $end_year = $start_year - 128; for ( $year = $start_year; $year > $end_year; $year-- ) { $html .= '<option'; if ( $year === $default_year ) { $html .= ' selected="selected"'; } $html .= '>' . $year . '</option>'; } unset( $start_year ); unset( $default_year ); unset( $end_year ); $html .= '</select>'; $html .= '<input type="submit" value="' . __( 'Submit', 'jetpack' ) . '" style="cursor:pointer;border-radius: 1em;border:1px solid #333;background-color:#333;background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0, #444), color-stop(1, #111) );background:-moz-linear-gradient(center top, #444 0%, #111 100%);font-size:13px;padding:4px 10px 5px;line-height:1em;vertical-align:top;color:white;text-decoration:none;margin:0" />'; $html .= '</fieldset>'; $html .= '<p style="padding-top:20px;padding-bottom:60px;text-align:' . $text_align . ';"><a rel="nofollow noopener noreferrer" href="https://videopress.com/" target="_blank" style="color:rgb(128,128,128);text-decoration:underline;font-size:15px">' . __( 'More information', 'jetpack' ) . '</a></p>'; $html .= '</div>'; return $html; } /** * Return HTML5 video static markup for the given video parameters. * Use default browser player controls. * No Flash fallback. * * @since 1.2 * @link https://html.spec.whatwg.org/multipage/media.html#the-video-element HTML5 video * @return string HTML5 video element and children */ private function html5_static() { wp_enqueue_script( 'videopress' ); $thumbnail = esc_url( $this->video->poster_frame_uri ); $html = "<video id=\"{$this->video_id}\" width=\"{$this->video->calculated_width}\" height=\"{$this->video->calculated_height}\" poster=\"$thumbnail\" controls=\"true\""; $preload = 'metadata'; if ( isset( $this->options['preloadContent'] ) && videopress_is_valid_preload( $this->options['preloadContent'] ) ) { $preload = $this->options['preloadContent']; } if ( isset( $this->options['autoplay'] ) && $this->options['autoplay'] === true ) { $html .= ' autoplay="true"'; } else { $html .= ' preload="' . esc_attr( $preload ) . '"'; } if ( isset( $this->video->text_direction ) ) { $html .= ' dir="' . esc_attr( $this->video->text_direction ) . '"'; } if ( isset( $this->video->language ) ) { $html .= ' lang="' . esc_attr( $this->video->language ) . '"'; } $html .= '>'; if ( ( ! isset( $this->options['freedom'] ) || $this->options['freedom'] === false ) && isset( $this->video->videos->mp4 ) ) { $mp4 = $this->video->videos->mp4->url; if ( ! empty( $mp4 ) ) { $html .= '<source src="' . esc_url( $mp4 ) . '" type="video/mp4; codecs="' . esc_attr( $this->video->videos->mp4->codecs ) . '"" />'; } unset( $mp4 ); } if ( isset( $this->video->videos->ogv ) ) { $ogg = $this->video->videos->ogv->url; if ( ! empty( $ogg ) ) { $html .= '<source src="' . esc_url( $ogg ) . '" type="video/ogg; codecs="' . esc_attr( $this->video->videos->ogv->codecs ) . '"" />'; } unset( $ogg ); } $html .= '<div><img alt="'; if ( isset( $this->video->title ) ) { $html .= esc_attr( $this->video->title ); } $html .= '" src="' . $thumbnail . '" width="' . $this->video->calculated_width . '" height="' . $this->video->calculated_height . '" /></div>'; if ( isset( $this->options['freedom'] ) && $this->options['freedom'] === true ) { /* translators: %s url to the gnu.org website */ $html .= '<p class="robots-nocontent">' . sprintf( __( 'You do not have sufficient <a rel="nofollow noopener noreferrer" href="%s" target="_blank">freedom levels</a> to view this video. Support free software and upgrade.', 'jetpack' ), 'https://www.gnu.org/philosophy/free-sw.html' ) . '</p>'; } elseif ( isset( $this->video->title ) ) { $html .= '<p>' . esc_html( $this->video->title ) . '</p>'; } $html .= '</video>'; return $html; } /** * Click to play dynamic HTML5-capable player. * The player displays a video preview section including poster frame, * video title, play button and watermark on the original page load * and calculates the playback capabilities of the browser. The video player * is loaded when the visitor clicks on the video preview area. * If Flash Player 10 or above is available the browser will display * the Flash version of the video. If HTML5 video appears to be supported * and the browser may be capable of MP4 (H.264, AAC) or OGV (Theora, Vorbis) * playback the browser will display its native HTML5 player. * * @since 1.5 * @return string HTML markup */ private function html5_dynamic() { /** * Filter the VideoPress legacy player feature * * This filter allows you to control whether the legacy VideoPress player should be used * instead of the improved one. * * @module videopress * * @since 3.7.0 * * @param boolean $videopress_use_legacy_player */ if ( ! apply_filters( 'jetpack_videopress_use_legacy_player', false ) ) { return $this->html5_dynamic_next(); } wp_enqueue_script( 'videopress' ); $video_placeholder_id = $this->video_container_id . '-placeholder'; $age_gate_required = $this->age_gate_required(); $width = absint( $this->video->calculated_width ); $height = absint( $this->video->calculated_height ); $html = '<div id="' . $video_placeholder_id . '" class="videopress-placeholder" style="'; if ( $age_gate_required ) { $html .= "min-width:{$width}px;min-height:{$height}px"; } else { $html .= "width:{$width}px;height:{$height}px"; } $html .= ';display:none;cursor:pointer !important;position:relative;'; if ( isset( $this->video->skin ) && isset( $this->video->skin->background_color ) ) { $html .= 'background-color:' . esc_attr( $this->video->skin->background_color ) . ';'; } $html .= 'font-family: \'Helvetica Neue\',Arial,Helvetica,\'Nimbus Sans L\',sans-serif;font-weight:bold;font-size:18px">' . PHP_EOL; /** * Do not display a poster frame, title, or any other content hints for mature content. */ if ( ! $age_gate_required ) { if ( ! empty( $this->video->title ) ) { $html .= '<div class="videopress-title" style="display:inline;position:absolute;margin:20px 20px 0 20px;padding:4px 8px;vertical-align:top;text-align:'; if ( $this->video->text_direction === 'rtl' ) { $html .= 'right" dir="rtl"'; } else { $html .= 'left" dir="ltr"'; } if ( isset( $this->video->language ) ) { $html .= ' lang="' . esc_attr( $this->video->language ) . '"'; } $html .= '><span style="padding:3px 0;line-height:1.5em;'; if ( isset( $this->video->skin ) && isset( $this->video->skin->background_color ) ) { $html .= 'background-color:'; if ( $this->video->skin->background_color === 'rgb(0,0,0)' ) { $html .= 'rgba(0,0,0,0.8)'; } else { $html .= esc_attr( $this->video->skin->background_color ); } $html .= ';'; } $html .= 'color:rgb(255,255,255)">' . esc_html( $this->video->title ) . '</span></div>'; } $html .= '<img class="videopress-poster" alt="'; if ( ! empty( $this->video->title ) ) { /* translators: %s is the video title */ $html .= esc_attr( $this->video->title ) . '" title="' . esc_attr( sprintf( _x( 'Watch: %s', 'watch a video title', 'jetpack' ), $this->video->title ) ); } $html .= '" src="' . esc_url( $this->video->poster_frame_uri, array( 'http', 'https' ) ) . '" width="' . $width . '" height="' . $height . '" />' . PHP_EOL; // style a play button hovered over the poster frame $html .= '<div class="play-button"><span style="z-index:2;display:block;position:absolute;top:50%;left:50%;text-align:center;vertical-align:middle;color:rgb(255,255,255);opacity:0.9;margin:0 0 0 -0.45em;padding:0;line-height:0;font-size:500%;text-shadow:0 0 40px rgba(0,0,0,0.5)">▶</span></div>' . PHP_EOL; // watermark if ( isset( $this->video->skin ) && isset( $this->video->skin->watermark ) ) { $html .= '<div style="position:relative;margin-top:-40px;height:25px;margin-bottom:35px;'; if ( $this->video->text_direction === 'rtl' ) { $html .= 'margin-left:20px;text-align:left;'; } else { $html .= 'margin-right:20px;text-align:right;'; } $html .= 'vertical-align:bottom;z-index:3">'; $html .= '<img alt="" src="' . esc_url( $this->video->skin->watermark, array( 'http', 'https' ) ) . '" width="90" height="13" style="background-color:transparent;background-image:none;background-repeat:no-repeat;border:none;margin:0;padding:0"/>'; $html .= '</div>' . PHP_EOL; } } $data = array( 'blog' => absint( $this->video->blog_id ), 'post' => absint( $this->video->post_id ), 'duration' => absint( $this->video->duration ), 'poster' => esc_url_raw( $this->video->poster_frame_uri, array( 'http', 'https' ) ), 'hd' => (bool) $this->options['hd'], ); if ( isset( $this->video->videos ) ) { if ( isset( $this->video->videos->mp4 ) && isset( $this->video->videos->mp4->url ) ) { $data['mp4'] = array( 'size' => $this->video->videos->mp4->format, 'uri' => esc_url_raw( $this->video->videos->mp4->url, array( 'http', 'https' ) ), ); } if ( isset( $this->video->videos->ogv ) && isset( $this->video->videos->ogv->url ) ) { $data['ogv'] = array( 'size' => 'std', 'uri' => esc_url_raw( $this->video->videos->ogv->url, array( 'http', 'https' ) ), ); } } $locale = array( 'dir' => $this->video->text_direction ); if ( isset( $this->video->language ) ) { $locale['lang'] = $this->video->language; } $data['locale'] = $locale; unset( $locale ); $guid = $this->video->guid; $guid_js = wp_json_encode( $guid, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); $html .= '<script type="text/javascript">' . PHP_EOL; $html .= 'jQuery(document).ready(function() {'; $html .= 'if ( !jQuery.VideoPress.data[' . $guid_js . '] ) { jQuery.VideoPress.data[' . $guid_js . '] = new Array(); }' . PHP_EOL; $html .= 'jQuery.VideoPress.data[' . $guid_js . '][' . self::$shown[ $guid ] . ']=' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';' . PHP_EOL; unset( $data ); $jq_container = wp_json_encode( '#' . $this->video_container_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); $jq_placeholder = wp_json_encode( '#' . $video_placeholder_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); $player_config = "{width:{$width},height:{$height},"; if ( isset( $this->options['freedom'] ) && $this->options['freedom'] === true ) { $player_config .= 'freedom:"true",'; } $player_config .= 'container:jQuery(' . $jq_container . ')}'; $html .= "jQuery({$jq_placeholder}).show(0,function(){jQuery.VideoPress.analytics.impression({$guid_js})});" . PHP_EOL; if ( $age_gate_required ) { $html .= 'if ( jQuery.VideoPress.support.flash() ) {' . PHP_EOL; /** * Insert alternative content for Flash players. * * @link https://github.com/swfobject/swfobject/wiki/SWFObject-API#swfobjectembedswfswfurlstr-replaceelemidstr-widthstr-heightstr-swfversionstr-xiswfurlstr-flashvarsobj-parobj-attobj-callbackfn */ $html .= 'swfobject.embedSWF(' . implode( ',', array( 'jQuery.VideoPress.video.flash.player_uri', wp_json_encode( $this->video_container_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ), wp_json_encode( $width, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ), wp_json_encode( $height, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ), 'jQuery.VideoPress.video.flash.min_version', 'jQuery.VideoPress.video.flash.expressinstall', // attempt to upgrade the Flash player if less than min_version. requires a 310x137 container or larger but we will always try to include '{guid:' . $guid_js . '}', // FlashVars 'jQuery.VideoPress.video.flash.params', 'null', // no attributes 'jQuery.VideoPress.video.flash.embedCallback', // error fallback ) ) . ');'; $html .= '} else {' . PHP_EOL; $html .= "if ( jQuery.VideoPress.video.prepare({$guid_js},{$player_config}," . self::$shown[ $guid ] . ') ) {' . PHP_EOL; $html .= 'if ( jQuery(' . $jq_container . ').data( "player" ) === "flash" ){jQuery.VideoPress.video.play(jQuery(' . wp_json_encode( '#' . $this->video_container_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . '));}else{'; $html .= 'jQuery(' . $jq_placeholder . ').html(' . wp_json_encode( $this->html_age_date(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ');' . PHP_EOL; $html .= 'jQuery(' . wp_json_encode( '#' . $video_placeholder_id . ' input[type="submit"]', JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ').one("click", function(event){jQuery.VideoPress.requirements.isSufficientAge(jQuery(' . $jq_container . '),' . absint( $this->video->age_rating ) . ')});' . PHP_EOL; $html .= '}}}' . PHP_EOL; } else { $html .= "if ( jQuery.VideoPress.video.prepare({$guid_js}, {$player_config}," . self::$shown[ $guid ] . ') ) {' . PHP_EOL; if ( isset( $this->options['autoplay'] ) && $this->options['autoplay'] === true ) { $html .= "jQuery.VideoPress.video.play(jQuery({$jq_container}));"; } else { $html .= 'jQuery(' . $jq_placeholder . ').one("click",function(){jQuery.VideoPress.video.play(jQuery(' . $jq_container . '))});'; } $html .= '}'; // close the jQuery(document).ready() function $html .= '});'; } $html .= '</script>' . PHP_EOL; $html .= '</div>' . PHP_EOL; /* * JavaScript required */ $noun = __( 'this video', 'jetpack' ); if ( ! $age_gate_required ) { $vid_type = ''; if ( ( isset( $this->options['freedom'] ) && $this->options['freedom'] === true ) && ( isset( $this->video->videos->ogv ) && isset( $this->video->videos->ogv->url ) ) ) { $vid_type = 'ogv'; } elseif ( isset( $this->video->videos->mp4 ) && isset( $this->video->videos->mp4->url ) ) { $vid_type = 'mp4'; } elseif ( isset( $this->video->videos->ogv ) && isset( $this->video->videos->ogv->url ) ) { $vid_type = 'ogv'; } if ( $vid_type !== '' ) { $noun = '<a '; if ( isset( $this->video->language ) ) { $noun .= 'hreflang="' . esc_attr( $this->video->language ) . '" '; } if ( $vid_type === 'mp4' ) { $noun .= 'type="video/mp4" href="' . esc_url( $this->video->videos->mp4->url, array( 'http', 'https' ) ); } elseif ( $vid_type === 'ogv' ) { $noun .= 'type="video/ogv" href="' . esc_url( $this->video->videos->ogv->url, array( 'http', 'https' ) ); } $noun .= '">'; if ( isset( $this->video->title ) ) { $noun .= esc_html( $this->video->title ); } else { $noun .= __( 'this video', 'jetpack' ); } $noun .= '</a>'; } elseif ( ! empty( $this->title ) ) { $noun = esc_html( $this->title ); } unset( $vid_type ); } /* translators: %s video title or generic 'this video' string */ $html .= '<noscript><p>' . sprintf( _x( 'JavaScript required to play %s.', 'Play as in playback or view a movie', 'jetpack' ), $noun ) . '</p></noscript>'; return $html; } /** * Output for the non-legacy HTML5 player. */ public function html5_dynamic_next() { $video_container_id = 'v-' . $this->video->guid; Jwt_Token_Bridge::enqueue_jwt_token_bridge(); // Must not use iframes for IE11 due to a fullscreen bug if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && stristr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 'Trident/7.0; rv:11.0' ) ) { $iframe_embed = false; } else { /** * Filter the VideoPress iframe embed * * This filter allows you to control whether the videos will be embedded using an iframe. * Set this to false in order to use an in-page embed rather than an iframe. * * @module videopress * * @since 3.7.0 * * @param boolean $videopress_player_use_iframe */ $iframe_embed = apply_filters( 'jetpack_videopress_player_use_iframe', true ); } if ( ! array_key_exists( 'hd', $this->options ) ) { $this->options['hd'] = (bool) get_option( 'video_player_high_quality', false ); } if ( ! array_key_exists( 'cover', $this->options ) ) { $this->options['cover'] = true; } $videopress_options = array( 'width' => absint( $this->video->calculated_width ), 'height' => absint( $this->video->calculated_height ), ); foreach ( $this->options as $option => $value ) { switch ( $option ) { case 'at': if ( (int) $value ) { $videopress_options[ $option ] = (int) $value; } break; case 'autoplay': $option = 'autoPlay'; // Fall-through ok. case 'hd': case 'loop': case 'permalink': case 'cover': case 'muted': case 'controls': case 'playsinline': case 'useAverageColor': if ( in_array( $value, array( true, 1, 'true' ), true ) ) { $videopress_options[ $option ] = true; } elseif ( in_array( $value, array( false, 0, 'false' ), true ) ) { $videopress_options[ $option ] = false; } // phpcs:enable break; case 'defaultlangcode': $option = 'defaultLangCode'; if ( $value ) { $videopress_options[ $option ] = $value; } break; case 'preloadContent': if ( $value ) { $videopress_options['preloadContent'] = $value; } } } if ( $iframe_embed ) { $iframe_url = "https://videopress.com/embed/{$this->video->guid}"; foreach ( $videopress_options as $option => $value ) { if ( ! in_array( $option, array( 'width', 'height' ), true ) ) { // add_query_arg ignores false as a value, so replacing it with 0 // @phan-suppress-next-line PhanPluginSimplifyExpressionBool -- Probably it could, but semantically let's keep it as-is. $iframe_url = add_query_arg( $option, ( false === $value ) ? 0 : $value, $iframe_url ); } } $cover = $videopress_options['cover'] ? ' data-resize-to-parent="true"' : ''; $js_url = 'https://s0.wp.com/wp-content/plugins/video/assets/js/next/videopress-iframe.js'; // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript return "<iframe title='" . __( 'VideoPress Video Player', 'jetpack' ) . "' aria-label='" . __( 'VideoPress Video Player', 'jetpack' ) . "' width='" . esc_attr( $videopress_options['width'] ) . "' height='" . esc_attr( $videopress_options['height'] ) . "' src='" . esc_attr( $iframe_url ) . "' frameborder='0' allowfullscreen" . $cover . " allow='clipboard-write'></iframe>" . "<script src='" . esc_attr( $js_url ) . "'></script>"; } else { $videopress_options = wp_json_encode( $videopress_options, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); $js_url = 'https://s0.wp.com/wp-content/plugins/video/assets/js/videojs/videopress.js'; return "<div id='{$video_container_id}'></div> <script src='{$js_url}'></script> <script> videopress('{$this->video->guid}', document.querySelector('#{$video_container_id}'), {$videopress_options}); </script>"; // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript } } /** * Only allow legitimate Flash parameters and their values * * @since 1.2 * @link https://helpx.adobe.com/flash/kb/flash-object-embed-tag-attributes.html Flash object and embed attributes * @link https://helpx.adobe.com/flash/kb/font-outlines-device-fonts.html devicefont * @link https://helpx.adobe.com/flash/kb/control-access-scripts-host-web.html allowscriptaccess * @link https://www.adobe.com/devnet/flashplayer/articles/full_screen_mode.html full screen mode * @link https://help.adobe.com/en_US/as3/dev/WS1EFE2EDA-026D-4d14-864E-79DFD56F87C6.html allownetworking * @param array $flash_params Flash parameters expressed in key-value form. * @return array validated Flash parameters */ public static function esc_flash_params( $flash_params ) { $allowed_params = array( 'swliveconnect' => array( 'true', 'false' ), 'play' => array( 'true', 'false' ), 'loop' => array( 'true', 'false' ), 'menu' => array( 'true', 'false' ), 'quality' => array( 'low', 'autolow', 'autohigh', 'medium', 'high', 'best' ), 'scale' => array( 'default', 'noborder', 'exactfit', 'noscale' ), 'align' => array( 'l', 'r', 't' ), 'salign' => array( 'l', 'r', 't', 'tl', 'tr', 'bl', 'br' ), 'wmode' => array( 'window', 'opaque', 'transparent', 'direct', 'gpu' ), 'devicefont' => array( '_sans', '_serif', '_typewriter' ), 'allowscriptaccess' => array( 'always', 'samedomain', 'never' ), 'allownetworking' => array( 'all', 'internal', 'none' ), 'seamlesstabbing' => array( 'true', 'false' ), 'allowfullscreen' => array( 'true', 'false' ), 'fullScreenAspectRatio' => array( 'portrait', 'landscape' ), 'base', 'bgcolor', 'flashvars', ); $allowed_params_keys = array_keys( $allowed_params ); $filtered_params = array(); foreach ( $flash_params as $param => $value ) { if ( empty( $param ) || empty( $value ) ) { continue; } $param = strtolower( $param ); if ( in_array( $param, $allowed_params_keys, true ) ) { if ( isset( $allowed_params[ $param ] ) && is_array( $allowed_params[ $param ] ) ) { $value = strtolower( $value ); if ( in_array( $value, $allowed_params[ $param ], true ) ) { $filtered_params[ $param ] = $value; } } else { $filtered_params[ $param ] = $value; } } } unset( $allowed_params_keys ); /** * Flash specifies sameDomain, not samedomain. change from lowercase value for preciseness */ if ( isset( $filtered_params['allowscriptaccess'] ) && $filtered_params['allowscriptaccess'] === 'samedomain' ) { $filtered_params['allowscriptaccess'] = 'sameDomain'; } return $filtered_params; } /** * Filter Flash variables from the response, taking into consideration player options. * * @since 1.3 * @return array Flash variable key value pairs */ private function get_flash_variables() { if ( ! isset( $this->video->players->swf->vars ) ) { return array(); } $flashvars = (array) $this->video->players->swf->vars; if ( isset( $this->options['autoplay'] ) && $this->options['autoplay'] === true ) { $flashvars['autoPlay'] = 'true'; } return $flashvars; } /** * Validate and filter Flash parameters * * @since 1.3 * @return array Flash parameters passed through key and value validation */ private function get_flash_parameters() { if ( ! isset( $this->video->players->swf->params ) ) { return array(); } else { return self::esc_flash_params( /** * Filters the Flash parameters of the VideoPress player. * * @module videopress * * @since 1.2.0 * * @param array $this->video->players->swf->params Array of swf parameters for the VideoPress flash player. */ apply_filters( 'video_flash_params', (array) $this->video->players->swf->params, 10, 1 ) ); } } /** * Flash player markup in a HTML embed element. * * @since 1.1 * @link https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-embed-element embed element * @link http://www.google.com/support/reader/bin/answer.py?answer=70664 Google Reader markup support * @return string HTML markup. Embed element with no children */ private function flash_embed() { wp_enqueue_script( 'videopress' ); if ( ! isset( $this->video->players->swf ) || ! isset( $this->video->players->swf->url ) ) { return ''; } $embed = array( 'id' => $this->video_id, 'src' => esc_url_raw( $this->video->players->swf->url . '&' . http_build_query( $this->get_flash_variables(), '', '&' ), array( 'http', 'https' ) ), 'type' => 'application/x-shockwave-flash', 'width' => $this->video->calculated_width, 'height' => $this->video->calculated_height, ); if ( isset( $this->video->title ) ) { $embed['title'] = $this->video->title; } $embed = array_merge( $embed, $this->get_flash_parameters() ); $html = '<embed'; foreach ( $embed as $attribute => $value ) { $html .= ' ' . esc_html( $attribute ) . '="' . esc_attr( $value ) . '"'; } unset( $embed ); $html .= '></embed>'; return $html; } /** * Double-baked Flash object markup for Internet Explorer and more standards-friendly consuming agents. * * @since 1.1 * @return string HTML markup. Object and children. */ private function flash_object() { wp_enqueue_script( 'videopress' ); if ( ! isset( $this->video->players->swf ) || ! isset( $this->video->players->swf->url ) ) { return ''; } $thumbnail_html = '<img alt="'; if ( isset( $this->video->title ) ) { $thumbnail_html .= esc_attr( $this->video->title ); } $thumbnail_html .= '" src="' . esc_url( $this->video->poster_frame_uri, array( 'http', 'https' ) ) . '" width="' . $this->video->calculated_width . '" height="' . $this->video->calculated_height . '" />'; $flash_vars = esc_attr( http_build_query( $this->get_flash_variables(), '', '&' ) ); $flash_params = ''; foreach ( $this->get_flash_parameters() as $attribute => $value ) { $flash_params .= '<param name="' . esc_attr( $attribute ) . '" value="' . esc_attr( $value ) . '" />'; } /* translators: %s url to the Adobe Flash Player website */ $flash_help = sprintf( __( 'This video requires <a rel="nofollow noopener noreferrer" href="%s" target="_blank">Adobe Flash</a> for playback.', 'jetpack' ), 'https://get.adobe.com/flashplayer/' ); $flash_player_url = esc_url( $this->video->players->swf->url, array( 'http', 'https' ) ); $description = ''; if ( isset( $this->video->title ) ) { $standby = $this->video->title; $description = '<p><strong>' . esc_html( $this->video->title ) . '</strong></p>'; } else { $standby = __( 'Loading video...', 'jetpack' ); } $standby = ' standby="' . esc_attr( $standby ) . '"'; return <<<OBJECT <script type="text/javascript">if(typeof swfobject!=="undefined"){swfobject.registerObject("{$this->video_id}", "{$this->video->players->swf->version}");}</script> <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="{$this->video->calculated_width}" height="{$this->video->calculated_height}" id="{$this->video_id}"{$standby}> <param name="movie" value="{$flash_player_url}" /> {$flash_params} <param name="flashvars" value="{$flash_vars}" /> <!--[if !IE]>--> <object type="application/x-shockwave-flash" data="{$flash_player_url}" width="{$this->video->calculated_width}" height="{$this->video->calculated_height}"{$standby}> {$flash_params} <param name="flashvars" value="{$flash_vars}" /> <!--<![endif]--> {$thumbnail_html}{$description}<p class="robots-nocontent">{$flash_help}</p> <!--[if !IE]>--> </object> <!--<![endif]--> </object> OBJECT; } } videopress/videopress-admin.min.css 0000644 00000002606 15174711637 0013514 0 ustar 00 .videopress-modal-backdrop{background:#000;height:100%;opacity:.7;overflow:hidden;position:absolute;top:0;width:100%;z-index:100}.videopress-modal{background:#fff;border-bottom-left-radius:2px;border-bottom-right-radius:2px;box-shadow:2px 2px 5px 2px #00000080;left:50%;margin-left:-220px;overflow:hidden;padding:10px 20px;position:absolute;top:0;width:440px;z-index:101}.videopress-modal .submit{padding:10px 0 5px;text-align:right}.videopress-preview{background:#000;display:block;float:right;margin-top:18px;min-height:97px;text-decoration:none;width:65%}.vp-preview span.videopress-preview-unavailable{float:right;margin-right:0;text-align:left;width:65%}.videopress-preview img{float:left;width:100%}.videopress-preview span{color:#fff!important;display:block;padding-top:40px;text-align:center}.vp-setting .help{margin:0 0 4px 35%}.media-sidebar .vp-setting input[type=checkbox]{float:left;margin-top:10px}.vp-setting label{float:left;margin:8px 8px 0 5px;max-width:135px}.vp-setting input[type=radio]{float:left;margin-top:9px;width:auto}.vp-preview span{margin-top:18px}.uploader-videopress{margin:16px}.uploader-videopress .videopress-errors div{margin:16px 0}.compat-field-allow_download input[type=checkbox],.compat-field-display_embed input[type=checkbox],.compat-field-video-rating input[type=radio]{margin-left:5px!important;margin-right:5px!important;margin-top:-1px!important;vertical-align:middle} videopress/js/videopress-add-resumable-upload-support.js 0000644 00000000052 15174711637 0017574 0 ustar 00 window.videoPressResumableEnabled = true; videopress/js/videopress-uploader.js 0000644 00000010431 15174711637 0013710 0 ustar 00 /* globals plupload, pluploadL10n, error */ window.wp = window.wp || {}; ( function ( wp ) { var VideoPress = { originalOptions: {}, /** * This is the standard uploader response handler. */ handleStandardResponse: function ( response, file ) { if ( response === null || typeof response !== 'object' || typeof response.success === 'undefined' ) { return error( pluploadL10n.default_error, null, file ); } else if ( ! response.success ) { return error( response.data && response.data.message, response.data, file ); } return response; }, /** * Handle response from the WPCOM Rest API. */ handleRestApiResponse: function ( response, file ) { if ( response.media.length !== 1 ) { return error( pluploadL10n.default_error, null, file ); } var media = response.media[ 0 ], mimeParts = media.mime_type.split( '/' ), data = { alt: '', author: media.author_ID || 0, authorName: '', caption: '', compat: { item: '', meta: '' }, date: media.date || '', dateFormatted: media.date || '', description: media.description || '', editLink: '', filename: media.file || '', filesizeHumanReadable: '', filesizeInBytes: '', height: media.height, icon: media.icon || '', id: media.ID || '', link: media.URL || '', menuOrder: 0, meta: false, mime: media.mime_type || '', modified: 0, name: '', nonces: { update: '', delete: '', edit: '' }, orientation: '', sizes: undefined, status: '', subtype: mimeParts[ 1 ] || '', title: media.title || '', type: mimeParts[ 0 ] || '', uploadedTo: 1, uploadedToLink: '', uploadedToTitle: '', url: media.URL || '', width: media.width, success: '', videopress: { guid: media.videopress_guid || null, processing_done: media.videopress_processing_done || false, }, }; response.data = data; return response; }, /** * Make sure that all of the original variables have been reset, so the uploader * doesn't try to go to VideoPress again next time. * * @param up */ resetToOriginalOptions: function ( up ) { if ( typeof VideoPress.originalOptions.url !== 'undefined' ) { up.setOption( 'url', VideoPress.originalOptions.url ); delete VideoPress.originalOptions.url; } if ( typeof VideoPress.originalOptions.multipart_params !== 'undefined' ) { up.setOption( 'multipart_params', VideoPress.originalOptions.multipart_params ); delete VideoPress.originalOptions.multipart_params; } if ( typeof VideoPress.originalOptions.file_data_name !== 'undefined' ) { up.setOption( 'file_data_name', VideoPress.originalOptions.file_data_name ); delete VideoPress.originalOptions.file_data_name; } }, }; if ( typeof wp.Uploader !== 'undefined' ) { var media = wp.media; /** * A plupload code specifically for videopress failures. * * @type {string} */ plupload.VIDEOPRESS_TOKEN_FAILURE = 'VP_TOKEN_FAILURE'; /** * Adds a filter that checks all files to see if they are videopress files and if they are * it will download extra metadata for them. */ plupload.addFileFilter( 'videopress_check_uploads', function ( maxSize, file, cb ) { var mimeParts = file.type.split( '/' ); var self = this; if ( mimeParts[ 0 ] === 'video' ) { media .ajax( 'videopress-get-upload-token', { async: false, data: { filename: file.name } } ) .done( function ( response ) { file.videopress = response; cb( true ); } ) .fail( function ( response ) { self.trigger( 'Error', { code: plupload.VIDEOPRESS_TOKEN_FAILURE, message: plupload.translate( 'Could not get the VideoPress token needed for uploading' ), file: file, response: response, } ); cb( false ); } ); } else { // Handles the normal max_file_size functionality. var undef; // Invalid file size if ( file.size !== undef && maxSize && file.size > maxSize ) { this.trigger( 'Error', { code: plupload.FILE_SIZE_ERROR, message: plupload.translate( 'File size error.' ), file: file, } ); cb( false ); } else { cb( true ); } } } ); } wp.VideoPress = VideoPress; } )( window.wp ); videopress/js/videopress-plupload.js 0000644 00000036002 15174711637 0013717 0 ustar 00 /* global pluploadL10n, plupload, _wpPluploadSettings */ window.wp = window.wp || {}; ( function ( exports, $ ) { var Uploader, vp; if ( typeof _wpPluploadSettings === 'undefined' ) { return; } /** * A WordPress uploader. * * The Plupload library provides cross-browser uploader UI integration. * This object bridges the Plupload API to integrate uploads into the * WordPress back end and the WordPress media experience. * * @param {object} options The options passed to the new plupload instance. * @param {object} options.container The id of uploader container. * @param {object} options.browser The id of button to trigger the file select. * @param {object} options.dropzone The id of file drop target. * @param {object} options.plupload An object of parameters to pass to the plupload instance. * @param {object} options.params An object of parameters to pass to $_POST when uploading the file. * Extends this.plupload.multipart_params under the hood. */ Uploader = function ( options ) { var self = this, isIE = navigator.userAgent.indexOf( 'Trident/' ) !== -1 || navigator.userAgent.indexOf( 'MSIE ' ) !== -1, elements = { container: 'container', browser: 'browse_button', dropzone: 'drop_element', }, key, error; this.supports = { upload: Uploader.browser.supported, }; this.supported = this.supports.upload; if ( ! this.supported ) { return; } // Arguments to send to pluplad.Uploader(). // Use deep extend to ensure that multipart_params and other objects are cloned. this.plupload = $.extend( true, { multipart_params: {} }, Uploader.defaults ); this.container = document.body; // Set default container. // Extend the instance with options. // // Use deep extend to allow options.plupload to override individual // default plupload keys. $.extend( true, this, options ); // Proxy all methods so this always refers to the current instance. for ( key in this ) { if ( $.isFunction( this[ key ] ) ) { this[ key ] = $.proxy( this[ key ], this ); } } // Ensure all elements are jQuery elements and have id attributes, // then set the proper plupload arguments to the ids. for ( key in elements ) { if ( ! this[ key ] ) { continue; } this[ key ] = $( this[ key ] ).first(); if ( ! this[ key ].length ) { delete this[ key ]; continue; } if ( ! this[ key ].prop( 'id' ) ) { this[ key ].prop( 'id', '__wp-uploader-id-' + Uploader.uuid++ ); } this.plupload[ elements[ key ] ] = this[ key ].prop( 'id' ); } // If the uploader has neither a browse button nor a dropzone, bail. if ( ! ( this.browser && this.browser.length ) && ! ( this.dropzone && this.dropzone.length ) ) { return; } // Make sure flash sends cookies (seems in IE it does without switching to urlstream mode) if ( ! isIE && 'flash' === plupload.predictRuntime( this.plupload ) && ( ! this.plupload.required_features || ! Object.hasOwn( this.plupload.required_features, 'send_binary_string' ) ) ) { this.plupload.required_features = this.plupload.required_features || {}; this.plupload.required_features.send_binary_string = true; } // Initialize the plupload instance. this.uploader = new plupload.Uploader( this.plupload ); delete this.plupload; // Set default params and remove this.params alias. this.param( this.params || {} ); delete this.params; // Make sure that the VideoPress object is available if ( typeof exports.VideoPress !== 'undefined' ) { vp = exports.VideoPress; } else { window.console && window.console.error( 'The VideoPress object was not loaded. Errors may occur.' ); } /** * Custom error callback. * * Add a new error to the errors collection, so other modules can track * and display errors. {@see wp.Uploader.errors}. * * @param {string} message * @param {object} data * @param {plupload.File} file File that was uploaded. */ error = function ( message, data, file ) { if ( file.attachment ) { file.attachment.destroy(); } Uploader.errors.unshift( { message: message || pluploadL10n.default_error, data: data, file: file, } ); self.error( message, data, file ); }; /** * After the Uploader has been initialized, initialize some behaviors for the dropzone. * * @param {plupload.Uploader} uploader Uploader instance. */ this.uploader.bind( 'init', function ( uploader ) { var timer, active, dragdrop, dropzone = self.dropzone; dragdrop = self.supports.dragdrop = uploader.features.dragdrop && ! Uploader.browser.mobile; // Generate drag/drop helper classes. if ( ! dropzone ) { return; } dropzone.toggleClass( 'supports-drag-drop', !! dragdrop ); if ( ! dragdrop ) { return dropzone.unbind( '.wp-uploader' ); } // 'dragenter' doesn't fire correctly, simulate it with a limited 'dragover'. dropzone.bind( 'dragover.wp-uploader', function () { if ( timer ) { clearTimeout( timer ); } if ( active ) { return; } dropzone.trigger( 'dropzone:enter' ).addClass( 'drag-over' ); active = true; } ); dropzone.bind( 'dragleave.wp-uploader, drop.wp-uploader', function () { // Using an instant timer prevents the drag-over class from // being quickly removed and re-added when elements inside the // dropzone are repositioned. // // @see https://core.trac.wordpress.org/ticket/21705 timer = setTimeout( function () { active = false; dropzone.trigger( 'dropzone:leave' ).removeClass( 'drag-over' ); }, 0 ); } ); self.ready = true; $( self ).trigger( 'uploader:ready' ); } ); this.uploader.bind( 'postinit', function ( up ) { up.refresh(); self.init(); } ); this.uploader.init(); if ( this.browser ) { this.browser.on( 'mouseenter', this.refresh ); } else { this.uploader.disableBrowse( true ); // If HTML5 mode, hide the auto-created file container. $( '#' + this.uploader.id + '_html5_container' ).hide(); } /** * After files were filtered and added to the queue, create a model for each. * * @event FilesAdded * @param {plupload.Uploader} uploader Uploader instance. * @param {Array} files Array of file objects that were added to queue by the user. */ this.uploader.bind( 'FilesAdded', function ( up, files ) { for ( const file of files ) { var attributes, image; // Ignore failed uploads. if ( plupload.FAILED === file.status ) { return; } // Generate attributes for a new `Attachment` model. attributes = { file: file, uploading: true, date: new Date(), filename: file.name, menuOrder: 0, uploadedTo: wp.media.model.settings.post.id, ...Object.fromEntries( Object.entries( file ).filter( ( [ k ] ) => k === 'loaded' || k === 'size' || k === 'percent' ) ), }; // Handle early mime type scanning for images. image = /(?:jpe?g|png|gif|webp)$/i.exec( file.name ); // For images set the model's type and subtype attributes. if ( image ) { attributes.type = 'image'; // `jpeg`, `png` and `gif` are valid subtypes. // `jpg` is not, so map it to `jpeg`. attributes.subtype = 'jpg' === image[ 0 ] ? 'jpeg' : image[ 0 ]; } // Create a model for the attachment, and add it to the Upload queue collection // so listeners to the upload queue can track and display upload progress. file.attachment = wp.media.model.Attachment.create( attributes ); Uploader.queue.add( file.attachment ); self.added( file.attachment ); } up.refresh(); up.start(); } ); this.uploader.bind( 'UploadProgress', function ( up, file ) { file.attachment.set( Object.fromEntries( Object.entries( file ).filter( ( [ k ] ) => k === 'loaded' || k === 'percent' ) ) ); self.progress( file.attachment ); } ); /** * After a file is successfully uploaded, update its model. * * @param {plupload.Uploader} uploader Uploader instance. * @param {plupload.File} file File that was uploaded. * @param {Object} response Object with response properties. * @return {mixed} */ this.uploader.bind( 'FileUploaded', function ( up, file, response ) { var complete; try { response = JSON.parse( response.response ); } catch ( e ) { return error( pluploadL10n.default_error, e, file ); } if ( typeof response.media !== 'undefined' ) { response = vp.handleRestApiResponse( response, file ); } else { response = vp.handleStandardResponse( response, file ); } for ( const k of [ 'file', 'loaded', 'size', 'percent' ] ) { file.attachment.unset( k ); } file.attachment.set( { ...response.data, uploading: false } ); var att = wp.media.model.Attachment.get( response.data.id, file.attachment ); /* Once the new "empty" attachment is added to the collection above, check if it exists on the server, then set the new data. * If it happens to not exist on the server yet (xmlrpc delayed or not working), then the empty media item will still be on the page, * so no need to do any special error handling here on the sync. * * This is only necessary if `vp.handleRestApiResponse` was used above, as it is what returns the "empty" media item. */ if ( typeof response.media !== 'undefined' ) { att.sync( 'read' ).then( function ( data ) { wp.media.model.Attachment.get( att.id ).set( data ); } ); } complete = Uploader.queue.all( function ( attachment ) { return ! attachment.get( 'uploading' ); } ); if ( complete ) { vp && vp.resetToOriginalOptions( up ); Uploader.queue.reset(); } self.success( file.attachment ); } ); /** * When plupload surfaces an error, send it to the error handler. * * @param {plupload.Uploader} uploader Uploader instance. * @param {Object} error Contains code, message and sometimes file and other details. */ this.uploader.bind( 'Error', function ( up, pluploadError ) { var message = pluploadL10n.default_error; // Check for plupload errors. for ( var k in Uploader.errorMap ) { if ( pluploadError.code === plupload[ k ] ) { message = Uploader.errorMap[ k ]; if ( typeof message === 'function' ) { message = message( pluploadError.file, pluploadError ); } break; } } if ( 'response' in pluploadError ) { try { var pluploadResponseObject = JSON.parse( pluploadError.response ); if ( typeof pluploadResponseObject === 'object' ) { if ( 'errors' in pluploadResponseObject && typeof pluploadResponseObject.errors === 'object' ) { pluploadResponseObject = pluploadResponseObject.errors.shift(); } if ( 'message' in pluploadResponseObject ) { message = pluploadResponseObject.message; } } } catch { // Do nothing ... } } error( message, pluploadError, pluploadError.file ); vp && vp.resetToOriginalOptions( up ); up.refresh(); } ); /** * Add in a way for the uploader to reset itself when uploads are complete. */ this.uploader.bind( 'UploadComplete', function ( up ) { vp && vp.resetToOriginalOptions( up ); } ); /** * Before we upload, check to see if this file is a videopress upload, if so, set new options and save the old ones. */ this.uploader.bind( 'BeforeUpload', function ( up, file ) { if ( typeof file.videopress !== 'undefined' ) { vp.originalOptions.url = up.getOption( 'url' ); vp.originalOptions.multipart_params = up.getOption( 'multipart_params' ); vp.originalOptions.file_data_name = up.getOption( 'file_data_name' ); up.setOption( 'file_data_name', 'media[]' ); up.setOption( 'url', file.videopress.upload_action_url ); up.setOption( 'headers', { Authorization: 'X_UPLOAD_TOKEN token="' + file.videopress.upload_token + '" blog_id="' + file.videopress.upload_blog_id + '"', } ); } } ); }; // Adds the 'defaults' and 'browser' properties. $.extend( Uploader, _wpPluploadSettings ); Uploader.uuid = 0; // Map Plupload error codes to user friendly error messages. Uploader.errorMap = { FAILED: pluploadL10n.upload_failed, FILE_EXTENSION_ERROR: pluploadL10n.invalid_filetype, IMAGE_FORMAT_ERROR: pluploadL10n.not_an_image, IMAGE_MEMORY_ERROR: pluploadL10n.image_memory_exceeded, IMAGE_DIMENSIONS_ERROR: pluploadL10n.image_dimensions_exceeded, GENERIC_ERROR: pluploadL10n.upload_failed, IO_ERROR: pluploadL10n.io_error, HTTP_ERROR: pluploadL10n.http_error, SECURITY_ERROR: pluploadL10n.security_error, FILE_SIZE_ERROR: function ( file ) { return pluploadL10n.file_exceeds_size_limit.replace( '%s', file.name ); }, }; $.extend( Uploader.prototype, { /** * Acts as a shortcut to extending the uploader's multipart_params object. * * param( key ) * Returns the value of the key. * * param( key, value ) * Sets the value of a key. * * param( map ) * Sets values for a map of data. */ param: function ( key, value ) { if ( arguments.length === 1 && typeof key === 'string' ) { return this.uploader.settings.multipart_params[ key ]; } if ( arguments.length > 1 ) { this.uploader.settings.multipart_params[ key ] = value; } else { $.extend( this.uploader.settings.multipart_params, key ); } }, /** * Make a few internal event callbacks available on the wp.Uploader object * to change the Uploader internals if absolutely necessary. */ init: function () {}, error: function () {}, success: function () {}, added: function () {}, progress: function () {}, complete: function () {}, refresh: function () { var node, attached, container, id; if ( this.browser ) { node = this.browser[ 0 ]; // Check if the browser node is in the DOM. while ( node ) { if ( node === document.body ) { attached = true; break; } node = node.parentNode; } // If the browser node is not attached to the DOM, use a // temporary container to house it, as the browser button // shims require the button to exist in the DOM at all times. if ( ! attached ) { id = 'wp-uploader-browser-' + this.uploader.id; container = $( '#' + id ); if ( ! container.length ) { container = $( '<div class="wp-uploader-browser" />' ) .css( { position: 'fixed', top: '-1000px', left: '-1000px', height: 0, width: 0, } ) .attr( 'id', 'wp-uploader-browser-' + this.uploader.id ) .appendTo( 'body' ); } container.append( this.browser ); } } this.uploader.refresh(); }, } ); // Create a collection of attachments in the upload queue, // so that other modules can track and display upload progress. Uploader.queue = new wp.media.model.Attachments( [], { query: false } ); // Create a collection to collect errors incurred while attempting upload. Uploader.errors = new Backbone.Collection(); exports.Uploader = Uploader; } )( wp, jQuery ); videopress/js/media-video-widget-extensions.js 0000644 00000003520 15174711637 0015556 0 ustar 00 window.wp = window.wp || {}; ( function ( wp ) { if ( wp.mediaWidgets ) { // Over-ride core media_video#mapMediaToModelProps to set the url based upon videopress_guid if it exists. wp.mediaWidgets.controlConstructors.media_video.prototype.mapMediaToModelProps = ( function ( originalMapMediaToModelProps ) { return function ( mediaFrameProps ) { var newProps, originalProps, videoPressGuid; originalProps = originalMapMediaToModelProps.call( this, mediaFrameProps ); newProps = _.extend( {}, originalProps ); // API response on new media will have the guid at videopress.guid. if ( mediaFrameProps.videopress && mediaFrameProps.videopress.guid ) { videoPressGuid = mediaFrameProps.videopress.guid; } // Selecting an existing VideoPress file will have the guid at .videopress_guid[ 0 ]. if ( ! videoPressGuid && mediaFrameProps.videopress_guid && mediaFrameProps.videopress_guid.length ) { videoPressGuid = mediaFrameProps.videopress_guid[ 0 ]; } if ( videoPressGuid ) { newProps = _.extend( {}, originalProps, { url: 'https://videopress.com/v/' + videoPressGuid, attachment_id: 0, } ); } return newProps; }; } )( wp.mediaWidgets.controlConstructors.media_video.prototype.mapMediaToModelProps ); // Over-ride core media_video#isHostedVideo() to add support for videopress oembed urls. wp.mediaWidgets.controlConstructors.media_video.prototype.isHostedVideo = ( function ( originalIsHostedVideo ) { return function ( url ) { var parsedUrl = document.createElement( 'a' ); parsedUrl.href = url; if ( 'videopress.com' === parsedUrl.hostname ) { return true; } return originalIsHostedVideo.call( this, url ); }; } )( wp.mediaWidgets.controlConstructors.media_video.prototype.isHostedVideo ); } } )( window.wp ); videopress/js/gutenberg-video-upload.js 0000644 00000013355 15174711637 0014274 0 ustar 00 window.videoPressUploadPoster = function ( guid, data ) { return new Promise( function ( resolve, reject ) { wp.media.ajax( 'videopress-get-upload-token', { async: true } ).done( function ( response ) { // Set auth header with upload token. var headers = {}, options = {}; var body = new FormData(); headers.Authorization = 'X_UPLOAD_TOKEN token="' + response.upload_token + '" blog_id="' + response.upload_blog_id + '"'; options.headers = headers; options.method = 'POST'; options.url = 'https://public-api.wordpress.com/rest/v1.1/videos/' + guid + '/poster'; // Handle CORS. options.credentials = 'omit'; Object.keys( data ).forEach( key => { body.append( key, data[ key ] ); } ); options.body = body; wp.apiFetch( options ) .then( function ( res ) { resolve( res ); } ) .catch( function ( error ) { reject( error ); } ); } ); } ); }; window.videoPressGetPoster = function ( guid ) { const getPosterRequest = ( resolve, reject, jwt = null ) => { let url = 'https://public-api.wordpress.com/rest/v1.1/videos/' + guid + '/poster'; if ( jwt && jwt.length ) { url += '?metadata_token=' + jwt; } wp.apiFetch( { url: url, method: 'GET', credentials: 'omit', } ) .then( function ( res ) { resolve( res ); } ) .catch( function ( error ) { reject( error ); } ); }; return new Promise( function ( resolve, reject ) { wp.ajax .post( 'videopress-get-playback-jwt', { async: true, guid: guid, } ) .done( function ( response ) { getPosterRequest( resolve, reject, response.jwt ); } ) .fail( () => { // Also try on ajax failure if the video doesn't need a jwt anyway getPosterRequest( resolve, reject ); } ); } ); }; window.videoPressUploadTrack = function ( guid, kind, srcLang, label, tmpFile ) { return new Promise( function ( resolve, reject ) { wp.media .ajax( 'videopress-get-upload-token', { async: true, data: { filename: tmpFile.name } } ) // todo: maybe remove filename from here (not needed) .done( function ( response ) { // Set auth header with upload token. var headers = {}, options = {}; var body = new FormData(); headers.Authorization = 'X_UPLOAD_TOKEN token="' + response.upload_token + '" blog_id="' + response.upload_blog_id + '"'; options.headers = headers; options.method = 'POST'; options.url = 'https://public-api.wordpress.com/rest/v1.1/videos/' + guid + '/tracks'; // Handle CORS. options.credentials = 'omit'; body.append( 'kind', kind ); body.append( 'srclang', srcLang ); body.append( 'label', label ); body.append( 'vtt', tmpFile ); options.body = body; wp.apiFetch( options ) .then( function ( res ) { resolve( res ); } ) .catch( function ( error ) { reject( error ); } ); } ); } ); }; window.videoPressDeleteTrack = function ( guid, kind, srcLang ) { return new Promise( function ( resolve, reject ) { wp.media.ajax( 'videopress-get-upload-token', { async: true } ).done( function ( response ) { // Set auth header with upload token. var headers = {}, options = {}; var body = new FormData(); headers.Authorization = 'X_UPLOAD_TOKEN token="' + response.upload_token + '" blog_id="' + response.upload_blog_id + '"'; options.headers = headers; options.method = 'POST'; options.url = 'https://public-api.wordpress.com/rest/v1.1/videos/' + guid + '/tracks/delete'; // Handle CORS. options.credentials = 'omit'; body.append( 'kind', kind ); body.append( 'srclang', srcLang ); options.body = body; wp.apiFetch( options ) .then( function ( res ) { resolve( res ); } ) .catch( function ( error ) { reject( error ); } ); } ); } ); }; wp.apiFetch.use( function ( options, next ) { var path = options.path; var method = options.method; var body = options.body; // Override only requests to the WP REST API media endpoint uploading new videos. if ( ! path || path.indexOf( '/wp/v2/media' ) === -1 ) { return next( options ); } if ( ! method || 'post' !== method.toLowerCase() ) { return next( options ); } var file = body ? body.get( 'file' ) : null; if ( ! file || file.type.indexOf( 'video/' ) !== 0 ) { return next( options ); } // Get upload token. wp.media .ajax( 'videopress-get-upload-token', { async: false, data: { filename: file.name } } ) .done( function ( response ) { // Set auth header with upload token. var headers = options.headers || {}; headers.Authorization = 'X_UPLOAD_TOKEN token="' + response.upload_token + '" blog_id="' + response.upload_blog_id + '"'; options.headers = headers; // Replace upload URL. delete options.path; options.url = response.upload_action_url; // Handle CORS. options.credentials = 'omit'; // Set data in expected param by WP.com media endpoint. body.set( 'media[]', file ); body.delete( 'file' ); options.body = body; } ); var result = next( options ); return new Promise( function ( resolve, reject ) { result .then( function ( response ) { if ( response instanceof Response && response.ok ) { return response.json(); } return response; // if not a response object, then its our parsed body so return that } ) .then( function ( data ) { var wpcomMediaObject = data?.media?.[ 0 ]; var id = wpcomMediaObject?.ID; var gutenbergMediaObject = wp.apiFetch( { path: '/wp/v2/media/' + id, } ); resolve( gutenbergMediaObject ); } ) .catch( function ( error ) { if ( 'errors' in error && 'object' === typeof error.errors && error.errors.length > 0 ) { error = error.errors.shift(); } reject( error ); } ); } ); } ); videopress/js/editor-view.js 0000644 00000017161 15174711637 0012161 0 ustar 00 /* global tinyMCE, vpEditorView */ ( function ( $, wp, vpEditorView ) { wp.mce = wp.mce || {}; if ( 'undefined' === typeof wp.mce.views ) { return; } wp.mce.videopress_wp_view_renderer = { shortcode_string: 'videopress', shortcode_data: {}, defaults: { w: '', at: '', permalink: true, hd: false, loop: false, freedom: false, autoplay: false, flashonly: false, }, coerce: wp.media.coerce, template: wp.template( 'videopress_iframe_vnext' ), getContent: function () { var urlargs = 'for=' + encodeURIComponent( vpEditorView.home_url_host ), named = this.shortcode.attrs.named, options, key, width; for ( key in named ) { switch ( key ) { case 'at': if ( parseInt( named[ key ], 10 ) ) { urlargs += '&' + key + '=' + parseInt( named[ key ], 10 ); } // Else omit, as it's the default. break; case 'permalink': if ( 'false' === named[ key ] ) { urlargs += '&' + key + '=0'; } // Else omit, as it's the default. break; case 'hd': case 'loop': case 'autoplay': if ( 'true' === named[ key ] ) { urlargs += '&' + key + '=1'; } // Else omit, as it's the default. break; default: // Unknown parameters? Ditch it! break; } } options = { width: vpEditorView.content_width, height: vpEditorView.content_width * 0.5625, guid: this.shortcode.attrs.numeric[ 0 ], urlargs: urlargs, }; if ( typeof named.w !== 'undefined' ) { width = parseInt( named.w, 10 ); if ( width >= vpEditorView.min_content_width && width < vpEditorView.content_width ) { options.width = width; options.height = parseInt( width * 0.5625, 10 ); } } options.ratio = 100 * ( options.height / options.width ); return this.template( options ); }, edit: function ( data ) { var shortcode_data = wp.shortcode.next( this.shortcode_string, data ), named = shortcode_data.shortcode.attrs.named, editor = tinyMCE.activeEditor, renderer = this, oldRenderFormItem = tinyMCE.ui.FormItem.prototype.renderHtml; /** * Override TextBox renderHtml to support html5 attrs. * @link https://github.com/tinymce/tinymce/pull/2784 * * @return {string} */ tinyMCE.ui.TextBox.prototype.renderHtml = function () { var self = this, settings = self.settings, element = document.createElement( settings.multiline ? 'textarea' : 'input' ), extraAttrs = [ 'rows', 'spellcheck', 'maxLength', 'size', 'readonly', 'min', 'max', 'step', 'list', 'pattern', 'placeholder', 'required', 'multiple', ], i, key; for ( i = 0; i < extraAttrs.length; i++ ) { key = extraAttrs[ i ]; if ( typeof settings[ key ] !== 'undefined' ) { element.setAttribute( key, settings[ key ] ); } } if ( settings.multiline ) { element.innerText = self.state.get( 'value' ); } else { element.setAttribute( 'type', settings.subtype ? settings.subtype : 'text' ); element.setAttribute( 'value', self.state.get( 'value' ) ); } element.id = self._id; element.className = self.classes; element.setAttribute( 'hidefocus', 1 ); if ( self.disabled() ) { element.disabled = true; } return element.outerHTML; }; tinyMCE.ui.FormItem.prototype.renderHtml = function () { for ( const [ key, value ] of Object.entries( vpEditorView.modal_labels ) ) { if ( value === this.settings.items.text ) { this.classes.add( 'videopress-field-' + key ); } } if ( [ vpEditorView.modal_labels.hd, vpEditorView.modal_labels.permalink, vpEditorView.modal_labels.autoplay, vpEditorView.modal_labels.loop, vpEditorView.modal_labels.freedom, vpEditorView.modal_labels.flashonly, ].includes( this.settings.items.text ) ) { this.classes.add( 'videopress-checkbox' ); } return oldRenderFormItem.call( this ); }; /** * Populate the defaults. */ for ( const [ key ] of Object.entries( this.defaults ) ) { named[ key ] = this.coerce( named, key ); } /** * Declare the fields that will show in the popup when editing the shortcode. */ editor.windowManager.open( { title: vpEditorView.modal_labels.title, id: 'videopress-shortcode-settings-modal', width: 520, height: 240, body: [ { type: 'textbox', disabled: true, name: 'guid', label: vpEditorView.modal_labels.guid, value: shortcode_data.shortcode.attrs.numeric[ 0 ], }, { type: 'textbox', subtype: 'number', min: vpEditorView.min_content_width, // The `min` may supported be in the future. https://github.com/tinymce/tinymce/pull/2784 name: 'w', label: vpEditorView.modal_labels.w, value: named.w, }, { type: 'textbox', subtype: 'number', min: 0, // The `min` may supported be in the future. https://github.com/tinymce/tinymce/pull/2784 name: 'at', label: vpEditorView.modal_labels.at, value: named.at, }, { type: 'checkbox', name: 'hd', label: vpEditorView.modal_labels.hd, checked: named.hd, }, { type: 'checkbox', name: 'permalink', label: vpEditorView.modal_labels.permalink, checked: named.permalink, }, { type: 'checkbox', name: 'autoplay', label: vpEditorView.modal_labels.autoplay, checked: named.autoplay, }, { type: 'checkbox', name: 'loop', label: vpEditorView.modal_labels.loop, checked: named.loop, }, { type: 'checkbox', name: 'freedom', label: vpEditorView.modal_labels.freedom, checked: named.freedom, }, { type: 'checkbox', name: 'flashonly', label: vpEditorView.modal_labels.flashonly, checked: named.flashonly, }, ], onsubmit: function ( e ) { var args = { tag: renderer.shortcode_string, type: 'single', attrs: { named: Object.fromEntries( Object.entries( e.data ).filter( ( [ k ] ) => k in renderer.defaults ) ), numeric: [ e.data.guid ], }, }; if ( '0' === args.attrs.named.at ) { args.attrs.named.at = ''; } for ( const [ key, value ] of Object.entries( renderer.defaults ) ) { args.attrs.named[ key ] = renderer.coerce( args.attrs.named, key ); if ( value === args.attrs.named[ key ] ) { delete args.attrs.named[ key ]; } } editor.insertContent( wp.shortcode.string( args ) ); }, onopen: function ( e ) { var prefix = 'mce-videopress-field-'; for ( const value of [ 'w', 'at' ] ) { e.target.$el .find( '.' + prefix + value + ' .mce-container-body' ) .append( '<span class="' + prefix + 'unit ' + prefix + 'unit-' + value + '">' + vpEditorView.modal_labels[ value + '_unit' ] ); } $( 'body' ).addClass( 'modal-open' ); }, onclose: function () { $( 'body' ).removeClass( 'modal-open' ); }, } ); // Set it back to its original renderer. tinyMCE.ui.FormItem.prototype.renderHtml = oldRenderFormItem; }, }; // Extend the videopress one to also handle `wpvideo` instances. wp.mce.wpvideo_wp_view_renderer = Object.assign( {}, wp.mce.videopress_wp_view_renderer, { shortcode_string: 'wpvideo', } ); wp.mce.views.register( 'videopress', wp.mce.videopress_wp_view_renderer ); wp.mce.views.register( 'wpvideo', wp.mce.wpvideo_wp_view_renderer ); } )( jQuery, wp, vpEditorView ); videopress/class.videopress-scheduler.php 0000644 00000012114 15174711637 0014716 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName /** * VideoPress cron scheduler. * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VideoPress Cron Scheduler. */ class VideoPress_Scheduler { /** * The name of the function used to run the cleanup cron. */ const CLEANUP_CRON_METHOD = 'videopress_cleanup_media_library'; /** * Singleton instance. * * @var VideoPress_Scheduler $instance A VideoPress_Scheduler singleton instance (default null) **/ private static $instance = null; /** * A list of all of the crons that are to be activated, along with their interval timings. * * @var array */ protected $crons = array( // phpcs:ignore Squiz.PHP.CommentedOutCode.Found // 'cleanup' => array( // 'method' => self::CLEANUP_CRON_METHOD, // 'interval' => 'minutes_30', // ), ); /** * Private VideoPress_Scheduler constructor. * * Use the VideoPress_Scheduler::init() method to get an instance. */ private function __construct() { add_filter( 'cron_schedules', array( $this, 'add_30_minute_cron_interval' ) ); // Activate the cleanup cron if videopress is enabled, jetpack is activated, or jetpack is updated. add_action( 'jetpack_activate_module_videopress', array( $this, 'activate_all_crons' ) ); add_action( 'updating_jetpack_version', array( $this, 'activate_all_crons' ) ); add_action( 'activated_plugin', array( $this, 'activate_crons_on_jetpack_activation' ) ); // Deactivate the cron if either videopress is disabled or Jetpack is disabled. add_action( 'jetpack_deactivate_module_videopress', array( $this, 'deactivate_all_crons' ) ); register_deactivation_hook( plugin_basename( JETPACK__PLUGIN_FILE ), array( $this, 'deactivate_all_crons' ) ); } /** * Initialize the VideoPress_Scheduler and get back a singleton instance. * * @return VideoPress_Scheduler */ public static function init() { if ( self::$instance === null ) { self::$instance = new VideoPress_Scheduler(); } return self::$instance; } /** * Adds 30 minute running interval to the cron schedules. * * @param array $current_schedules Currently defined schedules list. * * @return array */ public function add_30_minute_cron_interval( $current_schedules ) { // Only add the 30 minute interval if it wasn't already set. if ( ! isset( $current_schedules['minutes_30'] ) ) { $current_schedules['minutes_30'] = array( 'interval' => 30 * MINUTE_IN_SECONDS, 'display' => 'Every 30 minutes', ); } return $current_schedules; } /** * Activate a single cron * * @param string $cron_name The name of the cron to activate. * * @return bool */ public function activate_cron( $cron_name ) { if ( ! $this->is_cron_valid( $cron_name ) ) { return false; } if ( ! $this->check_cron( $cron_name ) ) { wp_schedule_event( time(), $this->crons[ $cron_name ]['interval'], $this->crons[ $cron_name ]['method'] ); } } /** * Activates widget update cron task. */ public function activate_all_crons() { if ( ! Jetpack::is_module_active( 'videopress' ) ) { return false; } foreach ( $this->crons as $cron_name => $cron ) { if ( ! $this->check_cron( $cron_name ) ) { wp_schedule_event( time(), $cron['interval'], $cron['method'] ); } } } /** * Only activate the crons if it is Jetpack that was activated. * * @param string $plugin_file_name The name of the plugin that was activated. */ public function activate_crons_on_jetpack_activation( $plugin_file_name ) { if ( plugin_basename( JETPACK__PLUGIN_FILE ) === $plugin_file_name ) { $this->activate_all_crons(); } } /** * Deactivates any crons associated with the VideoPress module. * * @param string $cron_name Name of the cron to deactivate. * * @return bool */ public function deactivate_cron( $cron_name ) { if ( ! $this->is_cron_valid( $cron_name ) ) { return false; } $next_scheduled_time = $this->check_cron( $cron_name ); wp_unschedule_event( $next_scheduled_time, $this->crons[ $cron_name ]['method'] ); return true; } /** * Deactivates any crons associated with the VideoPress module.. */ public function deactivate_all_crons() { foreach ( $this->crons as $cron_name => $cron ) { $this->deactivate_cron( $cron_name ); } } /** * Is the given cron job currently active? * * If so, return when it will next run, * * @param string $cron_name Cron job name. * * @return int|bool Timestamp of the next run time OR false. */ public function check_cron( $cron_name ) { if ( ! $this->is_cron_valid( $cron_name ) ) { return false; } return wp_next_scheduled( $this->crons[ $cron_name ]['method'] ); } /** * Check that the given cron job name is valid. * * @param string $cron_name Cron job name. * * @return bool */ public function is_cron_valid( $cron_name ) { if ( ! isset( $this->crons[ $cron_name ]['method'] ) || ! isset( $this->crons[ $cron_name ]['interval'] ) ) { return false; } return true; } /** * Get a list of all of the crons that are available. * * @return array */ public function get_crons() { return $this->crons; } } verification-tools/blog-verification-tools.php 0000644 00000006167 15174711637 0015664 0 ustar 00 <?php /** * Handles site verification services. * * @package jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Return an array of supported verification services. * Add new services to this function. * * @return array - an array of supported services. */ function jetpack_verification_services() { return array( 'google' => array( 'name' => 'Google Search Console', 'key' => 'google-site-verification', 'format' => 'dBw5CvburAxi537Rp9qi5uG2174Vb6JwHwIRwPSLIK8', 'url' => 'https://www.google.com/webmasters/tools/', ), 'bing' => array( 'name' => 'Bing Webmaster Center', 'key' => 'msvalidate.01', 'format' => '12C1203B5086AECE94EB3A3D9830B2E', 'url' => 'https://www.bing.com/toolbox/webmaster/', ), 'pinterest' => array( 'name' => 'Pinterest Site Verification', 'key' => 'p:domain_verify', 'format' => 'f100679e6048d45e4a0b0b92dce1efce', 'url' => 'https://pinterest.com/website/verify/', ), 'yandex' => array( 'name' => 'Yandex.Webmaster', 'key' => 'yandex-verification', 'format' => '44d68e1216009f40', 'url' => 'https://webmaster.yandex.com/sites/', ), 'facebook' => array( 'name' => 'Facebook Domain Verification', 'key' => 'facebook-domain-verification', 'format' => 'rvv8b23jxlp1lq41I9rwsvpzncy1fd', 'url' => 'https://business.facebook.com/settings/', ), ); } /** * Register Jetpack verification settings. */ function jetpack_verification_options_init() { register_setting( 'verification_services_codes_fields', 'verification_services_codes', array( 'sanitize_callback' => 'jetpack_verification_validate' ) ); } add_action( 'admin_init', 'jetpack_verification_options_init' ); add_action( 'rest_api_init', 'jetpack_verification_options_init' ); /** * Print the site verification meta in the page head. */ function jetpack_verification_print_meta() { $verification_services_codes = Jetpack_Options::get_option_and_ensure_autoload( 'verification_services_codes', '0' ); if ( is_array( $verification_services_codes ) ) { $ver_output = "<!-- Jetpack Site Verification Tags -->\n"; foreach ( jetpack_verification_services() as $name => $service ) { if ( is_array( $service ) && ! empty( $verification_services_codes[ "$name" ] ) ) { if ( preg_match( '#^<meta name="([a-z0-9_\-.:]+)?" content="([a-z0-9_-]+)?" />$#i', $verification_services_codes[ "$name" ], $matches ) ) { $verification_code = $matches[2]; } else { $verification_code = $verification_services_codes[ "$name" ]; } $ver_tag = sprintf( '<meta name="%s" content="%s" />', esc_attr( $service['key'] ), esc_attr( $verification_code ) ); /** * Filter the meta tag template used for all verification tools. * * @module verification-tools * * @since 3.0.0 * * @param string $ver_tag Verification Tool meta tag. */ $ver_output .= apply_filters( 'jetpack_site_verification_output', $ver_tag ); $ver_output .= "\n"; } } echo $ver_output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } add_action( 'wp_head', 'jetpack_verification_print_meta', 1 ); verification-tools/verification-tools-utils.php 0000644 00000003204 15174711637 0016066 0 ustar 00 <?php /** * Helper functions that are called from API even when module is inactive should be added here. * This file will be included in module-extras.php. * * @package jetpack */ if ( ! function_exists( 'jetpack_verification_validate' ) ) { /** * Validate jetpack verification codes. * * @param array $verification_services_codes - array of verification codes. */ function jetpack_verification_validate( $verification_services_codes ) { foreach ( $verification_services_codes as $key => $code ) { // Parse html meta tag if it does not look like a valid code. if ( ! preg_match( '/^[a-z0-9_-]+$/i', $code ) ) { $code = jetpack_verification_get_code( $code ); } $code = esc_attr( trim( $code ) ); // limit length to 100 chars. $code = substr( $code, 0, 100 ); /** * Fire after each Verification code was validated. * * @module verification-tools * * @since 3.0.0 * * @param string $key Verification service name. * @param string $code Verification service code provided in field in the Tools menu. */ do_action( 'jetpack_site_verification_validate', $key, $code ); $verification_services_codes[ $key ] = $code; } return $verification_services_codes; } } if ( ! function_exists( 'jetpack_verification_get_code' ) ) { /** * Return the code we're trying to verify after decoding. * * @param string $code - the code we need to parse. */ function jetpack_verification_get_code( $code ) { $pattern = '/content=["\']?([^"\' ]*)["\' ]/is'; preg_match( $pattern, $code, $match ); if ( $match ) { return urldecode( $match[1] ); } else { return false; } } } vaultpress.php 0000644 00000003464 15174711637 0007510 0 ustar 00 <?php // phpcs:disable WordPress.WP.CapitalPDangit.MisspelledInComment /** * Module Name: VaultPress Backup * Module Description: Real-time backups save every change, and one-click restores get you back online quickly. * First Introduced: 0:1.2 * Sort Order: 32 * Deactivate: false * Free: false * Requires Connection: Yes * Auto Activate: No * Feature: Security, Health * Additional Search Queries: backup, cloud backup, database backup, restore, wordpress backup, backup plugin, wordpress backup plugin, back up, backup wordpress, backwpup, vaultpress, backups, off-site backups, offsite backup, offsite, off-site, antivirus, malware scanner, security, virus, viruses, prevent viruses, scan, anti-virus, antimalware, protection, safe browsing, malware, wp security, wordpress security * Plans: personal, business, premium, security, complete */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } // phpcs:enable WordPress.WP.CapitalPDangit.MisspelledInComment add_action( 'jetpack_modules_loaded', 'vaultpress_jetpack_stub' ); /** * Conditionally enable module configuration. */ function vaultpress_jetpack_stub() { if ( class_exists( 'VaultPress' ) || function_exists( 'vaultpress_contact_service' ) ) { Jetpack::enable_module_configurable( __FILE__ ); add_filter( 'jetpack_module_configuration_url_vaultpress', 'vaultpress_jetpack_configure_url' ); add_filter( 'jetpack_module_free_text_vaultpress', 'vaultpress_jetpack_module_free_text' ); } } /** * Text for filter jetpack_module_free_text_vaultpress. */ function vaultpress_jetpack_module_free_text() { return __( 'Active', 'jetpack' ); } /** * URL for filter jetpack_module_configuration_url_vaultpress. */ function vaultpress_jetpack_configure_url() { include_once ABSPATH . '/wp-admin/includes/plugin.php'; return menu_page_url( 'vaultpress', false ); } shortcodes.php 0000644 00000014716 15174711637 0007457 0 ustar 00 <?php /** * Module Name: Shortcode Embeds * Module Description: Easily embed rich media like YouTube videos and tweets using simple shortcodes. * Sort Order: 3 * First Introduced: 1.1 * Major Changes In: 1.2 * Requires Connection: No * Auto Activate: No * Module Tags: Photos and Videos, Social, Writing, Appearance * Feature: Writing * Additional Search Queries: shortcodes, shortcode, embeds, media, bandcamp, dailymotion, facebook, flickr, google calendars, google maps, polldaddy, recipe, recipes, scribd, slideshare, slideshow, slideshows, soundcloud, ted, twitter, vimeo, vine, youtube * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } // Load shortcode utils. require_once __DIR__ . '/shortcodes/shortcode-utils.php'; /** * Transforms the $atts array into a string that the old functions expected * * The old way was: * [shortcode a=1&b=2&c=3] or [shortcode=1] * This is parsed as array( a => '1&b=2&c=3' ) and array( 0 => '=1' ), which is useless * * @param array $params Array of old shortcode parameters. * @param bool $old_format_support true if [shortcode=foo] format is possible. * * @return string $params */ function shortcode_new_to_old_params( $params, $old_format_support = false ) { $str = ''; if ( $old_format_support && isset( $params[0] ) ) { $str = ltrim( $params[0], '=' ); } elseif ( is_array( $params ) ) { foreach ( array_keys( $params ) as $key ) { if ( ! is_numeric( $key ) ) { $str = $key . '=' . $params[ $key ]; } } } return str_replace( array( '&', '&' ), '&', $str ); } /** * Load all available Jetpack shortcode files. */ function jetpack_load_shortcodes() { // Prevent third-party shortcode plugins when loading shortcode files. // Format: shortcode => condition_when_to_skip $shortcode_skips = array( 'shortcode-utils' => true, // Utils aren't shortcodes. 'soundcloud' => function_exists( 'soundcloud_shortcode' ), // SoundCloud Shortcodes plugin ); $shortcode_includes = array(); foreach ( Jetpack::glob_php( __DIR__ . '/shortcodes' ) as $file ) { $filename = substr( basename( $file ), 0, -4 ); if ( empty( $shortcode_skips[ $filename ] ) ) { $shortcode_includes[ $filename ] = $file; } } /** * This filter allows other plugins to override which shortcodes Jetpack loads. * * Fires as part of the `after_setup_theme` WP hook, so modifying code needs to be in a plugin, not in a theme's functions.php. * * @module shortcodes * * @since 2.2.1 * @since 4.2.0 Added filename without extension as array key. * * @param array $shortcode_includes An array of which shortcodes to include. */ $shortcode_includes = apply_filters( 'jetpack_shortcodes_to_include', $shortcode_includes ); foreach ( $shortcode_includes as $include ) { include_once $include; } } /** * Runs preg_replace so that replacements don't happen within open tags. * Parameters are the same as preg_replace, with an added optional search param for improved performance * * @param string $pattern Pattern to search for. * @param string $replacement String to replace. * @param string $content Post content. * @param string $search String to search for. * * @return string $content Replaced post content. */ function jetpack_preg_replace_outside_tags( $pattern, $replacement, $content, $search = null ) { if ( $search && ! str_contains( $content, $search ) ) { return $content; } $textarr = wp_html_split( $content ); unset( $content ); foreach ( $textarr as &$element ) { if ( '' === $element || '<' === $element[0] ) { continue; } $element = preg_replace( $pattern, $replacement, $element ); } return implode( $textarr ); } /** * Runs preg_replace_callback so that replacements don't happen within open tags. * Parameters are the same as preg_replace, with an added optional search param for improved performance. * * @param string $pattern Pattern to search for. * @param string $callback Callback returning the replacement string. * @param string $content Post content. * @param string $search String to search for. * * @return string $content Replaced post content. */ function jetpack_preg_replace_callback_outside_tags( $pattern, $callback, $content, $search = null ) { if ( $search && ! str_contains( $content, $search ) ) { return $content; } $textarr = wp_html_split( $content ); unset( $content ); foreach ( $textarr as &$element ) { if ( '' === $element || '<' === $element[0] ) { continue; } $element = preg_replace_callback( $pattern, $callback, $element ); } return implode( $textarr ); } if ( ! function_exists( 'jetpack_shortcode_get_wpvideo_id' ) ) { /** * Get VideoPress ID from wpvideo shortcode attributes. * * @param array $atts Shortcode attributes. * * @return string|int $id VideoPress ID. */ function jetpack_shortcode_get_wpvideo_id( $atts ) { if ( isset( $atts[0] ) ) { return $atts[0]; } else { return 0; } } } if ( ! function_exists( 'jetpack_shortcode_get_videopress_id' ) ) { /** * Get VideoPress ID from videopress shortcode attributes. * * @param array $atts Shortcode attributes. * @return int $id VideoPress ID. */ function jetpack_shortcode_get_videopress_id( $atts ) { if ( isset( $atts[0] ) ) { return $atts[0]; } else { return 0; } } } /** * Common element attributes parsing and sanitizing for src, width and height. * * @since 4.5.0 * * @param array $attrs With original values. * * @return array $attrs With sanitized values. */ function wpcom_shortcodereverse_parseattr( $attrs ) { $defaults = array( 'src' => false, 'width' => false, 'height' => false, ); $attrs = shortcode_atts( $defaults, $attrs ); $attrs['src'] = wp_strip_all_tags( $attrs['src'] ); // For sanity. $attrs['width'] = ( is_numeric( $attrs['width'] ) ) ? abs( (int) $attrs['width'] ) : $defaults['width']; $attrs['height'] = ( is_numeric( $attrs['height'] ) ) ? abs( (int) $attrs['height'] ) : $defaults['height']; return $attrs; } /** * When an embed service goes away, we can use this handler * to output a link for history's sake. * * @param array $matches Regex partial matches against the URL passed. * @param array $attr Attributes received in embed response. * @param string $url Requested URL to be embedded. * @return string Link to output. */ function jetpack_deprecated_embed_handler( $matches, $attr, $url ) { return sprintf( '<a href="%s">%s</a>', esc_url( $url ), esc_html( esc_url( $url ) ) ); } jetpack_load_shortcodes(); infinite-scroll.php 0000644 00000020373 15174711637 0010377 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Module Name: Infinite Scroll * Module Description: Automatically load new posts as visitors scroll down your site. * Sort Order: 26 * First Introduced: 2.0 * Requires Connection: No * Auto Activate: No * Module Tags: Appearance * Feature: Appearance * Additional Search Queries: scroll, infinite, infinite scroll */ use Automattic\Jetpack\Current_Plan as Jetpack_Plan; use Automattic\Jetpack\Stats\Options as Stats_Options; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Jetpack-specific elements of Infinite Scroll */ class Jetpack_Infinite_Scroll_Extras { /** * Class variable singleton. * * @var Jetpack_Infinite_Scroll_Extras */ private static $instance = null; /** * Option names. * * @var string */ private $option_name_google_analytics = 'infinite_scroll_google_analytics'; /** * Singleton implementation * * @return object */ public static function instance() { if ( ! self::$instance instanceof Jetpack_Infinite_Scroll_Extras ) { self::$instance = new Jetpack_Infinite_Scroll_Extras(); } return self::$instance; } /** * Register actions and filters * * @uses add_action, add_filter */ private function __construct() { add_action( 'jetpack_modules_loaded', array( $this, 'action_jetpack_modules_loaded' ) ); add_action( 'admin_init', array( $this, 'action_admin_init' ), 11 ); add_action( 'after_setup_theme', array( $this, 'action_after_setup_theme' ), 5 ); add_filter( 'infinite_scroll_js_settings', array( $this, 'filter_infinite_scroll_js_settings' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ) ); } /** * Enable "Configure" button on module card * * @uses Jetpack::enable_module_configurable * @action jetpack_modules_loaded */ public function action_jetpack_modules_loaded() { Jetpack::enable_module_configurable( __FILE__ ); } /** * Register Google Analytics setting * * @uses add_settings_field, __, register_setting * @action admin_init */ public function action_admin_init() { if ( ! Jetpack_Plan::supports( 'google-analytics' ) ) { return; } add_settings_field( $this->option_name_google_analytics, '<span id="infinite-scroll-google-analytics">' . __( 'Use Google Analytics with Infinite Scroll', 'jetpack' ) . '</span>', array( $this, 'setting_google_analytics' ), 'reading' ); register_setting( 'reading', $this->option_name_google_analytics, array( $this, 'sanitize_boolean_value' ) ); } /** * Render Google Analytics option * * @uses checked, get_option, __ */ public function setting_google_analytics() { echo '<label><input name="infinite_scroll_google_analytics" type="checkbox" value="1" ' . checked( true, (bool) get_option( $this->option_name_google_analytics, false ), false ) . ' /> ' . esc_html__( 'Track each scroll load (7 posts by default) as a page view in Google Analytics', 'jetpack' ) . '</label>'; echo '<p class="description">' . esc_html__( 'Check the box above to record each new set of posts loaded via Infinite Scroll as a page view in Google Analytics.', 'jetpack' ) . '</p>'; } /** * Sanitize value as a boolean * * @param mixed $value - the value we're sanitizing. * @return bool */ public function sanitize_boolean_value( $value ) { return (bool) $value; } /** * Load theme's infinite scroll annotation file, if present in the IS plugin. * The `setup_theme` action is used because the annotation files should be using `after_setup_theme` to register support for IS. * * As released in Jetpack 2.0, a child theme's parent wasn't checked for in the plugin's bundled support, hence the convoluted way the parent is checked for now. * * @uses is_admin, wp_get_theme, apply_filters * @action setup_theme * @return null */ public function action_after_setup_theme() { $theme = wp_get_theme(); if ( ! $theme instanceof WP_Theme && ! is_array( $theme ) ) { return; } /** This filter is already documented in modules/infinite-scroll/infinity.php */ $customization_file = apply_filters( 'infinite_scroll_customization_file', __DIR__ . "/infinite-scroll/themes/{$theme['Stylesheet']}.php", $theme['Stylesheet'] ); if ( is_readable( $customization_file ) ) { require_once $customization_file; } elseif ( ! empty( $theme['Template'] ) ) { $customization_file = __DIR__ . "/infinite-scroll/themes/{$theme['Template']}.php"; if ( is_readable( $customization_file ) ) { require_once $customization_file; } } } /** * Modify Infinite Scroll configuration information * * @uses Jetpack::get_active_modules, is_user_logged_in, stats_get_options, Jetpack_Options::get_option, get_option, JETPACK__API_VERSION, JETPACK__VERSION * @filter infinite_scroll_js_settings * * @param array $settings - the settings. * @return array */ public function filter_infinite_scroll_js_settings( $settings ) { // Provide WP Stats info for tracking Infinite Scroll loads // Abort if Stats module isn't active if ( in_array( 'stats', Jetpack::get_active_modules(), true ) ) { // Abort if user is logged in but logged-in users shouldn't be tracked. if ( is_user_logged_in() ) { $stats_options = Stats_Options::get_options(); $track_loggedin_users = isset( $stats_options['count_roles'] ) ? (bool) $stats_options['count_roles'] : false; if ( ! $track_loggedin_users ) { return $settings; } } // We made it this far, so gather the data needed to track IS views $settings['stats'] = 'blog=' . Jetpack_Options::get_option( 'id' ) . '&host=' . wp_parse_url( get_option( 'home' ), PHP_URL_HOST ) . '&v=ext&j=' . JETPACK__API_VERSION . ':' . JETPACK__VERSION; // Pagetype parameter $settings['stats'] .= '&x_pagetype=infinite'; if ( 'click' === $settings['type'] ) { $settings['stats'] .= '-click'; } $settings['stats'] .= '-jetpack'; } // Check if Google Analytics tracking is requested. $settings['google_analytics'] = Jetpack_Plan::supports( 'google-analytics' ) && Jetpack_Options::get_option_and_ensure_autoload( $this->option_name_google_analytics, 0 ); return $settings; } /** * Always load certain scripts when IS is enabled, as they can't be loaded after `document.ready` fires, meaning they can't leverage IS's script loader. * * @global $videopress * @uses do_action() * @uses apply_filters() * @uses wp_enqueue_style() * @uses wp_enqueue_script() * @action wp_enqueue_scripts * @return null */ public function action_wp_enqueue_scripts() { // Do not load scripts and styles on singular pages and static pages $load_scripts_and_styles = ! ( is_singular() || is_page() ); if ( /** * Allow plugins to enqueue all Infinite Scroll scripts and styles on singular pages as well. * * @module infinite-scroll * * @since 3.1.0 * * @param bool $load_scripts_and_styles Should scripts and styles be loaded on singular pahes and static pages. Default to false. */ ! apply_filters( 'jetpack_infinite_scroll_load_scripts_and_styles', $load_scripts_and_styles ) ) { return; } // VideoPress stand-alone plugin global $videopress; if ( ! empty( $videopress ) && The_Neverending_Home_Page::archive_supports_infinity() && is_a( $videopress, 'VideoPress' ) && method_exists( $videopress, 'enqueue_scripts' ) ) { $videopress->enqueue_scripts(); } // VideoPress Jetpack module if ( Jetpack::is_module_active( 'videopress' ) ) { wp_enqueue_script( 'videopress' ); } // Fire the post_gallery action early so Carousel scripts are present. if ( Jetpack::is_module_active( 'carousel' ) ) { /** This filter is already documented in core/wp-includes/media.php */ do_action( 'post_gallery', '', '', 0 ); } // Always enqueue Tiled Gallery scripts when both IS and Tiled Galleries are enabled if ( Jetpack::is_module_active( 'tiled-gallery' ) ) { Jetpack_Tiled_Gallery::default_scripts_and_styles(); } } } Jetpack_Infinite_Scroll_Extras::instance(); /** * Load main IS file */ require_once __DIR__ . '/infinite-scroll/infinity.php'; /** * Remove the IS annotation loading function bundled with the IS plugin in favor of the Jetpack-specific version in Jetpack_Infinite_Scroll_Extras::action_after_setup_theme(); */ remove_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 ); widget-visibility/widget-conditions.php 0000644 00000131657 15174711637 0014410 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Main class file for the Widget Visibility module. * * @package automattic/jetpack */ use Automattic\Block_Scanner; use Automattic\Jetpack\Assets; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Hide or show legacy widgets conditionally. * * This class has two responsiblities - administrating the conditions in which legacy widgets may be hidden or shown * and hiding/showing the legacy widgets on the front-end of the site, depending upon the evaluation of those conditions. * * Administrating the conditions can be done in one of four different WordPress screens, plus direct use of the API and * is supplemented with a legacy widget preview screen. The four different admin screens are * * Gutenberg widget experience - widget admin (widgets.php + API + legacy widget preview) * Gutenberg widget experience - Customizer (customizer screen/API + API + legacy widget preview) * Classic widget experience - widget admin (widgets.php + admin-ajax XHR requests) * Classic widget experience - Customizer (customizer screen/API) * * An introduction to the API endpoints can be found here: https://make.wordpress.org/core/2021/06/29/rest-api-changes-in-wordpress-5-8/ */ class Jetpack_Widget_Conditions { /** * Stores condition for template_redirect action. * * @var bool */ public static $passed_template_redirect = false; /** * Class initializer. */ public static function init() { global $pagenow; // The Gutenberg based widget experience will show a preview of legacy widgets by including a URL beginning // widgets.php?legacy-widget-preview inside an iframe. Previews don't need widget editing loaded and also don't // want to run the filter - if the widget is filtered out it'll be empty, which would be confusing. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['legacy-widget-preview'] ) ) { return; } // If action is posted and it's save-widget then it's relevant to widget conditions, otherwise it's something // else and it's not worth registering hooks. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( isset( $_POST['action'] ) && ! isset( $_POST['customize_changeset_uuid'] ) && ! in_array( $_POST['action'], array( 'save-widget', 'update-widget' ), true ) ) { return; } // API call to *list* the widget types doesn't use editing visibility or display widgets. if ( isset( $_SERVER['REQUEST_URI'] ) && str_contains( $_SERVER['REQUEST_URI'], '/widget-types?' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return; } $add_data_assets_to_page = false; $add_html_to_form = false; $handle_widget_updates = false; $add_block_controls = false; // Check to see if using the customizer, but not using the preview. The preview should filter out widgets, // the customizer controls in the sidebar should not (so they can be edited). // phpcs:ignore WordPress.Security.NonceVerification.Recommended $customizer_not_previewer = is_customize_preview() && ! isset( $_GET['customize_changeset_uuid'] ); $using_classic_experience = ! wp_use_widgets_block_editor(); if ( $using_classic_experience && ( $customizer_not_previewer || 'widgets.php' === $pagenow || // phpcs:ignore WordPress.Security.NonceVerification.Missing ( 'admin-ajax.php' === $pagenow && array_key_exists( 'action', $_POST ) && 'save-widget' === $_POST['action'] ) ) ) { $add_data_assets_to_page = true; $add_html_to_form = true; $handle_widget_updates = true; } else { // On a screen that is hosting the API in the gutenberg editing experience. if ( $customizer_not_previewer || 'widgets.php' === $pagenow ) { $add_data_assets_to_page = true; $add_block_controls = true; } // Encoding for a particular widget end point. if ( isset( $_SERVER['REQUEST_URI'] ) && 1 === preg_match( '|/widget-types/.*/encode|', $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $add_html_to_form = true; $handle_widget_updates = true; } // Batch API is usually saving but could be anything. $current_url = ! empty( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; if ( str_contains( $current_url, '/wp-json/batch/v1' ) || 1 === preg_match( '/^\/wp\/v2\/sites\/\d+\/batch\/v1/', $current_url ) ) { $handle_widget_updates = true; $add_html_to_form = true; } // Saving widgets via non-batch API. This isn't used within WordPress but could be used by third parties in theory. if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] && str_contains( $_SERVER['REQUEST_URI'], '/wp/v2/widgets' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $handle_widget_updates = true; $add_html_to_form = true; } } if ( $add_html_to_form ) { add_action( 'in_widget_form', array( __CLASS__, 'widget_conditions_admin' ), 10, 3 ); } if ( $handle_widget_updates ) { add_filter( 'widget_update_callback', array( __CLASS__, 'widget_update' ), 10, 3 ); } if ( $add_data_assets_to_page ) { add_action( 'sidebar_admin_setup', array( __CLASS__, 'widget_admin_setup' ) ); } if ( $add_block_controls ) { add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'setup_block_controls' ) ); } if ( ! $add_html_to_form && ! $handle_widget_updates && ! $add_data_assets_to_page && ! in_array( $pagenow, array( 'wp-login.php', 'wp-register.php' ), true ) ) { // Not hit any known widget admin endpoint, register widget display hooks instead. add_filter( 'widget_display_callback', array( __CLASS__, 'filter_widget' ) ); add_filter( 'sidebars_widgets', array( __CLASS__, 'sidebars_widgets' ) ); add_action( 'template_redirect', array( __CLASS__, 'template_redirect' ) ); } } /** * Enqueue the block-based widget visibility scripts. */ public static function setup_block_controls() { Assets::register_script( 'widget-visibility-editor', '_inc/build/widget-visibility/editor/index.js', JETPACK__PLUGIN_FILE, array( 'in_footer' => true, 'textdomain' => 'jetpack', ) ); Assets::enqueue_script( 'widget-visibility-editor' ); } /** * Add the 'conditions' attribute, where visibility rules are stored, to some blocks. * * We normally add the block attributes in the browser's javascript env only, * but these blocks use a ServerSideRender dynamic preview, so the php env needs * to know about the new attribute, too. */ public static function add_block_attributes_filter() { $blocks = array( // These use <ServerSideRender>. 'core/calendar', 'core/latest-comments', 'core/rss', 'core/archives', 'core/tag-cloud', 'core/page-list', 'core/latest-posts', 'woocommerce/product-categories', ); /** * Filters the list of widget visibility blocks using <ServerSideRender>. * * @since 12.4 * * @module widget-visibility * * @param string[] $blocks Array of block names from WordPress core and WooCommerce. */ $blocks_to_add_visibility_conditions = apply_filters( 'jetpack_widget_visibility_server_side_render_blocks', $blocks ); /** * Block registration filter callback. * * @param array $settings Array of arguments for registering a block type. * @param string $name Block type name including namespace. * @return array */ $filter_metadata_registration = function ( $settings, $name ) use ( $blocks_to_add_visibility_conditions ) { if ( in_array( $name, $blocks_to_add_visibility_conditions, true ) && ! empty( $settings['attributes'] ) ) { $settings['attributes']['conditions'] = array( 'type' => 'object', ); } return $settings; }; add_filter( 'register_block_type_args', $filter_metadata_registration, 10, 2 ); } /** * Prepare the interface for editing widgets - loading css, javascript & data */ public static function widget_admin_setup() { wp_enqueue_style( 'widget-conditions', plugins_url( 'widget-conditions/widget-conditions.css', __FILE__ ), array( 'widgets' ), JETPACK__VERSION ); wp_style_add_data( 'widget-conditions', 'rtl', 'replace' ); wp_enqueue_script( 'widget-conditions', Assets::get_file_url_for_environment( '_inc/build/widget-visibility/widget-conditions/widget-conditions.min.js', 'modules/widget-visibility/widget-conditions/widget-conditions.js' ), array( 'jquery', 'jquery-ui-core' ), JETPACK__VERSION, true ); // Set up a single copy of all of the data that Widget Visibility needs. // This allows all widget conditions to reuse the same data, keeping page size down // and eliminating the AJAX calls we used to have to use to fetch the minor rule options. $widget_conditions_data = array(); $widget_conditions_data['category'] = array(); $widget_conditions_data['category'][] = array( '', __( 'All category pages', 'jetpack' ) ); $categories = get_categories( array( /** * Specific a maximum number of categories to query for the Widget visibility UI. * * @module widget-visibility * * @since 9.1.0 * * @param int $number Maximum number of categories displayed in the Widget visibility UI. */ 'number' => (int) apply_filters( 'jetpack_widget_visibility_max_number_categories', 1000 ), 'orderby' => 'count', 'order' => 'DESC', ) ); usort( $categories, array( __CLASS__, 'strcasecmp_name' ) ); foreach ( $categories as $category ) { $widget_conditions_data['category'][] = array( (string) $category->term_id, $category->name ); } $widget_conditions_data['loggedin'] = array(); $widget_conditions_data['loggedin'][] = array( 'loggedin', __( 'Logged In', 'jetpack' ) ); $widget_conditions_data['loggedin'][] = array( 'loggedout', __( 'Logged Out', 'jetpack' ) ); $widget_conditions_data['author'] = array(); $widget_conditions_data['author'][] = array( '', __( 'All author pages', 'jetpack' ) ); /* * Query for users with publish caps. */ $authors_args = array( 'orderby' => 'name', 'capability' => array( 'edit_posts' ), 'fields' => array( 'ID', 'display_name' ), ); $authors = get_users( $authors_args ); foreach ( $authors as $author ) { $widget_conditions_data['author'][] = array( (string) $author->ID, $author->display_name ); } $widget_conditions_data['role'] = array(); global $wp_roles; foreach ( $wp_roles->roles as $role_key => $role ) { $widget_conditions_data['role'][] = array( (string) $role_key, $role['name'] ); } $widget_conditions_data['tag'] = array(); $widget_conditions_data['tag'][] = array( '', __( 'All tag pages', 'jetpack' ) ); $tags = get_tags( array( /** * Specific a maximum number of tags to query for the Widget visibility UI. * * @module widget-visibility * * @since 9.1.0 * * @param int $number Maximum number of tags displayed in the Widget visibility UI. */ 'number' => (int) apply_filters( 'jetpack_widget_visibility_max_number_tags', 1000 ), 'orderby' => 'count', 'order' => 'DESC', ) ); usort( $tags, array( __CLASS__, 'strcasecmp_name' ) ); foreach ( $tags as $tag ) { $widget_conditions_data['tag'][] = array( (string) $tag->term_id, $tag->name ); } $widget_conditions_data['date'] = array(); $widget_conditions_data['date'][] = array( '', __( 'All date archives', 'jetpack' ) ); $widget_conditions_data['date'][] = array( 'day', __( 'Daily archives', 'jetpack' ) ); $widget_conditions_data['date'][] = array( 'month', __( 'Monthly archives', 'jetpack' ) ); $widget_conditions_data['date'][] = array( 'year', __( 'Yearly archives', 'jetpack' ) ); $widget_conditions_data['page'] = array(); $widget_conditions_data['page'][] = array( 'front', __( 'Front page', 'jetpack' ) ); $widget_conditions_data['page'][] = array( 'posts', __( 'Posts page', 'jetpack' ) ); $widget_conditions_data['page'][] = array( 'archive', __( 'Archive page', 'jetpack' ) ); $widget_conditions_data['page'][] = array( '404', __( '404 error page', 'jetpack' ) ); $widget_conditions_data['page'][] = array( 'search', __( 'Search results', 'jetpack' ) ); $post_types = get_post_types( array( 'public' => true ), 'objects' ); $widget_conditions_post_types = array(); $widget_conditions_post_type_archives = array(); foreach ( $post_types as $post_type ) { $widget_conditions_post_types[] = array( 'post_type-' . $post_type->name, $post_type->labels->singular_name ); $widget_conditions_post_type_archives[] = array( 'post_type_archive-' . $post_type->name, $post_type->labels->name ); } $widget_conditions_data['page'][] = array( __( 'Post type:', 'jetpack' ), $widget_conditions_post_types ); $widget_conditions_data['page'][] = array( __( 'Post type Archives:', 'jetpack' ), $widget_conditions_post_type_archives ); $pages = self::get_pages(); $dropdown_tree_args = array( 'depth' => 0, 'child_of' => 0, 'selected' => 0, 'echo' => false, 'name' => 'page_id', 'id' => '', 'class' => '', 'show_option_none' => '', 'show_option_no_change' => '', 'option_none_value' => '', 'value_field' => 'ID', ); $pages_dropdown = walk_page_dropdown_tree( $pages, 0, $dropdown_tree_args ); preg_match_all( '/value=.([0-9]+).[^>]*>([^<]+)</', $pages_dropdown, $page_ids_and_titles, PREG_SET_ORDER ); $static_pages = array(); foreach ( $page_ids_and_titles as $page_id_and_title ) { $static_pages[] = array( (string) $page_id_and_title[1], $page_id_and_title[2] ); } $widget_conditions_data['page'][] = array( __( 'Static page:', 'jetpack' ), $static_pages ); $widget_conditions_data['taxonomy'] = array(); $widget_conditions_data['taxonomy'][] = array( '', __( 'All taxonomy pages', 'jetpack' ) ); $taxonomies = get_taxonomies( /** * Filters args passed to get_taxonomies. * * @see https://developer.wordpress.org/reference/functions/get_taxonomies/ * * @since 5.3.0 * * @module widget-visibility * * @param array $args Widget Visibility taxonomy arguments. */ apply_filters( 'jetpack_widget_visibility_tax_args', array( '_builtin' => false ) ), 'objects' ); usort( $taxonomies, array( __CLASS__, 'strcasecmp_name' ) ); foreach ( $taxonomies as $taxonomy ) { $taxonomy_terms = get_terms( array( $taxonomy->name ), array( 'number' => 250, 'hide_empty' => false, ) ); $widget_conditions_terms = array(); $widget_conditions_terms[] = array( $taxonomy->name, $taxonomy->labels->all_items ); foreach ( $taxonomy_terms as $term ) { $widget_conditions_terms[] = array( $taxonomy->name . '_tax_' . $term->term_id, $term->name ); } $widget_conditions_data['taxonomy'][] = array( $taxonomy->labels->name . ':', $widget_conditions_terms ); } wp_localize_script( 'widget-conditions', 'widget_conditions_data', $widget_conditions_data ); // Save a list of the IDs of all pages that have children for dynamically showing the "Include children" checkbox. $all_pages = self::get_pages(); $all_parents = array(); foreach ( $all_pages as $page ) { if ( $page->post_parent ) { $all_parents[ (string) $page->post_parent ] = true; } } $front_page_id = get_option( 'page_on_front' ); if ( isset( $all_parents[ $front_page_id ] ) ) { $all_parents['front'] = true; } wp_localize_script( 'widget-conditions', 'widget_conditions_parent_pages', $all_parents ); } /** * Retrieves a full list of all pages, containing just the IDs, post_parent, post_title, and post_status fields. * * Since the WordPress' `get_pages` function does not allow us to fetch only the fields mentioned * above, we need to introduce a custom method using a direct SQL query fetching those. * * By fetching only those four fields and not populating the object cache for all the pages, we can * improve the performance of the query on sites having a lot of pages. * * @see https://core.trac.wordpress.org/ticket/51469 * * @return array List of all pages on the site (stdClass objects containing ID, post_title, post_parent, and post_status only). */ public static function get_pages() { global $wpdb; $last_changed = wp_cache_get_last_changed( 'posts' ); $cache_key = "get_pages:$last_changed"; $pages = wp_cache_get( $cache_key, 'widget_conditions' ); if ( false === $pages ) { $pages = $wpdb->get_results( "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_parent, {$wpdb->posts}.post_title, {$wpdb->posts}.post_status FROM {$wpdb->posts} WHERE {$wpdb->posts}.post_type = 'page' AND {$wpdb->posts}.post_status = 'publish' ORDER BY {$wpdb->posts}.post_title ASC" ); wp_cache_set( $cache_key, $pages, 'widget_conditions' ); } // Copy-pasted from the get_pages function. For usage in the `widget_conditions_get_pages` filter. $parsed_args = array( 'child_of' => 0, 'sort_order' => 'ASC', 'sort_column' => 'post_title', 'hierarchical' => 1, 'exclude' => array(), 'include' => array(), 'meta_key' => '', 'meta_value' => '', 'authors' => '', 'parent' => -1, 'exclude_tree' => array(), 'number' => '', 'offset' => 0, 'post_type' => 'page', 'post_status' => 'publish', ); /** * Filters the retrieved list of pages. * * @since 9.1.0 * * @module widget-visibility * * @param stdClass[] $pages Array of objects containing only the ID, post_parent, post_title, and post_status fields. * @param array $parsed_args Array of get_pages() arguments. */ return apply_filters( 'jetpack_widget_visibility_get_pages', $pages, $parsed_args ); } /** * Add the widget conditions to each widget in the admin. * * @param WP_Widget $widget Widget to add conditions settings to. * @param null $return unused. * @param array $instance The widget settings. */ public static function widget_conditions_admin( $widget, $return, $instance ) { $conditions = array(); if ( isset( $instance['conditions'] ) ) { $conditions = $instance['conditions']; } if ( ! isset( $conditions['action'] ) ) { $conditions['action'] = 'show'; } if ( empty( $conditions['rules'] ) ) { $conditions['rules'][] = array( 'major' => '', 'minor' => '', 'has_children' => '', ); } if ( empty( $conditions['match_all'] ) ) { $conditions['match_all'] = false; } ?> <div class=" widget-conditional <?php // $_POST['widget-conditions-visible'] is used in the classic widget experience to decide whether to // display the visibility panel open, e.g. when saving. In the gutenberg widget experience the POST // value will always be empty, but this is fine - it doesn't rerender the HTML when saving anyway. if ( // phpcs:ignore WordPress.Security.NonceVerification.Missing empty( $_POST['widget-conditions-visible'] ) || '0' === $_POST['widget-conditions-visible'] ) { ?> widget-conditional-hide <?php } ?> <?php if ( ! empty( $conditions['match_all'] ) && $conditions['match_all'] ) { ?> intersection <?php } else { ?> conjunction <?php } ?> "> <input type="hidden" name="widget-conditions-visible" value=" <?php if ( isset( $_POST['widget-conditions-visible'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing echo esc_attr( filter_var( wp_unslash( $_POST['widget-conditions-visible'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } else { echo 0; } ?> " /> <?php if ( ! isset( $_POST['widget-conditions-visible'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing ?> <a href="#" class="button display-options"><?php esc_html_e( 'Visibility', 'jetpack' ); ?></a><?php } ?> <div class="widget-conditional-inner"> <div class="condition-top"> <?php printf( // translators: %s is a HTML select widget for widget visibility, 'show' and 'hide' are it's options. It will read like 'show if' or 'hide if'. esc_html_x( '%s if:', 'placeholder: dropdown menu to select widget visibility; hide if or show if', 'jetpack' ), '<select name="' . esc_attr( $widget->get_field_name( 'conditions[action]' ) ) . '"> <option value="show" ' . selected( $conditions['action'], 'show', false ) . '>' . esc_html_x( 'Show', 'Used in the "%s if:" translation for the widget visibility dropdown', 'jetpack' ) . '</option> <option value="hide" ' . selected( $conditions['action'], 'hide', false ) . '>' . esc_html_x( 'Hide', 'Used in the "%s if:" translation for the widget visibility dropdown', 'jetpack' ) . '</option> </select>' ); ?> </div><!-- .condition-top --> <div class="conditions"> <?php foreach ( $conditions['rules'] as $rule_index => $rule ) { $rule = wp_parse_args( $rule, array( 'major' => '', 'minor' => '', 'has_children' => '', ) ); ?> <div class="condition" data-rule-major="<?php echo esc_attr( $rule['major'] ); ?>" data-rule-minor="<?php echo esc_attr( $rule['minor'] ); ?>" data-rule-has-children="<?php echo esc_attr( $rule['has_children'] ); ?>"> <div class="selection alignleft"> <select class="conditions-rule-major" name="<?php echo esc_attr( $widget->get_field_name( 'conditions[rules_major][]' ) ); ?>"> <option value="" <?php selected( '', $rule['major'] ); ?>><?php echo esc_html_x( '-- Select --', 'Used as the default option in a dropdown list', 'jetpack' ); ?></option> <option value="category" <?php selected( 'category', $rule['major'] ); ?>><?php esc_html_e( 'Category', 'jetpack' ); ?></option> <option value="author" <?php selected( 'author', $rule['major'] ); ?>><?php echo esc_html_x( 'Author', 'Noun, as in: "The author of this post is..."', 'jetpack' ); ?></option> <?php if ( ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) { // this doesn't work on .com because of caching. ?> <option value="loggedin" <?php selected( 'loggedin', $rule['major'] ); ?>><?php echo esc_html_x( 'User', 'Noun', 'jetpack' ); ?></option> <option value="role" <?php selected( 'role', $rule['major'] ); ?>><?php echo esc_html_x( 'Role', 'Noun, as in: "The user role of that can access this widget is..."', 'jetpack' ); ?></option> <?php } ?> <option value="tag" <?php selected( 'tag', $rule['major'] ); ?>><?php echo esc_html_x( 'Tag', 'Noun, as in: "This post has one tag."', 'jetpack' ); ?></option> <option value="date" <?php selected( 'date', $rule['major'] ); ?>><?php echo esc_html_x( 'Date', 'Noun, as in: "This page is a date archive."', 'jetpack' ); ?></option> <option value="page" <?php selected( 'page', $rule['major'] ); ?>><?php echo esc_html_x( 'Page', 'Example: The user is looking at a page, not a post.', 'jetpack' ); ?></option> <?php if ( get_taxonomies( array( '_builtin' => false ) ) ) : ?> <option value="taxonomy" <?php selected( 'taxonomy', $rule['major'] ); ?>><?php echo esc_html_x( 'Taxonomy', 'Noun, as in: "This post has one taxonomy."', 'jetpack' ); ?></option> <?php endif; ?> </select> <?php echo esc_html_x( 'is', 'Widget Visibility: {Rule Major [Page]} is {Rule Minor [Search results]}', 'jetpack' ); ?> <select class="conditions-rule-minor" name="<?php echo esc_attr( $widget->get_field_name( 'conditions[rules_minor][]' ) ); ?>" <?php if ( ! $rule['major'] ) { echo ' disabled="disabled"'; } ?> > <?php /* Include the currently selected value so that if the widget is saved without expanding the Visibility section, we don't lose the minor part of the rule. If it is opened, this list is cleared out and populated with all the values. */ ?> <option value="<?php echo esc_attr( $rule['minor'] ); ?>" selected="selected"></option> </select> <span class="conditions-rule-has-children" <?php if ( ! $rule['has_children'] ) { echo ' style="display: none;"'; } ?> > <label> <input type="checkbox" name="<?php echo esc_attr( $widget->get_field_name( "conditions[page_children][$rule_index]" ) ); ?>" value="has" <?php checked( $rule['has_children'], true ); ?> /> <?php echo esc_html_x( 'Include children', 'Checkbox on Widget Visibility if children of the selected page should be included in the visibility rule.', 'jetpack' ); ?> </label> </span> </div> <div class="condition-control"> <span class="condition-conjunction"> <?php echo esc_html_x( 'or', 'Shown between widget visibility conditions.', 'jetpack' ); ?> </span> <span class="condition-intersection"> <?php echo esc_html_x( 'and', 'Shown between widget visibility conditions.', 'jetpack' ); ?> </span> <div class="actions alignright"> <a href="#" class="delete-condition dashicons dashicons-no"><?php esc_html_e( 'Delete', 'jetpack' ); ?></a><a href="#" class="add-condition dashicons dashicons-plus"><?php esc_html_e( 'Add', 'jetpack' ); ?></a> </div> </div> </div><!-- .condition --> <?php } ?> </div><!-- .conditions --> <div class="conditions"> <div class="condition-top"> <label> <input type="checkbox" name="<?php echo esc_attr( $widget->get_field_name( 'conditions[match_all]' ) ); ?>" value="1" class="conditions-match-all" <?php checked( $conditions['match_all'], '1' ); ?> /> <?php esc_html_e( 'Match all conditions', 'jetpack' ); ?> </label> </div><!-- .condition-top --> </div><!-- .conditions --> </div><!-- .widget-conditional-inner --> </div><!-- .widget-conditional --> <?php } /** * On an AJAX update of the widget settings, process the display conditions. * * @param array $instance The current instance's settings. * @param array $new_instance New settings for this instance as input by the user. * @param array $old_instance Old settings for this instance. * @return array Modified settings. */ public static function widget_update( $instance, $new_instance, $old_instance ) { $conditions = array(); $conditions['action'] = isset( $new_instance['conditions']['action'] ) ? $new_instance['conditions']['action'] : null; $conditions['match_all'] = ! empty( $new_instance['conditions']['match_all'] ) ? '1' : '0'; $conditions['rules'] = isset( $new_instance['conditions']['rules'] ) ? $new_instance['conditions']['rules'] : array(); if ( isset( $new_instance['conditions']['rules_major'] ) ) { foreach ( $new_instance['conditions']['rules_major'] as $index => $major_rule ) { if ( ! $major_rule ) { continue; } $conditions['rules'][] = array( 'major' => $major_rule, 'minor' => isset( $new_instance['conditions']['rules_minor'][ $index ] ) ? $new_instance['conditions']['rules_minor'][ $index ] : '', 'has_children' => isset( $new_instance['conditions']['page_children'][ $index ] ) ? true : false, ); } } if ( ! empty( $conditions['rules'] ) ) { $instance['conditions'] = $conditions; } elseif ( empty( $new_instance['conditions']['rules'] ) ) { unset( $instance['conditions'] ); } if ( ( isset( $instance['conditions'] ) && ! isset( $old_instance['conditions'] ) ) || ( isset( $instance['conditions'], $old_instance['conditions'] ) && serialize( $instance['conditions'] ) !== serialize( $old_instance['conditions'] ) // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize ) ) { /** * Fires after the widget visibility conditions are saved. * * @module widget-visibility * * @since 2.4.0 */ do_action( 'widget_conditions_save' ); } elseif ( ! isset( $instance['conditions'] ) && isset( $old_instance['conditions'] ) ) { /** * Fires after the widget visibility conditions are deleted. * * @module widget-visibility * * @since 2.4.0 */ do_action( 'widget_conditions_delete' ); } return $instance; } /** * Filter the list of widgets for a sidebar so that active sidebars work as expected. * * @param array $widget_areas An array of widget areas and their widgets. * @return array The modified $widget_area array. */ public static function sidebars_widgets( $widget_areas ) { $settings = array(); if ( ! is_array( $widget_areas ) ) { return $widget_areas; } foreach ( $widget_areas as $widget_area => $widgets ) { if ( empty( $widgets ) ) { continue; } if ( ! is_array( $widgets ) ) { continue; } if ( 'wp_inactive_widgets' === $widget_area ) { continue; } foreach ( $widgets as $position => $widget_id ) { // Find the conditions for this widget. if ( preg_match( '/^(.+?)-(\d+)$/', $widget_id, $matches ) ) { $id_base = $matches[1]; $widget_number = (int) $matches[2]; } else { $id_base = $widget_id; $widget_number = null; } if ( ! isset( $settings[ $id_base ] ) ) { $settings[ $id_base ] = get_option( 'widget_' . $id_base ); } // New multi widget (WP_Widget). if ( $widget_number !== null ) { if ( isset( $settings[ $id_base ][ $widget_number ] ) && false === self::filter_widget( $settings[ $id_base ][ $widget_number ] ) ) { unset( $widget_areas[ $widget_area ][ $position ] ); } } elseif ( ! empty( $settings[ $id_base ] ) && false === self::filter_widget( $settings[ $id_base ] ) ) { // Old single widget. unset( $widget_areas[ $widget_area ][ $position ] ); } } } return $widget_areas; } /** * Set field $passed_template_redirect to true. */ public static function template_redirect() { self::$passed_template_redirect = true; } /** * Generates a condition key based on the rule array. * * @param array $rule rule data. * @return string key used to retrieve the condition. */ public static function generate_condition_key( $rule ) { if ( isset( $rule['has_children'] ) ) { return $rule['major'] . ':' . $rule['minor'] . ':' . $rule['has_children']; } return $rule['major'] . ':' . $rule['minor']; } /** * Normalize widget `content` into a string suitable for block scanning. * * @since 15.1 * * @param mixed $content The widget instance 'content' value. * @return string|false Normalized string content or false if none. */ private static function normalize_widget_content( $content ) { if ( empty( $content ) ) { return false; } if ( is_string( $content ) ) { return $content; } if ( ! is_array( $content ) ) { return false; } if ( isset( $content['content'] ) && is_string( $content['content'] ) ) { return $content['content']; } if ( isset( $content[0] ) && is_array( $content[0] ) && isset( $content[0]['blockName'] ) ) { // Looks like a parsed blocks array. return serialize_blocks( $content ); } // Unknown array shape: treat as no visibility rules. return false; } /** * Determine whether the widget should be displayed based on conditions set by the user. * * @param array $instance The widget settings. * @return array Settings to display or bool false to hide. */ public static function filter_widget( $instance ) { // Don't filter widgets from the REST API when it's called via the widgets admin page - otherwise they could get // filtered out and become impossible to edit. if ( strpos( wp_get_raw_referer(), '/wp-admin/widgets.php' ) && isset( $_SERVER['REQUEST_URI'] ) && str_contains( filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ), '/wp-json/' ) ) { return $instance; } // WordPress.com specific check - here, referer ends in /rest-proxy/ and doesn't tell us what's requesting. $current_url = ! empty( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $nonce = ! empty( $_REQUEST['_gutenberg_nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_gutenberg_nonce'] ) ) : ''; $context = ! empty( $_REQUEST['context'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['context'] ) ) : ''; if ( wp_verify_nonce( $nonce, 'gutenberg_request' ) && 1 === preg_match( '~^/wp/v2/sites/\d+/(sidebars|widgets)~', $current_url ) && 'edit' === $context ) { return $instance; } if ( ! empty( $instance['conditions']['rules'] ) ) { // Legacy widgets: visibility found. if ( self::filter_widget_check_conditions( $instance['conditions'] ) ) { return $instance; } return false; } if ( empty( $instance['content'] ) ) { return $instance; } $content = self::normalize_widget_content( isset( $instance['content'] ) ? $instance['content'] : null ); if ( false === $content || ! has_blocks( $content ) ) { // No visibility found. return $instance; } $scanner = Block_Scanner::create( $content ); if ( ! $scanner ) { // No Rules: Display widget. return $instance; } // Find the first block that opens while ( $scanner->next_delimiter() ) { if ( ! $scanner->opens_block() ) { continue; } $attributes = $scanner->allocate_and_return_parsed_attributes(); if ( ! is_array( $attributes ) || empty( $attributes['conditions']['rules'] ) ) { // No Rules: Display widget. return $instance; } if ( self::filter_widget_check_conditions( $attributes['conditions'] ) ) { // Rules passed checks: Display widget. return $instance; } // Rules failed checks: Hide widget. return false; } // No visibility found. return $instance; } /** * Determine whether the widget should be displayed based on conditions set by the user. * * @param array $conditions Visibility Conditions: An array with keys 'rules', 'action', and 'match_all'. * @return bool If the widget controlled by these conditions should show. */ public static function filter_widget_check_conditions( $conditions ) { global $wp_query; // Store the results of all in-page condition lookups so that multiple widgets with // the same visibility conditions don't result in duplicate DB queries. static $condition_result_cache = array(); $condition_result = false; foreach ( $conditions['rules'] as $rule ) { $condition_result = false; $condition_key = self::generate_condition_key( $rule ); if ( isset( $condition_result_cache[ $condition_key ] ) ) { $condition_result = $condition_result_cache[ $condition_key ]; } else { switch ( $rule['major'] ) { case 'date': switch ( $rule['minor'] ) { case '': $condition_result = is_date(); break; case 'month': $condition_result = is_month(); break; case 'day': $condition_result = is_day(); break; case 'year': $condition_result = is_year(); break; } break; case 'page': // Previously hardcoded post type options. if ( 'post' === $rule['minor'] ) { $rule['minor'] = 'post_type-post'; } elseif ( ! $rule['minor'] ) { $rule['minor'] = 'post_type-page'; } switch ( $rule['minor'] ) { case '404': $condition_result = is_404(); break; case 'search': $condition_result = is_search(); break; case 'archive': $condition_result = is_archive(); break; case 'posts': $condition_result = $wp_query->is_posts_page; break; case 'home': $condition_result = is_home(); break; case 'front': if ( current_theme_supports( 'infinite-scroll' ) ) { $condition_result = is_front_page(); } else { $condition_result = is_front_page() && ! is_paged(); } break; default: if ( str_starts_with( $rule['minor'], 'post_type-' ) ) { $condition_result = is_singular( substr( $rule['minor'], 10 ) ); } elseif ( str_starts_with( $rule['minor'], 'post_type_archive-' ) ) { $condition_result = is_post_type_archive( substr( $rule['minor'], 18 ) ); } elseif ( get_option( 'page_for_posts' ) === $rule['minor'] ) { // If $rule['minor'] is a page ID which is also the posts page. $condition_result = $wp_query->is_posts_page; } else { // $rule['minor'] is a page ID $condition_result = is_page() && ( get_the_ID() === (int) $rule['minor'] ); // Check if $rule['minor'] is parent of page ID. if ( ! $condition_result && isset( $rule['has_children'] ) && $rule['has_children'] ) { $condition_result = wp_get_post_parent_id( get_the_ID() ) === (int) $rule['minor']; } } break; } break; case 'tag': // All tag pages. if ( ! $rule['minor'] ) { if ( is_tag() ) { $condition_result = true; } elseif ( is_singular() ) { if ( in_array( 'post_tag', get_post_taxonomies(), true ) ) { $condition_result = true; } } break; } // All pages with the specified tag term. if ( is_tag( $rule['minor'] ) ) { $condition_result = true; } elseif ( is_singular() && has_term( $rule['minor'], 'post_tag' ) ) { $condition_result = true; } break; case 'category': // All category pages. if ( ! $rule['minor'] ) { if ( is_category() ) { $condition_result = true; } elseif ( is_singular() ) { if ( in_array( 'category', get_post_taxonomies(), true ) ) { $condition_result = true; } } break; } // All pages with the specified category term. if ( is_category( $rule['minor'] ) ) { $condition_result = true; } elseif ( is_singular() && has_term( $rule['minor'], 'category' ) ) { $condition_result = true; } break; case 'loggedin': $condition_result = is_user_logged_in(); if ( 'loggedin' !== $rule['minor'] ) { $condition_result = ! $condition_result; } break; case 'author': if ( ! $rule['minor'] && is_author() ) { $condition_result = true; } elseif ( $rule['minor'] && is_author( $rule['minor'] ) ) { $condition_result = true; } elseif ( is_singular() && $rule['minor'] ) { $post = get_post(); if ( $post && $rule['minor'] === $post->post_author ) { $condition_result = true; } } break; case 'role': if ( is_user_logged_in() ) { $current_user = wp_get_current_user(); $user_roles = $current_user->roles; if ( in_array( $rule['minor'], $user_roles, true ) ) { $condition_result = true; } else { $condition_result = false; } } else { $condition_result = false; } break; case 'post_type': if ( str_starts_with( $rule['minor'], 'post_type-' ) ) { $condition_result = is_singular( substr( $rule['minor'], 10 ) ); } elseif ( str_starts_with( $rule['minor'], 'post_type_archive-' ) ) { $condition_result = is_post_type_archive( substr( $rule['minor'], 18 ) ); } break; case 'taxonomy': // All taxonomy pages. if ( ! $rule['minor'] ) { if ( is_archive() ) { if ( is_tag() || is_category() || is_tax() ) { $condition_result = true; } } elseif ( is_singular() ) { $post_taxonomies = get_post_taxonomies(); $condition_result = ! empty( $post_taxonomies ); } break; } // Specified taxonomy page. $term = explode( '_tax_', $rule['minor'] ); // $term[0] is taxonomy name; $term[1] is term id. if ( isset( $term[0] ) && isset( $term[1] ) ) { $term[1] = self::maybe_get_split_term( $term[1], $term[0] ); } // All pages of the specified taxonomy. if ( ! isset( $term[1] ) || ! $term[1] ) { if ( is_tax( $term[0] ) ) { $condition_result = true; } elseif ( is_singular() ) { if ( in_array( $term[0], get_post_taxonomies(), true ) ) { $condition_result = true; } } break; } // All pages with the specified taxonomy term. if ( is_tax( $term[0], $term[1] ) ) { $condition_result = true; } elseif ( is_singular() && has_term( $term[1], $term[0] ) ) { $condition_result = true; } break; } if ( $condition_result || self::$passed_template_redirect ) { // Some of the conditions will return false when checked before the template_redirect // action has been called, like is_page(). Only store positive lookup results, which // won't be false positives, before template_redirect, and everything after. $condition_result_cache[ $condition_key ] = $condition_result; } } if ( isset( $conditions['match_all'] ) && '1' === $conditions['match_all'] && ! $condition_result ) { // In case the match_all flag was set we quit on first failed condition. break; } elseif ( ( empty( $conditions['match_all'] ) || '1' !== $conditions['match_all'] ) && $condition_result ) { // Only quit on first condition if the match_all flag was not set. break; } } if ( ( 'show' === $conditions['action'] && ! $condition_result ) || ( 'hide' === $conditions['action'] && $condition_result ) ) { return false; } return true; } /** * Helper function wrapping strcasecmp to compare term names. * * @param string $a str1. * @param string $b str2. */ public static function strcasecmp_name( $a, $b ) { return strcasecmp( $a->name, $b->name ); } /** * Determine if provided term has been split. * * @param int $old_term_id Old term id to test. * @param string $taxonomy Taxonmy that $old_term_id belongs to. */ public static function maybe_get_split_term( $old_term_id = '', $taxonomy = '' ) { $term_id = $old_term_id; if ( 'tag' === $taxonomy ) { $taxonomy = 'post_tag'; } $new_term_id = wp_get_split_term( $old_term_id, $taxonomy ); if ( $new_term_id ) { $term_id = $new_term_id; } return $term_id; } /** * Upgrade routine to go through all widgets and move the Post Type * setting to its newer location. * * @since 4.7.1 */ public static function migrate_post_type_rules() { global $wp_widget_factory, $wp_registered_widgets; '@phan-var \WP_Widget_Factory $wp_widget_factory'; $sidebars_widgets = get_option( 'sidebars_widgets' ); if ( ! is_array( $sidebars_widgets ) ) { return; } // Going through all sidebars and through inactive and orphaned widgets. foreach ( $sidebars_widgets as $sidebar ) { if ( ! is_array( $sidebar ) ) { continue; } foreach ( $sidebar as $widget ) { // $widget is the id of the widget if ( empty( $wp_registered_widgets[ $widget ] ) ) { continue; } $id_base = wp_parse_widget_id( $widget )['id_base']; $widget_object = $wp_widget_factory->get_widget_object( $id_base ); if ( ! $widget_object ) { continue; } $instances = get_option( $widget_object->option_name ); if ( ! is_array( $instances ) || empty( $instances ) ) { continue; } // Going through each instance of the widget. foreach ( $instances as $number => $instance ) { if ( ! is_array( $instance ) || empty( $instance['conditions']['rules'] ) || ! is_array( $instance['conditions']['rules'] ) ) { continue; } // Going through all visibility rules. foreach ( $instance['conditions']['rules'] as $index => $rule ) { // We only need Post Type rules. if ( 'post_type' !== $rule['major'] ) { continue; } $rule_type = false; // Post type or type archive rule. if ( str_starts_with( $rule['minor'], 'post_type_archive' ) ) { $rule_type = 'post_type_archive'; } elseif ( str_starts_with( $rule['minor'], 'post_type' ) ) { $rule_type = 'post_type'; } if ( $rule_type ) { $post_type = substr( $rule['minor'], strlen( $rule_type ) + 1 ); $rule['minor'] = $rule_type . '-' . $post_type; $rule['major'] = 'page'; $instances[ $number ]['conditions']['rules'][ $index ] = $rule; } } } update_option( $widget_object->option_name, $instances ); } } } } add_action( 'init', array( 'Jetpack_Widget_Conditions', 'init' ) ); // Add the 'conditions' attribute to server side rendered blocks // 'init' happens too late to hook on block registration. global $pagenow; $current_url = ! empty( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; if ( is_customize_preview() || 'widgets.php' === $pagenow || str_contains( $current_url, '/wp-json/wp/v2/block-renderer' ) || 1 === preg_match( '~^/wp/v2/sites/\d+/block-renderer~', $current_url ) ) { Jetpack_Widget_Conditions::add_block_attributes_filter(); } widget-visibility/widget-conditions/widget-conditions.min.css 0000644 00000007333 15174711637 0020616 0 ustar 00 .wp-customizer .expanded .widget-conditional .widget-conditional-inner{box-sizing:border-box;width:98%}.wp-customizer .expanded .widget-conditional .form{margin-bottom:20px;overflow:scroll}.widget-liquid-right .widget.expanded{overflow:visible}.widget-conditional-hide{display:none}.widget-conditional .widget-conditional-inner{background:#f6f7f7;border:1px solid #dcdcde;padding:12px 10px 0}.widget-conditional{margin-bottom:12px;margin-top:10px}.widget-conditional .conditions{margin-bottom:12px}.widget-conditional .condition,.widget-conditional .condition-top{clear:both}.widget-conditional .condition{padding-top:12px;position:relative}.widget-conditional .condition select{position:relative;width:120px;z-index:2}.widget-conditional .condition-top select{width:auto}.widget-conditional .condition-control{clear:both;margin-top:-20px;padding-top:4px}.widget-conditional .selection{margin-left:20px;margin-right:50px}.widget-conditional .conditions-rule-has-children{display:block}.widget-conditional .condition .actions{margin-top:-28px}.widget-conditional .condition-control a{position:absolute;text-decoration:none;text-indent:-9999px;top:17px;z-index:1}.wp-block-legacy-widget__edit-form .widget-conditional .condition-control a{top:20px}.widget-conditional .condition-control a:before{left:0;position:absolute;text-indent:0;top:0}.widget-conditional .condition-control .delete-condition,.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .condition-control .delete-condition{color:#f11;left:0}.widget-conditional .condition-control .add-condition{right:0}.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .widget-conditional-inner a.dashicons{font-family:dashicons}.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional-inner select{background-color:#fff;display:initial;width:auto}.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .widget-conditional-inner select:disabled{background-color:#f6f7f7;border-color:#dcdcde;color:#a7aaad}.editor-styles-wrapper .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .alignleft{margin-left:20px}.widget-conditional .condition:last-child .condition-conjunction,.widget-conditional .condition:last-child .condition-intersection,.widget-conditional.conjunction .condition-intersection,.widget-conditional.intersection .condition-conjunction{display:none}.wp-core-ui .button.display-options{margin-right:5px}.wp-core-ui .button.display-options:hover{text-decoration:none}.wp-customizer .widget-conditional select{height:auto;max-width:none;min-width:0}.wp-customizer .widget-conditional .condition-control a{top:15px}@media screen and (max-width:782px){.widget-conditional .condition-control a{top:20px}}.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional-inner{font-size:13px}.widget-vis__rule,.widget-vis__rule .components-base-control,.widget-vis__wrapper .components-base-control{margin-bottom:12px}.widget-vis__rule{border:1px solid #dcdcde;display:flex;flex-direction:column;padding:12px}.widget-vis__rule-major,.widget-vis__rule-minor{align-items:center;display:flex;justify-content:stretch}.widget-vis__rule-minor{margin-top:8px}.widget-vis__if,.widget-vis__is{margin-right:8px;width:10px}.widget-vis__show-hide{margin-bottom:0}.components-button.widget-vis__add-new-rule{display:block}.widget-vis__delete-rule{margin-top:6px;text-align:right}.widget-vis__match-all{margin-top:12px}.widget-vis__select{width:100%}.widget-vis__select .components-input-control__container{position:relative;top:4px}.widget-vis__select-multi-level select option:disabled{font-size:18px;font-weight:600}.widget-vis__delete-rule .components-button.is-secondary{margin-top:8px} widget-visibility/widget-conditions/widget-conditions.js 0000644 00000022674 15174711637 0017665 0 ustar 00 /* global isRtl, widget_conditions_parent_pages, widget_conditions_data */ jQuery( function ( $ ) { // Gutenberg 'widgets.php' screen. var widgets_shell = $( '#widgets-editor' ); if ( 0 === widgets_shell.length ) { // Legacy 'widgets.php' screen + customizer. widgets_shell = $( 'div#widgets-right' ); // For backwards compatibility if ( 0 === widgets_shell.length ) { widgets_shell = $( 'form#customize-controls' ); } } function setWidgetMargin( $widget ) { var currentWidth, extra; if ( $( 'body' ).hasClass( 'wp-customizer' ) ) { // set the inside widget 2 top this way we can see the widget settings $widget.find( '.widget-inside' ).css( 'top', 0 ); return; } if ( $widget.hasClass( 'expanded' ) ) { // The expanded widget must be at least 400px wide in order to // contain the visibility settings. IE wasn't handling the // margin-left value properly. if ( $widget.attr( 'style' ) ) { $widget.data( 'original-style', $widget.attr( 'style' ) ); } currentWidth = $widget.width(); if ( currentWidth < 400 ) { extra = 400 - currentWidth; if ( isRtl ) { $widget .css( 'position', 'relative' ) .css( 'right', '-' + extra + 'px' ) .css( 'width', '400px' ); } else { $widget .css( 'position', 'relative' ) .css( 'left', '-' + extra + 'px' ) .css( 'width', '400px' ); } } } else if ( $widget.data( 'original-style' ) ) { // Restore any original inline styles when visibility is toggled off. $widget.attr( 'style', $widget.data( 'original-style' ) ).data( 'original-style', null ); } else { $widget.removeAttr( 'style' ); } } function moveWidgetVisibilityButton( $widget ) { var $displayOptionsButton = $widget.find( 'a.display-options' ).first(), $relativeWidget = $widget.find( 'input.widget-control-save' ); if ( 0 === $relativeWidget.length ) { // The save button doesn't exist in gutenberg widget editor, the conditional HTML ought to be displayed // last inside the widget options, so display the button before that. $relativeWidget = $widget.find( 'div.widget-conditional' ); } $displayOptionsButton.insertBefore( $relativeWidget ); // Widgets with no configurable options don't show the Save button's container. $displayOptionsButton .parent() .removeClass( 'widget-control-noform' ) .find( '.spinner' ) .remove() .css( 'float', 'left' ) .prependTo( $displayOptionsButton.parent() ); } $( '.widget' ).each( function () { moveWidgetVisibilityButton( $( this ) ); } ); $( document ).on( 'widget-added', function ( e, $widget ) { if ( $widget.find( 'div.widget-control-actions a.display-options' ).length === 0 ) { moveWidgetVisibilityButton( $widget ); } } ); widgets_shell.on( 'click.widgetconditions', 'a.add-condition', function ( e ) { var $condition = $( this ).closest( 'div.condition' ), $conditionClone = $condition .clone() .data( 'rule-major', '' ) .data( 'rule-minor', '' ) .data( 'has-children', '' ) .insertAfter( $condition ); e.preventDefault(); $conditionClone.find( 'select.conditions-rule-major' ).val( '' ); $conditionClone.find( 'select.conditions-rule-minor' ).html( '' ).attr( 'disabled' ); $conditionClone .find( 'span.conditions-rule-has-children' ) .hide() .find( 'input[type="checkbox"]' ) .removeAttr( 'checked' ); resetRuleIndexes( $conditionClone.closest( '.conditions' ) ); } ); widgets_shell.on( 'click.widgetconditions', 'a.display-options', function ( e ) { var $displayOptionsButton = $( this ), $widget = $displayOptionsButton.closest( 'div.widget' ); e.preventDefault(); $widget.find( 'div.widget-conditional' ).toggleClass( 'widget-conditional-hide' ); $( this ).toggleClass( 'active' ); $widget.toggleClass( 'expanded' ); setWidgetMargin( $widget ); if ( $( this ).hasClass( 'active' ) ) { $widget.find( 'input[name=widget-conditions-visible]' ).val( '1' ); $widget.find( '.condition' ).each( function () { buildMinorConditions( $( this ) ); } ); } else { $widget.find( 'input[name=widget-conditions-visible]' ).val( '0' ); } } ); widgets_shell.on( 'click.widgetconditions', 'a.delete-condition', function ( e ) { var $condition = $( this ).closest( 'div.condition' ); e.preventDefault(); if ( $condition.is( ':first-child' ) && $condition.is( ':last-child' ) ) { $( this ).closest( 'div.widget' ).find( 'a.display-options' ).click(); $condition.find( 'select.conditions-rule-major' ).val( '' ).change(); } else { $condition.find( 'select.conditions-rule-major' ).change(); $condition.detach(); } resetRuleIndexes( $condition.closest( '.conditions' ) ); } ); widgets_shell.on( 'click.widgetconditions', 'div.widget-top', function () { var $widget = $( this ).closest( 'div.widget' ), $displayOptionsButton = $widget.find( 'a.display-options' ); if ( $displayOptionsButton.hasClass( 'active' ) ) { $displayOptionsButton.attr( 'opened', 'true' ); } if ( $displayOptionsButton.attr( 'opened' ) ) { $displayOptionsButton.removeAttr( 'opened' ); $widget.toggleClass( 'expanded' ); setWidgetMargin( $widget ); } } ); widgets_shell.on( 'change.widgetconditions', 'input.conditions-match-all', function () { $( this ) .parents( '.widget-conditional' ) .toggleClass( 'conjunction' ) .toggleClass( 'intersection' ); } ); $( document ).on( 'change.widgetconditions', 'select.conditions-rule-major', function () { var $conditionsRuleMajor = $( this ), $conditionsRuleMinor = $conditionsRuleMajor.siblings( 'select.conditions-rule-minor:first' ), $conditionsRuleHasChildren = $conditionsRuleMajor.siblings( 'span.conditions-rule-has-children' ), $condition = $conditionsRuleMinor.closest( '.condition' ); $condition.data( 'rule-minor', '' ).data( 'rule-major', $conditionsRuleMajor.val() ); if ( $conditionsRuleMajor.val() ) { buildMinorConditions( $condition ); } else { $conditionsRuleMajor .siblings( 'select.conditions-rule-minor' ) .attr( 'disabled', 'disabled' ) .html( '' ); $conditionsRuleHasChildren.hide().find( 'input[type="checkbox"]' ).removeAttr( 'checked' ); } } ); $( document ).on( 'change.widgetconditions', 'select.conditions-rule-minor', function () { var $conditionsRuleMinor = $( this ), $conditionsRuleMajor = $conditionsRuleMinor.siblings( 'select.conditions-rule-major' ), $conditionsRuleHasChildren = $conditionsRuleMinor.siblings( 'span.conditions-rule-has-children' ), $condition = $conditionsRuleMinor.closest( '.condition' ); $condition.data( 'rule-minor', $conditionsRuleMinor.val() ); if ( $conditionsRuleMajor.val() === 'page' ) { if ( $conditionsRuleMinor.val() in widget_conditions_parent_pages ) { $conditionsRuleHasChildren.show(); } else { $conditionsRuleHasChildren.hide().find( 'input[type="checkbox"]' ).removeAttr( 'checked' ); } } else { $conditionsRuleHasChildren.hide().find( 'input[type="checkbox"]' ).removeAttr( 'checked' ); } } ); $( document ).on( 'widget-updated widget-synced', function ( e, widget ) { widget.find( '.condition' ).each( function () { buildMinorConditions( $( this ) ); } ); } ); function buildMinorConditions( condition ) { var minor, hasChildren, majorData, i, j, key, val, _len, _jlen, subkey, subval, optgroup, select = condition.find( '.conditions-rule-minor' ).html( '' ), major = condition.data( 'rule-major' ); // Disable the select, if major rule is empty or if it's a `post_type`. // "Post Type" rule has been removed in Jetpack 4.7, and // because it breaks all other rules we should `return`. if ( ! major || 'post_type' === major ) { select.attr( 'disabled', 'disabled' ); return; } minor = condition.data( 'rule-minor' ); hasChildren = condition.data( 'rule-has-children' ); majorData = widget_conditions_data[ major ]; for ( i = 0, _len = majorData.length; i < _len; i++ ) { key = majorData[ i ][ 0 ]; val = majorData[ i ][ 1 ]; if ( typeof val === 'object' ) { optgroup = $( '<optgroup/>' ).attr( 'label', key ); for ( j = 0, _jlen = val.length; j < _jlen; j++ ) { subkey = majorData[ i ][ 1 ][ j ][ 0 ]; subval = majorData[ i ][ 1 ][ j ][ 1 ]; optgroup.append( $( '<option/>' ) .val( subkey ) .text( decodeEntities( subval.replace( / /g, '\xA0' ) ) ) ); } select.append( optgroup ); } else { select.append( $( '<option/>' ) .val( key ) .text( decodeEntities( val.replace( / /g, '\xA0' ) ) ) ); } } select.removeAttr( 'disabled' ); select.val( minor ); if ( 'page' === major && minor in widget_conditions_parent_pages ) { select.siblings( 'span.conditions-rule-has-children' ).show(); if ( hasChildren ) { select .siblings( 'span.conditions-rule-has-children' ) .find( 'input[type="checkbox"]' ) .attr( 'checked', 'checked' ); } } else { select .siblings( 'span.conditions-rule-has-children' ) .hide() .find( 'input[type="checkbox"]' ) .removeAttr( 'checked' ); } } function resetRuleIndexes( widget ) { var index = 0; widget .find( 'span.conditions-rule-has-children' ) .find( 'input[type="checkbox"]' ) .each( function () { $( this ).attr( 'name', 'conditions[page_children][' + index + ']' ); index++; } ); } function decodeEntities( encodedString ) { var textarea = document.createElement( 'textarea' ); textarea.innerHTML = encodedString; return textarea.value; } } ); widget-visibility/widget-conditions/widget-conditions.css 0000644 00000010623 15174711637 0020030 0 ustar 00 .wp-customizer .expanded .widget-conditional .widget-conditional-inner { width: 98%; box-sizing: border-box; } .wp-customizer .expanded .widget-conditional .form { overflow: scroll; margin-bottom: 20px; } .widget-liquid-right .widget.expanded { overflow: visible; } .widget-conditional-hide { display: none; } .widget-conditional .widget-conditional-inner { background: #f6f7f7; border: 1px solid #dcdcde; padding: 12px 10px 0; } .widget-conditional { margin-bottom: 12px; margin-top: 10px; } .widget-conditional .conditions { margin-bottom: 12px; } .widget-conditional .condition, .widget-conditional .condition-top { clear: both; } .widget-conditional .condition { padding-top: 12px; position: relative; } .widget-conditional .condition select { width: 120px; position: relative; z-index: 2; } .widget-conditional .condition-top select { width: auto; } .widget-conditional .condition-control { padding-top: 4px; clear: both; margin-top: -20px; } .widget-conditional .selection { margin-right: 50px; margin-left: 20px; } .widget-conditional .conditions-rule-has-children { display: block; } .widget-conditional .condition .actions { margin-top: -28px; } .widget-conditional .condition-control a { text-decoration: none; position: absolute; top: 17px; text-indent: -9999px; z-index: 1; } .wp-block-legacy-widget__edit-form .widget-conditional .condition-control a { top: 20px; } .widget-conditional .condition-control a::before { position: absolute; text-indent: 0; top: 0; left: 0; } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .condition-control .delete-condition, .widget-conditional .condition-control .delete-condition { left: 0; color: #f11; } .widget-conditional .condition-control .add-condition { right: 0; } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .widget-conditional-inner a.dashicons { font-family: dashicons; } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional-inner select { display: initial; width: auto; background-color: #fff; } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .widget-conditional-inner select:disabled { color: #a7aaad; border-color: #dcdcde; background-color: #f6f7f7; } .editor-styles-wrapper .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .alignleft { margin-left: 20px; } .widget-conditional .condition:last-child .condition-conjunction, .widget-conditional .condition:last-child .condition-intersection { display: none; } .widget-conditional.conjunction .condition-intersection { display: none; } .widget-conditional.intersection .condition-conjunction { display: none; } .wp-core-ui .button.display-options { margin-right: 5px; } .wp-core-ui .button.display-options:hover { text-decoration: none; } .wp-customizer .widget-conditional select { min-width: 0; max-width: none; height: auto; } .wp-customizer .widget-conditional .condition-control a { top: 15px; } @media screen and ( max-width: 782px ) { .widget-conditional .condition-control a { top: 20px; } } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional-inner { /* * fonts of labels are reset to 13px in gutenberg editor for legacy widgets * ensure a consistent look on non-labels */ font-size: 13px; } /* * Rules for gutenberg advanced panel */ .widget-vis__wrapper .components-base-control, .widget-vis__rule .components-base-control { margin-bottom: 12px; } .widget-vis__rule { padding: 12px; border: 1px solid #dcdcde; display: flex; flex-direction: column; margin-bottom: 12px; } .widget-vis__rule-major, .widget-vis__rule-minor { display: flex; justify-content: stretch; align-items: center; } .widget-vis__rule-minor { margin-top: 8px; } .widget-vis__if, .widget-vis__is { margin-right: 8px; width: 10px; } .widget-vis__show-hide { margin-bottom: 0; } .components-button.widget-vis__add-new-rule { display: block; } .widget-vis__delete-rule { margin-top: 6px; text-align: right; } .widget-vis__match-all { margin-top: 12px; } .widget-vis__select { width: 100%; } .widget-vis__select .components-input-control__container { position: relative; top: 4px; } .widget-vis__select-multi-level select option:disabled { font-size: 18px; font-weight: 600; } .widget-vis__delete-rule .components-button.is-secondary { margin-top: 8px; } widget-visibility/widget-conditions/widget-conditions-rtl.min.css 0000644 00000007332 15174711637 0021414 0 ustar 00 .wp-customizer .expanded .widget-conditional .widget-conditional-inner{box-sizing:border-box;width:98%}.wp-customizer .expanded .widget-conditional .form{margin-bottom:20px;overflow:scroll}.widget-liquid-right .widget.expanded{overflow:visible}.widget-conditional-hide{display:none}.widget-conditional .widget-conditional-inner{background:#f6f7f7;border:1px solid #dcdcde;padding:12px 10px 0}.widget-conditional{margin-bottom:12px;margin-top:10px}.widget-conditional .conditions{margin-bottom:12px}.widget-conditional .condition,.widget-conditional .condition-top{clear:both}.widget-conditional .condition{padding-top:12px;position:relative}.widget-conditional .condition select{position:relative;width:120px;z-index:2}.widget-conditional .condition-top select{width:auto}.widget-conditional .condition-control{clear:both;margin-top:-20px;padding-top:4px}.widget-conditional .selection{margin-left:50px;margin-right:20px}.widget-conditional .conditions-rule-has-children{display:block}.widget-conditional .condition .actions{margin-top:-28px}.widget-conditional .condition-control a{position:absolute;text-decoration:none;text-indent:-9999px;top:17px;z-index:1}.wp-block-legacy-widget__edit-form .widget-conditional .condition-control a{top:20px}.widget-conditional .condition-control a:before{position:absolute;right:0;text-indent:0;top:0}.widget-conditional .condition-control .delete-condition,.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .condition-control .delete-condition{color:#f11;right:0}.widget-conditional .condition-control .add-condition{left:0}.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .widget-conditional-inner a.dashicons{font-family:dashicons}.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional-inner select{background-color:#fff;display:initial;width:auto}.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .widget-conditional-inner select:disabled{background-color:#f6f7f7;border-color:#dcdcde;color:#a7aaad}.editor-styles-wrapper .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .alignleft{margin-right:20px}.widget-conditional .condition:last-child .condition-conjunction,.widget-conditional .condition:last-child .condition-intersection,.widget-conditional.conjunction .condition-intersection,.widget-conditional.intersection .condition-conjunction{display:none}.wp-core-ui .button.display-options{margin-left:5px}.wp-core-ui .button.display-options:hover{text-decoration:none}.wp-customizer .widget-conditional select{height:auto;max-width:none;min-width:0}.wp-customizer .widget-conditional .condition-control a{top:15px}@media screen and (max-width:782px){.widget-conditional .condition-control a{top:20px}}.wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional-inner{font-size:13px}.widget-vis__rule,.widget-vis__rule .components-base-control,.widget-vis__wrapper .components-base-control{margin-bottom:12px}.widget-vis__rule{border:1px solid #dcdcde;display:flex;flex-direction:column;padding:12px}.widget-vis__rule-major,.widget-vis__rule-minor{align-items:center;display:flex;justify-content:stretch}.widget-vis__rule-minor{margin-top:8px}.widget-vis__if,.widget-vis__is{margin-left:8px;width:10px}.widget-vis__show-hide{margin-bottom:0}.components-button.widget-vis__add-new-rule{display:block}.widget-vis__delete-rule{margin-top:6px;text-align:left}.widget-vis__match-all{margin-top:12px}.widget-vis__select{width:100%}.widget-vis__select .components-input-control__container{position:relative;top:4px}.widget-vis__select-multi-level select option:disabled{font-size:18px;font-weight:600}.widget-vis__delete-rule .components-button.is-secondary{margin-top:8px} widget-visibility/widget-conditions/widget-conditions-rtl.css 0000644 00000010740 15174711637 0020627 0 ustar 00 .wp-customizer .expanded .widget-conditional .widget-conditional-inner { width: 98%; box-sizing: border-box; } .wp-customizer .expanded .widget-conditional .form { overflow: scroll; margin-bottom: 20px; } .widget-liquid-right .widget.expanded { overflow: visible; } .widget-conditional-hide { display: none; } .widget-conditional .widget-conditional-inner { background: #f6f7f7; border: 1px solid #dcdcde; padding: 12px 10px 0; } .widget-conditional { margin-bottom: 12px; margin-top: 10px; } .widget-conditional .conditions { margin-bottom: 12px; } .widget-conditional .condition, .widget-conditional .condition-top { clear: both; } .widget-conditional .condition { padding-top: 12px; position: relative; } .widget-conditional .condition select { width: 120px; position: relative; z-index: 2; } .widget-conditional .condition-top select { width: auto; } .widget-conditional .condition-control { padding-top: 4px; clear: both; margin-top: -20px; } .widget-conditional .selection { margin-left: 50px; margin-right: 20px; } .widget-conditional .conditions-rule-has-children { display: block; } .widget-conditional .condition .actions { margin-top: -28px; } .widget-conditional .condition-control a { text-decoration: none; position: absolute; top: 17px; text-indent: -9999px; z-index: 1; } .wp-block-legacy-widget__edit-form .widget-conditional .condition-control a { top: 20px; } .widget-conditional .condition-control a::before { position: absolute; text-indent: 0; top: 0; right: 0; } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .condition-control .delete-condition, .widget-conditional .condition-control .delete-condition { right: 0; color: #f11; } .widget-conditional .condition-control .add-condition { left: 0; } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .widget-conditional-inner a.dashicons { font-family: dashicons; } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional-inner select { display: initial; width: auto; background-color: #fff; } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .widget-conditional-inner select:disabled { color: #a7aaad; border-color: #dcdcde; background-color: #f6f7f7; } .editor-styles-wrapper .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional .alignleft { margin-right: 20px; } .widget-conditional .condition:last-child .condition-conjunction, .widget-conditional .condition:last-child .condition-intersection { display: none; } .widget-conditional.conjunction .condition-intersection { display: none; } .widget-conditional.intersection .condition-conjunction { display: none; } .wp-core-ui .button.display-options { margin-left: 5px; } .wp-core-ui .button.display-options:hover { text-decoration: none; } .wp-customizer .widget-conditional select { min-width: 0; max-width: none; height: auto; } .wp-customizer .widget-conditional .condition-control a { top: 15px; } @media screen and (max-width: 782px) { .widget-conditional .condition-control a { top: 20px; } } .wp-block-legacy-widget__edit-form .widget-inside.widget-inside .widget-conditional-inner { /* * fonts of labels are reset to 13px in gutenberg editor for legacy widgets * ensure a consistent look on non-labels */ font-size: 13px; } /* * Rules for gutenberg advanced panel */ .widget-vis__wrapper .components-base-control, .widget-vis__rule .components-base-control { margin-bottom: 12px; } .widget-vis__rule { padding: 12px; border: 1px solid #dcdcde; display: flex; flex-direction: column; margin-bottom: 12px; } .widget-vis__rule-major, .widget-vis__rule-minor { display: flex; justify-content: stretch; align-items: center; } .widget-vis__rule-minor { margin-top: 8px; } .widget-vis__if, .widget-vis__is { margin-left: 8px; width: 10px; } .widget-vis__show-hide { margin-bottom: 0; } .components-button.widget-vis__add-new-rule { display: block; } .widget-vis__delete-rule { margin-top: 6px; text-align: left; } .widget-vis__match-all { margin-top: 12px; } .widget-vis__select { width: 100%; } .widget-vis__select .components-input-control__container { position: relative; top: 4px; } .widget-vis__select-multi-level select option:disabled { font-size: 18px; font-weight: 600; } .widget-vis__delete-rule .components-button.is-secondary { margin-top: 8px; } widget-visibility/widget-conditions/rtl/widget-conditions-rtl.css 0000644 00000004665 15174711637 0021441 0 ustar 00 /* Do not modify this file directly. It is concatenated from individual module CSS files. */ .wp-customizer .expanded .widget-conditional .widget-conditional-inner { width: 98%; /* Safari/Chrome, other WebKit */ /* Firefox, other Gecko */ box-sizing: border-box; } .wp-customizer .expanded .widget-conditional .form{ overflow: scroll; margin-bottom: 20px; } .widget-liquid-right .widget.expanded { overflow: visible; } .widget-conditional-hide { display: none; } .widget-conditional .widget-conditional-inner { background: #F9F9F9; border: 1px solid #DFDFDF; padding: 12px 10px 0; } .widget-conditional { margin-bottom: 12px; } .widget-conditional .conditions{ margin-bottom: 12px; } .widget-conditional .condition, .widget-conditional .condition-top { clear:both; } .widget-conditional .condition { padding-top: 12px; position: relative; } .widget-conditional .condition select { width: 120px; position: relative; z-index: 2; } .widget-conditional .condition-top select { width: auto; } .widget-conditional .condition-control { padding-top: 4px; clear: both; margin-top: -20px; } .widget-conditional .selection { margin-left: 50px; margin-right: 20px; } .widget-conditional .conditions-rule-has-children { display: block; } .widget-conditional .condition .actions { margin-top: -28px; }.widget-conditional .condition .actions { margin-top: -28px; } .widget-conditional .condition-control a { text-decoration: none; position: absolute; top: 17px; text-indent: -9999px; z-index: 1; } .widget-conditional .condition-control a:before { position: absolute; text-indent: 0; right: 0; } .widget-conditional .condition-control .delete-condition { right: 0; color: #f11; } .widget-conditional .condition-control .add-condition { left: 0; } .widget-conditional .condition:last-child .condition-conjunction, .widget-conditional .condition:last-child .condition-intersection { display: none; } .widget-conditional.conjunction .condition-intersection { display: none; } .widget-conditional.intersection .condition-conjunction { display: none; } .wp-core-ui .button.display-options { margin-left: 5px; } .wp-core-ui .button.display-options:hover { text-decoration: none; } .wp-customizer .widget-conditional select { min-width: 0; max-width: none; height: auto; } .wp-customizer .widget-conditional .condition-control a { top: 15px; } @media screen and ( max-width: 782px ) { .widget-conditional .condition-control a { top: 20px; } } waf.php 0000644 00000000465 15174711637 0006053 0 ustar 00 <?php /** * Module Name: Firewall * Module Description: Filter malicious traffic in real time with Jetpack’s site firewall. * Sort Order: 5 * First Introduced: 10.9 * Requires Connection: Yes * Auto Activate: No * Module Tags: Firewall, WAF * Feature: Security * * @package automattic/jetpack */ monitor.php 0000644 00000007102 15174711637 0006760 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Module Name: Downtime Monitor * Module Description: Get instant alerts if your site goes down and know when it’s back online. * Sort Order: 28 * Recommendation Order: 10 * First Introduced: 2.6 * Requires Connection: Yes * Requires User Connection: Yes * Auto Activate: No * Module Tags: Recommended * Feature: Security * Additional Search Queries: monitor, uptime, downtime, monitoring, maintenance, maintenance mode, offline, site is down, site down, down, repair, error * * @package automattic/jetpack */ use Automattic\Jetpack\Connection\Manager as Connection_Manager; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Class Jetpack_Monitor * * @phan-constructor-used-for-side-effects */ class Jetpack_Monitor { /** * Name of the module. * * @var string Name of module. */ public $module = 'monitor'; /** * Constructor. */ public function __construct() { add_action( 'jetpack_modules_loaded', array( $this, 'jetpack_modules_loaded' ) ); add_action( 'jetpack_activate_module_monitor', array( $this, 'activate_module' ) ); } /** * Runs upon module activation. * * @return void */ public function activate_module() { if ( ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) { self::update_option_receive_jetpack_monitor_notification( true ); } } /** * Runs on the jetpack_modules_loaded hook to enable configuation. * * @return void */ public function jetpack_modules_loaded() { Jetpack::enable_module_configurable( $this->module ); } /** * Whether to receive the notifications. * * @param bool $value `true` to enable notifications, `false` to disable them. * * @return bool */ public function update_option_receive_jetpack_monitor_notification( $value ) { $xml = new Jetpack_IXR_Client( array( 'user_id' => get_current_user_id(), ) ); $xml->query( 'jetpack.monitor.setNotifications', (bool) $value ); if ( $xml->isError() ) { wp_die( sprintf( '%s: %s', esc_html( $xml->getErrorCode() ), esc_html( $xml->getErrorMessage() ) ) ); } // To be used only in Jetpack_Core_Json_Api_Endpoints::get_remote_value. update_option( 'monitor_receive_notifications', (bool) $value ); return true; } /** * Checks the status of notifications for current Jetpack site user. * * @since 2.8 * @since 4.1.0 New parameter $die_on_error. * * @param bool $die_on_error Whether to issue a wp_die when an error occurs or return a WP_Error object. * * @return boolean|WP_Error */ public static function user_receives_notifications( $die_on_error = true ) { $xml = new Jetpack_IXR_Client( array( 'user_id' => get_current_user_id(), ) ); $xml->query( 'jetpack.monitor.isUserInNotifications' ); if ( $xml->isError() ) { if ( $die_on_error ) { wp_die( sprintf( '%s: %s', esc_html( $xml->getErrorCode() ), esc_html( $xml->getErrorMessage() ) ), 400 ); } else { return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage(), array( 'status' => 400 ) ); } } return $xml->getResponse(); } /** * Returns date of the last downtime. * * @since 4.0.0 * @return string date in YYYY-MM-DD HH:mm:ss format */ public function monitor_get_last_downtime() { $xml = new Jetpack_IXR_Client(); $xml->query( 'jetpack.monitor.getLastDowntime' ); if ( $xml->isError() ) { return new WP_Error( 'monitor-downtime', $xml->getErrorMessage() ); } set_transient( 'monitor_last_downtime', $xml->getResponse(), 10 * MINUTE_IN_SECONDS ); return $xml->getResponse(); } } new Jetpack_Monitor(); theme-tools.php 0000644 00000004741 15174711637 0007537 0 ustar 00 <?php /** * Module: Theme Tools * * Load code specific to themes or theme tools * This file is special, and is not an actual `module` as such. * It is included by ./module-extras.php * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Conditionally require the Tonesque lib depending on theme support. */ function jetpack_load_theme_tools() { if ( current_theme_supports( 'tonesque' ) ) { require_once JETPACK__PLUGIN_DIR . '/_inc/lib/tonesque.php'; } } add_action( 'init', 'jetpack_load_theme_tools', 30 ); /** * Load theme compat file if it exists. */ function jetpack_load_theme_compat() { /** * Filter theme compat files. * * Themes can add their own compat files here if they like. For example: * * add_filter( 'jetpack_theme_compat_files', 'mytheme_jetpack_compat_file' ); * function mytheme_jetpack_compat_file( $files ) { * $files['mytheme'] = locate_template( 'jetpack-compat.php' ); * return $files; * } * * @module theme-tools * * @since 2.8.0 * * @param array Associative array of theme compat files to load. */ $compat_files = apply_filters( 'jetpack_theme_compat_files', array( 'twentyfourteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyfourteen.php', 'twentyfifteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentyfifteen.php', 'twentysixteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentysixteen.php', 'twentynineteen' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentynineteen.php', 'twentytwenty' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentytwenty.php', 'twentytwentyone' => JETPACK__PLUGIN_DIR . 'modules/theme-tools/compat/twentytwentyone.php', ) ); _jetpack_require_compat_file( get_stylesheet(), $compat_files ); if ( is_child_theme() ) { _jetpack_require_compat_file( get_template(), $compat_files ); } } add_action( 'after_setup_theme', 'jetpack_load_theme_compat', -1 ); /** * Requires a file once, if the passed key exists in the files array. * * @access private * @param string $key The key to check. * @param array $files Array of files to check in. * @return void|WP_Error */ function _jetpack_require_compat_file( $key, $files ) { if ( ! is_string( $key ) ) { return new WP_Error( 'key_not_string', 'The specified key is not actually a string.', compact( 'key' ) ); } if ( array_key_exists( $key, $files ) && is_readable( $files[ $key ] ) ) { require_once $files[ $key ]; } } contact-form.php 0000644 00000001436 15174711637 0007671 0 ustar 00 <?php /** * Contact form module. * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } use Automattic\Jetpack\Forms\Jetpack_Forms; /** * Module Name: Forms * Module Description: Add contact, registration, and feedback forms directly from the block editor. * Sort Order: 15 * Recommendation Order: 14 * First Introduced: 1.3 * Requires Connection: No * Auto Activate: Yes * Module Tags: Other * Feature: Writing * Additional Search Queries: contact, form, grunion, feedback, submission, contact form, email, feedback, contact form plugin, custom form, custom form plugin, form builder, forms, form maker, survey, contact by jetpack, contact us, forms free, creator */ /** * Load the newer Jetpack Forms package. */ Jetpack_Forms::load_contact_form(); module-info.php 0000644 00000066427 15174711637 0007526 0 ustar 00 <?php /** * "Learn More" information blocks for all modules live in this file. * * Each module must include 2 functions: * - The first one creates a button where users can find more information about the module. * It is hooked into `jetpack_learn_more_button_ . $module` * - The second creates a information block. * It is hooked into `jetpack_module_more_info_ . $module` * * @package automattic/jetpack */ use Automattic\Jetpack\Redirect; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * VaultPress (stub) support link. */ function vaultpress_jetpack_load_more_link() { echo 'https://help.vaultpress.com/get-to-know/'; } add_filter( 'jetpack_learn_more_button_vaultpress', 'vaultpress_jetpack_load_more_link' ); /** * VaultPress (stub) description. */ function vaultpress_jetpack_more_info() { esc_html_e( 'We keep a daily or real-time backup of your site so that when mistakes or accidents occur, restoring your site to any location takes a matter of minutes. Your site’s files are regularly scanned for unauthorized or suspicious modifications that could compromise your security and data. In many cases, we can fix them automatically (and will notify you). When we can’t, we provide you with expert support.', 'jetpack' ); } add_action( 'jetpack_module_more_info_vaultpress', 'vaultpress_jetpack_more_info' ); /** * Gravatar Hovercards support link. */ function grofiles_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-gravatar-hovercards' ) ); } add_filter( 'jetpack_learn_more_button_gravatar-hovercards', 'grofiles_load_more_link' ); /** * Gravatar Hovercards description. */ function grofiles_more_info() { esc_html_e( 'Enhance plain Gravatar images with information about a person (including a name, bio, pictures, and contact info) when they leave a comment on one of your posts.', 'jetpack' ); } add_action( 'jetpack_module_more_info_gravatar-hovercards', 'grofiles_more_info' ); /** * Shortcodes support link. */ function jetpack_shortcodes_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-shortcode-embeds' ) ); } add_filter( 'jetpack_learn_more_button_shortcodes', 'jetpack_shortcodes_load_more_link' ); /** * Shortcodes description. */ function jetpack_shortcodes_more_info() { esc_html_e( 'Easily and safely embed media from YouTube, Facebook, Flickr, Vimeo, Instagram, Google Maps, SlideShare, Vine, SoundCloud, and more. Just enter the appropriate shortcode directly into the editor and click “Publish.”', 'jetpack' ); } add_action( 'jetpack_module_more_info_shortcodes', 'jetpack_shortcodes_more_info' ); /** * Shortlinks support link. */ function wpme_load_more_link() { echo 'https://wp.me/p1moTy-DL'; } add_filter( 'jetpack_learn_more_button_shortlinks', 'wpme_load_more_link' ); /** * Shortlinks description */ function wpme_more_info() { esc_html_e( 'Grab short and simple links to your posts and pages using the compact wp.me domain name. Perfect for use on Twitter, Facebook, and in text messages where every character counts.', 'jetpack' ); } add_action( 'jetpack_module_more_info_shortlinks', 'wpme_more_info' ); /** * Jetpack Stats support link. */ function stats_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-wordpress-com-stats' ) ); } add_filter( 'jetpack_learn_more_button_stats', 'stats_load_more_link' ); /** * Jetpack Stats description. */ function stats_more_info() { esc_html_e( 'Simple and concise statistics about your traffic. Jetpack Stats collects data on page views, likes, comments, locations, and top posts. View them in your dashboard or on WordPress.com.', 'jetpack' ); } add_action( 'jetpack_module_more_info_stats', 'stats_more_info' ); /** * Publicize support link. */ function publicize_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-publicize' ) ); } add_filter( 'jetpack_learn_more_button_publicize', 'publicize_load_more_link' ); /** * Publicize description. */ function publicize_more_info() { esc_html_e( 'Automatically share and promote newly published posts to Facebook, Tumblr, and LinkedIn. You can add connections for yourself or for all users on your site.', 'jetpack' ); } add_action( 'jetpack_module_more_info_publicize', 'publicize_more_info' ); /** * Notifications */ function notes_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-notifications' ) ); } add_filter( 'jetpack_learn_more_button_notes', 'notes_load_more_link' ); /** * Notifications description. */ function notes_more_info() { esc_html_e( 'You will receive instant notifications in your dashboard or your mobile device when somebody comments on any of your sites. Reply directly wherever you are to keep the conversation going.', 'jetpack' ); } add_filter( 'jetpack_module_more_info_notes', 'notes_more_info' ); /** * LaTeX support link. */ function latex_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-beautiful-math-with-latex' ) ); } add_filter( 'jetpack_learn_more_button_latex', 'latex_load_more_link' ); /** * LaTeX description. */ function latex_more_info() { esc_html_e( 'LaTeX is a powerful markup language for writing complex mathematical equations and formulas. Jetpack combines the power of LaTeX and the simplicity of WordPress to give you the ultimate in math blogging platforms. Use $latex your latex code here$ or [latex]your latex code here[/latex] to include in your posts and comments. Enjoy all sorts of options and embrace your inner nerd.', 'jetpack' ); } add_action( 'jetpack_module_more_info_latex', 'latex_more_info' ); /** * Sharing support link. */ function sharedaddy_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-sharing' ) ); } add_filter( 'jetpack_learn_more_button_sharedaddy', 'sharedaddy_load_more_link' ); /** * Sharing description. */ function sharedaddy_more_info() { esc_html_e( 'Visitors can share your posts with Twitter, Facebook, Reddit, Digg, LinkedIn, print, and email. You can configure services to appear as icons, text, or both and some services like Twitter have additional options.', 'jetpack' ); } add_action( 'jetpack_module_more_info_sharedaddy', 'sharedaddy_more_info' ); /** * Extra Sidebar Widgets support link. */ function jetpack_widgets_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-extra-sidebar-widgets' ) ); } add_filter( 'jetpack_learn_more_button_widgets', 'jetpack_widgets_load_more_link' ); /** * Extra Sidebar Widgets description. */ function jetpack_widgets_more_info() { esc_html_e( 'Add as many custom widgets as you like by dragging and dropping and customize each to fit your needs, including, Twitter streams, Facebook like boxes, custom images, Gravatars, tiled galleries, recent posts, or social icons.', 'jetpack' ); } add_action( 'jetpack_module_more_info_widgets', 'jetpack_widgets_more_info' ); /** * Subscriptions support link. */ function jetpack_subscriptions_load_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-subscriptions' ) ); } add_action( 'jetpack_learn_more_button_subscriptions', 'jetpack_subscriptions_load_more_link' ); /** * Subscriptions description. */ function jetpack_subscriptions_more_info() { esc_html_e( 'A widget in your sidebar allows visitors to subscribe to your site so that they receive an email each time you publish new content. Your visitors can also subscribe to a post\'s comments to keep up with the conversation.', 'jetpack' ); } add_action( 'jetpack_module_more_info_subscriptions', 'jetpack_subscriptions_more_info' ); /** * Protect support link. */ function jetpack_protect_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-protect' ) ); } add_action( 'jetpack_learn_more_button_protect', 'jetpack_protect_more_link' ); /** * Protect description. */ function jetpack_protect_more_info() { esc_html_e( 'Most sites will come under attack from automated bots that attempt to log in for malicious purposes. We protect you automatically from unauthorized access by using data from millions of sites.', 'jetpack' ); } add_action( 'jetpack_module_more_info_protect', 'jetpack_protect_more_info' ); /** * JSON API support link. */ function jetpack_json_api_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-json-api' ) ); } add_action( 'jetpack_learn_more_button_json-api', 'jetpack_json_api_more_link' ); /** * JSON API description. */ function jetpack_json_api_more_info() { esc_html_e( 'Authorize applications and services to securely connect to your site. Developers can use WordPress.com\'s OAuth2 authentication system and WordPress.com REST API to manage and access your site\'s content.', 'jetpack' ); } add_action( 'jetpack_module_more_info_json-api', 'jetpack_json_api_more_info' ); /** * Contact Form support link. */ function jetpack_contact_form_learn_more_button() { echo esc_url( Redirect::get_url( 'jetpack-support-contact-form' ) ); } add_action( 'jetpack_learn_more_button_contact-form', 'jetpack_contact_form_learn_more_button' ); /** * Contact Form description. */ function jetpack_contact_form_more_info() { esc_html_e( 'Create simple contact forms without any coding. You can have multiple forms and when a user submits it, their feedback will be emailed directly to you. If Akismet is active, submissions will be automatically filtered for spam.', 'jetpack' ); } add_action( 'jetpack_module_more_info_contact-form', 'jetpack_contact_form_more_info' ); /** * Comments support link. */ function jetpack_comments_learn_more_button() { echo esc_url( Redirect::get_url( 'jetpack-support-comments' ) ); } add_action( 'jetpack_learn_more_button_comments', 'jetpack_comments_learn_more_button' ); /** * Comments description. */ function jetpack_comments_more_info() { esc_html_e( 'Allow visitors to use their WordPress.com or Facebook accounts when commenting on your site. Jetpack will match your site\'s color scheme automatically (but you can adjust that).', 'jetpack' ); } add_action( 'jetpack_module_more_info_comments', 'jetpack_comments_more_info' ); /** * Carousel support link. */ function jetpack_carousel_learn_more_button() { echo esc_url( Redirect::get_url( 'jetpack-support-carousel' ) ); } add_action( 'jetpack_learn_more_button_carousel', 'jetpack_carousel_learn_more_button' ); /** * Carousel description. */ function jetpack_carousel_more_info() { esc_html_e( 'With Carousel active, any standard WordPress galleries or single images you have embedded in posts or pages will launch a full-screen photo browsing experience with comments and EXIF metadata.', 'jetpack' ); } add_action( 'jetpack_module_more_info_carousel', 'jetpack_carousel_more_info' ); /** * Infinite Scroll support link. */ function jetpack_infinite_scroll_more_button() { echo esc_url( Redirect::get_url( 'jetpack-support-infinite-scroll' ) ); } add_action( 'jetpack_learn_more_button_infinite-scroll', 'jetpack_infinite_scroll_more_button' ); /** * Infinite Scroll description. */ function jetpack_infinite_scroll_more_info() { esc_html_e( 'Infinite scrolling pulls the next set of posts automatically into view when the reader approaches the bottom of the page. This helps you reader see more of your content.', 'jetpack' ); } add_action( 'jetpack_module_more_info_infinite-scroll', 'jetpack_infinite_scroll_more_info' ); /** * Post by Email support link. */ function jetpack_post_by_email_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-post-by-email' ) ); } add_action( 'jetpack_learn_more_button_post-by-email', 'jetpack_post_by_email_more_link' ); /** * Post by Email description. */ function jetpack_post_by_email_more_info() { esc_html_e( 'Publish posts on your site by writing and sending an email from any email client instead of using the post editor.', 'jetpack' ); } add_action( 'jetpack_module_more_info_post-by-email', 'jetpack_post_by_email_more_info' ); /** * Photon support link. */ function jetpack_photon_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-photon' ) ); } add_action( 'jetpack_learn_more_button_photon', 'jetpack_photon_more_link' ); /** * Photon description. */ function jetpack_photon_more_info() { esc_html_e( 'Jetpack will optimize your images and serve them from the server location nearest to your visitors. Using our global content delivery network will boost the loading speed of your site.', 'jetpack' ); } add_action( 'jetpack_module_more_info_photon', 'jetpack_photon_more_info' ); /** * Tiled Galleries support link. */ function jetpack_tiled_gallery_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-tiled-galleries' ) ); } add_action( 'jetpack_learn_more_button_tiled-gallery', 'jetpack_tiled_gallery_more_link' ); /** * Tiled Galleries description. */ function jetpack_tiled_gallery_more_info() { esc_html_e( 'When adding an image gallery, you will have the option to create elegant magazine-style mosaic layouts for your photos, including mosaic (default), square, and circular layouts.', 'jetpack' ); } add_action( 'jetpack_module_more_info_tiled-gallery', 'jetpack_tiled_gallery_more_info' ); /** * Likes support link. */ function jetpack_likes_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-likes' ) ); } add_action( 'jetpack_learn_more_button_likes', 'jetpack_likes_more_link' ); /** * Likes description. */ function jetpack_likes_more_info() { esc_html_e( 'Allow your readers to show their appreciation for your posts and other content. Likes show up below each post and your readers will also be able to review their liked posts from WordPress.com.', 'jetpack' ); } add_action( 'jetpack_module_more_info_likes', 'jetpack_likes_more_info' ); /** * Widget Visibility support link. */ function jetpack_widget_visibility_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-widget-visibility' ) ); } add_action( 'jetpack_learn_more_button_widget-visibility', 'jetpack_widget_visibility_more_link' ); /** * Widget Visibility description. */ function jetpack_widget_visibility_more_info() { esc_html_e( 'Choose from a set of visibility options for sidebar widgets such as showing them only certain categories, only on error pages, or only search results pages. You can also do the reverse and choose to hide them on certain pages.', 'jetpack' ); } add_action( 'jetpack_module_more_info_widget-visibility', 'jetpack_widget_visibility_more_info' ); /** * VideoPress support link. */ function jetpack_videopress_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-videopress' ) ); } add_action( 'jetpack_learn_more_button_videopress', 'jetpack_videopress_more_link' ); /** * VideoPress description. */ function jetpack_videopress_more_info() { esc_html_e( 'The easiest way to upload ad-free and unbranded videos to your site. You get stats on video playback and shares and the player is lightweight and responsive.', 'jetpack' ); } add_action( 'jetpack_module_more_info_videopress', 'jetpack_videopress_more_info' ); /** * SSO support link. */ function jetpack_sso_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-sso' ) ); } add_action( 'jetpack_learn_more_button_sso', 'jetpack_sso_more_link' ); /** * SSO description. */ function jetpack_sso_more_info() { esc_html_e( 'Your users will be able to log in to your site with their WordPress.com account. This includes two-factor authentication making it the safest login mechanism for your site.', 'jetpack' ); } add_action( 'jetpack_module_more_info_sso', 'jetpack_sso_more_info' ); /** * Monitor support link. */ function jetpack_monitor_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-monitor' ) ); } add_action( 'jetpack_learn_more_button_monitor', 'jetpack_monitor_more_link' ); /** * Monitor description. */ function jetpack_monitor_more_info() { esc_html_e( 'Jetpack checks your site every five minutes and if any downtime is detected you will receive an email notification alerting you to the issue, so you can act quickly and get your site back online.', 'jetpack' ); } add_action( 'jetpack_module_more_info_monitor', 'jetpack_monitor_more_info' ); /** * Related Posts support link. */ function jetpack_related_posts_more_button() { echo esc_url( Redirect::get_url( 'jetpack-support-related-posts' ) ); } add_action( 'jetpack_learn_more_button_related-posts', 'jetpack_related_posts_more_button' ); /** * Related Posts description. */ function jetpack_related_posts_more_info() { esc_html_e( 'Show visitors related content from your site at the bottom of your posts. This encourages them to browse more content, explore your site, and transform them into regular readers.', 'jetpack' ); } add_action( 'jetpack_module_more_info_related-posts', 'jetpack_related_posts_more_info' ); /** * Markdown support link. */ function jetpack_markdown_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-markdown' ) ); } add_action( 'jetpack_learn_more_button_markdown', 'jetpack_markdown_more_link' ); /** * Markdown description. */ function jetpack_markdown_more_info() { esc_html_e( 'Compose posts and comments with links, lists, and other styles using regular characters and punctuation marks. A quick and easy way to format text without needing any HTML or coding.', 'jetpack' ); } add_action( 'jetpack_module_more_info_markdown', 'jetpack_markdown_more_info' ); /** * Site Verification Tools support link. */ function jetpack_verification_tools_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-site-verification-tools' ) ); } add_action( 'jetpack_learn_more_button_verification-tools', 'jetpack_verification_tools_more_link' ); /** * Site Verification Tools description. */ function jetpack_verification_tools_more_info() { esc_html_e( 'Verify your site ownership with services like Google, Bing, Pinterest, Yandex, and Facebook. This gives you access to advanced features on these services and get verification badges.', 'jetpack' ); } add_action( 'jetpack_module_more_info_verification-tools', 'jetpack_verification_tools_more_info' ); /** * SEO Tools support link. */ function jetpack_seo_tools_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-seo-tools' ) ); } add_action( 'jetpack_learn_more_button_seo-tools', 'jetpack_seo_tools_more_link' ); /** * SEO Tools description. */ function jetpack_seo_tools_more_info() { esc_html_e( 'Better results on search engines and social media.', 'jetpack' ); } add_action( 'jetpack_module_more_info_seo-tools', 'jetpack_seo_tools_more_info' ); /** * Custom Content Types support link. */ function jetpack_custom_content_types_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-custom-content-types' ) ); } add_action( 'jetpack_learn_more_button_custom-content-types', 'jetpack_custom_content_types_more_link' ); /** * Custom Content Types description. */ function jetpack_custom_content_types_more_info() { esc_html_e( 'Add and organize content that doesn’t necessarily fit into a post or static page such as portfolios or testimonials. Custom content can be visible at specific URLs, or you may add them with shortcodes.', 'jetpack' ); } add_action( 'jetpack_module_more_info_custom-content-types', 'jetpack_custom_content_types_more_info' ); /** * Manage support link. */ function jetpack_manage_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-site-management' ) ); } add_action( 'jetpack_learn_more_button_manage', 'jetpack_manage_more_link' ); /** * Manage description. */ function jetpack_custom_jetpack_manage() { esc_html_e( 'Manage and update this and other WordPress sites from one simple dashboard on WordPress.com. You can update plugins, set them to automatically update, and (de)activate them on a per-site basis or in bulk from wordpress.com/plugins. You can also use the brand new and mobile-friendly post editor on WordPress.com as well as view and activate installed themes and create or edit site menus.', 'jetpack' ); } add_action( 'jetpack_module_more_info_manage', 'jetpack_custom_jetpack_manage' ); /** * Post list info. */ function jetpack_post_list_link() { echo esc_url( Redirect::get_url( 'jetpack-support-post-list' ) ); } add_action( 'jetpack_learn_more_button_post-list', 'jetpack_post_list_link' ); /** * Post List description. */ function jetpack_post_list_info() { esc_html_e( 'Display extra information alongside each post in your dashboard’s Posts screen.', 'jetpack' ); } add_action( 'jetpack_module_more_info_post-list', 'jetpack_post_list_info' ); /** * Sitemaps support link. */ function jetpack_sitemaps_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-sitemaps' ) ); } add_action( 'jetpack_learn_more_button_sitemaps', 'jetpack_sitemaps_more_link' ); /** * Sitemaps description. */ function jetpack_xml_sitemap_more_info() { esc_html_e( 'Automatically create two sitemap files that list the URLs of posts and pages in your site. This makes it easier for search engines (like Google) to include your site in relevant search results.', 'jetpack' ); } add_action( 'jetpack_module_more_info_sitemaps', 'jetpack_xml_sitemap_more_info' ); /** * WordAds support link. */ function jetpack_wordads_more_link() { echo 'https://wordads.co/'; } add_action( 'jetpack_learn_more_button_wordads', 'jetpack_wordads_more_link' ); /** * WordAds description. */ function jetpack_wordads_more_info() { esc_html_e( 'By default ads are shown at the end of every page, post, or the first article on your front page. You can also add them to the top of your site and to any widget area to increase your earnings!', 'jetpack' ); } add_action( 'jetpack_module_more_info_wordads', 'jetpack_wordads_more_info' ); /** * Google Analytics support link. */ function jetpack_google_analytics_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-google-analytics' ) ); } add_action( 'jetpack_learn_more_button_google-analytics', 'jetpack_google_analytics_more_link' ); /** * Google Analytics description. */ function jetpack_google_analytics_more_info() { esc_html_e( 'Track website statistics with Google Analytics for a deeper understanding of your website visitors and customers.', 'jetpack' ); } add_action( 'jetpack_module_more_info_google-analytics', 'jetpack_google_analytics_more_info' ); /** * WooCommerce Analytics support link. */ function jetpack_woocommerce_analytics_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-woocommerce-analytics' ) ); } add_action( 'jetpack_learn_more_button_woocommerce-analytics', 'jetpack_woocommerce_analytics_more_link' ); /** * WooCommerce Analytics description. */ function jetpack_woocommerce_analytics_more_info() { esc_html_e( 'Enhanced analytics for WooCommerce and Jetpack users.', 'jetpack' ); } add_action( 'jetpack_module_more_info_woocommerce-analytics', 'jetpack_woocommerce_analytics_more_info' ); /** * Search support link. */ function jetpack_search_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-search' ) ); } add_action( 'jetpack_learn_more_button_search', 'jetpack_search_more_link' ); /** * Search description. */ function jetpack_search_more_info() { esc_html_e( 'Help visitors quickly find answers with highly relevant instant search results and powerful filtering.', 'jetpack' ); } add_action( 'jetpack_module_more_info_search', 'jetpack_search_more_info' ); /** * Comment Likes support link. */ function jetpack_comment_likes_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-comment-likes' ) ); } add_action( 'jetpack_learn_more_button_comment-likes', 'jetpack_comment_likes_more_link' ); /** * Comment Likes description. */ function jetpack_comment_likes_more_info() { esc_html_e( 'Increase visitor engagement by adding a Like button to comments.', 'jetpack' ); } add_action( 'jetpack_module_more_info_comment-likes', 'jetpack_comment_likes_more_info' ); /** * Asset CDN support link. */ function jetpack_assetcdn_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-asset-cdn' ) ); } add_action( 'jetpack_learn_more_button_photon-cdn', 'jetpack_assetcdn_more_link' ); /** * Asset CDN description. */ function jetpack_assetcdn_more_info() { esc_html_e( 'Our asset CDN is a site acceleration service. That means that we host static assets like JavaScript and CSS shipped with WordPress Core and Jetpack from our servers, alleviating the load on your server.', 'jetpack' ); } add_action( 'jetpack_module_more_info_photon-cdn', 'jetpack_assetcdn_more_info' ); /** * Copy Post support link. */ function jetpack_copy_post_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-copy-post' ) ); } add_action( 'jetpack_learn_more_button_copy-post', 'jetpack_copy_post_more_link' ); /** * Copy Post description. */ function jetpack_more_info_copy_post() { esc_html_e( 'Create a new post based on an existing post.', 'jetpack' ); } add_action( 'jetpack_module_more_info_copy-post', 'jetpack_more_info_copy_post' ); /** * Google Fonts support link. */ function jetpack_google_fonts_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-google-fonts' ) ); } add_action( 'jetpack_learn_more_button_google-fonts', 'jetpack_google_fonts_more_link' ); /** * Google Fonts description. */ function jetpack_more_info_google_fonts() { esc_html_e( 'A selection of Google fonts for block enabled themes. This feature is still being developed.', 'jetpack' ); } add_action( 'jetpack_module_more_info_google-fonts', 'jetpack_more_info_google_fonts' ); /** * Account Protection support link. */ function jetpack_account_protection_more_link() { echo esc_url( Redirect::get_url( 'jetpack-account-protection' ) ); } add_action( 'jetpack_learn_more_button_account-protection', 'jetpack_account_protection_more_link' ); /** * Account Protection description. */ function jetpack_more_info_account_protection() { esc_html_e( 'Enabling this setting enhances account security by detecting compromised passwords and enforcing additional verification when needed.', 'jetpack' ); } add_action( 'jetpack_module_more_info_account-protection', 'jetpack_more_info_account_protection' ); /** * WAF support link. */ function jetpack_waf_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-waf' ) ); } add_action( 'jetpack_learn_more_button_waf', 'jetpack_waf_more_link' ); /** * WAF description. */ function jetpack_more_info_waf() { esc_html_e( 'The Jetpack Firewall is a web application firewall designed to protect your WordPress site from malicious requests.', 'jetpack' ); } add_action( 'jetpack_module_more_info_waf', 'jetpack_more_info_waf' ); /** * Blaze support link. */ function jetpack_blaze_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-blaze' ) ); } add_action( 'jetpack_learn_more_button_blaze', 'jetpack_blaze_more_link' ); /** * Blaze description. */ function jetpack_more_info_blaze() { esc_html_e( 'Grow your audience by promoting your content across Tumblr and WordPress.com.', 'jetpack' ); } add_action( 'jetpack_module_more_info_blaze', 'jetpack_more_info_blaze' ); /** * WordPress.com Reader support link. */ function jetpack_wpcom_reader_more_link() { echo esc_url( Redirect::get_url( 'jetpack-support-reader' ) ); } add_action( 'jetpack_learn_more_button_wpcom-reader', 'jetpack_wpcom_reader_more_link' ); /** * WordPress.com Reader description. */ function jetpack_more_info_wpcom_reader() { esc_html_e( 'Quickly access the WordPress.com Reader from your site’s admin bar.', 'jetpack' ); } add_action( 'jetpack_module_more_info_wpcom-reader', 'jetpack_more_info_wpcom_reader' ); notes.php 0000644 00000017603 15174711637 0006430 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Module Name: Notifications * Module Description: Receive real‑time notifications about site activity across your devices. * Sort Order: 13 * First Introduced: 1.9 * Requires Connection: Yes * Requires User Connection: Yes * Auto Activate: Yes * Module Tags: Other * Feature: General * Additional Search Queries: notification, notifications, toolbar, adminbar, push, comments * * @package automattic/jetpack */ use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Status\Host; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } if ( ! defined( 'JETPACK_NOTES__CACHE_BUSTER' ) ) { define( 'JETPACK_NOTES__CACHE_BUSTER', JETPACK__VERSION . '-' . gmdate( 'oW' ) . '-lite' ); } /** * Notifications class. */ class Jetpack_Notifications { /** * Jetpack object. * * @var bool|Jetpack Jetpack object. */ public $jetpack = false; /** * Singleton * * @static */ public static function init() { static $instance = array(); if ( ! $instance ) { $instance[0] = new Jetpack_Notifications(); } return $instance[0]; } /** * Constructor. */ private function __construct() { $this->jetpack = Jetpack::init(); add_action( 'init', array( $this, 'action_init' ) ); } /** * Adds s0.wp.com to a file path. * * @param string $file File path. * * @return string */ public function wpcom_static_url( $file ) { return 'https://s0.wp.com' . $file; } /** * Init the notifications admin bar. * * @return void */ public function action_init() { if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return; } if ( ! has_filter( 'show_admin_bar', '__return_true' ) && ! is_user_logged_in() ) { return; } // Do not show notifications in the Site Editor, which is always in fullscreen mode. global $pagenow; // Pre 13.7 pages that still need to be supported if < 13.7 is // still installed. $allowed_old_pages = array( 'admin.php', 'themes.php' ); $is_old_site_editor_page = in_array( $pagenow, $allowed_old_pages, true ) && isset( $_GET['page'] ) && 'gutenberg-edit-site' === $_GET['page']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended // For Gutenberg > 13.7, the core `site-editor.php` route is used instead $is_site_editor_page = 'site-editor.php' === $pagenow; if ( $is_site_editor_page || $is_old_site_editor_page ) { return; } add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 120 ); add_action( 'wp_head', array( $this, 'styles_and_scripts' ), 120 ); add_action( 'admin_head', array( $this, 'styles_and_scripts' ) ); } /** * Enqueues and registers styles/scripts for notifications. * * @return void */ public function styles_and_scripts() { if ( self::is_block_editor() ) { return; } $is_rtl = is_rtl(); if ( ( new Host() )->is_woa_site() ) { /** * Can be used to force Notifications to display in RTL style. * * @module notes * * @since 4.8.0 * * @param bool true Should notifications be displayed in RTL style. Defaults to false. */ $is_rtl = apply_filters( 'a8c_wpcom_masterbar_enqueue_rtl_notification_styles', false ); } if ( ! $is_rtl ) { wp_enqueue_style( 'wpcom-notes-admin-bar', $this->wpcom_static_url( '/wp-content/mu-plugins/notes/admin-bar-v2.css' ), array( 'admin-bar' ), JETPACK_NOTES__CACHE_BUSTER ); } else { wp_enqueue_style( 'wpcom-notes-admin-bar', $this->wpcom_static_url( '/wp-content/mu-plugins/notes/rtl/admin-bar-v2-rtl.css' ), array( 'admin-bar' ), JETPACK_NOTES__CACHE_BUSTER ); } wp_enqueue_style( 'noticons', $this->wpcom_static_url( '/i/noticons/noticons.css' ), array( 'wpcom-notes-admin-bar' ), JETPACK_NOTES__CACHE_BUSTER ); $this->print_js(); $script_handles = array(); wp_register_script( 'wpcom-notes-common', $this->wpcom_static_url( '/wp-content/mu-plugins/notes/notes-common-lite.min.js' ), array(), JETPACK_NOTES__CACHE_BUSTER, true ); $script_handles[] = 'wpcom-notes-common'; wp_enqueue_script( 'wpcom-notes-admin-bar', $this->wpcom_static_url( '/wp-content/mu-plugins/notes/admin-bar-v2.js' ), array( 'wpcom-notes-common' ), JETPACK_NOTES__CACHE_BUSTER, true ); $script_handles[] = 'wpcom-notes-admin-bar'; $wp_notes_args = 'var wpNotesArgs = ' . wp_json_encode( array( 'cacheBuster' => JETPACK_NOTES__CACHE_BUSTER ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';'; wp_add_inline_script( 'wpcom-notes-admin-bar', $wp_notes_args, 'before' ); if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { add_filter( 'script_loader_tag', function ( $tag, $handle ) use ( $script_handles ) { if ( in_array( $handle, $script_handles, true ) ) { $tag = preg_replace( '/(?<=<script)(?=\s|>)/i', ' data-ampdevmode', $tag ); } return $tag; }, 10, 2 ); } } /** * Adds notifications bubble to the admin bar. * * @return void */ public function admin_bar_menu() { global $wp_admin_bar; if ( ! is_object( $wp_admin_bar ) ) { return; } if ( self::is_block_editor() ) { return; } $user_locale = get_user_locale(); if ( ! class_exists( 'GP_Locales' ) ) { if ( defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) && file_exists( JETPACK__GLOTPRESS_LOCALES_PATH ) ) { require JETPACK__GLOTPRESS_LOCALES_PATH; } } if ( class_exists( 'GP_Locales' ) ) { $jetpack_locale_object = GP_Locales::by_field( 'slug', $user_locale ); if ( $jetpack_locale_object instanceof GP_Locale ) { $user_locale = $jetpack_locale_object->slug; } } $third_party_cookie_check_iframe = '<span style="display:none;"><iframe class="jetpack-notes-cookie-check" src="https://widgets.wp.com/3rd-party-cookie-check/index.html"></iframe></span>'; $title = self::get_notes_markup(); // The default fallback is `en_US`. Remove underscore if present, noting that lang codes can be more than three chars. $user_locale = strtolower( explode( '_', $user_locale, 2 )[0] ); $wp_admin_bar->add_menu( array( 'id' => 'notes', 'title' => $title, 'meta' => array( 'html' => '<div id="wpnt-notes-panel2" class="intrinsic-ignore" style="display:none" lang="' . esc_attr( $user_locale ) . '" dir="' . ( is_rtl() ? 'rtl' : 'ltr' ) . '"><div class="wpnt-notes-panel-header"><span class="wpnt-notes-header">' . __( 'Notifications', 'jetpack' ) . '</span><span class="wpnt-notes-panel-link"></span></div></div>' . $third_party_cookie_check_iframe, 'class' => 'menupop', ), 'parent' => 'top-secondary', 'href' => 'https://wordpress.com/reader/notifications', ) ); } /** * Returns the HTML markup for used by notification in top bar * * @return string */ private static function get_notes_markup() { return '<span id="wpnt-notes-unread-count" class="wpnt-loading wpn-read"></span> <span class="noticon noticon-bell ab-icon"></span> <span class="screen-reader-text">' . esc_html__( 'Notifications', 'jetpack' ) . '</span>'; } /** * Echos the Notes JS. * * @return void */ public function print_js() { $link_accounts_url = is_user_logged_in() && ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ? Jetpack::admin_url() : false; $script_contents = <<<'JS' var wpNotesIsJetpackClient = true; var wpNotesIsJetpackClientV2 = true; JS; if ( $link_accounts_url ) { $script_contents .= "\nvar wpNotesLinkAccountsURL = " . wp_json_encode( $link_accounts_url, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';'; } wp_print_inline_script_tag( $script_contents, array( 'data-ampdevmode' => true, ) ); } /** * Checks to see if we're in the block editor. */ public static function is_block_editor() { if ( function_exists( 'get_current_screen' ) ) { $current_screen = get_current_screen(); if ( ! empty( $current_screen ) && $current_screen->is_block_editor() ) { return true; } } return false; } } Jetpack_Notifications::init(); subscriptions.php 0000644 00000104165 15174711637 0010207 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName) /** * Module Name: Newsletter * Module Description: Grow your subscriber list and deliver your content directly to their email inbox. * Sort Order: 9 * Recommendation Order: 8 * First Introduced: 1.2 * Requires Connection: Yes * Requires User Connection: Yes * Auto Activate: No * Module Tags: Social * Feature: Engagement * Additional Search Queries: subscriptions, subscription, email, follow, followers, subscribers, signup, newsletter, creator */ // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files. use Automattic\Jetpack\Admin_UI\Admin_Menu; use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Connection\XMLRPC_Async_Call; use Automattic\Jetpack\Newsletter\Settings as Newsletter_Settings; use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host; use Automattic\Jetpack\Subscribers_Dashboard\Dashboard as Subscribers_Dashboard; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } add_action( 'jetpack_modules_loaded', 'jetpack_subscriptions_load' ); // Loads the User Content Link Redirection feature. require_once __DIR__ . '/subscriptions/jetpack-user-content-link-redirection.php'; /** * Loads the Subscriptions module. */ function jetpack_subscriptions_load() { Jetpack::enable_module_configurable( __FILE__ ); } /** * Cherry picks keys from `$_SERVER` array. * * @since 6.0.0 * * @return array An array of server data. */ function jetpack_subscriptions_cherry_pick_server_data() { $data = array(); foreach ( $_SERVER as $key => $value ) { if ( ! is_string( $value ) || str_starts_with( $key, 'HTTP_COOKIE' ) ) { continue; } if ( str_starts_with( $key, 'HTTP_' ) || in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ), true ) ) { $data[ $key ] = $value; } } return $data; } /** * Main class file for the Subscriptions module. * * @phan-constructor-used-for-side-effects */ class Jetpack_Subscriptions { /** * Whether Jetpack has been instantiated or not. * * @var bool */ public $jetpack = false; /** * Hash of the siteurl option. * * @var string */ public static $hash; /** * Singleton * * @static */ public static function init() { static $instance = false; if ( ! $instance ) { $instance = new Jetpack_Subscriptions(); } return $instance; } /** * Jetpack_Subscriptions constructor. */ public function __construct() { $this->jetpack = Jetpack::init(); // Don't use COOKIEHASH as it could be shared across installs && is non-unique in multisite. // @see: https://twitter.com/nacin/status/378246957451333632 . self::$hash = md5( get_option( 'siteurl' ) ); add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) ); // @todo remove sync from subscriptions and move elsewhere... // Add Configuration Page. add_action( 'admin_init', array( $this, 'configure' ) ); // Catch subscription widget submits. if ( isset( $_REQUEST['jetpack_subscriptions_widget'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce checked in widget_submit() for logged in users. add_action( 'template_redirect', array( $this, 'widget_submit' ) ); } // Set up the comment subscription checkboxes. add_filter( 'comment_form_submit_field', array( $this, 'comment_subscribe_init' ), 10, 2 ); // Catch comment posts and check for subscriptions. add_action( 'comment_post', array( $this, 'comment_subscribe_submit' ), 50, 2 ); // Adds post meta checkbox in the post submit metabox. add_action( 'post_submitbox_misc_actions', array( $this, 'subscription_post_page_metabox' ) ); add_action( 'transition_post_status', array( $this, 'maybe_send_subscription_email' ), 10, 3 ); add_filter( 'jetpack_published_post_flags', array( $this, 'set_post_flags' ), 10, 2 ); add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 18, 1 ); // Set "social_notifications_subscribe" option during the first-time activation. add_action( 'jetpack_activate_module_subscriptions', array( $this, 'set_social_notifications_subscribe' ) ); add_action( 'jetpack_activate_module_subscriptions', array( $this, 'set_featured_image_in_email_default' ) ); // Hide subscription messaging in Publish panel for posts that were published in the past add_action( 'init', array( $this, 'register_post_meta' ), 20 ); add_action( 'transition_post_status', array( $this, 'maybe_set_first_published_status' ), 10, 3 ); // Add Subscribers menu to Jetpack navigation. add_action( 'jetpack_admin_menu', array( $this, 'add_subscribers_menu' ) ); // Customize the configuration URL to lead to the Subscriptions settings. add_filter( 'jetpack_module_configuration_url_subscriptions', function () { return Jetpack::admin_url( array( 'page' => 'jetpack#/newsletter' ) ); } ); // Track categories created through the category editor page add_action( 'wp_ajax_add-tag', array( $this, 'track_newsletter_category_creation' ), 1 ); $subscribers_dashboard = new Subscribers_Dashboard(); $subscribers_dashboard::init(); $newsletter_settings = new Newsletter_Settings(); $newsletter_settings::init(); } /** * Jetpack_Subscriptions::xmlrpc_methods() * * Register subscriptions methods with the Jetpack XML-RPC server. * * @param array $methods Methods being registered. */ public function xmlrpc_methods( $methods ) { return array_merge( $methods, array( 'jetpack.subscriptions.subscribe' => array( $this, 'subscribe' ), ) ); } /** * Disable Subscribe on Single Post * Register post meta */ public function subscription_post_page_metabox() { if ( /** * Filter whether or not to show the per-post subscription option. * * @module subscriptions * * @since 3.7.0 * * @param bool true = show checkbox option on all new posts | false = hide the option. */ ! apply_filters( 'jetpack_allow_per_post_subscriptions', false ) ) { return; } if ( has_filter( 'jetpack_subscriptions_exclude_these_categories' ) || has_filter( 'jetpack_subscriptions_include_only_these_categories' ) ) { return; } global $post; $disable_subscribe_value = get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true ); // only show checkbox if post hasn't been published and is a 'post' post type. if ( get_post_status( $post->ID ) !== 'publish' && get_post_type( $post->ID ) === 'post' ) : // Nonce it. wp_nonce_field( 'disable_subscribe', 'disable_subscribe_nonce' ); ?> <div class="misc-pub-section"> <label for="_jetpack_dont_email_post_to_subs"><?php esc_html_e( 'Jetpack Subscriptions:', 'jetpack' ); ?></label><br> <input type="checkbox" name="_jetpack_dont_email_post_to_subs" id="jetpack-per-post-subscribe" value="1" <?php checked( $disable_subscribe_value, 1, true ); ?> /> <?php esc_html_e( 'Don’t send this to subscribers', 'jetpack' ); ?> </div> <?php endif; } /** * Checks whether or not the post should be emailed to subscribers * * It checks for the following things in order: * - Usage of filter jetpack_subscriptions_exclude_these_categories * - Usage of filter jetpack_subscriptions_include_only_these_categories * - Existence of the per-post checkbox option * * Only one of these can be used at any given time. * * @param string $new_status Tthe "new" post status of the transition when saved. * @param string $old_status The "old" post status of the transition when saved. * @param object $post obj The post object. */ public function maybe_send_subscription_email( $new_status, $old_status, $post ) { if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } // Make sure that the checkbox is preseved. if ( ! empty( $_POST['disable_subscribe_nonce'] ) && wp_verify_nonce( $_POST['disable_subscribe_nonce'], 'disable_subscribe' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP Core doesn't unslash or sanitize nonces either. $set_checkbox = isset( $_POST['_jetpack_dont_email_post_to_subs'] ) ? 1 : 0; update_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', $set_checkbox ); } } /** * Message used when publishing a post. * * @param array $messages Message array for a post. */ public function update_published_message( $messages ) { global $post; if ( ! $this->should_email_post_to_subscribers( $post ) ) { return $messages; } $view_post_link_html = sprintf( ' <a href="%1$s">%2$s</a>', esc_url( get_permalink( $post ) ), __( 'View post', 'jetpack' ) ); $messages['post'][6] = sprintf( /* translators: Message shown after a post is published */ esc_html__( 'Post published and sending emails to subscribers.', 'jetpack' ) ) . $view_post_link_html; return $messages; } /** * Determine if a post should notifiy subscribers via email. * * @param object $post The post. */ public function should_email_post_to_subscribers( $post ) { $should_email = true; if ( get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true ) ) { return false; } // Only posts are currently supported. if ( 'post' !== $post->post_type ) { return false; } // Private posts are not sent to subscribers. if ( 'private' === $post->post_status ) { return false; } /** * Array of categories that will never trigger subscription emails. * * Will not send subscription emails from any post from within these categories. * * @module subscriptions * * @since 3.7.0 * * @param array $args Array of category slugs or ID's. */ $excluded_categories = apply_filters( 'jetpack_subscriptions_exclude_these_categories', array() ); // Never email posts from these categories. if ( ! empty( $excluded_categories ) && in_category( $excluded_categories, $post->ID ) ) { $should_email = false; } /** * ONLY send subscription emails for these categories * * Will ONLY send subscription emails to these categories. * * @module subscriptions * * @since 3.7.0 * * @param array $args Array of category slugs or ID's. */ $only_these_categories = apply_filters( 'jetpack_subscriptions_exclude_all_categories_except', array() ); // Only emails posts from these categories. if ( ! empty( $only_these_categories ) && ! in_category( $only_these_categories, $post->ID ) ) { $should_email = false; } return $should_email; } /** * Retrieve which flags should be added to a particular post. * * @param array $flags Flags to be added. * @param object $post A post object. */ public function set_post_flags( $flags, $post ) { $flags['send_subscription'] = $this->should_email_post_to_subscribers( $post ); return $flags; } /** * Jetpack_Subscriptions::configure() * * Jetpack Subscriptions configuration screen. */ public function configure() { // Create the section. add_settings_section( 'jetpack_subscriptions', __( 'Jetpack Subscriptions Settings', 'jetpack' ), array( $this, 'subscriptions_settings_section' ), 'discussion' ); /** Subscribe to Posts */ add_settings_field( 'jetpack_subscriptions_post_subscribe', __( 'Follow Blog', 'jetpack' ), array( $this, 'subscription_post_subscribe_setting' ), 'discussion', 'jetpack_subscriptions' ); register_setting( 'discussion', 'stb_enabled' ); /** Subscribe to Comments */ add_settings_field( 'jetpack_subscriptions_comment_subscribe', __( 'Follow Comments', 'jetpack' ), array( $this, 'subscription_comment_subscribe_setting' ), 'discussion', 'jetpack_subscriptions' ); register_setting( 'discussion', 'stc_enabled' ); /** Email me whenever: Someone subscribes to my blog */ /* @since 8.1 */ add_settings_section( 'notifications_section', __( 'Someone subscribes to my blog', 'jetpack' ), array( $this, 'social_notifications_subscribe_section' ), 'discussion' ); add_settings_field( 'jetpack_subscriptions_social_notifications_subscribe', __( 'Email me whenever', 'jetpack' ), array( $this, 'social_notifications_subscribe_field' ), 'discussion', 'notifications_section' ); register_setting( 'discussion', 'social_notifications_subscribe', array( $this, 'social_notifications_subscribe_validate' ) ); } /** * Discussions setting section blurb. */ public function subscriptions_settings_section() { ?> <p id="jetpack-subscriptions-settings"><?php esc_html_e( 'Change whether your visitors can subscribe to your posts or comments or both.', 'jetpack' ); ?></p> <?php } /** * Post Subscriptions Toggle. */ public function subscription_post_subscribe_setting() { $stb_enabled = get_option( 'stb_enabled', 1 ); ?> <p class="description"> <input type="checkbox" name="stb_enabled" id="jetpack-post-subscribe" value="1" <?php checked( $stb_enabled, 1 ); ?> /> <?php echo wp_kses( __( "Show a <em>'follow blog'</em> option in the comment form", 'jetpack' ), array( 'em' => array() ) ); ?> </p> <?php } /** * Comments Subscriptions Toggle. */ public function subscription_comment_subscribe_setting() { $stc_enabled = get_option( 'stc_enabled', 1 ); ?> <p class="description"> <input type="checkbox" name="stc_enabled" id="jetpack-comment-subscribe" value="1" <?php checked( $stc_enabled, 1 ); ?> /> <?php echo wp_kses( __( "Show a <em>'follow comments'</em> option in the comment form", 'jetpack' ), array( 'em' => array() ) ); ?> </p> <?php } /** * Someone subscribes to my blog section * * @since 8.1 */ public function social_notifications_subscribe_section() { // Atypical usage here. We emit jquery to move subscribe notification checkbox to be with the rest of the email notification settings. ?> <script type="text/javascript"> jQuery( function( $ ) { var table = $( '#social_notifications_subscribe' ).parents( 'table:first' ), header = table.prevAll( 'h2:first' ), newParent = $( '#moderation_notify' ).parent( 'label' ).parent(); if ( ! table.length || ! header.length || ! newParent.length ) { return; } newParent.append( '<br/>' ).append( table.end().parent( 'label' ).siblings().andSelf() ); header.remove(); table.remove(); } ); </script> <?php } /** * Someone subscribes to my blog Toggle * * @since 8.1 */ public function social_notifications_subscribe_field() { $checked = (int) ( 'on' === get_option( 'social_notifications_subscribe', 'on' ) ); ?> <label> <input type="checkbox" name="social_notifications_subscribe" id="social_notifications_subscribe" value="1" <?php checked( $checked ); ?> /> <?php /* translators: this is a label for a setting that starts with "Email me whenever" */ esc_html_e( 'Someone subscribes to my blog', 'jetpack' ); ?> </label> <?php } /** * Validate "Someone subscribes to my blog" option * * @since 8.1 * * @param String $input the input string to be validated. * @return string on|off */ public function social_notifications_subscribe_validate( $input ) { // If it's not set (was unchecked during form submission) or was set to off (during option update), return 'off'. if ( ! $input || 'off' === $input ) { return 'off'; } // Otherwise we return 'on'. return 'on'; } /** * Jetpack_Subscriptions::subscribe() * * Send a synchronous XML-RPC subscribe to blog posts or subscribe to post comments request. * * @param string $email being subscribed. * @param array $post_ids (optional) defaults to 0 for blog posts only: array of post IDs to subscribe to blog's posts. * @param bool $async (optional) Should the subscription be performed asynchronously? Defaults to true. * @param array $extra_data Additional data passed to the `jetpack.subscribeToSite` call. * * @return true|WP_Error true on success * invalid_email : not a valid email address * invalid_post_id : not a valid post ID * unknown_post_id : unknown post * not_subscribed : strange error. Jetpack servers at WordPress.com could subscribe the email. * disabled : Site owner has disabled subscriptions. * active : Already subscribed. * pending : Tried to subscribe before but the confirmation link is never clicked. No confirmation email is sent. * unknown : strange error. Jetpack servers at WordPress.com returned something malformed. * unknown_status : strange error. Jetpack servers at WordPress.com returned something I didn't understand. */ public function subscribe( $email, $post_ids = 0, $async = true, $extra_data = array() ) { if ( ! is_email( $email ) ) { return new WP_Error( 'invalid_email' ); } if ( ! $async ) { $xml = new Jetpack_IXR_ClientMulticall(); } foreach ( (array) $post_ids as $post_id ) { $post_id = (int) $post_id; if ( $post_id < 0 ) { return new WP_Error( 'invalid_post_id' ); } elseif ( $post_id && ! get_post( $post_id ) ) { return new WP_Error( 'unknown_post_id' ); } if ( $async ) { XMLRPC_Async_Call::add_call( 'jetpack.subscribeToSite', 0, $email, $post_id, serialize( $extra_data ) ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize } else { // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $xml is set when $async is false $xml->addCall( 'jetpack.subscribeToSite', $email, $post_id, serialize( $extra_data ) ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize } } if ( $async ) { return; } // Call. // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $xml is set when $async is false, otherwise we return early $xml->query(); // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $xml is set when $async is false, otherwise we return early if ( $xml->isError() ) { // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $xml is set when $async is false, otherwise we return early return $xml->get_jetpack_error(); } // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $xml is set when $async is false $responses = $xml->getResponse(); $r = array(); foreach ( (array) $responses as $response ) { if ( isset( $response['faultCode'] ) || isset( $response['faultString'] ) ) { // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $xml is set when $async is false $r[] = $xml->get_jetpack_error( $response['faultCode'], $response['faultString'] ); continue; } if ( ! is_array( $response[0] ) || empty( $response[0]['status'] ) ) { $r[] = new WP_Error( 'unknown' ); continue; } switch ( $response[0]['status'] ) { case 'error': $r[] = new WP_Error( 'not_subscribed' ); continue 2; case 'disabled': $r[] = new WP_Error( 'disabled' ); continue 2; case 'active': $r[] = new WP_Error( 'active' ); continue 2; case 'confirming': $r[] = true; continue 2; case 'pending': $r[] = new WP_Error( 'pending' ); continue 2; default: $r[] = new WP_Error( 'unknown_status', (string) $response[0]['status'] ); continue 2; } } return $r; } /** * Jetpack_Subscriptions::widget_submit() * * When a user submits their email via the blog subscription widget, check the details and call the subsribe() method. */ public function widget_submit() { // Check the nonce. if ( ! wp_verify_nonce( isset( $_REQUEST['_wpnonce'] ) ? sanitize_key( $_REQUEST['_wpnonce'] ) : '', 'blogsub_subscribe_' . \Jetpack_Options::get_option( 'id' ) ) ) { return false; } if ( empty( $_REQUEST['email'] ) || ! is_string( $_REQUEST['email'] ) ) { return false; } $redirect_fragment = false; if ( isset( $_REQUEST['redirect_fragment'] ) ) { $redirect_fragment = preg_replace( '/[^a-z0-9_-]/i', '', $_REQUEST['redirect_fragment'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is manually unslashing and sanitizing. } if ( ! $redirect_fragment || ! is_string( $redirect_fragment ) ) { $redirect_fragment = 'subscribe-blog'; } $subscribe = self::subscribe( isset( $_REQUEST['email'] ) ? wp_unslash( $_REQUEST['email'] ) : null, // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated inside self::subscribe(). 0, false, array( 'source' => 'widget', 'widget-in-use' => is_active_widget( false, false, 'blog_subscription', true ) ? 'yes' : 'no', 'comment_status' => '', 'server_data' => jetpack_subscriptions_cherry_pick_server_data(), ) ); if ( is_wp_error( $subscribe ) ) { $error = $subscribe->get_error_code(); } else { $error = false; foreach ( $subscribe as $response ) { if ( is_wp_error( $response ) ) { $error = $response->get_error_code(); break; } } } switch ( $error ) { case false: $result = 'success'; break; case 'invalid_email': $result = $error; break; case 'blocked_email': $result = 'opted_out'; break; case 'active': $result = 'already'; break; case 'flooded_email': $result = 'many_pending_subs'; break; case 'pending': $result = 'pending'; break; default: $result = 'error'; break; } $redirect = add_query_arg( 'subscribe', $result ); /** * Fires on each subscription form submission. * * @module subscriptions * * @since 3.7.0 * * @param string $result Result of form submission: success, invalid_email, already, error. */ do_action( 'jetpack_subscriptions_form_submission', $result ); wp_safe_redirect( "$redirect#$redirect_fragment" ); exit( 0 ); } /** * Jetpack_Subscriptions::comment_subscribe_init() * * Set up and add the comment subscription checkbox to the comment form. * * @param string $submit_button HTML markup for the submit field. */ public function comment_subscribe_init( $submit_button ) { global $post; // Subscriptions are only available for posts so far. if ( ! $post || 'post' !== $post->post_type ) { return $submit_button; } $comments_checked = ''; $blog_checked = ''; // Check for a comment / blog submission and set a cookie to retain the setting and check the boxes. if ( isset( $_COOKIE[ 'jetpack_comments_subscribe_' . self::$hash . '_' . $post->ID ] ) ) { $comments_checked = ' checked="checked"'; } if ( isset( $_COOKIE[ 'jetpack_blog_subscribe_' . self::$hash ] ) ) { $blog_checked = ' checked="checked"'; } // Some themes call this function, don't show the checkbox again. remove_action( 'comment_form', 'subscription_comment_form' ); // Check if Mark Jaquith's Subscribe to Comments plugin is active - if so, suppress Jetpack checkbox. $str = ''; if ( false === has_filter( 'comment_form', 'show_subscription_checkbox' ) && 1 === (int) get_option( 'stc_enabled', 1 ) && empty( $post->post_password ) && 'post' === get_post_type() ) { // Subscribe to comments checkbox. $str .= '<p class="comment-subscription-form"><input type="checkbox" name="subscribe_comments" id="subscribe_comments" value="subscribe" style="width: auto; -moz-appearance: checkbox; -webkit-appearance: checkbox;"' . $comments_checked . ' /> '; $comment_sub_text = __( 'Notify me of follow-up comments by email.', 'jetpack' ); $str .= '<label class="subscribe-label" id="subscribe-label" for="subscribe_comments">' . esc_html( /** * Filter the Subscribe to comments text appearing below the comment form. * * @module subscriptions * * @since 3.4.0 * * @param string $comment_sub_text Subscribe to comments text. */ apply_filters( 'jetpack_subscribe_comment_label', $comment_sub_text ) ) . '</label>'; $str .= '</p>'; } if ( 1 === (int) get_option( 'stb_enabled', 1 ) ) { // Subscribe to blog checkbox. $str .= '<p class="comment-subscription-form"><input type="checkbox" name="subscribe_blog" id="subscribe_blog" value="subscribe" style="width: auto; -moz-appearance: checkbox; -webkit-appearance: checkbox;"' . $blog_checked . ' /> '; $blog_sub_text = __( 'Notify me of new posts by email.', 'jetpack' ); $str .= '<label class="subscribe-label" id="subscribe-blog-label" for="subscribe_blog">' . esc_html( /** * Filter the Subscribe to blog text appearing below the comment form. * * @module subscriptions * * @since 3.4.0 * * @param string $comment_sub_text Subscribe to blog text. */ apply_filters( 'jetpack_subscribe_blog_label', $blog_sub_text ) ) . '</label>'; $str .= '</p>'; } /** * Filter the output of the subscription options appearing below the comment form. * * @module subscriptions * * @since 1.2.0 * * @param string $str Comment Subscription form HTML output. */ $str = apply_filters( 'jetpack_comment_subscription_form', $str ); return $str . $submit_button; } /** * Jetpack_Subscriptions::comment_subscribe_init() * * When a user checks the comment subscribe box and submits a comment, subscribe them to the comment thread. * * @param int|string $comment_id Comment thread being subscribed to. * @param string $approved Comment status. */ public function comment_subscribe_submit( $comment_id, $approved ) { /** * Filters whether to skip comment subscription processing. * * @since 15.5 * * @param bool $skip Whether to skip comment subscription. Default false. */ if ( apply_filters( 'jetpack_subscription_comment_subscribe_skip', false ) ) { return; } if ( 'spam' === $approved ) { return; } $comment = get_comment( $comment_id ); if ( ! $comment ) { return; } // Set cookies for this post/comment. $this->set_cookies( isset( $_REQUEST['subscribe_comments'] ), $comment->comment_post_ID, isset( $_REQUEST['subscribe_blog'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_REQUEST['subscribe_comments'] ) && ! isset( $_REQUEST['subscribe_blog'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $post_ids = array(); if ( isset( $_REQUEST['subscribe_comments'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $post_ids[] = $comment->comment_post_ID; } if ( isset( $_REQUEST['subscribe_blog'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $post_ids[] = 0; } $result = self::subscribe( $comment->comment_author_email, $post_ids, true, array( 'source' => 'comment-form', 'widget-in-use' => is_active_widget( false, false, 'blog_subscription', true ) ? 'yes' : 'no', 'comment_status' => $approved, 'server_data' => jetpack_subscriptions_cherry_pick_server_data(), ) ); /** * Fires on each comment subscription form submission. * * @module subscriptions * * @since 5.5.0 * * @param NULL|WP_Error $result Result of form submission: NULL on success, WP_Error otherwise. * @param array $post_ids An array of post IDs that the user subscribed to, 0 means blog subscription. */ do_action( 'jetpack_subscriptions_comment_form_submission', $result, $post_ids ); } /** * Jetpack_Subscriptions::set_cookies() * * Set a cookie to save state on the comment and post subscription checkboxes. * * @param bool $subscribe_to_post Whether the user chose to subscribe to subsequent comments on this post. * @param int $post_id If $subscribe_to_post is true, the post ID they've subscribed to. * @param bool $subscribe_to_blog Whether the user chose to subscribe to all new posts on the blog. */ public function set_cookies( $subscribe_to_post = false, $post_id = null, $subscribe_to_blog = false ) { $post_id = (int) $post_id; /** This filter is already documented in core/wp-includes/comment-functions.php */ $cookie_lifetime = apply_filters( 'comment_cookie_lifetime', YEAR_IN_SECONDS ); /** * Filter the Jetpack Comment cookie path. * * @module subscriptions * * @since 2.5.0 * * @param string COOKIEPATH Cookie path. */ $cookie_path = apply_filters( 'jetpack_comment_cookie_path', COOKIEPATH ); /** * Filter the Jetpack Comment cookie domain. * * @module subscriptions * * @since 2.5.0 * * @param string COOKIE_DOMAIN Cookie domain. */ $cookie_domain = apply_filters( 'jetpack_comment_cookie_domain', COOKIE_DOMAIN ); if ( $subscribe_to_post && $post_id >= 0 ) { setcookie( 'jetpack_comments_subscribe_' . self::$hash . '_' . $post_id, '1', time() + $cookie_lifetime, $cookie_path, $cookie_domain, is_ssl(), true ); } else { setcookie( 'jetpack_comments_subscribe_' . self::$hash . '_' . $post_id, '', time() - 3600, $cookie_path, $cookie_domain, is_ssl(), true ); } if ( $subscribe_to_blog ) { setcookie( 'jetpack_blog_subscribe_' . self::$hash, '1', time() + $cookie_lifetime, $cookie_path, $cookie_domain, is_ssl(), true ); } else { setcookie( 'jetpack_blog_subscribe_' . self::$hash, '', time() - 3600, $cookie_path, $cookie_domain, is_ssl(), true ); } } /** * Set the social_notifications_subscribe option to `off` when the Subscriptions module is activated in the first time. * * @since 8.1 * * @return void */ public function set_social_notifications_subscribe() { if ( false === get_option( 'social_notifications_subscribe' ) ) { add_option( 'social_notifications_subscribe', 'off' ); } } /** * Set the featured image in email option to `1` when the Subscriptions module is activated in the first time. * * @return void */ public function set_featured_image_in_email_default() { add_option( 'wpcom_featured_image_in_email', 1 ); } /** * Save a flag when a post was ever published. * * It saves the post meta when the post was published and becomes a draft. * Then this meta is used to hide subscription messaging in Publish panel. * * @param string $new_status Tthe "new" post status of the transition when saved. * @param string $old_status The "old" post status of the transition when saved. * @param object $post obj The post object. */ public function maybe_set_first_published_status( $new_status, $old_status, $post ) { $was_post_ever_published = get_post_meta( $post->ID, '_jetpack_post_was_ever_published', true ); if ( ! $was_post_ever_published && 'publish' === $old_status && 'draft' === $new_status ) { update_post_meta( $post->ID, '_jetpack_post_was_ever_published', true ); } } /** * Checks if the current user can publish posts. * * @return bool */ public function first_published_status_meta_auth_callback() { /** * Filter the capability to view if a post was ever published in the Subscription Module. * * @module subscriptions * * @since 13.4 * * @param string $capability User capability needed to view if a post was ever published. Default to publish_posts. */ $capability = apply_filters( 'jetpack_subscriptions_post_was_ever_published_capability', 'publish_posts' ); if ( current_user_can( $capability ) ) { return true; } return false; } /** * Registers the 'post_was_ever_published' post meta for use in the REST API. */ public function register_post_meta() { $jetpack_post_was_ever_published = array( 'type' => 'boolean', 'description' => __( 'Whether the post was ever published.', 'jetpack' ), 'single' => true, 'default' => false, 'show_in_rest' => array( 'name' => 'jetpack_post_was_ever_published', ), 'auth_callback' => array( $this, 'first_published_status_meta_auth_callback' ), ); register_meta( 'post', '_jetpack_post_was_ever_published', $jetpack_post_was_ever_published ); } /** * Create a Subscribers menu displayed on self-hosted sites. * * - It is not displayed on WordPress.com sites. * - It directs you to Calypso to the existing Subscribers page. * * @return void */ public function add_subscribers_menu() { /** * Enables the new in development subscribers in wp-admin dashboard. * * @since 9.5.0 * * @param bool If the new dashboard is enabled. Default false. */ if ( apply_filters( 'jetpack_wp_admin_subscriber_management_enabled', false ) ) { return; } /* * Do not display any menu on WoA and WordPress.com Simple sites (unless Classic wp-admin is enabled). * They already get a menu item under Users via nav-unification. */ if ( ( new Host() )->is_wpcom_platform() && get_option( 'wpcom_admin_interface' ) !== 'wp-admin' ) { return; } $status = new Status(); /* * Do not display if we're in Offline mode, * or if the user is not connected. */ if ( $status->is_offline_mode() || ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) { return; } $blog_id = Connection_Manager::get_site_id( true ); $link = Redirect::get_url( 'jetpack-menu-jetpack-manage-subscribers', array( 'site' => $blog_id ? $blog_id : $status->get_site_suffix() ) ); Admin_Menu::add_menu( __( 'Subscribers', 'jetpack' ), __( 'Subscribers', 'jetpack' ) . ' <span aria-hidden="true">↗</span>', 'manage_options', esc_url( $link ), null, 11 ); } /** * Record tracks event if categories is created when user enters * the edit category page through the newsletter settings page. * * @return void */ public function track_newsletter_category_creation() { // phpcs:disable WordPress.Security.NonceVerification.Missing if ( empty( $_POST['_wp_http_referer'] ) ) { return; } if ( strpos( sanitize_url( wp_unslash( $_POST['_wp_http_referer'] ) ), 'referer=newsletter-categories' ) > -1 ) { $parent = filter_var( empty( $_POST['parent'] ) ? 0 : wp_unslash( $_POST['parent'] ), FILTER_SANITIZE_NUMBER_INT ); $is_child_category = $parent > 0; $tracking = new Automattic\Jetpack\Tracking(); $tracking->tracks_record_event( wp_get_current_user(), 'jetpack_newsletter_add_category', array( 'is_child_category' => $is_child_category, ) ); } } } Jetpack_Subscriptions::init(); require __DIR__ . '/subscriptions/views.php'; require __DIR__ . '/subscriptions/subscribe-modal/class-jetpack-subscribe-modal.php'; require __DIR__ . '/subscriptions/subscribe-overlay/class-jetpack-subscribe-overlay.php'; require __DIR__ . '/subscriptions/subscribe-floating-button/class-jetpack-subscribe-floating-button.php'; require __DIR__ . '/subscriptions/newsletter-widget/class-jetpack-newsletter-dashboard-widget.php'; plugin-search/psh-128.png 0000644 00000012243 15174711637 0011133 0 ustar 00 �PNG IHDR <�4� aPLTE ��������������������������������������������л�ջ�ջ�ջ�մ�ў�Ə�����k��c��\��Fy�y����å��M~�Fy�Fy�Fy�Fy�Fy����Fy�Fy�Fy�h��Fy�W��x��������������Fy���ƴ�ҏ��k��X�������������۪�ͽ������������������������������������������������������ �( �( �( �( �( �( �( �( �( �( �( �( �( �( �( �(D�`�ȑ�=��Ʀ˱M�g �3&�H�̻@�]�ʦf�|3�R�ɜY�rsLJ@�^pچ�5P�k�箏㡯�������0�P �C`�y�ߔ���������֞�����������x��o���������������������ݤ�ɥ��~ȏ����֤�ɖ��o��o��o���������������������������5�������������������������������������+�N������������������������������������G�g�����������Ԁי�����������������ת߾!�r� �tRNS @`�0p��� �ߏP�`�����������������P@ ���`��������0������������������������������P����0�`���p @����������������������������������������������������@���������� @`���0�����@���ϟP�߿������p�������38 )IDATx���� @aHڐ%www��OU��uj�+��jx�(��S��'����Pad�Ԑ�D�̐�BX.tj�oZ�}�!X�d�A�`aS�Q!��U��8�b�U�y% �E[E�U-�Ŕi�M+�E�i�,G���� ��Wٸ��Qч�1ga3S+c��\,���U&~���`1�b!�E�ԊT+��5���,Ը��C~,bE��y%"Xh�3���`A����`�`A����E4�,�~�BD�,<X�b2Xİ�,�,d�BF��,`�2X$��,��X���,`X���,`X���,�a!���,`A�`!#X�2����\-��Z]s��1����m���Z�� ���m��qg#ङݽ�������cqT1P��_ :�9X̄���`o�xw�d����S�8<����Ύk{���b�|ww��]�U���t}ssskj)Yx,dw�����m�y�g�.�d/7팻�f�^�z{h��8 }<8�|sf�� �,`X���,`�XXX�`�a�=X���E�pX�b��,`X���,`XhX����G������wqd� ����#���,`QfώV�� �쁶��45j��#K���s�o?F!�*�����n��,.�c��{�"(�����^Y(��H�}ě���,�/��Hwe�튔E}sz�E�,�,�0�XX���+�>Y�o�H��E��Fe�� ���L��X�,`�I銅��.o��q|e��2Y`V}�X�Єl��|(m�8�8�ŕ,�¾ H���"L���y,���R2,�a�,�H��N,��\�n,����Y;d�,��.��#S�Yzd�,d�]D`�X��z�Oʢ�@�X ��e+Oe� ����L�8�f`L�/v�܄�SY��/f !�QY���BY�}� $�1>x釅���/8 �R�98+wa���[Y|� �q6�-��� ���8-e!�0��`�U�,�b���)\8�V����.q�� e���c�Q��1s�X�?>��/eq)���݅��@�� ��Sa�d�c��zq/fF+[�������Y�5��ŗ��Yp�������x��i�,���� ��-�X��_� ��om�b$���c�s��[.X�E1ϧ�� �5D!X�EQ� �`�,��U�� ��O˅`!���,"ڐ��`�53���T@�=SX�7��#��V ���|ڞ��H��Q`a�LҾ�,c��t@I��$����D���7k��,�C\�0���>��&4f�t\q�����ת���&���,N�j� Y��щ%��S'^�m�Ұ>��Xܟ��sC�L�lOXn������N�Xɂ�D�<6K���t�|�`�� ߓ���C�}E/��t�dz`qH����1��X�'�TR�t�<���!�uD|���+��O�Tb�C:o��y`��a�%�������Y�o����x6�O��`���S�ɳ|[��-Jf�Hz������-Q��ΠD�I,V�.�Xk�Di,f�I>Xp��v�c�.dU�8�?���? H��vwש�}�w=�;�'6��8|Ow�g F C�#��R,~o�Rf1��s�)����PDv!5k���"� �0�,j�� �2,��,V�bX��+��1@�EX�ܬ��3�����XHvQ��h�\fq�X)o�f^/S �,�oV�{�+��r��$$��,~�,���hUc h�ܫl�E�Y0��T�w���2,�n�Sf!1N[�s@�=w����`�{���:z �㜗�B�w���m��f���Y|}�, �1+v]8��Q��\���źX��)E�x�� L��4�i_=�j�+���v#/F* B�6��ga���_Y�����Y� ���#ހģ��<�E��>�,,�r:�� �<^� v\,��,HM�1k V&�_��"豋 �iLfY߯�EfAF��-��N%vQ�5+��̢sa�jĔe�q.{4,�̢��@��s�f�X� P ��"&5&3g�ͱ�,��f+ti �R|c,2�+zS�J)KCsG�=��BAo[=;�̶Xd�m�h�FU� ���$�hc�N˟���&W��������0 �;��g��]Z'� j��gD���ΖYh��\�J�T-���Q��X<yAt�$�h�@5g� �i�KuA�ӷ?ʉ����)����B͝CX�q5-5�x!o��b��"�8k�2��X�)��af�{#,�O���yc���o j�M9���� �YDt��-b(f�n�s!M[dU���Y�r���O*Ь;���VPUa6�"���B2��t���P��P��:8�!U��/�QA�4��Bć���Q�͝X4 0N��Y`?*��Xf� Xb�dI���!6!!b�6�^�Y�Y�^Z�<^�f\�5��E��Y\D�O2�> x��7-�ɣy���%,�8��v���R�����jrAi�*�hq��cy�Mf1���$Ѧ+Yr.+=<�W���eѫx�dCl^E,���1 f�79X��.��mn���/ 1���,�?��ka�O��梢�d�M�%��`���}Y'�Mf1�~��??y���Tʡ��wE��N�f�tG�2�(��CY��b�y���\����6���F�t�,��t�;�� ��Ƣ��Sv��.��Ys�>�> p�+"Swlj�Dɢ!�N�*�I�@�[��t�V��̠�����v�0��l�;3疙�&) ;���-��N� ��ҕs|&"��Y<�'�uP� qrk�o(���b����\��Ŧ;#ocx��?L�B)c̲�Se��(�R�d*���,�A��*�y����Y�Q�$u߳P�C5�7!�M��h5)��j�Y��B��㮷Y 7���͆�6,[�6�r�OY,�2�nB�z�B�ٮj7(i��ź�d2�q4��U1�,���� ��f��,69��}�ܡ�[�A�z�P�lOU�wYl�#�����1��Bޅ*�N��ě,�8� �k�Ȃ�7�ʢk����e����F"{��xj*hFg�ի�%�P���3�Įn� h4�����-�,�_��K}��ˆ�)�Vg���e��b J'��|N1���B��Y��s0\X"S|`�fu��,���L� h_b`�":qm��b��������'S�[���Q��p�&�����+���,d1�&���R�����<F�f��o�i`&���,T`-�,n�{@��d߰�2�,>]��''>�����?~]&�'*�]�S�O��BĹ�ā����"�ƁW:o X��,( �R�I�:��?EPLv`�F�:�p<E89�5�Y�(p�#�H��Y��˦Cs����7�va +Ea�Dfp(mܭ3Z}o�}�$�=_ ?�O�"Z�pY,�g�I��-�͂�PY��E�����YD�(�)��Udu�,�F�1����,���K?f����"z��!̂��dA�M�Y\TdAs}�!�B�� @��z��|�M�~�A=�枅I��7;w�F?�k5��f�|+j܍���:`?CTB��⹓�!�Y(�7*�L�adff�Ѹ��4#�{Z�3�ҘG����=e��02��b�&����}e�P�:��Y0��F�&�����Yة���,t����g�Ytj��4�HnX��%���U]Ə{�Ԯ##�i�.���:�8*�>�Հ"�b��R�N��6�Ux�@:�x�]Ա��w���R>G�h���i��+���ۅA1 �ŝ�p��n?����_�-gɡ��Y%CMt+�@��m�/N�=>���0pS�6�X���"�%�IH�M�%��p���/i���̖*t_y��刦Zt�ߒ��7 ��އZm�����-�U��X_�R%)r� �$�V��!|�pó���E�E%���6Ʌ��r��g�^DU*�3� �=}�8���M�*�O�PWa�I�2�'���v��Ϸ�d2��Ǒ�3/z0Uw�}� ����k���q��_z]?ER�2a�.� `�����5A �u�̤.:]���qp��}�'��4\�v��ǘ����)R��/��fT-S��T��*ڱ�oMci#溜/��� myj@�b,�*� 8 �~n'��x IEND�B`� plugin-search/psh-256.png 0000644 00000025146 15174711637 0011143 0 ustar 00 �PNG IHDR , + p�� �PLTE �����������������������������������������������л�ջ�ջ�ե�ʞ�Ɓ��y��c��U��Fy�M~����\��k����Ѭ��c��Fy�Fy�Fy�Fy����r��Fy�Fy�Fy�Fy�Fy�Fy�Fy�P�Fy�h�����������Fy�N~�p��W���������ִ�ҏ��t��k��a��_��������O���ȫ����ͪ�͆�������������������������������������������������������������� �( �( �( �( �( �( �( �( �( �( �( �( �( �( �( �(�̻"�D�˱M�g�ʦ3�R�ȑ���sLJ�= �3&�H�ɜY�r@�]f�| �C0�Ppچ����5��׀ߔ��@�^`�y��ɯ�P�k��������֓���o��x��������ۜ������ѥ�́������������䤹ɉ��o��o��o��o��v��������o��o��������������������������������������+�N���������������������������������������������U�s������������������ڥ�������������������A������������������G�g�י���cр�߾rԌ���9�Z�5����6�ۭ#�� �tRNS 0`���ϯ�P��p@ �@����������������p@��߿�P ��ߏ����������������������������������������������@���`��0�P�� p����������������������������������������������������������` @��������`@0���p����Ϗ��`��P��p@��Ͽ� ����߀P����������߿���6 &iIDATx�݅��H��_̰w����Y��30� ��)ʪn�����~���,�Ӽ��f��0�}�ȭLy��12B����5� HPG&��ND�6oPX�6s�şc�ۧ@,ؚ�)�9�a�|iE]pú}��u�]5 y��[ѭB�b��R�A,X��1N�X0��=X�7 ��Xе��1.�X0����bA�^*Ă+�V$@,��c�6��Vp��Ƃ*��&�{ʺ� ̶oA,���)@,x>��nYV+�`k�b�-s��M�� O,R^� ����@,Hўj��.�e���:�e���)�BR�Y ��-d�X��3|� E{j b��J,���>%�b�G,|�b�X �bb! bb! b!�b�X �bb! bb!���{�X���Io_���b!bb! b!�b�X �b�W�b�X �E�����B,�b�X �b�X b! bb!b�X �b�X �b�X��X�X�4�b�X k$@,�-�!�b�X �b�Xd�@,��@,��ǎ= V`ͨ��Զ5�����_�W�%9��sX��`!X��`!X���XH��,�B�,�B�,RN�,�B�,�O�,�B�,��(A��` �,`!�R.�,�B�,�FX�B�H��,�B�,�B��,$X�B�,`!X��`!X��`!X�,`�K���QE�B�H��������W]��R�B����5�s��oD�a�2� t�n�X���h����O��L�<�`��>��陙�8�pXt���Щ �}R��$Ģ:R氘��1zL�瑲����X��,�I��������I�����|~��E�t_-��,�kF�XH+��������WUU�F�\b�����>)A�+�m�֫J�5�����q�Q�/Nm��vVW�K�;�Y���������vxtt�O���ʥm����w��n,`Qڛ���;��m����e\ֻ��ޯ�݇�J���"A��xpI���_�U��y���RU�=�,�O�,X�B��,X�B��,`X���`!X�,`!X��,`!X�"�B�,X�B������6,X�B��,X�2X���`!XM���`��fX�,`�B�,��`�^���`X��`!X��E3,`!X�"�B�,��`!X��,`!X�"S�B�,�E}$X(A�h��9X��X�B�H, ���o*�ﰀE�C���!X���`Q��,`!X�,`!X��,`!X�u�`� X�",X�`q��}�ʫa�i����,����]M"�aB�� ���ՌF=�ܘ'(�9X< �_`;*� �Ga�_� �%�``�"�ś�X�E% ,�,�,jIX�'� �h�,��=�E�Ҳ ,�,��%u����,��$�%� � ��JI�S>��3��o�~`��r/�``�X�j$9� �X ,����a?�,v�EH7<�[3����X<��rK��q+)>9`A`a�^C�j�f �~z�B�'�,,�ZR�����8�$�t��p|&,�������^R\����FV'eσXX$�z)�.-Hj�X�*�^�\R&�~",���"i�K~��+TJ��0$����x��"�թ�M+$)ΰ�'���$u`OX$U^�27M/5�X%*��n=,�������Q�1,4Y���[�,���?��ہ��ѕ:�V�R�t�QM��z�ʊ���Ŏ�y�4���5[��@�$.�T `�d��e3�s�ߧX�v�Rέ9���� � kQ̶='3���#�+w,�?`[�"��H�.Ӊ^�l���X!����-/�tja�&ٲ�l`�B`�Y-�d�S�;.$S�u!``�-�d�/���#[a`ۮV�E/c��H�gu:�:� ����ʹ8N�qDa��6���XX��Em$�7/��epW��6� ���:&�#��L%�:r�V ��b�����+f��F�&~H�L� ���,�,��E�;lj1"q�K�%*��Gk��l� �XA�h��Q���'!}�u'+�,�bW��g?�(.����ױ���m+����Xl^�r�߈E9�K���E�̭��X�X�LcC�u��u*���J��ͼ���-c+h���B��^��^ /�d|�!�H�%���CW��bT��պ,YptA����!�H������+!��T�ZD�d5�ts�ne������#АZ��ߠK�����V�V����Z�y�"���h7;�´,,�Y>��Y���� � � ��tUԤ��xoW�V����낦e��κQ�V�V���n�C����,,vą|�y�n�iXX��z;߸5�]q�vC�V ,,6^��e�[)� ��|u�b�>X�8�bo�]�ۅ�}<��v�����6+���b��e��|�`A`�ת!h��`A`��&jV۹u�}����v���*�,�X��S�O��,������F@���}df��"�0�,=0 :1���]�+.\ �p=kG�lbb��WA(��X�X��ԂX�X�ي�<)bb�"�5�Hq��nE��ͭH�bb��Gbb��ԂX�Xlo� ��Z��VPbbq|+�Z�X�X�i+�� ��zǛZ�c[���b���b��q�X�Xx�oP��E�l( (l,�@�6�$g���5?}d�"���R^ca�?Y兂����ӝX����1A�bbM�8A$(N,@,�X�6 ň�E��)�cAbbz" �E��Dwb-B,@,fV�"N�"Lj�<�s��b�S�bf�����X��H��,� �C,ܩ�"�"��a��6�U�D�b6����X\S�lR���;' I����$yP��%�6�8�o�/��'��X\EϬ�l���*�Z̀ �ŵ[�j���jF_@,2֯� *bq��3�P��Xd�i6V��ޞ�Ь�P@,��6�t��b?/_�U�|zA,2R~�B�뚤>�^b�:�T�b���J���!� o���b������$�j�ւX<�^oA0����[gI�b���Fo^��1�.>� Ģ�,�w̓�X���9�dL0��e��be��T��b��O��F� �ŹՖc��m_����J��m2� ��ُN�sP1w�kSҭ3o�);����X�E�����V�Y�HgE������Q��bq��}g����Z7!�lf�&�R<I��$cw& '�C�zõ�կ�h9ʼn���֎�d�nH,���ײj[ޤ�L"���D5��X����W`�M\b�{��%)��q�\��i� !@0���x��3��N��H�a����6c+A� X�9v���Ӛ�V��}���_��No�,���?�vXI+�����Hq,>}�U����������_��`o `��Ӣe�i��-O�P��G��W=:,��E���U3/n�MK甓U�5�O�@`�f�6Ur3��V���)Y���4�1� "J3�!]��.�l���M0�� l�0�+��fu�X�\�����@L)����*r��,��x>-����d�X�蠇��b{-��b��Iug[e��7,���X\����D�Yċq�TS-���Q?`q=��)�0XX1�1S-2����XlѰ�״�,�X�Q�Z ���#�.�%J�?J��!89�7�ؠ��8�j��(K1,pv�,Sch=^��beFX�h��X��%�焸�E ,0h�,��.�a�൰��c�w ,6(xk��dG�*��*l��,0h,�fvuG��/Ce.X<�Ѳw����^��r�z�P�:�Yҩ�g,0f�A`��������I�cu���`�m���`�LS�L،S�)�� ��,0h,�.�.s��!���N���c�����˹ �O���X]���<����7����f����xFSUW~YȽc��,Le&���?�tt#�J���X,2Ƣ!��) 7ƪhH��cY��;���T�`ꦯJ�4״�t�Y�� ��.>���"��4Z� �c�p�o��^���m����3��>��+XlХ�AN��OBuK��mt�Y`�pb*X #�yi�۬��q5"s,� ��;��.����.T#r�}�c,�`?�Up?,bek�Ѯ,��81� ,pb�zZR̰?,�Bթ��"{,� 啕��bv�Âݏ#�s��>.`�yo`�Q���1<�(�\ c��/{��7��~��_��Z�j4�ŢG�Xp�C��TE��txX�r�l��$�nu�_���s�ڢ�X �&�$�y:%��M�9�1���G�X�o-ܵ�j�@ZKE�\q,�X �+��~Z�k�/L,���X4��*YQѕ\ م�!�"=O����R�8b�X�*~��S%�鬱x�#`,�䅍����3�i���e�X��)� �Z��� ��ʋ��o ��3,��X�<P+\)1��g-<�8N�X0{�e!�i�d�߭U,����]5~���(�ť�y�'`,�E6i�|}i�s���I����@�X��ݍ�iO3M�X<��z�`�\x��E�X������!&���X�<`,B�V���:[Z��� X�:`,tM*�O����ĥ�.�]��"���$2-�M^�[��,�EÞW<!e�b� C�,�EPDTnt/5V_e@�4d�`�h����Ι2a=F,v�?`�8���)gS��_`��~�����Іn �}�Pݙa����J ��������H( G'7{e!KE1WpO;,�ӯ���~�뽁�t�n��+�BhGw���IQ�w�h`��~�Y��� ,擓a�)�[*EC����!x_��L���X��O`,f/!���<�)7<jp� ��~�K<�X �������v��'�k������X�Ί?~�E�lO!^Z�X}qAgyfF*Z����bG�"N��`�e����R0�ny�\73/TWE�����?,��}��o�KY1���PX�n����Q��Y���X�R�O! ]�.WF�������c`Ѥ?�4�P�|��E�V���d�b�K~ �-X���mM�� ��ŷdz���XNB�4Vb�-�$����aDz�"�~��>� `�&��\�;�[.�0'<�E�V��`���n�X�X C�nrb�En#�qX ��{Y_����{o����bS�$`!�7=�Se��f$Qn��.~�P9Y��b�V��O�B--�b�x�Z�C�5tG���b�V��+��,��l�n�F�ia�r����f��`��PV��c? X ;}P�K'T˭�H\�m�NN%{��4��ё�X���M�7˚9�g�N���#;bs%�؛��/�����.��t��NT)�t� J�{��ya,���l����*�����魊R�X�o?�!��XL�n4���t<&��-C�\[�� ;ݠS��4�M��� ���BKb��������d1��2��aA���X �˳�\1����ª2OY��'���%��8 ��E�x+���w��f��V(�u��G�_ I�5`q ,�F����ťE�p�>���/3߷�����ӂK�"&����U�2��2��q�V0;,�����V�*�� ���Ѣ:�M�`q�'6{��X EӚg��]�M9\*�:�K�ɚ�$-`q_��_nb_/�МL��i�9W�8�n�ԋ�6o,��� ��&� ���)8�a��7����h3� ,��}�A*Z�Tz���8`�?�G!R&7q�X����M�X�@�6 \1}�_���<����=y����i�\g������7�Xܣ�A� ���/?J�"zyc� ۋ�� d�>���y�A�CYڪ��/o,(�!�+,p�X�[cd.�4��H;�����j/ֳj85����B�Y�K���o9��<kL�W����yMq5XX�jt����[�?e���\/��� {X�Zl����=c�����j��;C,��Պ_z6p��N��b_K0���x��EӄCS`,��!dc,D��Td�ţ�.��C�-{X����#��5��i�G��A�V��C%pOB���X�J�h��evF���_���)��",t�nخ�CNYX۹��-"G���.�$�b&X�x1}�����Y�{�u� ��`Q��Z0��ɇ���Fl���c�xރ�d�vk,ī��7� y��k�}�w��E���쫆Ń�7��wJ��Z�]��%o�0sr��y�3v�g�Ц�0'ϸ̧�hK��{�MK�<.�w�1bFŏ$���e���<z��X|��X0V 7�Ef-�EE��8di%FI2��xEŖ���]��n��nbfѦXQO�$�+�4.�w�}��v��2Za����&�,��ҲG�ȣ��j�v��~��O� aTN�o�:��Q���g��kZ��[��E�b�� ���8ΣP���Kd(��d>��8R։�dϛ�<�7;�%c,%֊ӎ4�"��Ij�,��Q�䩚jY,��ъqM�9E,��t��'���жX\�b��%���¶P�2�E��K��J�}oPm+<��#�(�$m�Ѭ��V��U*PO��zU@�ݗ!��b:Oe>����✮Uǭj���I��b1�ei#�f�I�`nh;kث�=� �/C��4K-�N���Ⲿ�_��璤o}����m0���&��.UhO��so�V�>1v^� �q�"ٸ\4-��B�zU�p��&X�e�ձ�1m��\4)�$�p�1V���q �C ��p�S��Z���d!֙����CU���.Of`��7b!o4���]=K�-�<���7y�,��%�^��1X��7��=��H-:�c,��u4C,`>]X��T�=��B�I�Gb�^���T�~O1{����A��X��kbq�*��+��7nv�i�t��(l��U�O]-�� ,��d!0*C�c���Q�`������fN�Ol��m�S\w�V��<9E,�sĴX�"-�;���Xpg�Z �L,��Ub�]�t�խp�&ǜ� 0�C�X���o���j�+1�0�+Y�� bq�4�����+���X\� ��ᒔ�ne�X@���M�cN�j�X��0��ynv,.����7`��<A,��bq�4r;��=��N9f@, �̍�Ubts��V�.NH �b�߲ǜ�� ,��F,`n����oT�M繳��@��O�����OU�yW�����~��ie��맭���u�4cG�܀ju� ���s��N�f�:j�.��A, �q�t��2��[�`-�H����M��v����6��|� ������ A, �C�>� ,�+� �yn��C���E� �C�AOz`�Eb�,���!qXd!`�#$����b�%1]�KS�NG,@��$�gR+�b��oI�=O=�� �q�M�$I~��'3<`�>�ġ �~79���ؤ��-bڽ�xGH_�F,���C<{#�x��X`Ԣ�C,��������nn���X`���X`��=/�b�-N���S�qu�\87��`�sS� ��Q�X@aaQ�X����D]b��@�X��Ec����E)b�]&����,-,,�K������X�Y������������ہ;���@�X���7��p�>�"p4�U�X�㸏M��^����[<G���yβ�_Y���������"�ڼ]����)�i�ӥ�i1�_tj��}�"bvs�H�b���� �h�a�s���c��#`�z���a2�_�>���%�:[�h�E�Z��|���]j l�>B�{�gY6��1>#��\�ɣ�Ýq�>�><6��a4�sW�X��tb�ŝ�v��;"�x�ω��!�h��pj�X�w��ǰdbф��:�,?�aQ!��mb!���}�bq�«�Ëb�9�b�X|ϯ�c�bqB�ް��!_)� �@,N�0�٥ �@,�4�2�c��k�Ew=��nUz%�M��"Ģ�^��3�X Z��F�]�=֞y��YР|�:g��Z�j�d��I�l�1�}�ZhE�E���V|d�LZ�ZQ�2��P������Њ2&ɮj�5�C6y�Z�w�C5M>DPs�%���,�=������� �w��_Z��. ����6�5���� �vD^Y���.g��7I��wE�W�y�e��D9�9��c��2}l~����]`� a ֒Z��QnXf:��(�^ff(s{�VA�K2�r;��_�s"�T�O�M&f ��J��rEQ���9kuj���G@�T���vOtM�PEhf�s�cWY`M�KA��,\ı �� ɳ���_+$-�@tAxS�(��p� 泹�{���O�@Bv��� !�T�����.�b�"��p�!kYj�J�r�>E�O ��R�\�^p{� ��R�48��}+�dll�1�nii�= �<��(Cꐪ�����$� F8��({�T�R�(z����#谻B��X�@�)EO3�s�N@���A>�Q����2����&���C�\.���6���Q�%GH����3��)�@Z̬\|��6%�ƩL�$Z e�ߨV 71i�_��G�|?-v�g��:�\>��kR\ 42�a��o�Ew��#-~�@��t��o����i@��E��?�d1�C��Y^���ɺ�� �$ۃ�m���W�.�H������'���i��Z��5�77Yb�I�2t�����-���e#���v�� � ��v�ݭ�"8w��"� <�u1��I�rzB* �� �� ʼ��Z*EuE�����J7�|�f���?� �F�]�T��]�f��-E6�7�qR�� �I��iT�#@/�����=���P�楨�uy֧:����+ �O{p, 0��z�� R��S�M IEND�B`� plugin-search/psh.svg 0000644 00000012574 15174711637 0010645 0 ustar 00 <svg viewBox="0 0 183 104" xmlns="http://www.w3.org/2000/svg"><path d="m.3 99.6c11-.6 22.1-.7 33.1-.8l16.6-.1h16.6l33.1.2c11 .1 22.1.2 33.1.6.2 0 .4.2.4.4s-.2.4-.4.4c-11 .4-22.1.5-33.1.6l-33.1.2h-16.6l-16.6-.1c-11-.1-22.1-.3-33.1-.8-.1 0-.2-.1-.2-.2 0-.3.1-.4.2-.4zm152.5 0c2.5-.5 5-.7 7.5-.8l3.7-.1h3.7c2.5 0 5 .1 7.5.2s5 .2 7.5.6c.2 0 .4.2.4.5 0 .2-.2.3-.4.4-2.5.4-5 .5-7.5.6s-5 .1-7.5.2h-3.7l-3.7-.1c-2.5-.1-5-.3-7.5-.8-.1 0-.2-.1-.2-.2 0-.4.1-.5.2-.5z" fill="#e3eaf0"/><path d="m46.1 82.8v-57.8h70.1v57.8h-66.3" fill="#d8dee4"/><path d="m46.1 24.7v-13.6h98.2v13.5h-92.8" fill="#BBC9D5"/><path d="m144.3 25.2.9 57.3-28.9.1v-57.4z" fill="#ccced0"/><path d="m102.5 82.7h-48.2v-32.1h53.1v32.1m0-35.7h-53.1v-13.2h53.1z" fill="#fff"/><path d="m96.1 41.6c-.7.2-1.4.5-2.1.8-.6.4-1.3.7-1.9 1.1-.4.3-.9.6-1.4.9.4-.5.9-1.1 1.3-1.6.5-.6 1-1.3 1.4-2 .5-.7.9-1.4 1.2-2.2 0-.1 0-.1-.1-.1h-.1c-.7.5-1.3 1.1-1.8 1.7s-1 1.3-1.5 1.9c-.4.6-.9 1.2-1.3 1.9.2-.7.5-1.4.7-2.2l.6-2.4c.2-.8.3-1.6.3-2.5v-.1h-.1c-.4.7-.8 1.5-1 2.2-.3.8-.5 1.6-.7 2.3-.2.6-.3 1.2-.4 1.8 0-.7-.1-1.5-.1-2.2-.1-.9-.2-1.7-.3-2.6s-.3-1.7-.5-2.6c0-.1-.1-.2-.2-.1-.1 0-.1.1-.1.2-.1.9-.1 1.8-.1 2.6 0 .9.1 1.7.1 2.6.1.9.1 1.7.2 2.6-.2-.5-.5-1.1-.8-1.6-.3-.6-.7-1.3-1-1.9l-1.2-1.8c-.1-.1-.2-.1-.3 0-.1 0-.1.1-.1.2.2.7.4 1.4.7 2.1s.6 1.3.9 2c.2.3.3.6.5.9-.5-.5-1-1.1-1.5-1.6-.7-.6-1.3-1.3-2-1.9s-1.4-1.2-2.2-1.7c-.1-.1-.2 0-.3.1v.2c.5.8 1.1 1.5 1.7 2.1.6.7 1.3 1.3 1.9 2 .6.6 1.3 1.3 2 1.9l.1.1c-.4-.2-.8-.4-1.2-.5-.6-.2-1.2-.5-1.9-.7-.6-.2-1.3-.4-1.9-.5-.1 0-.2.1-.2.2s0 .1.1.2c.5.4 1.1.7 1.7 1s1.2.6 1.8.8c.6.3 1.2.5 1.9.7.6.2 1.2.3 1.9.4h.3c.7-.3 1.4-.6 2-.9.6-.4 1.3-.7 1.9-1.1l1.8-1.2c.6-.4 1.2-.9 1.7-1.5v-.1c-.3.1-.4.1-.4.1z" fill="#ccced0"/><path d="m147.6 24.9c-8.7.9-17.4.9-26.1 1l-26.1.3h-26.1c-8.7 0-17.4.1-26.1-.7v-.5c8.7-.9 17.4-.9 26.1-1l26.1-.3h26.1c8.7 0 17.4-.1 26.1.7z" fill="#46799A"/><path d="m77.3 70c-1.1.6-2.1 1.3-2.9 2.2l.3-1.1c.4-1.7 1-3.4 1.6-5.1l-.3-.2c-.6.7-1.1 1.5-1.4 2.4-.4.9-.7 1.7-.9 2.6s-.4 1.8-.5 2.8v.8c-.2.6-.3 1.4-.1 2h.2l.1-.1v.1h.2c.1-.5.3-.9.4-1.4.1-.1.1-.2.2-.3.3-.5.7-1 1-1.5.4-.5.8-.9 1.3-1.4s.9-.9 1.3-1.5c-.2 0-.5-.3-.5-.3zm24.6-10.4c-.5-.9-1.3-2-2.8-2.5-.6-.2-1.3-.3-2-.3.3-.4.6-.7.9-1.1l-.3-.3c-.6.5-1.1 1-1.7 1.5-.5.1-1 .3-1.4.5-.2-1-.6-2.3-1.8-3.3-.8-.7-1.9-1.1-2.9-1.2-1.1-.1-2.1.7-2.3 1.8 0 .3 0 .5.1.8.3 1.1.9 2 1.8 2.7 1 .8 2.3 1.3 3.6 1.3.3 0 .6 0 .9-.1-1 1.5-1.8 3.1-2.5 4.8l-.1.1c-.2-1-.7-2.8-2.5-3.8-1.1-.6-2.3-.9-3.6-.7-1 .1-1.7 1-1.6 2.1 0 .1 0 .3.1.4.4 1.2 1.2 2.2 2.3 2.9 1.4.8 3 1 4.5.5 0 .1.1.2.2.3-.5 1.7-.8 3.4-1 5.1-.2-.6-1.4-2.8-3.9-3.3-1.2-.2-2.5 0-3.6.5-.9.5-1.3 1.6-.8 2.5.1.1.1.2.2.3.8 1 1.9 1.7 3.2 1.9.3.1.7.1 1.1.1 1.2 0 2.3-.4 3.3-1l-.1.2.2.3c.1.2.2.3.3.5-.1 1.5 0 3.1.2 4.6h.2c.2-1.3.3-2.5.4-3.8.9.8 2.1 1.2 3.4 1.2 1.2 0 2.3-.4 3.3-1 .5-.3.8-.8.8-1.4s-.2-1.1-.6-1.5c-.7-.7-1.9-1.5-3.5-1.5-1.1 0-2.1.3-3 .9.2-1.2.4-2.4.8-3.6.9.8 2 1.3 3.1 1.4h.5c1 0 2-.3 2.9-.8.5-.3.8-.8.9-1.3.1-.6-.1-1.1-.5-1.5-.7-.7-1.8-1.6-3.3-1.7-.8-.1-1.7.1-2.5.4.4-1 .9-2 1.5-3 .1-.1.1-.2.2-.4.6.9 1.6 1.7 2.7 2 .6.2 1.2.3 1.8.3s1.1-.1 1.6-.2c.5-.2 1-.6 1.2-1.1.5-.4.4-1 .1-1.5zm-15.2 4.9c-.9-.5-1.5-1.3-1.9-2.3-.1-.4.1-.9.5-1.1h.8c.8 0 1.6.2 2.3.6 1.5.9 1.9 2.6 2 3.3-1.2.3-2.6.1-3.7-.5zm-1.5 7.7c-1-.2-1.9-.7-2.6-1.6-.3-.4-.2-.9.1-1.2l.1-.1c.7-.3 1.4-.5 2.2-.5.3 0 .5 0 .8.1 1.7.3 2.7 1.8 3 2.5-.9.8-2.3 1.1-3.6.8zm8.8-1.3c1.3 0 2.2.6 2.8 1.2.3.3.4.9 0 1.2 0 0-.1.1-.2.1-.7.6-1.6.9-2.6.9-1.6 0-2.6-.8-3.2-1.5 0-.2.1-.4.1-.6.6-.5 1.7-1.3 3.1-1.3zm1.4-6.6c1.2.1 2.2.8 2.7 1.4.2.2.2.4.2.7s-.2.5-.4.6c-.9.5-1.9.7-2.9.6-1.1-.1-2.2-.7-2.9-1.5v-.1c.1-.3.2-.7.3-1 .9-.5 1.9-.8 3-.7zm-1.7-5.6c-1.3.2-2.6-.2-3.6-1-.7-.6-1.2-1.3-1.4-2.2-.2-.6.2-1.2.7-1.3h.4c.9.1 1.7.4 2.4 1 1.4 1 1.5 2.7 1.5 3.5zm7.4 2.1c-.1.2-.3.4-.6.5-.9.3-1.9.3-2.9-.1-1-.3-1.9-1.1-2.4-2l.9-1.2c.9-.2 1.8-.2 2.6.1 1.2.4 1.9 1.3 2.3 2 .2.2.2.5.1.7z" fill="#ccced0"/><path d="m178.2 57.1c-.1-.5-.5-.9-1-.9-2.1 0-4.2-.2-6.3-.5s-4.2-.8-6.2-1.5-3.9-1.6-5.7-2.7-3.3-2.5-4.6-4.1l-.1-.1c-.4-.3-1-.3-1.3.1-1.2 1.4-2.8 2.8-4.3 4-1.6 1.2-3.3 2.2-5.1 3s-3.7 1.4-5.6 1.8-3.9.5-5.9.3c-.4 0-.7.3-.8.6-.7 4.8-.9 9.5-.6 14.3.3 4.7 1.2 9.5 2.9 13.9 1.7 4.5 4.3 8.6 7.8 11.9s7.8 5.5 12.4 6.7h.1c2.5-.1 4.9-.8 7.1-1.8s4.4-2.3 6.2-4c3.8-3.2 6.7-7.4 8.6-12 2-4.6 2.8-9.5 3.1-14.4s0-9.8-.7-14.6z" fill="#069e08"/><path d="m145.3 78.3 7.4 5.9 15.1-17.2" fill="#069e08"/><path d="m168.1 66.9c-.1-.1-.3-.2-.5-.1-1.5 1.2-2.9 2.6-4.2 3.9l-3.9 4.2c-2.3 2.6-4.6 5.1-6.9 7.7-1-.8-1.9-1.5-2.9-2.3l-1.9-1.5c-.7-.5-1.2-1.1-2.1-1.2h-.4c-.4.2-.6.6-.4 1 .3.9 1 1.2 1.6 1.8l1.8 1.5c1.2 1 2.4 2 3.7 3 .5.4 1.1.3 1.5-.1 2.5-2.9 5-5.7 7.5-8.6 1.2-1.5 2.5-2.9 3.7-4.4s2.3-3 3.3-4.7c.2.1.1-.1.1-.2z" fill="#fff"/><path d="m137.2 99.7c-5-.2-10-.4-15-.5s-10-.2-15-.2c-10-.1-19.9-.1-29.9-.1l-29.9-.1h-18.7c-.7 0-1.1 0-1.6-.1s-.9-.3-1.3-.6c-.8-.6-1.3-1.4-1.5-2.4 0-.2-.1-.5-.1-.7v-1.6l52.9-.1h4.8v2.4c0 .3.2.5.5.5h24.5c.3 0 .5-.2.5-.5v-2.7c3.5 0 6.9-.1 10.4-.2 4.5-.1 9-.2 13.5-.4.1 0 .2-.1.2-.3 0-.1-.1-.2-.2-.2-4.5-.2-9-.3-13.5-.4s-9-.2-13.5-.2c-9-.1-18-.1-27-.1l-54.2-.2c-.6 0-1.2.5-1.2 1.2v2.8c0 .4 0 .8.1 1.2.5 2.3 2.2 4.2 4.5 4.8.7.2 1.6.2 2.2.2h18.7l29.9-.1c10 0 19.9 0 29.9-.1 5 0 10-.1 15-.2s10-.2 15-.5c.1 0 .2-.1.2-.3s-.1-.3-.2-.3zm16.6-99.4-25.8-.1h-85.1c-.6 0-1.2.1-1.8.3-1.2.3-2.3.9-3.3 1.7-1.9 1.6-3.1 4-3 6.5v82.5h2.2v-82.4c0-2.9 1.9-5.4 4.6-6.1.4-.1.9-.2 1.4-.2h85.1l24.9-.1.2 19.7.1 10.3.2 10.3c0 .2.2.3.4.3.1 0 .3-.1.3-.3l.2-10.3.1-10.3.2-20.6c-.1-.7-.4-1.1-.9-1.2z" fill="#ccced0"/><path d="m54.3 78.8c13.6-5.6 42.9-1.9 52.5 4.1l-52.5-.2z" fill="#6F93AD"/></svg> plugin-search/plugin-search.css 0000644 00000002231 15174711637 0012572 0 ustar 00 .plugin-card-jetpack-plugin-search .plugin-action-buttons { white-space: nowrap; } .plugin-card-jetpack-plugin-search .plugin-action-buttons .jetpack-plugin-search__primary { background: #069e08; border-color: #00a523; color: #fff; box-shadow: 0 1px 0 #c5e2c3; } .plugin-card-jetpack-plugin-search .plugin-action-buttons .jetpack-plugin-search__primary:hover { background: #00a523; border-color: #008b1d; color: #fff; } .plugin-card-jetpack-plugin-search .plugin-card-bottom { display: none; } .jetpack-plugin-search__bottom { display: flex; align-items: center; align-content: space-between; clear: both; padding: 12px 20px; background-color: #f6f7f7; border-top: 1px solid #dcdcde; overflow: hidden; } .jetpack-plugin-search__text { flex: 1; margin: 0 24px 0 16px; } /* Hides the link to dismiss cards when it's in action links * are before being moved to bottom row */ .action-links .jetpack-plugin-search__dismiss { display: none; } .jetpack-plugin-search__bottom .jetpack-plugin-search__dismiss { color: #484848; font-style: italic; text-decoration: underline; cursor: pointer; } .jetpack-plugin-search__dismiss:hover { color: #646970; } plugin-search/plugin-search.js 0000644 00000016764 15174711637 0012436 0 ustar 00 /** * Handles the activation of a Jetpack feature, dismissing the card, and replacing the bottom row * of the card with customized content. */ /* global jetpackPluginSearch, jpTracksAJAX */ var JetpackPSH = {}; ( function ( $, jpsh ) { JetpackPSH = { $pluginFilter: $( '#plugin-filter' ), /** * Get parent search hint element. * @return {Element | null} */ getCard: function () { return document.querySelector( '.plugin-card-jetpack-plugin-search' ); }, /** * Track user event such as a click on a button or a link. * * @param {string} eventName Event identifier. * @param {object} feature Identifier of feature involved in the event. * @param {object} target Object where action was performed. */ trackEvent: function ( eventName, feature, target ) { jpTracksAJAX .record_ajax_event( eventName, 'click', { feature: feature } ) .always( function () { if ( 'undefined' !== typeof target && !! target.getAttribute( 'href' ) ) { // If it has an href, follow it. window.location = target.getAttribute( 'href' ); } } ); }, /** * Replace bottom row of the card to insert logo, text and link to dismiss the card. */ replaceCardBottom: function () { var hint = JetpackPSH.getCard(); if ( 'object' === typeof hint && null !== hint ) { hint.querySelector( '.plugin-card-bottom' ).outerHTML = '<div class="jetpack-plugin-search__bottom"><img src="' + jetpackPluginSearch.logo + '" width="32" />' + '<p class="jetpack-plugin-search__text">' + jetpackPluginSearch.legend + ' <a class="jetpack-plugin-search__support_link" href="' + jetpackPluginSearch.supportLink + '" target="_blank" rel="noopener noreferrer" data-track="support_link" >' + jetpackPluginSearch.supportText + '</a>' + '</p>' + '</div>'; // Remove link and parent li from action links and move it to bottom row var dismissLink = document.querySelector( '.jetpack-plugin-search__dismiss' ); dismissLink.parentNode.parentNode.removeChild( dismissLink.parentNode ); document.querySelector( '.jetpack-plugin-search__bottom' ).appendChild( dismissLink ); } }, /** * Check if plugin card list nodes changed. If there's a Jetpack PSH card, replace the bottom row. * @param {array} mutationsList */ replaceOnNewResults: function ( mutationsList ) { mutationsList.forEach( function ( mutation ) { if ( 'childList' === mutation.type && 1 === document.querySelectorAll( '.plugin-card-jetpack-plugin-search' ).length ) { JetpackPSH.replaceCardBottom(); } } ); }, dismiss: function ( moduleName ) { document.getElementById( 'the-list' ).removeChild( JetpackPSH.getCard() ); $.ajax( { url: jpsh.base_rest_url + '/hints', method: 'post', beforeSend: function ( xhr ) { xhr.setRequestHeader( 'X-WP-Nonce', jpsh.nonce ); }, data: JSON.stringify( { hint: moduleName, } ), contentType: 'application/json', dataType: 'json', } ).done( function () { JetpackPSH.trackEvent( 'wpa_plugin_search_dismiss', moduleName ); } ); }, ajaxActivateModule: function ( moduleName ) { var $moduleBtn = JetpackPSH.$pluginFilter.find( '#plugin-select-activate' ); $moduleBtn.toggleClass( 'install-now updating-message' ); $moduleBtn.prop( 'disabled', true ); $moduleBtn.text( jpsh.activating ); var data = {}; data[ moduleName ] = true; $.ajax( { url: jpsh.base_rest_url + '/settings', method: 'post', beforeSend: function ( xhr ) { xhr.setRequestHeader( 'X-WP-Nonce', jpsh.nonce ); }, data: JSON.stringify( data ), contentType: 'application/json', dataType: 'json', } ) .done( function () { JetpackPSH.updateButton( moduleName ); JetpackPSH.trackEvent( 'wpa_plugin_search_activate', moduleName ); } ) .error( function () { $moduleBtn.toggleClass( 'install-now updating-message' ); } ); }, // Remove onclick handler, disable loading spinner, update button to redirect to module settings. updateButton: function ( moduleName ) { $.ajax( { url: jpsh.base_rest_url + '/module/' + moduleName, method: 'get', beforeSend: function ( xhr ) { xhr.setRequestHeader( 'X-WP-Nonce', jpsh.nonce ); }, dataType: 'json', } ).done( function ( response ) { var $moduleBtn = JetpackPSH.$pluginFilter.find( '#plugin-select-activate' ); $moduleBtn.prop( 'onclick', null ).off( 'click' ); $moduleBtn.toggleClass( 'install-now updating-message' ); $moduleBtn.text( jpsh.activated ); setTimeout( function () { var url = 'https://jetpack.com/redirect/?source=plugin-hint-learn-' + moduleName, label = jpsh.getStarted, classes = 'jetpack-plugin-search__primary button', track = 'configure'; // If the feature has options in Jetpack admin UI, link to them. if ( response.options && 0 < Object.keys( response.options ).length ) { url = $moduleBtn.data( 'configure-url' ); label = jpsh.manageSettings; classes += ' jetpack-plugin-search__configure'; } else { // If it has no options, the Get started button will be displayed so remove the Learn more link if it's there. var learnMore = document.querySelector( '.jetpack-plugin-search__learn-more' ); learnMore.parentNode.removeChild( learnMore ); classes += ' jetpack-plugin-search__get-started'; track = 'get_started'; } $moduleBtn.replaceWith( '<a id="plugin-select-settings" class="' + classes + '" href="' + url + '" data-module="' + moduleName + '" data-track="' + track + '">' + label + '</a>' ); }, 1000 ); } ); }, /** * Start suggesting. */ init: function () { if ( JetpackPSH.$pluginFilter.length < 1 ) { return; } // Replace PSH bottom row on page load JetpackPSH.replaceCardBottom(); // Listen for changes in plugin search results var resultsObserver = new MutationObserver( JetpackPSH.replaceOnNewResults ); resultsObserver.observe( document.getElementById( 'plugin-filter' ), { childList: true } ); JetpackPSH.$pluginFilter .on( 'click', '.jetpack-plugin-search__dismiss', function ( event ) { event.preventDefault(); JetpackPSH.dismiss( $( this ).data( 'module' ) ); } ) .on( 'click', 'button#plugin-select-activate', function ( event ) { event.preventDefault(); JetpackPSH.ajaxActivateModule( $( this ).data( 'module' ) ); } ) .on( 'click', '.jetpack-plugin-search__primary', function ( event ) { event.preventDefault(); var $this = $( this ); if ( $this.data( 'track' ) ) { // This catches Purchase, Configure, and Get started. Feature activation is tracked when it ends successfully, in its callback. JetpackPSH.trackEvent( 'wpa_plugin_search_' + $this.data( 'track' ), $this.data( 'module' ), $this.get( 0 ) ); } } ) .on( 'click', '.jetpack-plugin-search__learn-more', function ( event ) { event.preventDefault(); var $this = $( this ); JetpackPSH.trackEvent( 'wpa_plugin_search_learn_more', $this.data( 'module' ), $this.get( 0 ) ); } ) .on( 'click', '.jetpack-plugin-search__support_link', function ( event ) { event.preventDefault(); var $this = $( this ); JetpackPSH.trackEvent( 'wpa_plugin_search_support_link', $this.data( 'module' ), $this.get( 0 ) ); } ); }, }; JetpackPSH.init(); } )( jQuery, jetpackPluginSearch ); plugin-search.php 0000644 00000053151 15174711637 0010037 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Plugin Search Hints, aka Feature Suggestions. * * @since 7.1.0 * * @package automattic/jetpack */ // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files. use Automattic\Jetpack\Constants; use Automattic\Jetpack\Current_Plan as Jetpack_Plan; use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Tracking; // Disable direct access and execution. if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } if ( is_admin() && Jetpack::is_connection_ready() && /** This filter is documented in _inc/lib/admin-pages/class.jetpack-react-page.php */ apply_filters( 'jetpack_show_promotions', true ) && // Disable feature hints when plugins cannot be installed. ! Constants::is_true( 'DISALLOW_FILE_MODS' ) && jetpack_is_psh_active() ) { Jetpack_Plugin_Search::init(); } // Register endpoints when WP REST API is initialized. add_action( 'rest_api_init', array( 'Jetpack_Plugin_Search', 'register_endpoints' ) ); /** * Class that includes cards in the plugin search results when users enter terms that match some Jetpack feature. * Card can be dismissed and includes a title, description, button to enable the feature and a link for more information. * * @since 7.1.0 */ class Jetpack_Plugin_Search { /** * PSH slug name. * * @var string */ public static $slug = 'jetpack-plugin-search'; /** * Singleton constructor. * * @return Jetpack_Plugin_Search */ public static function init() { static $instance = null; if ( ! $instance ) { $instance = new Jetpack_Plugin_Search(); } return $instance; } /** * Jetpack_Plugin_Search constructor. */ public function __construct() { add_action( 'current_screen', array( $this, 'start' ) ); } /** * Add actions and filters only if this is the plugin installation screen and it's the first page. * * @param object $screen WP SCreen object. * * @since 7.1.0 */ public function start( $screen ) { if ( 'plugin-install' === $screen->base && ( ! isset( $_GET['paged'] ) || 1 === intval( $_GET['paged'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended add_action( 'admin_enqueue_scripts', array( $this, 'load_plugins_search_script' ) ); add_filter( 'plugins_api_result', array( $this, 'inject_jetpack_module_suggestion' ), 10, 3 ); add_filter( 'self_admin_url', array( $this, 'plugin_details' ) ); add_filter( 'plugin_install_action_links', array( $this, 'insert_module_related_links' ), 10, 2 ); } } /** * Modify URL used to fetch to plugin information so it pulls Jetpack plugin page. * * @param string $url URL to load in dialog pulling the plugin page from wporg. * * @since 7.1.0 * * @return string The URL with 'jetpack' instead of 'jetpack-plugin-search'. */ public function plugin_details( $url ) { return false !== stripos( $url, 'tab=plugin-information&plugin=' . self::$slug ) ? 'plugin-install.php?tab=plugin-information&plugin=jetpack&TB_iframe=true&width=600&height=550' : $url; } /** * Register REST API endpoints. * * @since 7.1.0 */ public static function register_endpoints() { register_rest_route( 'jetpack/v4', '/hints', array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => __CLASS__ . '::dismiss', 'permission_callback' => __CLASS__ . '::can_request', 'args' => array( 'hint' => array( 'default' => '', 'type' => 'string', 'required' => true, 'validate_callback' => __CLASS__ . '::is_hint_id', ), ), ) ); } /** * A WordPress REST API permission callback method that accepts a request object and * decides if the current user has enough privileges to act. * * @since 7.1.0 * * @return bool does a current user have enough privileges. */ public static function can_request() { return current_user_can( 'jetpack_admin_page' ); } /** * Validates that the ID of the hint to dismiss is a string. * * @since 7.1.0 * * @param string|bool $value Value to check. * @param WP_REST_Request $request The request sent to the WP REST API. * @param string $param Name of the parameter passed to endpoint holding $value. * * @return bool|WP_Error */ public static function is_hint_id( $value, $request, $param ) { return in_array( $value, Jetpack::get_available_modules(), true ) ? true /* translators: %s is the name of a parameter passed to an endpoint. */ : new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string.', 'jetpack' ), $param ) ); } /** * A WordPress REST API callback method that accepts a request object and decides what to do with it. * * @param WP_REST_Request $request { * Array of parameters received by request. * * @type string $hint Slug of card to dismiss. * } * * @since 7.1.0 * * @return bool|array|WP_Error a resulting value or object, or an error. */ public static function dismiss( WP_REST_Request $request ) { return self::add_to_dismissed_hints( $request['hint'] ) ? rest_ensure_response( array( 'code' => 'success' ) ) : new WP_Error( 'not_dismissed', esc_html__( 'The card could not be dismissed', 'jetpack' ), array( 'status' => 400 ) ); } /** * Returns a list of previously dismissed hints. * * @since 7.1.0 * * @return array List of dismissed hints. */ protected static function get_dismissed_hints() { $dismissed_hints = Jetpack_Options::get_option( 'dismissed_hints' ); return isset( $dismissed_hints ) && is_array( $dismissed_hints ) ? $dismissed_hints : array(); } /** * Save the hint in the list of dismissed hints. * * @since 7.1.0 * * @param string $hint The hint id, which is a Jetpack module slug. * * @return bool Whether the card was added to the list and hence dismissed. */ protected static function add_to_dismissed_hints( $hint ) { return Jetpack_Options::update_option( 'dismissed_hints', array_merge( self::get_dismissed_hints(), array( $hint ) ) ); } /** * Checks that the module slug passed should be displayed. * * A feature hint will be displayed if it has not been dismissed before or if 2 or fewer other hints have been dismissed. * * @since 7.2.1 * * @param string $hint The hint id, which is a Jetpack module slug. * * @return bool True if $hint should be displayed. */ protected function should_display_hint( $hint ) { $dismissed_hints = static::get_dismissed_hints(); // If more than 2 hints have been dismissed, then show no more. if ( 2 < count( $dismissed_hints ) ) { return false; } $plan = Jetpack_Plan::get(); if ( isset( $plan['class'] ) && ( 'free' === $plan['class'] || 'personal' === $plan['class'] ) && 'vaultpress' === $hint ) { return false; } return ! in_array( $hint, $dismissed_hints, true ); } /** * Load the search scripts and CSS for PSH. */ public function load_plugins_search_script() { wp_enqueue_script( self::$slug, plugins_url( 'modules/plugin-search/plugin-search.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), JETPACK__VERSION, true ); wp_localize_script( self::$slug, 'jetpackPluginSearch', array( 'nonce' => wp_create_nonce( 'wp_rest' ), 'base_rest_url' => rest_url( '/jetpack/v4' ), 'manageSettings' => esc_html__( 'Configure', 'jetpack' ), 'activateModule' => esc_html__( 'Activate Module', 'jetpack' ), 'getStarted' => esc_html__( 'Get started', 'jetpack' ), 'activated' => esc_html__( 'Activated', 'jetpack' ), 'activating' => esc_html__( 'Activating', 'jetpack' ), 'logo' => 'https://ps.w.org/jetpack/assets/icon.svg?rev=1791404', 'legend' => esc_html__( 'This suggestion was made by Jetpack, the security and performance plugin already installed on your site.', 'jetpack' ), 'supportText' => esc_html__( 'Learn more about these suggestions.', 'jetpack' ), 'supportLink' => Redirect::get_url( 'plugin-hint-learn-support' ), 'hideText' => esc_html__( 'Hide this suggestion', 'jetpack' ), ) ); wp_enqueue_style( self::$slug, plugins_url( 'modules/plugin-search/plugin-search.css', JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION ); } /** * Get the plugin repo's data for Jetpack to populate the fields with. * * @return array|mixed|object|WP_Error */ public static function get_jetpack_plugin_data() { $data = get_transient( 'jetpack_plugin_data' ); if ( false === $data || is_wp_error( $data ) ) { include_once ABSPATH . 'wp-admin/includes/plugin-install.php'; $data = plugins_api( 'plugin_information', array( 'slug' => 'jetpack', 'is_ssl' => is_ssl(), 'fields' => array( 'banners' => true, 'reviews' => true, 'active_installs' => true, 'versions' => false, 'sections' => false, ), ) ); set_transient( 'jetpack_plugin_data', $data, DAY_IN_SECONDS ); } return $data; } /** * Create a list with additional features for those we don't have a module, like Akismet. * * @since 7.1.0 * * @return array List of features. */ public function get_extra_features() { return array( 'akismet' => array( 'name' => 'Akismet', 'search_terms' => 'akismet, anti-spam, antispam, comments, spam, spam protection, form spam, captcha, no captcha, nocaptcha, recaptcha, phising, google', 'short_description' => esc_html__( 'Keep your visitors and search engines happy by stopping comment and contact form spam with Akismet.', 'jetpack' ), 'requires_connection' => true, 'module' => 'akismet', 'sort' => '16', 'learn_more_button' => Redirect::get_url( 'plugin-hint-upgrade-akismet' ), 'configure_url' => admin_url( 'admin.php?page=akismet-key-config' ), ), 'sharing-block' => array( 'name' => esc_html__( 'Sharing buttons block', 'jetpack' ), 'search_terms' => 'share, sharing, sharing block, sharing button, social buttons, buttons, share facebook, share twitter, social share, icons, email, facebook, twitter, x, linkedin, pinterest, social media', 'short_description' => esc_html__( 'Add sharing buttons blocks anywhere on your website to help your visitors share your content.', 'jetpack' ), 'requires_connection' => false, 'module' => 'sharing-block', 'sort' => '13', 'learn_more_button' => Redirect::get_url( 'jetpack-support-sharing-block' ), 'configure_url' => admin_url( 'site-editor.php?path=%2Fwp_template' ), ), ); } /** * Intercept the plugins API response and add in an appropriate card for Jetpack * * @param object $result Plugin search results. * @param string $action unused. * @param object $args Search args. */ public function inject_jetpack_module_suggestion( $result, $action, $args ) { /* * Bail if something else hooks into the Plugins' API response * and does not return results. */ if ( empty( $result->plugins ) || is_wp_error( $result ) ) { return $result; } // Looks like a search query; it's matching time. if ( ! empty( $args->search ) ) { $searchable_modules = array( 'contact-form', 'monitor', 'photon', 'photon-cdn', 'protect', 'publicize', 'related-posts', 'akismet', 'vaultpress', 'videopress', 'search', ); /* * Let's handle the Sharing feature differently. * If we're using a block-based theme, we should suggest the sharing block. * If using a classic theme, we should suggest the old sharing module. */ if ( wp_is_block_theme() ) { $searchable_modules[] = 'sharing-block'; } else { $searchable_modules[] = 'sharedaddy'; } require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; $tracking = new Tracking(); $jetpack_modules_list = array_intersect_key( array_merge( $this->get_extra_features(), Jetpack_Admin::init()->get_modules() ), array_flip( $searchable_modules ) ); uasort( $jetpack_modules_list, array( $this, 'by_sorting_option' ) ); // Record event when user searches for a term over 3 chars (less than 3 is not very useful). if ( strlen( $args->search ) >= 3 ) { $tracking->record_user_event( 'wpa_plugin_search_term', array( 'search_term' => $args->search ) ); } // Lowercase, trim, remove punctuation/special chars, decode url, remove 'jetpack'. $normalized_term = $this->sanitize_search_term( $args->search ); $matching_module = null; // Try to match a passed search term with module's search terms. foreach ( $jetpack_modules_list as $module_slug => $module_opts ) { /* * Does the site's current plan support the feature? * We don't use Jetpack_Plan::supports() here because * that check always returns Akismet as supported, * since Akismet has a free version. */ $current_plan = Jetpack_Plan::get(); $is_supported_by_plan = in_array( $module_slug, $current_plan['supports'], true ); if ( false !== stripos( $module_opts['search_terms'] . ', ' . $module_opts['name'], $normalized_term ) && $is_supported_by_plan ) { $matching_module = $module_slug; break; } } if ( isset( $matching_module ) && $this->should_display_hint( $matching_module ) ) { // Record event when a matching feature is found. $tracking->record_user_event( 'wpa_plugin_search_match_found', array( 'feature' => $matching_module ) ); $inject = (array) self::get_jetpack_plugin_data(); $image_url = plugins_url( 'modules/plugin-search/psh', JETPACK__PLUGIN_FILE ); $overrides = array( 'plugin-search' => true, // Helps to determine if that an injected card. 'name' => sprintf( // Supplement name/description so that they clearly indicate this was added. /* translators: Jetpack module name */ esc_html_x( 'Jetpack: %s', 'Jetpack: Module Name', 'jetpack' ), $jetpack_modules_list[ $matching_module ]['name'] ), 'short_description' => $jetpack_modules_list[ $matching_module ]['short_description'], 'author' => esc_attr__( 'Jetpack (installed)', 'jetpack' ), 'requires_connection' => (bool) $jetpack_modules_list[ $matching_module ]['requires_connection'], 'slug' => self::$slug, 'version' => JETPACK__VERSION, 'icons' => array( '1x' => "$image_url-128.png", '2x' => "$image_url-256.png", 'svg' => "$image_url.svg", ), ); // Splice in the base module data. $inject = array_merge( $inject, $jetpack_modules_list[ $matching_module ], $overrides ); // Add it to the top of the list. $result->plugins = array_filter( $result->plugins, array( $this, 'filter_cards' ) ); array_unshift( $result->plugins, $inject ); } } return $result; } /** * Remove cards for Jetpack plugins since we don't want duplicates. * * @since 7.1.0 * @since 7.2.0 Only remove Jetpack. * @since 7.4.0 Simplify for WordPress 5.1+. * * @param array|object $plugin WordPress search result card. * * @return bool */ public function filter_cards( $plugin ) { /* * $plugin is normally an array. * However, since the response data can be filtered, * we cannot fully trust its format. * Let's handle both arrays and objects, and bail if it's neither. */ if ( is_array( $plugin ) && ! empty( $plugin['slug'] ) ) { $slug = $plugin['slug']; } elseif ( is_object( $plugin ) && ! empty( $plugin->slug ) ) { $slug = $plugin->slug; } else { return false; } return ! in_array( $slug, array( 'jetpack' ), true ); } /** * Take a raw search query and return something a bit more standardized and * easy to work with. * * @param string $term The raw search term. * @return string A simplified/sanitized version. */ private function sanitize_search_term( $term ) { $term = strtolower( urldecode( $term ) ); // remove non-alpha/space chars. $term = preg_replace( '/[^a-z ]/', '', $term ); // remove strings that don't help matches. $term = trim( str_replace( array( 'jetpack', 'jp', 'free', 'wordpress' ), '', $term ) ); return $term; } /** * Callback function to sort the array of modules by the sort option. * * @param array $m1 Array 1 to sort. * @param array $m2 Array 2 to sort. */ private function by_sorting_option( $m1, $m2 ) { return $m1['sort'] <=> $m2['sort']; } /** * Modify the URL to the feature settings, for example Publicize. * Sharing is included here because while we still have a page in WP Admin, * we prefer to send users to Calypso. * * @param string $feature Feature. * @param string $configure_url URL to configure feature. * * @return string * @since 7.1.0 */ private function get_configure_url( $feature, $configure_url ) { switch ( $feature ) { case 'sharing': case 'publicize': $configure_url = Redirect::get_url( 'calypso-marketing-connections' ); break; case 'seo-tools': $configure_url = Redirect::get_url( 'calypso-marketing-traffic', array( 'anchor' => 'seo', ) ); break; case 'google-analytics': $configure_url = Redirect::get_url( 'calypso-marketing-traffic', array( 'anchor' => 'analytics', ) ); break; case 'wordads': $configure_url = Redirect::get_url( 'wpcom-ads-settings' ); break; } return $configure_url; } /** * Put some more appropriate links on our custom result cards. * * @param array $links Related links. * @param array $plugin Plugin result information. */ public function insert_module_related_links( $links, $plugin ) { if ( self::$slug !== $plugin['slug'] ) { return $links; } // By the time this filter is applied, self_admin_url was already applied and we don't need it anymore. remove_filter( 'self_admin_url', array( $this, 'plugin_details' ) ); $links = array(); if ( 'sharing-block' === $plugin['module'] ) { $links['jp_get_started'] = '<a id="plugin-select-settings" class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button" href="' . esc_url( admin_url( 'site-editor.php?path=%2Fwp_template' ) ) . '" data-module="' . esc_attr( $plugin['module'] ) . '" data-track="get_started" >' . esc_html__( 'Add block', 'jetpack' ) . '</a>'; } elseif ( 'akismet' === $plugin['module'] || 'vaultpress' === $plugin['module'] ) { $links['jp_get_started'] = '<a id="plugin-select-settings" class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button" href="' . esc_url( Redirect::get_url( 'plugin-hint-learn-' . $plugin['module'] ) ) . '" data-module="' . esc_attr( $plugin['module'] ) . '" data-track="get_started" >' . esc_html__( 'Get started', 'jetpack' ) . '</a>'; // Jetpack installed, active, feature not enabled; prompt to enable. } elseif ( current_user_can( 'jetpack_activate_modules' ) && ! Jetpack::is_module_active( $plugin['module'] ) && Jetpack_Plan::supports( $plugin['module'] ) ) { $links[] = '<button id="plugin-select-activate" class="jetpack-plugin-search__primary button" data-module="' . esc_attr( $plugin['module'] ) . '" data-configure-url="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '" > ' . esc_html__( 'Enable', 'jetpack' ) . '</button>'; // Jetpack installed, active, feature enabled; link to settings. } elseif ( ! empty( $plugin['configure_url'] ) && current_user_can( 'jetpack_configure_modules' ) && Jetpack::is_module_active( $plugin['module'] ) && /** This filter is documented in class.jetpack-admin.php */ apply_filters( 'jetpack_module_configurable_' . $plugin['module'], false ) ) { $links[] = '<a id="plugin-select-settings" class="jetpack-plugin-search__primary button jetpack-plugin-search__configure" href="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '" data-module="' . esc_attr( $plugin['module'] ) . '" data-track="configure" >' . esc_html__( 'Configure', 'jetpack' ) . '</a>'; // Module is active, doesn't have options to configure. } elseif ( Jetpack::is_module_active( $plugin['module'] ) ) { $links['jp_get_started'] = '<a id="plugin-select-settings" class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button" href="' . esc_url( Redirect::get_url( 'plugin-hint-learn-' . $plugin['module'] ) ) . '" data-module="' . esc_attr( $plugin['module'] ) . '" data-track="get_started" >' . esc_html__( 'Get started', 'jetpack' ) . '</a>'; } // Add link pointing to a relevant doc page in jetpack.com only if the Get started button isn't displayed. if ( ! empty( $plugin['learn_more_button'] ) && ! isset( $links['jp_get_started'] ) ) { $links[] = '<a class="jetpack-plugin-search__learn-more" href="' . esc_url( $plugin['learn_more_button'] ) . '" target="_blank" data-module="' . esc_attr( $plugin['module'] ) . '" data-track="learn_more" >' . esc_html__( 'Learn more', 'jetpack' ) . '</a>'; } // Dismiss link. $links[] = '<a class="jetpack-plugin-search__dismiss" data-module="' . esc_attr( $plugin['module'] ) . '" >' . esc_html__( 'Hide this suggestion', 'jetpack' ) . '</a>'; return $links; } } /** * Master control that checks if Plugin search hints is active. * * @since 7.1.1 * * @return bool True if PSH is active. */ function jetpack_is_psh_active() { /** * Disables the Plugin Search Hints feature found when searching the plugins page. * * @since 8.7.0 * * @param bool Set false to disable the feature. */ return apply_filters( 'jetpack_psh_active', true ); } woocommerce-analytics/class-jetpack-woocommerce-analytics.php 0000644 00000010212 15174711637 0020617 0 ustar 00 <?php /** * Jetpack_WooCommerce_Analytics is ported from the Jetpack_Google_Analytics code. * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } require __DIR__ . '/classes/class-jetpack-woocommerce-analytics-trait.php'; require_once __DIR__ . '/classes/class-jetpack-woocommerce-analytics-universal.php'; require_once __DIR__ . '/classes/class-jetpack-woocommerce-analytics-my-account.php'; require_once __DIR__ . '/classes/class-jetpack-woocommerce-analytics-checkout-flow.php'; /** * Class Jetpack_WooCommerce_Analytics * Instantiate WooCommerce Analytics */ class Jetpack_WooCommerce_Analytics { /** * Instance of this class * * @var Jetpack_WooCommerce_Analytics - Static property to hold our singleton instance */ private static $instance = false; /** * Instance of the Universal functions * * @var Static property to hold concrete analytics implementation that does the work (universal or legacy) */ private static $analytics = false; /** * Instance of the My account functions * * @var Static property to hold concrete analytics implementation that does the work. */ private static $myaccount = false; /** * Instance of the Checkout Flow functions * * @var Static property to hold concrete analytics implementation that does the work. */ private static $views = false; /** * WooCommerce Analytics is only available to Jetpack connected WooCommerce stores with both plugins set to active * and WooCommerce version 3.0 or higher * * @return bool */ public static function should_track_store() { /** * Make sure WooCommerce is installed and active * * This action is documented in https://docs.woocommerce.com/document/create-a-plugin */ if ( ! in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', Jetpack::get_active_plugins() ), true ) ) { return false; } // Tracking only Site pages. if ( is_admin() ) { return false; } // Make sure Jetpack is installed and connected. if ( ! Jetpack::is_connection_ready() ) { return false; } // Ensure the WooCommerce class exists and is a valid version. $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, '3.0', '>=' ); if ( ! $minimum_woocommerce_active ) { return false; } return true; } /** * This is our constructor, which is private to force the use of get_instance() * * @return void */ private function __construct() { // loading _wca. add_action( 'wp_head', array( $this, 'wp_head_top' ), 1 ); // loading s.js. add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_tracking_script' ) ); self::$analytics = new Jetpack_WooCommerce_Analytics_Universal(); self::$myaccount = new Jetpack_WooCommerce_Analytics_My_Account(); if ( class_exists( 'Automattic\WooCommerce\Blocks\Package' ) && version_compare( Automattic\WooCommerce\Blocks\Package::get_version(), '11.6.2', '>=' ) ) { self::$views = new Jetpack_WooCommerce_Analytics_Checkout_Flow(); } } /** * Make _wca available to queue events */ public function wp_head_top() { if ( is_cart() || is_checkout() || is_checkout_pay_page() || is_order_received_page() || is_add_payment_method_page() ) { echo '<script>window._wca_prevent_referrer = true;</script>' . "\r\n"; } echo '<script>window._wca = window._wca || [];</script>' . "\r\n"; } /** * Place script to call s.js, Store Analytics. */ public function enqueue_tracking_script() { $url = sprintf( 'https://stats.wp.com/s-%d.js', gmdate( 'YW' ) ); wp_enqueue_script( 'woocommerce-analytics', $url, array(), null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- The version is set in the URL. array( 'in_footer' => false, 'strategy' => 'defer', ) ); } /** * Function to instantiate our class and make it a singleton */ public static function get_instance() { if ( ! self::should_track_store() ) { return; } if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } } global $jetpack_woocommerce_analytics; $jetpack_woocommerce_analytics = Jetpack_WooCommerce_Analytics::get_instance(); woocommerce-analytics/classes/class-jetpack-woocommerce-analytics-universal.php 0000644 00000041272 15174711637 0024274 0 ustar 00 <?php /** * Jetpack_WooCommerce_Analytics_Universal * * @deprecated 13.3 * * @package automattic/jetpack * @author Automattic */ /** * Bail if accessed directly */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Class Jetpack_WooCommerce_Analytics_Universal * Filters and Actions added to Store pages to perform analytics * * @deprecated 13.3 */ class Jetpack_WooCommerce_Analytics_Universal { /** * Trait to handle common analytics functions. */ use Jetpack_WooCommerce_Analytics_Trait; /** * Jetpack_WooCommerce_Analytics_Universal constructor. * * @deprecated 13.3 */ public function __construct() { $this->find_cart_checkout_content_sources(); $this->additional_blocks_on_cart_page = $this->get_additional_blocks_on_page( 'cart' ); $this->additional_blocks_on_checkout_page = $this->get_additional_blocks_on_page( 'checkout' ); // add to carts from non-product pages or lists -- search, store etc. add_action( 'wp_head', array( $this, 'loop_session_events' ), 2 ); // Capture cart events. add_action( 'woocommerce_add_to_cart', array( $this, 'capture_add_to_cart' ), 10, 6 ); add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart' ) ); add_action( 'woocommerce_after_mini_cart', array( $this, 'remove_from_cart' ) ); add_action( 'wcct_before_cart_widget', array( $this, 'remove_from_cart' ) ); add_filter( 'woocommerce_cart_item_remove_link', array( $this, 'remove_from_cart_attributes' ), 10, 2 ); // Checkout. // Send events after checkout template (shortcode). add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) ); // Send events after checkout block. add_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after', array( $this, 'checkout_process' ) ); // order confirmed. add_action( 'woocommerce_thankyou', array( $this, 'order_process' ), 10, 1 ); add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart_via_quantity' ), 10, 1 ); add_filter( 'woocommerce_checkout_posted_data', array( $this, 'save_checkout_post_data' ), 10, 1 ); add_action( 'woocommerce_created_customer', array( $this, 'capture_created_customer' ), 10, 2 ); } /** * On product lists or other non-product pages, add an event listener to "Add to Cart" button click * * @deprecated 13.3 */ public function loop_session_events() { // Check for previous events queued in session data. if ( is_object( WC()->session ) ) { $data = WC()->session->get( 'wca_session_data' ); if ( ! empty( $data ) ) { foreach ( $data as $data_instance ) { $this->record_event( $data_instance['event'], array( 'pq' => $data_instance['quantity'], ), $data_instance['product_id'] ); } // Clear data, now that these events have been recorded. WC()->session->set( 'wca_session_data', '' ); } } } /** * On the cart page, add an event listener for removal of product click * * @deprecated 13.3 */ public function remove_from_cart() { $common_props = wp_json_encode( array_merge( array( '_en' => 'woocommerceanalytics_remove_from_cart', ), $this->get_common_properties() ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); // We listen at div.woocommerce because the cart 'form' contents get forcibly // updated and subsequent removals from cart would then not have this click // handler attached. wc_enqueue_js( "jQuery( 'div.woocommerce' ).on( 'click', 'a.remove', function() { var productID = jQuery( this ).data( 'product_id' ); var quantity = jQuery( this ).parent().parent().find( '.qty' ).val() var common_props = $common_props; common_props.pi = productID; common_props.pq = quantity ? quantity : '1'; _wca.push( common_props ); } );" ); } /** * Adds the product ID to the remove product link (for use by remove_from_cart above) if not present * * @deprecated 13.3 * * @param string $url Full HTML a tag of the link to remove an item from the cart. * @param string $key Unique Key ID for a cart item. * * @return string */ public function remove_from_cart_attributes( $url, $key ) { if ( str_contains( $url, 'data-product_id' ) ) { return $url; } $item = WC()->cart->get_cart_item( $key ); $product = $item['data']; $new_attributes = sprintf( '" data-product_id="%s">', esc_attr( $product->get_id() ) ); $url = str_replace( '">', $new_attributes, $url ); return $url; } /** * Get the selected shipping option for a cart item. If the name cannot be found in the options table, the method's * ID will be used. * * @deprecated 13.3 * * @param string $cart_item_key the cart item key. * * @return mixed|bool */ public function get_shipping_option_for_item( $cart_item_key ) { $packages = wc()->shipping()->get_packages(); $selected_options = wc()->session->get( 'chosen_shipping_methods' ); if ( ! is_array( $packages ) || ! is_array( $selected_options ) ) { return false; } foreach ( $packages as $package_id => $package ) { if ( ! isset( $package['contents'] ) || ! is_array( $package['contents'] ) ) { return false; } foreach ( $package['contents'] as $package_item ) { if ( ! isset( $package_item['key'] ) || $package_item['key'] !== $cart_item_key || ! isset( $selected_options[ $package_id ] ) ) { continue; } $selected_rate_id = $selected_options[ $package_id ]; $method_key_id = sanitize_text_field( str_replace( ':', '_', $selected_rate_id ) ); $option_name = 'woocommerce_' . $method_key_id . '_settings'; $option_value = get_option( $option_name ); $title = ''; if ( is_array( $option_value ) && isset( $option_value['title'] ) ) { $title = $option_value['title']; } if ( ! $title ) { return $selected_rate_id; } return $title; } } return false; } /** * On the Checkout page, trigger an event for each product in the cart * * @deprecated 13.3 */ public function checkout_process() { global $post; $checkout_page_id = wc_get_page_id( 'checkout' ); $cart = WC()->cart->get_cart(); $enabled_payment_options = array_filter( WC()->payment_gateways->get_available_payment_gateways(), function ( $payment_gateway ) { if ( ! $payment_gateway instanceof WC_Payment_Gateway ) { return false; } return $payment_gateway->is_available(); } ); $enabled_payment_options = array_keys( $enabled_payment_options ); $is_in_checkout_page = $checkout_page_id === $post->ID ? 'Yes' : 'No'; $session = WC()->session; if ( is_object( $session ) ) { $session->set( 'checkout_page_used', true ); $session->save_data(); } foreach ( $cart as $cart_item_key => $cart_item ) { /** * This filter is already documented in woocommerce/templates/cart/cart.php */ $product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); if ( ! $product || ! $product instanceof WC_Product ) { continue; } $data = $this->get_cart_checkout_shared_data(); $data['from_checkout'] = $is_in_checkout_page; if ( ! empty( $data['products'] ) ) { unset( $data['products'] ); } if ( ! empty( $data['shipping_options_count'] ) ) { unset( $data['shipping_options_count'] ); } $data['pq'] = $cart_item['quantity']; $properties = $this->process_event_properties( 'woocommerceanalytics_product_checkout', $data, $product->get_id() ); wc_enqueue_js( " var cartItem_{$cart_item_key}_logged = false; var properties = {$properties}; // Check if jQuery is available if ( typeof jQuery !== 'undefined' ) { // This is only triggered on the checkout shortcode. jQuery( document.body ).on( 'init_checkout', function () { if ( true === cartItem_{$cart_item_key}_logged ) { return; } wp.hooks.addAction( 'wcpay.payment-request.availability', 'wcpay', function ( args ) { properties.express_checkout = args.paymentRequestType; } ); properties.checkout_page_contains_checkout_block = '0'; properties.checkout_page_contains_checkout_shortcode = '1'; _wca.push( properties ); cartItem_{$cart_item_key}_logged = true; } ); } if ( typeof wp !== 'undefined' && typeof wp.data !== 'undefined' && typeof wp.data.subscribe !== 'undefined' ) { wp.data.subscribe( function () { if ( true === cartItem_{$cart_item_key}_logged ) { return; } const checkoutDataStore = wp.data.select( 'wc/store/checkout' ); // Ensures we're not in Cart, but in Checkout page. if ( typeof checkoutDataStore !== 'undefined' && checkoutDataStore.getOrderId() !== 0 ) { properties.express_checkout = Object.keys( wc.wcBlocksRegistry.getExpressPaymentMethods() ); properties.checkout_page_contains_checkout_block = '1'; properties.checkout_page_contains_checkout_shortcode = '0'; _wca.push( properties ); cartItem_{$cart_item_key}_logged = true; } } ); } " ); } } /** * After the checkout process, fire an event for each item in the order * * @deprecated 13.3 * * @param string $order_id Order Id. */ public function order_process( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order || ! $order instanceof WC_Order ) { return; } $payment_option = $order->get_payment_method(); if ( is_object( WC()->session ) ) { $create_account = true === WC()->session->get( 'wc_checkout_createaccount_used' ) ? 'Yes' : 'No'; $checkout_page_used = true === WC()->session->get( 'checkout_page_used' ) ? 'Yes' : 'No'; } else { $create_account = 'No'; $checkout_page_used = 'No'; } $guest_checkout = $order->get_user() ? 'No' : 'Yes'; $express_checkout = 'null'; // When the payment option is woocommerce_payment // See if Google Pay or Apple Pay was used. if ( 'woocommerce_payments' === $payment_option ) { $payment_option_title = $order->get_payment_method_title(); if ( 'Google Pay (WooCommerce Payments)' === $payment_option_title ) { $express_checkout = array( 'google_pay' ); } elseif ( 'Apple Pay (WooCommerce Payments)' === $payment_option_title ) { $express_checkout = array( 'apple_pay' ); } } $checkout_page_contains_checkout_block = '0'; $checkout_page_contains_checkout_shortcode = '0'; $order_source = $order->get_created_via(); if ( 'store-api' === $order_source ) { $checkout_page_contains_checkout_block = '1'; } elseif ( 'checkout' === $order_source ) { $checkout_page_contains_checkout_shortcode = '1'; } // loop through products in the order and queue a purchase event. foreach ( $order->get_items() as $order_item ) { // @phan-suppress-next-line PhanUndeclaredMethod -- Checked before being called. See also https://github.com/phan/phan/issues/1204. $product_id = is_callable( array( $order_item, 'get_product_id' ) ) ? $order_item->get_product_id() : -1; $order_items = $order->get_items(); $order_items_count = 0; if ( is_array( $order_items ) ) { $order_items_count = count( $order_items ); } $order_coupons = $order->get_coupons(); $order_coupons_count = 0; if ( is_array( $order_coupons ) ) { $order_coupons_count = count( $order_coupons ); } $this->record_event( 'woocommerceanalytics_product_purchase', array( 'oi' => $order->get_order_number(), 'pq' => $order_item->get_quantity(), 'payment_option' => $payment_option, 'create_account' => $create_account, 'guest_checkout' => $guest_checkout, 'express_checkout' => $express_checkout, 'products_count' => $order_items_count, 'coupon_used' => $order_coupons_count, 'order_value' => $order->get_total(), 'from_checkout' => $checkout_page_used, 'checkout_page_contains_checkout_block' => $checkout_page_contains_checkout_block, 'checkout_page_contains_checkout_shortcode' => $checkout_page_contains_checkout_shortcode, ), $product_id ); } } /** * Listen for clicks on the "Update Cart" button to know if an item has been removed by * updating its quantity to zero * * @deprecated 13.3 */ public function remove_from_cart_via_quantity() { $common_props = wp_json_encode( array_merge( array( '_en' => 'woocommerceanalytics_remove_from_cart', ), $this->get_common_properties() ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); wc_enqueue_js( " jQuery( 'button[name=update_cart]' ).on( 'click', function() { var cartItems = jQuery( '.cart_item' ); cartItems.each( function( item ) { var qty = jQuery( this ).find( 'input.qty' ); if ( qty && qty.val() === '0' ) { var productID = jQuery( this ).find( '.product-remove a' ).data( 'product_id' ); var common_props = $common_props; common_props.pi = productID; _wca.push( common_props ); } } ); } );" ); } /** * Gets the inner blocks of a block. * * @deprecated 13.3 * * @param array $inner_blocks The inner blocks. * * @return array */ private function get_inner_blocks( $inner_blocks ) { $block_names = array(); if ( ! empty( $inner_blocks['blockName'] ) ) { $block_names[] = $inner_blocks['blockName']; } if ( isset( $inner_blocks['innerBlocks'] ) && is_array( $inner_blocks['innerBlocks'] ) ) { $block_names = array_merge( $block_names, $this->get_inner_blocks( $inner_blocks['innerBlocks'] ) ); } return $block_names; } /** * Track adding items to the cart. * * @deprecated 13.3 * * @param string $cart_item_key Cart item key. * @param int $product_id Product added to cart. * @param int $quantity Quantity added to cart. * @param int $variation_id Product variation. * @param array $variation Variation attributes.. * @param array $cart_item_data Other cart data. */ public function capture_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $referer_postid = isset( $_SERVER['HTTP_REFERER'] ) ? url_to_postid( esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) ) : 0; // if the referring post is not a product OR the product being added is not the same as post. // (eg. related product list on single product page) then include a product view event. $product_by_referer_postid = wc_get_product( $referer_postid ); if ( ! $product_by_referer_postid instanceof WC_Product || (int) $product_id !== $referer_postid ) { $this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_product_view' ); } // add cart event to the session data. $this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_add_to_cart' ); } /** * Track in-session data. * * @deprecated 13.3 * * @param int $product_id Product ID. * @param int $quantity Quantity. * @param string $event Fired event. */ public function capture_event_in_session_data( $product_id, $quantity, $event ) { $product = wc_get_product( $product_id ); if ( ! $product instanceof WC_Product ) { return; } $quantity = ( 0 === $quantity ) ? 1 : $quantity; // check for existing data. if ( is_object( WC()->session ) ) { $data = WC()->session->get( 'wca_session_data' ); if ( empty( $data ) || ! is_array( $data ) ) { $data = array(); } } else { $data = array(); } // extract new event data. $new_data = array( 'event' => $event, 'product_id' => (string) $product_id, 'quantity' => (string) $quantity, ); // append new data. $data[] = $new_data; WC()->session->set( 'wca_session_data', $data ); } /** * Save createaccount post data to be used in $this->order_process. * * @deprecated 13.3 * * @param array $data post data from the checkout page. * * @return array */ public function save_checkout_post_data( array $data ) { $session = WC()->session; if ( is_object( $session ) ) { if ( isset( $data['createaccount'] ) && ! empty( $data['createaccount'] ) ) { $session->set( 'wc_checkout_createaccount_used', true ); $session->save_data(); } } return $data; } /** * Capture the create account event. Similar to save_checkout_post_data but works with Store API. * * @deprecated 13.3 * * @param int $customer_id Customer ID. * @param array $new_customer_data New customer data. */ public function capture_created_customer( $customer_id, $new_customer_data ) { $session = WC()->session; if ( is_object( $session ) ) { if ( str_contains( $new_customer_data['source'], 'store-api' ) ) { $session->set( 'wc_checkout_createaccount_used', true ); $session->save_data(); } } } } woocommerce-analytics/classes/class-jetpack-woocommerce-analytics-trait.php 0000644 00000044435 15174711637 0023413 0 ustar 00 <?php /** * Jetpack_WooCommerce_Analytics_Trait * * @deprecated 13.3 * * @package automattic/jetpack * @author Automattic */ /** * Bail if accessed directly */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Jetpack_WooCommerce_Analytics_Trait * Common functionality for WooCommerce Analytics classes. * * @deprecated 13.3 */ trait Jetpack_WooCommerce_Analytics_Trait { /** * Saves whether the cart/checkout templates are in use based on WC Blocks version. * * @var bool true if the templates are in use. */ protected $cart_checkout_templates_in_use; /** * The content of the cart page or where the cart page is ultimately derived from if using a template. * * @var string */ protected $cart_content_source = ''; /** * The content of the checkout page or where the cart page is ultimately derived from if using a template. * * @var string */ protected $checkout_content_source = ''; /** * Tracks any additional blocks loaded on the Cart page. * * @var array */ protected $additional_blocks_on_cart_page; /** * Tracks any additional blocks loaded on the Checkout page. * * @var array */ protected $additional_blocks_on_checkout_page; /** * Format Cart Items or Order Items to an array * * @deprecated 13.3 * * @param array|WC_Order_Item[] $items Cart Items or Order Items. */ protected function format_items_to_json( $items ) { $products = array(); foreach ( $items as $item ) { if ( $item instanceof WC_Order_Item_Product ) { $product = wc_get_product( $item->get_product_id() ); } else { $product = $item['data']; } if ( ! $product || ! $product instanceof WC_Product ) { continue; } $data = $this->get_product_details( $product ); if ( $item instanceof WC_Order_Item_Product ) { $data['pq'] = $item->get_quantity(); } else { $data['pq'] = $item['quantity']; } $products[] = $data; } return wp_json_encode( $products, JSON_UNESCAPED_SLASHES ); } /** * Get Cart/Checkout page view shared data * * @deprecated 13.3 */ protected function get_cart_checkout_shared_data() { $cart = WC()->cart; $guest_checkout = ucfirst( get_option( 'woocommerce_enable_guest_checkout', 'No' ) ); $create_account = ucfirst( get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'No' ) ); $coupons = $cart->get_coupons(); $coupon_used = 0; if ( is_countable( $coupons ) ) { $coupon_used = count( $coupons ) ? 1 : 0; } $enabled_payment_options = array_filter( WC()->payment_gateways->get_available_payment_gateways(), function ( $payment_gateway ) { if ( ! $payment_gateway instanceof WC_Payment_Gateway ) { return false; } return $payment_gateway->is_available(); } ); $enabled_payment_options = array_keys( $enabled_payment_options ); $cart_total = wc_prices_include_tax() ? $cart->get_cart_contents_total() + $cart->get_cart_contents_tax() : $cart->get_cart_contents_total(); $shared_data = array( 'products' => $this->format_items_to_json( $cart->get_cart() ), 'create_account' => $create_account, 'guest_checkout' => $guest_checkout, 'express_checkout' => 'null', // TODO: not solved yet. 'products_count' => $cart->get_cart_contents_count(), 'order_value' => $cart_total, 'shipping_options_count' => 'null', // TODO: not solved yet. 'coupon_used' => $coupon_used, 'payment_options' => $enabled_payment_options, ); return $shared_data; } /** * Gets the content of the cart/checkout page or where the cart/checkout page is ultimately derived from if using a template. * This method sets the class properties $checkout_content_source and $cart_content_source. * * @deprecated 13.3 * * @return void Does not return, but sets class properties. */ public function find_cart_checkout_content_sources() { /** * The steps we take to find the content are: * 1. Check the transient, if that contains content and is not expired, return that. * 2. Check if the cart/checkout templates are in use. If *not in use*, get the content from the pages and * return it, there is no need to dig further. * 3. If the templates *are* in use, check if the `page-content-wrapper` block is in use. If so, get the content * from the pages (same as step 2) and return it. * 4. If the templates are in use but `page-content-wrapper` is not, then get the content directly from the * template and return it. * 5. At the end of each step, assign the found content to the relevant class properties and save them in a * transient with a 1-day lifespan. This will prevent us from having to do this work on every page load. */ $cart_checkout_content_cache_transient_name = 'jetpack_woocommerce_analytics_cart_checkout_content_sources'; $transient_value = get_transient( $cart_checkout_content_cache_transient_name ); if ( false !== $transient_value && ! empty( $transient_value['checkout_content_source'] ) && ! empty( $transient_value['cart_content_source'] ) ) { $this->cart_content_source = $transient_value['cart_content_source']; $this->checkout_content_source = $transient_value['checkout_content_source']; return; } $this->cart_checkout_templates_in_use = wp_is_block_theme() && class_exists( 'Automattic\WooCommerce\Blocks\Package' ) && version_compare( Automattic\WooCommerce\Blocks\Package::get_version(), '10.6.0', '>=' ); // Cart/Checkout *pages* are in use if the templates are not in use. Return their content and do nothing else. if ( ! $this->cart_checkout_templates_in_use ) { $cart_page = get_post( wc_get_page_id( 'cart' ) ); $checkout_page = get_post( wc_get_page_id( 'checkout' ) ); if ( $cart_page && isset( $cart_page->post_content ) ) { $this->cart_content_source = $cart_page->post_content; } if ( $checkout_page && isset( $checkout_page->post_content ) ) { $this->checkout_content_source = $checkout_page->post_content; } set_transient( $cart_checkout_content_cache_transient_name, array( 'cart_content_source' => $this->cart_content_source, 'checkout_content_source' => $this->checkout_content_source, ), DAY_IN_SECONDS ); return; } // We are in a Block theme - so we need to find out if the templates are being used. if ( function_exists( 'get_block_template' ) ) { $checkout_template = get_block_template( 'woocommerce/woocommerce//page-checkout' ); $cart_template = get_block_template( 'woocommerce/woocommerce//page-cart' ); if ( ! $checkout_template ) { $checkout_template = get_block_template( 'woocommerce/woocommerce//checkout' ); } if ( ! $cart_template ) { $cart_template = get_block_template( 'woocommerce/woocommerce//cart' ); } } if ( ! empty( $checkout_template->content ) ) { // Checkout template is in use, but we need to see if the page-content-wrapper is in use, or if the template is being used directly. $this->checkout_content_source = $checkout_template->content; $is_using_page_content = str_contains( $checkout_template->content, '<!-- wp:woocommerce/page-content-wrapper {"page":"checkout"}' ); if ( $is_using_page_content ) { // The page-content-wrapper is in use, so we need to get the page content. $checkout_page = get_post( wc_get_page_id( 'checkout' ) ); if ( $checkout_page && isset( $checkout_page->post_content ) ) { $this->checkout_content_source = $checkout_page->post_content; } } } if ( ! empty( $cart_template->content ) ) { // Cart template is in use, but we need to see if the page-content-wrapper is in use, or if the template is being used directly. $this->cart_content_source = $cart_template->content; $is_using_page_content = str_contains( $cart_template->content, '<!-- wp:woocommerce/page-content-wrapper {"page":"cart"}' ); if ( $is_using_page_content ) { // The page-content-wrapper is in use, so we need to get the page content. $cart_page = get_post( wc_get_page_id( 'cart' ) ); if ( $cart_page && isset( $cart_page->post_content ) ) { $this->cart_content_source = $cart_page->post_content; } } } set_transient( $cart_checkout_content_cache_transient_name, array( 'cart_content_source' => $this->cart_content_source, 'checkout_content_source' => $this->checkout_content_source, ), DAY_IN_SECONDS ); } /** * Default event properties which should be included with all events. * * @deprecated 13.3 * * @return array Array of standard event props. */ public function get_common_properties() { $site_info = array( 'blog_id' => Jetpack::get_option( 'id' ), 'ui' => $this->get_user_id(), 'url' => home_url(), 'woo_version' => WC()->version, 'store_admin' => in_array( array( 'administrator', 'shop_manager' ), wp_get_current_user()->roles, true ) ? 1 : 0, 'device' => wp_is_mobile() ? 'mobile' : 'desktop', 'template_used' => $this->cart_checkout_templates_in_use ? '1' : '0', 'additional_blocks_on_cart_page' => $this->additional_blocks_on_cart_page, 'additional_blocks_on_checkout_page' => $this->additional_blocks_on_checkout_page, 'store_currency' => get_woocommerce_currency(), ); $cart_checkout_info = $this->get_cart_checkout_info(); return array_merge( $site_info, $cart_checkout_info ); } /** * Record an event with optional product and custom properties. * * @deprecated 13.3 * * @param string $event_name The name of the event to record. * @param array $properties Optional array of (key => value) event properties. * @param integer $product_id The id of the product relating to the event. * * @return string|void */ public function record_event( $event_name, $properties = array(), $product_id = null ) { $js = $this->process_event_properties( $event_name, $properties, $product_id ); wc_enqueue_js( "_wca.push({$js});" ); } /** * Gather relevant product information * * @deprecated 13.3 * * @param \WC_Product $product product. * @return array */ public function get_product_details( $product ) { return array( 'pi' => $product->get_id(), 'pn' => $product->get_title(), 'pc' => $this->get_product_categories_concatenated( $product ), 'pp' => $product->get_price(), 'pt' => $product->get_type(), ); } /** * Gets product categories or varation attributes as a formatted concatenated string * * @deprecated 13.3 * * @param object $product WC_Product. * @return string */ public function get_product_categories_concatenated( $product ) { if ( ! $product instanceof WC_Product ) { return ''; } $variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : ''; if ( is_array( $variation_data ) && ! empty( $variation_data ) ) { $line = wc_get_formatted_variation( $variation_data, true ); } else { $out = array(); $categories = get_the_terms( $product->get_id(), 'product_cat' ); if ( $categories ) { foreach ( $categories as $category ) { $out[] = $category->name; } } $line = implode( '/', $out ); } return $line; } /** * Compose event properties. * * @deprecated 13.3 * * @param string $event_name The name of the event to record. * @param array $properties Optional array of (key => value) event properties. * @param integer $product_id Optional id of the product relating to the event. * * @return string|void */ public function process_event_properties( $event_name, $properties = array(), $product_id = null ) { // Only set product details if we have a product id. if ( $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product instanceof WC_Product ) { return; } $product_details = $this->get_product_details( $product ); } /** * Allow defining custom event properties in WooCommerce Analytics. * * @module woocommerce-analytics * * @since 12.5 * * @param array $all_props Array of event props to be filtered. */ $all_props = apply_filters( 'jetpack_woocommerce_analytics_event_props', array_merge( $this->get_common_properties(), // We put this here to allow override of common props. $properties ) ); if ( isset( $product_details ) ) { $all_props = array_merge( $all_props, $product_details ); } return wp_json_encode( array_merge( array( '_en' => $event_name ), $all_props ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); } /** * Get the current user id * * @deprecated 13.3 * * @return int */ public function get_user_id() { if ( is_user_logged_in() ) { $blogid = Jetpack::get_option( 'id' ); $userid = get_current_user_id(); return $blogid . ':' . $userid; } return 'null'; } /** * Gets the IDs of additional blocks on the Cart/Checkout pages or templates. * * @deprecated 13.3 * * @param string $cart_or_checkout Whether to get blocks on the cart or checkout page. * @return array All inner blocks on the page. */ public function get_additional_blocks_on_page( $cart_or_checkout = 'cart' ) { $additional_blocks_on_page_transient_name = 'jetpack_woocommerce_analytics_additional_blocks_on_' . $cart_or_checkout . '_page'; $additional_blocks_on_page = get_transient( $additional_blocks_on_page_transient_name ); if ( false !== $additional_blocks_on_page ) { return $additional_blocks_on_page; } $content = $this->cart_content_source; if ( 'checkout' === $cart_or_checkout ) { $content = $this->checkout_content_source; } $parsed_blocks = parse_blocks( $content ); $other_blocks = array_filter( $parsed_blocks, function ( $block ) use ( $cart_or_checkout ) { if ( ! isset( $block['blockName'] ) ) { return false; } if ( 'woocommerce/classic-shortcode' === $block['blockName'] ) { return false; } if ( 'core/shortcode' === $block['blockName'] ) { return false; } if ( 'checkout' === $cart_or_checkout && 'woocommerce/checkout' !== $block['blockName'] ) { return true; } if ( 'cart' === $cart_or_checkout && 'woocommerce/cart' !== $block['blockName'] ) { return true; } return false; } ); $all_inner_blocks = array(); // Loop over each "block group". In templates the blocks are grouped up. foreach ( $other_blocks as $block ) { // This check is necessary because sometimes this is null when using templates. if ( ! empty( $block['blockName'] ) ) { $all_inner_blocks[] = $block['blockName']; } if ( ! isset( $block['innerBlocks'] ) || ! is_array( $block['innerBlocks'] ) || 0 === count( $block['innerBlocks'] ) ) { continue; } foreach ( $block['innerBlocks'] as $inner_content ) { $all_inner_blocks = array_merge( $all_inner_blocks, $this->get_inner_blocks( $inner_content ) ); } } set_transient( $additional_blocks_on_page_transient_name, $all_inner_blocks, DAY_IN_SECONDS ); return $all_inner_blocks; } /** * Gets an array containing the block or shortcode use properties for the Cart page. * * @deprecated 13.3 * * @return array An array containing the block or shortcode use properties for the Cart page. */ public function get_cart_page_block_usage() { $new_info = array(); $content = $this->cart_content_source; $block_presence = str_contains( $content, '<!-- wp:woocommerce/cart' ); $shortcode_presence = str_contains( $content, '[woocommerce_cart]' ); $classic_shortcode_presence = str_contains( $content, '<!-- wp:woocommerce/classic-shortcode' ); $new_info['cart_page_contains_cart_block'] = $block_presence ? '1' : '0'; $new_info['cart_page_contains_cart_shortcode'] = $shortcode_presence || $classic_shortcode_presence ? '1' : '0'; return $new_info; } /** * Gets an array containing the block or shortcode use properties for the Checkout page. * * @deprecated 13.3 * * @return array An array containing the block or shortcode use properties for the Checkout page. */ public function get_checkout_page_block_usage() { $new_info = array(); $content = $this->checkout_content_source; $block_presence = str_contains( $content, '<!-- wp:woocommerce/checkout' ); $shortcode_presence = str_contains( $content, '[woocommerce_checkout]' ); $classic_shortcode_presence = str_contains( $content, '<!-- wp:woocommerce/classic-shortcode' ); $new_info['checkout_page_contains_checkout_block'] = $block_presence ? '1' : '0'; $new_info['checkout_page_contains_checkout_shortcode'] = $shortcode_presence || $classic_shortcode_presence ? '1' : '0'; return $new_info; } /** * Get info about the cart & checkout pages, in particular * whether the store is using shortcodes or Gutenberg blocks. * This info is cached in a transient. * * Note: similar code is in a WooCommerce core PR: * https://github.com/woocommerce/woocommerce/pull/25932 * * @deprecated 13.3 * * @return array */ public function get_cart_checkout_info() { $info = array_merge( $this->get_cart_page_block_usage(), $this->get_checkout_page_block_usage() ); return $info; } /** * Search a specific post for text content. * * Note: similar code is in a WooCommerce core PR: * https://github.com/woocommerce/woocommerce/pull/25932 * * @deprecated 13.3 * * @param integer $post_id The id of the post to search. * @param string $text The text to search for. * @return integer 1 if post contains $text (otherwise 0). */ public function post_contains_text( $post_id, $text ) { global $wpdb; // Search for the text anywhere in the post. $wildcarded = "%{$text}%"; // No better way to search post content without having filters expanding blocks. // This is already cached up in the parent function. $result = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( " SELECT COUNT( * ) FROM {$wpdb->prefix}posts WHERE ID=%d AND {$wpdb->prefix}posts.post_content LIKE %s ", array( $post_id, $wildcarded ) ) ); return ( '0' !== $result ) ? 1 : 0; } } woocommerce-analytics/classes/class-jetpack-woocommerce-analytics-checkout-flow.php 0000644 00000013426 15174711637 0025036 0 ustar 00 <?php /** * Jetpack_WooCommerce_Analytics_Checkout_Flow * * @deprecated 13.3 * * @package automattic/jetpack * @author Automattic */ /** * Bail if accessed directly */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Class Jetpack_WooCommerce_Analytics_Checkout_Flow * Class that handles all page view events for the checkout flow (from product view to order confirmation view) * * @deprecated 13.3 */ class Jetpack_WooCommerce_Analytics_Checkout_Flow { use Jetpack_WooCommerce_Analytics_Trait; /** * Jetpack_WooCommerce_Analytics_Checkout_Flow constructor. * * @deprecated 13.3 */ public function __construct() { $this->find_cart_checkout_content_sources(); $this->additional_blocks_on_cart_page = $this->get_additional_blocks_on_page( 'cart' ); $this->additional_blocks_on_checkout_page = $this->get_additional_blocks_on_page( 'checkout' ); // single product page view. add_action( 'woocommerce_after_single_product', array( $this, 'capture_product_view' ) ); // order confirmed page view add_action( 'woocommerce_thankyou', array( $this, 'capture_order_confirmation_view' ), 10, 1 ); // cart page view add_action( 'wp_footer', array( $this, 'capture_cart_view' ) ); // checkout page view add_action( 'wp_footer', array( $this, 'capture_checkout_view' ) ); } /** * Track a product page view * * @deprecated 13.3 */ public function capture_product_view() { global $product; if ( ! $product instanceof WC_Product ) { return; } $this->record_event( 'woocommerceanalytics_product_view', array(), $product->get_id() ); } /** * Track the order confirmation page view * * @deprecated 13.3 */ public function capture_order_confirmation_view() { $order_id = absint( get_query_var( 'order-received' ) ); if ( ! $order_id ) { return; } if ( ! is_order_received_page() ) { return; } $order = wc_get_order( $order_id ); $order_source = $order->get_created_via(); $checkout_page_contains_checkout_block = '0'; $checkout_page_contains_checkout_shortcode = '0'; if ( 'store-api' === $order_source ) { $checkout_page_contains_checkout_block = '1'; } elseif ( 'checkout' === $order_source ) { $checkout_page_contains_checkout_shortcode = '1'; } $coupons = $order->get_coupons(); $coupon_used = 0; if ( is_countable( $coupons ) ) { $coupon_used = count( $coupons ) ? 1 : 0; } if ( is_object( WC()->session ) ) { $create_account = true === WC()->session->get( 'wc_checkout_createaccount_used' ) ? 'Yes' : 'No'; $checkout_page_used = true === WC()->session->get( 'checkout_page_used' ) ? 'Yes' : 'No'; } else { $create_account = 'No'; $checkout_page_used = 'No'; } $this->record_event( 'woocommerceanalytics_order_confirmation_view', array( 'coupon_used' => $coupon_used, 'create_account' => $create_account, 'express_checkout' => 'null', // TODO: not solved yet. 'guest_checkout' => $order->get_customer_id() ? 'No' : 'Yes', 'oi' => $order->get_id(), 'order_value' => $order->get_total(), 'payment_option' => $order->get_payment_method(), 'products_count' => $order->get_item_count(), 'products' => $this->format_items_to_json( $order->get_items() ), 'order_note' => $order->get_customer_note(), 'shipping_option' => $order->get_shipping_method(), 'from_checkout' => $checkout_page_used, 'checkout_page_contains_checkout_block' => $checkout_page_contains_checkout_block, 'checkout_page_contains_checkout_shortcode' => $checkout_page_contains_checkout_shortcode, ) ); } /** * Track the cart page view * * @deprecated 13.3 */ public function capture_cart_view() { if ( ! is_cart() ) { return; } $this->record_event( 'woocommerceanalytics_cart_view', array_merge( $this->get_cart_checkout_shared_data(), array() ) ); } /** * Track the checkout page view * * @deprecated 13.3 */ public function capture_checkout_view() { global $post; $checkout_page_id = wc_get_page_id( 'checkout' ); $is_checkout = $checkout_page_id && is_page( $checkout_page_id ) || wc_post_content_has_shortcode( 'woocommerce_checkout' ) || has_block( 'woocommerce/checkout', $post ) || has_block( 'woocommerce/classic-shortcode', $post ) || apply_filters( 'woocommerce_is_checkout', false ) || \Automattic\Jetpack\Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ); if ( ! $is_checkout ) { return; } $is_in_checkout_page = $checkout_page_id === $post->ID ? 'Yes' : 'No'; $checkout_page_contains_checkout_block = '0'; $checkout_page_contains_checkout_shortcode = '1'; $session = WC()->session; if ( is_object( $session ) ) { $session->set( 'checkout_page_used', true ); $session->save_data(); $draft_order_id = $session->get( 'store_api_draft_order', 0 ); if ( $draft_order_id ) { $checkout_page_contains_checkout_block = '1'; $checkout_page_contains_checkout_shortcode = '0'; } } // Order received page is also a checkout page, so we need to bail out if we are on that page. if ( is_order_received_page() ) { return; } $this->record_event( 'woocommerceanalytics_checkout_view', array_merge( $this->get_cart_checkout_shared_data(), array( 'from_checkout' => $is_in_checkout_page, 'checkout_page_contains_checkout_block' => $checkout_page_contains_checkout_block, 'checkout_page_contains_checkout_shortcode' => $checkout_page_contains_checkout_shortcode, ) ) ); } } woocommerce-analytics/classes/class-jetpack-woocommerce-analytics-my-account.php 0000644 00000025217 15174711637 0024344 0 ustar 00 <?php /** * Jetpack_WooCommerce_Analytics_My_Account * * @deprecated 13.3 * * @package automattic/jetpack * @author Automattic */ /** * Bail if accessed directly */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Class Jetpack_WooCommerce_Analytics_My_Account * Filters and Actions added to My Account pages to perform analytics * * @deprecated 13.3 */ class Jetpack_WooCommerce_Analytics_My_Account { use Jetpack_WooCommerce_Analytics_Trait; /** * Jetpack_WooCommerce_Analytics_My_Account constructor. * * @deprecated 13.3 */ public function __construct() { add_action( 'woocommerce_account_content', array( $this, 'track_tabs' ) ); add_action( 'woocommerce_account_content', array( $this, 'track_logouts' ) ); add_action( 'woocommerce_customer_save_address', array( $this, 'track_save_address' ), 10, 2 ); add_action( 'wp_head', array( $this, 'trigger_queued_events' ) ); add_action( 'wp', array( $this, 'track_add_payment_method' ) ); add_action( 'wp', array( $this, 'track_delete_payment_method' ) ); add_action( 'woocommerce_save_account_details', array( $this, 'track_save_account_details' ) ); add_filter( 'woocommerce_my_account_my_orders_actions', array( $this, 'add_initiator_prop_to_my_account_action_links' ) ); add_action( 'woocommerce_cancelled_order', array( $this, 'track_order_cancel_event' ), 10, 0 ); add_action( 'before_woocommerce_pay', array( $this, 'track_order_pay_event' ) ); add_action( 'woocommerce_before_account_orders', array( $this, 'add_initiator_prop_to_order_urls' ), 9 ); add_filter( 'query_vars', array( $this, 'add_initiator_param_to_query_vars' ) ); } /** * Track my account tabs, we only trigger an event if a tab is viewed. * * We also track other events here, like order number clicks, order action clicks, * address clicks, payment method add and delete. * * @deprecated 13.3 */ public function track_tabs() { global $wp; // WooCommerce keeps a map of my-account endpoints keys and their custom permalinks. $core_endpoints = WC()->query->get_query_vars(); if ( ! empty( $wp->query_vars ) ) { foreach ( $wp->query_vars as $key => $value ) { // we skip pagename. if ( 'pagename' === $key ) { continue; } // When no permalink is set, the first page is page_id, so we skip it. if ( 'page_id' === $key ) { continue; } // We don't want to track our own analytics params. if ( '_wca_initiator' === $key ) { continue; } if ( isset( $core_endpoints['view-order'] ) && $core_endpoints['view-order'] === $key && is_numeric( $value ) ) { $initiator = get_query_var( '_wca_initiator' ); if ( 'number' === $initiator ) { $this->record_event( 'woocommerceanalytics_my_account_order_number_click' ); continue; } if ( 'action' === $initiator ) { $this->record_event( 'woocommerceanalytics_my_account_order_action_click', array( 'action' => 'view' ) ); continue; } } if ( isset( $core_endpoints['edit-address'] ) && $core_endpoints['edit-address'] === $key && in_array( $value, array( 'billing', 'shipping' ), true ) ) { $refer = wp_get_referer(); if ( $refer === wc_get_endpoint_url( 'edit-address', $value ) ) { // It means we're likely coming from the same page after a failed save and don't want to retrigger the address click event. continue; } $this->record_event( 'woocommerceanalytics_my_account_address_click', array( 'address' => $value ) ); continue; } if ( isset( $core_endpoints['add-payment-method'] ) && $core_endpoints['add-payment-method'] === $key ) { $this->record_event( 'woocommerceanalytics_my_account_payment_add' ); continue; } if ( isset( $core_endpoints['edit-address'] ) && $core_endpoints['edit-address'] ) { $refer = wp_get_referer(); if ( $refer === wc_get_endpoint_url( 'edit-address', 'billing' ) || $refer === wc_get_endpoint_url( 'edit-address', 'shipping' ) ) { // It means we're likely coming from the edit page save and don't want to retrigger the page view event. continue; } } /** * The main dashboard view has page as key, so we rename it. */ if ( 'page' === $key ) { $key = 'dashboard'; } /** * If a custom permalink is used for one of the pages, query_vars will have 2 keys, the custom permalink and the core endpoint key. * To avoid triggering the event twice, we skip the core one and only track the custom one. * Tracking the custom endpoint is safer than hoping the duplicated, redundant core endpoint is always present. */ if ( isset( $core_endpoints[ $key ] ) && $core_endpoints[ $key ] !== $key ) { continue; } /** * $core_endpoints is an array of core_permalink => custom_permalink, * query_vars gives us the custom_permalink, but we want to track it as core_permalink. */ if ( array_search( $key, $core_endpoints, true ) ) { $key = array_search( $key, $core_endpoints, true ); } $this->record_event( 'woocommerceanalytics_my_account_page_view', array( 'tab' => $key ) ); } } } /** * Track address save events, this can only come from the my account page. * * @deprecated 13.3 * * @param int $customer_id The customer id. * @param string $load_address The address type (billing, shipping). */ public function track_save_address( $customer_id, $load_address ) { $this->queue_event( 'woocommerceanalytics_my_account_address_save', array( 'address' => $load_address ) ); } /** * Track payment method add events, this can only come from the my account page. * * @deprecated 13.3 */ public function track_add_payment_method() { if ( isset( $_POST['woocommerce_add_payment_method'] ) && isset( $_POST['payment_method'] ) ) { $nonce_value = wc_get_var( $_REQUEST['woocommerce-add-payment-method-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-add-payment-method' ) ) { return; } $this->queue_event( 'woocommerceanalytics_my_account_payment_save' ); return; } } /** * Track payment method delete events. * * @deprecated 13.3 */ public function track_delete_payment_method() { global $wp; if ( isset( $wp->query_vars['delete-payment-method'] ) ) { $this->queue_event( 'woocommerceanalytics_my_account_payment_delete' ); return; } } /** * Track order cancel events. * * @deprecated 13.3 */ public function track_order_cancel_event() { if ( isset( $_GET['_wca_initiator'] ) && ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'woocommerce-cancel_order' ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $this->queue_event( 'woocommerceanalytics_my_account_order_action_click', array( 'action' => 'cancel' ) ); } } /** * Track order pay events. * * @deprecated 13.3 */ public function track_order_pay_event() { if ( isset( $_GET['_wca_initiator'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended $this->record_event( 'woocommerceanalytics_my_account_order_action_click', array( 'action' => 'pay' ) ); } } /** * Track account details save events, this can only come from the my account page. * * @deprecated 13.3 */ public function track_save_account_details() { $this->queue_event( 'woocommerceanalytics_my_account_details_save' ); } /** * Track logout events. * * @deprecated 13.3 */ public function track_logouts() { $common_props = wp_json_encode( array_merge( array( '_en' => 'woocommerceanalytics_my_account_tab_click', 'tab' => 'logout', ), $this->get_common_properties() ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); wc_enqueue_js( " jQuery(document).ready(function($) { // Attach event listener to the logout link jQuery('.woocommerce-MyAccount-navigation-link--customer-logout').on('click', function() { _wca.push($common_props); }); }); " ); } /** * Add referrer prop to my account action links * * @deprecated 13.3 * * @param array $actions My account action links. * @return array */ public function add_initiator_prop_to_my_account_action_links( $actions ) { foreach ( $actions as $key => $action ) { if ( ! isset( $action['url'] ) ) { continue; } // Check if the action key is view, pay, or cancel. if ( ! in_array( $key, array( 'view', 'pay', 'cancel' ), true ) ) { continue; } $url = add_query_arg( array( '_wca_initiator' => 'action' ), $action['url'] ); $actions[ $key ]['url'] = $url; } return $actions; } /** * Add an initiator prop to the order url. * * The get_view_order_url is used in a lot of places, * so we want to limit it just to my account page. * * @deprecated 13.3 */ public function add_initiator_prop_to_order_urls() { add_filter( 'woocommerce_get_view_order_url', function ( $url ) { return add_query_arg( array( '_wca_initiator' => 'number' ), $url ); }, 10, 1 ); add_filter( 'woocommerce_get_endpoint_url', function ( $url, $endpoint ) { if ( 'edit-address' === $endpoint ) { return add_query_arg( array( '_wca_initiator' => 'action' ), $url ); } return $url; }, 10, 2 ); } /** * Add initiator to query vars * * @deprecated 13.3 * * @param array $query_vars Query vars. * @return array */ public function add_initiator_param_to_query_vars( $query_vars ) { $query_vars[] = '_wca_initiator'; return $query_vars; } /** * Record all queued up events in session. * * This is called on every page load, and will record all events that were queued up in session. * * @deprecated 13.3 */ public function trigger_queued_events() { if ( is_object( WC()->session ) ) { $events = WC()->session->get( 'wca_queued_events', array() ); foreach ( $events as $event ) { $this->record_event( $event['event_name'], $event['event_props'] ); } // Clear data, now that these events have been recorded. WC()->session->set( 'wca_queued_events', array() ); } } /** * Queue an event in session to be recorded later on next page load. * * @deprecated 13.3 * * @param string $event_name The event name. * @param array $event_props The event properties. */ protected function queue_event( $event_name, $event_props = array() ) { if ( is_object( WC()->session ) ) { $events = WC()->session->get( 'wca_queued_events', array() ); $events[] = array( 'event_name' => $event_name, 'event_props' => $event_props, ); WC()->session->set( 'wca_queued_events', $events ); } } } photon-cdn.php 0000644 00000031302 15174711637 0007341 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Module Name: Asset CDN * Module Description: Serve static files like CSS and JS from Jetpack’s global CDN for faster load times. * Sort Order: 26 * Recommendation Order: 1 * First Introduced: 6.6 * Requires Connection: No * Auto Activate: No * Module Tags: Photos and Videos, Appearance, Recommended * Feature: Recommended, Appearance * Additional Search Queries: site accelerator, accelerate, static, assets, javascript, css, files, performance, cdn, bandwidth, content delivery network, pagespeed, combine js, optimize css * * @package automattic/jetpack */ use Automattic\Jetpack\Assets; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } $GLOBALS['concatenate_scripts'] = false; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited Assets::add_resource_hint( '//c0.wp.com', 'preconnect' ); /** * Asset CDN module main class file. */ class Jetpack_Photon_Static_Assets_CDN { const CDN = 'https://c0.wp.com/'; /** * Sets up action handlers needed for Jetpack CDN. */ public static function go() { add_action( 'wp_print_scripts', array( __CLASS__, 'cdnize_assets' ) ); add_action( 'wp_print_styles', array( __CLASS__, 'cdnize_assets' ) ); add_action( 'admin_print_scripts', array( __CLASS__, 'cdnize_assets' ) ); add_action( 'admin_print_styles', array( __CLASS__, 'cdnize_assets' ) ); add_action( 'wp_footer', array( __CLASS__, 'cdnize_assets' ) ); add_filter( 'load_script_textdomain_relative_path', array( __CLASS__, 'fix_script_relative_path' ), 10, 2 ); add_filter( 'load_script_translation_file', array( __CLASS__, 'fix_local_script_translation_path' ), 10, 3 ); } /** * Sets up CDN URLs for assets that are enqueued by the WordPress Core. */ public static function cdnize_assets() { global $wp_scripts, $wp_styles, $wp_version; /* * Short-circuit if AMP since not relevant as custom JS is not allowed and CSS is inlined. * Note that it is not suitable to use the jetpack_force_disable_site_accelerator filter for this * because it will be applied before the wp action, which is the point at which the queried object * is available and we know whether the response will be AMP or not. This is particularly important * for AMP-first (native AMP) pages where there are no AMP-specific URLs. */ if ( class_exists( Jetpack_AMP_Support::class ) && Jetpack_AMP_Support::is_amp_request() ) { return; } /** * Filters Jetpack CDN's Core version number and locale. Can be used to override the values * that Jetpack uses to retrieve assets. Expects the values to be returned in an array. * * @module photon-cdn * * @since 6.6.0 * * @param array $values array( $version = core assets version, i.e. 4.9.8, $locale = desired locale ) */ list( $version, $locale ) = apply_filters( // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable 'jetpack_cdn_core_version_and_locale', array( $wp_version, get_locale() ) ); if ( self::is_public_version( $version ) ) { $site_url = trailingslashit( site_url() ); if ( $wp_scripts instanceof WP_Scripts && is_array( $wp_scripts->registered ) ) { foreach ( $wp_scripts->registered as $handle => $thing ) { if ( wp_startswith( $thing->src, self::CDN ) ) { continue; } if ( ! is_string( $thing->src ) ) { continue; } $src = ltrim( str_replace( $site_url, '', $thing->src ), '/' ); if ( self::is_js_or_css_file( $src ) && in_array( substr( $src, 0, 9 ), array( 'wp-admin/', 'wp-includ' ), true ) ) { $wp_scripts->registered[ $handle ]->src = sprintf( self::CDN . 'c/%1$s/%2$s', $version, $src ); $wp_scripts->registered[ $handle ]->ver = null; } } } if ( $wp_styles instanceof WP_Styles && is_array( $wp_styles->registered ) ) { foreach ( $wp_styles->registered as $handle => $thing ) { if ( wp_startswith( $thing->src, self::CDN ) ) { continue; } if ( ! is_string( $thing->src ) ) { continue; } $src = ltrim( str_replace( $site_url, '', $thing->src ), '/' ); if ( self::is_js_or_css_file( $src ) && in_array( substr( $src, 0, 9 ), array( 'wp-admin/', 'wp-includ' ), true ) ) { $wp_styles->registered[ $handle ]->src = sprintf( self::CDN . 'c/%1$s/%2$s', $version, $src ); $wp_styles->registered[ $handle ]->ver = null; } } } } self::cdnize_plugin_assets( 'jetpack', JETPACK__VERSION ); if ( class_exists( 'WooCommerce' ) ) { self::cdnize_plugin_assets( 'woocommerce', WC_VERSION ); } } /** * Ensure use of the correct relative path when determining the JavaScript file names. * * @param string $relative The relative path of the script. False if it could not be determined. * @param string $src The full source url of the script. * @return string The expected relative path for the CDN-ed URL. */ public static function fix_script_relative_path( $relative, $src ) { // Note relevant in AMP responses. See note above. if ( class_exists( Jetpack_AMP_Support::class ) && Jetpack_AMP_Support::is_amp_request() ) { return $relative; } $strpos = strpos( $src, '/wp-includes/' ); // We only treat URLs that have wp-includes in them. Cases like language textdomains // can also use this filter, they don't need to be touched because they are local paths. if ( false !== $strpos ) { return substr( $src, 1 + $strpos ); } // Get the local path from a URL which was CDN'ed by cdnize_plugin_assets(). if ( preg_match( '#^' . preg_quote( self::CDN, '#' ) . 'p/[^/]+/[^/]+/(.*)$#', $src, $m ) ) { return $m[1]; } return $relative; } /** * Ensure use of the correct local path when loading the JavaScript translation file for a CDN'ed asset. * * @param string|false $file Path to the translation file to load. False if there isn't one. * @param string $handle The script handle. * @param string $domain The text domain. * * @return string The transformed local languages path. */ public static function fix_local_script_translation_path( $file, $handle, $domain ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable global $wp_scripts; // This is a rewritten plugin URL, so load the language file from the plugins path. if ( $file && isset( $wp_scripts->registered[ $handle ] ) && wp_startswith( $wp_scripts->registered[ $handle ]->src, self::CDN . 'p' ) ) { return WP_LANG_DIR . '/plugins/' . basename( $file ); } return $file; } /** * Sets up CDN URLs for supported plugin assets. * * @param String $plugin_slug plugin slug string. * @param String $current_version plugin version string. * @return null|bool */ public static function cdnize_plugin_assets( $plugin_slug, $current_version ) { global $wp_scripts, $wp_styles; /** * Filters Jetpack CDN's plugin slug and version number. Can be used to override the values * that Jetpack uses to retrieve assets. For example, when testing a development version of Jetpack * the assets are not yet published, so you may need to override the version value to either * trunk, or the latest available version. Expects the values to be returned in an array. * * @module photon-cdn * * @since 6.6.0 * * @param array $values array( $slug = the plugin repository slug, i.e. jetpack, $version = the plugin version, i.e. 6.6 ) */ list( $plugin_slug, $current_version ) = apply_filters( 'jetpack_cdn_plugin_slug_and_version', array( $plugin_slug, $current_version ) ); $assets = self::get_plugin_assets( $plugin_slug, $current_version ); $plugin_directory_url = plugins_url() . '/' . $plugin_slug . '/'; if ( is_wp_error( $assets ) || ! is_array( $assets ) ) { return false; } if ( $wp_scripts instanceof WP_Scripts && is_array( $wp_scripts->registered ) ) { foreach ( $wp_scripts->registered as $handle => $thing ) { if ( wp_startswith( $thing->src, self::CDN ) ) { continue; } if ( wp_startswith( $thing->src, $plugin_directory_url ) ) { $local_path = substr( $thing->src, strlen( $plugin_directory_url ) ); if ( in_array( $local_path, $assets, true ) ) { $wp_scripts->registered[ $handle ]->src = sprintf( self::CDN . 'p/%1$s/%2$s/%3$s', $plugin_slug, $current_version, $local_path ); $wp_scripts->registered[ $handle ]->ver = null; } } } } if ( $wp_styles instanceof WP_Styles && is_array( $wp_styles->registered ) ) { foreach ( $wp_styles->registered as $handle => $thing ) { if ( wp_startswith( $thing->src, self::CDN ) ) { continue; } if ( wp_startswith( $thing->src, $plugin_directory_url ) ) { $local_path = substr( $thing->src, strlen( $plugin_directory_url ) ); if ( in_array( $local_path, $assets, true ) ) { $wp_styles->registered[ $handle ]->src = sprintf( self::CDN . 'p/%1$s/%2$s/%3$s', $plugin_slug, $current_version, $local_path ); $wp_styles->registered[ $handle ]->ver = null; } } } } } /** * Returns cdn-able assets for a given plugin. * * @param string $plugin plugin slug string. * @param string $version plugin version number string. * @return array|bool Will return false if not a public version. */ public static function get_plugin_assets( $plugin, $version ) { if ( 'jetpack' === $plugin && JETPACK__VERSION === $version ) { if ( ! self::is_public_version( $version ) || ! file_exists( JETPACK__PLUGIN_DIR . 'modules/photon-cdn/jetpack-manifest.php' ) ) { return false; } $assets = array(); // The variable will be redefined in the included file. include JETPACK__PLUGIN_DIR . 'modules/photon-cdn/jetpack-manifest.php'; return $assets; } /** * Used for other plugins to provide their bundled assets via filter to * prevent the need of storing them in an option or an external api request * to w.org. * * @module photon-cdn * * @since 6.6.0 * * @param array $assets The assets array for the plugin. * @param string $version The version of the plugin being requested. */ $assets = apply_filters( "jetpack_cdn_plugin_assets-{$plugin}", null, $version ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores if ( is_array( $assets ) ) { return $assets; } if ( ! self::is_public_version( $version ) ) { return false; } $cache = Jetpack_Options::get_option( 'static_asset_cdn_files', array() ); if ( isset( $cache[ $plugin ][ $version ] ) ) { if ( is_array( $cache[ $plugin ][ $version ] ) ) { return $cache[ $plugin ][ $version ]; } if ( is_numeric( $cache[ $plugin ][ $version ] ) ) { // Cache an empty result for up to 24h. if ( (int) $cache[ $plugin ][ $version ] + DAY_IN_SECONDS > time() ) { return array(); } } } $url = sprintf( 'http://downloads.wordpress.org/plugin-checksums/%s/%s.json', $plugin, $version ); if ( wp_http_supports( array( 'ssl' ) ) ) { $url = set_url_scheme( $url, 'https' ); } $response = wp_remote_get( $url ); $body = trim( wp_remote_retrieve_body( $response ) ); $body = json_decode( $body, true ); $return = time(); if ( is_array( $body ) && isset( $body['files'] ) && is_array( $body['files'] ) ) { $return = array_filter( array_keys( $body['files'] ), array( __CLASS__, 'is_js_or_css_file' ) ); } $cache[ $plugin ] = array(); $cache[ $plugin ][ $version ] = $return; Jetpack_Options::update_option( 'static_asset_cdn_files', $cache, true ); return $return; } /** * Checks a path whether it is a JS or CSS file. * * @param String $path file path. * @return Boolean whether the file is a JS or CSS. */ public static function is_js_or_css_file( $path ) { return ( ! str_contains( $path, '?' ) ) && in_array( substr( $path, -3 ), array( 'css', '.js' ), true ); } /** * Checks whether the version string indicates a production version. * * @param String $version the version string. * @param Boolean $include_beta_and_rc whether to count beta and RC versions as production. * @return Boolean */ public static function is_public_version( $version, $include_beta_and_rc = false ) { if ( preg_match( '/^\d+(\.\d+)+$/', $version ) ) { /** Example matches: `1`, `1.2`, `1.2.3`. */ return true; } elseif ( $include_beta_and_rc && preg_match( '/^\d+(\.\d+)+(-(beta|rc|pressable)\d?)$/i', $version ) ) { /** Example matches: `1.2.3`, `1.2.3-beta`, `1.2.3-pressable`, `1.2.3-beta1`, `1.2.3-rc`, `1.2.3-rc2`. */ return true; } // Unrecognized version. return false; } } /** * Allow plugins to short-circuit the Asset CDN, even when the module is on. * * @module photon-cdn * * @since 6.7.0 * * @param false bool Should the Asset CDN be blocked? False by default. */ if ( true !== apply_filters( 'jetpack_force_disable_site_accelerator', false ) ) { Jetpack_Photon_Static_Assets_CDN::go(); } likes.php 0000644 00000050543 15174711637 0006407 0 ustar 00 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Module Name: Likes * Module Description: Let readers like your posts to show appreciation and encourage interaction. * First Introduced: 2.2 * Sort Order: 23 * Requires Connection: Yes * Auto Activate: No * Module Tags: Social * Feature: Engagement * Additional Search Queries: like, likes, wordpress.com * * @package automattic/jetpack */ /** * NOTE: While the front-end behavior currently varies, try to keep the data * model here the same as on wpcom to facilitate Simple→Atomic moves and * possible future work to recombine the front-ends. */ // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files. use Automattic\Jetpack\Assets; use Automattic\Jetpack\Status\Host; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } Assets::add_resource_hint( array( '//widgets.wp.com', '//s0.wp.com', '//0.gravatar.com', '//1.gravatar.com', '//2.gravatar.com', ), 'dns-prefetch' ); require_once __DIR__ . '/likes/jetpack-likes-master-iframe.php'; require_once __DIR__ . '/likes/jetpack-likes-settings.php'; /** * Jetpack Like Class */ class Jetpack_Likes { /** * Jetpack_Likes_Settings object * * @var Jetpack_Likes_Settings */ public $settings; /** * Initialize class */ public static function init() { static $instance = null; if ( ! $instance ) { $instance = new Jetpack_Likes(); } return $instance; } /** * Constructs Likes class */ public function __construct() { $this->settings = new Jetpack_Likes_Settings(); // We need to run on wp hook rather than init because we check is_amp_endpoint() // when bootstrapping hooks. add_action( 'wp', array( $this, 'action_init' ), 99 ); add_action( 'admin_init', array( $this, 'admin_init' ) ); add_action( 'jetpack_activate_module_likes', array( $this, 'set_social_notifications_like' ) ); add_action( 'jetpack_deactivate_module_likes', array( $this, 'delete_social_notifications_like' ) ); // The `enable_module_configurable` method doesn't exist in the WP.com loader implementation. if ( ! ( new Host() )->is_wpcom_simple() ) { Jetpack::enable_module_configurable( __FILE__ ); } add_filter( 'jetpack_module_configuration_url_likes', array( $this, 'jetpack_likes_configuration_url' ) ); add_action( 'admin_print_scripts-settings_page_sharing', array( $this, 'load_jp_css' ) ); add_filter( 'sharing_show_buttons_on_row_start', array( $this, 'configuration_target_area' ) ); $publicize_active = Jetpack::is_module_active( 'publicize' ); $sharedaddy_active = Jetpack::is_module_active( 'sharedaddy' ); if ( $publicize_active && ! $sharedaddy_active ) { // we have a sharing page but not the global options area. add_action( 'pre_admin_screen_sharing', array( $this->settings, 'sharing_block' ), 20 ); add_action( 'pre_admin_screen_sharing', array( $this->settings, 'updated_message' ), -10 ); } if ( ! $sharedaddy_active ) { add_action( 'admin_init', array( $this->settings, 'process_update_requests_if_sharedaddy_not_loaded' ) ); add_action( 'sharing_global_options', array( $this->settings, 'admin_settings_showbuttonon_init' ), 19 ); add_action( 'sharing_admin_update', array( $this->settings, 'admin_settings_showbuttonon_callback' ), 19 ); add_action( 'admin_init', array( $this->settings, 'add_meta_box' ) ); } else { add_filter( 'sharing_meta_box_title', array( $this->settings, 'add_likes_to_sharing_meta_box_title' ) ); add_action( 'start_sharing_meta_box_content', array( $this->settings, 'meta_box_content' ) ); } add_action( 'admin_init', array( $this, 'admin_discussion_likes_settings_init' ) ); // Likes notifications. add_action( 'wp_enqueue_scripts', array( $this, 'load_styles_register_scripts' ) ); add_action( 'save_post', array( $this->settings, 'meta_box_save' ) ); add_action( 'edit_attachment', array( $this->settings, 'meta_box_save' ) ); add_action( 'sharing_global_options', array( $this->settings, 'admin_settings_init' ), 20 ); add_action( 'sharing_admin_update', array( $this->settings, 'admin_settings_callback' ), 20 ); } /** * Set the social_notifications_like option to `on` when the Likes module is activated. * * @since 3.7.0 */ public function set_social_notifications_like() { update_option( 'social_notifications_like', 'on' ); } /** * Delete the social_notifications_like option that was set to `on` on module activation. * * @since 3.7.0 */ public function delete_social_notifications_like() { delete_option( 'social_notifications_like' ); } /** * Overrides default configuration url * * @uses admin_url * @return string module settings URL */ public function jetpack_likes_configuration_url() { return admin_url( 'options-general.php?page=sharing#likes' ); } /** * Loads Jetpack's CSS on the sharing page so we can use .jetpack-targetable */ public function load_jp_css() { /** * Do we really need `admin_styles`? With the new admin UI, it's breaking some bits. * Jetpack::init()->admin_styles(); */ } /** * Load scripts and styles for front end. */ public function load_styles_register_scripts() { $style_url = Assets::get_file_url_for_environment( '_inc/build/likes/style.min.css', 'modules/likes/style.css' ); wp_enqueue_style( 'jetpack_likes', $style_url, array(), JETPACK__VERSION ); $style_path = plugin_dir_path( JETPACK__PLUGIN_FILE ) . ( /** This filter is documented in projects/plugins/jetpack/load-jetpack.php */ apply_filters( 'jetpack_should_use_minified_assets', true ) ? '_inc/build/likes/style.min.css' : 'modules/likes/style.css' ); wp_style_add_data( 'jetpack_likes', 'path', $style_path ); wp_register_script( 'jetpack_likes_queuehandler', Assets::get_file_url_for_environment( '_inc/build/likes/queuehandler.min.js', 'modules/likes/queuehandler.js' ), array(), JETPACK__VERSION, true ); } /** * Adds in the jetpack-targetable class so when we visit sharing#likes our like settings get highlighted by a yellow box * * @param string $html row heading for the sharedaddy "which page" setting. * @return string $html with the jetpack-targetable class and likes id. tbody gets closed after the like settings */ public function configuration_target_area( $html = '' ) { $html = "<tbody id='likes' class='jetpack-targetable'>" . $html; return $html; } /** * Options to be added to the discussion page (see also admin_settings_init, etc below for Sharing settings page) */ public function admin_discussion_likes_settings_init() { // Add a temporary section, until we can move the setting out of there and with the rest of the email notification settings. add_settings_section( 'likes-notifications', __( 'Likes Notifications', 'jetpack' ), array( $this, 'admin_discussion_likes_settings_section' ), 'discussion' ); add_settings_field( 'social-notifications', __( 'Email me whenever', 'jetpack' ), array( $this, 'admin_discussion_likes_settings_field' ), 'discussion', 'likes-notifications' ); // Register the setting. register_setting( 'discussion', 'social_notifications_like', array( $this, 'admin_discussion_likes_settings_validate' ) ); } /** Add email notification options to WordPress discussion settings */ public function admin_discussion_likes_settings_section() { // Atypical usage here. We emit jquery to move likes notification checkbox to be with the rest of the email notification settings. ?> <script type="text/javascript"> jQuery( function( $ ) { var table = $( '#social_notifications_like' ).parents( 'table:first' ), header = table.prevAll( 'h2:first' ), newParent = $( '#moderation_notify' ).parent( 'label' ).parent(); if ( !table.length || !header.length || !newParent.length ) { return; } newParent.append( '<br/>' ).append( table.end().parent( 'label' ).siblings().andSelf() ); header.remove(); table.remove(); } ); </script> <?php } /** Check if email notifications for likes is on or off. * * @param string $option - which option we're checking (social_notifications_like). */ public function admin_likes_get_option( $option ) { $option_setting = get_option( $option, 'on' ); return (int) ( 'on' === $option_setting ); } /** Display email notification for likes setting in WordPress' discussion settings. */ public function admin_discussion_likes_settings_field() { $like = $this->admin_likes_get_option( 'social_notifications_like' ); ?> <label><input type="checkbox" id="social_notifications_like" name="social_notifications_like" value="1" <?php checked( $like ); ?> /> <?php esc_html_e( 'Someone likes one of my posts', 'jetpack' ); ?></label> <?php } /** * Validate email notification settings. * * @param string $input - determines if checbox is on or off. */ public function admin_discussion_likes_settings_validate( $input ) { // If it's not set (was unchecked during form submission) or was set to off (during option update), return 'off'. if ( ! $input || 'off' === $input ) { return 'off'; } // Otherwise return 'on'. return 'on'; } /** Initialize admin settings */ public function admin_init() { add_filter( 'manage_posts_columns', array( $this, 'add_like_count_column' ) ); add_filter( 'manage_pages_columns', array( $this, 'add_like_count_column' ) ); add_action( 'manage_posts_custom_column', array( $this, 'likes_edit_column' ), 10, 2 ); add_action( 'manage_pages_custom_column', array( $this, 'likes_edit_column' ), 10, 2 ); add_action( 'admin_print_styles-edit.php', array( $this, 'load_admin_css' ) ); add_action( 'admin_print_scripts-edit.php', array( $this, 'enqueue_admin_scripts' ) ); } /** Initialize action */ public function action_init() { /* * Only check if the module is enabled here because * we are not currently in The Loop and do not yet have access to check * the switch_like_status post meta flag for the post to be loaded. */ if ( is_admin() || ! $this->settings->is_likes_module_enabled() ) { return; } if ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'APP_REQUEST' ) && APP_REQUEST ) || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) || ( defined( 'COOKIE_AUTH_REQUEST' ) && COOKIE_AUTH_REQUEST ) || ( defined( 'JABBER_SERVER' ) && JABBER_SERVER ) ) { return; } if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { return; } add_filter( 'the_content', array( $this, 'post_likes' ), 30, 1 ); add_filter( 'the_excerpt', array( $this, 'post_likes' ), 30, 1 ); } /** * Load the CSS needed for the wp-admin area. */ public function load_admin_css() { ?> <style type="text/css"> .vers img { display: none; } .metabox-prefs .vers img { display: inline; } .fixed .column-likes { width: 2.5em; padding: 4px 0; text-align: left; } .fixed .column-stats { width: 5em; white-space: nowrap; } .fixed .column-likes .post-com-count { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; display: inline-block; padding: 0 4px; min-width: 2em; text-align: center; height: 2em; margin-top: 5px; -webkit-border-radius: 5px; border-radius: 5px; background-color: #787c82; color: #FFF; font-size: 11px; line-height: 21px; } .fixed .column-likes .post-com-count::after { border: none !important; } .fixed .column-likes .post-com-count:hover { background-color: #2271b1; } .fixed .column-likes .vers::before { font: normal 20px/1 dashicons; content: '\f155'; speak: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @media screen and (max-width: 782px) { .fixed .column-likes { display: none; } } </style> <?php } /** * Load the JS required for loading the like counts. */ public function enqueue_admin_scripts() { if ( empty( $_GET['post_type'] ) || 'post' === $_GET['post_type'] || 'page' === $_GET['post_type'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended wp_enqueue_script( 'likes-post-count', Assets::get_file_url_for_environment( '_inc/build/likes/post-count.min.js', 'modules/likes/post-count.js' ), array( 'jquery' ), JETPACK__VERSION, false ); wp_enqueue_script( 'likes-post-count-jetpack', Assets::get_file_url_for_environment( '_inc/build/likes/post-count-jetpack.min.js', 'modules/likes/post-count-jetpack.js' ), array( 'jquery', 'likes-post-count' ), JETPACK__VERSION, false ); } } /** * Add "Likes" column data to the post edit table in wp-admin. * * @param string $column_name - name of the column. * @param int $post_id - the post id. */ public function likes_edit_column( $column_name, $post_id ) { if ( 'likes' === $column_name ) { $blog_id = Jetpack_Options::get_option( 'id' ); $permalink = get_permalink( get_the_ID() ); ?> <a title="" data-post-id="<?php echo (int) $post_id; ?>" class="post-com-count post-like-count" id="post-like-count-<?php echo (int) $post_id; ?>" data-blog-id="<?php echo (int) $blog_id; ?>" href="<?php echo esc_url( $permalink ); ?>#like-<?php echo (int) $post_id; ?>"> <span class="comment-count">0</span> </a> <?php } } /** * Add a "Likes" column header to the post edit table in wp-admin. * * @param array $columns - array of columns in wp-admin. */ public function add_like_count_column( $columns ) { $date = $columns['date']; unset( $columns['date'] ); $columns['likes'] = '<span class="vers"><img title="' . esc_attr__( 'Likes', 'jetpack' ) . '" alt="' . esc_attr__( 'Likes', 'jetpack' ) . '" src="//s0.wordpress.com/i/like-grey-icon.png" /><span class="screen-reader-text">' . __( 'Likes', 'jetpack' ) . '</span></span>'; $columns['date'] = $date; return $columns; } /** * Append like button to content. * * @param string $content - content of the page. */ public function post_likes( $content ) { global $wp_current_filter; $post_id = get_the_ID(); if ( ! is_numeric( $post_id ) || ! $this->settings->is_likes_visible() ) { return $content; } // Do not output Likes on requests for ActivityPub requests. if ( function_exists( '\Activitypub\is_activitypub_request' ) && \Activitypub\is_activitypub_request() ) { return $content; } // Ensure we don't display like button on post excerpts that are hooked inside the post content if ( in_array( 'the_excerpt', (array) $wp_current_filter, true ) && in_array( 'the_content', (array) $wp_current_filter, true ) ) { return $content; } $blog_id = Jetpack_Options::get_option( 'id' ); $url = home_url(); $url_parts = wp_parse_url( $url ); $domain = $url_parts['host']; // Make sure to include the `queuehandler` scripts before the iframe otherwise the script won't find the iframe. if ( ! has_action( 'wp_footer', 'jetpack_likes_master_iframe' ) ) { add_action( 'wp_footer', 'jetpack_likes_master_iframe', 21 ); } /** * If the same post appears more then once on a page the page goes crazy * we need a slightly more unique id / name for the widget wrapper. */ $uniqid = uniqid(); $src = sprintf( 'https://widgets.wp.com/likes/?ver=%1$s#blog_id=%2$d&post_id=%3$d&origin=%4$s&obj_id=%2$d-%3$d-%5$s', rawurlencode( JETPACK__VERSION ), $blog_id, $post_id, $domain, $uniqid ); $name = sprintf( 'like-post-frame-%1$d-%2$d-%3$s', $blog_id, $post_id, $uniqid ); $wrapper = sprintf( 'like-post-wrapper-%1$d-%2$d-%3$s', $blog_id, $post_id, $uniqid ); $headline = sprintf( /** This filter is already documented in modules/sharedaddy/sharing-service.php */ apply_filters( 'jetpack_sharing_headline_html', '<h3 class="sd-title">%s</h3>', esc_html__( 'Like this:', 'jetpack' ), 'likes' ), esc_html__( 'Like this:', 'jetpack' ) ); $title = esc_html__( 'Like or Reblog', 'jetpack' ); /** This filter is documented in modules/likes/jetpack-likes-master-iframe.php */ $src = apply_filters( 'jetpack_likes_iframe_src', $src ); $html = "<div class='sharedaddy sd-block sd-like jetpack-likes-widget-wrapper jetpack-likes-widget-unloaded' id='$wrapper' data-src='$src' data-name='$name' data-title='$title'>"; $html .= $headline; $html .= "<div class='likes-widget-placeholder post-likes-widget-placeholder' style='height: 55px;'><span class='button'><span>" . esc_html__( 'Like', 'jetpack' ) . '</span></span> <span class="loading">' . esc_html__( 'Loading...', 'jetpack' ) . '</span></div>'; $html .= "<span class='sd-text-color'></span><a class='sd-link-color'></a>"; $html .= '</div>'; // Let's make sure that the script is enqueued. wp_enqueue_script( 'jetpack_likes_queuehandler' ); return $content . $html; } } /** * Callback to get the value for the jetpack_likes_enabled field. * * Warning: this behavior is somewhat complicated! * When the switch_like_status post_meta is unset, we follow the global setting in Sharing. * When it is set to 0, we disable likes on the post, regardless of the global setting. * When it is set to 1, we enable likes on the post, regardless of the global setting. * * @param array $post - post data we're checking. * * @return bool */ function jetpack_post_likes_get_value( array $post ) { if ( ! isset( $post['id'] ) ) { return false; } $post_likes_switched = get_post_meta( $post['id'], 'switch_like_status', true ); /** This filter is documented in modules/jetpack-likes-settings.php */ $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) ); // An empty string: post meta was not set, so go with the global setting. if ( '' === $post_likes_switched ) { return $sitewide_likes_enabled; } elseif ( '0' === $post_likes_switched ) { // User overrode the global setting to disable likes. return false; } elseif ( '1' === $post_likes_switched ) { // User overrode the global setting to enable likes. return true; } // No default fallback, let's stay explicit. } /** * Callback to set switch_like_status post_meta when jetpack_likes_enabled is updated. * * Warning: this behavior is somewhat complicated! * When the switch_like_status post_meta is unset, we follow the global setting in Sharing. * When it is set to 0, we disable likes on the post, regardless of the global setting. * When it is set to 1, we enable likes on the post, regardless of the global setting. * * @param bool $enable_post_likes - checks if post likes are enabled. * @param object $post_object - object containing post data. */ function jetpack_post_likes_update_value( $enable_post_likes, $post_object ) { /** This filter is documented in modules/jetpack-likes-settings.php */ $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) ); $should_switch_status = $enable_post_likes !== $sitewide_likes_enabled; if ( $should_switch_status ) { // Set the meta to 0 if the user wants to disable likes, 1 if user wants to enable. $switch_like_status = ( $enable_post_likes ? 1 : 0 ); return update_post_meta( $post_object->ID, 'switch_like_status', $switch_like_status ); } else { // Unset the meta otherwise. return delete_post_meta( $post_object->ID, 'switch_like_status' ); } } /** * Add Likes post_meta to the REST API Post response. * * @action rest_api_init * @uses register_rest_field * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/ */ function jetpack_post_likes_register_rest_field() { $post_types = get_post_types( array( 'public' => true ) ); foreach ( $post_types as $post_type ) { register_rest_field( $post_type, 'jetpack_likes_enabled', array( 'get_callback' => 'jetpack_post_likes_get_value', 'update_callback' => 'jetpack_post_likes_update_value', 'schema' => array( 'description' => __( 'Are Likes enabled?', 'jetpack' ), 'type' => 'boolean', ), ) ); /** * Ensures all public internal post-types support `likes` * This feature support flag is used by the REST API and Gutenberg. */ add_post_type_support( $post_type, 'jetpack-post-likes' ); } } // Add Likes post_meta to the REST API Post response. add_action( 'rest_api_init', 'jetpack_post_likes_register_rest_field' ); // Some CPTs (e.g. Jetpack portfolios and testimonials) get registered with // restapi_theme_init because they depend on theme support, so let's also hook to that. add_action( 'restapi_theme_init', 'jetpack_post_likes_register_rest_field', 20 ); Jetpack_Likes::init(); search.php 0000644 00000002270 15174711637 0006537 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Module Name: Search * Module Description: Instantly deliver the most relevant results to your visitors. * First Introduced: 5.0 * Sort Order: 34 * Free: false * Requires Connection: Yes * Auto Activate: No * Feature: Search * Additional Search Queries: search, elastic, elastic search, elasticsearch, fast search, search results, search performance, google search * Plans: business, complete * * @package automattic/jetpack */ use Automattic\Jetpack\Search\Classic_Search; use Automattic\Jetpack\Search\Helper as Search_Helper; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * These are old legacy class names that were deprecated due to the move to packages. * * @todo Does this make more sense as a legacy dir in the search package? */ /** * Jetpack Search deprecated class. * * @deprecated 10.6 */ class Jetpack_Search { /** * Singleton */ protected function __construct() { } /** * Return the instance of the new class. */ public static function instance() { // Explicitly provide the blog ID, just in case. return Classic_Search::instance( Search_Helper::get_wpcom_site_id() ); } } widget-visibility.php 0000644 00000001056 15174711637 0010743 0 ustar 00 <?php /** * Module: Widget Visibility * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Module Name: Widget Visibility * Module Description: Choose which widgets appear on specific pages or posts with advanced controls. * First Introduced: 2.4 * Requires Connection: No * Auto Activate: No * Sort Order: 17 * Module Tags: Appearance * Feature: Appearance * Additional Search Queries: widget visibility, logic, conditional, widgets, widget */ require __DIR__ . '/widget-visibility/widget-conditions.php'; copy-post.php 0000644 00000036674 15174711637 0007246 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Module Name: Copy Post * Module Description: Duplicate any post or page in one click to speed up content creation. * Sort Order: 15 * First Introduced: 7.0 * Requires Connection: No * Auto Activate: No * Module Tags: Writing * Feature: Writing * Additional Search Queries: copy, duplicate * * @package automattic/jetpack */ use Automattic\Jetpack\Assets\Logo; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files. /** * Copy Post class. * * @phan-constructor-used-for-side-effects */ class Jetpack_Copy_Post { /** * Jetpack_Copy_Post_By_Param constructor. * Add row actions to post/page/CPT listing screens. * Process any `?copy` param if on a create new post/page/CPT screen. * * @return void */ public function __construct() { if ( 'edit.php' === $GLOBALS['pagenow'] || ( 'admin-ajax.php' === $GLOBALS['pagenow'] && ! empty( $_POST['post_view'] ) && 'list' === $_POST['post_view'] && ! empty( $_POST['action'] ) && 'inline-save' === $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- update_post_data() handles access check. add_action( 'admin_head', array( $this, 'print_inline_styles' ) ); add_filter( 'post_row_actions', array( $this, 'add_row_action' ), 10, 2 ); add_filter( 'page_row_actions', array( $this, 'add_row_action' ), 10, 2 ); return; } if ( ! empty( $_GET['jetpack-copy'] ) && 'post-new.php' === $GLOBALS['pagenow'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- update_post_data() handles access check. add_action( 'wp_insert_post', array( $this, 'update_post_data' ), 10, 3 ); add_filter( 'pre_option_default_post_format', '__return_empty_string' ); } } /** * Echos inline styles for the Jetpack logo. * * @return void */ public function print_inline_styles() { echo ' <style id="jetpack-copy-post-styles"> #jetpack-logo__icon { height: 14px; width: 14px; vertical-align: text-bottom; } #jetpack-logo__icon path { fill: inherit; } </style> '; } /** * Update the new (target) post data with the source post data. * * @param int $target_post_id Target post ID. * @param WP_Post $post Target post object (not used). * @param bool $update Whether this is an existing post being updated or not. * @return void */ public function update_post_data( $target_post_id, $post, $update ) { // This `$update` check avoids infinite loops of trying to update our updated post. if ( $update ) { return; } // Shouldn't happen, since this filter is only added when the value isn't empty, but check anyway. if ( empty( $_GET['jetpack-copy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $source_post = get_post( intval( $_GET['jetpack-copy'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! $source_post instanceof WP_Post || ! $this->user_can_access_post( $source_post->ID ) || ! $this->validate_post_type( $source_post ) ) { return; } $update_results = array( 'update_content' => $this->update_content( $source_post, $target_post_id ), 'update_featured_image' => $this->update_featured_image( $source_post, $target_post_id ), 'update_post_format' => $this->update_post_format( $source_post, $target_post_id ), 'update_likes_sharing' => $this->update_likes_sharing( $source_post, $target_post_id ), 'update_post_type_terms' => $this->update_post_type_terms( $source_post, $target_post_id ), ); // Required to satisfy get_default_post_to_edit(), which has these filters after post creation. add_filter( 'default_title', array( $this, 'filter_title' ), 10, 2 ); add_filter( 'default_content', array( $this, 'filter_content' ), 10, 2 ); add_filter( 'default_excerpt', array( $this, 'filter_excerpt' ), 10, 2 ); // Required to avoid the block editor from adding default blocks according to post format. add_filter( 'block_editor_settings_all', array( $this, 'remove_post_format_template' ) ); /** * Fires after all updates have been performed, and default content filters have been added. * Allows for any cleanup or post operations, and default content filters can be removed or modified. * * @module copy-post * * @since 7.0.0 * * @param WP_Post $source_post Post object that was copied. * @param int $target_post_id Target post ID. * @param array $update_results Results of all update operations, allowing action to be taken. */ do_action( 'jetpack_copy_post', $source_post, $target_post_id, $update_results ); } /** * Determine if the current user has edit access to the source post. * * @param int $post_id Source post ID (the post being copied). * @return bool True if user has the meta cap of `edit_post` for the given post ID, false otherwise. */ protected function user_can_access_post( $post_id ) { return current_user_can( 'edit_post', $post_id ); } /** * Update the target post's title, content, excerpt, categories, and tags. * * @param WP_Post $source_post Post object to be copied. * @param int $target_post_id Target post ID. * @return int 0 on failure, or the updated post ID on success. */ protected function update_content( $source_post, $target_post_id ) { $data = array( 'ID' => $target_post_id, 'post_title' => $source_post->post_title, 'post_content' => $source_post->post_content, 'post_excerpt' => $source_post->post_excerpt, 'comment_status' => $source_post->comment_status, 'ping_status' => $source_post->ping_status, 'post_category' => wp_get_post_categories( $source_post->ID ), 'post_password' => $source_post->post_password, 'tags_input' => $source_post->tags_input, ); // Copy footnotes with regenerated IDs. $data = $this->copy_footnotes( $data, $source_post, $target_post_id ); /** * Fires just before the target post is updated with its new data. * Allows for final data adjustments before updating the target post. * * @module copy-post * * @since 7.0.0 * * @param array $data Post data with which to update the target (new) post. * @param WP_Post $source_post Post object being copied. * @param int $target_post_id Target post ID. */ $data = apply_filters( 'jetpack_copy_post_data', $data, $source_post, $target_post_id ); return wp_update_post( $data ); } /** * Update terms for post types. * * @param WP_Post $source_post Post object to be copied. * @param int $target_post_id Target post ID. * @return array Results of attempts to set each term to the target (new) post. */ protected function update_post_type_terms( $source_post, $target_post_id ) { $results = array(); $bypassed_post_types = apply_filters( 'jetpack_copy_post_bypassed_post_types', array( 'post', 'page' ), $source_post, $target_post_id ); if ( in_array( $source_post->post_type, $bypassed_post_types, true ) ) { return $results; } $taxonomies = get_object_taxonomies( $source_post, 'objects' ); foreach ( $taxonomies as $taxonomy ) { $terms = wp_get_post_terms( $source_post->ID, $taxonomy->name, array( 'fields' => 'ids' ) ); $results[] = wp_set_post_terms( $target_post_id, $terms, $taxonomy->name ); } return $results; } /** * Update the target post's featured image. * * @param WP_Post $source_post Post object to be copied. * @param int $target_post_id Target post ID. * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure. */ protected function update_featured_image( $source_post, $target_post_id ) { $featured_image_id = get_post_thumbnail_id( $source_post ); return update_post_meta( $target_post_id, '_thumbnail_id', $featured_image_id ); } /** * Update the target post's post format. * * @param WP_Post $source_post Post object to be copied. * @param int $target_post_id Target post ID. * @return array|WP_Error|false WP_Error on error, array of affected term IDs on success. */ protected function update_post_format( $source_post, $target_post_id ) { $post_format = get_post_format( $source_post ); return set_post_format( $target_post_id, $post_format ); } /** * Ensure the block editor doesn't modify the source post content for non-standard post formats. * * @param array $settings Settings to be passed into the block editor. * @return array Settings with any `template` key removed. */ public function remove_post_format_template( $settings ) { unset( $settings['template'] ); return $settings; } /** * Update the target post's Likes and Sharing statuses. * * @param WP_Post $source_post Post object to be copied. * @param int $target_post_id Target post ID. * @return array Array with the results of each update action. */ protected function update_likes_sharing( $source_post, $target_post_id ) { $likes = get_post_meta( $source_post->ID, 'switch_like_status', true ); $sharing = get_post_meta( $source_post->ID, 'sharing_disabled', true ); if ( '' !== $likes ) { $likes_result = update_post_meta( $target_post_id, 'switch_like_status', $likes ); } else { $likes_result = null; } if ( '' !== $sharing ) { $sharing_result = update_post_meta( $target_post_id, 'sharing_disabled', $sharing ); } else { $sharing_result = null; } return array( 'likes' => $likes_result, 'sharing' => $sharing_result, ); } /** * Copy footnotes from source post, regenerating IDs for uniqueness. * * Gutenberg's Footnotes block stores content in post meta rather than * in the block markup itself. The IDs must be regenerated to ensure uniqueness, * otherwise multiple posts on the same page (e.g., archive views) would * have conflicting footnote anchor links. * * @param array $data Post data with which to update the target (new) post. * @param WP_Post $source_post Post object being copied. * @param int $target_post_id Target post ID. * @return array Modified post data. */ public function copy_footnotes( $data, $source_post, $target_post_id ) { $footnotes_json = get_post_meta( $source_post->ID, 'footnotes', true ); if ( '' === $footnotes_json ) { return $data; } $footnotes = json_decode( $footnotes_json, true ); if ( ! is_array( $footnotes ) || empty( $footnotes ) ) { return $data; } // Build a mapping of old IDs to new IDs and update the footnotes array. $id_mapping = array(); foreach ( $footnotes as &$footnote ) { if ( isset( $footnote['id'] ) ) { $old_id = $footnote['id']; $new_id = wp_generate_uuid4(); $id_mapping[ $old_id ] = $new_id; $footnote['id'] = $new_id; } } unset( $footnote ); // Update the post content to use the new footnote IDs. foreach ( $id_mapping as $old_id => $new_id ) { // Replace data-fn attribute values. $data['post_content'] = str_replace( 'data-fn="' . $old_id . '"', 'data-fn="' . $new_id . '"', $data['post_content'] ); // Replace href anchor links. $data['post_content'] = str_replace( 'href="#' . $old_id . '"', 'href="#' . $new_id . '"', $data['post_content'] ); // Replace id attributes (e.g., id="uuid-link"). $data['post_content'] = str_replace( 'id="' . $old_id . '-link"', 'id="' . $new_id . '-link"', $data['post_content'] ); $data['post_content'] = str_replace( 'id="' . $old_id . '"', 'id="' . $new_id . '"', $data['post_content'] ); } // Save the footnotes meta with new IDs. update_post_meta( $target_post_id, 'footnotes', wp_json_encode( $footnotes, JSON_UNESCAPED_SLASHES ) ); return $data; } /** * Update the target post's title. * * @param string $post_title Post title determined by `get_default_post_to_edit()`. * @param WP_Post $post Post object of newly-inserted post. * @return string Updated post title from source post. */ public function filter_title( $post_title, $post ) { return $post->post_title; } /** * Update the target post's content (`post_content`). * * @param string $post_content Post content determined by `get_default_post_to_edit()`. * @param WP_Post $post Post object of newly-inserted post. * @return string Updated post content from source post. */ public function filter_content( $post_content, $post ) { return $post->post_content; } /** * Update the target post's excerpt. * * @param string $post_excerpt Post excerpt determined by `get_default_post_to_edit()`. * @param WP_Post $post Post object of newly-inserted post. * @return string Updated post excerpt from source post. */ public function filter_excerpt( $post_excerpt, $post ) { return $post->post_excerpt; } /** * Validate the post type to be used for the target post. * * @param WP_Post $post Post object of current post in listing. * @return bool True if the post type is in a list of supported psot types; false otherwise. */ protected function validate_post_type( $post ) { /** * Fires when determining if the "Copy" row action should be made available. * Allows overriding supported post types. * * @module copy-post * * @since 7.0.0 * * @param array Post types supported by default. * @param WP_Post $post Post object of current post in listing. */ $valid_post_types = apply_filters( 'jetpack_copy_post_post_types', array( 'post', 'page', 'jetpack-testimonial', 'jetpack-portfolio', ), $post ); return in_array( $post->post_type, $valid_post_types, true ); } /** * Add a "Copy" row action to supported posts/pages/CPTs on list views. * * @param array $actions Existing actions. * @param WP_Post $post Post object of current post in list. * @return array Array of updated row actions. */ public function add_row_action( $actions, $post ) { if ( ! $this->user_can_access_post( $post->ID ) || ! $post instanceof WP_Post || ! $this->validate_post_type( $post ) ) { return $actions; } $edit_url = add_query_arg( array( 'post_type' => $post->post_type, 'jetpack-copy' => $post->ID, ), admin_url( 'post-new.php' ) ); $jetpack_logo = new Logo(); $edit_action = array( 'jetpack-copy' => sprintf( '<a href="%1$s" aria-label="%2$s">%3$s %4$s</a>', esc_url( $edit_url ), esc_attr__( 'Duplicate this post with Jetpack.', 'jetpack' ), esc_html__( 'Duplicate', 'jetpack' ), $jetpack_logo->get_jp_emblem() ), ); // Insert the Copy action before the Trash action. $edit_offset = array_search( 'trash', array_keys( $actions ), true ); $updated_actions = array_merge( array_slice( $actions, 0, $edit_offset ), $edit_action, array_slice( $actions, $edit_offset ) ); /** * Fires after the new Copy action has been added to the row actions. * Allows changes to the action presentation, or other final checks. * * @module copy-post * * @since 7.0.0 * * @param array $updated_actions Updated row actions with the Copy Post action. * @param array $actions Original row actions passed to this filter. * @param WP_Post $post Post object of current post in listing. */ return apply_filters( 'jetpack_copy_post_row_actions', $updated_actions, $actions, $post ); } } /** * Instantiate an instance of Jetpack_Copy_Post on the `admin_init` hook. */ function jetpack_copy_post_init() { new Jetpack_Copy_Post(); } add_action( 'admin_init', 'jetpack_copy_post_init' ); seo-tools/class-jetpack-seo.php 0000644 00000021505 15174711637 0012530 0 ustar 00 <?php /** * Main class file for the SEO Tools module. * * @package automattic/jetpack */ /** * An SEO expert walks into a bar, bars, pub, public house, Irish pub, drinks, beer, wine, liquor, Grey Goose, Cristal... * * @phan-constructor-used-for-side-effects */ class Jetpack_SEO { /** * Constructor. */ public function __construct() { add_action( 'init', array( $this, 'init' ) ); } /** * Initialization method for Jetpack_SEO. */ public function init() { /** * Can be used to prevent SEO tools from inserting custom meta tags. * * @module seo-tools * * @since 4.4.0 * * @param bool true Should Jetpack's SEO Meta Tags be enabled. Defaults to true. */ if ( apply_filters( 'jetpack_seo_meta_tags_enabled', true ) ) { add_action( 'wp_head', array( $this, 'meta_tags' ) ); // Add support for editing page excerpts in pages, regardless of theme support. add_post_type_support( 'page', 'excerpt' ); } /** * Can be used to prevent SEO tools from modifying site titles. * * @module seo-tools * * @since 4.4.0 * * @param bool true Should Jetpack SEO modify site titles. Defaults to true. */ if ( apply_filters( 'jetpack_seo_custom_titles', true ) ) { // Overwrite page title with custom SEO meta title for themes that support title-tag. add_filter( 'pre_get_document_title', array( 'Jetpack_SEO_Titles', 'get_custom_title' ) ); // Add overwrite support for themes that don't support title-tag. add_filter( 'wp_title', array( 'Jetpack_SEO_Titles', 'get_custom_title' ) ); } add_filter( 'jetpack_open_graph_tags', array( $this, 'set_custom_og_tags' ) ); Jetpack_SEO_Posts::register_post_meta(); // Exclude posts with 'jetpack_seo_noindex' set true from the Jetpack sitemap. add_filter( 'jetpack_sitemap_skip_post', array( 'Jetpack_SEO_Posts', 'exclude_noindex_posts_from_jetpack_sitemap' ), 10, 2 ); add_action( 'rest_api_init', array( $this, 'add_custom_field_post_type_meta' ) ); } /** * Add custom field meta to all public post types that don't already have it. */ public function add_custom_field_post_type_meta() { /** * Filter the list of post types for which custom fields support is added. * * This filter allows modification of the post types that will be processed * to add support for custom fields if they do not already support it. * * @since 14.2 * * @param array $post_types An array of post type names. */ $post_types = apply_filters( 'jetpack_seo_custom_field_post_types', get_post_types( array( 'public' => true, 'show_ui' => true, '_builtin' => false, ) ) ); foreach ( $post_types as $post_type ) { if ( ! post_type_supports( $post_type, 'custom-fields' ) ) { add_post_type_support( $post_type, 'custom-fields' ); } } } /** * Helper method to fetch authors. */ private function get_authors() { global $wp_query; $authors = array(); foreach ( $wp_query->posts as $post ) { if ( ! $post instanceof WP_Post ) { continue; } $authors[] = get_the_author_meta( 'display_name', (int) $post->post_author ); } $authors = array_unique( $authors ); return $authors; } /** * Constructs open graph tag data. * * @param array $tags Array of tag data. * @return array of tag data. */ public function set_custom_og_tags( $tags ) { $custom_title = Jetpack_SEO_Titles::get_custom_title(); if ( ! empty( $custom_title ) ) { $tags['og:title'] = $custom_title; } $post_custom_description = Jetpack_SEO_Posts::get_post_custom_description( get_post() ); $front_page_meta = Jetpack_SEO_Utils::get_front_page_meta_description(); if ( class_exists( 'woocommerce' ) && is_shop() ) { $shop_page_id = get_option( 'woocommerce_shop_page_id' ); if ( $shop_page_id ) { $post_custom_description = Jetpack_SEO_Posts::get_post_custom_description( get_post( $shop_page_id ) ); } } if ( is_front_page() && ! empty( $front_page_meta ) ) { $tags['og:description'] = $front_page_meta; } elseif ( ! empty( $post_custom_description ) ) { $tags['og:description'] = $post_custom_description; } return $tags; } /** * Outputs Jetpack's SEO <meta> tags. */ public function meta_tags() { global $wp_query; $post_count = is_countable( $wp_query->posts ) ? count( $wp_query->posts ) : 0; $period = ''; $template = ''; $meta = array(); /** * Can be used to specify a list of themes that set their own meta tags. * * If current site is using one of the themes listed as conflicting, inserting Jetpack SEO * meta tags will be prevented. * * @module seo-tools * * @since 4.4.0 * * @param array List of conflicted theme names. Defaults to empty array. */ $conflicted_themes = apply_filters( 'jetpack_seo_meta_tags_conflicted_themes', array() ); if ( isset( $conflicted_themes[ get_option( 'template' ) ] ) ) { return; } $front_page_meta = Jetpack_SEO_Utils::get_front_page_meta_description(); $description = $front_page_meta ? $front_page_meta : get_bloginfo( 'description' ); $meta['description'] = trim( $description ); // Try to target things if we're on a "specific" page of any kind. if ( is_singular() ) { if ( ! ( is_front_page() && Jetpack_SEO_Utils::get_front_page_meta_description() ) ) { $description = Jetpack_SEO_Posts::get_post_description( get_post() ); if ( $description ) { $description = wp_trim_words( strip_shortcodes( wp_strip_all_tags( $description, true ) ) ); $meta['description'] = $description; } } } elseif ( is_author() ) { $obj = get_queried_object(); $meta['description'] = sprintf( /* translators: first property is an user's display name, the second is the site's title. */ _x( 'Read all of the posts by %1$s on %2$s', 'Read all of the posts by Author Name on Blog Title', 'jetpack' ), isset( $obj->display_name ) ? $obj->display_name : __( 'the author', 'jetpack' ), get_bloginfo( 'title' ) ); } elseif ( is_tag() || is_category() || is_tax() ) { $obj = get_queried_object(); $description = ''; if ( isset( $obj->term_id ) && isset( $obj->taxonomy ) ) { $description = get_term_field( 'description', $obj->term_id, $obj->taxonomy, 'raw' ); } if ( ! is_wp_error( $description ) && $description ) { $meta['description'] = wp_trim_words( $description ); } else { $authors = $this->get_authors(); $meta['description'] = wp_sprintf( /* translators: %1$s: A post category. %2$l: Post authors. */ _x( 'Posts about %1$s written by %2$l', 'Posts about Category written by John and Bob', 'jetpack' ), single_term_title( '', false ), $authors ); } } elseif ( is_date() ) { if ( is_year() ) { $period = get_query_var( 'year' ); /* translators: %1$s: Number of posts published. %2$l: Post author. %3$s: A year date. */ $template = _nx( '%1$s post published by %2$l in the year %3$s', // Singular. '%1$s posts published by %2$l in the year %3$s', // Plural. $post_count, // Number. '10 posts published by John in the year 2012', // Context. 'jetpack' ); } elseif ( is_month() ) { $period = gmdate( 'F Y', mktime( 0, 0, 0, get_query_var( 'monthnum' ), 1, get_query_var( 'year' ) ) ); /* translators: %1$s: Number of posts published. %2$l: Post author. %3$s: A month/year date. */ $template = _nx( '%1$s post published by %2$l during %3$s', // Singular. '%1$s posts published by %2$l during %3$s', // Plural. $post_count, // Number. '10 posts publishes by John during May 2012', // Context. 'jetpack' ); } elseif ( is_day() ) { $period = gmdate( 'F j, Y', mktime( 0, 0, 0, get_query_var( 'monthnum' ), get_query_var( 'day' ), get_query_var( 'year' ) ) ); /* translators: %1$s: Number of posts published. %2$l: Post author. %3$s: A month/day/year date. */ $template = _nx( '%1$s post published by %2$l on %3$s', // Singular. '%1$s posts published by %2$l on %3$s', // Plural. $post_count, // Number. '10 posts published by John on May 30, 2012', // Context. 'jetpack' ); } $authors = $this->get_authors(); $meta['description'] = wp_sprintf( $template, $post_count, $authors, $period ); } $mark_as_noindex = Jetpack_SEO_Posts::get_post_noindex_setting( get_post() ); if ( $mark_as_noindex ) { $meta['robots'] = 'noindex'; } /** * Can be used to edit the default SEO tools meta tags. * * @module seo-tools * * @since 4.4.0 * * @param array Array that consists of meta name and meta content pairs. */ $meta = apply_filters( 'jetpack_seo_meta_tags', $meta ); // Output them. foreach ( $meta as $name => $content ) { if ( ! empty( $content ) ) { echo '<meta name="' . esc_attr( $name ) . '" content="' . esc_attr( $content ) . '" />' . "\n"; } } } } seo-tools/class-jetpack-seo-titles.php 0000644 00000023036 15174711637 0014033 0 ustar 00 <?php /** * Class containing utility static methods for managing SEO custom title formats. * * @package automattic/jetpack */ /* * Each title format is an array of arrays containing two values: * - type * - value * * Possible values for type are: 'token' and 'string'. * Possible values for 'value' are: any string in case that 'type' is set * to 'string', or allowed token values for page type in case that 'type' * is set to 'token'. * * Examples of valid formats: * * [ * 'front_page' => [ * [ 'type' => 'string', 'value' => 'Front page title and site name:'], * [ 'type' => 'token', 'value' => 'site_name'] * ], * 'posts' => [ * [ 'type' => 'token', 'value' => 'site_name' ], * [ 'type' => 'string', 'value' => ' | ' ], * [ 'type' => 'token', 'value' => 'post_title' ] * ], * 'pages' => [], * 'groups' => [], * 'archives' => [] * ] * Custom title for given page type is created by concatenating all of the array 'value' parts. * Tokens are replaced with their corresponding values for current site. * Empty array signals that we are not overriding the default title for particular page type. */ /** * Class containing utility static methods for managing SEO custom title formats. */ class Jetpack_SEO_Titles { /** * Site option name used to store custom title formats. */ const TITLE_FORMATS_OPTION = 'advanced_seo_title_formats'; /** * Retrieves custom title formats from site option. * * @return array Array of custom title formats, or empty array. */ public static function get_custom_title_formats() { if ( Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { return get_option( self::TITLE_FORMATS_OPTION, array() ); } return array(); } /** * Returns tokens that are currently supported for each page type. * * @return array Array of allowed token strings. */ public static function get_allowed_tokens() { return array( 'front_page' => array( 'site_name', 'tagline' ), 'posts' => array( 'site_name', 'tagline', 'post_title' ), 'pages' => array( 'site_name', 'tagline', 'page_title' ), 'groups' => array( 'site_name', 'tagline', 'group_title' ), 'archives' => array( 'site_name', 'tagline', 'date', 'archive_title' ), ); } /** * Used to modify the default title with custom SEO title. * * @param string $default_title Default title for current page. * * @return string A custom per-post title, custom title structure with replaced tokens, or default title. */ public static function get_custom_title( $default_title = '' ) { // Don't filter title for unsupported themes. if ( self::is_conflicted_theme() ) { return $default_title; } $page_type = self::get_page_type(); // Keep default title if invalid page type is supplied. if ( empty( $page_type ) ) { return $default_title; } if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { return $default_title; } // If it's a singular -- page or post -- check for a meta title override. if ( 'pages' === $page_type || 'posts' === $page_type ) { $post = get_post(); if ( $post instanceof WP_Post ) { $custom_title = get_post_meta( $post->ID, Jetpack_SEO_Posts::HTML_TITLE_META_KEY, true ); if ( ! empty( trim( $custom_title ) ) ) { return esc_html( $custom_title ); } } } $title_formats = self::get_custom_title_formats(); // Keep default title if user has not defined custom title for this page type. if ( empty( $title_formats[ $page_type ] ) ) { return $default_title; } $custom_title = ''; $format_array = $title_formats[ $page_type ]; foreach ( $format_array as $item ) { if ( 'token' === $item['type'] ) { $custom_title .= self::get_token_value( $item['value'] ); } else { $custom_title .= $item['value']; } } return esc_html( $custom_title ); } /** * Returns string value for given token. * * @param string $token_name The token name value that should be replaced. * * @return string Token replacement for current site, or empty string for unknown token name. */ public static function get_token_value( $token_name ) { switch ( $token_name ) { case 'site_name': return get_bloginfo( 'name' ); case 'tagline': return get_bloginfo( 'description' ); case 'post_title': case 'page_title': return the_title_attribute( array( 'echo' => false ) ); case 'group_title': return single_tag_title( '', false ); case 'date': case 'archive_title': return self::get_archive_title(); default: return ''; } } /** * Returns page type for current page. We need this helper in order to determine what * user defined title format should be used for custom title. * * @return string|bool Type of current page or false if unsupported. */ public static function get_page_type() { if ( is_front_page() ) { return 'front_page'; } if ( is_category() || is_tag() || is_tax() ) { return 'groups'; } if ( is_archive() && ! is_author() ) { return 'archives'; } if ( is_page() ) { return 'pages'; } if ( is_singular() ) { return 'posts'; } return false; } /** * Returns the value that should be used as a replacement for the `date` or `archive_title` tokens. * For date-based archives, a date is returned. Otherwise the `post_type_archive_title` is returned. * * The `archive_title` token was added after the `date` token to provide a more generic option * that would work for non date-based archives. * * @return string Token replaced string. */ public static function get_archive_title() { // If archive year, month, and day are specified. if ( is_day() ) { return get_the_date(); } // If archive year, and month are specified. if ( is_month() ) { return trim( single_month_title( ' ', false ) ); } // Only archive year is specified. if ( is_year() ) { return get_query_var( 'year' ); } // Not a date based archive. // An example would be "Projects" for Jetpack's Portoflio CPT. return post_type_archive_title( '', false ); } /** * Checks if current theme is defining custom title that won't work nicely * with our custom SEO title override. * * @return bool True if current theme sets custom title, false otherwise. */ public static function is_conflicted_theme() { /** * Can be used to specify a list of themes that use their own custom title format. * * If current site is using one of the themes listed as conflicting, * Jetpack SEO custom title formats will be disabled. * * @module seo-tools * * @since 4.4.0 * * @param array List of conflicted theme names. Defaults to empty array. */ $conflicted_themes = apply_filters( 'jetpack_seo_custom_title_conflicted_themes', array() ); return isset( $conflicted_themes[ get_option( 'template' ) ] ); } /** * Checks if a given format conforms to predefined SEO title templates. * * Every format type and token must be specifically allowed. * * @see get_allowed_tokens() * * @param array $title_formats Template of SEO title to check. * * @return bool True if the formats are valid, false otherwise. */ public static function are_valid_title_formats( $title_formats ) { $allowed_tokens = self::get_allowed_tokens(); if ( ! is_array( $title_formats ) ) { return false; } foreach ( $title_formats as $format_type => $format_array ) { if ( ! array_key_exists( $format_type, $allowed_tokens ) ) { return false; } if ( '' === $format_array ) { continue; } if ( ! is_array( $format_array ) ) { return false; } foreach ( $format_array as $item ) { if ( empty( $item['type'] ) || empty( $item['value'] ) ) { return false; } if ( 'token' === $item['type'] ) { if ( ! in_array( $item['value'], $allowed_tokens[ $format_type ], true ) ) { return false; } } } } return true; } /** * Sanitizes the arbitrary user input strings for custom SEO titles. * * @param array $title_formats Array of custom title formats. * * @return array The sanitized array. */ public static function sanitize_title_formats( $title_formats ) { foreach ( $title_formats as &$format_array ) { foreach ( $format_array as &$item ) { if ( 'string' === $item['type'] ) { // From `wp_strip_all_tags`, but omitting the `trim` portion since we want spacing preserved. $item['value'] = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $item['value'] ); $item['value'] = strip_tags( $item['value'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags $item['value'] = preg_replace( '/[\r\n\t ]+/', ' ', $item['value'] ); } } } unset( $format_array ); unset( $item ); return $title_formats; } /** * Combines the previous values of title formats, stored as array in site options, * with the new values that are provided. * * @param array $new_formats Array containing new title formats. * * @return array $result Array of updated title formats, or empty array if no update was performed. */ public static function update_title_formats( $new_formats ) { $new_formats = self::sanitize_title_formats( $new_formats ); // Empty array signals that custom title shouldn't be used. $empty_formats = array( 'front_page' => array(), 'posts' => array(), 'pages' => array(), 'groups' => array(), 'archives' => array(), ); $previous_formats = self::get_custom_title_formats(); $result = array_merge( $empty_formats, $previous_formats, $new_formats ); if ( update_option( self::TITLE_FORMATS_OPTION, $result ) ) { return $result; } return array(); } } seo-tools/class-jetpack-seo-posts.php 0000644 00000012074 15174711637 0013677 0 ustar 00 <?php /** * Class containing utility static methods for managing SEO options for Posts and Pages. * * @package automattic/jetpack */ /** * Provides static utility methods for managing SEO options for Posts and Pages. */ class Jetpack_SEO_Posts { /** * Key of the post meta values that will be used to store post custom data. */ const DESCRIPTION_META_KEY = 'advanced_seo_description'; const HTML_TITLE_META_KEY = 'jetpack_seo_html_title'; const NOINDEX_META_KEY = 'jetpack_seo_noindex'; const POST_META_KEYS_ARRAY = array( self::DESCRIPTION_META_KEY, self::HTML_TITLE_META_KEY, self::NOINDEX_META_KEY, ); /** * Build meta description for post SEO. * * @param WP_Post|null $post Source of data for custom description. * * @return string Post description or empty string. */ public static function get_post_description( $post = null ) { $post = get_post( $post ); if ( ! ( $post instanceof WP_Post ) ) { return ''; } if ( post_password_required() || ! is_singular() ) { return ''; } // Business users can overwrite the description. $custom_description = self::get_post_custom_description( $post ); if ( ! empty( $custom_description ) ) { return $custom_description; } if ( ! empty( $post->post_excerpt ) ) { return $post->post_excerpt; } // Remove content within wp:query blocks and return. return Jetpack_SEO_Utils::remove_query_blocks( $post->post_content ); } /** * Returns post's custom meta description if it is set, and if * SEO tools are enabled for current blog. * * @param WP_Post|null $post Source of data for custom description. * * @return string Custom description or empty string */ public static function get_post_custom_description( $post = null ) { $post = get_post( $post ); if ( ! ( $post instanceof WP_Post ) ) { return ''; } $custom_description = get_post_meta( $post->ID, self::DESCRIPTION_META_KEY, true ); if ( empty( $custom_description ) || ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { return ''; } return $custom_description; } /** * Gets a custom HTML title for a post if one is set, and if * SEO tools are enabled for the current blog. * * @param WP_Post|null $post Source of data for the custom HTML title. * * @return string Custom HTML title or an empty string if not set. */ public static function get_post_custom_html_title( $post = null ) { $post = get_post( $post ); if ( ! ( $post instanceof WP_Post ) ) { return ''; } $custom_html_title = get_post_meta( $post->ID, self::HTML_TITLE_META_KEY, true ); if ( empty( $custom_html_title ) || ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { return ''; } return $custom_html_title; } /** * Gets the `jetpack_seo_noindex` setting for a post, if * SEO tools are enabled for the current blog. * * @param WP_Post|null $post Provided post or defaults to the global post. * * @return bool True if post should be marked as noindex, false otherwise. */ public static function get_post_noindex_setting( $post = null ) { $post = get_post( $post ); if ( ! ( $post instanceof WP_Post ) ) { return false; } $mark_as_noindex = get_post_meta( $post->ID, self::NOINDEX_META_KEY, true ); if ( empty( $mark_as_noindex ) || ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) { return false; } return (bool) $mark_as_noindex; } /** * Filter callback for `jetpack_sitemap_skip_post`; if a post has `jetpack_seo_noindex` set to true, * then exclude that post from the Jetpack sitemap. * * @param bool $skip Whether to skip the post in the sitemap. * @param WP_Post $post The post to check. * * @return bool */ public static function exclude_noindex_posts_from_jetpack_sitemap( $skip, $post ) { $exclude = self::get_post_noindex_setting( $post ); if ( $exclude ) { $skip = true; } return $skip; } /** * Registers the following meta keys for use in the REST API: * - self::DESCRIPTION_META_KEY * - self::HTML_TITLE_META_KEY */ public static function register_post_meta() { $description_args = array( 'type' => 'string', 'description' => __( 'Custom post description to be used in HTML <meta /> tag.', 'jetpack' ), 'single' => true, 'default' => '', 'show_in_rest' => array( 'name' => self::DESCRIPTION_META_KEY, ), ); $html_title_args = array( 'type' => 'string', 'description' => __( 'Custom title to be used in HTML <title /> tag.', 'jetpack' ), 'single' => true, 'default' => '', 'show_in_rest' => array( 'name' => self::HTML_TITLE_META_KEY, ), ); $noindex_args = array( 'type' => 'boolean', 'description' => __( 'Whether to hide the post from search engines and the Jetpack sitemap.', 'jetpack' ), 'single' => true, 'default' => false, 'show_in_rest' => array( 'name' => self::NOINDEX_META_KEY, ), ); register_meta( 'post', self::DESCRIPTION_META_KEY, $description_args ); register_meta( 'post', self::HTML_TITLE_META_KEY, $html_title_args ); register_meta( 'post', self::NOINDEX_META_KEY, $noindex_args ); } } seo-tools/class-jetpack-seo-utils.php 0000644 00000010572 15174711637 0013670 0 ustar 00 <?php /** * Class containing utility static methods that other SEO tools are relying on. * * @package automattic/jetpack */ /** * Class containing utility static methods that other SEO tools are relying on. */ class Jetpack_SEO_Utils { /** * Site option name used to store front page meta description. */ const FRONT_PAGE_META_OPTION = 'advanced_seo_front_page_description'; /** * The LEGACY_META_OPTION is used to support legacy usage on WPcom simple sites (free or paid). * For WPorg JP sites, the JP seo-tools features were made free for all sites (free or paid). */ const LEGACY_META_OPTION = 'seo_meta_description'; /** * Used to check whether SEO tools are enabled for given site. * * @return bool True if SEO tools are enabled, false otherwise. */ public static function is_enabled_jetpack_seo() { /** * Can be used by SEO plugin authors to disable the conflicting output of SEO Tools. * * @module seo-tools * * @since 5.0.0 * * @param bool True if SEO Tools should be disabled, false otherwise. */ if ( apply_filters( 'jetpack_disable_seo_tools', false ) ) { return false; } if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { return wpcom_site_has_feature( 'advanced-seo', get_current_blog_id() ); } return true; } /** * Checks if this option was set while it was freely available to all WPcom simple sites. * * @return bool True if we should enable legacy usage, false otherwise. */ public static function has_legacy_front_page_meta() { return ! self::is_enabled_jetpack_seo() && get_option( self::LEGACY_META_OPTION ); } /** * Returns front page meta description for current site. * * @return string Front page meta description string or empty string. */ public static function get_front_page_meta_description() { if ( self::is_enabled_jetpack_seo() ) { $front_page_meta = get_option( self::FRONT_PAGE_META_OPTION ); return $front_page_meta ? $front_page_meta : get_option( self::LEGACY_META_OPTION, '' ); } // Support legacy usage for WPcom simple sites. return get_option( self::LEGACY_META_OPTION, '' ); } /** * Sanitizes the custom front page meta description input. * * @param string $value Front page meta string. * * @return string The sanitized string. */ public static function sanitize_front_page_meta_description( $value ) { return wp_strip_all_tags( $value ); } /** * Updates the site option value for front page meta description. * * @param string $value New value for front page meta description. * * @return string Saved value, or empty string if no update was performed. */ public static function update_front_page_meta_description( $value ) { $front_page_description = self::sanitize_front_page_meta_description( $value ); /** * Can be used to limit the length of front page meta description. * * @module seo-tools * * @since 4.4.0 * * @param int Maximum length of front page meta description. Defaults to 300. */ $description_max_length = apply_filters( 'jetpack_seo_front_page_description_max_length', 300 ); if ( function_exists( 'mb_substr' ) ) { $front_page_description = mb_substr( $front_page_description, 0, $description_max_length ); } else { $front_page_description = substr( $front_page_description, 0, $description_max_length ); } $can_set_meta = self::is_enabled_jetpack_seo(); $legacy_meta_option = get_option( self::LEGACY_META_OPTION ); $has_old_meta = ! empty( $legacy_meta_option ); $option_name = self::has_legacy_front_page_meta() ? self::LEGACY_META_OPTION : self::FRONT_PAGE_META_OPTION; $did_update = update_option( $option_name, $front_page_description ); if ( $did_update && $has_old_meta && $can_set_meta ) { // Delete legacy option if user has switched to Business or eCommerce plan and updated the front page meta description. delete_option( self::LEGACY_META_OPTION ); } if ( $did_update ) { return $front_page_description; } return ''; } /** * Remove content within wp:query blocks. * * @uses jetpack_og_remove_query_blocks * * @since 14.9 * * @param string $content Post content. * * @return string Post content stripped from wp:query blocks. */ public static function remove_query_blocks( $content ) { if ( ! function_exists( 'jetpack_og_remove_query_blocks' ) ) { return $content; } return jetpack_og_remove_query_blocks( $content ); } } blaze.php 0000644 00000002035 15174711637 0006366 0 ustar 00 <?php /** * Module Name: Blaze * Module Description: Promote your posts and pages across millions of sites in the WordPress.com and Tumblr ad network. * Sort Order: 22 * Recommendation Order: 12 * First Introduced: 12.3 * Requires Connection: Yes * Auto Activate: Yes * Module Tags: Traffic, Social * Additional Search Queries: advertising, ads * * @package automattic/jetpack */ use Automattic\Jetpack\Blaze; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } Blaze::init(); /** * Remove post row Blaze actions in the Jetpack plugin. * Keep them on for products. * * @param bool $are_quick_links_enabled Should Blaze row actions be enabled. * @param WP_Post $post The current post in the post list table. * * @return bool */ function jetpack_blaze_post_row_actions_disable( $are_quick_links_enabled, $post ) { if ( 'product' !== $post->post_type ) { return false; } return $are_quick_links_enabled; } add_filter( 'jetpack_blaze_post_row_actions_enable', 'jetpack_blaze_post_row_actions_disable', 10, 2 ); gravatar/gravatar-hovercards-amp.css 0000644 00000000057 15174711637 0013623 0 ustar 00 .comment-author figcaption { display: none; } comment-likes/comment-like-count.js 0000644 00000002060 15174711637 0013375 0 ustar 00 jQuery( document ).ready( function ( $ ) { var jsonAPIbase = 'https://public-api.wordpress.com/rest/v1', APIqueue = []; function getCommentLikeCounts() { $( '.comment-like-count' ).each( function () { var blogId = $( this ).attr( 'data-blog-id' ), commentId = $( this ).attr( 'data-comment-id' ); APIqueue.push( '/sites/' + blogId + '/comments/' + commentId + '/likes' ); } ); return $.ajax( { type: 'GET', url: jsonAPIbase + '/batch', dataType: 'jsonp', data: 'urls[]=' + APIqueue.map( encodeURIComponent ).join( '&urls[]=' ), success: function ( response ) { for ( var path in response ) { if ( ! response[ path ].error_data ) { var urlPieces = path.split( '/' ), commentId = urlPieces[ 4 ], likeCount = response[ path ].found; if ( likeCount < 1 ) { return; } $( '#comment-like-count-' + commentId ) .find( '.like-count' ) .hide() .text( likeCount ) .fadeIn(); } } }, error: function () {}, } ); } getCommentLikeCounts(); } ); comment-likes/admin-style.css 0000644 00000001514 15174711637 0012270 0 ustar 00 .fixed .column-comment_likes { width: 5.5em; padding: 8px 0; text-align: center; } .fixed .column-stats { width: 5em; white-space: nowrap; } .fixed .column-comment_likes .comment-like-count { box-sizing: border-box; display: inline-block; padding: 0 8px; height: 2em; margin-top: 5px; border-radius: 5px; background-color: #787c82; color: #fff; font-size: 11px; line-height: 21px; } .fixed .column-comment_likes .comment-like-count::after { border: none; } .fixed .column-comment_likes .comment-like-count:hover { background-color: #2271b1; } .fixed .column-comment_likes .vers::before { font: 400 20px/1 dashicons; content: "\f155"; speak: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @media screen and (max-width: 782px) { .fixed .column-comment_likes { display: none; } } markdown/easy-markdown.php 0000644 00000074533 15174711637 0011710 0 ustar 00 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Plugin URI: https://automattic.com/ * Plugin Name: Easy Markdown * Description: Write in Markdown, publish in WordPress * Version: 0.1 * Author: Matt Wiebe * Author URI: https://automattic.com/ * Text Domain: jetpack * * @package automattic/jetpack */ /** * Copyright (c) Automattic. All rights reserved. * * Released under the GPL license * https://www.opensource.org/licenses/gpl-license.php * * This is an add-on for WordPress * https://wordpress.org/ * * ********************************************************************** * 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. * ********************************************************************** */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * WPCom_Markdown class. */ class WPCom_Markdown { const POST_OPTION = 'wpcom_publish_posts_with_markdown'; const COMMENT_OPTION = 'wpcom_publish_comments_with_markdown'; const POST_TYPE_SUPPORT = 'wpcom-markdown'; const IS_MD_META = '_wpcom_is_markdown'; /** * Our markdown parser. * * @var WPCom_GHF_Markdown_Parser */ private static $parser; /** * An instance of the markdown class. * * @var WPCom_Markdown */ private static $instance; /** * To ensure that our munged posts over xml-rpc are removed from the cache. * * @var array */ public $posts_to_uncache = array(); /** * Posts and parents to monitor. * * @var array */ private $monitoring = array( 'post' => array(), 'parent' => array(), ); /** * Whether or not kses filters were removed. Only set if removal was attempted. * * @var ?bool */ public $kses; /** * Yay singletons! * * @return object WPCom_Markdown instance */ public static function get_instance() { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Kicks things off on `init` action */ public function load() { $this->add_default_post_type_support(); $this->maybe_load_actions_and_filters(); if ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) { // phpcs:ignore WPCUT.SwitchBlog.SwitchBlog -- wpcom flags **every** use of switch_blog, apparently expecting valid instances to ignore or suppress the sniff. add_action( 'switch_blog', array( $this, 'maybe_load_actions_and_filters' ), 10, 2 ); } add_action( 'admin_init', array( $this, 'register_setting' ) ); add_action( 'admin_init', array( $this, 'maybe_unload_for_bulk_edit' ) ); if ( current_theme_supports( 'o2' ) || class_exists( 'P2' ) ) { $this->add_o2_helpers(); } } /** * If we're in a bulk edit session, unload so that we don't lose our markdown metadata */ public function maybe_unload_for_bulk_edit() { if ( isset( $_REQUEST['bulk_edit'] ) && $this->is_posting_enabled() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $this->unload_markdown_for_posts(); } } /** * Called on init and fires on switch_blog to decide if our actions and filters * should be running. * * @param int|null $new_blog_id New blog ID. * @param int|null $old_blog_id Old blog ID. * @return null */ public function maybe_load_actions_and_filters( $new_blog_id = null, $old_blog_id = null ) { // When WP sites are being installed, the options table is not available yet. if ( function_exists( 'wp_installing' ) && wp_installing() ) { return; } // If this is a switch_to_blog call, and the blog isn't changing, we'll already be loaded. if ( $new_blog_id && $new_blog_id === $old_blog_id ) { return; } if ( $this->is_posting_enabled() ) { $this->load_markdown_for_posts(); } else { $this->unload_markdown_for_posts(); } if ( $this->is_commenting_enabled() ) { $this->load_markdown_for_comments(); } else { $this->unload_markdown_for_comments(); } } /** * Set up hooks for enabling Markdown conversion on posts */ public function load_markdown_for_posts() { add_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ), 10, 2 ); add_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) ); add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) ); add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 ); add_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 ); add_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 ); add_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 ); add_filter( '_wp_post_revision_fields', array( $this, 'wp_post_revision_fields' ) ); add_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) ); add_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 ); if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { $this->check_for_early_methods(); } } /** * Removes hooks to disable Markdown conversion on posts */ public function unload_markdown_for_posts() { remove_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ) ); remove_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) ); remove_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) ); remove_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10 ); remove_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10 ); remove_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10 ); remove_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10 ); remove_filter( '_wp_post_revision_fields', array( $this, 'wp_post_revision_fields' ) ); remove_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) ); remove_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 ); } /** * Set up hooks for enabling Markdown conversion on comments */ protected function load_markdown_for_comments() { // Use priority 9 so that Markdown runs before KSES, which can clean up // any munged HTML. add_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 ); } /** * Removes hooks to disable Markdown conversion */ protected function unload_markdown_for_comments() { remove_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 ); } /** * The o2 plugin does some of what we do. Let's take precedence. */ public function add_o2_helpers() { if ( $this->is_posting_enabled() ) { add_filter( 'content_save_pre', array( $this, 'o2_escape_lists' ), 1 ); } add_filter( 'o2_preview_post', array( $this, 'o2_preview_post' ) ); add_filter( 'o2_preview_comment', array( $this, 'o2_preview_comment' ) ); add_filter( 'wpcom_markdown_transform_pre', array( $this, 'o2_unescape_lists' ) ); add_filter( 'wpcom_untransformed_content', array( $this, 'o2_unescape_lists' ) ); } /** * If Markdown is enabled for posts on this blog, filter the text for o2 previews * * @param string $text Post text. * @return string Post text transformed through the magic of Markdown */ public function o2_preview_post( $text ) { if ( $this->is_posting_enabled() ) { $text = $this->transform( $text, array( 'unslash' => false ) ); } return $text; } /** * If Markdown is enabled for comments on this blog, filter the text for o2 previews * * @param string $text Comment text. * @return string Comment text transformed through the magic of Markdown */ public function o2_preview_comment( $text ) { if ( $this->is_commenting_enabled() ) { $text = $this->transform( $text, array( 'unslash' => false ) ); } return $text; } /** * Escapes lists so that o2 doesn't trounce them * * @param string $text Post/comment text. * @return string Text escaped with HTML entity for asterisk. */ public function o2_escape_lists( $text ) { return preg_replace( '/^\\* /um', '* ', $text ); } /** * Unescapes the token we inserted on o2_escape_lists * * @param string $text Post/comment text with HTML entities for asterisks. * @return string Text with the HTML entity removed */ public function o2_unescape_lists( $text ) { return preg_replace( '/^[&]\#042; /um', '* ', $text ); } /** * Preserve code blocks from being munged by KSES before they have a chance * * @param string $text post content. * @return string post content with code blocks escaped. */ public function preserve_code_blocks( $text ) { return $this->get_parser()->codeblock_preserve( $text ); } /** * Remove KSES if it's there. Store the result to manually invoke later if needed. */ public function maybe_remove_kses() { // Filters return true if they existed before you removed them. if ( $this->is_posting_enabled() ) { $this->kses = remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' ) && remove_filter( 'content_save_pre', 'wp_filter_post_kses' ); } } /** * Add our Writing and Discussion settings. */ public function register_setting() { add_settings_field( self::POST_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'post_field' ), 'writing' ); register_setting( 'writing', self::POST_OPTION, array( $this, 'sanitize_setting' ) ); add_settings_field( self::COMMENT_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'comment_field' ), 'discussion' ); register_setting( 'discussion', self::COMMENT_OPTION, array( $this, 'sanitize_setting' ) ); } /** * Sanitize setting. Don't really want to store "on" value, so we'll store "1" instead! * * @param string $input Value received by settings API via $_POST. * @return bool Cast to boolean. */ public function sanitize_setting( $input ) { return (bool) $input; } /** * Prints HTML for the Writing setting */ public function post_field() { printf( '<label><input name="%1$s" id="%1$s" type="checkbox"%2$s /> %3$s</label><p class="description">%4$s</p>', esc_attr( self::POST_OPTION ), checked( $this->is_posting_enabled(), true, false ), esc_html__( 'Use Markdown for posts and pages.', 'jetpack' ), sprintf( '<a href="%s" data-target="wpcom-help-center">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) ) ); } /** * Prints HTML for the Discussion setting */ public function comment_field() { printf( '<label><input name="%1$s" id="%1$s" type="checkbox"%2$s /> %3$s</label><p class="description">%4$s</p>', esc_attr( self::COMMENT_OPTION ), checked( $this->is_commenting_enabled(), true, false ), esc_html__( 'Use Markdown for comments.', 'jetpack' ), sprintf( '<a href="%s" data-target="wpcom-help-center">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) ) ); } /** * Get the support url for Markdown * * @uses apply_filters * @return string support url */ protected function get_support_url() { /** * Filter the Markdown support URL. * * @module markdown * * @since 2.8.0 * * @param string $url Markdown support URL. */ return apply_filters( 'easy_markdown_support_url', 'https://en.support.wordpress.com/markdown-quick-reference/' ); } /** * Is Mardown conversion for posts enabled? * * @return boolean */ public function is_posting_enabled() { return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::POST_OPTION, '' ); } /** * Is Markdown conversion for comments enabled? * * @return boolean */ public function is_commenting_enabled() { return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::COMMENT_OPTION, '' ); } /** * Check if a $post_id has Markdown enabled * * @param int $post_id A post ID. * @return boolean */ public function is_markdown( $post_id ) { return get_metadata( 'post', $post_id, self::IS_MD_META, true ); } /** * Set Markdown as enabled on a post_id. We skip over update_postmeta so we * can sneakily set metadata on post revisions, which we need. * * @param int $post_id A post ID. * @return bool The metadata was successfully set. */ protected function set_as_markdown( $post_id ) { return update_metadata( 'post', $post_id, self::IS_MD_META, true ); } /** * Get our Markdown parser object, optionally requiring all of our needed classes and * instantiating our parser. * * @return object WPCom_GHF_Markdown_Parser instance. */ public function get_parser() { if ( ! self::$parser ) { require_once JETPACK__PLUGIN_DIR . '/_inc/lib/markdown.php'; self::$parser = new WPCom_GHF_Markdown_Parser(); } return self::$parser; } /** * We don't want Markdown conversion all over the place. */ public function add_default_post_type_support() { add_post_type_support( 'post', self::POST_TYPE_SUPPORT ); add_post_type_support( 'page', self::POST_TYPE_SUPPORT ); add_post_type_support( 'revision', self::POST_TYPE_SUPPORT ); } /** * Figure out the post type of the post screen we're on * * @deprecated since 10.8 * @return string Current post_type */ protected function get_post_screen_post_type() { _deprecated_function( __METHOD__, 'jetpack-10.8', '' ); global $pagenow; $post_type = filter_input( INPUT_GET, 'post_type', FILTER_UNSAFE_RAW ); $post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT ); if ( 'post-new.php' === $pagenow ) { return ! empty( $post_type ) ? $post_type : 'post'; } if ( $post_id ) { $post_type = get_post_type( $post_id ); } return ! empty( $post_type ) ? $post_type : 'post'; } /** * Swap post_content and post_content_filtered for editing * * @param string $content Post content. * @param int $id post ID. * @return string Swapped content */ public function edit_post_content( $content, $id ) { if ( $this->is_markdown( $id ) ) { $post = get_post( $id ); if ( $post && ! empty( $post->post_content_filtered ) ) { $post = $this->swap_for_editing( $post ); return $post->post_content; } } return $content; } /** * Swap post_content_filtered and post_content for editing * * @param string $content Post content_filtered. * @param int $id post ID. * @return string Swapped content */ public function edit_post_content_filtered( $content, $id ) { // if markdown was disabled, let's turn this off. if ( ! $this->is_posting_enabled() && $this->is_markdown( $id ) ) { $post = get_post( $id ); if ( $post && ! empty( $post->post_content_filtered ) ) { $content = ''; } } return $content; } /** * Some tags are allowed to have a 'markdown' attribute, allowing them to contain Markdown. * We need to tell KSES about those tags. * * @param array $tags List of tags that KSES allows. * @param string $context The context that KSES is allowing these tags. * @return array The tags that KSES allows, with our extra 'markdown' parameter where necessary. */ public function wp_kses_allowed_html( $tags, $context ) { if ( 'post' !== $context ) { return $tags; } $re = '/' . $this->get_parser()->contain_span_tags_re . '/'; foreach ( $tags as $tag => $attributes ) { // In case other filters have changed the value to a non-array, we skip it. if ( ! is_array( $attributes ) ) { continue; } if ( preg_match( $re, $tag ) ) { $attributes['markdown'] = true; $tags[ $tag ] = $attributes; } } return $tags; } /** * TinyMCE needs to know not to strip the 'markdown' attribute. Unfortunately, it doesn't * really offer a nice API for allowed attributes, so we have to manually add it * to the schema instead. */ public function after_wp_tiny_mce() { ?> <script type="text/javascript"> jQuery( function() { ( 'undefined' !== typeof tinymce ) && tinymce.on( 'AddEditor', function( event ) { event.editor.on( 'BeforeSetContent', function( event ) { var editor = event.target; Object.keys( editor.schema.elements ).forEach( function( key, index ) { editor.schema.elements[ key ].attributes['markdown'] = {}; editor.schema.elements[ key ].attributesOrder.push( 'markdown' ); } ); } ); }, true ); } ); </script> <?php } /** * Magic happens here. Markdown is converted and stored on post_content. Original Markdown is stored * in post_content_filtered so that we can continue editing as Markdown. * * @param array $post_data The post data that will be inserted into the DB. Slashed. * @param array $postarr All the stuff that was in $_POST. * @return array $post_data with post_content and post_content_filtered modified */ public function wp_insert_post_data( $post_data, $postarr ) { // $post_data array is slashed! $post_id = isset( $postarr['ID'] ) ? $postarr['ID'] : false; // bail early if markdown is disabled or this post type is unsupported. if ( ! $this->is_posting_enabled() || ! post_type_supports( $post_data['post_type'], self::POST_TYPE_SUPPORT ) ) { // it's disabled, but maybe this *was* a markdown post before. if ( $this->is_markdown( $post_id ) && ! empty( $post_data['post_content_filtered'] ) ) { $post_data['post_content_filtered'] = ''; } // we have no context to determine supported post types in the `post_content_pre` hook, // which already ran to sanitize code blocks. Undo that. $post_data['post_content'] = $this->get_parser()->codeblock_restore( $post_data['post_content'] ); return $post_data; } // rejigger post_content and post_content_filtered // revisions are already in the right place, except when we're restoring, but that's taken care of elsewhere // also prevent quick edit feature from overriding already-saved markdown (issue https://github.com/Automattic/jetpack/issues/636). if ( 'revision' !== $post_data['post_type'] && ! isset( $_POST['_inline_edit'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing /** * Filter the original post content passed to Markdown. * * @module markdown * * @since 2.8.0 * * @param string $post_data['post_content'] Untransformed post content. */ $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] ); $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_id ) ); /** This filter is already documented in core/wp-includes/default-filters.php */ $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] ); } elseif ( str_starts_with( $post_data['post_name'], $post_data['post_parent'] . '-autosave' ) ) { // autosaves for previews are weird. /** This filter is already documented in modules/markdown/easy-markdown.php */ $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] ); $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_data['post_parent'] ) ); /** This filter is already documented in core/wp-includes/default-filters.php */ $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] ); } // set as markdown on the wp_insert_post hook later. if ( $post_id ) { $this->monitoring['post'][ $post_id ] = true; } else { $this->monitoring['content'] = wp_unslash( $post_data['post_content'] ); } if ( 'revision' === $postarr['post_type'] && $this->is_markdown( $postarr['post_parent'] ) ) { $this->monitoring['parent'][ $postarr['post_parent'] ] = true; } return $post_data; } /** * Calls on wp_insert_post action, after wp_insert_post_data. This way we can * still set postmeta on our revisions after it's all been deleted. * * @param int $post_id The post ID that has just been added/updated. */ public function wp_insert_post( $post_id ) { $post_parent = get_post_field( 'post_parent', $post_id ); // this didn't have an ID yet. Compare the content that was just saved. if ( isset( $this->monitoring['content'] ) && get_post_field( 'post_content', $post_id ) === $this->monitoring['content'] ) { unset( $this->monitoring['content'] ); $this->set_as_markdown( $post_id ); } if ( isset( $this->monitoring['post'][ $post_id ] ) ) { unset( $this->monitoring['post'][ $post_id ] ); $this->set_as_markdown( $post_id ); } elseif ( isset( $this->monitoring['parent'][ $post_parent ] ) ) { unset( $this->monitoring['parent'][ $post_parent ] ); $this->set_as_markdown( $post_id ); } } /** * Run a comment through Markdown. Easy peasy. * * @param string $content - the content. * @return string */ public function pre_comment_content( $content ) { return $this->transform( $content, array( 'id' => $this->comment_hash( $content ), ) ); } /** * Return a comment hash. * * @param string $content - the content of the comment. */ protected function comment_hash( $content ) { return 'c-' . substr( md5( $content ), 0, 8 ); } /** * Markdown conversion. Some DRYness for repetitive tasks. * * @param string $text Content to be run through Markdown. * @param array $args Arguments, with keys: * id: provide a string to prefix footnotes with a unique identifier * unslash: when true, expects and returns slashed data * decode_code_blocks: when true, assume that text in fenced code blocks is already * HTML encoded and should be decoded before being passed to Markdown, which does * its own encoding. * @return string Markdown-processed content */ public function transform( $text, $args = array() ) { // If this contains Gutenberg content, let's keep it intact. if ( has_blocks( $text ) ) { return $text; } $args = wp_parse_args( $args, array( 'id' => false, 'unslash' => true, 'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode, ) ); // probably need to unslash. if ( $args['unslash'] ) { $text = wp_unslash( $text ); } /** * Filter the content to be run through Markdown, before it's transformed by Markdown. * * @module markdown * * @since 2.8.0 * * @param string $text Content to be run through Markdown * @param array $args Array of Markdown options. */ $text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args ) ?? ''; // ensure our paragraphs are separated. $text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text ); // visual editor likes to add <p>s. Buh-bye. $text = $this->get_parser()->unp( $text ); // sometimes we get an encoded > at start of line, breaking blockquotes. $text = preg_replace( '/^>/m', '>', $text ); // prefixes are because we need to namespace footnotes by post_id. $this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : ''; // If we're not using the code shortcode, prevent over-encoding. if ( $args['decode_code_blocks'] ) { $text = $this->get_parser()->codeblock_restore( $text ); } // Transform it! $text = $this->get_parser()->transform( $text ); // Fix footnotes - kses doesn't like the : IDs it supplies. $text = preg_replace( '/((id|href)="#?fn(ref)?):/', '$1-', $text ); // Markdown inserts extra spaces to make itself work. Buh-bye. $text = rtrim( $text ); /** * Filter the content to be run through Markdown, after it was transformed by Markdown. * * @module markdown * * @since 2.8.0 * * @param string $text Content to be run through Markdown * @param array $args Array of Markdown options. */ $text = apply_filters( 'wpcom_markdown_transform_post', $text, $args ); // probably need to re-slash. if ( $args['unslash'] ) { $text = wp_slash( $text ); } return $text; } /** * Shows Markdown in the Revisions screen, and ensures that post_content_filtered * is maintained on revisions * * @param array $fields Post fields pertinent to revisions. */ public function wp_post_revision_fields( $fields ) { $fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' ); return $fields; } /** * Do some song and dance to keep all post_content and post_content_filtered content * in the expected place when a post revision is restored. * * @param int $post_id The post ID have a restore done to it. * @param int $revision_id The revision ID being restored. */ public function wp_restore_post_revision( $post_id, $revision_id ) { if ( $this->is_markdown( $revision_id ) ) { $revision = get_post( $revision_id, ARRAY_A ); $post = get_post( $post_id, ARRAY_A ); $post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that. // set this flag so we can restore the post_content_filtered on the last revision later. $this->monitoring['restore'] = true; // let's not make a revision of our fixing update. add_filter( 'wp_revisions_to_keep', '__return_false', 99 ); wp_update_post( $post ); $this->fix_latest_revision_on_restore( $post_id ); remove_filter( 'wp_revisions_to_keep', '__return_false', 99 ); } } /** * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered * column after a restore. * * @param int $post_id The post ID that was just restored. */ protected function fix_latest_revision_on_restore( $post_id ) { global $wpdb; $post = get_post( $post_id ); $last_revision = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_type = 'revision' AND post_parent = %d ORDER BY ID DESC", $post->ID ) ); $last_revision->post_content_filtered = $post->post_content_filtered; wp_insert_post( (array) $last_revision ); } /** * Kicks off magic for an XML-RPC session. We want to keep editing Markdown * and publishing HTML. * * @param string $xmlrpc_method The current XML-RPC method. */ public function xmlrpc_actions( $xmlrpc_method ) { switch ( $xmlrpc_method ) { case 'metaWeblog.getRecentPosts': case 'wp.getPosts': case 'wp.getPages': add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 ); break; case 'wp.getPost': $this->prime_post_cache(); break; } } /** * Function metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called. * So, we have to detect those methods and prime the post cache early. * * @return null */ protected function check_for_early_methods() { $raw_post_data = file_get_contents( 'php://input' ); if ( ! str_contains( $raw_post_data, 'metaWeblog.getPost' ) && ! str_contains( $raw_post_data, 'wp.getPage' ) ) { return; } include_once ABSPATH . WPINC . '/class-IXR.php'; $message = new IXR_Message( $raw_post_data ); $message->parse(); $post_id_position = 'metaWeblog.getPost' === $message->methodName ? 0 : 1; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $this->prime_post_cache( $message->params[ $post_id_position ] ?? false ); } /** * Prime the post cache with swapped post_content. This is a sneaky way of getting around * the fact that there are no good hooks to call on the *.getPost xmlrpc methods. * * @param bool $post_id - the post ID that we're priming. */ private function prime_post_cache( $post_id = false ) { global $wp_xmlrpc_server; if ( ! $post_id ) { if ( isset( $wp_xmlrpc_server->message->params[3] ) ) { $post_id = $wp_xmlrpc_server->message->params[3]; } else { return; // Exit early if we can't get a valid post_id } } // prime the post cache. if ( $this->is_markdown( $post_id ) ) { $post = get_post( $post_id ); if ( ! empty( $post->post_content_filtered ) ) { wp_cache_delete( $post->ID, 'posts' ); $post = $this->swap_for_editing( $post ); wp_cache_add( $post->ID, $post, 'posts' ); $this->posts_to_uncache[] = $post_id; } } // uncache munged posts if using a persistent object cache. if ( wp_using_ext_object_cache() ) { add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) ); } } /** * Swaps `post_content_filtered` back to `post_content` for editing purposes. * * @param object $post WP_Post object. * @return object WP_Post object with swapped `post_content_filtered` and `post_content`. */ protected function swap_for_editing( $post ) { $markdown = $post->post_content_filtered; // unencode encoded code blocks. $markdown = $this->get_parser()->codeblock_restore( $markdown ); // restore beginning of line blockquotes. $markdown = preg_replace( '/^> /m', '> ', $markdown ); $post->post_content_filtered = $post->post_content; $post->post_content = $markdown; return $post; } /** * We munge the post cache to serve proper markdown content to XML-RPC clients. * Uncache these after the XML-RPC session ends. */ public function uncache_munged_posts() { // $this context gets lost in testing sometimes. Weird. foreach ( self::get_instance()->posts_to_uncache as $post_id ) { wp_cache_delete( $post_id, 'posts' ); } } /** * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to * turn them back on so that we can swap things for editing. * * @param object $wp_query WP_Query object. */ public function make_filterable( $wp_query ) { $wp_query->set( 'suppress_filters', false ); add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 ); } /** * Swaps post_content and post_content_filtered for editing. * * @param array $posts Posts returned by the just-completed query. * @return array Modified $posts */ public function the_posts( $posts ) { foreach ( $posts as $key => $post ) { if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) { $markdown = $posts[ $key ]->post_content_filtered; $posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content; $posts[ $key ]->post_content = $markdown; } } return $posts; } /** * Singleton silence is golden */ private function __construct() {} } add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) ); sitemaps/sitemap-buffer-fallback.php 0000644 00000010126 15174711637 0013564 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * The fallback buffer for users with no XML support. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing master sitemap xml files. * * @since 5.1.0 */ abstract class Jetpack_Sitemap_Buffer_Fallback extends Jetpack_Sitemap_Buffer { /** * The buffer contents. * * @access protected * @since 5.3.0 * @var string The buffer contents. */ protected $buffer; /** * Jetpack_Sitemap_Buffer_Fallback constructor. * * @param int $item_limit The maximum size of the buffer in items. * @param int $byte_limit The maximum size of the buffer in bytes. * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format. */ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) { $this->is_full_flag = false; $this->is_empty_flag = true; $this->timestamp = $time; $this->finder = new Jetpack_Sitemap_Finder(); $this->item_capacity = max( 1, (int) $item_limit ); $this->byte_capacity = max( 1, (int) $byte_limit ) - strlen( $this->contents() ); } /** * Append an item to the buffer, if there is room for it, * and set is_empty_flag to false. If there is no room, * we set is_full_flag to true. If $item is null, * don't do anything and report success. * * @since 5.3.0 * * @param array $array The item to be added. * * @return bool True if the append succeeded, False if not. */ public function append( $array ) { if ( $array === null ) { return true; } if ( $this->is_full_flag ) { return false; } if ( 0 >= $this->item_capacity || 0 >= $this->byte_capacity ) { $this->is_full_flag = true; return false; } else { $this->item_capacity -= 1; $added_string = $this->array_to_xml_string( $array ); $this->buffer .= $added_string; $this->is_empty_flag = false; mbstring_binary_safe_encoding(); // So we can safely use strlen(). $this->byte_capacity -= strlen( $added_string ); reset_mbstring_encoding(); return true; } } /** * Detect whether the buffer is empty. * * @since 5.3.0 * * @return bool True if the buffer is empty, false otherwise. */ public function is_empty() { return $this->is_empty_flag; } /** * Retrieve the contents of the buffer. * * @since 5.3.0 * * @return string The contents of the buffer (with the footer included). */ public function contents() { $root = $this->get_root_element(); return '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL . $root[0] . $this->buffer . $root[1] . PHP_EOL; } /** * Legacy implementation of array to XML conversion without using DOMDocument. * * @param array $array Item to append to buffer. * @param DOMElement $parent (optional) an element to which new children should be added. * @param DOMDocument $root (optional) the parent document. * @return String $result */ public function array_to_xml_string( $array, $parent = null, $root = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $string = ''; foreach ( $array as $key => $value ) { // Only allow a-z, A-Z, colon, underscore, and hyphen. $tag = preg_replace( '/[^a-zA-Z:_-]/', '_', $key ); if ( is_array( $value ) ) { $string .= "<$tag>"; $string .= $this->array_to_xml_string( $value ); $string .= "</$tag>"; } elseif ( $value === null ) { $string .= "<$tag />"; } else { $string .= "<$tag>" . htmlspecialchars( $value, ENT_COMPAT ) . "</$tag>"; } } return $string; } /** * Render an associative array of XML attribute key/value pairs. * * @access public * @since 5.3.0 * * @param array $array Key/value array of attributes. * * @return string The rendered attribute string. */ public static function array_to_xml_attr_string( $array ) { $string = ''; foreach ( $array as $key => $value ) { $key = preg_replace( '/[^a-zA-Z:_-]/', '_', $key ); $string .= ' ' . $key . '="' . esc_attr( $value ) . '"'; } return $string; } } sitemaps/sitemap-buffer-video-fallback.php 0000644 00000004226 15174711637 0014674 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Video * extends the Jetpack_Sitemap_Buffer class to represent the single video sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap video xml files for users without libxml support. * * @since 5.3.0 * @phan-suppress PhanRedefinedClassReference -- Don't conflict with real version. */ class Jetpack_Sitemap_Buffer_Video extends Jetpack_Sitemap_Buffer_Fallback { // @phan-suppress-previous-line UnusedSuppression -- It's used. /** * Returns a DOM element that contains all video sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { /** * Filter the XML namespaces included in video sitemaps. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_video_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:video' => 'http://www.google.com/schemas/sitemap-video/1.1', ) ); $video_sitemap_xsl_url = $this->finder->construct_sitemap_url( 'video-sitemap.xsl' ); $jetpack_version = JETPACK__VERSION; $this->root = array( "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL . "<?xml-stylesheet type='text/xsl' href='{$video_sitemap_xsl_url}'?>" . PHP_EOL . '<urlset ' . $this->array_to_xml_attr_string( $namespaces ) . '>', '</urlset>', ); $this->byte_capacity -= strlen( implode( '', $this->root ) ); } return $this->root; } } sitemaps/sitemap-constants.php 0000644 00000013317 15174711637 0012577 0 ustar 00 <?php /** * Sitemap-related constants. * * @package automattic/jetpack * @since 4.8.0 * @author Automattic */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Number of seconds between sitemap and news sitemap updates in development code. * In production, sitemaps are cached for 12 hours. * In development, sitemaps are cache for 1 minute. * * @since 7.7.0 */ if ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) { if ( ! defined( 'JP_SITEMAP_INTERVAL' ) ) { define( 'JP_SITEMAP_INTERVAL', 60 ); } if ( ! defined( 'JP_NEWS_SITEMAP_INTERVAL' ) ) { define( 'JP_NEWS_SITEMAP_INTERVAL', 60 ); } } /** * Maximum size (in bytes) of a sitemap xml file. * Max is 716800 = 700kb to avoid potential failures for default memcached limits (1MB) * * @link https://www.sitemaps.org/ * @since 4.8.0 */ if ( ! defined( 'JP_SITEMAP_MAX_BYTES' ) ) { define( 'JP_SITEMAP_MAX_BYTES', 716800 ); } /** * Maximum size (in url nodes) of a sitemap xml file. * Per the spec, max value is 50000. * * @link https://www.sitemaps.org/ * @since 4.8.0 */ if ( ! defined( 'JP_SITEMAP_MAX_ITEMS' ) ) { define( 'JP_SITEMAP_MAX_ITEMS', 1000 ); } /** * Maximum size (in url nodes) of a news sitemap xml file. * Per the spec, max value is 1000. * * @link https://support.google.com/news/publisher/answer/74288?hl=en * @since 4.8.0 */ if ( ! defined( 'JP_NEWS_SITEMAP_MAX_ITEMS' ) ) { define( 'JP_NEWS_SITEMAP_MAX_ITEMS', 1000 ); } /** * Batch size for database queries. * * @since 4.8.0 */ if ( ! defined( 'JP_SITEMAP_BATCH_SIZE' ) ) { define( 'JP_SITEMAP_BATCH_SIZE', 50 ); } /** * Number of sitemap files to update on each run. * * @since 4.8.0 */ if ( ! defined( 'JP_SITEMAP_UPDATE_SIZE' ) ) { define( 'JP_SITEMAP_UPDATE_SIZE', 100 ); } /** * Number of seconds between sitemap updates. * * @since 4.8.0 */ if ( ! defined( 'JP_SITEMAP_INTERVAL' ) ) { define( 'JP_SITEMAP_INTERVAL', 12 * HOUR_IN_SECONDS ); } /** * Number of seconds to lock the sitemap state. * * @since 4.8.0 */ if ( ! defined( 'JP_SITEMAP_LOCK_INTERVAL' ) ) { define( 'JP_SITEMAP_LOCK_INTERVAL', 15 * MINUTE_IN_SECONDS ); } /** * Number of seconds between news sitemap updates. * * @since 4.8.0 */ if ( ! defined( 'JP_NEWS_SITEMAP_INTERVAL' ) ) { define( 'JP_NEWS_SITEMAP_INTERVAL', 12 * HOUR_IN_SECONDS ); } /* * These constants represent the types of various kinds of sitemaps. * Note: these strings are used as 'post_types' in the database, and * so must be at most 20 characters long. */ if ( ! defined( 'JP_MASTER_SITEMAP_TYPE' ) ) { define( 'JP_MASTER_SITEMAP_TYPE', 'jp_sitemap_master' ); } if ( ! defined( 'JP_PAGE_SITEMAP_TYPE' ) ) { define( 'JP_PAGE_SITEMAP_TYPE', 'jp_sitemap' ); } if ( ! defined( 'JP_PAGE_SITEMAP_INDEX_TYPE' ) ) { define( 'JP_PAGE_SITEMAP_INDEX_TYPE', 'jp_sitemap_index' ); } if ( ! defined( 'JP_IMAGE_SITEMAP_TYPE' ) ) { define( 'JP_IMAGE_SITEMAP_TYPE', 'jp_img_sitemap' ); } if ( ! defined( 'JP_IMAGE_SITEMAP_INDEX_TYPE' ) ) { define( 'JP_IMAGE_SITEMAP_INDEX_TYPE', 'jp_img_sitemap_index' ); } if ( ! defined( 'JP_VIDEO_SITEMAP_TYPE' ) ) { define( 'JP_VIDEO_SITEMAP_TYPE', 'jp_vid_sitemap' ); } if ( ! defined( 'JP_VIDEO_SITEMAP_INDEX_TYPE' ) ) { define( 'JP_VIDEO_SITEMAP_INDEX_TYPE', 'jp_vid_sitemap_index' ); } /** * The name (with extension) of a sitemap file of the given * type and number. * * @since 4.8.0 * * @param string $type The sitemap type. * @param string $number The sitemap number. * * @return string The filename. */ function jp_sitemap_filename( $type, $number = null ) { if ( $number === null ) { return "error-not-int-$type-null.xml"; } elseif ( JP_MASTER_SITEMAP_TYPE === $type ) { return 'sitemap.xml'; } elseif ( JP_PAGE_SITEMAP_TYPE === $type ) { return "sitemap-$number.xml"; } elseif ( JP_PAGE_SITEMAP_INDEX_TYPE === $type ) { return "sitemap-index-$number.xml"; } elseif ( JP_IMAGE_SITEMAP_TYPE === $type ) { return "image-sitemap-$number.xml"; } elseif ( JP_IMAGE_SITEMAP_INDEX_TYPE === $type ) { return "image-sitemap-index-$number.xml"; } elseif ( JP_VIDEO_SITEMAP_TYPE === $type ) { return "video-sitemap-$number.xml"; } elseif ( JP_VIDEO_SITEMAP_INDEX_TYPE === $type ) { return "video-sitemap-index-$number.xml"; } else { return "error-bad-type-$type-$number.xml"; } } /** * The index type corresponding to a sitemap type. * * @since 4.8.0 * * @param string $type The sitemap type. * * @return string The index type. */ function jp_sitemap_index_type_of( $type ) { if ( JP_PAGE_SITEMAP_TYPE === $type ) { return JP_PAGE_SITEMAP_INDEX_TYPE; } elseif ( JP_IMAGE_SITEMAP_TYPE === $type ) { return JP_IMAGE_SITEMAP_INDEX_TYPE; } elseif ( JP_VIDEO_SITEMAP_TYPE === $type ) { return JP_VIDEO_SITEMAP_INDEX_TYPE; } else { return "error-bad-type-$type"; } } /** * The sitemap type corresponding to an index type. * * @since 4.8.0 * * @param string $type The index type. * * @return string The sitemap type. */ function jp_sitemap_child_type_of( $type ) { if ( JP_PAGE_SITEMAP_INDEX_TYPE === $type ) { return JP_PAGE_SITEMAP_TYPE; } elseif ( JP_IMAGE_SITEMAP_INDEX_TYPE === $type ) { return JP_IMAGE_SITEMAP_TYPE; } elseif ( JP_VIDEO_SITEMAP_INDEX_TYPE === $type ) { return JP_VIDEO_SITEMAP_TYPE; } else { return "error-bad-type-$type"; } } /** * Convert '0000-00-00 00:00:00' to '0000-00-00T00:00:00Z'. * Note that the input is assumed to be in UTC (a.k.a. GMT). * * @link https://www.w3.org/TR/NOTE-datetime * @since 4.8.0 * * @param string $datetime The timestamp to convert. * * @return string The converted timestamp. */ function jp_sitemap_datetime( $datetime ) { $regex = '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/'; if ( preg_match( $regex, $datetime ) ) { return str_replace( ' ', 'T', $datetime ) . 'Z'; } else { return $datetime; } } sitemaps/sitemap-buffer-factory.php 0000644 00000004030 15174711637 0013471 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Factory class for creating sitemap buffers. * * @since 14.6 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Creates the appropriate sitemap buffer based on available PHP extensions. * * @since 14.6 */ class Jetpack_Sitemap_Buffer_Factory { /** * Create a new sitemap buffer instance. * * @since 14.6 * * @param string $type The type of sitemap buffer ('page', 'image', 'video', etc.). * @param int $item_limit The maximum number of items in the buffer. * @param int $byte_limit The maximum number of bytes in the buffer. * @param string $time The initial datetime of the buffer. * * @return Jetpack_Sitemap_Buffer|null The created buffer or null if type is invalid. */ public static function create( $type, $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) { /** * Hook to allow for XMLWriter usage. * * Temporary filter to disallow XMLWriter usage. * * @since 14.7 * @since 15.2 Update default to true. * @module sitemaps * * @param bool $use_xmlwriter Whether to use XMLWriter. Current default is true. */ $use_xmlwriter = apply_filters( 'jetpack_sitemap_use_xmlwriter', true ); // First try XMLWriter if available if ( $use_xmlwriter && class_exists( 'XMLWriter' ) ) { $class_name = 'Jetpack_Sitemap_Buffer_' . ucfirst( $type ) . '_XMLWriter'; if ( class_exists( $class_name ) ) { return new $class_name( $item_limit, $byte_limit, $time ); } } // Then try DOMDocument if ( class_exists( 'DOMDocument' ) ) { $class_name = 'Jetpack_Sitemap_Buffer_' . ucfirst( $type ); if ( class_exists( $class_name ) ) { return new $class_name( $item_limit, $byte_limit, $time ); } } // Finally fall back to the basic implementation $class_name = 'Jetpack_Sitemap_Buffer_' . ucfirst( $type ) . '_Fallback'; if ( class_exists( $class_name ) ) { return new $class_name( $item_limit, $byte_limit, $time ); } return null; } } sitemaps/sitemap-buffer-image-fallback.php 0000644 00000004215 15174711637 0014646 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Image * extends the Jetpack_Sitemap_Buffer class to represent the single image sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap image xml files for users that have no libxml support. * * @since 5.3.0 * @phan-suppress PhanRedefinedClassReference -- Don't conflict with real version. */ class Jetpack_Sitemap_Buffer_Image extends Jetpack_Sitemap_Buffer_Fallback { // @phan-suppress-previous-line UnusedSuppression -- It's used. /** * Returns a DOM element that contains all image sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { /** * Filter the XML namespaces included in image sitemaps. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_image_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:image' => 'http://www.google.com/schemas/sitemap-image/1.1', ) ); $sitemap_xsl_url = $this->finder->construct_sitemap_url( 'sitemap.xsl' ); $jetpack_version = JETPACK__VERSION; $this->root = array( "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL . "<?xml-stylesheet type='text/xsl' href='{$sitemap_xsl_url}'?>" . PHP_EOL . '<urlset ' . $this->array_to_xml_attr_string( $namespaces ) . '>' . PHP_EOL, '</urlset>', ); $this->byte_capacity -= strlen( implode( '', $this->root ) ); } return $this->root; } } sitemaps/sitemap-stylist.php 0000644 00000051042 15174711637 0012273 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * The XSL used to style sitemaps is essentially a bunch of * static strings. This class handles the construction of * those strings. * * @package automattic/jetpack * @since 4.8.0 */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Builds the XSL files required by Jetpack_Sitemap_Manager. * * @since 4.8.0 */ class Jetpack_Sitemap_Stylist { /** * Convert named entities, strip all HTML except anchor tags, * and interpolate with vsprintf. This is a helper function * for all the internationalized UI strings in this class * which have to include URLs. * * Note that $url_array should be indexed by integers like so: * * array( * 1 => 'example.com', * 2 => 'example.org', * ); * * Then '%1$s' in the format string will substitute 'example.com' * and '%2$s' will substitute 'example.org'. * * @access private * @since 4.8.0 * @link https://php.net/manual/en/function.vsprintf.php Format string documentation. * * @param string $format A vsprintf-style format string to be sanitized. * @param array $url_array The string substitution array to be passed to vsprintf. * * @return string The sanitized string. */ private static function sanitize_with_links( $format, $url_array ) { return vsprintf( wp_kses( ent2ncr( $format ), array( 'a' => array( 'href' => true, 'title' => true, ), ) ), $url_array ); } /** * Returns the xsl of a sitemap xml file as a string. * * @access public * @since 4.8.0 * * @return string The contents of the xsl file. */ public static function sitemap_xsl() { $title = esc_html( ent2ncr( __( 'XML Sitemap', 'jetpack' ) ) ); $header_url = esc_html( ent2ncr( __( 'URL', 'jetpack' ) ) ); $header_lastmod = esc_html( ent2ncr( __( 'Last Modified', 'jetpack' ) ) ); $description = self::sanitize_with_links( /* translators: %1$s: jetpack.com URL. %2$s: google.com URL. %3$s: bing.com URL. */ __( 'This is an XML Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( 1 => 'https://jetpack.com/', 2 => 'https://www.google.com/', 3 => 'https://www.bing.com/', ) ); $more_info = self::sanitize_with_links( /* translators: %1$s: sitemaps.org URL. */ __( 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( 1 => 'https://sitemaps.org', ) ); $generated_by = self::sanitize_with_links( /* translators: %s: jetpack.com URL. */ __( 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( 1 => 'https://jetpack.com', ) ); $css = self::sitemap_xsl_css(); return <<<XSL <?xml version='1.0' encoding='UTF-8'?> <xsl:stylesheet version='2.0' xmlns:html='http://www.w3.org/TR/REC-html40' xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'> <xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/> <xsl:template match="/"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>$title</title> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/> <style type='text/css'> $css </style> </head> <body> <div id='description'> <h1>$title</h1> <p>$description</p> <p>$more_info</p> </div> <div id='content'> <!-- <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> --> <table> <tr> <th>#</th> <th>$header_url</th> <th>$header_lastmod</th> </tr> <xsl:for-each select="sitemap:urlset/sitemap:url"> <tr> <xsl:choose> <xsl:when test='position() mod 2 != 1'> <xsl:attribute name="class">odd</xsl:attribute> </xsl:when> </xsl:choose> <td> <xsl:value-of select = "position()" /> </td> <td> <xsl:variable name='itemURL'> <xsl:value-of select='sitemap:loc'/> </xsl:variable> <a href='{\$itemURL}'> <xsl:value-of select='sitemap:loc'/> </a> </td> <td> <xsl:value-of select='sitemap:lastmod'/> </td> </tr> </xsl:for-each> </table> </div> <div id='footer'> <p>$generated_by</p> </div> </body> </html> </xsl:template> </xsl:stylesheet>\n XSL; } /** * Returns the xsl of a sitemap index xml file as a string. * * @access public * @since 4.8.0 * * @return string The contents of the xsl file. */ public static function sitemap_index_xsl() { $title = esc_html( ent2ncr( __( 'XML Sitemap Index', 'jetpack' ) ) ); $header_url = esc_html( ent2ncr( __( 'Sitemap URL', 'jetpack' ) ) ); $header_lastmod = esc_html( ent2ncr( __( 'Last Modified', 'jetpack' ) ) ); $description = self::sanitize_with_links( /* translators: %1$s: jetpack.com URL. %2$s: google.com URL. %3$s: bing.com URL. */ __( 'This is an XML Sitemap Index generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( 1 => 'https://jetpack.com/', 2 => 'https://www.google.com/', 3 => 'https://www.bing.com/', ) ); if ( current_user_can( 'manage_options' ) ) { $next = human_time_diff( wp_next_scheduled( 'jp_sitemap_cron_hook' ) ); /* translators: %s is a human_time_diff until next sitemap generation. */ $no_nodes_warning = sprintf( __( 'No sitemap found. The system will try to build it again in %s.', 'jetpack' ), $next ); } else { $no_nodes_warning = ''; } $more_info = self::sanitize_with_links( /* translators: %1$s: sitemaps.org URL. */ __( 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( 1 => 'https://sitemaps.org', ) ); $generated_by = self::sanitize_with_links( /* translators: %s: jetpack.com URL. */ __( 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( 1 => 'https://jetpack.com', ) ); $css = self::sitemap_xsl_css(); return <<<XSL <?xml version='1.0' encoding='UTF-8'?> <xsl:stylesheet version='2.0' xmlns:html='http://www.w3.org/TR/REC-html40' xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'> <xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/> <xsl:template match="/"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>$title</title> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/> <style type='text/css'> $css </style> </head> <body> <div id='description'> <h1>$title</h1> <xsl:choose> <xsl:when test='not(sitemap:sitemapindex/sitemap:sitemap)'> <p><strong>$no_nodes_warning</strong></p> </xsl:when> </xsl:choose> <p>$description</p> <p>$more_info</p> </div> <div id='content'> <table> <tr> <th>#</th> <th>$header_url</th> <th>$header_lastmod</th> </tr> <xsl:for-each select='sitemap:sitemapindex/sitemap:sitemap'> <tr> <xsl:choose> <xsl:when test='position() mod 2 != 1'> <xsl:attribute name="class">odd</xsl:attribute> </xsl:when> </xsl:choose> <td> <xsl:value-of select = "position()" /> </td> <td> <xsl:variable name='itemURL'> <xsl:value-of select='sitemap:loc'/> </xsl:variable> <a href='{\$itemURL}'> <xsl:value-of select='sitemap:loc'/> </a> </td> <td> <xsl:value-of select='sitemap:lastmod'/> </td> </tr> </xsl:for-each> </table> </div> <div id='footer'> <p>$generated_by</p> </div> </body> </html> </xsl:template> </xsl:stylesheet>\n XSL; } /** * Returns the xsl of an image sitemap xml file as a string. * * @access public * @since 4.8.0 * * @return string The contents of the xsl file. */ public static function image_sitemap_xsl() { $title = esc_html( ent2ncr( __( 'XML Image Sitemap', 'jetpack' ) ) ); $header_url = esc_html( ent2ncr( __( 'Page URL', 'jetpack' ) ) ); $header_image_url = esc_html( ent2ncr( __( 'Image URL', 'jetpack' ) ) ); $header_thumbnail = esc_html( ent2ncr( __( 'Thumbnail', 'jetpack' ) ) ); $header_lastmod = esc_html( ent2ncr( __( 'Last Modified', 'jetpack' ) ) ); $description = self::sanitize_with_links( /* translators: %1$s: jetpack.com URL. %2$s: google.com URL. %3$s: bing.com URL. */ __( 'This is an XML Image Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( 1 => 'https://jetpack.com/', 2 => 'https://www.google.com/', 3 => 'https://www.bing.com/', ) ); $more_info = self::sanitize_with_links( /* translators: %1$s: sitemaps.org URL. */ __( 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( 1 => 'https://sitemaps.org', ) ); $generated_by = self::sanitize_with_links( /* translators: %s: jetpack.com URL. */ __( 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( 1 => 'https://jetpack.com', ) ); $css = self::sitemap_xsl_css(); return <<<XSL <?xml version='1.0' encoding='UTF-8'?> <xsl:stylesheet version='2.0' xmlns:html='http://www.w3.org/TR/REC-html40' xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9' xmlns:image='http://www.google.com/schemas/sitemap-image/1.1' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'> <xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/> <xsl:template match="/"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>$title</title> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/> <style type='text/css'> $css </style> </head> <body> <div id='description'> <h1>$title</h1> <p>$description</p> <p>$more_info</p> </div> <div id='content'> <!-- <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> --> <table> <tr> <th>#</th> <th>$header_url</th> <th>$header_image_url</th> <th>$header_lastmod</th> <th>$header_thumbnail</th> </tr> <xsl:for-each select="sitemap:urlset/sitemap:url"> <tr> <xsl:choose> <xsl:when test='position() mod 2 != 1'> <xsl:attribute name="class">odd</xsl:attribute> </xsl:when> </xsl:choose> <td> <xsl:value-of select = "position()" /> </td> <td> <xsl:variable name='pageURL'> <xsl:value-of select='sitemap:loc'/> </xsl:variable> <a href='{\$pageURL}'> <xsl:value-of select='sitemap:loc'/> </a> </td> <xsl:variable name='itemURL'> <xsl:value-of select='image:image/image:loc'/> </xsl:variable> <td> <a href='{\$itemURL}'> <xsl:value-of select='image:image/image:loc'/> </a> </td> <td> <xsl:value-of select='sitemap:lastmod'/> </td> <td> <a href='{\$itemURL}'> <img class='thumbnail' src='{\$itemURL}'/> </a> </td> </tr> </xsl:for-each> </table> </div> <div id='footer'> <p>$generated_by</p> </div> </body> </html> </xsl:template> </xsl:stylesheet>\n XSL; } /** * Returns the xsl of a video sitemap xml file as a string. * * @access public * @since 4.8.0 * * @return string The contents of the xsl file. */ public static function video_sitemap_xsl() { $title = esc_html( ent2ncr( __( 'XML Video Sitemap', 'jetpack' ) ) ); $header_url = esc_html( ent2ncr( __( 'Page URL', 'jetpack' ) ) ); $header_image_url = esc_html( ent2ncr( __( 'Video URL', 'jetpack' ) ) ); $header_thumbnail = esc_html( ent2ncr( __( 'Thumbnail', 'jetpack' ) ) ); $header_title = esc_html( ent2ncr( __( 'Title', 'jetpack' ) ) ); $header_lastmod = esc_html( ent2ncr( __( 'Last Modified', 'jetpack' ) ) ); $header_description = esc_html( ent2ncr( __( 'Description', 'jetpack' ) ) ); $description = self::sanitize_with_links( /* translators: %1$s: jetpack.com URL. %2$s: google.com URL. %3$s: bing.com URL. */ __( 'This is an XML Video Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( 1 => 'https://jetpack.com/', 2 => 'https://www.google.com/', 3 => 'https://www.bing.com/', ) ); $more_info = self::sanitize_with_links( /* translators: %1$s: sitemaps.org URL. */ __( 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( 1 => 'https://sitemaps.org', ) ); $generated_by = self::sanitize_with_links( /* translators: %s: jetpack.com URL. */ __( 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( 1 => 'https://jetpack.com', ) ); $css = self::sitemap_xsl_css(); return <<<XSL <?xml version='1.0' encoding='UTF-8'?> <xsl:stylesheet version='2.0' xmlns:html='http://www.w3.org/TR/REC-html40' xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9' xmlns:video='http://www.google.com/schemas/sitemap-video/1.1' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'> <xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/> <xsl:template match="/"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>$title</title> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/> <style type='text/css'> $css </style> </head> <body> <div id='description'> <h1>$title</h1> <p>$description</p> <p>$more_info</p> </div> <div id='content'> <!-- <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> --> <table> <tr> <th>#</th> <th>$header_url</th> <th>$header_image_url</th> <th>$header_title</th> <th>$header_description</th> <th>$header_lastmod</th> <th>$header_thumbnail</th> </tr> <xsl:for-each select="sitemap:urlset/sitemap:url"> <tr> <xsl:choose> <xsl:when test='position() mod 2 != 1'> <xsl:attribute name="class">odd</xsl:attribute> </xsl:when> </xsl:choose> <td> <xsl:value-of select = "position()" /> </td> <td> <xsl:variable name='pageURL'> <xsl:value-of select='sitemap:loc'/> </xsl:variable> <a href='{\$pageURL}'> <xsl:value-of select='sitemap:loc'/> </a> </td> <xsl:variable name='itemURL'> <xsl:value-of select='video:video/video:content_loc'/> </xsl:variable> <td> <a href='{\$itemURL}'> <xsl:value-of select='video:video/video:content_loc'/> </a> </td> <td> <xsl:value-of select='video:video/video:title'/> </td> <td> <xsl:value-of select='video:video/video:description' disable-output-escaping='yes'/> </td> <td> <xsl:value-of select='sitemap:lastmod'/> </td> <td> <xsl:variable name='thumbURL'> <xsl:value-of select='video:video/video:thumbnail_loc'/> </xsl:variable> <a href='{\$thumbURL}'> <img class='thumbnail' src='{\$thumbURL}'/> </a> </td> </tr> </xsl:for-each> </table> </div> <div id='footer'> <p>$generated_by</p> </div> </body> </html> </xsl:template> </xsl:stylesheet>\n XSL; } /** * Returns the xsl of a news sitemap xml file as a string. * * @access public * @since 4.8.0 * * @return string The contents of the xsl file. */ public static function news_sitemap_xsl() { $title = esc_html( ent2ncr( __( 'XML News Sitemap', 'jetpack' ) ) ); $header_url = esc_html( ent2ncr( __( 'Page URL', 'jetpack' ) ) ); $header_title = esc_html( ent2ncr( __( 'Title', 'jetpack' ) ) ); $header_pubdate = esc_html( ent2ncr( __( 'Publication Date', 'jetpack' ) ) ); $description = self::sanitize_with_links( /* translators: %1$s: jetpack.com URL. %2$s: google.com URL. %3$s: bing.com URL. */ __( 'This is an XML News Sitemap generated by <a href="%1$s" rel="noopener noreferrer" target="_blank">Jetpack</a>, meant to be consumed by search engines like <a href="%2$s" rel="noopener noreferrer" target="_blank">Google</a> or <a href="%3$s" rel="noopener noreferrer" target="_blank">Bing</a>.', 'jetpack' ), array( 1 => 'https://jetpack.com/', 2 => 'https://www.google.com/', 3 => 'https://www.bing.com/', ) ); $more_info = self::sanitize_with_links( /* translators: %1$s: sitemaps.org URL. */ __( 'You can find more information on XML sitemaps at <a href="%1$s" rel="noopener noreferrer" target="_blank">sitemaps.org</a>', 'jetpack' ), array( 1 => 'https://sitemaps.org', ) ); $generated_by = self::sanitize_with_links( /* translators: %s: jetpack.com URL. */ __( 'Generated by <a href="%s" rel="noopener noreferrer" target="_blank">Jetpack for WordPress</a>', 'jetpack' ), array( 1 => 'https://jetpack.com', ) ); $css = self::sitemap_xsl_css(); return <<<XSL <?xml version='1.0' encoding='UTF-8'?> <xsl:stylesheet version='2.0' xmlns:html='http://www.w3.org/TR/REC-html40' xmlns:sitemap='http://www.sitemaps.org/schemas/sitemap/0.9' xmlns:news='http://www.google.com/schemas/sitemap-news/0.9' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'> <xsl:output method='html' version='1.0' encoding='UTF-8' indent='yes'/> <xsl:template match="/"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>$title</title> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/> <style type='text/css'> $css </style> </head> <body> <div id='description'> <h1>$title</h1> <p>$description</p> <p>$more_info</p> </div> <div id='content'> <!-- <xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> --> <table> <tr> <th>#</th> <th>$header_url</th> <th>$header_title</th> <th>$header_pubdate</th> </tr> <xsl:for-each select="sitemap:urlset/sitemap:url"> <tr> <xsl:choose> <xsl:when test='position() mod 2 != 1'> <xsl:attribute name="class">odd</xsl:attribute> </xsl:when> </xsl:choose> <td> <xsl:value-of select = "position()" /> </td> <xsl:variable name='pageURL'> <xsl:value-of select='sitemap:loc'/> </xsl:variable> <td> <a href='{\$pageURL}'> <xsl:value-of select='sitemap:loc'/> </a> </td> <td> <a href='{\$pageURL}'> <xsl:value-of select='news:news/news:title'/> </a> </td> <td> <xsl:value-of select='news:news/news:publication_date'/> </td> </tr> </xsl:for-each> </table> </div> <div id='footer'> <p>$generated_by</p> </div> </body> </html> </xsl:template> </xsl:stylesheet>\n XSL; } /** * The CSS to be included in sitemap xsl stylesheets; * factored out for uniformity. * * @access public * @since 4.8.0 * * @return string The CSS. */ public static function sitemap_xsl_css() { return <<<'CSS' body { font: 14px 'Open Sans', Helvetica, Arial, sans-serif; margin: 0; } a { color: #3498db; text-decoration: none; } h1 { margin: 0; } #description { background-color: #f0f2eb; color: #000; padding: 30px 30px 20px; } #description a { color: #008710; } #content { padding: 10px 30px 30px; background: #fff; } a:hover { border-bottom: 1px solid; } th, td { font-size: 12px; } th { text-align: left; border-bottom: 1px solid #ccc; } th, td { padding: 10px 15px; } .odd { background: linear-gradient( 159.87deg, #f6f6f4 7.24%, #f7f4ea 64.73%, #ddedd5 116.53% ); } #footer { margin: 20px 30px; font-size: 12px; color: #999; } #footer a { color: inherit; } #description a, #footer a { border-bottom: 1px solid; } #description a:hover, #footer a:hover { border-bottom: none; } img { max-height: 100px; max-width: 100px; } CSS; } } sitemaps/sitemap-builder.php 0000644 00000123140 15174711637 0012205 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Build the sitemap tree. * * @package automattic/jetpack * @since 4.8.0 * @author Automattic */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /* Include sitemap subclasses, if not already, and include proper buffer based on phpxml's availability. */ require_once __DIR__ . '/sitemap-constants.php'; require_once __DIR__ . '/sitemap-buffer.php'; if ( ! class_exists( 'DOMDocument' ) ) { require_once __DIR__ . '/sitemap-buffer-fallback.php'; require_once __DIR__ . '/sitemap-buffer-image-fallback.php'; require_once __DIR__ . '/sitemap-buffer-master-fallback.php'; require_once __DIR__ . '/sitemap-buffer-news-fallback.php'; require_once __DIR__ . '/sitemap-buffer-page-fallback.php'; require_once __DIR__ . '/sitemap-buffer-video-fallback.php'; } else { require_once __DIR__ . '/sitemap-buffer-image.php'; require_once __DIR__ . '/sitemap-buffer-master.php'; require_once __DIR__ . '/sitemap-buffer-news.php'; require_once __DIR__ . '/sitemap-buffer-page.php'; require_once __DIR__ . '/sitemap-buffer-video.php'; } require_once __DIR__ . '/sitemap-librarian.php'; require_once __DIR__ . '/sitemap-finder.php'; require_once __DIR__ . '/sitemap-state.php'; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { require_once __DIR__ . '/sitemap-logger.php'; } /** * Simple class for rendering an empty sitemap with a short TTL */ class Jetpack_Sitemap_Buffer_Empty extends Jetpack_Sitemap_Buffer { /** * Jetpack_Sitemap_Buffer_Empty constructor. */ public function __construct() { parent::__construct( JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES, '1970-01-01 00:00:00' ); $this->doc->appendChild( $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" ) ); $this->doc->appendChild( $this->doc->createComment( 'Jetpack_Sitemap_Buffer_Empty' ) ); $this->doc->appendChild( $this->doc->createProcessingInstruction( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap-index.xsl' ) . '"' ) ); } /** * Returns a DOM element for an empty sitemap. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { $this->root = $this->doc->createElement( 'sitemapindex' ); $this->root->setAttribute( 'xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9' ); $this->doc->appendChild( $this->root ); $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) ); } return $this->root; } } /** * The Jetpack_Sitemap_Builder object handles the construction of * all sitemap files (except the XSL files, which are handled by * Jetpack_Sitemap_Stylist.) Other than the constructor, there are * only two public functions: build_all_sitemaps and news_sitemap_xml. * * @since 4.8.0 */ class Jetpack_Sitemap_Builder { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound,Generic.Classes.OpeningBraceSameLine.ContentAfterBrace /** * Librarian object for storing and retrieving sitemap data. * * @access private * @since 4.8.0 * @var $librarian Jetpack_Sitemap_Librarian */ private $librarian; /** * Logger object for reporting debug messages. * * @access private * @since 4.8.0 * @var $logger Jetpack_Sitemap_Logger */ private $logger = false; /** * Finder object for dealing with sitemap URIs. * * @access private * @since 4.8.0 * @var $finder Jetpack_Sitemap_Finder */ private $finder; /** * Construct a new Jetpack_Sitemap_Builder object. * * @access public * @since 4.8.0 */ public function __construct() { $this->librarian = new Jetpack_Sitemap_Librarian(); $this->finder = new Jetpack_Sitemap_Finder(); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { $this->logger = new Jetpack_Sitemap_Logger(); } update_option( 'jetpack_sitemap_post_types', /** * The array of post types to be included in the sitemap. * * Add your custom post type name to the array to have posts of * that type included in the sitemap. The default array includes * 'page' and 'post'. * * The result of this filter is cached in an option, 'jetpack_sitemap_post_types', * so this filter only has to be applied once per generation. * * @since 4.8.0 */ apply_filters( 'jetpack_sitemap_post_types', array( 'post', 'page' ) ) ); } /** * Update the sitemap. * * All we do here is call build_next_sitemap_file a bunch of times. * * @since 4.8.0 */ public function update_sitemap() { if ( $this->logger ) { $this->logger->report( '-- Updating...' ); if ( ! class_exists( 'DOMDocument' ) ) { $this->logger->report( __( 'Jetpack cannot load necessary XML manipulation libraries. Please ask your hosting provider to refer to our server requirements at https://jetpack.com/support/server-requirements/ .', 'jetpack' ), true ); } } /** * Filters whether to suspend cache addition for the entire sitemap generation. * * @since 15.0 * * @param bool|null $suspend_addition Whether to suspend cache addition. Defaults to null. * @return bool|null Whether to suspend cache addition. */ $suspend_addition = apply_filters( 'jetpack_sitemap_suspend_cache_addition', null ); // Cache the previous state in case something else changed it. $prev_suspend_addition = wp_suspend_cache_addition(); wp_suspend_cache_addition( $suspend_addition ); for ( $i = 1; $i <= JP_SITEMAP_UPDATE_SIZE; $i++ ) { if ( true === $this->build_next_sitemap_file() ) { break; // All finished! } } // Restore previous state. wp_suspend_cache_addition( $prev_suspend_addition ); if ( $this->logger ) { $this->logger->report( '-- ...done for now.' ); $this->logger->time(); } } /** * Generate the next sitemap file. * * Reads the most recent state of the sitemap generation phase, * constructs the next file, and updates the state. * * @since 4.8.0 * * @return bool True when finished. */ private function build_next_sitemap_file() { $finished = false; // Initialize finished flag. // Get the most recent state, and lock the state. $state = Jetpack_Sitemap_State::check_out(); // Do nothing if the state was locked. if ( false === $state ) { return false; } // Otherwise, branch on the sitemap-type key of $state. switch ( $state['sitemap-type'] ) { case JP_PAGE_SITEMAP_TYPE: $this->build_next_sitemap_of_type( JP_PAGE_SITEMAP_TYPE, array( $this, 'build_one_page_sitemap' ), $state ); break; case JP_PAGE_SITEMAP_INDEX_TYPE: $this->build_next_sitemap_index_of_type( JP_PAGE_SITEMAP_INDEX_TYPE, JP_IMAGE_SITEMAP_TYPE, $state ); break; case JP_IMAGE_SITEMAP_TYPE: $this->build_next_sitemap_of_type( JP_IMAGE_SITEMAP_TYPE, array( $this, 'build_one_image_sitemap' ), $state ); break; case JP_IMAGE_SITEMAP_INDEX_TYPE: $this->build_next_sitemap_index_of_type( JP_IMAGE_SITEMAP_INDEX_TYPE, JP_VIDEO_SITEMAP_TYPE, $state ); break; case JP_VIDEO_SITEMAP_TYPE: $this->build_next_sitemap_of_type( JP_VIDEO_SITEMAP_TYPE, array( $this, 'build_one_video_sitemap' ), $state ); break; case JP_VIDEO_SITEMAP_INDEX_TYPE: $this->build_next_sitemap_index_of_type( JP_VIDEO_SITEMAP_INDEX_TYPE, JP_MASTER_SITEMAP_TYPE, $state ); break; case JP_MASTER_SITEMAP_TYPE: $this->build_master_sitemap( $state['max'] ); // Reset the state and quit. Jetpack_Sitemap_State::reset( JP_PAGE_SITEMAP_TYPE ); if ( $this->logger ) { $this->logger->report( '-- Finished.' ); $this->logger->time(); } $finished = true; break; default: Jetpack_Sitemap_State::reset( JP_PAGE_SITEMAP_TYPE ); $finished = true; break; } // End switch. // Unlock the state. Jetpack_Sitemap_State::unlock(); return $finished; } /** * Build the next sitemap of a given type and update the sitemap state. * * @since 4.8.0 * * @param string $sitemap_type The type of the sitemap being generated. * @param callback $build_one A callback which builds a single sitemap file. * @param array $state A sitemap state. */ private function build_next_sitemap_of_type( $sitemap_type, $build_one, $state ) { $index_type = jp_sitemap_index_type_of( $sitemap_type ); // Try to build a sitemap. $result = call_user_func_array( $build_one, array( $state['number'] + 1, $state['last-added'], ) ); if ( false === $result ) { // If no sitemap was generated, advance to the next type. Jetpack_Sitemap_State::check_in( array( 'sitemap-type' => $index_type, 'last-added' => 0, 'number' => 0, 'last-modified' => '1970-01-01 00:00:00', ) ); if ( $this->logger ) { $this->logger->report( "-- Cleaning Up $sitemap_type" ); } // Clean up old files. $this->librarian->delete_numbered_sitemap_rows_after( $state['number'], $sitemap_type ); return; } // Otherwise, update the state. Jetpack_Sitemap_State::check_in( array( 'sitemap-type' => $state['sitemap-type'], 'last-added' => $result['last_id'], 'number' => $state['number'] + 1, 'last-modified' => $result['last_modified'], ) ); if ( true === $result['any_left'] ) { // If there's more work to be done with this type, return. return; } // Otherwise, advance state to the next sitemap type. Jetpack_Sitemap_State::check_in( array( 'sitemap-type' => $index_type, 'last-added' => 0, 'number' => 0, 'last-modified' => '1970-01-01 00:00:00', ) ); if ( $this->logger ) { $this->logger->report( "-- Cleaning Up $sitemap_type" ); } // Clean up old files. $this->librarian->delete_numbered_sitemap_rows_after( $state['number'] + 1, $sitemap_type ); } /** * Build the next sitemap index of a given type and update the state. * * @since 4.8.0 * * @param string $index_type The type of index being generated. * @param string $next_type The next type to generate after this one. * @param array $state A sitemap state. */ private function build_next_sitemap_index_of_type( $index_type, $next_type, $state ) { $sitemap_type = jp_sitemap_child_type_of( $index_type ); $sitemap_type_exists = isset( $state['max'][ $sitemap_type ] ) && is_array( $state['max'][ $sitemap_type ] ); // If only 0 or 1 sitemaps were built, advance to the next type and return. if ( $sitemap_type_exists && 1 >= $state['max'][ $sitemap_type ]['number'] ) { Jetpack_Sitemap_State::check_in( array( 'sitemap-type' => $next_type, 'last-added' => 0, 'number' => 0, 'last-modified' => '1970-01-01 00:00:00', ) ); if ( $this->logger ) { $this->logger->report( "-- Cleaning Up $index_type" ); } // There are no indices of this type. $this->librarian->delete_numbered_sitemap_rows_after( 0, $index_type ); return; } // Otherwise, try to build a sitemap index. $result = $this->build_one_sitemap_index( $state['number'] + 1, $state['last-added'], $state['last-modified'], $index_type ); // If no index was built, advance to the next type and return. if ( false === $result ) { Jetpack_Sitemap_State::check_in( array( 'sitemap-type' => $next_type, 'last-added' => 0, 'number' => 0, 'last-modified' => '1970-01-01 00:00:00', ) ); if ( $this->logger ) { $this->logger->report( "-- Cleaning Up $index_type" ); } // Clean up old files. $this->librarian->delete_numbered_sitemap_rows_after( $state['number'], $index_type ); return; } // Otherwise, check in the state. Jetpack_Sitemap_State::check_in( array( 'sitemap-type' => $index_type, 'last-added' => $result['last_id'], 'number' => $state['number'] + 1, 'last-modified' => $result['last_modified'], ) ); // If there are still sitemaps left to index, return. if ( true === $result['any_left'] ) { return; } // Otherwise, advance to the next type. Jetpack_Sitemap_State::check_in( array( 'sitemap-type' => $next_type, 'last-added' => 0, 'number' => 0, 'last-modified' => '1970-01-01 00:00:00', ) ); if ( $this->logger ) { $this->logger->report( "-- Cleaning Up $index_type" ); } // We're done generating indices of this type. $this->librarian->delete_numbered_sitemap_rows_after( $state['number'] + 1, $index_type ); } /** * Builds the master sitemap index. * * @param array $max Array of sitemap types with max index and datetime. * * @since 4.8.0 */ private function build_master_sitemap( $max ) { $page = array(); $image = array(); $video = array(); if ( $this->logger ) { $this->logger->report( '-- Building Master Sitemap.' ); } $buffer = Jetpack_Sitemap_Buffer_Factory::create( 'master', JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES ); if ( ! $buffer ) { return; } if ( isset( $max[ JP_PAGE_SITEMAP_TYPE ] ) && 0 < $max[ JP_PAGE_SITEMAP_TYPE ]['number'] ) { if ( 1 === $max[ JP_PAGE_SITEMAP_TYPE ]['number'] ) { $page['filename'] = jp_sitemap_filename( JP_PAGE_SITEMAP_TYPE, 1 ); $page['last_modified'] = jp_sitemap_datetime( $max[ JP_PAGE_SITEMAP_TYPE ]['lastmod'] ); } else { $page['filename'] = jp_sitemap_filename( JP_PAGE_SITEMAP_INDEX_TYPE, $max[ JP_PAGE_SITEMAP_INDEX_TYPE ]['number'] ); $page['last_modified'] = jp_sitemap_datetime( $max[ JP_PAGE_SITEMAP_INDEX_TYPE ]['lastmod'] ); } $buffer->append( array( 'sitemap' => array( 'loc' => $this->finder->construct_sitemap_url( $page['filename'] ), 'lastmod' => $page['last_modified'], ), ) ); } if ( isset( $max[ JP_IMAGE_SITEMAP_TYPE ] ) && 0 < $max[ JP_IMAGE_SITEMAP_TYPE ]['number'] ) { if ( 1 === $max[ JP_IMAGE_SITEMAP_TYPE ]['number'] ) { $image['filename'] = jp_sitemap_filename( JP_IMAGE_SITEMAP_TYPE, 1 ); $image['last_modified'] = jp_sitemap_datetime( $max[ JP_IMAGE_SITEMAP_TYPE ]['lastmod'] ); } else { $image['filename'] = jp_sitemap_filename( JP_IMAGE_SITEMAP_INDEX_TYPE, $max[ JP_IMAGE_SITEMAP_INDEX_TYPE ]['number'] ); $image['last_modified'] = jp_sitemap_datetime( $max[ JP_IMAGE_SITEMAP_INDEX_TYPE ]['lastmod'] ); } $buffer->append( array( 'sitemap' => array( 'loc' => $this->finder->construct_sitemap_url( $image['filename'] ), 'lastmod' => $image['last_modified'], ), ) ); } if ( isset( $max[ JP_VIDEO_SITEMAP_TYPE ] ) && 0 < $max[ JP_VIDEO_SITEMAP_TYPE ]['number'] ) { if ( 1 === $max[ JP_VIDEO_SITEMAP_TYPE ]['number'] ) { $video['filename'] = jp_sitemap_filename( JP_VIDEO_SITEMAP_TYPE, 1 ); $video['last_modified'] = jp_sitemap_datetime( $max[ JP_VIDEO_SITEMAP_TYPE ]['lastmod'] ); } else { $video['filename'] = jp_sitemap_filename( JP_VIDEO_SITEMAP_INDEX_TYPE, $max[ JP_VIDEO_SITEMAP_INDEX_TYPE ]['number'] ); $video['last_modified'] = jp_sitemap_datetime( $max[ JP_VIDEO_SITEMAP_INDEX_TYPE ]['lastmod'] ); } $buffer->append( array( 'sitemap' => array( 'loc' => $this->finder->construct_sitemap_url( $video['filename'] ), 'lastmod' => $video['last_modified'], ), ) ); } $this->librarian->store_sitemap_data( 0, JP_MASTER_SITEMAP_TYPE, $buffer->contents(), '' ); } /** * Build and store a single page sitemap. Returns false if no sitemap is built. * * Side effect: Create/update a sitemap row. * * @access private * @since 4.8.0 * * @param int $number The number of the current sitemap. * @param int $from_id The greatest lower bound of the IDs of the posts to be included. * * @return bool|array @args { * @type int $last_id The ID of the last item to be successfully added to the buffer. * @type bool $any_left 'true' if there are items which haven't been saved to a sitemap, 'false' otherwise. * @type string $last_modified The most recent timestamp to appear on the sitemap. * } */ public function build_one_page_sitemap( $number, $from_id ) { $last_post_id = $from_id; $any_posts_left = true; if ( $this->logger ) { $debug_name = jp_sitemap_filename( JP_PAGE_SITEMAP_TYPE, $number ); $this->logger->report( "-- Building $debug_name" ); } $buffer = Jetpack_Sitemap_Buffer_Factory::create( 'page', JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES ); if ( ! $buffer ) { return false; } // Add entry for the main page (only if we're at the first one) and it isn't already going to be included as a page. if ( 1 === $number && 'page' !== get_option( 'show_on_front' ) ) { $item_array = array( 'url' => array( 'loc' => home_url( '/' ), ), ); /** * Filter associative array with data to build <url> node * and its descendants for site home. * * @module sitemaps * * @since 3.9.0 * * @param array $blog_home Data to build parent and children nodes for site home. */ $item_array = apply_filters( 'jetpack_sitemap_url_home', $item_array ); $buffer->append( $item_array ); } // Add as many items to the buffer as possible. while ( $last_post_id >= 0 && false === $buffer->is_full() ) { $posts = $this->librarian->query_posts_after_id( $last_post_id, JP_SITEMAP_BATCH_SIZE ); if ( null == $posts ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok. $any_posts_left = false; break; } foreach ( $posts as $post ) { $current_item = $this->post_to_sitemap_item( $post ); if ( true === $buffer->append( $current_item['xml'] ) ) { $last_post_id = $post->ID; $buffer->view_time( $current_item['last_modified'] ); } else { break; } } } // Handle other page sitemap URLs. if ( false === $any_posts_left || $last_post_id < 0 ) { // Negative IDs are used to track URL indexes. $last_post_id = min( 0, $last_post_id ); $any_posts_left = true; // Reinitialize. /** * Filter other page sitemap URLs. * * @module sitemaps * * @since 6.1.0 * * @param array $urls An array of other URLs. */ $other_urls = apply_filters( 'jetpack_page_sitemap_other_urls', array() ); if ( $other_urls ) { // Start with index [1]. $other_urls = array_values( $other_urls ); array_unshift( $other_urls, $other_urls[0] ); unset( $other_urls[0] ); } // Add as many items to the buffer as possible. while ( false === $buffer->is_full() ) { $last_post_id_index = abs( $last_post_id ); $start_from_post_id_index = $last_post_id_index ? $last_post_id_index + 1 : 0; $urls = array_slice( $other_urls, $start_from_post_id_index, JP_SITEMAP_BATCH_SIZE, true ); if ( ! $urls ) { $any_posts_left = false; break; } foreach ( $urls as $index => $url ) { if ( ! is_array( $url ) ) { $url = array( 'loc' => $url ); } $item = array( 'xml' => compact( 'url' ) ); if ( true === $buffer->append( $item['xml'] ) ) { $last_post_id = -$index; if ( isset( $url['lastmod'] ) ) { $buffer->view_time( jp_sitemap_datetime( $url['lastmod'] ) ); } } else { break; } } } } // If no items were added, return false. if ( true === $buffer->is_empty() ) { return false; } /** * Filter sitemap before rendering it as XML. * * @module sitemaps * * @since 3.9.0 * @since 5.3.0 returns an element of DOMDocument type instead of SimpleXMLElement * * @param DOMDocument $doc Data tree for sitemap. * @param string $last_modified Date of last modification. */ if ( has_filter( 'jetpack_print_sitemap' ) ) { apply_filters( 'jetpack_print_sitemap', $buffer->get_document(), $buffer->last_modified() ); } // Store the buffer as the content of a sitemap row. $this->librarian->store_sitemap_data( $number, JP_PAGE_SITEMAP_TYPE, $buffer->contents(), $buffer->last_modified() ); /* * Now report back with the ID of the last post ID to be * successfully added and whether there are any posts left. */ return array( 'last_id' => $last_post_id, 'any_left' => $any_posts_left, 'last_modified' => $buffer->last_modified(), ); } /** * Build and store a single image sitemap. Returns false if no sitemap is built. * * Side effect: Create/update an image sitemap row. * * @access private * @since 4.8.0 * * @param int $number The number of the current sitemap. * @param int $from_id The greatest lower bound of the IDs of the posts to be included. * * @return bool|array @args { * @type int $last_id The ID of the last item to be successfully added to the buffer. * @type bool $any_left 'true' if there are items which haven't been saved to a sitemap, 'false' otherwise. * @type string $last_modified The most recent timestamp to appear on the sitemap. * } */ public function build_one_image_sitemap( $number, $from_id ) { $last_post_id = $from_id; $any_posts_left = true; if ( $this->logger ) { $debug_name = jp_sitemap_filename( JP_IMAGE_SITEMAP_TYPE, $number ); $this->logger->report( "-- Building $debug_name" ); } $buffer = Jetpack_Sitemap_Buffer_Factory::create( 'image', JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES ); if ( ! $buffer ) { return false; } // Add as many items to the buffer as possible. while ( false === $buffer->is_full() ) { $posts = $this->librarian->query_images_after_id( $last_post_id, JP_SITEMAP_BATCH_SIZE ); if ( null == $posts ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok. $any_posts_left = false; break; } foreach ( $posts as $post ) { $current_item = $this->image_post_to_sitemap_item( $post ); if ( true === $buffer->append( $current_item['xml'] ) ) { $last_post_id = $post->ID; $buffer->view_time( $current_item['last_modified'] ); } else { break; } } } // If no items were added, return false. if ( true === $buffer->is_empty() ) { return false; } // Store the buffer as the content of a jp_sitemap post. $this->librarian->store_sitemap_data( $number, JP_IMAGE_SITEMAP_TYPE, $buffer->contents(), $buffer->last_modified() ); /* * Now report back with the ID of the last post to be * successfully added and whether there are any posts left. */ return array( 'last_id' => $last_post_id, 'any_left' => $any_posts_left, 'last_modified' => $buffer->last_modified(), ); } /** * Build and store a single video sitemap. Returns false if no sitemap is built. * * Side effect: Create/update an video sitemap row. * * @access private * @since 4.8.0 * * @param int $number The number of the current sitemap. * @param int $from_id The greatest lower bound of the IDs of the posts to be included. * * @return bool|array @args { * @type int $last_id The ID of the last item to be successfully added to the buffer. * @type bool $any_left 'true' if there are items which haven't been saved to a sitemap, 'false' otherwise. * @type string $last_modified The most recent timestamp to appear on the sitemap. * } */ public function build_one_video_sitemap( $number, $from_id ) { $last_post_id = $from_id; $any_posts_left = true; if ( $this->logger ) { $debug_name = jp_sitemap_filename( JP_VIDEO_SITEMAP_TYPE, $number ); $this->logger->report( "-- Building $debug_name" ); } $buffer = Jetpack_Sitemap_Buffer_Factory::create( 'video', JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES ); if ( ! $buffer ) { return false; } // Add as many items to the buffer as possible. while ( false === $buffer->is_full() ) { $posts = $this->librarian->query_videos_after_id( $last_post_id, JP_SITEMAP_BATCH_SIZE ); if ( null == $posts ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok. $any_posts_left = false; break; } foreach ( $posts as $post ) { $current_item = $this->video_post_to_sitemap_item( $post ); if ( true === $buffer->append( $current_item['xml'] ) ) { $last_post_id = $post->ID; $buffer->view_time( $current_item['last_modified'] ); } else { break; } } } // If no items were added, return false. if ( true === $buffer->is_empty() ) { return false; } if ( false === $buffer->is_empty() ) { $this->librarian->store_sitemap_data( $number, JP_VIDEO_SITEMAP_TYPE, $buffer->contents(), $buffer->last_modified() ); } /* * Now report back with the ID of the last post to be * successfully added and whether there are any posts left. */ return array( 'last_id' => $last_post_id, 'any_left' => $any_posts_left, 'last_modified' => $buffer->last_modified(), ); } /** * Build and store a single page sitemap index. Return false if no index is built. * * Side effect: Create/update a sitemap index row. * * @access private * @since 4.8.0 * * @param int $number The number of the current sitemap index. * @param int $from_id The greatest lower bound of the IDs of the sitemaps to be included. * @param string $datetime Datetime of previous sitemap in 'YYYY-MM-DD hh:mm:ss' format. * @param string $index_type Sitemap index type. * * @return bool|array @args { * @type int $last_id The ID of the last item to be successfully added to the buffer. * @type bool $any_left 'true' if there are items which haven't been saved to a sitemap, 'false' otherwise. * @type string $last_modified The most recent timestamp to appear on the sitemap. * } */ private function build_one_sitemap_index( $number, $from_id, $datetime, $index_type ) { $last_sitemap_id = $from_id; $any_sitemaps_left = true; // Check the datetime format. $datetime = jp_sitemap_datetime( $datetime ); $sitemap_type = jp_sitemap_child_type_of( $index_type ); if ( $this->logger ) { $index_debug_name = jp_sitemap_filename( $index_type, $number ); $this->logger->report( "-- Building $index_debug_name" ); } $buffer = Jetpack_Sitemap_Buffer_Factory::create( 'master', JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES, $datetime ); if ( ! $buffer ) { return false; } // Add pointer to the previous sitemap index (unless we're at the first one). if ( 1 !== $number ) { $i = $number - 1; $prev_index_url = $this->finder->construct_sitemap_url( jp_sitemap_filename( $index_type, $i ) ); $item_array = array( 'sitemap' => array( 'loc' => $prev_index_url, 'lastmod' => $datetime, ), ); $buffer->append( $item_array ); } // Add as many items to the buffer as possible. while ( false === $buffer->is_full() ) { // Retrieve a batch of posts (in order). $posts = $this->librarian->query_sitemaps_after_id( $sitemap_type, $last_sitemap_id, JP_SITEMAP_BATCH_SIZE ); // If there were no posts to get, make a note. if ( null == $posts ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok. $any_sitemaps_left = false; break; } // Otherwise, loop through each post in the batch. foreach ( $posts as $post ) { // Generate the sitemap XML for the post. $current_item = $this->sitemap_row_to_index_item( (array) $post ); // Try adding this item to the buffer. if ( true === $buffer->append( $current_item['xml'] ) ) { $last_sitemap_id = $post['ID']; $buffer->view_time( $current_item['last_modified'] ); } else { // Otherwise stop looping through posts. break; } } } // If no items were added, return false. if ( true === $buffer->is_empty() ) { return false; } $this->librarian->store_sitemap_data( $number, $index_type, $buffer->contents(), $buffer->last_modified() ); /* * Now report back with the ID of the last sitemap post ID to * be successfully added, whether there are any sitemap posts * left, and the most recent modification time seen. */ return array( 'last_id' => $last_sitemap_id, 'any_left' => $any_sitemaps_left, 'last_modified' => $buffer->last_modified(), ); } /** * Construct the sitemap index url entry for a sitemap row. * * @link https://www.sitemaps.org/protocol.html#sitemapIndex_sitemap * * @access private * @since 4.8.0 * * @param array $row The sitemap data to be processed. * * @return string An XML fragment representing the post URL. */ private function sitemap_row_to_index_item( $row ) { $url = $this->finder->construct_sitemap_url( $row['post_title'] ); $item_array = array( 'sitemap' => array( 'loc' => $url, 'lastmod' => jp_sitemap_datetime( $row['post_date'] ), ), ); return array( 'xml' => $item_array, 'last_modified' => $row['post_date'], ); } /** * This is served instead of a 404 when the master sitemap is requested * but not yet generated. * * @access public * @since 6.7.0 * * @return string The empty sitemap xml. */ public function empty_sitemap_xml() { $empty_sitemap = new Jetpack_Sitemap_Buffer_Empty(); return $empty_sitemap->contents(); } /** * Build and return the news sitemap xml. Note that the result of this * function is cached in the transient 'jetpack_news_sitemap_xml'. * * @access public * @since 4.8.0 * * @return string The news sitemap xml. */ public function news_sitemap_xml() { $buffer = Jetpack_Sitemap_Buffer_Factory::create( 'news', JP_SITEMAP_MAX_ITEMS, JP_SITEMAP_MAX_BYTES ); if ( ! $buffer ) { return ''; } $the_stored_news_sitemap = get_transient( 'jetpack_news_sitemap_xml' ); if ( false === $the_stored_news_sitemap ) { if ( $this->logger ) { $this->logger->report( 'Beginning news sitemap generation.' ); } /** * Filter limit of entries to include in news sitemap. * * @module sitemaps * * @since 3.9.0 * * @param int $count Number of entries to include in news sitemap. */ $item_limit = apply_filters( 'jetpack_sitemap_news_sitemap_count', JP_NEWS_SITEMAP_MAX_ITEMS ); $posts = $this->librarian->query_most_recent_posts( $item_limit ); if ( empty( $posts ) ) { $buffer->append( array( 'url' => array( 'loc' => home_url( '/' ) ) ) ); } else { foreach ( $posts as $post ) { $current_item = $this->post_to_news_sitemap_item( $post ); if ( $current_item['xml'] !== null && false === $buffer->append( $current_item['xml'] ) ) { break; } } } if ( $this->logger ) { $this->logger->time( 'End news sitemap generation.' ); } $the_stored_news_sitemap = $buffer->contents(); set_transient( 'jetpack_news_sitemap_xml', $the_stored_news_sitemap, JP_NEWS_SITEMAP_INTERVAL ); } // End if. return $the_stored_news_sitemap; } /** * Construct the sitemap url entry for a WP_Post. * * @link https://www.sitemaps.org/protocol.html#urldef * @access private * @since 4.8.0 * * @param object $post The post to be processed. Similar to WP_Post, but without post_content and post_content_filtered. * * @return array * @type array $xml An XML fragment representing the post URL. * @type string $last_modified Date post was last modified. */ private function post_to_sitemap_item( $post ) { /** * Filter condition to allow skipping specific posts in sitemap. * * @module sitemaps * * @since 3.9.0 * * @param bool $skip Current boolean. False by default, so no post is skipped. * @param object $post Current post in the form of a $wpdb result object. Not WP_Post. * Doesn't have all the properties of a WP_Post. */ if ( true === apply_filters( 'jetpack_sitemap_skip_post', false, $post ) ) { return array( 'xml' => null, 'last_modified' => null, ); } $url = esc_url( get_permalink( $post ) ); /* * Spec requires the URL to be <=2048 bytes. * In practice this constraint is unlikely to be violated. */ if ( 2048 < strlen( $url ) ) { $url = home_url() . '/?p=' . $post->ID; } $last_modified = $post->post_modified_gmt; // Check for more recent comments. // Note that 'Y-m-d h:i:s' strings sort lexicographically. if ( 0 < $post->comment_count ) { $last_modified = max( $last_modified, $this->librarian->query_latest_approved_comment_time_on_post( $post->ID ) ); } $item_array = array( 'url' => array( 'loc' => $url, 'lastmod' => jp_sitemap_datetime( $last_modified ), ), ); /** * Filter sitemap URL item before rendering it as XML. * * @module sitemaps * * @since 3.9.0 * * @param array $tree Associative array representing sitemap URL element. * @param int $post_id ID of the post being processed. */ $item_array = apply_filters( 'jetpack_sitemap_url', $item_array, $post->ID ); return array( 'xml' => $item_array, 'last_modified' => $last_modified, ); } /** * Construct the image sitemap url entry for a WP_Post of image type. * * @link https://www.sitemaps.org/protocol.html#urldef * * @access private * @since 4.8.0 * * @param WP_Post $post The image post to be processed. * * @return array * @type array $xml An XML fragment representing the post URL. * @type string $last_modified Date post was last modified. */ private function image_post_to_sitemap_item( $post ) { /** * Filter condition to allow skipping specific image posts in the sitemap. * * @module sitemaps * * @since 4.8.0 * * @param bool $skip Current boolean. False by default, so no post is skipped. * @param WP_POST $post Current post object. */ if ( apply_filters( 'jetpack_sitemap_image_skip_post', false, $post ) ) { return array( 'xml' => null, 'last_modified' => null, ); } $url = wp_get_attachment_url( $post->ID ); // Do not include the image if the attached parent is not published. // Unattached will be published. Otherwise, will inherit parent status. if ( 'publish' !== get_post_status( $post ) ) { return array( 'xml' => null, 'last_modified' => null, ); } $parent_url = get_permalink( get_post( $post->post_parent ) ); if ( '' == $parent_url ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok. $parent_url = get_permalink( $post ); } $item_array = array( 'url' => array( 'loc' => $parent_url, 'lastmod' => jp_sitemap_datetime( $post->post_modified_gmt ), 'image:image' => array( 'image:loc' => $url, ), ), ); /** * Filter associative array with data to build <url> node * and its descendants for current post in image sitemap. * * @module sitemaps * * @since 4.8.0 * * @param array $item_array Data to build parent and children nodes for current post. * @param int $post_id Current image post ID. */ $item_array = apply_filters( 'jetpack_sitemap_image_sitemap_item', $item_array, $post->ID ); return array( 'xml' => $item_array, 'last_modified' => $post->post_modified_gmt, ); } /** * Construct the video sitemap url entry for a WP_Post of video type. * * @link https://www.sitemaps.org/protocol.html#urldef * @link https://developers.google.com/webmasters/videosearch/sitemaps * * @access private * @since 4.8.0 * * @param WP_Post $post The video post to be processed. * * @return array * @type array $xml An XML fragment representing the post URL. * @type string $last_modified Date post was last modified. */ private function video_post_to_sitemap_item( $post ) { /** * Filter condition to allow skipping specific video posts in the sitemap. * * @module sitemaps * * @since 4.8.0 * * @param bool $skip Current boolean. False by default, so no post is skipped. * @param WP_POST $post Current post object. */ if ( apply_filters( 'jetpack_sitemap_video_skip_post', false, $post ) ) { return array( 'xml' => null, 'last_modified' => null, ); } // Do not include the video if the attached parent is not published. // Unattached will be published. Otherwise, will inherit parent status. if ( 'publish' !== get_post_status( $post ) ) { return array( 'xml' => null, 'last_modified' => null, ); } $parent_url = esc_url( get_permalink( get_post( $post->post_parent ) ) ); if ( '' == $parent_url ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- WPCS: loose comparison ok. $parent_url = esc_url( get_permalink( $post ) ); } // Prepare the content like get_the_content_feed(). $content = $post->post_content; /** This filter is already documented in core/wp-includes/post-template.php */ $content = apply_filters( 'the_content', $content ); /** This filter is already documented in core/wp-includes/feed.php */ $content = apply_filters( 'the_content_feed', $content, 'rss2' ); // Include thumbnails for VideoPress videos, use blank image for others. if ( 'complete' === get_post_meta( $post->ID, 'videopress_status', true ) && has_post_thumbnail( $post ) ) { $video_thumbnail_url = get_the_post_thumbnail_url( $post ); } else { /** * Filter the thumbnail image used in the video sitemap for non-VideoPress videos. * * @since 7.2.0 * * @param string $str Image URL. */ $video_thumbnail_url = apply_filters( 'jetpack_video_sitemap_default_thumbnail', 'https://s0.wp.com/i/blank.jpg' ); } $item_array = array( 'url' => array( 'loc' => $parent_url, 'lastmod' => jp_sitemap_datetime( $post->post_modified_gmt ), 'video:video' => array( /** This filter is already documented in core/wp-includes/feed.php */ 'video:title' => apply_filters( 'the_title_rss', $post->post_title ), 'video:thumbnail_loc' => esc_url( $video_thumbnail_url ), 'video:description' => $content, 'video:content_loc' => esc_url( wp_get_attachment_url( $post->ID ) ), ), ), ); // TODO: Integrate with VideoPress here. // cf. video:player_loc tag in video sitemap spec. /** * Filter associative array with data to build <url> node * and its descendants for current post in video sitemap. * * @module sitemaps * * @since 4.8.0 * * @param array $item_array Data to build parent and children nodes for current post. * @param int $post_id Current video post ID. */ $item_array = apply_filters( 'jetpack_sitemap_video_sitemap_item', $item_array, $post->ID ); return array( 'xml' => $item_array, 'last_modified' => $post->post_modified_gmt, ); } /** * Construct the news sitemap url entry for a WP_Post. * * @link https://www.sitemaps.org/protocol.html#urldef * * @access private * @since 4.8.0 * * @param object $post The post to be processed. Similar to WP_Post, but without post_content and post_content_filtered. * * @return string An XML fragment representing the post URL. */ private function post_to_news_sitemap_item( $post ) { // Exclude posts with meta 'jetpack_seo_noindex' set true from the Jetpack news sitemap. add_filter( 'jetpack_sitemap_news_skip_post', array( 'Jetpack_SEO_Posts', 'exclude_noindex_posts_from_jetpack_sitemap' ), 10, 2 ); /** * Filter condition to allow skipping specific posts in news sitemap. * * @module sitemaps * * @since 3.9.0 * * @param bool $skip Current boolean. False by default, so no post is skipped. * @param object $post Current post in the form of a $wpdb result object. Not WP_Post. * Doesn't have all the properties of a WP_Post. */ if ( apply_filters( 'jetpack_sitemap_news_skip_post', false, $post ) ) { return array( 'xml' => null, ); } $url = get_permalink( $post ); /* * Spec requires the URL to be <=2048 bytes. * In practice this constraint is unlikely to be violated. */ if ( 2048 < strlen( $url ) ) { $url = home_url() . '/?p=' . $post->ID; } /* * Trim the locale to an ISO 639 language code as required by Google. * Special cases are zh-cn (Simplified Chinese) and zh-tw (Traditional Chinese). * @link https://www.loc.gov/standards/iso639-2/php/code_list.php */ $language = strtolower( get_locale() ); if ( in_array( $language, array( 'zh_tw', 'zh_cn' ), true ) ) { $language = str_replace( '_', '-', $language ); } else { $language = preg_replace( '/(_.*)$/i', '', $language ); } $item_array = array( 'url' => array( 'loc' => $url, 'lastmod' => jp_sitemap_datetime( $post->post_modified_gmt ), 'news:news' => array( 'news:publication' => array( 'news:name' => html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ), 'news:language' => $language, ), /** This filter is already documented in core/wp-includes/feed.php */ 'news:title' => apply_filters( 'the_title_rss', $post->post_title ), 'news:publication_date' => jp_sitemap_datetime( $post->post_date_gmt ), 'news:genres' => 'Blog', ), ), ); /** * Filter associative array with data to build <url> node * and its descendants for current post in news sitemap. * * @module sitemaps * * @since 3.9.0 * * @param array $item_array Data to build parent and children nodes for current post. * @param int $post_id Current post ID. */ $item_array = apply_filters( 'jetpack_sitemap_news_sitemap_item', $item_array, $post->ID ); return array( 'xml' => $item_array, ); } } sitemaps/sitemap-buffer-image-xmlwriter.php 0000644 00000004534 15174711637 0015150 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * XMLWriter implementation of the image sitemap buffer. * * @since 14.6 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap image xml files using XMLWriter. * * @since 14.6 */ class Jetpack_Sitemap_Buffer_Image_XMLWriter extends Jetpack_Sitemap_Buffer_XMLWriter { /** * Initialize the buffer with required headers (no root element here). */ protected function initialize_buffer() { // Add generator comment $this->writer->writeComment( "generator='jetpack-" . JETPACK__VERSION . "'" ); $this->writer->writeComment( 'Jetpack_Sitemap_Buffer_Image_XMLWriter' ); // Add stylesheet $this->writer->writePi( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'image-sitemap.xsl' ) . '"' ); } /** * Start the root element and write its namespaces. */ protected function start_root() { $this->writer->startElement( 'urlset' ); /** * Filter the XML namespaces included in image sitemaps. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_image_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:image' => 'http://www.google.com/schemas/sitemap-image/1.1', ) ); foreach ( $namespaces as $name => $value ) { $this->writer->writeAttribute( $name, $value ); } } /** * Append a URL entry with image information to the sitemap. * * @param array $array The URL item to append. */ protected function append_item( $array ) { if ( ! isset( $array['url'] ) || ! is_array( $array['url'] ) ) { return; } if ( isset( $array['url']['image:image'] ) ) { if ( empty( $array['url']['image:image']['image:title'] ) ) { unset( $array['url']['image:image']['image:title'] ); } if ( empty( $array['url']['image:image']['image:caption'] ) ) { unset( $array['url']['image:image']['image:caption'] ); } } $this->array_to_xml( $array ); } } sitemaps/sitemap-buffer-master-xmlwriter.php 0000644 00000002550 15174711637 0015355 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * XMLWriter implementation of the master sitemap buffer. * * @since 14.6 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing master sitemap xml files using XMLWriter. * * @since 14.6 */ class Jetpack_Sitemap_Buffer_Master_XMLWriter extends Jetpack_Sitemap_Buffer_XMLWriter { /** * Initialize the buffer with required headers (no root element here). */ protected function initialize_buffer() { // Add generator comment $this->writer->writeComment( "generator='jetpack-" . JETPACK__VERSION . "'" ); $this->writer->writeComment( 'Jetpack_Sitemap_Buffer_Master_XMLWriter' ); // Add stylesheet $this->writer->writePi( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap-index.xsl' ) . '"' ); } /** * Start the root element and write its namespaces. */ protected function start_root() { $this->writer->startElement( 'sitemapindex' ); $this->writer->writeAttribute( 'xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9' ); } /** * Append a sitemap entry to the master sitemap. * * @param array $array The sitemap item to append. */ protected function append_item( $array ) { if ( ! empty( $array['sitemap'] ) ) { $this->array_to_xml( $array ); } } } sitemaps/sitemap-buffer-video-xmlwriter.php 0000644 00000004100 15174711637 0015161 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * XMLWriter implementation of the video sitemap buffer. * * @since 14.6 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap video xml files using XMLWriter. * * @since 14.6 */ class Jetpack_Sitemap_Buffer_Video_XMLWriter extends Jetpack_Sitemap_Buffer_XMLWriter { /** * Initialize the buffer with required headers (no root element here). */ protected function initialize_buffer() { // Add generator comment $this->writer->writeComment( "generator='jetpack-" . JETPACK__VERSION . "'" ); $this->writer->writeComment( 'Jetpack_Sitemap_Buffer_Video_XMLWriter' ); // Add stylesheet $this->writer->writePi( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'video-sitemap.xsl' ) . '"' ); } /** * Start the root element and write its namespaces. */ protected function start_root() { $this->writer->startElement( 'urlset' ); /** * Filter the XML namespaces included in video sitemaps. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_video_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:video' => 'http://www.google.com/schemas/sitemap-video/1.1', ) ); foreach ( $namespaces as $name => $value ) { $this->writer->writeAttribute( $name, $value ); } } /** * Append a URL entry with video information to the sitemap. * * @param array $array The URL item to append. */ protected function append_item( $array ) { // Return early if missing fundamental data. if ( empty( $array['url']['video:video'] ) ) { return; } $this->array_to_xml( $array ); } } sitemaps/sitemap-buffer-page.php 0000644 00000005061 15174711637 0012743 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Page * extends the Jetpack_Sitemap_Buffer class to represent the single page sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap page xml files. * * @since 5.3.0 */ class Jetpack_Sitemap_Buffer_Page extends Jetpack_Sitemap_Buffer { /** * Jetpack_Sitemap_Buffer_Page constructor. * * @param int $item_limit The maximum size of the buffer in items. * @param int $byte_limit The maximum size of the buffer in bytes. * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format. */ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) { parent::__construct( $item_limit, $byte_limit, $time ); $this->doc->appendChild( $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" ) ); $this->doc->appendChild( $this->doc->createComment( 'Jetpack_Sitemap_Buffer_Page' ) ); $this->doc->appendChild( $this->doc->createProcessingInstruction( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap.xsl' ) . '"' ) ); } /** * Returns a DOM element that contains all single page sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { /** * Filter the attribute value pairs used for namespace and namespace URI mappings. * * @module sitemaps * * @since 3.9.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', ) ); $this->root = $this->doc->createElement( 'urlset' ); foreach ( $namespaces as $name => $value ) { $this->root->setAttribute( $name, $value ); } $this->doc->appendChild( $this->root ); $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) ); } return $this->root; } } sitemaps/sitemap-buffer-news-fallback.php 0000644 00000004245 15174711637 0014543 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_News * extends the Jetpack_Sitemap_Buffer class to represent the single news sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap news xml files for users without libxml support. * * @since 5.3.0 * @phan-suppress PhanRedefinedClassReference -- Don't conflict with real version. */ class Jetpack_Sitemap_Buffer_News extends Jetpack_Sitemap_Buffer_Fallback { // @phan-suppress-previous-line UnusedSuppression -- It's used. /** * Returns a DOM element that contains all news sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { /** * Filter the attribute value pairs used for namespace and namespace URI mappings. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_news_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:news' => 'http://www.google.com/schemas/sitemap-news/0.9', ) ); $jetpack_version = JETPACK__VERSION; $news_sitemap_xsl_url = $this->finder->construct_sitemap_url( 'news-sitemap.xsl' ); $this->root = array( "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL . "<?xml-stylesheet type='text/xsl' href='{$news_sitemap_xsl_url}'?>" . PHP_EOL . '<urlset ' . $this->array_to_xml_attr_string( $namespaces ) . '>', '</urlset>', ); $this->byte_capacity -= strlen( implode( '', $this->root ) ); } return $this->root; } } sitemaps/sitemap-buffer-xmlwriter.php 0000644 00000023071 15174711637 0014065 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * XMLWriter implementation of the sitemap buffer. * * @since 14.6 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap xml files using XMLWriter. * * @since 14.6 */ abstract class Jetpack_Sitemap_Buffer_XMLWriter { /** * Largest number of items the buffer can hold. * * @access protected * @since 14.6 * @var int $item_capacity The item capacity. */ protected $item_capacity; /** * Largest number of bytes the buffer can hold. * * @access protected * @since 14.6 * @var int $byte_capacity The byte capacity. */ protected $byte_capacity; /** * Flag which detects when the buffer is full. * * @access protected * @since 14.6 * @var bool $is_full_flag The flag value. */ protected $is_full_flag; /** * Flag which detects when the buffer is empty. * Set true on construction and flipped to false only after a successful append. * * @since 15.0 * @var bool */ protected $is_empty_flag = true; /** * The most recent timestamp seen by the buffer. * * @access protected * @since 14.6 * @var string $timestamp Must be in 'YYYY-MM-DD hh:mm:ss' format. */ protected $timestamp; /** * The XMLWriter instance used to construct the XML. * * @access protected * @since 14.6 * @var XMLWriter $writer */ protected $writer; /** * Helper class to construct sitemap paths. * * @since 14.6 * @protected * @var Jetpack_Sitemap_Finder */ protected $finder; /** * The XML content chunks collected from XMLWriter. * * Collect chunks and join once at the end to reduce string reallocations * and improve performance on large sitemaps. * * @access protected * @since 15.0 * @var array $chunks */ protected $chunks = array(); /** * Tracks whether the root element has been started. * * @since 15.0 * @var bool */ protected $root_started = false; /** * Mirror DOMDocument built on-demand for jetpack_print_sitemap compatibility. * * @since 15.0 * @var DOMDocument|null */ protected $dom_document = null; /** * Tracks whether XMLWriter document has been finalized (closed and flushed). * * @since 15.0 * @var bool */ protected $is_finalized = false; /** * Construct a new Jetpack_Sitemap_Buffer_XMLWriter. * * @since 14.6 * * @param int $item_limit The maximum size of the buffer in items. * @param int $byte_limit The maximum size of the buffer in bytes. * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format. */ public function __construct( $item_limit, $byte_limit, $time ) { $this->is_full_flag = false; $this->is_empty_flag = true; $this->timestamp = $time; $this->finder = new Jetpack_Sitemap_Finder(); $this->writer = new XMLWriter(); $this->writer->openMemory(); $this->writer->setIndent( true ); $this->writer->startDocument( '1.0', 'UTF-8' ); $this->item_capacity = max( 1, (int) $item_limit ); $this->byte_capacity = max( 1, (int) $byte_limit ); // Capture and account the XML declaration bytes to mirror DOM behavior. $declaration = $this->writer->outputMemory( true ); $this->chunks[] = $declaration; $this->byte_capacity -= strlen( $declaration ); // Allow subclasses to write comments and processing instructions only. $this->initialize_buffer(); // Capture pre-root bytes (comments/PI). Do not subtract from capacity. $pre_root_output = $this->writer->outputMemory( true ); $this->chunks[] = $pre_root_output; } /** * Initialize the buffer with any required headers or setup. * This should be implemented by child classes. * * @access protected * @since 14.6 */ abstract protected function initialize_buffer(); /** * Start the root element (e.g., urlset or sitemapindex) and write its attributes. * Implemented by subclasses. * * @since 15.0 * @access protected * @return void */ abstract protected function start_root(); /** * Ensure the root element has been started and account its bytes once. * * @since 15.0 * @access protected * @return void */ protected function ensure_root_started() { if ( $this->root_started ) { return; } $this->start_root(); $root_chunk = $this->writer->outputMemory( true ); $this->chunks[] = $root_chunk; $this->byte_capacity -= strlen( $root_chunk ); $this->root_started = true; } /** * Finalize writer output once by closing the root and document and flushing. * * @since 15.0 * @access protected * @return void */ protected function finalize_writer_output() { if ( $this->is_finalized ) { return; } $this->ensure_root_started(); $this->writer->endElement(); // End root element (urlset/sitemapindex) $this->writer->endDocument(); $final_content = $this->writer->outputMemory( true ); $this->chunks[] = $final_content; $this->is_finalized = true; } /** * Append an item to the buffer. * * @since 14.6 * * @param array $array The item to be added. * @return bool True if the append succeeded, False if not. */ public function append( $array ) { if ( $array === null ) { return true; } if ( $this->is_full_flag ) { return false; } if ( 0 >= $this->item_capacity || 0 >= $this->byte_capacity ) { $this->is_full_flag = true; return false; } // Ensure root is started on first append and account its bytes. $this->ensure_root_started(); // Attempt to render the item. Subclasses may decide to skip writing // if the input structure is invalid for that sitemap type. $this->append_item( $array ); // Capture only the bytes produced by this item. $new_content = $this->writer->outputMemory( true ); // If nothing was written, treat as a no-op: keep the buffer "empty" // and do not consume item/byte capacities. if ( '' === $new_content ) { return true; } // Persist newly written bytes and update capacities. $this->chunks[] = $new_content; $this->item_capacity -= 1; $this->byte_capacity -= strlen( $new_content ); $this->is_empty_flag = false; // Check both capacity limits. if ( 0 >= $this->item_capacity || $this->byte_capacity <= 0 ) { $this->is_full_flag = true; } return true; } /** * Append a specific item to the buffer. * This should be implemented by child classes. * * @access protected * @since 14.6 * @param array $array The item to be added. */ abstract protected function append_item( $array ); /** * Recursively writes XML elements from an associative array. * * This method iterates through an array and writes XML elements using the XMLWriter instance. * If a value in the array is itself an array, it calls itself recursively. * * @access protected * @since 15.0 * * @param array $data The array to convert to XML. */ protected function array_to_xml( $data ) { foreach ( (array) $data as $tag => $value ) { if ( is_array( $value ) ) { $this->writer->startElement( $tag ); $this->array_to_xml( $value ); $this->writer->endElement(); } else { // Write raw text; XMLWriter will escape XML-reserved chars, matching DOMDocument behavior. $this->writer->writeElement( $tag, (string) $value ); } } } /** * Retrieve the contents of the buffer. * * @since 14.6 * @return string The contents of the buffer. */ public function contents() { $this->finalize_writer_output(); if ( $this->dom_document instanceof DOMDocument ) { return $this->dom_document->saveXML(); } if ( empty( $this->chunks ) ) { // If buffer is empty, return a minimal valid XML structure return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"></urlset>"; } return implode( '', $this->chunks ); } /** * Detect whether the buffer is full. * * @since 14.6 * @return bool True if the buffer is full, false otherwise. */ public function is_full() { return $this->is_full_flag; } /** * Detect whether the buffer is empty. * * @since 14.6 * @return bool True if the buffer is empty, false otherwise. */ public function is_empty() { return $this->is_empty_flag; } /** * Update the timestamp of the buffer. * * @since 14.6 * @param string $new_time A datetime string in 'YYYY-MM-DD hh:mm:ss' format. */ public function view_time( $new_time ) { $this->timestamp = max( $this->timestamp, $new_time ); } /** * Retrieve the timestamp of the buffer. * * @since 14.6 * @return string A datetime string in 'YYYY-MM-DD hh:mm:ss' format. */ public function last_modified() { return $this->timestamp; } /** * Compatibility method for the old DOMDocument implementation. * This is only here to satisfy the jetpack_print_sitemap filter. * * @since 14.6 * @return DOMDocument DOM representation of the current sitemap contents. */ public function get_document() { if ( $this->dom_document instanceof DOMDocument ) { return $this->dom_document; } $this->finalize_writer_output(); $dom = new DOMDocument( '1.0', 'UTF-8' ); $dom->formatOutput = true; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $dom->preserveWhiteSpace = false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase // Load current XML content into DOM for compatibility with filters. @$dom->loadXML( implode( '', $this->chunks ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Avoid fatal on unexpected content $this->dom_document = $dom; return $this->dom_document; } } sitemaps/sitemap-finder.php 0000644 00000005775 15174711637 0012043 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * The functions in this class provide an API for handling * sitemap related URIs. * * @package automattic/jetpack * @since 4.8.0 * @author Automattic */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * The Jetpack_Sitemap_Finder object deals with constructing * sitemap URIs. * * @since 4.8.0 */ class Jetpack_Sitemap_Finder { /** * Construct the complete URL of a sitemap file. Depends on * permalink settings. * * @access public * @since 4.8.0 * @since 4.8.1 Call jetpack_sitemap_uri() * * @param string $filename The filename of the sitemap. * * @return string Complete URI of the given sitemap file. */ public function construct_sitemap_url( $filename ) { $url = jetpack_sitemap_uri( $filename ); if ( pathinfo( $filename, PATHINFO_EXTENSION ) === 'xsl' ) { // Strip scheme for sites where sitemap could be access via http or https. $url = preg_replace( '/^https?:/', '', $url ); } return $url; } /** * Path and query prefix of sitemap files. Depends on permalink * settings. * * @access public * @since 4.8.0 * * @return string The path+query prefix. */ public function the_jetpack_sitemap_path_and_query_prefix() { global $wp_rewrite; // Get path fragment from home_url(). $home = wp_parse_url( home_url() ); if ( isset( $home['path'] ) ) { $home_path = $home['path']; } else { $home_path = ''; } // Get additional path fragment from filter. $location = Jetpack_Options::get_option_and_ensure_autoload( 'jetpack_sitemap_location', '' ); if ( $wp_rewrite->using_index_permalinks() ) { return $home_path . '/index.php' . $location . '/'; } elseif ( $wp_rewrite->using_permalinks() ) { return $home_path . $location . '/'; } else { return $home_path . $location . '/?jetpack-sitemap='; } } /** * Examine a path+query URI fragment looking for a sitemap request. * * @access public * @since 4.8.0 * * @param string $raw_uri A URI (path+query only) to test for sitemap-ness. * * @return array @args { * @type string $sitemap_name The recognized sitemap name (or null). * } */ public function recognize_sitemap_uri( $raw_uri ) { // The path+query where sitemaps are served. $sitemap_path = $this->the_jetpack_sitemap_path_and_query_prefix(); // A regex which detects $sitemap_path at the beginning of a string. $path_regex = '/^' . preg_quote( $sitemap_path, '/' ) . '/'; // Check that the request URI begins with the sitemap path. if ( preg_match( $path_regex, $raw_uri ) ) { // Strip off the $sitemap_path and any trailing slash. $stripped_uri = preg_replace( $path_regex, '', rtrim( $raw_uri, '/' ) ); } else { $stripped_uri = ''; } // Check that the stripped uri begins with one of the sitemap prefixes. if ( preg_match( '/^sitemap|^image-s|^news-s|^video-s/', $stripped_uri ) ) { $filename = $stripped_uri; } else { $filename = null; } return array( 'sitemap_name' => $filename, ); } } sitemaps/sitemaps.php 0000644 00000044124 15174711637 0010750 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Generate sitemap files in base XML as well as some namespace extensions. * * This module generates two different base sitemaps. * * 1. sitemap.xml * The basic sitemap is updated regularly by wp-cron. It is stored in the * database and retrieved when requested. This sitemap aims to include canonical * URLs for all published content and abide by the sitemap spec. This is the root * of a tree of sitemap and sitemap index xml files, depending on the number of URLs. * * By default the sitemap contains published posts of type 'post' and 'page', as * well as the home url. To include other post types use the 'jetpack_sitemap_post_types' * filter. * * @link https://www.sitemaps.org/protocol.html Base sitemaps protocol. * @link https://support.google.com/webmasters/answer/178636 Image sitemap extension. * @link https://developers.google.com/webmasters/videosearch/sitemaps Video sitemap extension. * * 2. news-sitemap.xml * The news sitemap is generated on the fly when requested. It does not aim for * completeness, instead including at most 1000 of the most recent published posts * from the previous 2 days, per the news-sitemap spec. * * @link https://support.google.com/webmasters/answer/74288 News sitemap extension. * * @package automattic/jetpack * @since 3.9.0 * @since 4.8.0 Remove 1000 post limit. * @author Automattic */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /* Include all of the sitemap subclasses. */ require_once __DIR__ . '/sitemap-constants.php'; require_once __DIR__ . '/sitemap-buffer.php'; require_once __DIR__ . '/sitemap-buffer-fallback.php'; require_once __DIR__ . '/sitemap-buffer-xmlwriter.php'; require_once __DIR__ . '/sitemap-buffer-page-xmlwriter.php'; require_once __DIR__ . '/sitemap-buffer-image-xmlwriter.php'; require_once __DIR__ . '/sitemap-buffer-video-xmlwriter.php'; require_once __DIR__ . '/sitemap-buffer-news-xmlwriter.php'; require_once __DIR__ . '/sitemap-buffer-master-xmlwriter.php'; require_once __DIR__ . '/sitemap-buffer-factory.php'; require_once __DIR__ . '/sitemap-stylist.php'; require_once __DIR__ . '/sitemap-librarian.php'; require_once __DIR__ . '/sitemap-finder.php'; require_once __DIR__ . '/sitemap-builder.php'; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { require_once __DIR__ . '/sitemap-logger.php'; } // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files. /** * Governs the generation, storage, and serving of sitemaps. * * @since 4.8.0 * * @phan-constructor-used-for-side-effects */ class Jetpack_Sitemap_Manager { /** * Librarian object for storing and retrieving sitemap data. * * @see Jetpack_Sitemap_Librarian * @since 4.8.0 * @var Jetpack_Sitemap_Librarian $librarian Librarian object for storing and retrieving sitemap data. */ private $librarian; /** * Logger object for reporting debug messages. * * @see Jetpack_Sitemap_Logger * @since 4.8.0 * @var Jetpack_Sitemap_Logger $logger Logger object for reporting debug messages. */ private $logger; /** * Finder object for handling sitemap URIs. * * @see Jetpack_Sitemap_Finder * @since 4.8.0 * @var Jetpack_Sitemap_Finder $finder Finder object for handling with sitemap URIs. */ private $finder; /** * Construct a new Jetpack_Sitemap_Manager. * * @access public * @since 4.8.0 */ public function __construct() { $this->librarian = new Jetpack_Sitemap_Librarian(); $this->finder = new Jetpack_Sitemap_Finder(); if ( defined( 'WP_DEBUG' ) && ( true === WP_DEBUG ) ) { $this->logger = new Jetpack_Sitemap_Logger(); } // Add callback for sitemap URL handler. add_action( 'wp_loaded', array( $this, 'callback_action_catch_sitemap_urls' ), defined( 'IS_WPCOM' ) && IS_WPCOM ? 100 : 10 ); // Add generator to wp_cron task list. $this->schedule_sitemap_generation(); // Add sitemap to robots.txt. add_action( 'do_robotstxt', array( $this, 'callback_action_do_robotstxt' ), 20 ); // The news sitemap is cached; here we add a callback to // flush the cached news sitemap when a post is published. add_action( 'publish_post', array( $this, 'callback_action_flush_news_sitemap_cache' ), 10 ); // In case we need to purge all sitemaps, we do this. add_action( 'jetpack_sitemaps_purge_data', array( $this, 'callback_action_purge_data' ) ); /* * Module parameters are stored as options in the database. * This allows us to avoid having to process all of init * before serving the sitemap data. The following actions * process and store these filters. */ // Process filters and store location string for sitemap. add_action( 'init', array( $this, 'callback_action_filter_sitemap_location' ), 999 ); } /** * Echo a raw string of given content-type. * * @access private * @since 4.8.0 * * @param string $the_content_type The content type to be served. * @param string $the_content The string to be echoed. * @return never */ private function serve_raw_and_die( $the_content_type, $the_content ) { header( 'Content-Type: ' . $the_content_type . '; charset=UTF-8' ); global $wp_query; $wp_query->is_feed = true; set_query_var( 'feed', 'sitemap' ); if ( '' === $the_content ) { $error = __( 'No sitemap found. Please try again later.', 'jetpack' ); if ( current_user_can( 'manage_options' ) ) { $next = human_time_diff( wp_next_scheduled( 'jp_sitemap_cron_hook' ) ); /* translators: %s is a human_time_diff until next sitemap generation. */ $error = sprintf( __( 'No sitemap found. The system will try to build it again in %s.', 'jetpack' ), $next ); } wp_die( esc_html( $error ), esc_html__( 'Sitemaps', 'jetpack' ), array( 'response' => 404, ) ); } echo $the_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All content created by Jetpack. die( 0 ); } /** * Callback to intercept sitemap url requests and serve sitemap files. * * @access public * @since 4.8.0 */ public function callback_action_catch_sitemap_urls() { // Regular expressions for sitemap URL routing. $regex = array( 'sitemap' => '/^sitemap-[1-9][0-9]*\.xml$/', 'index' => '/^sitemap-index-[1-9][0-9]*\.xml$/', 'image' => '/^image-sitemap-[1-9][0-9]*\.xml$/', 'image-index' => '/^image-sitemap-index-[1-9][0-9]*\.xml$/', 'video' => '/^video-sitemap-[1-9][0-9]*\.xml$/', 'video-index' => '/^video-sitemap-index-[1-9][0-9]*\.xml$/', ); // The raw path(+query) of the requested URI. if ( isset( $_SERVER['REQUEST_URI'] ) ) { // WPCS: Input var okay. $raw_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) // WPCS: Input var okay. ); } else { $raw_uri = ''; } $request = $this->finder->recognize_sitemap_uri( $raw_uri ); if ( isset( $request['sitemap_name'] ) ) { /** * Filter the content type used to serve the sitemap XML files. * * @module sitemaps * * @since 3.9.0 * * @param string $xml_content_type By default, it's 'text/xml'. */ $xml_content_type = apply_filters( 'jetpack_sitemap_content_type', 'text/xml' ); // Catch master sitemap xml. if ( 'sitemap.xml' === $request['sitemap_name'] ) { $sitemap_content = $this->librarian->get_sitemap_text( jp_sitemap_filename( JP_MASTER_SITEMAP_TYPE, 0 ), JP_MASTER_SITEMAP_TYPE ); // if there is no master sitemap yet, let's just return an empty sitemap with a short TTL instead of a 404. if ( empty( $sitemap_content ) ) { $builder = new Jetpack_Sitemap_Builder(); $sitemap_content = $builder->empty_sitemap_xml(); } $this->serve_raw_and_die( $xml_content_type, $sitemap_content ); } // Catch sitemap xsl. if ( 'sitemap.xsl' === $request['sitemap_name'] ) { $this->serve_raw_and_die( 'application/xml', Jetpack_Sitemap_Stylist::sitemap_xsl() ); } // Catch sitemap index xsl. if ( 'sitemap-index.xsl' === $request['sitemap_name'] ) { $this->serve_raw_and_die( 'application/xml', Jetpack_Sitemap_Stylist::sitemap_index_xsl() ); } // Catch image sitemap xsl. if ( 'image-sitemap.xsl' === $request['sitemap_name'] ) { $this->serve_raw_and_die( 'application/xml', Jetpack_Sitemap_Stylist::image_sitemap_xsl() ); } // Catch video sitemap xsl. if ( 'video-sitemap.xsl' === $request['sitemap_name'] ) { $this->serve_raw_and_die( 'application/xml', Jetpack_Sitemap_Stylist::video_sitemap_xsl() ); } // Catch news sitemap xml. if ( 'news-sitemap.xml' === $request['sitemap_name'] ) { $sitemap_builder = new Jetpack_Sitemap_Builder(); $this->serve_raw_and_die( $xml_content_type, $sitemap_builder->news_sitemap_xml() ); } // Catch news sitemap xsl. if ( 'news-sitemap.xsl' === $request['sitemap_name'] ) { $this->serve_raw_and_die( 'application/xml', Jetpack_Sitemap_Stylist::news_sitemap_xsl() ); } // Catch sitemap xml. if ( preg_match( $regex['sitemap'], $request['sitemap_name'] ) ) { $this->serve_raw_and_die( $xml_content_type, $this->librarian->get_sitemap_text( $request['sitemap_name'], JP_PAGE_SITEMAP_TYPE ) ); } // Catch sitemap index xml. if ( preg_match( $regex['index'], $request['sitemap_name'] ) ) { $this->serve_raw_and_die( $xml_content_type, $this->librarian->get_sitemap_text( $request['sitemap_name'], JP_PAGE_SITEMAP_INDEX_TYPE ) ); } // Catch image sitemap xml. if ( preg_match( $regex['image'], $request['sitemap_name'] ) ) { $this->serve_raw_and_die( $xml_content_type, $this->librarian->get_sitemap_text( $request['sitemap_name'], JP_IMAGE_SITEMAP_TYPE ) ); } // Catch image sitemap index xml. if ( preg_match( $regex['image-index'], $request['sitemap_name'] ) ) { $this->serve_raw_and_die( $xml_content_type, $this->librarian->get_sitemap_text( $request['sitemap_name'], JP_IMAGE_SITEMAP_INDEX_TYPE ) ); } // Catch video sitemap xml. if ( preg_match( $regex['video'], $request['sitemap_name'] ) ) { $this->serve_raw_and_die( $xml_content_type, $this->librarian->get_sitemap_text( $request['sitemap_name'], JP_VIDEO_SITEMAP_TYPE ) ); } // Catch video sitemap index xml. if ( preg_match( $regex['video-index'], $request['sitemap_name'] ) ) { $this->serve_raw_and_die( $xml_content_type, $this->librarian->get_sitemap_text( $request['sitemap_name'], JP_VIDEO_SITEMAP_INDEX_TYPE ) ); } } } /** * Callback for adding sitemap-interval to the list of schedules. * * @access public * @since 4.8.0 * * @param array $schedules The array of WP_Cron schedules. * * @return array The updated array of WP_Cron schedules. */ public function callback_add_sitemap_schedule( $schedules ) { $schedules['sitemap-interval'] = array( 'interval' => JP_SITEMAP_INTERVAL, 'display' => __( 'Sitemap Interval', 'jetpack' ), ); return $schedules; } /** * Callback handler for sitemap cron hook * * @access public */ public function callback_sitemap_cron_hook() { $sitemap_builder = new Jetpack_Sitemap_Builder(); $sitemap_builder->update_sitemap(); } /** * Add actions to schedule sitemap generation. * Should only be called once, in the constructor. * * @access private * @since 4.8.0 */ private function schedule_sitemap_generation() { // Add cron schedule. add_filter( 'cron_schedules', array( $this, 'callback_add_sitemap_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected add_action( 'jp_sitemap_cron_hook', array( $this, 'callback_sitemap_cron_hook' ) ); if ( ! wp_next_scheduled( 'jp_sitemap_cron_hook' ) ) { /** * Filter the delay in seconds until sitemap generation cron job is started. * * This filter allows a site operator or hosting provider to potentialy spread out sitemap generation for a * lot of sites over time. By default, it will be randomly done over 15 minutes. * * @module sitemaps * @since 6.6.1 * * @param int $delay Time to delay in seconds. */ $delay = apply_filters( 'jetpack_sitemap_generation_delay', MINUTE_IN_SECONDS * wp_rand( 1, 15 ) ); // Randomly space it out to start within next fifteen minutes. wp_schedule_event( time() + $delay, 'sitemap-interval', 'jp_sitemap_cron_hook' ); } } /** * Callback to add sitemap to robots.txt. * * @access public * @since 4.8.0 */ public function callback_action_do_robotstxt() { /** * Filter whether to make the default sitemap discoverable to robots or not. Default true. * * @module sitemaps * @since 3.9.0 * @deprecated 7.4.0 * * @param bool $discover_sitemap Make default sitemap discoverable to robots. */ $discover_sitemap = apply_filters_deprecated( 'jetpack_sitemap_generate', array( true ), 'jetpack-7.4.0', 'jetpack_sitemap_include_in_robotstxt' ); /** * Filter whether to make the default sitemap discoverable to robots or not. Default true. * * @module sitemaps * @since 7.4.0 * * @param bool $discover_sitemap Make default sitemap discoverable to robots. */ $discover_sitemap = apply_filters( 'jetpack_sitemap_include_in_robotstxt', $discover_sitemap ); if ( true === $discover_sitemap ) { $sitemap_url = $this->finder->construct_sitemap_url( 'sitemap.xml' ); echo 'Sitemap: ' . esc_url( $sitemap_url ) . "\n"; } /** * Filter whether to make the news sitemap discoverable to robots or not. Default true. * * @module sitemaps * @since 3.9.0 * @deprecated 7.4.0 * * @param bool $discover_news_sitemap Make default news sitemap discoverable to robots. */ $discover_news_sitemap = apply_filters_deprecated( 'jetpack_news_sitemap_generate', array( true ), 'jetpack-7.4.0', 'jetpack_news_sitemap_include_in_robotstxt' ); /** * Filter whether to make the news sitemap discoverable to robots or not. Default true. * * @module sitemaps * @since 7.4.0 * * @param bool $discover_news_sitemap Make default news sitemap discoverable to robots. */ $discover_news_sitemap = apply_filters( 'jetpack_news_sitemap_include_in_robotstxt', $discover_news_sitemap ); if ( true === $discover_news_sitemap ) { $news_sitemap_url = $this->finder->construct_sitemap_url( 'news-sitemap.xml' ); echo 'Sitemap: ' . esc_url( $news_sitemap_url ) . "\n"; } } /** * Callback to delete the news sitemap cache. * * @access public * @since 4.8.0 */ public function callback_action_flush_news_sitemap_cache() { delete_transient( 'jetpack_news_sitemap_xml' ); } /** * Callback for resetting stored sitemap data. * * @access public * @since 5.3.0 * @since 6.7.0 Schedules a regeneration. */ public function callback_action_purge_data() { $this->callback_action_flush_news_sitemap_cache(); $this->librarian->delete_all_stored_sitemap_data(); /** This filter is documented in modules/sitemaps/sitemaps.php */ $delay = apply_filters( 'jetpack_sitemap_generation_delay', MINUTE_IN_SECONDS * wp_rand( 1, 15 ) ); // Randomly space it out to start within next fifteen minutes. wp_schedule_single_event( time() + $delay, 'jp_sitemap_cron_hook' ); } /** * Callback to set the sitemap location. * * @access public * @since 4.8.0 */ public function callback_action_filter_sitemap_location() { update_option( 'jetpack_sitemap_location', /** * Additional path for sitemap URIs. Default value is empty. * * This string is any additional path fragment you want included between * the home URL and the sitemap filenames. Exactly how this fragment is * interpreted depends on your permalink settings. For example: * * Pretty permalinks: * home_url() . jetpack_sitemap_location . '/sitemap.xml' * * Plain ("ugly") permalinks: * home_url() . jetpack_sitemap_location . '/?jetpack-sitemap=sitemap.xml' * * PATHINFO permalinks: * home_url() . '/index.php' . jetpack_sitemap_location . '/sitemap.xml' * * where 'sitemap.xml' is the name of a specific sitemap file. * The value of this filter must be a valid path fragment per RFC 3986; * in particular it must either be empty or begin with a '/'. * Also take care that any restrictions on sitemap location imposed by * the sitemap protocol are satisfied. * * The result of this filter is stored in an option, 'jetpack_sitemap_location'; * that option is what gets read when the sitemap location is needed. * This way we don't have to wait for init to finish before building sitemaps. * * @link https://tools.ietf.org/html/rfc3986#section-3.3 RFC 3986 * @link https://www.sitemaps.org/ The sitemap protocol * * @since 4.8.0 */ apply_filters( 'jetpack_sitemap_location', '' ) ); } } // End Jetpack_Sitemap_Manager class. new Jetpack_Sitemap_Manager(); /** * Absolute URL of the current blog's sitemap. * * @module sitemaps * * @since 3.9.0 * @since 4.8.1 Code uses method found in Jetpack_Sitemap_Finder::construct_sitemap_url in 4.8.0. * It has been moved here to avoid fatal errors with other plugins that were expecting to find this function. * * @param string $filename Sitemap file name. Defaults to 'sitemap.xml', the initial sitemaps page. * * @return string Sitemap URL. */ function jetpack_sitemap_uri( $filename = 'sitemap.xml' ) { global $wp_rewrite; $location = Jetpack_Options::get_option_and_ensure_autoload( 'jetpack_sitemap_location', '' ); if ( $wp_rewrite->using_index_permalinks() ) { $sitemap_url = home_url( '/index.php' . $location . '/' . $filename ); } elseif ( $wp_rewrite->using_permalinks() ) { $sitemap_url = home_url( $location . '/' . $filename ); } else { $sitemap_url = home_url( $location . '/?jetpack-sitemap=' . $filename ); } /** * Filter sitemap URL relative to home URL. * * @module sitemaps * * @since 3.9.0 * * @param string $sitemap_url Sitemap URL. */ return apply_filters( 'jetpack_sitemap_location', $sitemap_url ); } sitemaps/sitemap-buffer-image.php 0000644 00000005165 15174711637 0013116 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Image * extends the Jetpack_Sitemap_Buffer class to represent the single image sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap image xml files. * * @since 5.3.0 */ class Jetpack_Sitemap_Buffer_Image extends Jetpack_Sitemap_Buffer { /** * Jetpack_Sitemap_Buffer_Image constructor. * * @param int $item_limit The maximum size of the buffer in items. * @param int $byte_limit The maximum size of the buffer in bytes. * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format. */ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) { parent::__construct( $item_limit, $byte_limit, $time ); $this->doc->appendChild( $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" ) ); $this->doc->appendChild( $this->doc->createComment( 'Jetpack_Sitemap_Buffer_Image' ) ); $this->doc->appendChild( $this->doc->createProcessingInstruction( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'image-sitemap.xsl' ) . '"' ) ); } /** * Returns a DOM element that contains all image sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { /** * Filter the XML namespaces included in image sitemaps. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_image_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:image' => 'http://www.google.com/schemas/sitemap-image/1.1', ) ); $this->root = $this->doc->createElement( 'urlset' ); foreach ( $namespaces as $name => $value ) { $this->root->setAttribute( $name, $value ); } $this->doc->appendChild( $this->root ); $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) ); } return $this->root; } } sitemaps/sitemap-buffer-news-xmlwriter.php 0000644 00000004013 15174711637 0015032 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * XMLWriter implementation of the news sitemap buffer. * * @since 14.6 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap news xml files using XMLWriter. * * @since 14.6 */ class Jetpack_Sitemap_Buffer_News_XMLWriter extends Jetpack_Sitemap_Buffer_XMLWriter { /** * Initialize the buffer with required headers (no root element here). */ protected function initialize_buffer() { // Add generator comment $this->writer->writeComment( "generator='jetpack-" . JETPACK__VERSION . "'" ); $this->writer->writeComment( 'Jetpack_Sitemap_Buffer_News_XMLWriter' ); // Add stylesheet $this->writer->writePi( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'news-sitemap.xsl' ) . '"' ); } /** * Start the root element and write its namespaces. */ protected function start_root() { $this->writer->startElement( 'urlset' ); /** * Filter the attribute value pairs used for namespace and namespace URI mappings. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_news_ns', array( 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:news' => 'http://www.google.com/schemas/sitemap-news/0.9', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', ) ); foreach ( $namespaces as $name => $value ) { $this->writer->writeAttribute( $name, $value ); } } /** * Append a URL entry with news information to the sitemap. * * @param array $array The URL item to append. */ protected function append_item( $array ) { if ( ! empty( $array['url'] ) ) { $this->array_to_xml( $array ); } } } sitemaps/sitemap-buffer-master-fallback.php 0000644 00000003014 15174711637 0015053 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Master * extends the Jetpack_Sitemap_Buffer class to represent the master sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing master sitemap xml files for users without libxml support. * * @since 5.3.0 * @phan-suppress PhanRedefinedClassReference -- Don't conflict with real version. */ class Jetpack_Sitemap_Buffer_Master extends Jetpack_Sitemap_Buffer_Fallback { // @phan-suppress-previous-line UnusedSuppression -- It's used. /** * Returns a DOM element that contains all master sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { $sitemap_index_xsl_url = $this->finder->construct_sitemap_url( 'sitemap-index.xsl' ); $jetpack_version = JETPACK__VERSION; $this->root = array( "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL . "<?xml-stylesheet type='text/xsl' href='{$sitemap_index_xsl_url}'?>" . PHP_EOL . "<sitemapindex xmlns='http://www.sitemaps.org/schemas/sitemap/0.9'>" . PHP_EOL, '</sitemapindex>', ); $this->byte_capacity -= strlen( implode( '', $this->root ) ); } return $this->root; } } sitemaps/sitemap-logger.php 0000644 00000005117 15174711637 0012041 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * A message logger for the Jetpack Sitemap module. * * @package automattic/jetpack * @since 4.8.0 */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Handles logging errors and debug messages for sitemap generator. * * A Jetpack_Sitemap_Logger object keeps track of its birth time as well * as a "unique" ID string. Calling the report() method writes a message * to the PHP error log as well as the ID string for easier grepping. * * @since 4.8.0 */ class Jetpack_Sitemap_Logger { /** * A unique-ish string for each logger, enabling us to grep * for the messages written by an individual generation phase. * * @access private * @since 4.8.0 * @var string $key The key string. */ private $key; /** * The birth time of this object in microseconds. * * @access private * @since 4.8.0 * @var int $starttime The birth time. */ private $starttime; /** * Initializes a new logger object. * * @access public * @since 4.8.0 * * @param string $message An optional message string to be written to the debug log on initialization. */ public function __construct( $message = null ) { $this->key = wp_generate_password( 5, false ); $this->starttime = microtime( true ); if ( $message !== null ) { $this->report( $message ); } } /** * Writes a string to the debug log, including the logger's ID string. * * @access public * @since 4.8.0 * * @param string $message The string to be written to the log. * @param boolean $is_error If true, $message will be logged even if JETPACK_DEV_DEBUG is not enabled. */ public function report( $message, $is_error = false ) { $message = 'jp-sitemap-' . $this->key . ': ' . $message; if ( ! ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) { return; } if ( ! $is_error && ! ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) ) { return; } // Append memory usage in MB (human readable) $usage = memory_get_usage( true ); $usage_mb = round( $usage / MB_IN_BYTES, 2 ); $message .= ' [Memory usage: ' . $usage_mb . ' MB]'; error_log( $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } /** * Writes the elapsed lifetime of the logger to the debug log, with an optional message. * * @access public * @since 4.8.0 * * @param string $message The optional message string. Default is the empty string. */ public function time( $message = '' ) { $time = round( microtime( true ) - $this->starttime, 3 ); $this->report( $message . ' ' . $time . ' seconds elapsed.' ); } } sitemaps/sitemap-buffer-news.php 0000644 00000005204 15174711637 0013002 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_News * extends the Jetpack_Sitemap_Buffer class to represent the single news sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap news xml files. * * @since 5.3.0 */ class Jetpack_Sitemap_Buffer_News extends Jetpack_Sitemap_Buffer { /** * Jetpack_Sitemap_Buffer_News constructor. * * @param int $item_limit The maximum size of the buffer in items. * @param int $byte_limit The maximum size of the buffer in bytes. * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format. */ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) { parent::__construct( $item_limit, $byte_limit, $time ); $this->doc->appendChild( $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" ) ); $this->doc->appendChild( $this->doc->createComment( 'Jetpack_Sitemap_Buffer_News' ) ); $this->doc->appendChild( $this->doc->createProcessingInstruction( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'news-sitemap.xsl' ) . '"' ) ); } /** * Returns a DOM element that contains all news sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { /** * Filter the attribute value pairs used for namespace and namespace URI mappings. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_news_ns', array( 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:news' => 'http://www.google.com/schemas/sitemap-news/0.9', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', ) ); $this->root = $this->doc->createElement( 'urlset' ); foreach ( $namespaces as $name => $value ) { $this->root->setAttribute( $name, $value ); } $this->doc->appendChild( $this->root ); $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) ); } return $this->root; } } sitemaps/sitemap-buffer-video.php 0000644 00000005163 15174711637 0013140 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Video * extends the Jetpack_Sitemap_Buffer class to represent the single video sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap video xml files. * * @since 5.3.0 */ class Jetpack_Sitemap_Buffer_Video extends Jetpack_Sitemap_Buffer { /** * Jetpack_Sitemap_Buffer_Video constructor. * * @param int $item_limit The maximum size of the buffer in items. * @param int $byte_limit The maximum size of the buffer in bytes. * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format. */ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) { parent::__construct( $item_limit, $byte_limit, $time ); $this->doc->appendChild( $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" ) ); $this->doc->appendChild( $this->doc->createComment( 'Jetpack_Sitemap_Buffer_Video' ) ); $this->doc->appendChild( $this->doc->createProcessingInstruction( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'video-sitemap.xsl' ) . '"' ) ); } /** * Returns a DOM element that contains all video sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { /** * Filter the XML namespaces included in video sitemaps. * * @module sitemaps * * @since 4.8.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_video_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:video' => 'http://www.google.com/schemas/sitemap-video/1.1', ) ); $this->root = $this->doc->createElement( 'urlset' ); foreach ( $namespaces as $name => $value ) { $this->root->setAttribute( $name, $value ); } $this->doc->appendChild( $this->root ); $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) ); } return $this->root; } } sitemaps/sitemap-buffer-page-xmlwriter.php 0000644 00000003635 15174711637 0015003 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * XMLWriter implementation of the page sitemap buffer. * * @since 14.6 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap page xml files using XMLWriter. * * @since 14.6 */ class Jetpack_Sitemap_Buffer_Page_XMLWriter extends Jetpack_Sitemap_Buffer_XMLWriter { /** * Initialize the buffer with required headers (no root element here). */ protected function initialize_buffer() { // Add generator comment $this->writer->writeComment( "generator='jetpack-" . JETPACK__VERSION . "'" ); $this->writer->writeComment( 'Jetpack_Sitemap_Buffer_Page_XMLWriter' ); // Add stylesheet $this->writer->writePi( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap.xsl' ) . '"' ); } /** * Start the root element and write its namespaces. */ protected function start_root() { $this->writer->startElement( 'urlset' ); /** * Filter the attribute value pairs used for namespace and namespace URI mappings. * * @module sitemaps * * @since 3.9.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', ) ); foreach ( $namespaces as $name => $value ) { $this->writer->writeAttribute( $name, $value ); } } /** * Append a URL entry to the sitemap. * * @param array $array The URL item to append. */ protected function append_item( $array ) { if ( ! empty( $array['url'] ) ) { $this->array_to_xml( $array ); } } } sitemaps/sitemap-state.php 0000644 00000010436 15174711637 0011702 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Abstract sitemap generation state class. * * @package automattic/jetpack * @since 4.8.0 * @author Automattic */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /* Include standard constants and librarian. */ require_once __DIR__ . '/sitemap-constants.php'; require_once __DIR__ . '/sitemap-librarian.php'; if ( defined( 'WP_DEBUG' ) && ( true === WP_DEBUG ) ) { require_once __DIR__ . '/sitemap-logger.php'; } /** * This class provides an interface for storing and retrieving * the state of a sitemap generation phase. Whenever the builder * wants to build a new sitemap page, it uses this class to see * what the current state of the sitemap is. The lock is stored * as a transient with max lifetime of 15 minutes; this way if our * builder times out before unlocking the state, the lock will expire * before the builder tries again. * * @since 4.8.0 */ class Jetpack_Sitemap_State { /** * Initial state for the sitemap generator. * * @access public * @since 4.8.0 * * @param string $type The initial sitemap type. * * @return array $args { * @type string sitemap-type The type of sitemap to be generated. * @type int last-added The largest index to be added to a generated sitemap page. * @type int number The index of the last sitemap to be generated. * @type string last-modified The latest timestamp seen. * @type array max The latest index of each sitemap type seen. * } */ private static function initial( $type = JP_PAGE_SITEMAP_TYPE ) { return array( 'sitemap-type' => $type, 'last-added' => 0, 'number' => 0, 'last-modified' => '1970-01-01 00:00:00', 'max' => array(), ); } /** * Reset the sitemap state. * * @param string $type The initial sitemap type. * * @access public * @since 4.8.0 */ public static function reset( $type ) { delete_transient( 'jetpack-sitemap-state-lock' ); update_option( 'jetpack-sitemap-state', self::initial( $type ) ); } /** * Store a sitemap state, and unlock it. * * @access public * @since 4.8.0 * * @param array $state Array of the Sitemap state details. * @type string sitemap-type The type of sitemap to be generated. * @type int last-added The largest index to be added to a generated sitemap page. * @type int number The index of the last sitemap to be generated. * @type string last-modified The latest timestamp seen. */ public static function check_in( $state ) { // Get the old max value. $sitemap_old = get_option( 'jetpack-sitemap-state', self::initial() ); $state['max'] = $sitemap_old['max']; // Update the max value of the current type. $state['max'][ $state['sitemap-type'] ]['number'] = $state['number']; $state['max'][ $state['sitemap-type'] ]['lastmod'] = $state['last-modified']; update_option( 'jetpack-sitemap-state', $state ); } /** * Unlock the sitemap state. * * @access public * @since 4.8.0 */ public static function unlock() { delete_transient( 'jetpack-sitemap-state-lock' ); } /** * Read the stored sitemap state. Returns false if the state is locked. * * @access public * @since 4.8.0 * * @return bool|array $args { * @type string sitemap-type The type of sitemap to be generated. * @type int last-added The largest index to be added to a generated sitemap page. * @type int number The index of the last sitemap to be generated. * @type string last-modified The latest timestamp seen. * @type array max The latest index of each sitemap type seen. * } */ public static function check_out() { // See if the state is locked. if ( true === get_transient( 'jetpack-sitemap-state-lock' ) ) { // If it is, return false. return false; } else { // Otherwise, lock the state for 15 minutes and then return it. set_transient( 'jetpack-sitemap-state-lock', true, JP_SITEMAP_LOCK_INTERVAL ); return get_option( 'jetpack-sitemap-state', self::initial() ); } } /** * Delete the stored state and lock. * * @access public * @since 4.8.0 */ public static function delete() { delete_transient( 'jetpack-sitemap-state-lock' ); delete_option( 'jetpack-sitemap-state' ); } } sitemaps/sitemap-buffer.php 0000644 00000020261 15174711637 0012030 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer * class abstracts the details of constructing these lists while * maintaining the constraints. * * @since 4.8.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap xml files. * * Models a list of strings such that * * 1. the list must have a bounded number of entries, * 2. the concatenation of the strings must have bounded * length (including some header and footer strings), and * 3. each item has a timestamp, and we need to keep track * of the most recent timestamp of the items in the list. * * @since 4.8.0 */ abstract class Jetpack_Sitemap_Buffer { /** * Largest number of items the buffer can hold. * * @access protected * @since 4.8.0 * @var int $item_capacity The item capacity. */ protected $item_capacity; /** * Largest number of bytes the buffer can hold. * * @access protected * @since 4.8.0 * @var int $byte_capacity The byte capacity. */ protected $byte_capacity; /** * Flag which detects when the buffer is full. * * @access protected * @since 4.8.0 * @var bool $is_full_flag The flag value. This flag is set to false on construction and only flipped to true if we've tried to add something and failed. */ protected $is_full_flag; /** * Flag which detects when the buffer is empty. * * @access protected * @since 4.8.0 * @var bool $is_empty_flag The flag value. This flag is set to true on construction and only flipped to false if we've tried to add something and succeeded. */ protected $is_empty_flag; /** * The most recent timestamp seen by the buffer. * * @access protected * @since 4.8.0 * @var string $timestamp Must be in 'YYYY-MM-DD hh:mm:ss' format. */ protected $timestamp; /** * The DOM document object that is currently being used to construct the XML doc. * * @access protected * @since 5.3.0 * @var DOMDocument $doc */ protected $doc = null; /** * The root DOM element object that holds everything inside. Do not use directly, call * the get_root_element getter method instead. * * @access protected * @since 5.3.0 * @var DOMElement $doc */ protected $root = null; /** * Helper class to construct sitemap paths. * * @since 5.3.0 * @protected * @var Jetpack_Sitemap_Finder */ protected $finder; /** * Construct a new Jetpack_Sitemap_Buffer. * * @since 4.8.0 * * @param int $item_limit The maximum size of the buffer in items. * @param int $byte_limit The maximum size of the buffer in bytes. * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format. */ public function __construct( $item_limit, $byte_limit, $time ) { $this->is_full_flag = false; $this->timestamp = $time; $this->finder = new Jetpack_Sitemap_Finder(); $this->doc = new DOMDocument( '1.0', 'UTF-8' ); $this->doc->formatOutput = true; $this->doc->preserveWhiteSpace = false; $this->item_capacity = max( 1, (int) $item_limit ); $this->byte_capacity = max( 1, (int) $byte_limit ) - strlen( $this->doc->saveXML() ); } /** * Returns a DOM element that contains all sitemap elements. * * @access protected * @since 5.3.0 * @return DOMElement $root */ abstract protected function get_root_element(); /** * Append an item to the buffer, if there is room for it, * and set is_empty_flag to false. If there is no room, * we set is_full_flag to true. If $item is null, * don't do anything and report success. * * @since 5.3.0 * * @param array $array The item to be added. * * @return bool True if the append succeeded, False if not. */ public function append( $array ) { if ( $array === null ) { return true; } if ( $this->is_full_flag ) { return false; } if ( 0 >= $this->item_capacity || 0 >= $this->byte_capacity ) { $this->is_full_flag = true; return false; } else { $this->item_capacity -= 1; $added_element = $this->array_to_xml_string( $array, $this->get_root_element(), $this->doc ); $this->byte_capacity -= strlen( $this->doc->saveXML( $added_element ) ); return true; } } /** * Retrieve the contents of the buffer. * * @since 4.8.0 * * @return string The contents of the buffer (with the footer included). */ public function contents() { if ( $this->is_empty() ) { // The sitemap should have at least the root element added to the DOM. $this->get_root_element(); } return $this->doc->saveXML(); } /** * Retrieve the document object. * * @since 5.3.0 * @return DOMDocument $doc */ public function get_document() { return $this->doc; } /** * Detect whether the buffer is full. * * @since 4.8.0 * * @return bool True if the buffer is full, false otherwise. */ public function is_full() { return $this->is_full_flag; } /** * Detect whether the buffer is empty. * * @since 4.8.0 * * @return bool True if the buffer is empty, false otherwise. */ public function is_empty() { return ( ! isset( $this->root ) || ! $this->root->hasChildNodes() ); } /** * Update the timestamp of the buffer. * * @since 4.8.0 * * @param string $new_time A datetime string in 'YYYY-MM-DD hh:mm:ss' format. */ public function view_time( $new_time ) { $this->timestamp = max( $this->timestamp, $new_time ); } /** * Retrieve the timestamp of the buffer. * * @since 4.8.0 * * @return string A datetime string in 'YYYY-MM-DD hh:mm:ss' format. */ public function last_modified() { return $this->timestamp; } /** * Render an associative array as an XML string. This is needed because * SimpleXMLElement only handles valid XML, but we sometimes want to * pass around (possibly invalid) fragments. Note that 'null' values make * a tag self-closing; this is only sometimes correct (depending on the * version of HTML/XML); see the list of 'void tags'. * * Example: * * array( * 'html' => array( |<html xmlns="foo"> * 'head' => array( | <head> * 'title' => 'Woo!', | <title>Woo!</title> * ), | </head> * 'body' => array( ==> | <body> * 'h2' => 'Some thing', | <h2>Some thing</h2> * 'p' => 'it's all up ons', | <p>it's all up ons</p> * 'br' => null, | <br /> * ), | </body> * ), |</html> * ) * * @access protected * @since 3.9.0 * @since 4.8.0 Rename, add $depth parameter, and change return type. * @since 5.3.0 Refactor, remove $depth parameter, add $parent and $root, make access protected. * * @param array $array A recursive associative array of tag/child relationships. * @param DOMElement $parent (optional) an element to which new children should be added. * @param DOMDocument $root (optional) the parent document. * * @return string|DOMDocument The rendered XML string or an object if root element is specified. */ protected function array_to_xml_string( $array, $parent = null, $root = null ) { $element = null; $return_string = false; if ( null === $parent ) { $return_string = true; $root = new DOMDocument(); $parent = $root; } if ( is_array( $array ) ) { foreach ( $array as $key => $value ) { $element = $root->createElement( $key ); $parent->appendChild( $element ); if ( is_array( $value ) ) { foreach ( $value as $child_key => $child_value ) { $child = $root->createElement( $child_key ); $element->appendChild( $child ); $child->appendChild( self::array_to_xml_string( $child_value, $child, $root ) ); } } else { $element->appendChild( $root->createTextNode( $value ) ); } } } else { $element = $root->createTextNode( $array ); $parent->appendChild( $element ); } if ( $return_string ) { return $root->saveHTML(); } else { return $element; } } } sitemaps/sitemap-librarian.php 0000644 00000032402 15174711637 0012522 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Sitemaps are stored in the database using a custom table. This class * provides a small API for storing and retrieving sitemap data so we can * avoid lots of explicit SQL juggling while building sitemaps. This file * also includes the SQL used to retrieve posts and images to be included * in the sitemaps. * * @since 4.8.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /* Ensure sitemap constants are available. */ require_once __DIR__ . '/sitemap-constants.php'; /** * This object handles any database interaction required * for sitemap generation. * * @since 4.8.0 */ class Jetpack_Sitemap_Librarian { /** * Retrieve a single sitemap with given name and type. * Returns null if no such sitemap exists. * * @access public * @since 4.8.0 * * @param string $name Name of the sitemap to be retrieved. * @param string $type Type of the sitemap to be retrieved. * * @return array $args { * @type int $id ID number of the sitemap in the database. * @type string $timestamp Most recent timestamp of the resources pointed to. * @type string $name Name of the sitemap in the database. * @type string $type Type of the sitemap in the database. * @type string $text The content of the sitemap. * } */ public function read_sitemap_data( $name, $type ) { $post_array = get_posts( array( 'numberposts' => 1, 'title' => $name, 'post_type' => $type, 'post_status' => 'draft', ) ); $the_post = array_shift( $post_array ); if ( null === $the_post ) { return null; } else { return array( 'id' => $the_post->ID, 'timestamp' => $the_post->post_date, 'name' => $the_post->post_title, 'type' => $the_post->post_type, 'text' => base64_decode( $the_post->post_content ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode ); } } /** * Store a sitemap of given type and index in the database. * Note that the timestamp is reencoded as 'Y-m-d H:i:s'. * * If a sitemap with that type and name does not exist, create it. * If a sitemap with that type and name does exist, update it. * * This method uses get_current_sitemap_post_id() for efficiency, * as it only retrieves the post ID, which will be typically cached in the persistent object cache. * This approach avoids loading unnecessary data (like post content) into memory, * unlike using read_sitemap_data() which would retrieve the full post object. * * @access public * @since 4.8.0 * * @param string $index Index of the sitemap to be stored. * @param string $type Type of the sitemap to be stored. * @param string $contents Contents of the sitemap to be stored. * @param string $timestamp Timestamp of the sitemap to be stored, in 'YYYY-MM-DD hh:mm:ss' format. */ public function store_sitemap_data( $index, $type, $contents, $timestamp ) { $name = jp_sitemap_filename( $type, $index ); $post_id = $this->get_current_sitemap_post_id( $name, $type ); if ( null === $post_id ) { // Post does not exist. wp_insert_post( array( 'post_title' => $name, 'post_content' => base64_encode( $contents ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode 'post_type' => $type, 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( $timestamp ) ), ) ); } else { // Post does exist. wp_insert_post( array( 'ID' => $post_id, 'post_title' => $name, 'post_content' => base64_encode( $contents ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode 'post_type' => $type, 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( $timestamp ) ), ) ); } } /** * Get the current sitemap post ID. * * @param string $name The name of the sitemap. * @param string $type The type of the sitemap. * @return int|null The post ID if it exists, null otherwise. */ private function get_current_sitemap_post_id( $name, $type ) { $args = array( 'post_type' => $type, 'post_status' => 'draft', 'posts_per_page' => 1, 'title' => $name, 'fields' => 'ids', ); $query = new WP_Query( $args ); return $query->posts ? $query->posts[0] : null; } /** * Delete a sitemap by name and type. * * @access public * @since 4.8.0 * * @param string $name Row name. * @param string $type Row type. * * @return bool 'true' if a row was deleted, 'false' otherwise. */ public function delete_sitemap_data( $name, $type ) { $the_post = $this->read_sitemap_data( $name, $type ); if ( null === $the_post ) { return false; } else { wp_delete_post( $the_post['id'] ); return true; } } /** * Retrieve the contents of a sitemap with given name and type. * If no such sitemap exists, return the empty string. Note that the * returned string is run through wp_specialchars_decode. * * @access public * @since 4.8.0 * * @param string $name Row name. * @param string $type Row type. * * @return string Text of the specified sitemap, or the empty string. */ public function get_sitemap_text( $name, $type ) { $row = $this->read_sitemap_data( $name, $type ); if ( null === $row ) { return ''; } else { return $row['text']; } } /** * Delete numbered sitemaps named prefix-(p+1), prefix-(p+2), ... * until the first nonexistent sitemap is found. * * @access public * @since 4.8.0 * * @param int $position Number before the first sitemap to be deleted. * @param string $type Sitemap type. */ public function delete_numbered_sitemap_rows_after( $position, $type ) { $any_left = true; while ( true === $any_left ) { ++$position; $name = jp_sitemap_filename( $type, $position ); $any_left = $this->delete_sitemap_data( $name, $type ); } } /** * Deletes all stored sitemap data. * * @access public * @since 4.8.0 */ public function delete_all_stored_sitemap_data() { $this->delete_sitemap_type_data( JP_MASTER_SITEMAP_TYPE ); $this->delete_sitemap_type_data( JP_PAGE_SITEMAP_TYPE ); $this->delete_sitemap_type_data( JP_PAGE_SITEMAP_INDEX_TYPE ); $this->delete_sitemap_type_data( JP_IMAGE_SITEMAP_TYPE ); $this->delete_sitemap_type_data( JP_IMAGE_SITEMAP_INDEX_TYPE ); $this->delete_sitemap_type_data( JP_VIDEO_SITEMAP_TYPE ); $this->delete_sitemap_type_data( JP_VIDEO_SITEMAP_INDEX_TYPE ); } /** * Deletes all sitemap data of specific type * * @access protected * @since 5.3.0 * * @param String $type Type of sitemap. */ protected function delete_sitemap_type_data( $type ) { $ids = get_posts( array( 'post_type' => $type, 'post_status' => 'draft', 'fields' => 'ids', ) ); foreach ( $ids as $id ) { wp_trash_post( $id ); } } /** * Retrieve an array of sitemap rows (of a given type) sorted by ID. * * Returns the smallest $num_posts sitemap rows (measured by ID) * of the given type which are larger than $from_id. * * @access public * @since 4.8.0 * * @param string $type Type of the sitemap rows to retrieve. * @param int $from_id Greatest lower bound of retrieved sitemap post IDs. * @param int $num_posts Largest number of sitemap posts to retrieve. * * @return array The sitemaps, as an array of associative arrays. */ public function query_sitemaps_after_id( $type, $from_id, $num_posts ) { global $wpdb; return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_type=%s AND post_status=%s AND ID>%d ORDER BY ID ASC LIMIT %d;", $type, 'draft', $from_id, $num_posts ), ARRAY_A ); // WPCS: db call ok; no-cache ok. } /** * Retrieve an array of posts sorted by ID. * * More precisely, returns the smallest $num_posts posts * (measured by ID) which are larger than $from_id. * * @access public * @since 4.8.0 * * @param int $from_id Greatest lower bound of retrieved post IDs. * @param int $num_posts Largest number of posts to retrieve. * * @return array The posts. */ public function query_posts_after_id( $from_id, $num_posts ) { global $wpdb; // Get the list of post types to include and prepare for query. $post_types = Jetpack_Options::get_option_and_ensure_autoload( 'jetpack_sitemap_post_types', array( 'page', 'post' ) ); foreach ( (array) $post_types as $i => $post_type ) { $post_types[ $i ] = $wpdb->prepare( '%s', $post_type ); } $post_types_list = implode( ',', $post_types ); $columns_list = $this->get_sanitized_post_columns( $wpdb ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPCS: db call ok; no-cache ok. return $wpdb->get_results( $wpdb->prepare( "SELECT $columns_list FROM $wpdb->posts WHERE post_status='publish' AND post_type IN ($post_types_list) AND ID>%d ORDER BY ID ASC LIMIT %d;", $from_id, $num_posts ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Get the most recent timestamp among approved comments for the given post_id. * * @access public * @since 4.8.0 * * @param int $post_id Post identifier. * * @return string Timestamp in 'Y-m-d h:i:s' format (UTC) of the most recent comment on the given post, or null if no such comments exist. */ public function query_latest_approved_comment_time_on_post( $post_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT MAX(comment_date_gmt) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type in ( '', 'comment' )", $post_id ) ); } /** * Retrieve an array of image posts sorted by ID. * * More precisely, returns the smallest $num_posts image posts * (measured by ID) which are larger than $from_id. * * @access public * @since 4.8.0 * * @param int $from_id Greatest lower bound of retrieved image post IDs. * @param int $num_posts Largest number of image posts to retrieve. * * @return array The posts. */ public function query_images_after_id( $from_id, $num_posts ) { global $wpdb; return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_type='attachment' AND post_mime_type LIKE %s AND ID>%d ORDER BY ID ASC LIMIT %d;", 'image/%', $from_id, $num_posts ) ); // WPCS: db call ok; no-cache ok. } /** * Retrieve an array of video posts sorted by ID. * * More precisely, returns the smallest $num_posts video posts * (measured by ID) which are larger than $from_id. * * @access public * @since 4.8.0 * * @param int $from_id Greatest lower bound of retrieved video post IDs. * @param int $num_posts Largest number of video posts to retrieve. * * @return array The posts. */ public function query_videos_after_id( $from_id, $num_posts ) { global $wpdb; return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_type='attachment' AND post_mime_type LIKE %s AND ID>%d ORDER BY ID ASC LIMIT %d;", 'video/%', $from_id, $num_posts ) ); // WPCS: db call ok; no-cache ok. } /** * Retrieve an array of published posts from the last 2 days. * * @access public * @since 4.8.0 * * @param int $num_posts Largest number of posts to retrieve. * * @return array The posts. */ public function query_most_recent_posts( $num_posts ) { global $wpdb; $two_days_ago = gmdate( 'Y-m-d', strtotime( '-2 days' ) ); /** * Filter post types to be included in news sitemap. * * @module sitemaps * * @since 3.9.0 * * @param array $post_types Array with post types to include in news sitemap. */ $post_types = apply_filters( 'jetpack_sitemap_news_sitemap_post_types', array( 'page', 'post' ) ); foreach ( (array) $post_types as $i => $post_type ) { $post_types[ $i ] = $wpdb->prepare( '%s', $post_type ); } $post_types_list = implode( ',', $post_types ); $columns_list = $this->get_sanitized_post_columns( $wpdb ); // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPCS: db call ok; no-cache ok. return $wpdb->get_results( $wpdb->prepare( "SELECT $columns_list FROM $wpdb->posts WHERE post_status='publish' AND post_date >= '%s' AND post_type IN ($post_types_list) ORDER BY post_date DESC LIMIT %d;", $two_days_ago, $num_posts ) ); // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder,WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Returns all columns from the posts table, * except post_content and post_content_filtered. * * @param object $wpdb The WordPress database object. * @return string The sanitized post columns. */ private function get_sanitized_post_columns( $wpdb ) { $columns = array_filter( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->get_col( "SHOW COLUMNS FROM $wpdb->posts" ), function ( $column ) { return $column !== 'post_content' && $column !== 'post_content_filtered'; } ); return implode( ',', array_map( 'esc_sql', $columns ) ); } } sitemaps/sitemap-buffer-master.php 0000644 00000003715 15174711637 0013326 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Master * extends the Jetpack_Sitemap_Buffer class to represent the master sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing master sitemap xml files. * * @since 5.3.0 */ class Jetpack_Sitemap_Buffer_Master extends Jetpack_Sitemap_Buffer { /** * Jetpack_Sitemap_Buffer_Master constructor. * * @param int $item_limit The maximum size of the buffer in items. * @param int $byte_limit The maximum size of the buffer in bytes. * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format. */ public function __construct( $item_limit, $byte_limit, $time = '1970-01-01 00:00:00' ) { parent::__construct( $item_limit, $byte_limit, $time ); $this->doc->appendChild( $this->doc->createComment( "generator='jetpack-" . JETPACK__VERSION . "'" ) ); $this->doc->appendChild( $this->doc->createComment( 'Jetpack_Sitemap_Buffer_Master' ) ); $this->doc->appendChild( $this->doc->createProcessingInstruction( 'xml-stylesheet', 'type="text/xsl" href="' . $this->finder->construct_sitemap_url( 'sitemap-index.xsl' ) . '"' ) ); } /** * Returns a DOM element that contains all master sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { $this->root = $this->doc->createElement( 'sitemapindex' ); $this->root->setAttribute( 'xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9' ); $this->doc->appendChild( $this->root ); $this->byte_capacity -= strlen( $this->doc->saveXML( $this->root ) ); } return $this->root; } } sitemaps/sitemap-buffer-page-fallback.php 0000644 00000004104 15174711637 0014475 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName // phpcs:disable Generic.Classes.DuplicateClassName.Found -- sitemap-builder.php will require correct class file. /** * Sitemaps (per the protocol) are essentially lists of XML fragments; * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer_Page * extends the Jetpack_Sitemap_Buffer class to represent the single page sitemap * buffer. * * @since 5.3.0 * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * A buffer for constructing sitemap page xml files for users with no libxml support. * * @since 5.3.0 * @phan-suppress PhanRedefinedClassReference -- Don't conflict with real version. */ class Jetpack_Sitemap_Buffer_Page extends Jetpack_Sitemap_Buffer_Fallback { // @phan-suppress-previous-line UnusedSuppression -- It's used. /** * Returns a DOM element that contains all single page sitemap elements. */ protected function get_root_element() { if ( ! isset( $this->root ) ) { /** * Filter the attribute value pairs used for namespace and namespace URI mappings. * * @module sitemaps * * @since 3.9.0 * * @param array $namespaces Associative array with namespaces and namespace URIs. */ $namespaces = apply_filters( 'jetpack_sitemap_ns', array( 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd', 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', ) ); $jetpack_version = JETPACK__VERSION; $sitemap_xsl_url = $this->finder->construct_sitemap_url( 'sitemap.xsl' ); $this->root = array( "<!-- generator='jetpack-{$jetpack_version}' -->" . PHP_EOL . "<?xml-stylesheet type='text/xsl' href='{$sitemap_xsl_url}'?>" . PHP_EOL . '<urlset ' . $this->array_to_xml_attr_string( $namespaces ) . '>', '</urlset>', ); $this->byte_capacity -= strlen( implode( '', $this->root ) ); } return $this->root; } } stats/class-jetpack-stats-upgrade-nudges.php 0000644 00000044475 15174711637 0015235 0 ustar 00 <?php /** * Adds a section with Upgrade nudges to the Status Report page * * @package jetpack */ use Automattic\Jetpack\Connection\Manager; use Automattic\Jetpack\Current_Plan as Jetpack_Plan; use Automattic\Jetpack\Jetpack_CRM_Data; use Automattic\Jetpack\Plugins_Installer; use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Tracking; /** * Class that adds a new section to the Stats Report page */ class Jetpack_Stats_Upgrade_Nudges { /** * Indicates whether the class initialized or not * * @var bool */ private static $initialized = false; /** * Initialize the class by registering the action * * @return void */ public static function init() { if ( ! self::$initialized ) { self::$initialized = true; add_action( 'jetpack_admin_pages_wrap_ui_after_callback', array( __CLASS__, 'render' ) ); } } /** * Unsets the collapse nudges setting. */ public static function unset_nudges_setting() { $stats_options = get_option( 'stats_options' ); if ( $stats_options ) { unset( $stats_options['collapse_nudges'] ); update_option( 'stats_options', $stats_options ); } } /** * Determines whether Backup is active * * @return boolean */ private static function is_backup_active() { $rewind_data = Jetpack_Core_Json_Api_Endpoints::rewind_data(); return is_object( $rewind_data ) && isset( $rewind_data->state ) && 'unavailable' !== $rewind_data->state; } /** * Checks if a plugin is installed. * * @param string $plugin_file The plugin filename. * @return boolean */ private static function is_plugin_installed( $plugin_file ) { $plugins = Plugins_Installer::get_plugins(); return isset( $plugins[ $plugin_file ] ); } /** * Checks if a plugin is active. * * @param string $plugin_file The plugin filename. * @return boolean */ private static function is_plugin_active( $plugin_file ) { $plugins = Plugins_Installer::get_plugins(); return isset( $plugins[ $plugin_file ] ) && isset( $plugins[ $plugin_file ]['active'] ) && $plugins[ $plugin_file ]['active']; } /** * Determines whether Scan is active * * @return boolean */ private static function is_scan_active() { $scan_data = Jetpack_Core_Json_Api_Endpoints::scan_state(); return is_object( $scan_data ) && isset( $scan_data->state ) && 'unavailable' !== $scan_data->state; } /** * Determines whether Search module is active * * @return boolean */ private static function is_search_active() { return Jetpack::is_module_active( 'search' ); } /** * Determines whether the Site is on a Security Plan. It will also return true if site has backup, scan and akismet. * * @return boolean */ private static function has_security_plan() { $plan_data = Jetpack_Plan::get(); if ( is_array( $plan_data ) && isset( $plan_data['product_slug'] ) ) { $has_plan = wp_startswith( $plan_data['product_slug'], 'jetpack_security' ); return ( $has_plan || ( self::is_backup_active() && self::is_scan_active() && self::is_akismet_active() ) ); } return false; } /** * Determines whether the Site is on the Complete Plan. * * @return boolean */ private static function has_complete_plan() { $plan_data = Jetpack_Plan::get(); if ( is_array( $plan_data ) && isset( $plan_data['product_slug'] ) ) { return wp_startswith( $plan_data['product_slug'], 'jetpack_complete' ); } return false; } /** * Determines whether the Site has a specific product. * * @param string $target_product_slug The product slug we are looking for. * * @return boolean */ private static function has_product( $target_product_slug ) { $site_products_slugs = array_column( Jetpack_Plan::get_products(), 'product_slug' ); foreach ( $site_products_slugs as $product_slug ) { if ( wp_startswith( $product_slug, $target_product_slug ) ) { return true; } } return false; } /** * Determines whether Akismet is active * * @return boolean */ private static function is_akismet_active() { return Jetpack::is_akismet_active(); } /** * Outputs the header of the Upgrade Secion * * @return void */ private static function print_header() { if ( self::has_security_plan() ) { // translators: %s is the Site Name. $title = __( 'Performance and growth tools for %s', 'jetpack' ); } else { // translators: %s is the Site Name. $title = __( 'Security, performance, and growth tools for %s', 'jetpack' ); } $title = sprintf( $title, get_bloginfo( 'site_name' ) ); $aria_expanded = 'true'; $postbox_closed = ''; $stats_options = get_option( 'stats_options' ); if ( isset( $stats_options['collapse_nudges'] ) && $stats_options['collapse_nudges'] ) { $aria_expanded = 'false'; $postbox_closed = ' closed'; } ?> <div id="jp-stats-report-upgrade-wrap"> <div class="meta-box-sortables"> <div class="postbox<?php echo esc_attr( $postbox_closed ); ?>"> <div class="dops-card dops-section-header is-compact jp-stats-report-upgrade-header"> <div class="dops-section-header__label"> <span class="dops-section-header__label-text"> <?php echo esc_html( $title ); ?> </span> </div> <div class="dops-section-header__actions handle-actions hide-if-no-js"> <button type="button" id="stats_nudges_toggle" class="handlediv" aria-expanded="<?php echo esc_attr( $aria_expanded ); ?>"> <span class="screen-reader-text">Toggle Upgrade Nudges</span> <span class="toggle-indicator" aria-hidden="true"></span> </button> </div> </div> <div class="inside"> <?php } /** * Outputs the footer of the Upgrade Section * * @return void */ private static function print_footer() { ?> </div> </div> </div> </div> <?php } /** * Outputs the custom css rules of the Upgrade Section * * @return void */ private static function print_styles() { ?> <style> .dops-section-header.dops-card.jp-stats-report-upgrade-header { font-weight: bold; box-shadow: none; flex-wrap: nowrap; } #jp-stats-report-upgrade-wrap .dops-section-header__label-text { white-space: normal; } #stats_nudges_toggle { height: 100%; } .dops-banner.dops-card.is-product.jp-stats-report-upgrade-item { margin-bottom: 0; border-left: 0; box-shadow: none; border-top: 1px solid #c3c4c7; padding: 12px 24px; } .dops-banner.dops-card.jp-stats-report-upgrade-item.jp-stats-report-upgrade-subitem { margin-left: 72px; padding-left: 0; } .jp-stats-report-upgrade-item .dops-banner__action { margin-right: 0; } #jp-stats-report-upgrade-wrap .dops-card::after { content: ""; } .jp-stats-report-upgrade-item .dops-banner__title p { margin: 5px 0 0 0; font-weight: normal; } #jp-stats-report-upgrade-wrap .postbox { background-color: white; border: 1px solid #c3c4c7; margin-bottom: 0; } #jp-stats-report-upgrade-wrap .postbox .inside { padding: 0; } </style> <?php } /** * Add a script which handles collapse/expansion of the nudge UI. */ private static function print_script() { ?> <script> (function(window, document, undefined){ window.onload = set_up_click_handler; const stats_wrap = document.getElementById( 'jp-stats-wrap' ); stats_wrap.addEventListener( 'stats-loaded', function () { const stat_chart = document.getElementById( 'statchart' ); if ( stat_chart === null ) { document.getElementById( 'stats_nudges_toggle' ).style.display = 'none'; } }); function set_up_click_handler(){ document.getElementById( 'stats_nudges_toggle' ).onclick = function () { const collapseValue = 'true' === this.getAttribute( 'aria-expanded' ); fetch( '/wp-json/jetpack/v4/settings', { method: 'post', body: JSON.stringify( { collapse_nudges: collapseValue } ), headers: { 'X-WP-Nonce': <?php echo wp_json_encode( wp_create_nonce( 'wp_rest' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>, 'Content-type': 'application/json' } } ); }; } })(window, document, undefined); </script> <?php } /** * Gets the upgrade Redirect link * * @param string $source The source of the redirect link. * @return string */ private static function get_upgrade_link( $source ) { $args = array(); if ( ! ( new Manager( 'jetpack' ) )->has_connected_owner() ) { $args['query'] = 'unlinked=1'; } return Redirect::get_url( $source, $args ); } /** * Gets the product description link. * * @param string $product_key The product key of the product we wish to display. * @return string */ private static function get_product_description_link( $product_key ) { return Jetpack::admin_url( array( 'page' => sprintf( 'jetpack#/product/%s', $product_key ) ) ); } /** * Tracks an event in Tracks * * @param string $event_name The event name. * @return void */ private static function track_event( $event_name ) { $connection_manager = new Manager( 'jetpack' ); $tracking = new Tracking( 'jetpack', $connection_manager ); $tracking->record_user_event( $event_name, array( 'has_connected_owner' => $connection_manager->has_connected_owner(), ) ); } /** * Outputs one Upgrade item * * @param string $title The title of the item. * @param string $text The description of the item. * @param string $icon The path of the icon, relative to Jetpack images folder. * @param string $link The link of the button. * @param string $tracks_id The id used to identify the tracks events. Automatically prefixed with "jetpack_stats_nudges_{view|click|learn_more}_". * @param string $learn_more_link The target of the "Learn more" link. * @param boolean $subitem Whether it is a subitem or not. * @param string $button_label The button label. * @return void */ private static function print_item( $title, $text, $icon, $link, $tracks_id, $learn_more_link, $subitem = false, $button_label = null ) { $additional_classes = $subitem ? 'jp-stats-report-upgrade-subitem' : ''; $button_class = $subitem ? 'is-secondary' : 'is-primary'; $icon_url = plugins_url( '', JETPACK__PLUGIN_FILE ) . '/images/products/' . $icon; $button_label = $button_label === null ? _x( 'Upgrade', 'Call to action to buy a new plan', 'jetpack' ) : $button_label; $view_event = "stats_nudges_view_$tracks_id"; $click_event = "stats_nudges_click_$tracks_id"; $learn_more_event = "stats_nudges_learn_more_$tracks_id"; self::track_event( $view_event ); ?> <div class="dops-card dops-banner has-call-to-action is-product jp-stats-report-upgrade-item <?php echo esc_attr( $additional_classes ); ?>"> <div class="dops-banner__icon-plan"> <img src="<?php echo esc_attr( $icon_url ); ?>" alt="" width="32" height="32"> </div> <div class="dops-banner__content"> <div class="dops-banner__info"> <div class="dops-banner__title"> <?php echo esc_html( $title ); ?> <p> <?php echo esc_html( $text ); ?> <a href="<?php echo esc_attr( $learn_more_link ); ?>" class="jptracks" target="_blank" rel="noopener noreferrer" data-jptracks-name="<?php echo esc_attr( $learn_more_event ); ?>"> <?php esc_html_e( 'Learn more', 'jetpack' ); ?> </a> </p> </div> </div> <div class="dops-banner__action"> <a href="<?php echo esc_attr( $link ); ?>" type="button" class="jptracks dops-button is-compact <?php echo esc_attr( $button_class ); ?>" data-jptracks-name="<?php echo esc_attr( $click_event ); ?>"> <?php echo esc_html( $button_label ); ?> </a> </div> </div> </div> <?php } /** * Prints the Security item * * @return void */ private static function print_security() { $link = self::get_product_description_link( 'security' ); $learn_link = self::get_upgrade_link( 'stats-nudges-security-learn' ); $text = __( 'Comprehensive protection for your site, including Backup, Scan, and Anti-spam.', 'jetpack' ); self::print_item( _x( 'Security', 'Jetpack product name', 'jetpack' ), $text, 'product-jetpack.svg', $link, 'security', $learn_link ); } /** * Prints the Backup item * * @return void */ private static function print_backup() { $link = self::get_product_description_link( 'backup' ); $learn_link = self::get_upgrade_link( 'stats-nudges-backup-learn' ); $text = __( 'Save every change and get back online quickly with one-click restores.', 'jetpack' ); self::print_item( __( 'VaultPress Backup', 'jetpack' ), $text, 'product-jetpack-backup.svg', $link, 'backup', $learn_link, true ); } /** * Prints the Scan item * * @since 10.1 * @since 10.3 The scan nudge has been removed, but leaving this here in case we reverse course. * * @todo Remove this function is not used ~6 months. * * @return void */ private static function print_scan() { $link = self::get_product_description_link( 'scan' ); $learn_link = self::get_upgrade_link( 'stats-nudges-scan-learn' ); $text = __( 'Stay ahead of security threats with automated scanning and one-click fixes.', 'jetpack' ); self::print_item( __( 'Scan', 'jetpack' ), $text, 'product-jetpack-scan.svg', $link, 'scan', $learn_link, true ); } /** * Prints the Akismet item * * @since 10.1 * @since 10.3 The anti-spam nudge has been removed, but leaving this here in case we reverse course. * * @todo Remove this function is not used ~6 months. * * @return void */ private static function print_akismet() { $link = self::get_product_description_link( 'akismet' ); $learn_link = self::get_upgrade_link( 'stats-nudges-akismet-learn' ); $text = __( 'Automatically clear spam from comments and forms.', 'jetpack' ); self::print_item( __( 'Akismet Anti-spam', 'jetpack' ), $text, 'product-jetpack-anti-spam.svg', $link, 'akismet', $learn_link, true ); } /** * Prints the Search item * * @return void */ private static function print_search() { $link = self::get_product_description_link( 'search' ); $learn_link = self::get_upgrade_link( 'stats-nudges-search-learn' ); $text = __( 'Help your site visitors instantly find what they\'re looking for so they read and buy more.', 'jetpack' ); self::print_item( __( 'Search', 'jetpack' ), $text, 'product-jetpack-search.svg', $link, 'search', $learn_link ); } /** * Prints the VideoPress item * * @return void */ private static function print_videopress() { $link = self::get_product_description_link( 'videopress' ); $learn_link = self::get_upgrade_link( 'stats-nudges-videopress-learn' ); $text = __( 'Engage your visitors with high-quality, ad-free videos built specifically for WordPress.', 'jetpack' ); self::print_item( __( 'VideoPress', 'jetpack' ), $text, 'product-jetpack-videopress.svg', $link, 'videopress', $learn_link ); } /** * Prints the Boost item * * @param bool $print Whether to print the item output or just check whether it would be printed or not. * * @return bool */ private static function get_boost_output( $print = true ) { $plugin_file = 'jetpack-boost/jetpack-boost.php'; $plugin_slug = 'jetpack-boost'; if ( self::is_plugin_active( $plugin_file ) ) { return false; } elseif ( self::is_plugin_installed( $plugin_file ) ) { $label = __( 'Activate Boost', 'jetpack' ); $link = wp_nonce_url( 'plugins.php?action=activate&plugin=' . rawurlencode( $plugin_file ) . '&plugin_status=all&paged=1', 'activate-plugin_' . $plugin_file ); } else { $label = __( 'Install Boost', 'jetpack' ); $link = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug ), 'install-plugin_' . $plugin_slug ); } if ( $print ) { $learn_link = self::get_upgrade_link( 'stats-nudges-boost-learn' ); $text = __( 'Improve your site\'s performance and SEO in a few clicks with the free Jetpack Boost plugin.', 'jetpack' ); self::print_item( __( 'Boost', 'jetpack' ), $text, 'product-jetpack-boost.svg', $link, 'boost', $learn_link, false, $label ); } return true; } /** * Prints the CRM item * * @param bool $print Whether to print the item output or just check whether it would be printed or not. * * @since 10.1 * * @return bool */ private static function get_crm_output( $print = true ) { $plugin_file = Jetpack_CRM_Data::JETPACK_CRM_PLUGIN_SLUG; $plugin_slug = substr( $plugin_file, 0, strpos( $plugin_file, '/' ) ); if ( self::is_plugin_active( $plugin_file ) ) { return false; } elseif ( self::is_plugin_installed( $plugin_file ) ) { $link = wp_nonce_url( 'plugins.php?action=activate&plugin=' . rawurlencode( $plugin_file ) . '&plugin_status=all&paged=1', 'activate-plugin_' . $plugin_file ); $label = __( 'Activate CRM', 'jetpack' ); } else { $link = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug ), 'install-plugin_' . $plugin_slug ); $label = __( 'Install CRM', 'jetpack' ); } if ( $print ) { $learn_link = self::get_upgrade_link( 'stats-nudges-crm-learn' ); $text = __( 'Sell more and get more leads with the free Jetpack CRM plugin built specifically for WordPress.', 'jetpack' ); self::print_item( __( 'CRM', 'jetpack' ), $text, 'product-jetpack-crm.svg', $link, 'crm', $learn_link, false, $label ); } return true; } /** * Outputs the section to the Stats Report page * * @param string $callback The callback passed to the jetpack admin page. * @return void */ public static function render( $callback ) { /** This filter is documented in _inc/lib/admin-pages/class.jetpack-react-page.php */ if ( 'stats_reports_page' !== $callback || ! apply_filters( 'jetpack_show_promotions', true ) || ! current_user_can( 'manage_options' ) ) { return; } if ( self::has_complete_plan() ) { return; } if ( self::has_security_plan() && self::is_backup_active() && self::is_search_active() && ! self::get_boost_output( false ) && ! self::get_crm_output( false ) ) { return; } self::print_styles(); self::print_script(); self::print_header(); if ( ! self::has_security_plan() ) { self::print_security(); if ( ! self::is_backup_active() ) { self::print_backup(); } } if ( ! self::is_search_active() ) { self::print_search(); } if ( ! self::has_product( 'jetpack_videopress' ) ) { self::print_videopress(); } self::get_boost_output(); self::get_crm_output(); self::print_footer(); } } blocks.php 0000644 00000003510 15174711637 0006545 0 ustar 00 <?php /** * Module Name: Blocks * Module Description: Expand your editor with custom Jetpack blocks for rich content and layout options. * Sort Order: 5 * First Introduced: 13.9-a.8 * Requires Connection: No * Auto Activate: Yes * Module Tags: blocks * Feature: Writing * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } add_action( 'jetpack_activate_module_blocks', 'jetpack_blocks_activate_module' ); /** * Actions needed upon activating the blocks module. * * There is a legacy option to disable Jetpack blocks that we'll delete when this module is activated. * Via jetpack_get_default_modules filter, we remove blocks from the default if the option is true. * We'll leave that in place so _until the module is activated_ we will be sure to respect the previous * setting. * * @since 13.9 * @return void */ function jetpack_blocks_activate_module() { delete_option( 'jetpack_blocks_disabled' ); // The function will check and return early if not present. } Jetpack_Gutenberg::load_block_editor_extensions(); Jetpack_Gutenberg::load_independent_blocks(); Jetpack_Gutenberg::register_block_metadata_collection(); /** * We've switched from enqueue_block_editor_assets to enqueue_block_assets in WP-Admin because the assets with the former are loaded on the main site-editor.php. * * With the latter, the assets are now loaded in the SE iframe; the implementation is now faster because Gutenberg doesn't need to inject the assets in the iframe on client-side. */ if ( is_admin() ) { add_action( 'enqueue_block_assets', array( 'Jetpack_Gutenberg', 'enqueue_block_editor_assets' ) ); } else { add_action( 'enqueue_block_editor_assets', array( 'Jetpack_Gutenberg', 'enqueue_block_editor_assets' ) ); } add_filter( 'render_block', array( 'Jetpack_Gutenberg', 'display_deprecated_block_message' ), 10, 2 ); tiled-gallery.php 0000644 00000002166 15174711637 0010034 0 ustar 00 <?php /** * Module Name: Tiled Galleries * Module Description: Create visually engaging tiled image galleries with multiple layout options. * First Introduced: 2.1 * Requires Connection: No * Auto Activate: No * Module Tags: Photos and Videos * Feature: Appearance * Sort Order: 24 * Additional Search Queries: gallery, tiles, tiled, grid, mosaic, images * * @package jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Include the tiled gallery for loading. */ function jetpack_load_tiled_gallery() { include __DIR__ . '/tiled-gallery/tiled-gallery.php'; } add_action( 'jetpack_modules_loaded', 'jetpack_tiled_gallery_loaded' ); /** * Enable the tiled gallery module. */ function jetpack_tiled_gallery_loaded() { Jetpack::enable_module_configurable( __FILE__ ); add_filter( 'jetpack_module_configuration_url_tiled-gallery', 'jetpack_tiled_gallery_configuration_url' ); } /** * Overrides default configuration url * * @uses admin_url * @return string module settings URL */ function jetpack_tiled_gallery_configuration_url() { return admin_url( 'options-media.php' ); } jetpack_load_tiled_gallery(); wpgroho.js 0000644 00000003671 15174711637 0006612 0 ustar 00 /* global WPGroHo:true, Gravatar */ ( function () { var extend = function ( out ) { out = out || {}; for ( var i = 1; i < arguments.length; i++ ) { if ( ! arguments[ i ] ) { continue; } for ( var key in arguments[ i ] ) { if ( Object.hasOwn( arguments[ i ], key ) ) { out[ key ] = arguments[ i ][ key ]; } } } return out; }; WPGroHo = extend( { my_hash: '', data: {}, renderers: {}, syncProfileData: function ( hash, id ) { var hashElements; if ( ! WPGroHo.data[ hash ] ) { WPGroHo.data[ hash ] = {}; hashElements = document.querySelectorAll( 'div.grofile-hash-map-' + hash + ' span' ); for ( var i = 0; i < hashElements.length; i++ ) { WPGroHo.data[ hash ][ hashElements[ i ].className ] = hashElements[ i ].innerText; } } WPGroHo.appendProfileData( WPGroHo.data[ hash ], hash, id ); }, appendProfileData: function ( data, hash, id ) { for ( var key in data ) { if ( 'function' === typeof WPGroHo.renderers[ key ] ) { return WPGroHo.renderers[ key ]( data[ key ], hash, id, key ); } var card = document.getElementById( id ); if ( card ) { var heading = card.querySelector( 'h4' ); if ( heading ) { var extra = document.createElement( 'p' ); extra.className = 'grav-extra ' + key; extra.innerHTML = data[ key ]; heading.insertAdjacentElement( 'afterend', extra ); } } } }, }, WPGroHo || {} ); var jetpackHovercardsInit = function () { if ( 'undefined' === typeof Gravatar ) { return; } Gravatar.profile_cb = function ( h, d ) { WPGroHo.syncProfileData( h, d ); }; Gravatar.my_hash = WPGroHo.my_hash; Gravatar.init( 'body', '#wpadminbar' ); }; if ( document.readyState === 'interactive' || document.readyState === 'complete' ) { jetpackHovercardsInit(); } else { document.addEventListener( 'DOMContentLoaded', jetpackHovercardsInit ); } } )(); woocommerce-analytics.php 0000644 00000001640 15174711637 0011576 0 ustar 00 <?php /** * Module Name: WooCommerce Analytics * Module Description: Get actionable insights on your store’s orders, revenue, and customers. * Sort Order: 13 * First Introduced: 8.4 * Requires Connection: Yes * Auto Activate: Yes * Module Tags: Other, Recommended * Feature: Engagement * Additional Search Queries: woocommerce, analytics, stats, statistics, tracking, analytics, views * * @package automattic/jetpack */ use Automattic\Woocommerce_Analytics; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Load module functionality from the package only if * WC_ANALYTICS constant is not defined by WooCommerce * * When WC_ANALYTICS constant is defined it means WooCommerce_Analytics package is being * loaded by WooCommerce core instead of Jetpack. * * We maintain for now the initialization here for compatibility reasons. */ if ( ! defined( 'WC_ANALYTICS' ) ) { Woocommerce_Analytics::init(); } videopress.php 0000644 00000001735 15174711637 0007462 0 ustar 00 <?php /** * Module Name: VideoPress * Module Description: Powerful and flexible video hosting. * First Introduced: 2.5 * Requires Connection: Yes * Sort Order: 27 * Module Tags: Photos and Videos * Feature: Writing * Additional Search Queries: video, videos, videopress, video gallery, video player, videoplayer, mobile video, vimeo, youtube, html5 video, stream * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Require the VideoPress files. */ require_once __DIR__ . '/videopress/shortcode.php'; require_once __DIR__ . '/videopress/class.videopress-scheduler.php'; require_once __DIR__ . '/videopress/class.videopress-cli.php'; require_once __DIR__ . '/videopress/class.jetpack-videopress.php'; require_once __DIR__ . '/videopress/class-videopress-attachment-metadata.php'; if ( is_admin() ) { include_once __DIR__ . '/videopress/editor-media-view.php'; include_once __DIR__ . '/videopress/class.videopress-edit-attachment.php'; } sharedaddy/services/class-jetpack-mastodon-modal.php 0000644 00000026603 15174711637 0016673 0 ustar 00 <?php /** * Display a Mastodon modal, where folks can enter a Mastodon instance. * * @package automattic/jetpack */ /** * Class Mastodon_Modal */ class Jetpack_Mastodon_Modal { /** * Mastodon SVG icon. * * @var string */ private static $svg = '<svg width="261" height="66" viewBox="0 0 261 66" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <path d="M60.7539 14.4034C59.8143 7.41942 53.7273 1.91557 46.5117 0.849066C45.2943 0.668854 40.6819 0.0130005 29.9973 0.0130005H29.9175C19.2299 0.0130005 16.937 0.668854 15.7196 0.849066C8.70488 1.88602 2.29885 6.83152 0.744617 13.8982C-0.00294988 17.3784 -0.0827298 21.2367 0.0561464 24.7759C0.254119 29.8514 0.292531 34.918 0.753482 39.9728C1.07215 43.3305 1.62806 46.6614 2.41704 49.9406C3.89445 55.9969 9.87499 61.0369 15.7344 63.0931C22.0077 65.2374 28.7542 65.5934 35.2184 64.1212C35.9295 63.9558 36.6318 63.7638 37.3252 63.5451C38.8971 63.0459 40.738 62.4875 42.0913 61.5067C42.1099 61.4929 42.1251 61.4751 42.1358 61.4547C42.1466 61.4342 42.1526 61.4116 42.1534 61.3885V56.4903C42.153 56.4687 42.1479 56.4475 42.1383 56.4281C42.1287 56.4088 42.1149 56.3918 42.0979 56.3785C42.0809 56.3652 42.0611 56.3559 42.04 56.3512C42.019 56.3465 41.9971 56.3466 41.9761 56.3514C37.8345 57.3406 33.5905 57.8364 29.3324 57.8286C22.0045 57.8286 20.0336 54.3514 19.4693 52.9038C19.0156 51.6527 18.7275 50.3476 18.6124 49.0218C18.6112 48.9996 18.6153 48.9773 18.6243 48.9569C18.6333 48.9366 18.647 48.9186 18.6643 48.9045C18.6816 48.8904 18.7019 48.8805 18.7237 48.8758C18.7455 48.871 18.7681 48.8715 18.7897 48.8771C22.8622 49.8595 27.037 50.3553 31.2265 50.3542C32.234 50.3542 33.2387 50.3542 34.2463 50.3276C38.4598 50.2094 42.9009 49.9938 47.0465 49.1843C47.1499 49.1636 47.2534 49.1459 47.342 49.1193C53.881 47.8637 60.1038 43.9227 60.7362 33.9431C60.7598 33.5502 60.8189 29.8278 60.8189 29.4201C60.8218 28.0345 61.2651 19.5911 60.7539 14.4034Z" fill="url(#paint0_linear_89_11)"></path> <path d="M12.3442 18.3034C12.3442 16.2668 13.9777 14.6194 15.997 14.6194C18.0163 14.6194 19.6497 16.2668 19.6497 18.3034C19.6497 20.34 18.0163 21.9874 15.997 21.9874C13.9777 21.9874 12.3442 20.34 12.3442 18.3034Z" fill="currentColor"></path> <path d="M66.1484 21.4685V38.3839H59.4988V21.9744C59.4988 18.5109 58.0583 16.7597 55.1643 16.7597C51.9746 16.7597 50.3668 18.8482 50.3668 22.9603V31.9499H43.7687V22.9603C43.7687 18.8352 42.1738 16.7597 38.9712 16.7597C36.0901 16.7597 34.6367 18.5109 34.6367 21.9744V38.3839H28V21.4685C28 18.018 28.8746 15.268 30.6238 13.2314C32.4374 11.1948 34.8039 10.157 37.7365 10.157C41.132 10.157 43.7172 11.4802 45.415 14.1135L47.0742 16.9154L48.7334 14.1135C50.4311 11.4802 53.0035 10.157 56.4119 10.157C59.3444 10.157 61.711 11.1948 63.5246 13.2314C65.2738 15.268 66.1484 18.005 66.1484 21.4685ZM89.0297 29.8743C90.4059 28.4085 91.0619 26.5795 91.0619 24.3613C91.0619 22.1431 90.4059 20.3011 89.0297 18.9001C87.7049 17.4343 86.0329 16.7338 84.0007 16.7338C81.9685 16.7338 80.2965 17.4343 78.9717 18.9001C77.6469 20.3011 76.991 22.1431 76.991 24.3613C76.991 26.5795 77.6469 28.4215 78.9717 29.8743C80.2965 31.2753 81.9685 31.9888 84.0007 31.9888C86.0329 31.9888 87.7049 31.2883 89.0297 29.8743ZM91.0619 10.8316H97.6086V37.891H91.0619V34.6999C89.0811 37.3462 86.3416 38.6563 82.7788 38.6563C79.2161 38.6563 76.4765 37.3073 74.0456 34.5442C71.6533 31.7812 70.4443 28.3696 70.4443 24.3743C70.4443 20.3789 71.6661 17.0192 74.0456 14.2561C76.4893 11.4931 79.3833 10.0922 82.7788 10.0922C86.1744 10.0922 89.0811 11.3894 91.0619 14.0356V10.8445V10.8316ZM119.654 23.8683C121.583 25.3342 122.548 27.3837 122.496 29.9781C122.496 32.7411 121.532 34.9075 119.551 36.4122C117.57 37.878 115.178 38.6304 112.284 38.6304C107.049 38.6304 103.499 36.4641 101.621 32.1963L107.306 28.7847C108.065 31.1067 109.737 32.3001 112.284 32.3001C114.625 32.3001 115.782 31.5477 115.782 29.9781C115.782 28.8366 114.265 27.8118 111.165 27.0075C109.995 26.6833 109.03 26.359 108.271 26.0865C107.204 25.6585 106.29 25.1655 105.532 24.5688C103.654 23.103 102.689 21.1572 102.689 18.6666C102.689 16.0203 103.602 13.9059 105.429 12.3882C107.306 10.8186 109.596 10.0662 112.335 10.0662C116.709 10.0662 119.898 11.9601 121.982 15.7998L116.4 19.0428C115.59 17.2008 114.213 16.2798 112.335 16.2798C110.355 16.2798 109.39 17.0321 109.39 18.498C109.39 19.6395 110.908 20.6643 114.008 21.4685C116.4 22.0134 118.278 22.8176 119.641 23.8554L119.654 23.8683ZM140.477 17.538H134.741V28.7977C134.741 30.1468 135.255 30.964 136.22 31.3402C136.927 31.6126 138.355 31.6645 140.49 31.5607V37.891C136.079 38.4358 132.876 37.9948 130.998 36.5419C129.12 35.1409 128.207 32.5336 128.207 28.8106V17.538H123.795V10.8316H128.207V5.37038L134.754 3.25595V10.8316H140.49V17.538H140.477ZM161.352 29.7187C162.677 28.3177 163.333 26.5276 163.333 24.3613C163.333 22.195 162.677 20.4178 161.352 19.0039C160.027 17.6029 158.407 16.8894 156.426 16.8894C154.445 16.8894 152.825 17.5899 151.5 19.0039C150.227 20.4697 149.571 22.2469 149.571 24.3613C149.571 26.4757 150.227 28.2529 151.5 29.7187C152.825 31.1196 154.445 31.8331 156.426 31.8331C158.407 31.8331 160.027 31.1326 161.352 29.7187ZM146.883 34.5313C144.297 31.7682 143.024 28.4215 143.024 24.3613C143.024 20.3011 144.297 17.0062 146.883 14.2432C149.468 11.4802 152.67 10.0792 156.426 10.0792C160.182 10.0792 163.384 11.4802 165.97 14.2432C168.555 17.0062 169.88 20.4178 169.88 24.3613C169.88 28.3047 168.555 31.7682 165.97 34.5313C163.384 37.2943 160.233 38.6434 156.426 38.6434C152.619 38.6434 149.468 37.2943 146.883 34.5313ZM191.771 29.8743C193.095 28.4085 193.751 26.5795 193.751 24.3613C193.751 22.1431 193.095 20.3011 191.771 18.9001C190.446 17.4343 188.774 16.7338 186.742 16.7338C184.709 16.7338 183.037 17.4343 181.661 18.9001C180.336 20.3011 179.68 22.1431 179.68 24.3613C179.68 26.5795 180.336 28.4215 181.661 29.8743C183.037 31.2753 184.761 31.9888 186.742 31.9888C188.722 31.9888 190.446 31.2883 191.771 29.8743ZM193.751 0H200.298V37.891H193.751V34.6999C191.822 37.3462 189.082 38.6563 185.52 38.6563C181.957 38.6563 179.179 37.3073 176.735 34.5442C174.343 31.7812 173.134 28.3696 173.134 24.3743C173.134 20.3789 174.356 17.0192 176.735 14.2561C179.166 11.4931 182.111 10.0922 185.52 10.0922C188.928 10.0922 191.822 11.3894 193.751 14.0356V0.0129719V0ZM223.308 29.7057C224.633 28.3047 225.289 26.5146 225.289 24.3483C225.289 22.182 224.633 20.4048 223.308 18.9909C221.983 17.5899 220.363 16.8765 218.382 16.8765C216.401 16.8765 214.78 17.577 213.456 18.9909C212.182 20.4567 211.526 22.2339 211.526 24.3483C211.526 26.4627 212.182 28.2399 213.456 29.7057C214.78 31.1067 216.401 31.8201 218.382 31.8201C220.363 31.8201 221.983 31.1196 223.308 29.7057ZM208.838 34.5183C206.253 31.7553 204.98 28.4085 204.98 24.3483C204.98 20.2881 206.253 16.9932 208.838 14.2302C211.424 11.4672 214.626 10.0662 218.382 10.0662C222.137 10.0662 225.34 11.4672 227.925 14.2302C230.511 16.9932 231.835 20.4048 231.835 24.3483C231.835 28.2918 230.511 31.7553 227.925 34.5183C225.34 37.2813 222.189 38.6304 218.382 38.6304C214.575 38.6304 211.424 37.2813 208.838 34.5183ZM260.17 21.261V37.878H253.623V22.1301C253.623 20.34 253.173 18.9909 252.247 17.9661C251.385 17.0451 250.164 16.5651 248.594 16.5651C244.89 16.5651 243.012 18.7833 243.012 23.2716V37.878H236.466V10.8316H243.012V13.867C244.581 11.3245 247.077 10.0792 250.575 10.0792C253.366 10.0792 255.656 11.0521 257.431 13.0498C259.257 15.0474 260.17 17.7586 260.17 21.274" fill="currentColor"></path> <defs> <linearGradient id="paint0_linear_89_11" x1="30.5" y1="0.0130005" x2="30.5" y2="65.013" gradientUnits="userSpaceOnUse"> <stop stop-color="#6364FF"></stop> <stop offset="1" stop-color="#563ACC"></stop> </linearGradient> </defs> </svg>'; /** * Decide whether the modal should be displayed. * We want to show it when the user clicks on the Mastodon share button, * thus loading a page with ?share=mastodon&nb=1. * * @return bool */ public static function should_display_modal() { if ( // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Only used to display the modal. isset( $_GET['share'] ) && 'mastodon' === sanitize_text_field( wp_unslash( $_GET['share'] ) ) && isset( $_GET['nb'] ) && 1 === (int) $_GET['nb'] // phpcs:enable ) { return true; } return false; } /** * Hook the modal render into WordPress. */ public static function modal() { if ( ! self::should_display_modal() ) { return; } // Render the modal. self::render_modal(); die( 0 ); } /** * Render the modal. */ public static function render_modal() { ?> <!doctype html> <html <?php language_attributes(); ?>> <head> <meta charset="<?php bloginfo( 'charset' ); ?>" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="robots" content="noindex,follow" /> <meta name="referrer" content="no-referrer"> <?php rel_canonical(); ?> <title><?php esc_html_e( 'Share to Mastodon', 'jetpack' ); ?></title> <style type="text/css"> .sd-mastodon-modal { background-color: #282c37; color: #ffffff; display: flex; flex-direction: column; font-family: sans-serif; margin-top: 1em; } .sd-mastodon-modal__inner { margin: 0 auto; width: 450px; } .sd-mastodon-modal__title, .sd-mastodon-modal__logo { display: flex; font-size: 0; justify-content: center; align-items: center; } .sd-mastodon-modal__logo { margin-top: 50px; } .sd-mastodon-modal__logo svg { height: 42px; } .sd-mastodon-modal__inner form { margin: 2em 0; max-width: 400px; padding: 0 1em; } .sd-mastodon-modal__inner label { font-size: 14px; font-weight: 500; } .sd-mastodon-modal__inner input { box-sizing: border-box; font-size: 16px; color: #fff; display: block; width: 100%; outline: 0; font-family: inherit; resize: vertical; background: #131419; border: 1px solid #0a0b0e; border-radius: 4px; margin: 15px 10px 10px 0; padding: 10px; } .sd-mastodon-modal__inner input:focus { border-color: #8c8dff; background: #17191f; } .sd-mastodon-modal__submit button { display: block; width: 100%; border: 0; border-radius: 4px; background: #595aff; color: #fff; font-size: 18px; line-height: inherit; height: auto; margin: 15px 10px 10px 0; padding: 10px; text-decoration: none; text-transform: uppercase; text-align: center; box-sizing: border-box; cursor: pointer; font-weight: 500; outline: 0; } .sd-mastodon-modal__submit button:hover { background-color: #6364ff; } </style> </head> <body class="sd-mastodon-modal"> <div class="sd-mastodon-modal__inner"> <h2 class="sd-mastodon-modal__title"><?php esc_html_e( 'Share to Mastodon', 'jetpack' ); ?></h2> <div class="sd-mastodon-modal__logo"> <?php echo self::$svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- SVG is safe. ?> </div> <form method="post"> <label for="jetpack-mastodon-instance" class="sd-mastodon-modal__label"> <?php esc_html_e( 'Enter the full URL of the Mastodon instance where you’d like to share this post.', 'jetpack' ); ?> </label> <input required aria-required="true" type="url" id="jetpack-mastodon-instance" name="jetpack-mastodon-instance" placeholder="<?php echo esc_url( 'https://mastodon.social' ); ?>" /> <div class="sd-mastodon-modal__submit"> <?php wp_nonce_field( 'jetpack_share_mastodon_instance', '_wpnonce', true, true ); ?> <button id="mastodon-submit"><?php esc_html_e( 'Share', 'jetpack' ); ?></button> </div> </form> </div> </body> </html> <?php } } sharedaddy/admin-sharing-rtl.min.css 0000644 00000016476 15174711637 0013522 0 ustar 00 #services-config{float:right;min-width:700px;width:100%}#services-config h3{color:#464646;font-size:15px;font-weight:400;margin:0;overflow:hidden;padding:8px 10px;white-space:nowrap}#available-services,#enabled-services,#live-preview{border-spacing:0;padding:20px 0 0;width:100%}#enabled-services .ui-sortable{min-height:50px}#enabled-services{padding-bottom:20px}#available-services,#enabled-services{border-bottom:2px solid #ccc}#live-preview{border-bottom:1px solid #dcdcde;padding-bottom:60px}#available-services h3,#enabled-services h3,#live-preview h3{margin-bottom:1em;margin-top:0;padding:0}body.settings_page_sharing .description{vertical-align:top;width:180px}body.settings_page_sharing .description p{font-size:13px;font-style:italic}body.settings_page_sharing .services{padding:0 20px;vertical-align:top}body.settings_page_sharing .services ul li{cursor:move}body.settings_page_sharing .services ul li.divider{background:none;border:none;cursor:default;padding:0}body.settings_page_sharing ul.services-hidden{margin-bottom:0}#available-services .service,#enabled-services .service{background:#fff;border-radius:4px;box-shadow:0 1px 2px #0000001f,0 0 0 1px #0000001f;color:#2c3338!important;display:inline-block;font-family:Open Sans,sans-serif;font-size:13px;font-weight:500;line-height:23px;margin:0 0 8px 8px;padding:3px 9px 4px 11px}#available-services .service .options-left,#enabled-services .service .options-left{align-items:center;display:flex}#available-services .service:hover,#enabled-services .service:hover{box-shadow:0 1px 2px #00000038,0 0 0 1px #00000038}#available-services .service.share-deprecated,#enabled-services .service.share-deprecated{opacity:.5;padding:5px;text-decoration:line-through}#available-services .service.share-deprecated{display:none}li.service span:before{-webkit-font-smoothing:antialiased;font:400 18px/1 social-logos;height:16px;margin-left:6px;position:relative;top:-1px;width:16px}li.service.share-print span:before{content:"\f469"}li.service.share-digg span:before{content:"\f221"}li.service.share-email span:before{content:"\f410"}li.service.share-linkedin span:before{content:"\f207"}li.service.share-twitter span:before,li.service.share-x span:before{content:"\f10e"}li.service.share-reddit span:before{content:"\f222"}li.service.share-tumblr span:before{content:"\f214"}li.service.share-pinterest span:before{content:"\f209"}li.service.share-facebook span:before{content:"\f203"}li.service.share-press-this span:before{content:"\f205"}li.service.share-telegram span:before{content:"\f606"}li.service.share-threads span:before{content:"\f10d"}li.service.share-jetpack-whatsapp span:before{content:"\f608"}li.service.share-mastodon span:before{content:"\f10a"}li.service.share-nextdoor span:before{content:"\f10c"}li.service.share-bluesky span:before{content:"\f10f"}body.settings_page_sharing ul.preview{display:flex;flex-wrap:wrap;float:right}body.settings_page_sharing ul.preview li.preview-item,body.settings_page_sharing ul.preview li.preview-item a{cursor:default;text-decoration:none}div.sd-social-icon .inner li.preview-item a span,div.sd-social-icon ul.preview li.preview-item a span{display:none}div.sd-social-icon ul.preview li.preview-item.preview-custom a span{display:inline-block}.services .preview li.share-custom a{text-decoration:none}.services ul li.end-fix{clear:both;float:none;height:20px;margin:0;padding:0;visibility:hidden;width:0}#enabled-services h2{color:#999;font-size:20px;font-weight:400!important;padding-top:0}body.settings_page_sharing #live-preview h2{color:#e3e3e3;font-size:20px;font-weight:400!important}body.settings_page_sharing .clearing{clear:both}body.settings_page_sharing .options .options-left{float:right}body.settings_page_sharing .input label{font-size:11px;line-height:16px}body.settings_page_sharing .advanced-form{border-top:1px solid #e3e3e3;display:none;margin-right:-24px;margin-top:4px;padding:10px 10px 8px 14px}body.settings_page_sharing .utility{float:left;font-size:10px;padding-left:10px;padding-top:10px}body.settings_page_sharing .advanced input[type=submit]{float:right;margin-left:10px;margin-top:10px}.services li.dropzone{background:#e3e3e3;border:1px dashed #999;border-radius:3px;height:18px;margin-left:10px;padding:5px}.advanced-form .form-table th{width:auto!important}.advanced-form .button-secondary{margin-top:0!important}#hidden-drop-target{background:#e1e1e1;border:1px solid #cdcdcd;padding:10px;vertical-align:top;width:29%}#hidden-drop-target p{font-size:13px;font-style:italic;margin:0 0 10px}.preview li.preview-item{background-position:100% 5px;cursor:default}.preview .option-smart-on{margin:3px 0 0 5px}.preview-digg .option-smart-on{background:url(../../modules/sharedaddy/images/smart-digg.png) no-repeat 100% 0;background-size:76px 17px;height:17px;margin-top:2px;width:76px}.preview-facebook .option-smart-on{background:url(../../modules/sharedaddy/images/smart-like.png) no-repeat 100% 0;background-size:85px 20px;height:20px;margin-right:-4px;width:85px}.preview-twitter .option-smart-on,.preview-x .option-smart-on{background:url(../../modules/sharedaddy/images/smart-x.png) no-repeat 100% 0;background-size:60px 20px;height:20px;width:60px}.preview-linkedin .option-smart-on{background:url(../../modules/sharedaddy/images/linkedin-smart.png) no-repeat top;background-size:99px 18px;height:20px;width:99px}.preview-tumblr .option-smart-on{background:url(../../modules/sharedaddy/images/smart-tumblr.png) no-repeat 100% 0;background-size:62px 20px;height:20px;width:62px}.preview-pinterest .option-smart-on{background:url(../../modules/sharedaddy/images/smart-pinterest.png) no-repeat 100% 0;background-size:39px 20px;height:20px;width:39px}.preview-item.share-deprecated{opacity:.5}.preview-item.share-deprecated a span{text-decoration:line-through}@media (min-resolution:120dpi){.preview-digg .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-digg@2x.png)}.preview-facebook .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-like@2x.png)}.preview-twitter .option-smart-on,.preview-x .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-x@2x.png)}.preview-linkedin .option-smart-on{background-image:url(../../modules/sharedaddy/images/linkedin-smart@2x.png)}.preview-tumblr .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-tumblr@2x.png)}.preview-pinterest .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-pinterest@2x.png)}}.services .sharing-hidden li{background-color:initial}.sharing-hidden li.share-end{clear:both;float:none;height:0;margin:0!important;padding:0!important;visibility:hidden;width:0}.preview .sharing-label{border:0;font-weight:700;padding:4px 0 0 6px}#services-config a.remove{background:#dcdcde;border-radius:15px;color:#fff;font-size:10px;font-weight:700;padding:0 4px 2px;text-decoration:none}#services-config a.remove:hover{background:red}.sd-social-icon .inner a.sd-button span,.sd-social-icon .inner a.share-icon span{display:inline-block;overflow:hidden;text-indent:100%;width:0}.sharing-block-message__items-wrapper{column-gap:6rem;display:flex;flex-wrap:wrap}.sharing-block-message__buttons-wrapper .button{margin-left:1rem}.sharing-block-message__buttons-wrapper .button-primary:visited{color:#fff}.admin-sharing-settings__block-theme-description{font-style:italic;margin-top:1rem}a.dops-card__link{cursor:pointer}.settings-sharing__block-theme-description{font-style:italic} sharedaddy/sharing.php 0000644 00000077572 15174711637 0011056 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Set up Sharing functionality and management in wp-admin. * * @package automattic/jetpack */ // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files. use Automattic\Jetpack\Assets; use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Status; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } if ( ! defined( 'WP_SHARING_PLUGIN_URL' ) ) { define( 'WP_SHARING_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'WP_SHARING_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); } /** * Utilities to manage sharing settings from wp-admin. */ class Sharing_Admin { /** * Constructor. * Hook into WordPress to add our functionality. */ public function __construct() { require_once WP_SHARING_PLUGIN_DIR . 'sharing-service.php'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonces are handled in process_requests. if ( isset( $_GET['page'] ) && ( $_GET['page'] === 'sharing.php' || $_GET['page'] === 'sharing' ) ) { add_action( 'admin_init', array( $this, 'admin_init' ) ); } add_action( 'admin_menu', array( $this, 'subscription_menu' ) ); // Insert our CSS and JS add_action( 'load-settings_page_sharing', array( $this, 'sharing_head' ) ); // Catch AJAX add_action( 'wp_ajax_sharing_save_services', array( $this, 'ajax_save_services' ) ); add_action( 'wp_ajax_sharing_save_options', array( $this, 'ajax_save_options' ) ); add_action( 'wp_ajax_sharing_new_service', array( $this, 'ajax_new_service' ) ); add_action( 'wp_ajax_sharing_delete_service', array( $this, 'ajax_delete_service' ) ); } /** * Enqueue scripts and styles on the sharing settings page. * * @return void */ public function sharing_head() { wp_enqueue_script( 'sharing-js', Assets::get_file_url_for_environment( '_inc/build/sharedaddy/admin-sharing.min.js', 'modules/sharedaddy/admin-sharing.js' ), array( 'jquery', 'jquery-ui-draggable', 'jquery-ui-droppable', 'jquery-ui-sortable', 'jquery-form' ), JETPACK__VERSION, false ); /** * Filters the switch that if set to true allows Jetpack to use minified assets. Defaults to true * if the SCRIPT_DEBUG constant is not set or set to false. The filter overrides it. * * @since 6.2.0 * * @param boolean $var should Jetpack use minified assets. */ $postfix = apply_filters( 'jetpack_should_use_minified_assets', true ) ? '.min' : ''; if ( is_rtl() ) { wp_enqueue_style( 'sharing-admin', WP_SHARING_PLUGIN_URL . 'admin-sharing-rtl' . $postfix . '.css', false, JETPACK__VERSION ); } else { wp_enqueue_style( 'sharing-admin', WP_SHARING_PLUGIN_URL . 'admin-sharing' . $postfix . '.css', false, JETPACK__VERSION ); } wp_enqueue_style( 'sharing', WP_SHARING_PLUGIN_URL . 'sharing.css', false, JETPACK__VERSION ); wp_enqueue_style( 'social-logos' ); wp_enqueue_script( 'sharing-js-fe', WP_SHARING_PLUGIN_URL . 'sharing.js', array(), 4, false ); add_thickbox(); // On Jetpack sites, make sure we include CSS to style the admin page. if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) { Jetpack_Admin_Page::load_wrapper_styles(); } } /** * Load the process that handles saving changes on the sharing settings page. * * @return void */ public function admin_init() { $this->process_requests(); } /** * Save changes to sharing settings. * * @return void */ public function process_requests() { if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'sharing-options' ) ) { $sharer = new Sharing_Service(); $sharer->set_global_options( $_POST ); /** * Fires when updating sharing settings. * * @module sharedaddy * * @since 1.1.0 */ do_action( 'sharing_admin_update' ); wp_safe_redirect( admin_url( 'options-general.php?page=sharing&update=saved' ) ); die( 0 ); } } /** * Register Sharing settings menu page in Settings > Sharing. */ public function subscription_menu() { add_submenu_page( 'options-general.php', __( 'Sharing Settings', 'jetpack' ), __( 'Sharing', 'jetpack' ), 'manage_options', 'sharing', array( $this, 'wrapper_admin_page' ) ); } /** * Save changes to sharing services via AJAX. * * @return void */ public function ajax_save_services() { if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'sharing-options' ) && isset( $_POST['hidden'] ) && isset( $_POST['visible'] ) ) { $sharer = new Sharing_Service(); $sharer->set_blog_services( explode( ',', sanitize_text_field( wp_unslash( $_POST['visible'] ) ) ), explode( ',', sanitize_text_field( wp_unslash( $_POST['hidden'] ) ) ) ); die( 0 ); } } /** * Create a new custom sharing service via AJAX. * * @return never */ public function ajax_new_service() { if ( isset( $_POST['_wpnonce'] ) && isset( $_POST['sharing_name'] ) && isset( $_POST['sharing_url'] ) && isset( $_POST['sharing_icon'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'sharing-new_service' ) ) { $sharer = new Sharing_Service(); $service = $sharer->new_service( sanitize_text_field( wp_unslash( $_POST['sharing_name'] ) ), esc_url_raw( wp_unslash( $_POST['sharing_url'] ) ), esc_url_raw( wp_unslash( $_POST['sharing_icon'] ) ) ); if ( $service ) { $this->output_service( $service->get_id(), $service ); echo '<!--->'; $service->button_style = 'icon-text'; $this->output_preview( $service ); die( 0 ); } } // Fail die( '1' ); } /** * Delete a sharing service via AJAX. * * @return void */ public function ajax_delete_service() { if ( isset( $_POST['_wpnonce'] ) && isset( $_POST['service'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'sharing-options_' . sanitize_text_field( wp_unslash( $_POST['service'] ) ) ) ) { $sharer = new Sharing_Service(); $sharer->delete_service( sanitize_text_field( wp_unslash( $_POST['service'] ) ) ); } } /** * Save changes to sharing settings via AJAX. * * @return void */ public function ajax_save_options() { if ( isset( $_POST['_wpnonce'] ) && isset( $_POST['service'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'sharing-options_' . sanitize_text_field( wp_unslash( $_POST['service'] ) ) ) ) { $sharer = new Sharing_Service(); $service = $sharer->get_service( sanitize_text_field( wp_unslash( $_POST['service'] ) ) ); if ( $service && $service instanceof Sharing_Advanced_Source ) { $service->update_options( $_POST ); $sharer->set_service( sanitize_text_field( wp_unslash( $_POST['service'] ) ), $service ); } $this->output_service( $service->get_id(), $service, true ); echo '<!--->'; $service->button_style = 'icon-text'; $this->output_preview( $service ); die( 0 ); } } /** * Display a preview of a sharing service. * * @param object $service Sharing service object. * * @return void */ public function output_preview( $service ) { $klasses = array( 'advanced', 'preview-item' ); if ( $service->button_style !== 'text' || $service->has_custom_button_style() ) { $klasses[] = 'preview-' . $service->get_class(); $klasses[] = 'share-' . $service->get_class(); if ( $service->is_deprecated() ) { $klasses[] = 'share-deprecated'; } if ( $service->get_class() !== $service->get_id() ) { $klasses[] = 'preview-' . $service->get_id(); } } echo '<li class="' . esc_attr( implode( ' ', $klasses ) ) . '">'; $service->display_preview(); echo '</li>'; } /** * Display a specific sharing service. * * @param int $id Service unique ID. * @param object $service Sharing service. * @param bool $show_dropdown Display a dropdown. Not in use at the moment. * * @return void */ public function output_service( $id, $service, $show_dropdown = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $title = ''; $klasses = array( 'service', 'advanced', 'share-' . $service->get_class() ); $displayed_klasses = implode( ' ', $klasses ); if ( $service->is_deprecated() ) { /* translators: %1$s is the name of a deprecated Sharing Service like "Google+" */ $title = sprintf( __( 'The %1$s sharing service has shut down or discontinued support for sharing buttons. This sharing button is not displayed to your visitors and should be removed.', 'jetpack' ), $service->get_name() ); $klasses[] = 'share-deprecated'; } ?> <li class="<?php echo esc_attr( $displayed_klasses ); ?>" id="<?php echo esc_attr( $service->get_id() ); ?>" tabindex="0" title="<?php echo esc_attr( $title ); ?>"> <span class="options-left"><?php echo esc_html( $service->get_name() ); ?></span> <?php if ( str_starts_with( $service->get_id(), 'custom-' ) || $service->has_advanced_options() ) : ?> <span class="close"><a href="#" class="remove">×</a></span> <form method="post" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>"> <input type="hidden" name="action" value="sharing_delete_service" /> <input type="hidden" name="service" value="<?php echo esc_attr( $id ); ?>" /> <input type="hidden" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( 'sharing-options_' . $id ) ); ?>" /> </form> <?php endif; ?> </li> <?php } /** * Display admin UI within a Jetpack header and footer. * * @return void */ public function wrapper_admin_page() { Jetpack_Admin_Page::wrap_ui( array( $this, 'management_page' ), array( 'is-wide' => true ) ); } /** * Sharing settings inner page structure. * * @return void */ public function management_page() { if ( ! function_exists( 'mb_stripos' ) ) { echo '<div id="message" class="updated fade"><h3>' . esc_html__( 'Warning! Multibyte support missing!', 'jetpack' ) . '</h3>'; echo '<p>' . wp_kses( sprintf( /* Translators: placeholder is a link to a PHP support document. */ __( 'This plugin will work without it, but multibyte support is used <a href="%s" rel="noopener noreferrer" target="_blank">if available</a>. You may see minor problems with Tweets and other sharing services.', 'jetpack' ), 'https://www.php.net/manual/en/mbstring.installation.php' ), array( 'a' => array( 'href' => array(), 'rel' => array(), 'target' => array(), ), ) ) . '</p></div>'; } if ( isset( $_GET['update'] ) && 'saved' === $_GET['update'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only used to display a message. echo '<div class="updated"><p>' . esc_html__( 'Settings have been saved', 'jetpack' ) . '</p></div>'; } ?> <div class="wrap"> <div class="icon32" id="icon-options-general"><br /></div> <h1><?php esc_html_e( 'Sharing Settings', 'jetpack' ); ?></h1> <?php /** * Fires at the top of the admin sharing settings screen. * * @module sharedaddy * * @since 1.6.0 */ do_action( 'pre_admin_screen_sharing' ); ?> <?php $is_simple_site = defined( 'IS_WPCOM' ) && IS_WPCOM; $show_block_message = $this->should_use_site_editor() && ! $is_simple_site; // We either show old services config or the sharing block message. if ( current_user_can( 'manage_options' ) ) : $show_block_message ? $this->sharing_block_display() : $this->services_config_display(); endif; ?> </div> <script type="text/javascript"> var sharing_loading_icon = <?php echo wp_json_encode( admin_url( '/images/loading.gif' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>; <?php // phpcs:disable WordPress.Security.NonceVerification.Recommended -- we handle the nonce on the PHP side. if ( isset( $_GET['create_new_service'] ) && isset( $_GET['name'] ) && isset( $_GET['url'] ) && isset( $_GET['icon'] ) && 'true' == $_GET['create_new_service'] // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual ) : ?> jQuery(document).ready(function() { // Prefill new service box and then open it jQuery( '#new_sharing_name' ).val( <?php echo wp_json_encode( sanitize_text_field( wp_unslash( $_GET['name'] ) ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?> ); jQuery( '#new_sharing_url' ).val( <?php echo wp_json_encode( sanitize_text_field( wp_unslash( $_GET['url'] ) ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?> ); jQuery( '#new_sharing_icon' ).val( <?php echo wp_json_encode( sanitize_text_field( wp_unslash( $_GET['icon'] ) ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?> ); jQuery( '#add-a-new-service' ).click(); }); <?php endif; ?> </script> <?php // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Check if we should encourage to use the site editor instead of the legacy sharing settings. * * @return boolean */ public function should_use_site_editor() { $block_availability = Jetpack_Gutenberg::get_cached_availability(); $is_block_available = isset( $block_availability['sharing-buttons'] ) && $block_availability['sharing-buttons']['available']; $is_block_theme = wp_is_block_theme(); return $is_block_available && $is_block_theme; } /** * Display services admin UI for settings. * * @return void */ public function services_config_display() { $sharer = new Sharing_Service(); $enabled = $sharer->get_blog_services(); $global = $sharer->get_global_options(); $shows = array_values( get_post_types( array( 'public' => true ) ) ); array_unshift( $shows, 'index' ); if ( ! isset( $global['sharing_label'] ) ) { $global['sharing_label'] = __( 'Share this:', 'jetpack' ); } ?> <div class="share_manage_options"> <h2><?php esc_html_e( 'Sharing Buttons', 'jetpack' ); ?></h2> <p><?php esc_html_e( 'Add sharing buttons to your blog and allow your visitors to share posts with their friends.', 'jetpack' ); ?></p> <?php $is_simple_site = defined( 'IS_WPCOM' ) && IS_WPCOM; if ( $this->should_use_site_editor() && $is_simple_site ) : $this->site_editor_prompt_display(); ?> <div class="notice notice-info inline"> <p> <?php esc_html_e( 'You are using a block-based theme. We recommend that you disable the legacy sharing features below and add a sharing button block to your theme’s template instead.', 'jetpack' ); ?> </p> </div> <?php endif; ?> <div id="services-config"> <table id="available-services"> <tr> <td class="description"> <h3><?php esc_html_e( 'Available Services', 'jetpack' ); ?></h3> <p><?php esc_html_e( "Drag and drop the services you'd like to enable into the box below.", 'jetpack' ); ?></p> <p><a href="#TB_inline?height=395&width=600&inlineId=new-service" class="thickbox" id="add-a-new-service"><?php esc_html_e( 'Add a new service', 'jetpack' ); ?></a></p> </td> <td class="services"> <ul class="services-available" style="height: 100px;"> <?php foreach ( $sharer->get_all_services_blog() as $id => $service ) : ?> <?php if ( ! isset( $enabled['all'][ $id ] ) ) { $this->output_service( $id, $service ); } ?> <?php endforeach; ?> </ul> <?php if ( ( new Status() )->is_private_site() ) { echo '<p><strong>' . esc_html__( 'Please note that your services have been restricted because your site is private.', 'jetpack' ) . '</strong></p>'; } ?> <br class="clearing" /> </td> </tr> </table> <table id="enabled-services"> <tr> <td class="description"> <h3> <?php esc_html_e( 'Enabled Services', 'jetpack' ); ?> <img src="<?php echo esc_url( admin_url( 'images/loading.gif' ) ); ?>" width="16" height="16" alt="loading" style="vertical-align: middle; display: none" /> </h3> <p><?php esc_html_e( 'Services dragged here will appear individually.', 'jetpack' ); ?></p> </td> <td class="services" id="share-drop-target"> <h2 id="drag-instructions" <?php if ( is_countable( $enabled['visible'] ) && count( $enabled['visible'] ) > 0 ) { echo ' style="display: none"';} ?> ><?php esc_html_e( 'Drag and drop available services here.', 'jetpack' ); ?></h2> <ul class="services-enabled"> <?php foreach ( $enabled['visible'] as $id => $service ) : ?> <?php $this->output_service( $id, $service, true ); ?> <?php endforeach; ?> <li class="end-fix"></li> </ul> </td> <td id="hidden-drop-target" class="services"> <p><?php esc_html_e( 'Services dragged here will be hidden behind a share button.', 'jetpack' ); ?></p> <ul class="services-hidden"> <?php foreach ( $enabled['hidden'] as $id => $service ) : ?> <?php $this->output_service( $id, $service, true ); ?> <?php endforeach; ?> <li class="end-fix"></li> </ul> </td> </tr> </table> <table id="live-preview"> <tr> <td class="description"> <h3><?php esc_html_e( 'Live Preview', 'jetpack' ); ?></h3> </td> <td class="services"> <h2 <?php echo ( is_countable( $enabled['all'] ) && count( $enabled['all'] ) > 0 ) ? ' style="display: none"' : ''; ?>><?php esc_html_e( 'Sharing is off. Add services above to enable.', 'jetpack' ); ?></h2> <div class="sharedaddy sd-sharing-enabled"> <?php if ( is_countable( $enabled['all'] ) && count( $enabled['all'] ) > 0 ) : ?> <h3 class="sd-title"><?php echo esc_html( $global['sharing_label'] ); ?></h3> <?php endif; ?> <div class="sd-content"> <ul class="preview"> <?php foreach ( $enabled['visible'] as $id => $service ) : ?> <?php $this->output_preview( $service ); ?> <?php endforeach; ?> <?php if ( is_countable( $enabled['hidden'] ) && count( $enabled['hidden'] ) > 0 ) : ?> <li class="advanced"><a href="#" class="sharing-anchor sd-button share-more"><span><?php esc_html_e( 'More', 'jetpack' ); ?></span></a></li> <?php endif; ?> </ul> <?php if ( is_countable( $enabled['hidden'] ) && count( $enabled['hidden'] ) > 0 ) : ?> <div class="sharing-hidden"> <div class="inner" style="display: none; <?php echo count( $enabled['hidden'] ) === 1 ? 'width:150px;' : ''; ?>"> <?php if ( count( $enabled['hidden'] ) === 1 ) : ?> <ul style="background-image:none;"> <?php else : ?> <ul> <?php endif; ?> <?php foreach ( $enabled['hidden'] as $id => $service ) { $this->output_preview( $service ); } ?> </ul> </div> </div> <?php endif; ?> <ul class="archive" style="display:none;"> <?php foreach ( $sharer->get_all_services_blog() as $id => $service ) : if ( isset( $enabled['visible'][ $id ] ) ) { $service = $enabled['visible'][ $id ]; } elseif ( isset( $enabled['hidden'][ $id ] ) ) { $service = $enabled['hidden'][ $id ]; } $service->button_style = 'icon-text'; // The archive needs the full text, which is removed in JS later. $service->smart = false; $this->output_preview( $service ); endforeach; ?> <li class="advanced"><a href="#" class="sharing-anchor sd-button share-more"><span><?php esc_html_e( 'More', 'jetpack' ); ?></span></a></li> </ul> </div> </div> <br class="clearing" /> </td> </tr> </table> <form method="post" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" id="save-enabled-shares"> <input type="hidden" name="action" value="sharing_save_services" /> <input type="hidden" name="visible" value="<?php echo esc_attr( implode( ',', array_keys( $enabled['visible'] ) ) ); ?>" /> <input type="hidden" name="hidden" value="<?php echo esc_attr( implode( ',', array_keys( $enabled['hidden'] ) ) ); ?>" /> <input type="hidden" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( 'sharing-options' ) ); ?>" /> </form> </div> <form method="post" action=""> <table class="form-table"> <tbody> <tr valign="top"> <th scope="row"><label><?php esc_html_e( 'Button style', 'jetpack' ); ?></label></th> <td> <select name="button_style" id="button_style"> <option<?php echo ( $global['button_style'] === 'icon-text' ) ? ' selected="selected"' : ''; ?> value="icon-text"><?php esc_html_e( 'Icon + text', 'jetpack' ); ?></option> <option<?php echo ( $global['button_style'] === 'icon' ) ? ' selected="selected"' : ''; ?> value="icon"><?php esc_html_e( 'Icon only', 'jetpack' ); ?></option> <option<?php echo ( $global['button_style'] === 'text' ) ? ' selected="selected"' : ''; ?> value="text"><?php esc_html_e( 'Text only', 'jetpack' ); ?></option> <option<?php echo ( $global['button_style'] === 'official' ) ? ' selected="selected"' : ''; ?> value="official"><?php esc_html_e( 'Official buttons', 'jetpack' ); ?></option> </select> </td> </tr> <tr valign="top"> <th scope="row"><label><?php esc_html_e( 'Sharing label', 'jetpack' ); ?></label></th> <td> <input type="text" name="sharing_label" value="<?php echo esc_attr( $global['sharing_label'] ); ?>" /> </td> </tr> <?php /** * Filters the HTML at the beginning of the "Show button on" row. * * @module sharedaddy * * @since 2.1.0 * * @param string $var Opening HTML tag at the beginning of the "Show button on" row. */ echo apply_filters( 'sharing_show_buttons_on_row_start', '<tr valign="top">' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <th scope="row"><label><?php esc_html_e( 'Show buttons on', 'jetpack' ); ?></label></th> <td> <?php $br = false; foreach ( $shows as $show ) : if ( 'index' === $show ) { $label = __( 'Front Page, Archive Pages, and Search Results', 'jetpack' ); } else { $post_type_object = get_post_type_object( $show ); $label = $post_type_object->labels->name; } ?> <?php if ( $br ) { echo '<br />'; } ?> <label><input type="checkbox"<?php checked( in_array( $show, $global['show'], true ) ); ?> name="show[]" value="<?php echo esc_attr( $show ); ?>" /> <?php echo esc_html( $label ); ?></label> <?php $br = true; endforeach; ?> </td> <?php /** * Filters the HTML at the end of the "Show button on" row. * * @module sharedaddy * * @since 2.1.0 * * @param string $var Closing HTML tag at the end of the "Show button on" row. */ echo apply_filters( 'sharing_show_buttons_on_row_end', '</tr>' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php /** * Fires at the end of the sharing global options settings table. * * @module sharedaddy * * @since 1.1.0 */ do_action( 'sharing_global_options' ); ?> </tbody> </table> <p class="submit"> <input type="submit" name="submit" class="button-primary" value="<?php esc_attr_e( 'Save Changes', 'jetpack' ); ?>" /> </p> <input type="hidden" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( 'sharing-options' ) ); ?>" /> </form> <div id="new-service" style="display: none"> <form method="post" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" id="new-service-form"> <table class="form-table"> <tbody> <tr valign="top"> <th scope="row" width="100"><label><?php esc_html_e( 'Service name', 'jetpack' ); ?></label></th> <td> <input type="text" name="sharing_name" id="new_sharing_name" size="40" /> </td> </tr> <tr valign="top"> <th scope="row" width="100"><label><?php esc_html_e( 'Sharing URL', 'jetpack' ); ?></label></th> <td> <input type="text" name="sharing_url" id="new_sharing_url" size="40" /> <p><?php esc_html_e( 'You can add the following variables to your service sharing URL:', 'jetpack' ); ?><br/> <code>%post_id%</code>, <code>%post_title%</code>, <code>%post_slug%</code>, <code>%post_url%</code>, <code>%post_full_url%</code>, <code>%post_excerpt%</code>, <code>%post_tags%</code>, <code>%home_url%</code></p> </td> </tr> <tr valign="top"> <th scope="row" width="100"><label><?php esc_html_e( 'Icon URL', 'jetpack' ); ?></label></th> <td> <input type="text" name="sharing_icon" id="new_sharing_icon" size="40" /> <p><?php esc_html_e( 'Enter the URL of a 16x16px icon you want to use for this service.', 'jetpack' ); ?></p> </td> </tr> <tr valign="top" width="100"> <th scope="row"></th> <td> <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Create Share Button', 'jetpack' ); ?>" /> <img src="<?php echo esc_url( admin_url( 'images/loading.gif' ) ); ?>" width="16" height="16" alt="loading" style="vertical-align: middle; display: none" /> </td> </tr> <?php /** * Fires after the custom sharing service form * * @module sharedaddy * * @since 1.1.0 */ do_action( 'sharing_new_service_form' ); ?> </tbody> </table> <?php /** * Fires at the bottom of the admin sharing settings screen. * * @module sharedaddy * * @since 1.6.0 */ do_action( 'post_admin_screen_sharing' ); ?> <div class="inerror" style="display: none; margin-top: 15px"> <p><?php esc_html_e( 'An error occurred creating your new sharing service - please check you gave valid details.', 'jetpack' ); ?></p> </div> <input type="hidden" name="action" value="sharing_new_service" /> <input type="hidden" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( 'sharing-new_service' ) ); ?>" /> </form> </div> <?php } /** * Display sharing block admin UI for settings. * * @return void */ public function sharing_block_display() { $showcase_services = array( new Share_Tumblr( 'tumblr', array() ), new Share_Facebook( 'facebook', array() ), new Share_Email( 'email', array() ), new Share_Reddit( 'reddit', array() ), ); global $submenu; // Hide the link to Jetpack Sharing settings if no Jetpack Settings found in submenu list $show_jetpack_admin_settings_link = array_reduce( $submenu['jetpack'], function ( $carry, $item ) { return $carry || ( isset( $item[2] ) && $item[2] === 'jetpack#/settings' ); }, false ); ?> <div class="share_manage_options"> <br class="clearing" /> <h2><?php esc_html_e( 'Sharing Buttons', 'jetpack' ); ?></h2> <div class="sharing-block-message__items-wrapper"> <div> <p><?php esc_html_e( 'Add sharing buttons to your blog and allow your visitors to share posts with their friends.', 'jetpack' ); ?></p> <?php $this->site_editor_prompt_display(); ?> </div> <div> <p><?php esc_html_e( 'Sharing Buttons example:', 'jetpack' ); ?></p> <div class="sharedaddy sd-sharing-enabled"> <div class="sd-content"> <ul class="preview"> <?php foreach ( $showcase_services as $service ) : ?> <?php $this->output_preview( $service ); ?> <?php endforeach; ?> </ul> </div> </div> </div> <?php if ( $show_jetpack_admin_settings_link ) : ?> <p class="settings-sharing__block-theme-description"> <?php printf( wp_kses( /* translators: Link to Jetpack sharing settings. */ __( 'You are using a block-based theme. You can <a class="dops-card__link" href="%s">disable Jetpack’s legacy sharing buttons</a> and add a sharing block to your theme’s template instead.', 'jetpack' ), array( 'a' => array( 'href' => array() ), ) ), esc_url( admin_url( 'admin.php?page=jetpack#/sharing' ) ) ); ?> </p> <?php endif; ?> </div> <br class="clearing" /> </div> <?php } /** * Display the "Go to the site editor" prompt. * * @return void */ public function site_editor_prompt_display() { $host = new Status\Host(); $wpcom_link = 'https://wordpress.com/support/wordpress-editor/blocks/sharing-buttons-block/'; if ( function_exists( 'localized_wpcom_url' ) ) { $wpcom_link = localized_wpcom_url( $wpcom_link ); } $link = $host->is_wpcom_platform() ? $wpcom_link : Redirect::get_url( 'jetpack-support-sharing-block' ); ?> <div class="sharing-block-message__buttons-wrapper"> <a href="<?php echo esc_url( admin_url( 'site-editor.php?path=%2Fwp_template' ) ); ?>" class="button button-primary"> <?php esc_html_e( 'Go to the site editor', 'jetpack' ); ?> </a> <a data-target="wpcom-help-center" href="<?php echo esc_url( $link ); ?>" class="button" target="_blank" rel="noopener noreferrer"> <?php esc_html_e( 'Learn how to add Sharing Buttons', 'jetpack' ); ?> </a> </div> <?php } } /** * Callback to get the value for the jetpack_sharing_enabled field. * * When the sharing_disabled post_meta is unset, we follow the global setting in Sharing. * When it is set to 1, we disable sharing on the post, regardless of the global setting. * It is not possible to enable sharing on a post if it is disabled globally. * * @param array $post The post object. * * @return bool */ function jetpack_post_sharing_get_value( array $post ) { if ( ! isset( $post['id'] ) ) { return false; } // if sharing IS disabled on this post, enabled=false, so negate the meta return ! get_post_meta( $post['id'], 'sharing_disabled', true ); } /** * Callback to set sharing_disabled post_meta when the * jetpack_sharing_enabled field is updated. * * When the sharing_disabled post_meta is unset, we follow the global setting in Sharing. * When it is set to 1, we disable sharing on the post, regardless of the global setting. * It is not possible to enable sharing on a post if it is disabled globally. * * @param bool $enable_sharing Should sharing be enabled on this post. * @param WP_Post $post_object The post object. * * @return int|bool */ function jetpack_post_sharing_update_value( $enable_sharing, $post_object ) { if ( $enable_sharing ) { // delete the override if we want to enable sharing return delete_post_meta( $post_object->ID, 'sharing_disabled' ); } else { return update_post_meta( $post_object->ID, 'sharing_disabled', true ); } } /** * Add Sharing post_meta to the REST API Post response. * * @action rest_api_init * @uses register_rest_field * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/ */ function jetpack_post_sharing_register_rest_field() { $post_types = get_post_types( array( 'public' => true ) ); foreach ( $post_types as $post_type ) { register_rest_field( $post_type, 'jetpack_sharing_enabled', array( 'get_callback' => 'jetpack_post_sharing_get_value', 'update_callback' => 'jetpack_post_sharing_update_value', 'schema' => array( 'description' => __( 'Are sharing buttons enabled?', 'jetpack' ), 'type' => 'boolean', ), ) ); /** * Ensures all public internal post-types support `sharing` * This feature support flag is used by the REST API and Gutenberg. */ add_post_type_support( $post_type, 'jetpack-sharing-buttons' ); } } // Add Sharing post_meta to the REST API Post response. add_action( 'rest_api_init', 'jetpack_post_sharing_register_rest_field' ); // Some CPTs (e.g. Jetpack portfolios and testimonials) get registered with // restapi_theme_init because they depend on theme support, so let's also hook to that add_action( 'restapi_theme_init', 'jetpack_post_likes_register_rest_field', 20 ); /** * Initialize sharing settings in WP Admin. * * @return void */ function sharing_admin_init() { global $sharing_admin; $sharing_admin = new Sharing_Admin(); } add_action( 'init', 'sharing_admin_init' ); sharedaddy/recaptcha.php 0000644 00000014604 15174711637 0011340 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Google reCAPTCHA utilities, for use in the sharing feature. * * @package automattic/jetpack */ if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } /** * Class that handles reCAPTCHA. * * @deprecated 11.0 */ class Jetpack_ReCaptcha { /** * URL to which requests are POSTed. * * @const string */ const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'; /** * Site key to use in HTML code. * * @var string */ private $site_key; /** * Shared secret for the site. * * @var string */ private $secret_key; /** * Config for reCAPTCHA instance. * * @var array */ private $config; /** * Error codes returned from reCAPTCHA API. * * @see https://developers.google.com/recaptcha/docs/verify * * @var array */ private $error_codes; /** * Create a configured instance to use the reCAPTCHA service. * * @param string $site_key Site key to use in HTML code. * @param string $secret_key Shared secret between site and reCAPTCHA server. * @param array $config Config array to optionally configure reCAPTCHA instance. */ public function __construct( $site_key, $secret_key, $config = array() ) { $this->site_key = $site_key; $this->secret_key = $secret_key; $this->config = wp_parse_args( $config, $this->get_default_config() ); $this->error_codes = array( 'missing-input-secret' => __( 'The secret parameter is missing', 'jetpack' ), 'invalid-input-secret' => __( 'The secret parameter is invalid or malformed', 'jetpack' ), 'missing-input-response' => __( 'The response parameter is missing', 'jetpack' ), 'invalid-input-response' => __( 'The response parameter is invalid or malformed', 'jetpack' ), 'invalid-json' => __( 'Invalid JSON', 'jetpack' ), 'unexpected-response' => __( 'Unexpected response', 'jetpack' ), 'unexpected-hostname' => __( 'Unexpected hostname', 'jetpack' ), ); } /** * Get default config for this reCAPTCHA instance. * * @return array Default config */ public function get_default_config() { return array( 'language' => get_locale(), 'script_async' => false, 'script_defer' => true, 'script_lazy' => false, 'tag_class' => 'g-recaptcha', 'tag_attributes' => array( 'theme' => 'light', 'type' => 'image', 'tabindex' => 0, ), ); } /** * Calls the reCAPTCHA siteverify API to verify whether the user passes * CAPTCHA test. * * @param string $response The value of 'g-recaptcha-response' in the submitted * form. * @param string $remote_ip The end user's IP address. * * @return bool|WP_Error Returns true if verified. Otherwise WP_Error is returned. */ public function verify( $response, $remote_ip ) { // No need make a request if response is empty. if ( empty( $response ) ) { return new WP_Error( 'missing-input-response', $this->error_codes['missing-input-response'], 400 ); } $resp = wp_remote_post( self::VERIFY_URL, $this->get_verify_request_params( $response, $remote_ip ) ); if ( is_wp_error( $resp ) ) { return $resp; } $resp_decoded = json_decode( wp_remote_retrieve_body( $resp ), true ); if ( ! $resp_decoded ) { return new WP_Error( 'invalid-json', $this->error_codes['invalid-json'], 400 ); } // Default error code and message. $error_code = 'unexpected-response'; $error_message = $this->error_codes['unexpected-response']; // Use the first error code if exists. if ( isset( $resp_decoded['error-codes'] ) && is_array( $resp_decoded['error-codes'] ) ) { if ( isset( $resp_decoded['error-codes'][0] ) && isset( $this->error_codes[ $resp_decoded['error-codes'][0] ] ) ) { $error_message = $this->error_codes[ $resp_decoded['error-codes'][0] ]; $error_code = $resp_decoded['error-codes'][0]; } } if ( ! isset( $resp_decoded['success'] ) ) { return new WP_Error( $error_code, $error_message ); } if ( true !== $resp_decoded['success'] ) { return new WP_Error( $error_code, $error_message ); } // Validate the hostname matches expected source if ( isset( $resp_decoded['hostname'] ) ) { $url = wp_parse_url( get_home_url() ); /** * Allow other valid hostnames. * * This can be useful in cases where the token hostname is expected to be * different from the get_home_url (ex. AMP recaptcha token contains a different hostname) * * @module sharedaddy * * @since 9.1.0 * * @param array [ $url['host'] ] List of the valid hostnames to check against. */ $valid_hostnames = apply_filters( 'jetpack_recaptcha_valid_hostnames', array( $url['host'] ) ); if ( ! in_array( $resp_decoded['hostname'], $valid_hostnames, true ) ) { return new WP_Error( 'unexpected-host', $this->error_codes['unexpected-hostname'] ); } } return true; } /** * Get siteverify request parameters. * * @param string $response The value of 'g-recaptcha-response' in the submitted * form. * @param string $remote_ip The end user's IP address. * * @return array */ public function get_verify_request_params( $response, $remote_ip ) { return array( 'body' => array( 'secret' => $this->secret_key, 'response' => $response, 'remoteip' => $remote_ip, ), 'sslverify' => true, ); } /** * Get reCAPTCHA HTML to render. * * @return string */ public function get_recaptcha_html() { $url = sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s', rawurlencode( $this->config['language'] ) ); $html = sprintf( ' <div class="%s" data-sitekey="%s" data-theme="%s" data-type="%s" data-tabindex="%s" data-lazy="%s" data-url="%s"></div> ', esc_attr( $this->config['tag_class'] ), esc_attr( $this->site_key ), esc_attr( $this->config['tag_attributes']['theme'] ), esc_attr( $this->config['tag_attributes']['type'] ), esc_attr( $this->config['tag_attributes']['tabindex'] ), $this->config['script_lazy'] ? 'true' : 'false', esc_attr( $url ) ); if ( ! $this->config['script_lazy'] ) { $html = $html . sprintf( // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript '<script src="%s"%s%s></script> ', $url, $this->config['script_async'] && ! $this->config['script_defer'] ? ' async' : '', $this->config['script_defer'] ? ' defer' : '' ); } return $html; } } sharedaddy/admin-sharing.min.css 0000644 00000016454 15174711637 0012717 0 ustar 00 #services-config{float:left;min-width:700px;width:100%}#services-config h3{color:#464646;font-size:15px;font-weight:400;margin:0;overflow:hidden;padding:8px 10px;white-space:nowrap}#available-services,#enabled-services,#live-preview{border-spacing:0;padding:20px 0 0;width:100%}#enabled-services .ui-sortable{min-height:50px}#enabled-services{padding-bottom:20px}#available-services,#enabled-services{border-bottom:2px solid #ccc}#live-preview{border-bottom:1px solid #dcdcde;padding-bottom:60px}#available-services h3,#enabled-services h3,#live-preview h3{margin-bottom:1em;margin-top:0;padding:0}body.settings_page_sharing .description{vertical-align:top;width:180px}body.settings_page_sharing .description p{font-size:13px;font-style:italic}body.settings_page_sharing .services{padding:0 20px;vertical-align:top}body.settings_page_sharing .services ul li{cursor:move}body.settings_page_sharing .services ul li.divider{background:none;border:none;cursor:default;padding:0}body.settings_page_sharing ul.services-hidden{margin-bottom:0}#available-services .service,#enabled-services .service{background:#fff;border-radius:4px;box-shadow:0 1px 2px #0000001f,0 0 0 1px #0000001f;color:#2c3338!important;display:inline-block;font-family:Open Sans,sans-serif;font-size:13px;font-weight:500;line-height:23px;margin:0 8px 8px 0;padding:3px 11px 4px 9px}#available-services .service .options-left,#enabled-services .service .options-left{align-items:center;display:flex}#available-services .service:hover,#enabled-services .service:hover{box-shadow:0 1px 2px #00000038,0 0 0 1px #00000038}#available-services .service.share-deprecated,#enabled-services .service.share-deprecated{opacity:.5;padding:5px;text-decoration:line-through}#available-services .service.share-deprecated{display:none}li.service span:before{-webkit-font-smoothing:antialiased;font:400 18px/1 social-logos;height:16px;margin-right:6px;position:relative;top:-1px;width:16px}li.service.share-print span:before{content:"\f469"}li.service.share-digg span:before{content:"\f221"}li.service.share-email span:before{content:"\f410"}li.service.share-linkedin span:before{content:"\f207"}li.service.share-twitter span:before,li.service.share-x span:before{content:"\f10e"}li.service.share-reddit span:before{content:"\f222"}li.service.share-tumblr span:before{content:"\f214"}li.service.share-pinterest span:before{content:"\f209"}li.service.share-facebook span:before{content:"\f203"}li.service.share-press-this span:before{content:"\f205"}li.service.share-telegram span:before{content:"\f606"}li.service.share-threads span:before{content:"\f10d"}li.service.share-jetpack-whatsapp span:before{content:"\f608"}li.service.share-mastodon span:before{content:"\f10a"}li.service.share-nextdoor span:before{content:"\f10c"}li.service.share-bluesky span:before{content:"\f10f"}body.settings_page_sharing ul.preview{display:flex;flex-wrap:wrap;float:left}body.settings_page_sharing ul.preview li.preview-item,body.settings_page_sharing ul.preview li.preview-item a{cursor:default;text-decoration:none}div.sd-social-icon .inner li.preview-item a span,div.sd-social-icon ul.preview li.preview-item a span{display:none}div.sd-social-icon ul.preview li.preview-item.preview-custom a span{display:inline-block}.services .preview li.share-custom a{text-decoration:none}.services ul li.end-fix{clear:both;float:none;height:20px;margin:0;padding:0;visibility:hidden;width:0}#enabled-services h2{color:#999;font-size:20px;font-weight:400!important;padding-top:0}body.settings_page_sharing #live-preview h2{color:#e3e3e3;font-size:20px;font-weight:400!important}body.settings_page_sharing .clearing{clear:both}body.settings_page_sharing .options .options-left{float:left}body.settings_page_sharing .input label{font-size:11px;line-height:16px}body.settings_page_sharing .advanced-form{border-top:1px solid #e3e3e3;display:none;margin-left:-24px;margin-top:4px;padding:10px 14px 8px 10px}body.settings_page_sharing .utility{float:right;font-size:10px;padding-right:10px;padding-top:10px}body.settings_page_sharing .advanced input[type=submit]{float:left;margin-right:10px;margin-top:10px}.services li.dropzone{background:#e3e3e3;border:1px dashed #999;border-radius:3px;height:18px;margin-right:10px;padding:5px}.advanced-form .form-table th{width:auto!important}.advanced-form .button-secondary{margin-top:0!important}#hidden-drop-target{background:#e1e1e1;border:1px solid #cdcdcd;padding:10px;vertical-align:top;width:29%}#hidden-drop-target p{font-size:13px;font-style:italic;margin:0 0 10px}.preview li.preview-item{background-position:0 5px;cursor:default}.preview .option-smart-on{margin:3px 5px 0 0}.preview-digg .option-smart-on{background:url(../../modules/sharedaddy/images/smart-digg.png) no-repeat 0 0;background-size:76px 17px;height:17px;margin-top:2px;width:76px}.preview-facebook .option-smart-on{background:url(../../modules/sharedaddy/images/smart-like.png) no-repeat 0 0;background-size:85px 20px;height:20px;margin-left:-4px;width:85px}.preview-twitter .option-smart-on,.preview-x .option-smart-on{background:url(../../modules/sharedaddy/images/smart-x.png) no-repeat 0 0;background-size:60px 20px;height:20px;width:60px}.preview-linkedin .option-smart-on{background:url(../../modules/sharedaddy/images/linkedin-smart.png) no-repeat top;background-size:99px 18px;height:20px;width:99px}.preview-tumblr .option-smart-on{background:url(../../modules/sharedaddy/images/smart-tumblr.png) no-repeat 0 0;background-size:62px 20px;height:20px;width:62px}.preview-pinterest .option-smart-on{background:url(../../modules/sharedaddy/images/smart-pinterest.png) no-repeat 0 0;background-size:39px 20px;height:20px;width:39px}.preview-item.share-deprecated{opacity:.5}.preview-item.share-deprecated a span{text-decoration:line-through}@media (min-resolution:120dpi){.preview-digg .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-digg@2x.png)}.preview-facebook .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-like@2x.png)}.preview-twitter .option-smart-on,.preview-x .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-x@2x.png)}.preview-linkedin .option-smart-on{background-image:url(../../modules/sharedaddy/images/linkedin-smart@2x.png)}.preview-tumblr .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-tumblr@2x.png)}.preview-pinterest .option-smart-on{background-image:url(../../modules/sharedaddy/images/smart-pinterest@2x.png)}}.services .sharing-hidden li{background-color:initial}.sharing-hidden li.share-end{clear:both;float:none;height:0;margin:0!important;padding:0!important;visibility:hidden;width:0}.preview .sharing-label{border:0;font-weight:700;padding:4px 6px 0 0}#services-config a.remove{background:#dcdcde;border-radius:15px;color:#fff;font-size:10px;font-weight:700;padding:0 4px 2px;text-decoration:none}#services-config a.remove:hover{background:red}.sd-social-icon .inner a.sd-button span,.sd-social-icon .inner a.share-icon span{display:inline-block;overflow:hidden;text-indent:100%;width:0}.sharing-block-message__items-wrapper{column-gap:6rem;display:flex;flex-wrap:wrap}.sharing-block-message__buttons-wrapper .button{margin-right:1rem}.sharing-block-message__buttons-wrapper .button-primary:visited{color:#fff}.admin-sharing-settings__block-theme-description{font-style:italic;margin-top:1rem}a.dops-card__link{cursor:pointer}.settings-sharing__block-theme-description{font-style:italic} sharedaddy/sharing-service.php 0000644 00000104715 15174711637 0012502 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Utilities to register and interact with a sharing service. * * Sharing_Service gets info about a service. * Sharing_Service_Total and Sharing_Post_Total get stats data. * * @package automattic/jetpack * * phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound */ // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files. use Automattic\Jetpack\Assets; use Automattic\Jetpack\Sync\Settings; if ( ! defined( 'ABSPATH' ) ) { exit( 0 ); } require_once __DIR__ . '/sharing-sources.php'; define( 'WP_SHARING_PLUGIN_VERSION', JETPACK__VERSION ); /** * Interact with a sharing service. */ class Sharing_Service { /** * Should the service be available globally? * * @var bool */ private $global = false; /** * Default sharing label. * * @var string */ public $default_sharing_label = ''; /** * Initialize the sharing service. * Only run this method once upon module loading. * * @return void */ public static function init() { add_filter( 'the_content', 'sharing_display', 19 ); add_filter( 'the_excerpt', 'sharing_display', 19 ); } /** * Constructor. */ public function __construct() { $this->default_sharing_label = __( 'Share this:', 'jetpack' ); } /** * Gets a generic list of all services, without any config * * @return array */ public function get_all_services_blog() { $options = get_option( 'sharing-options' ); $all = $this->get_all_services(); $services = array(); foreach ( $all as $id => $name ) { if ( isset( $all[ $id ] ) ) { $config = array(); // Pre-load custom modules otherwise they won't know who they are if ( str_starts_with( $id, 'custom-' ) && is_array( $options[ $id ] ) ) { $config = $options[ $id ]; } $services[ $id ] = new $all[ $id ]( $id, $config ); } } return $services; } /** * Gets a list of all available service names and classes * * @param bool $include_custom Include custom sharing services. * * @return array */ public function get_all_services( $include_custom = true ) { // Default services // if you update this list, please update the REST API tests // in bin/tests/api/suites/SharingTest.php $services = array( 'print' => 'Share_Print', 'email' => 'Share_Email', 'facebook' => 'Share_Facebook', 'linkedin' => 'Share_LinkedIn', 'reddit' => 'Share_Reddit', 'twitter' => 'Share_Twitter', 'tumblr' => 'Share_Tumblr', 'pinterest' => 'Share_Pinterest', 'telegram' => 'Share_Telegram', 'threads' => 'Share_Threads', 'jetpack-whatsapp' => 'Jetpack_Share_WhatsApp', 'mastodon' => 'Share_Mastodon', 'nextdoor' => 'Share_Nextdoor', 'x' => 'Share_X', 'bluesky' => 'Share_Bluesky', ); if ( is_multisite() && is_plugin_active( 'press-this/press-this-plugin.php' ) ) { $services['press-this'] = 'Share_PressThis'; } if ( $include_custom ) { // Add any custom services in $options = $this->get_global_options(); if ( isset( $options['custom'] ) ) { foreach ( $options['custom'] as $custom_id ) { $services[ $custom_id ] = 'Share_Custom'; } } } /** * Filters the list of available Sharing Services. * * @module sharedaddy * * @since 1.1.0 * * @param array $services Array of all available Sharing Services. */ return apply_filters( 'sharing_services', $services ); } /** * Save a new custom sharing service. * * @param string $label Service name. * @param string $url Service sharing URL. * @param string $icon Service icon. * * @return bool|Share_Custom */ public function new_service( $label, $url, $icon ) { // Validate. $label = trim( wp_html_excerpt( wp_kses( $label, array() ), 30 ) ); $url = trim( esc_url_raw( $url ) ); $icon = trim( esc_url_raw( $icon ) ); if ( $label && $url && $icon ) { $options = get_option( 'sharing-options' ); if ( ! is_array( $options ) ) { $options = array(); } $service_id = 'custom-' . time(); // Add a new custom service $options['global']['custom'][] = $service_id; if ( false !== $this->global ) { $this->global['custom'][] = $service_id; } update_option( 'sharing-options', $options ); // Create a custom service and set the options for it $service = new Share_Custom( $service_id, array( 'name' => $label, 'url' => $url, 'icon' => $icon, ) ); $this->set_service( $service_id, $service ); // Return the service return $service; } return false; } /** * Delete a sharing service. * * @param string $service_id Service ID. * * @return bool */ public function delete_service( $service_id ) { $options = get_option( 'sharing-options' ); if ( isset( $options[ $service_id ] ) ) { unset( $options[ $service_id ] ); } $key = array_search( $service_id, $options['global']['custom'], true ); if ( $key !== false ) { unset( $options['global']['custom'][ $key ] ); } update_option( 'sharing-options', $options ); return true; } /** * Save enabled sharing services. * * @param array $visible Visible sharing services. * @param array $hidden Hidden sharing services (available under a dropdown). * * @return bool */ public function set_blog_services( array $visible, array $hidden ) { $services = $this->get_all_services(); // Validate the services $available = array_keys( $services ); // Only allow services that we have defined $hidden = array_intersect( $hidden, $available ); $visible = array_intersect( $visible, $available ); // Ensure we don't have the same ones in hidden and visible $hidden = array_diff( $hidden, $visible ); /** * Control the state of the list of sharing services. * * @module sharedaddy * * @since 1.1.0 * * @param array $args { * Array of options describing the state of the sharing services. * * @type array $services List of all available service names and classes. * @type array $available Validated list of all available service names and classes. * @type array $hidden List of services hidden behind a "More" button. * @type array $visible List of visible services. * @type array $this->get_blog_services() Array of Sharing Services currently enabled. * } */ do_action( 'sharing_get_services_state', array( 'services' => $services, 'available' => $available, 'hidden' => $hidden, 'visible' => $visible, 'currently_enabled' => $this->get_blog_services(), ) ); return update_option( 'sharing-services', array( 'visible' => $visible, 'hidden' => $hidden, ) ); } /** * Get information about enabled sharing services on the site. * * @return array */ public function get_blog_services() { $options = get_option( 'sharing-options' ); $enabled = get_option( 'sharing-services' ); $services = $this->get_all_services(); /** * Check if options exist and are well formatted. * This avoids issues on sites with corrupted options. * * @see https://github.com/Automattic/jetpack/issues/6121 */ if ( ! is_array( $options ) || ! isset( $options['button_style'] ) || ! isset( $options['global'] ) ) { $global_options = array( 'global' => $this->get_global_options() ); $options = is_array( $options ) ? array_merge( $options, $global_options ) : $global_options; } $global = $options['global']; // Default services if ( ! is_array( $enabled ) ) { $enabled = array( 'visible' => array( 'facebook', 'x', ), 'hidden' => array(), ); /** * Filters the list of default Sharing Services. * * @module sharedaddy * * @since 1.1.0 * * @param array $enabled Array of default Sharing Services. */ $enabled = apply_filters( 'sharing_default_services', $enabled ); } // Cleanup after any filters that may have produced duplicate services if ( isset( $enabled['visible'] ) && is_array( $enabled['visible'] ) ) { $enabled['visible'] = array_unique( $enabled['visible'] ); } else { $enabled['visible'] = array(); } if ( isset( $enabled['hidden'] ) && is_array( $enabled['hidden'] ) ) { $enabled['hidden'] = array_unique( $enabled['hidden'] ); } else { $enabled['hidden'] = array(); } // Form the enabled services $blog = array( 'visible' => array(), 'hidden' => array(), ); foreach ( $blog as $area => $stuff ) { foreach ( (array) $enabled[ $area ] as $service ) { if ( isset( $services[ $service ] ) ) { if ( ! isset( $options[ $service ] ) || ! is_array( $options[ $service ] ) ) { $options[ $service ] = array(); } $blog[ $area ][ $service ] = new $services[ $service ]( $service, array_merge( $global, $options[ $service ] ) ); } } } /** * Filters the list of enabled Sharing Services. * * @module sharedaddy * * @since 1.1.0 * * @param array $blog Array of enabled Sharing Services. */ $blog = apply_filters( 'sharing_services_enabled', $blog ); // Add CSS for NASCAR if ( ( is_countable( $blog['visible'] ) && count( $blog['visible'] ) ) || ( is_countable( $blog['hidden'] ) && count( $blog['hidden'] ) ) ) { add_filter( 'post_flair_block_css', 'post_flair_service_enabled_sharing' ); } // Convenience for checking if a service is present $blog['all'] = array_flip( array_merge( array_keys( $blog['visible'] ), array_keys( $blog['hidden'] ) ) ); return $blog; } /** * Get information about a specific enabled sharing service. * * @param string $service_name Service name. * * @return bool|Sharing_Source */ public function get_service( $service_name ) { $services = $this->get_blog_services(); if ( isset( $services['visible'][ $service_name ] ) ) { return $services['visible'][ $service_name ]; } if ( isset( $services['hidden'][ $service_name ] ) ) { return $services['hidden'][ $service_name ]; } return false; } /** * Update global sharing options. * * @param array $data Array of new sharing options to save. */ public function set_global_options( $data ) { $options = get_option( 'sharing-options' ); // No options yet. if ( ! is_array( $options ) ) { $options = array(); } // Defaults. $options['global'] = array( 'button_style' => 'icon-text', 'sharing_label' => $this->default_sharing_label, 'open_links' => 'same', 'show' => ! isset( $options['global'] ) ? array( 'post', 'page' ) : array(), 'custom' => isset( $options['global']['custom'] ) ? $options['global']['custom'] : array(), ); /** * Filters global sharing settings. * * @module sharedaddy * * @since 1.1.0 * * @param array $options['global'] Array of global sharing settings. */ $options['global'] = apply_filters( 'sharing_default_global', $options['global'] ); // Validate options and set from our data if ( isset( $data['button_style'] ) && in_array( $data['button_style'], array( 'icon-text', 'icon', 'text', 'official' ), true ) ) { $options['global']['button_style'] = $data['button_style']; } if ( isset( $data['sharing_label'] ) ) { if ( $this->default_sharing_label === $data['sharing_label'] ) { $options['global']['sharing_label'] = false; } else { $options['global']['sharing_label'] = trim( wp_kses( stripslashes( $data['sharing_label'] ), array() ) ); } } if ( isset( $data['open_links'] ) && in_array( $data['open_links'], array( 'new', 'same' ), true ) ) { $options['global']['open_links'] = $data['open_links']; } $shows = array_values( get_post_types( array( 'public' => true ) ) ); $shows[] = 'index'; if ( isset( $data['show'] ) ) { if ( is_scalar( $data['show'] ) ) { switch ( $data['show'] ) { case 'posts': $data['show'] = array( 'post', 'page' ); break; case 'index': $data['show'] = array( 'index' ); break; case 'posts-index': $data['show'] = array( 'post', 'page', 'index' ); break; } } $data['show'] = array_intersect( $data['show'], $shows ); if ( $data['show'] ) { $options['global']['show'] = $data['show']; } } update_option( 'sharing-options', $options ); return $options['global']; } /** * Get global sharing options for the site. * * @return array */ public function get_global_options() { if ( $this->global === false ) { $options = get_option( 'sharing-options' ); if ( is_array( $options ) && isset( $options['global'] ) && is_array( $options['global'] ) ) { $this->global = $options['global']; } else { $this->global = $this->set_global_options( $options ); } } if ( ! isset( $this->global['show'] ) ) { $this->global['show'] = array( 'post', 'page' ); } elseif ( is_scalar( $this->global['show'] ) ) { switch ( $this->global['show'] ) { case 'posts': $this->global['show'] = array( 'post', 'page' ); break; case 'index': $this->global['show'] = array( 'index' ); break; case 'posts-index': $this->global['show'] = array( 'post', 'page', 'index' ); break; } } if ( ! isset( $this->global['sharing_label'] ) || false === $this->global['sharing_label'] || $this->global['sharing_label'] === 'Share this:' ) { $this->global['sharing_label'] = $this->default_sharing_label; } return $this->global; } /** * Save a sharing service for use. * * @param int $id Sharing unique ID. * @param Sharing_Advanced_Source $service Sharing service. * * @return void */ public function set_service( $id, Sharing_Advanced_Source $service ) { // Update the options for this service $options = get_option( 'sharing-options' ); // No options yet if ( ! is_array( $options ) ) { $options = array(); } /** * Get the state of a sharing button. * * @module sharedaddy * * @since 1.1.0 * * @param array $args { * State of a sharing button. * * @type string $id Service ID. * @type array $options Array of all sharing options. * @type array $service Details about a service. * } */ do_action( 'sharing_get_button_state', array( 'id' => $id, 'options' => $options, 'service' => $service, ) ); $options[ $id ] = $service->get_options(); update_option( 'sharing-options', array_filter( $options ) ); } /** * Get stats for a site, a post, or a sharing service. * Soon to come to a .org plugin near you! * * @param string|bool $service_name Service name. * @param int|bool $post_id Post ID. * @param int|bool $_blog_id Blog ID. * * @return int */ public function get_total( $service_name = false, $post_id = false, $_blog_id = false ) { global $wpdb, $blog_id; if ( ! $_blog_id ) { $_blog_id = $blog_id; } if ( $service_name === false ) { if ( $post_id > 0 ) { // total number of shares for this post $sql = $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND post_id = %d', $_blog_id, $post_id ); $cache_key = "sharing_service_get_total_b{$_blog_id}_p{$post_id}"; } else { // total number of shares for this blog $sql = $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d', $_blog_id ); $cache_key = "sharing_service_get_total_b{$_blog_id}"; } } elseif ( $post_id > 0 ) { $sql = $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND post_id = %d AND share_service = %s', $_blog_id, $post_id, $service_name ); $cache_key = "sharing_service_get_total_b{$_blog_id}_p{$post_id}_s{$service_name}"; } else { $sql = $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND share_service = %s', $_blog_id, $service_name ); $cache_key = "sharing_service_get_total_b{$_blog_id}_s{$service_name}"; } $ret = wp_cache_get( $cache_key, 'sharing' ); if ( $ret === false ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery -- Prepared above. $ret = (int) $wpdb->get_var( $sql ); wp_cache_set( $cache_key, $ret, 'sharing', 5 * MINUTE_IN_SECONDS ); } return $ret; } /** * Get total stats for a site, for all sharing services. * * @param int|bool $post_id Post ID. * * @return array */ public function get_services_total( $post_id = false ) { $totals = array(); $services = $this->get_blog_services(); if ( ! empty( $services ) && isset( $services['all'] ) ) { foreach ( $services['all'] as $key => $value ) { $totals[ $key ] = new Sharing_Service_Total( $key, $this->get_total( $key, $post_id ) ); } } usort( $totals, array( 'Sharing_Service_Total', 'cmp' ) ); return $totals; } /** * Get sharing stats for all posts on the site. * * @return array */ public function get_posts_total() { $totals = array(); global $wpdb, $blog_id; $cache_key = "sharing_service_get_posts_total_{$blog_id}"; $my_data = wp_cache_get( $cache_key, 'sharing' ); if ( $my_data === false ) { $my_data = $wpdb->get_results( $wpdb->prepare( 'SELECT post_id as id, SUM( count ) as total FROM sharing_stats WHERE blog_id = %d GROUP BY post_id ORDER BY count DESC ', $blog_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery wp_cache_set( $cache_key, $my_data, 'sharing', 5 * MINUTE_IN_SECONDS ); } if ( ! empty( $my_data ) ) { foreach ( $my_data as $row ) { $totals[] = new Sharing_Post_Total( $row->id, $row->total ); } } usort( $totals, array( 'Sharing_Post_Total', 'cmp' ) ); return $totals; } } /** * Get stats for a specific sharing service. */ class Sharing_Service_Total { /** * Sharing service ID. * * @var int */ public $id = ''; /** * Service name. * * @var string */ public $name = ''; /** * Sharing service name. * * @var string */ public $service = ''; /** * Total number of shares for this service. * * @var string */ public $total = 0; /** * Constructor. * * @param int $id Service ID. * @param int $total Total shares. */ public function __construct( $id, $total ) { $services = new Sharing_Service(); $this->id = esc_html( $id ); $this->service = $services->get_service( $id ); $this->total = (int) $total; if ( $this->service instanceof Sharing_Source ) { $this->name = $this->service->get_name(); } } /** * Compare total shares between 2 posts. * * @param object $a Sharing_Service_Total object. * @param object $b Sharing_Service_Total object. * * @return int -1, 0, or 1 if $a is <, =, or > $b */ public static function cmp( $a, $b ) { if ( $a->total === $b->total ) { return $b->name <=> $a->name; } return $b->total <=> $a->total; } } /** * Get sharing stats for a specific post. */ class Sharing_Post_Total { /** * Sharing service ID. * * @var int */ public $id = 0; /** * Total shares. * * @var int */ public $total = 0; /** * Post title. * * @var string */ public $title = ''; /** * Post permalink. * * @var string */ public $url = ''; /** * Constructor. * * @param int $id Service ID. * @param int $total Total shares. */ public function __construct( $id, $total ) { $this->id = (int) $id; $this->total = (int) $total; $this->title = get_the_title( $this->id ); $this->url = get_permalink( $this->id ); } /** * Compare total shares between 2 posts. * * @param object $a Sharing_Post_Total object. * @param object $b Sharing_Post_Total object. * * @return int -1, 0, or 1 if $a is <, =, or > $b */ public static function cmp( $a, $b ) { if ( $a->total === $b->total ) { return $b->id <=> $a->id; } return $b->total <=> $a->total; } } /** * Populate sharing counts global with a post we want to count shares for. * * @param int $post_id Post ID. * * @return void */ function sharing_register_post_for_share_counts( $post_id ) { global $jetpack_sharing_counts; if ( ! isset( $jetpack_sharing_counts ) || ! is_array( $jetpack_sharing_counts ) ) { $jetpack_sharing_counts = array(); } $jetpack_sharing_counts[ (int) $post_id ] = get_permalink( $post_id ); } /** * Determine whether we should load sharing scripts or not. * * @return bool */ function sharing_maybe_enqueue_scripts() { $sharer = new Sharing_Service(); $global_options = $sharer->get_global_options(); $enqueue = false; if ( is_singular() && in_array( get_post_type(), $global_options['show'], true ) ) { $enqueue = true; } elseif ( in_array( 'index', $global_options['show'], true ) && ( is_home() || is_front_page() || is_archive() || is_search() || in_array( get_post_type(), $global_options['show'], true ) ) ) { $enqueue = true; } /** * Filter to decide when sharing scripts should be enqueued. * * @module sharedaddy * * @since 3.2.0 * * @param bool $enqueue Decide if the sharing scripts should be enqueued. */ return (bool) apply_filters( 'sharing_enqueue_scripts', $enqueue ); } /** * Add sharing JavaScript to the footer of a page. * * @return void */ function sharing_add_footer() { if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { return; } global $jetpack_sharing_counts; if ( /** * Filter all JavaScript output by the sharing module. * * @module sharedaddy * * @since 1.1.0 * * @param bool true Control whether the sharing module should add any JavaScript to the site. Default to true. */ apply_filters( 'sharing_js', true ) && sharing_maybe_enqueue_scripts() ) { if ( /** * Filter the display of sharing counts next to the sharing buttons. * * @module sharedaddy * * @since 3.2.0 * * @param bool true Control the display of counters next to the sharing buttons. Default to true. */ apply_filters( 'jetpack_sharing_counts', true ) && is_array( $jetpack_sharing_counts ) && count( $jetpack_sharing_counts ) ) : $sharing_post_urls = array_filter( $jetpack_sharing_counts ); if ( $sharing_post_urls ) : ?> <script type="text/javascript"> window.WPCOM_sharing_counts = <?php echo wp_json_encode( array_flip( $sharing_post_urls ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>; </script> <?php endif; endif; wp_enqueue_script( 'sharing-js' ); $sharing_js_options = array( 'lang' => get_base_recaptcha_lang_code(), /** This filter is documented in modules/sharedaddy/sharing-service.php */ 'counts' => apply_filters( 'jetpack_sharing_counts', true ), 'is_stats_active' => Jetpack::is_module_active( 'stats' ), ); wp_localize_script( 'sharing-js', 'sharing_js_options', $sharing_js_options ); } $sharer = new Sharing_Service(); $enabled = $sharer->get_blog_services(); foreach ( array_merge( $enabled['visible'], $enabled['hidden'] ) as $service ) { $service->display_footer(); } } /** * Enqueue sharing CSS in head. * * @return void */ function sharing_add_header() { $sharer = new Sharing_Service(); $enabled = $sharer->get_blog_services(); foreach ( array_merge( $enabled['visible'], $enabled['hidden'] ) as $service ) { $service->display_header(); } if ( is_countable( $enabled['all'] ) && ( count( $enabled['all'] ) > 0 ) && sharing_maybe_enqueue_scripts() ) { wp_enqueue_style( 'sharedaddy', plugin_dir_url( __FILE__ ) . 'sharing.css', array(), JETPACK__VERSION ); wp_enqueue_style( 'social-logos' ); } } add_action( 'wp_head', 'sharing_add_header', 1 ); /** * Launch sharing requests on page load when a specific query string is used. * * @return void */ function sharing_process_requests() { global $post; // Only process if: single post and share=X defined if ( ( is_page() || is_single() ) && isset( $_GET['share'] ) && is_string( $_GET['share'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $sharer = new Sharing_Service(); $service = $sharer->get_service( sanitize_text_field( wp_unslash( $_GET['share'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $service ) { $service->process_request( $post, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } } } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking for the data being present. if ( isset( $_GET['share'] ) ) { add_action( 'template_redirect', 'sharing_process_requests', 9 ); } /** * Gets the url to customise the sharing buttons in WP-Admin. * * @return string the customisation URL. */ function get_sharing_buttons_customisation_url() { return admin_url( 'options-general.php?page=sharing' ); } /** * Append sharing links to text. * * @param string $text The original text to append sharing links onto. * @param bool $echo Where to echo the text or return. * * @return string The original $text with, if conditions are met, the sharing links. */ function sharing_display( $text = '', $echo = false ) { global $post, $wp_current_filter; if ( Settings::is_syncing() ) { return $text; } // We require the post to not be empty and be an actual WordPress post object. If it's not - we just return. if ( empty( $post ) || ! $post instanceof \WP_Post ) { return $text; } if ( ( is_preview() || is_admin() ) && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { return $text; } // Prevent from rendering sharing buttons in block which is fetched from REST endpoint by editor if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return $text; } // Do not output sharing buttons for ActivityPub requests. if ( function_exists( '\Activitypub\is_activitypub_request' ) && \Activitypub\is_activitypub_request() ) { return $text; } // Don't output flair on excerpts. if ( in_array( 'get_the_excerpt', (array) $wp_current_filter, true ) ) { return $text; } // Ensure we don't display sharing buttons on post excerpts that are hooked inside the post content if ( in_array( 'the_excerpt', (array) $wp_current_filter, true ) && in_array( 'the_content', (array) $wp_current_filter, true ) ) { return $text; } // Don't allow flair to be added to the_content more than once (prevent infinite loops). $done = false; foreach ( $wp_current_filter as $filter ) { if ( 'the_content' === $filter ) { if ( $done ) { return $text; } else { $done = true; } } } // check whether we are viewing the front page and whether the front page option is checked. $options = get_option( 'sharing-options' ); $display_options = null; if ( is_array( $options ) ) { $display_options = $options['global']['show']; } if ( is_front_page() && ( is_array( $display_options ) && ! in_array( 'index', $display_options, true ) ) ) { return $text; } if ( is_attachment() && in_array( 'the_excerpt', (array) $wp_current_filter, true ) ) { // Many themes run the_excerpt() conditionally on an attachment page, then run the_content(). // We only want to output the sharing buttons once. Let's stick with the_content(). return $text; } $sharer = new Sharing_Service(); $global = $sharer->get_global_options(); $show = false; if ( ! is_feed() ) { if ( is_singular() && in_array( get_post_type(), $global['show'], true ) ) { $show = true; } elseif ( in_array( 'index', $global['show'], true ) && ( is_home() || is_front_page() || is_archive() || is_search() || in_array( get_post_type(), $global['show'], true ) ) ) { $show = true; } } /** * Filter to decide if sharing buttons should be displayed. * * @module sharedaddy * * @since 1.1.0 * * @param bool $show Should the sharing buttons be displayed. * @param WP_Post $post The post to share. */ $show = apply_filters( 'sharing_show', $show, $post ); // Disabled for this post? $switched_status = get_post_meta( $post->ID, 'sharing_disabled', false ); if ( ! empty( $switched_status ) ) { $show = false; } // Is the post private? $post_status = get_post_status( $post->ID ); if ( 'private' === $post_status ) { $show = false; } // Hide on password protected posts unless password is provided. if ( post_password_required( $post->ID ) ) { $show = false; } /** * Filter the Sharing buttons' Ajax action name Jetpack checks for. * This allows the use of the buttons with your own Ajax implementation. * * @module sharedaddy * * @since 7.3.0 * * @param string $sharing_ajax_action_name Name of the Sharing buttons' Ajax action. */ $ajax_action = apply_filters( 'sharing_ajax_action', 'get_latest_posts' ); // Allow to be used in ajax requests for latest posts. if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_REQUEST['action'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce handling happens within each custom implementation. && $ajax_action === $_REQUEST['action'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce handling happens within each custom implementation. ) { $show = true; } $sharing_content = ''; $enabled = false; if ( $show ) { /** * Filters the list of enabled Sharing Services. * * @module sharedaddy * * @since 2.2.3 * * @param array $sharer->get_blog_services() Array of Sharing Services currently enabled. */ $enabled = apply_filters( 'sharing_enabled', $sharer->get_blog_services() ); if ( is_countable( $enabled['all'] ) && ( count( $enabled['all'] ) > 0 ) ) { $dir = get_option( 'text_direction' ); // Wrapper. $sharing_content .= '<div class="sharedaddy sd-sharing-enabled"><div class="robots-nocontent sd-block sd-social sd-social-' . ( $global['button_style'] ?? 'icon-text' ) . ' sd-sharing">'; if ( '' !== $global['sharing_label'] ) { $sharing_content .= sprintf( /** * Filter the sharing buttons' headline structure. * * @module sharedaddy * * @since 4.4.0 * * @param string $sharing_headline Sharing headline structure. * @param string $global['sharing_label'] Sharing title. * @param string $sharing Module name. */ apply_filters( 'jetpack_sharing_headline_html', '<h3 class="sd-title">%s</h3>', $global['sharing_label'], 'sharing' ), esc_html( $global['sharing_label'] ) ); } $sharing_content .= '<div class="sd-content"><ul>'; // Visible items. $visible = ''; foreach ( $enabled['visible'] as $service ) { $klasses = array( 'share-' . $service->get_class() ); if ( $service->is_deprecated() ) { if ( ! current_user_can( 'manage_options' ) ) { continue; } $klasses[] = 'share-deprecated'; } // Individual HTML for sharing service. $visible .= '<li class="' . implode( ' ', $klasses ) . '">' . $service->get_display( $post ) . '</li>'; } $parts = array(); $parts[] = $visible; $count_hidden = is_countable( $enabled['hidden'] ) ? count( $enabled['hidden'] ) : 0; $count_visible = is_countable( $enabled['visible'] ) ? count( $enabled['visible'] ) : 0; if ( $count_hidden > 0 ) { if ( $count_visible > 0 ) { $expand = __( 'More', 'jetpack' ); } else { $expand = __( 'Share', 'jetpack' ); } $parts[] = '<li><a href="#" class="sharing-anchor sd-button share-more"><span>' . $expand . '</span></a></li>'; } if ( 'rtl' === $dir ) { $parts = array_reverse( $parts ); } $sharing_content .= implode( '', $parts ); $sharing_content .= '<li class="share-end"></li></ul>'; // Link to customization options if user can manage them. if ( current_user_can( 'manage_options' ) ) { $link_url = get_sharing_buttons_customisation_url(); if ( ! empty( $link_url ) ) { $link_text = __( 'Customize buttons', 'jetpack' ); $sharing_content .= '<p class="share-customize-link"><a href="' . esc_url( $link_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $link_text ) . '</a></p>'; } } if ( $count_hidden > 0 ) { $sharing_content .= '<div class="sharing-hidden"><div class="inner" style="display: none;'; if ( $count_hidden === 1 ) { $sharing_content .= 'width:150px;'; } $sharing_content .= '">'; if ( $count_hidden === 1 ) { $sharing_content .= '<ul style="background-image:none;">'; } else { $sharing_content .= '<ul>'; } foreach ( $enabled['hidden'] as $service ) { // Individual HTML for sharing service. $klasses = array( 'share-' . $service->get_class() ); if ( $service->is_deprecated() ) { if ( ! current_user_can( 'manage_options' ) ) { continue; } $klasses[] = 'share-deprecated'; } $sharing_content .= '<li class="' . implode( ' ', $klasses ) . '">'; $sharing_content .= $service->get_display( $post ); $sharing_content .= '</li>'; } // End of wrapper. $sharing_content .= '<li class="share-end"></li></ul></div></div>'; } $sharing_content .= '</div></div></div>'; // Register our JS. if ( defined( 'JETPACK__VERSION' ) ) { $ver = JETPACK__VERSION; } else { $ver = '20211226'; } // @todo: Investigate if we can load this JS in the footer instead. wp_register_script( 'sharing-js', Assets::get_file_url_for_environment( '_inc/build/sharedaddy/sharing.min.js', 'modules/sharedaddy/sharing.js' ), array(), $ver, false ); // Enqueue scripts for the footer. add_action( 'wp_footer', 'sharing_add_footer' ); } } /** * Filters the content markup of the Jetpack sharing links * * @module sharedaddy * * @since 3.8.0 * @since 6.2.0 Started sending $enabled as a second parameter. * * @param string $sharing_content Content markup of the Jetpack sharing links * @param array $enabled Array of Sharing Services currently enabled. */ $sharing_markup = apply_filters( 'jetpack_sharing_display_markup', $sharing_content, $enabled ); if ( $echo ) { echo $text . $sharing_markup; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { return $text . $sharing_markup; } } /** * Get reCAPTCHA language code based off the language code of the site. * * @return string */ function get_base_recaptcha_lang_code() { $base_recaptcha_lang_code_mapping = array( 'en' => 'en', 'nl' => 'nl', 'fr' => 'fr', 'fr-be' => 'fr', 'fr-ca' => 'fr', 'fr-ch' => 'fr', 'de' => 'de', 'pt' => 'pt', 'pt-br' => 'pt', 'ru' => 'ru', 'es' => 'es', 'tr' => 'tr', ); $blog_lang_code = get_bloginfo( 'language' ); if ( isset( $base_recaptcha_lang_code_mapping[ $blog_lang_code ] ) ) { return $base_recaptcha_lang_code_mapping[ $blog_lang_code ]; } // if no base mapping is found return default 'en' return 'en'; } Sharing_Service::init(); sharedaddy/amp-sharing.css 0000644 00000003364 15174711637 0011616 0 ustar 00 amp-social-share, .amp-social-share { color: #fff; border-radius: 50%; position: relative; line-height: 1; width: 32px; height: 32px; margin: 0 5px 5px 0; vertical-align: middle; } amp-social-share::before, .amp-social-share::before { display: inline-block; font: 400 18px/1 social-logos; padding: 7px; position: relative; top: 1px; vertical-align: top; text-align: center; } .amp-social-share.print { background: #e9e9e9; font-size: 0; cursor: pointer; display: inline-block; vertical-align: middle; border: none; padding: 0; } .amp-social-share.print::before { content: "\f469"; color: #656565; } amp-social-share[type="email"] { background: #e9e9e9; color: #656565; } amp-social-share[type="email"]::before { content: "\f410"; } amp-social-share[type="tumblr"] { background: #2c4762; } amp-social-share[type="tumblr"]::before { content: "\f607"; } amp-social-share[type="facebook"] { background: #0866ff; } amp-social-share[type="facebook"]::before { content: "\f203"; } amp-social-share[type="twitter"] { background: #000; } amp-social-share[type="twitter"]::before { content: "\f10e"; } amp-social-share[type="pinterest"] { background: #ca1f27; } amp-social-share[type="pinterest"]::before { content: "\f210"; } amp-social-share[type="telegram"] { background: #08c; } amp-social-share[type="telegram"]::before { content: "\f606"; } amp-social-share[type="linkedin"] { background: #0077b5; } amp-social-share[type="linkedin"]::before { content: "\f207"; } amp-social-share[type="reddit"] { background: #cee3f8; color: #555; } amp-social-share[type="reddit"]::before { content: "\f222"; } amp-social-share[type="whatsapp"] { background: #43d854; } amp-social-share[type="whatsapp"]::before { content: "\f608"; } sharedaddy/images/rss.png 0000644 00000001427 15174711637 0011456 0 ustar 00 �PNG IHDR �a �IDATxm��d9@o�¯jkl��m۶}��ڶm{<ͱ�U�[{o���>�F�>�:��M�5�3q����$�)�^W=*�k؈�a囡����`ʁ2 i� � p0h���D�+���~������S�?���׳�+��'h@,$� ���DQ���<�~��)���+��7�{�Q&�TI��ZP< ak�!�!p����Y�;B�<�0��G��f>k����$B�ո8@���:A� ]�����C�霩���ه��߯����:�(DG;:�&� ���v;������A�u7"7," �k+V�!TlK��Ա��w��Խp�|9i:,-[�#��e�+n<\�.D�Kd�!��ԒCz6��rݍ�<(1��s�����-���I둪�S�Kf*F���5kH�0���j�Xƺ)#�k)>����J��<�i�H ���t��h�F�J��Ҿπ{^��s��i(�=�*~^��{�MP�,t~� �� H�Ѣ� ���~�i{�kꮌ�� v8�n����ˇ�&@���IB ��?�‸r + (��q' 0EU��!�։����kN��)�t)k�ُ�2�[�V}H�[��靤)[D�Uđ%ѫ��x ��i��� Eǚ:ض'���DB�PC���te_lc�N��젱t�!��z�,�=�Z$nC�p �w@) ��)��6�� ��j���6 IEND�B`� sharedaddy/images/digg.png 0000644 00000000733 15174711637 0011560 0 ustar 00 �PNG IHDR ��h6 tRNS n�� �IDATxb ���`"Tu.� ���@�h@DQ�> �$HH����ڢ_H�� QB�{M��c�=�x$��ϲ��yo�$I,��A H�TU�ۏ���4�� �bY�7�^Q4qcܶmY� dY�$�l�;�s�w`��<ϲ%�y��`t]�LӼ���8��8�� ��E��z]׆aP�8�aڶ�� �i��4 ���@�ux��4��hl�a�K�.�P(�� �DuEl�%l����l8��켌���,K�qiH��Vk���>˲��$I �3uC����}w]����) i�g6MhE[ ��y����眔r]�y���bw/� qH�۶-�c��2��/�r��/P�0��i�(����JN۶8�����FR�#9y :[��2� IEND�B`� sharedaddy/images/feed.png 0000644 00000001272 15174711637 0011550 0 ustar 00 �PNG IHDR �a �IDATx���4I +3��l�m۶m���ٶm۶m�^c���m�SLxcߙ��k�k����>(S�ł�~Q����f;���}Ծv�8��J��0>��L�ʿ����ӏ//�l0�|[o�ٛ�(��cf�� v��"۔��^����K��wY,r���#����9������F`�t^��˕6yo��G3 ,��F���\dfY���6���y5@"� ����*T(��R,$��!�V؞��'n��G/��@ @#A�u\�B�o4�`�,�;�@�{��#h[qG�V������q�֫D Z+��&�1��Kt�wݛ��_2�ֶ춸����B9�D ���~�����s��N$7�*���'���Z��_)�9�A��50�w��J /�F�;i_~G,I(�,��W ε�z�����"�i�����4wPx�N$�� �J�۷IuMO�яy��� �`�U�j�N����=��{�������'B�OL S@q�)K���`�8�/�)�v; �~�� +8Yu��lgm�R:�Քi!vB��+^�o+@|�ҭ���`��.j�) b�Dɫ_W<� �X~ �` �bw�R�R ^~���P�'�m IEND�B`� sharedaddy/images/smart-tumblr@2x.png 0000644 00000007723 15174711637 0013657 0 ustar 00 �PNG IHDR | ( <�_ �IDATx���cM����ڶ5�w�8Y۶m۶�q��Ƶ6�[u������Iw��+q%�x�Z�J�S2Q��H"a��?���c�IW�0���B�L(���,�C�D�&�I�V4� ��C��$�i�M�M銃�lm�8�f�1�`�H��J"4Kɲ��q �c�B�;��c��?)r��ˌ�5b`j!�%���<���h&vE�!-i*��<V`� u�X�К����.�3�td��I��H݆f~�Z݆e��p�j�{u�Z4$:Ml3@���6NFR�R @�th�O��}Չԥ� ��Q✍�g���ڃ�G/~V-��)��*���{��*<xZ�>I�_��v}5�8P�Ӓ�\�i���>��h�,a�Z�V�7�ߵh���6�=�q�[� �N^��G/QY�?�?C0�<��Y'/ƾ#�p���U�� F��DQV�F��/�/m�NCZ�T_8�Mp.��ԥ�3*��- E���o�{�;$2�L{܀�ƽ��A8 �}!Ը�r�����k�T��ɋ���GP� 6�r��/˫�7��L;Ғ����o�s'7T]"�R��y�Dx*o\\��(�F$��!�|�����+��-�iISھ��U-8׀6}��ىP�vd����;6n����u�4���&k�k�+�|��8u�&.�|���ᴣ!-iJ�7��������PU)^W{���O�od������Q�3���ത)m� 烖���i�g.݂?T۱z�lG\�o�}���f�ֽT��%Mi�8��J+b#~�!=ǁkw7 �]^:n�'�P�`���s$J�b�Й>�A�$9�"�̎���"w��4��vz3FK;����A���\�x�6o&�[��C���� 6��3��乫yA�����n��kT.���3Wb��M�N�/�0}4�_������������0{f.�ӄ��-}7���{K�� eފ����;�W�ef.Z��1s�4��!-i�{�&8o���>w��������������o0�IrcY���;�Ȥ����NW�k?%x�dG�#�u�������(�5(�F�P���D�7�>��@�>��}��|�t\��Hj����q��K�'����ƒ�;��ō(�u��1o��G���߀e�B<xV�ФV5� 6�:�168��i�։�?�f�Ǒ���8�����`��-��3��,-�-<��}oݛ�:�%/m���t�ׅ3�hl�� �����^oX��dr�\����3a S���0 u �ܢS��6c8R6�3�,�s4ט��o��s &Y)u��ò��?Q�p��ǟ)�HnBE-� M�f���U� +E�ɦ#�v���6u}C���j2 X��m��بs� �r@�?��V�Ժ=p7V�ڥ,����D�j���(k�w�_о���[2��aTuW������a�nbX�k����[�M�U���6p�\����,(q�N�\l��W°�����+�dV���J� <���K$v^� w��}M*�bo^)KJ������<�7�}��=p7�����?t ���_��Ro��� �gЀ�4�F�f7(!wɂ%La��#��o��W>إ�W`9?U�&����s���9��X�1��� �Är#�ۍ�./Q���ec�U�g6��m����^����"4�<�wp� �-�\�MD���.(��FYș�r�P�� �������jT_>]h�WmI^�u X7F��;�z�8���`.�a�^��n1�y&����5����ݻ��7� r]�m�P�з�0���ċ�;�u��F����pu�qq6��H;th��ش����r�� p���߲;�z'�䐅E�-v�]�y���+�Jd> �^�g�,�ʙ����9,@�LD�D���B[���"�r���a%sI���~�Ɨlb:f�ə� �N�-��[����eUK/�KJ.��ǧ�661nS�(��l����F'g����)l=�td�� �/������r����K�:��?:zGH��֨���{���]���`s0V��_�d��.<��b%'�e�~��� H�h���Pt����bI83�yy�N'#�wxp�o���#r�ӳ��"?5;g���n��L�ާ���`CX��>p tz��wxݿ �uu��z���d�pA�K��'�x��ŵ�[2�Ɉ�-�Z �!��"$�-Xy������$�� ^��[��G ��1Z���K���N�F+ν)�%��FI�ܺ����=���R��p�s(%����\�d�js�g�� ?�o?t*�9��������ށ1�I*��0���/\h��?c�]C�۰�6-WI���LO�F�S�s� ������̎�)l}��NҖ�~I��h��OܤXHLt�1&�~{�lJ�.��4�r�&�+�¥O��w 8,�+l=�ds��h�@ɔ8���s�|�_^^�l�|X+B�{~�N����|η��?�+����es� �$)Kh����� CX��>p�9�_�?�8e�۟J��`;�u�Ms/V���Y�<����C5��X��bw�]tb���>��*+ɔ�����٧'�s���9�2�g �Z���^W�(��1�VTI&e^U��/���!<�����c/|.��6����%{���w�x,�����2 �@�rM���{X�>��*F�A}�U�����p�E�w�$�ԡ +Ӧ��/�����n$/{����t�f��@��ьs)7����ч7�p��'K�|VP��щY�_�U_� l���m�3/�=�ad�x�Gd�m=#Z_<��p�����p��˧ ��^�e/6��!>�CX��>p��Q�;�u���?��c�oF'ibVJט[^Si���8�|(M���i,,� @ ��Y8u,�'쀂��"ƇEuƋ�<�c�F�E�����XMc�}c7�TW4Y�Фb�|XV�:{G�dN����w���W�I�B����Y���/��{|�N��{6��R�Y���_e�T�UL�y�<kuW�ZH]4�XY�%��'����aLXOXyk���K�����%np�*p~��G3�I���D|qU}�Ű% �Pmj�ސ�4)��-h�Z\`�H`��%�ꤎK�N�8����}[cyC��Ї~mu�X/.,[@�+�$�h�K�RC�fJa��:�S�����$[:$%A��h:|G��d&�����N.(�mx�ݾ5��iӎe!O���$��[ê��w&p�� �du�� p��Ni�-,� p�3�)b'�M |��/C�oԜ ��ٴ�"�c$�[��E�hV���Wo�uC�en��|ee7(��r��o �1}�� �M)���f�=���|U���3�����3+�܄,�Ғ�˶_qyD��A/�'{`�5W�9^ �̑nh=[���a+�ֶ�g�$�pX�{�����(�q�n��`nV���t�[T~T���į�*<�GJ���E=��ڣŊ�o'�+�u;�L��X-�s��|���M���-3���O��gU���2;�]�rdTݵ�I�dv���I-{&�"٨yW hfA�/�FN��*1^o���ad��z��x��9��*�ܬw��͒���O�?n��s�:Xo|�/d��m��w�� ���)l=�&�u���Y�!Da����P��5@/�%����s�'�0�Y��Ѹ����&0�X_�w�v�2�C��m�p��*��bzv�E&�3f��e���*�+d =SRм@��#�rJ��p�&��A�y�-�la�^�ȝ"��;�����(�r��2��^��}t�� bǽ��7Z@K��`�|I� IEND�B`� sharedaddy/images/smart-digg@2x.png 0000644 00000002123 15174711637 0013251 0 ustar 00 �PNG IHDR � " �M� APLTELLL��t����ެ������ʽ��Ö�Ț�Ο�ӣ�٨TTT�䰪�y��~�����͇��������ŷ����Ϳ�����������ا���YYY�����핕���������������PPP���������kkklll������nnnooo������QQQ������ο�ο������������XXX�ŕSSS�Ë����������Ȕ���������^^^aaa��������⚚���䛛�����٧���cccqqq���eee��������������������묬����fff�����������췰����ggg���sǑ\ �IDATx^��� ��=��������]��9 3�I&{Sw�*U{���|��$������cW����W��g���������vsF�F�A�]�ć&�}��&D���� �;�[��9�>ة No�ą�v3��4�R��n��#�u@��[v`����n�b1�K&��L��? �vO'.Ƙ��9`���*�%��̩�ϭ�A����[e?N��R�;&QЬ�a�"� ����܊�3Y,��� a��� v�XL>!\�ZVX��rS`&�����i�MJ��A� ����`� �bP����� �J���/l��X�}̓���_ ��ȧX�dh�!�)HYĐ�u��`s�IBX� �w/`_֊`�c�X(P�ӣ�"|: V�LjO��d��ธ��C� `�zi�X�����@�\0F���S�)eCV�����Fz)���<�&������(0ᚒld��`*b��ŷ�t`��U������PY�*Q Ƈ`vn�U�A�-�tG����跬���#�B* 'T"�]TeY�)]Kϱ�}�a����Ӳ������Ű"> ��%妓p��r�`ڙ�ӗ��p�͡.�Aje��b������E�Âl� n�Я��TV �W�p�8� �P�E�pW��U���ě�[��3^m��;���`�-�|3/U:ڋ��w 5�Y�>��<Ѹ��Γ�_6�J��v IEND�B`� sharedaddy/images/linkedin-smart@2x.png 0000644 00000005403 15174711637 0014140 0 ustar 00 �PNG IHDR � $ ��ug �IDATx흅s"�������^�������������լkB\H�@b� ���Tg��!dȐ�n��~�������A���ĺ~��N���z�����^��9�O�$��v�� [��p�b�8�Τ��N�������� 31���AO(�D�U�{�e�Pp�g$���ߧ� V����p�3K��㣉ɮe?9�C��WV�=���̊O]���Ç��jp�7�Q�Ս��A�Y200���N477#�hK&���$#c(h�� �q�*;\a�K>W>���5cy�mCQ� Gǃ�N�`}�҂*����}g��\QT����?( 1ڔꁡ�8yq����Abr I,��T�s�s�k�Z����c8\���������6o*5u��v�i<[ށ���J�,p�\0��S(n`b �Ro| �Z���o4��s��v沈+#��ɋ���+�V�g �$F[d|7�W+����ׂí eN�����: ��١��C�q�Jy�)��� b ��a��b�jj�k�9�OH��n<��*m�ܿU�ii�ٳ�cF�_KNR���vN�ׄ�[wnnU��Bp�2+i `�{@�l�������]�����.� �X�p%�j�� � \�SYׄ�KU:�#�6n_�{#�q��ck�q}K*Z�OKK�+����\�����=��q[�_Gx\��3���%���6K�yd�$� (�~�� ?��.���A��Q�_�TY[+����>Ʌ�4����y��?�f�֭� VK/ș%.�Y�PT����<�������f�Q`װ+#�#`\�>�a�䬒:���d�7�V�Z��w�u�4���===</�Xii��w��&��=ϙ [;�̖V�ծn�c��)Uց�����/���TP���Pq�ļ]8q�]��Wg`�l��e|ї�o��Ʌ.2AX ��3b ��=%&P�>��|��Y-:6�_��L`�c�4LvM�9ܟ����Q�t�|a��V���8p��] v�ʆ�V���xqk^ܢ����J�m��=~~��_�S��7��8_�"����}Lq��q:��x��7Y9D5�c����T�HLLaT�b0:���yq|�;M�Ah����PA�)mL�n0�H�骵u��h%�v�9t�.�Ү`á+jp��j4�\Mk�p���p[>~�X��":6����%6�/�ɾ�a��#kA8��&=���^MMMKN�ɤ�R���}8 �?�G�w��u��G�`rcB0�%A27^#W� U!�V�� r� ;\nÁ?V��p�B�$H�?���-������,^�^�W�Z^�~�?ZIv�O��n���LZA*++E�ῲ4��}�G�R�$��) ��o��e�c^���}�2���H���1��= �Ő�[6�� �����}6�A(�V��� r��D��+������O�) 0�}��1 ��5�p���V�+K���}/o=�SS*��Ud�h�Tg��. h_��i�)[�> ��\I(P�Ɓ��x����J�m�c��,H�CD�@��̓ �U���DW ��i�N]QAY ��v�a�iA���P��W�}�Q9�D5�+��yr�2�6��y�j�sdAC��2i�d����s�=A"cSh���³�]���xG�`[|��WѸ_+�%\˽ /�Id?!�_G�1� O��O9�����r[U�U!�B8ǐ�w���DLWA(�n%bL�B��1��U�+�̈́�5�H((]G��dC�0N,tb(6��*�T=�Y�T-�6�� BY]A؇f�!�<W�����rb�[ ��s AfA�E�nYι��y�B 4�G�R�n�Q�BEoh "U�J��tL��cc˭ �#KA�=kA���f�&�TQ�fDNlÂ����y�NL�o��(~h�õ�J�������$���.�8d\�:�'�OA�g�˭i�2�D��lA�&��Ř �J�cȕ(A���0�e�d2��$�#\⍡��gwpZ�� Z�pX$W����^� BJ&��$�C.�Ű :�Dlc�H�)c�D~K�B�&���Q,o��sx������äL��1�5R�X�C���٩���lA�����"y�Ή�aA(�4L����M�<D�ȡ�n�n�[M��(I��'����?�g�jrrQ-���"��,� ������&f�b�|Â�o�$ݤ\o�aT�� B�t��e�MDz� �]\���X���>���� ⍊6��B!ȷez�[�5sK�!��n�qA�Բ$�N�<|� ��]���ȍ �Ћ�xr�n^�e#�vw&��$� r��|�s]t�Vӗy���Bq?ô���;����[�1$�6A�BA|�b��LY�h$�˙<����G��� :rX�,�Ϥ[�xTA���"DD�r�� :��� 9��Y��� b}��Ȃ�V����"���0��� ��q�f��8�㘘i$!�>5Z1d9���&�W�.����Ky��D���'�q7���7��K�g� f��W[_^}�;�&���&�����d7��-�A���f�S\f%|���q�������%O�%a%���'{�Hb0�?���,~@Ǫ��X��~@�N`�?���u�u�G��;8��d��O�]��o���U[?�f�dkkk"�� ��'ؼ�x�> V/�`�ƒC���d����i�V IEND�B`� sharedaddy/images/icon-twitter-2x.png 0000644 00000002423 15174711637 0013623 0 ustar 00 �PNG IHDR szz� �IDATx���3K�k6x�m۶m۶m۶m۶����`w�uOv��u�-'�ڞI���,�\�|�8�|&^��1_��DnN0⹈�f��WYz��.?v�Kn:m��D<�K<��-�y_֦��e M��W�sؒ\��d���v\�>�ׁ/Md ¸b}�� ��Ș%�p�b3�� �ó[,�G6Y �.?�>����`��c|r�����F�����uc]�]��Q��R}W�ʩ7��5��~�������=8���z���җ1/��I�ZY�|��6t�`��Lэ�^/�Z蜯(�充����d�|���x���j����J�qG|TCC���u?4�_�Rt����4��H�4��+ �]���n� ��o��j"�eٙ���8�:���G)GP� �&�֚��CS�5�t��ܸw�q�2�c�9���!����:Q�ه���a�Wy���+�������i�W����$��^�����/:���w�4�v���ꢟr�< �����xt�%0�'��RW2�{x�L��(�O��R�އ��c����@k� e������Z0V�M���W����7Ę&���x�Z��~�m�[q֜@��� �e�o%Zp�� Զ�b4z�F@��L��(��H�i��F�p�b�٧�H�Y|2�)HYw�W���_�&��Z{��]<�s�����j�ؘ���wV~'4P6CN��9 �~���$�����DK҃� �>�%��`~lIμo�.O���~��|��_����C�٩f�|�o�e����"���-��>k,<�����4c��)0X��) � a��f�V��a���c�hC�?M���b)��-��)������ �W��On[.2+�!*|��[�ib*ޯi��1��`el��)�� Ht���w�܈�g�`�9��|3Mk�:���v�]s�ob�Ts?�c��9 �S6��h��S^�bmI�:eNv������H�֧�?J�L��E ��s��6ȱc �!��"�afH'S��4�u�&k���@��\��V��b�3��������қ�9m�Q�� ���`�Y��ؤ��o�0=�!w��S�o��q&�&r�ÛP$�#�P8 ���P����ܔ�_nZ�v=�]��2��'��z��j�i3F�Ru1�I �a���z�jg���a(v̱$�_X�)�%!<�7�'�虶Zֻ��������ޫ6�# IEND�B`� sharedaddy/images/reddit@2x.png 0000644 00000002654 15174711637 0012477 0 ustar 00 �PNG IHDR szz� sIDATx�W��I̹p��ʿm۶m۶m۶�l۶�}�W_U�<N�������U���q�!�=�ʕ*p�/�����e��S2r���PѦRk���K0O��/���ʶ�A� �8��(�j���t�2A �+'���bi4�h�����?���al�M (�EDD`˖-X�r%�{ddd��ɓ�ѣ�V���3g"))� �]f ��ɓ'hݺ5����͛�m۶�ر#j֬�'N��;w��ի���]� ���0q�DlY����Ehh(F���M�"((�M�6a��HNN._ o߾E�^���cQ{�bРAX�p!�ܹ#2r��!��� �J% �9YYY06WWWu�z��v�Z(�J�J ��驩����<y2,X www����A�hj�4� ��t&�����[�n� X�d �̙#H���'M��e˖�ܹs�w���bЦ|� ����G��ƍ�x���+W���֭�`�ԩS�n�:9r7o��H�iӦ�K�.�ڵ+�.]��ׯ �^�N��ӧO���b >|X�T���ѠA4n�X�^�x1����_P k��}||D���۷GÆ Qkժ� 60HS LOtt4�U�&�����?2b�Ri��Я_?ԭ[���� A03� Xg��E�$�`�رc ZK���m��ݡӨy���v� Mb��x,̀D�#F���E�L�֯_�ǏG�f�P��nmV���B�S(gց:�aK��xy;�:��&��&w �A�R%IT�`����ӧ�o�PG��~ �P@�Z�͵ПbEO�K�@M�]P��5������/��lM�mH��l�����/+ʽRz��FP�?7p����jEG������I��q�5j **ʺp����_���4��� q�s.�"|�|}�[@@ ��rX@!�V�Z��4{#!!�:���" ۑc�$;�Q���a��}���l�[\\5j���m[�9R�2788� ��ݻ��lܸ�ϟ�۷o �h�B�c���Çs_���B�" �|�` �m��)G�!_��)�B|hyyy�{�.z����S��F�\ j:ՐR�V"9� � �DXX�h-�&g���� �g�Qv�A�����]�sܸq�x�w�^8p��b����~���KeeO� ��i��_���ٳgE�gϞ-F0U��g͚% ���/Hg���걜�?�iH.Ё!ɋ��t�C�����]�v�t�^���Y:��[����1r�Ֆq�L�>�}��g�b��w5s�gME-�]��y��Q�X�]�vq��5�����Ռ��w�@�r�H{'�����:���*�y0�)�e 09r�3�r����I+��� ��>�?���-�� IEND�B`� sharedaddy/images/kindle@2x.png 0000644 00000002603 15174711637 0012464 0 ustar 00 �PNG IHDR szz� JIDATx����_{��ϳgΜ�Y�Am۶�n�ڶ��n��Im��=�b�����ܼ����J�㙵���g���!�o���$�?TUVǫ?�s?�L����>��3O,��ҨH�*#��]��(�p� Fy�������r�^?�\.E����5$1���B�F��PHb�Zy�� Hr�Y,A�0j�+Fh-���=���2�$�;в:��'^�Q�w����z�oj�� Z;W��Pj� !�bX4��y��0���[ ��=�ݖFi-�rxՍ����Q) ��Q�1$r ! �s�&�"i�䮁 ��/ȡ T�ґ� ! )���SH��$t�$)�!ݼ �R��QDU8?6�aFDB�I����%�U"H��&��4")���{$�ndN1�BT�}˪�ċ�R'��l�QM=P���?��oy'�u�l�������E�.=��TU/[��>xhT����tk-ڬZ�>u�G?�ӯ�j�����W�?�o��lo��&)���n����/" �ngΝ��0�y�̿˻{�Ʌ����7��[}:�[�T�E`_`<w �K(���~�C�o~�������o��Z��M���z)���0���|��~a���0����wO{�}�Je-!Ւ�����nv���-ݎ����z����ޕlv�iO}IO�TJ Sb/�����{:y����4���������>�JF�H"q��f_��_�%��2[�^�BOfO��⡧�f��)�x���ӹ$ �9�m�n�M�[]����_�21���=�@�㯘�ċKhv[����#έ��������/����@�~�b�c�:@$Cf�n3;G����}������C���S�lwᯥ#��t#� Iٟ�1���d`�~w�l2t�o�����n=��+���E�C'h���y�Zc�r7��' ;j�����#ٵ�Mu��\��O6jXd��*��Հ�m��m���ZV�{�c~ڭ�mf�c��ˍ?�9��Je���^����I"B�s6��S���ʃϺ�ݿ���-:,n������틓�)��<����BPc�f�)&)�l��y�7�X��g���\��O��}jg�.(�{'i�����y����.�sTk��v�m��oe}�mnn����/|���G��n�w�����'����LQ��DY ���b�$���Z,Th�ҲY+�5��"�.X�sra��=�\I�4EEmo��5U�,Ől�!��%���F����,T�DBw���{/,����w{�w��[7�<Aܩ��ME2t!��P� -��B�TY`&TTH��rrrr����:���d� IEND�B`� sharedaddy/images/linkedin@2x.png 0000644 00000001573 15174711637 0013020 0 ustar 00 �PNG IHDR szz� BIDATx���,9��ӝA�g۶]~*�k��(c�2�*�QX۶�]۶���J�&�=�=7W߰'g��C��o�H{b�,/����!) A�� A#����qXx1%>���yvۚ-G7�D��p��&!� ����k��хڛ��S��7<�m�S[�BJӵ� w~�h���eI��W,�H ma���@N�����م�-���W�-g�Y�Qr~-��z�ӟ� `8����8 ��`a��@�[�{�B��; ��4 ˈ10� �]_�o���х�N�ܺ&P�qc`0�ZY���0��`r1�)�M� ��ⶁ�wȕ�# ۄ�dzsm@#� <�]y.c �9�Y�\�Lh:#��X��Dod���N��V���E0¥1 �M<wv;�\�����wYkKS �~xv-��ųRP�Z߅��[�QC�62A20�EP�v,��0��D`�]X����BQ�!����+���Z�< ��3\d�7�'#$b0�q\�o �,oG�((��G��撏��j���{���G��~f�|�`�Yf v��P��^k�����-ƺ���e՜d�5�<K��.��]u��>�L����-�p�6�a� �,�و��S�6=�Ҥ����O�<i�����t�Yui���$���zr��$�$GJ�%c������!��E֍O߷m�\Ɯ����(H�,_�~�i:�Ym�"p����4WW� ��A���2[8Eq�5K\� J�����K��3k�< 29����ׁ��uv�A����y+�5Es��,IOb&>�U�C�����w����I����7�cQc�^ՠ`� �weho�� IEND�B`� sharedaddy/images/icon-facebook-2x.png 0000644 00000001745 15174711637 0013700 0 ustar 00 �PNG IHDR szz� �IDATx�U�&]|şg۶m۶m���ٶm�v����o߷Ʒ����8�$���Z[��8ǟ�m~�QyU�0} X�ۢJrW�<a�+o��:[�tt@�Ύ���Q;��vrHT��%�O��fb��`�� , ��@TZ��"W]sTiM��Nh�E��P�ߵ� *���`OL]�sͷa��͘0g=��Y���퓂 �uY5�5} ��z� DDF!::<��W_��y�Y��^ �9���&0�݅��p�f�a8~��mF>���s�]�z��/�EEG#&&�Uk �B��rd�c�e �nQb!v� 2q����Hj�r'���z;�TC��$T�FVx�� �r���o ���UDZb�)�-;��=]P��5j���EBV_[�D+4�� [���� ��Nr������Z٠z;�: T� �md���]��o"