summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/_inc/lib')
-rw-r--r--plugins/jetpack/_inc/lib/admin-pages/class-jetpack-about-page.php679
-rw-r--r--plugins/jetpack/_inc/lib/admin-pages/class.jetpack-admin-page.php353
-rw-r--r--plugins/jetpack/_inc/lib/admin-pages/class.jetpack-landing-page.php3
-rw-r--r--plugins/jetpack/_inc/lib/admin-pages/class.jetpack-react-page.php399
-rw-r--r--plugins/jetpack/_inc/lib/admin-pages/class.jetpack-settings-page.php141
-rw-r--r--plugins/jetpack/_inc/lib/class.color.php755
-rw-r--r--plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php3196
-rw-r--r--plugins/jetpack/_inc/lib/class.jetpack-automatic-install-skin.php111
-rw-r--r--plugins/jetpack/_inc/lib/class.jetpack-iframe-embed.php84
-rw-r--r--plugins/jetpack/_inc/lib/class.jetpack-keyring-service-helper.php204
-rw-r--r--plugins/jetpack/_inc/lib/class.jetpack-password-checker.php1288
-rw-r--r--plugins/jetpack/_inc/lib/class.jetpack-photon-image-sizes.php182
-rw-r--r--plugins/jetpack/_inc/lib/class.jetpack-photon-image.php243
-rw-r--r--plugins/jetpack/_inc/lib/class.jetpack-search-performance-logger.php83
-rw-r--r--plugins/jetpack/_inc/lib/class.media-extractor.php436
-rw-r--r--plugins/jetpack/_inc/lib/class.media-summary.php369
-rw-r--r--plugins/jetpack/_inc/lib/class.media.php505
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class-wpcom-rest-field-controller.php333
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php1708
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php60
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php56
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php39
-rw-r--r--plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php40
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/business-hours.php49
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php79
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php71
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/hello.php22
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php187
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php121
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connections.php194
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-services.php167
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/service-api-keys.php281
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/sites-posts-featured-media-url.php37
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php62
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-fields/attachment-fields-videopress.php171
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-fields/post-fields-publicize-connections.php353
-rw-r--r--plugins/jetpack/_inc/lib/debugger/0-load.php24
-rw-r--r--plugins/jetpack/_inc/lib/debugger/class-jetpack-cxn-test-base.php471
-rw-r--r--plugins/jetpack/_inc/lib/debugger/class-jetpack-cxn-tests.php340
-rw-r--r--plugins/jetpack/_inc/lib/debugger/class-jetpack-debug-data.php400
-rw-r--r--plugins/jetpack/_inc/lib/debugger/class-jetpack-debugger.php440
-rw-r--r--plugins/jetpack/_inc/lib/debugger/debug-functions-for-php53.php92
-rw-r--r--plugins/jetpack/_inc/lib/functions.wp-notify.php353
-rw-r--r--plugins/jetpack/_inc/lib/icalendar-reader.php913
-rw-r--r--plugins/jetpack/_inc/lib/jetpack-wpes-query-builder/jetpack-wpes-query-builder.php341
-rw-r--r--plugins/jetpack/_inc/lib/jetpack-wpes-query-builder/jetpack-wpes-query-parser.php683
-rw-r--r--plugins/jetpack/_inc/lib/markdown/0-load.php6
-rw-r--r--plugins/jetpack/_inc/lib/markdown/README.md19
-rw-r--r--plugins/jetpack/_inc/lib/markdown/extra.php3207
-rw-r--r--plugins/jetpack/_inc/lib/markdown/gfm.php400
-rw-r--r--plugins/jetpack/_inc/lib/plugins.php132
-rw-r--r--plugins/jetpack/_inc/lib/tonesque.php237
-rw-r--r--plugins/jetpack/_inc/lib/tracks/class.tracks-client.php191
-rw-r--r--plugins/jetpack/_inc/lib/tracks/class.tracks-event.php149
-rw-r--r--plugins/jetpack/_inc/lib/tracks/client.php130
-rw-r--r--plugins/jetpack/_inc/lib/tracks/tracks-ajax.js62
-rw-r--r--plugins/jetpack/_inc/lib/tracks/tracks-callables.js76
-rw-r--r--plugins/jetpack/_inc/lib/widgets.php776
58 files changed, 22503 insertions, 0 deletions
diff --git a/plugins/jetpack/_inc/lib/admin-pages/class-jetpack-about-page.php b/plugins/jetpack/_inc/lib/admin-pages/class-jetpack-about-page.php
new file mode 100644
index 00000000..b9987a0f
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/admin-pages/class-jetpack-about-page.php
@@ -0,0 +1,679 @@
+<?php
+/**
+ * Class for the Jetpack About Page within the wp-admin.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Disable direct access and execution.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+require_once 'class.jetpack-admin-page.php';
+
+/**
+ * Builds the landing page and its menu.
+ */
+class Jetpack_About_Page extends Jetpack_Admin_Page {
+
+ /**
+ * Show the settings page only when Jetpack is connected or in dev mode.
+ *
+ * @var bool If the page should be shown.
+ */
+ protected $dont_show_if_not_active = true;
+
+ /**
+ * Add a submenu item to the Jetpack admin menu.
+ *
+ * @return string
+ */
+ public function get_page_hook() {
+ // Add the main admin Jetpack menu.
+ return add_submenu_page(
+ 'jetpack',
+ esc_html__( 'About Jetpack', 'jetpack' ),
+ esc_html__( 'About Jetpack', 'jetpack' ),
+ 'jetpack_admin_page',
+ 'jetpack_about',
+ array( $this, 'render' )
+ );
+ }
+
+ /**
+ * Add page action
+ *
+ * @param string $hook Hook of current page, unused.
+ */
+ public function add_page_actions( $hook ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ // Place the Jetpack menu item on top and others in the order they appear.
+ add_filter( 'custom_menu_order', '__return_true' );
+ add_filter( 'menu_order', array( $this, 'submenu_order' ) );
+ }
+
+ /**
+ * Enqueues scripts and styles for the admin page.
+ */
+ public function page_admin_scripts() {
+ wp_enqueue_style( 'plugin-install' );
+ wp_enqueue_script( 'plugin-install' );
+ // required for plugin modal action button functionality.
+ wp_enqueue_script( 'updates' );
+ // required for modal popup JS and styling.
+ wp_enqueue_style( 'thickbox' );
+ wp_enqueue_script( 'thickbox' );
+ }
+
+ /**
+ * Load styles for static page.
+ */
+ public function additional_styles() {
+ Jetpack_Admin_Page::load_wrapper_styles();
+ }
+
+ /**
+ * Render the page with a common top and bottom part, and page specific content
+ */
+ public function render() {
+ Jetpack_Admin_Page::wrap_ui( array( $this, 'page_render' ), array( 'show-nav' => false ) );
+ }
+
+ /**
+ * Change order of menu item so the About page menu item is below Site Stats.
+ *
+ * @param array $menu_order List of menu slugs. It's unaffected. This filter is used to reorder the Jetpack submenu items.
+ *
+ * @return array
+ */
+ public function submenu_order( $menu_order ) {
+ global $submenu;
+
+ $stats_key = null;
+ $about_key = null;
+
+ foreach ( $submenu['jetpack'] as $index => $menu_item ) {
+ if ( false !== array_search( 'stats', $menu_item, true ) ) {
+ $stats_key = $index;
+ }
+ if ( false !== array_search( 'jetpack_about', $menu_item, true ) ) {
+ $about_key = $index;
+ }
+ }
+
+ if ( $stats_key && $about_key ) {
+ $temp = $submenu['jetpack'][ $stats_key ];
+ $submenu['jetpack'][ $stats_key ] = $submenu['jetpack'][ $about_key ]; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ $submenu['jetpack'][ $about_key ] = $temp; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
+ return $menu_order;
+ }
+
+ /**
+ * Render the page content
+ */
+ public function page_render() {
+ ?>
+ <div class="jp-lower">
+ <div class="jetpack-about__link-back">
+ <a href="<?php echo esc_url( admin_url( 'admin.php?page=jetpack' ) ); ?>">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" fill="none" width="24" height="24"/><g><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></g></svg>
+ <?php esc_html_e( 'Back to Jetpack Dashboard', 'jetpack' ); ?>
+ </a>
+ </div>
+ <div class="jetpack-about__main">
+ <div class="jetpack-about__logo">
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 800 96" style="enable-background:new 0 0 800 96;" xml:space="preserve">
+ <g>
+ <path style="fill: #39c;" d="M292.922,78c-19.777,0-32.598-14.245-32.598-29.078V47.08c0-15.086,12.821-29.08,32.598-29.08
+ c19.861,0,32.682,13.994,32.682,29.08v1.843C325.604,63.755,312.783,78,292.922,78z M315.044,47.245
+ c0-10.808-7.877-20.447-22.122-20.447s-22.04,9.639-22.04,20.447v1.341c0,10.811,7.795,20.614,22.04,20.614
+ s22.122-9.803,22.122-20.614V47.245z"/>
+ <path d="M69.602,75.821l-7.374-13.826H29.463l-7.124,13.826H11.277l30.167-55.81h8.715l30.671,55.81H69.602z M45.552,30.906
+ L33.401,54.369h24.72L45.552,30.906z"/>
+ <path d="M128.427,78c-20.028,0-29.329-10.894-29.329-25.391V20.012h10.391v32.765c0,10.308,6.788,16.424,19.692,16.424
+ c13.242,0,18.687-6.116,18.687-16.424V20.012h10.475v32.598C158.342,66.436,149.46,78,128.427,78z"/>
+ <path d="M216.667,28.727v47.094h-10.475V28.727h-24.386v-8.715h59.245v8.715H216.667z"/>
+ <path d="M418.955,75.821V31.659l-2.766,4.861l-23.379,39.301h-5.112L364.569,36.52l-2.765-4.861v44.162h-10.224v-55.81h14.497
+ l22.038,38.296L390.713,63l2.599-4.692l21.786-38.296h14.331v55.81H418.955z"/>
+ <path d="M508.619,75.821l-7.374-13.826H468.48l-7.123,13.826h-11.061l30.167-55.81h8.715l30.669,55.81H508.619z M484.569,30.906
+ l-12.151,23.464h24.72L484.569,30.906z"/>
+ <path d="M562.081,28.727v47.094h-10.474V28.727h-24.386v-8.715h59.245v8.715H562.081z"/>
+ <path d="M638.924,28.727v47.094H628.45V28.727h-24.386v-8.715h59.245v8.715H638.924z"/>
+ <path d="M689.118,75.821v-50.53c4.19,0,5.866-2.263,5.866-5.28h4.442v55.81H689.118z"/>
+ <path d="M781.464,35.765c-5.028-4.609-12.402-8.967-22.374-8.967c-14.916,0-23.296,10.225-23.296,20.867v1.089
+ c0,10.558,8.464,20.445,24.05,20.445c9.303,0,17.012-4.441,21.872-8.965L788,66.854C781.883,72.887,771.492,78,759.174,78
+ c-21.118,0-33.939-13.743-33.939-28.828v-1.843c0-15.084,13.993-29.329,34.44-29.329c11.816,0,22.541,4.944,28.324,11.146
+ L781.464,35.765z"/>
+ <path d="M299.82,37.417c1.889,1.218,2.418,3.749,1.192,5.648l-9.553,14.797c-1.226,1.901-3.752,2.452-5.637,1.234l0,0
+ c-1.886-1.22-2.421-3.745-1.192-5.647l9.553-14.797C295.41,36.753,297.935,36.201,299.82,37.417L299.82,37.417z"/>
+ </g>
+ </svg>
+ </div>
+ <div class="jetpack-about__content">
+ <div class="jetpack-about__images">
+ <ul class="jetpack-about__gravatars">
+ <?php $this->display_gravatars(); ?>
+ </ul>
+ <p class="meet-the-team">
+ <a href="https://automattic.com/about/" target="_blank" rel="noopener noreferrer" class="jptracks" data-jptracks-name="jetpack_about_meet_the_team"><?php esc_html_e( 'Meet the Automattic team', 'jetpack' ); ?></a>
+ </p>
+ </div>
+
+ <div class="jetpack-about__text">
+ <p>
+ <?php esc_html_e( 'We are the people behind WordPress.com, WooCommerce, Jetpack, Simplenote, Longreads, VaultPress, Akismet, Gravatar, Crowdsignal, Cloudup, and more. We believe in making the web a better place.', 'jetpack' ); ?>
+ <a href="https://automattic.com/" target="_blank" rel="noopener noreferrer" class="jptracks" data-jptracks-name="jetpack_about_learn_more">
+ <?php esc_html_e( 'Learn more about us.', 'jetpack' ); ?>
+ </a>
+ </p>
+ <p>
+ <?php esc_html_e( 'We’re a distributed company with over 875 Automatticians in more than 67 countries speaking at least 83 different languages. Our common goal is to democratize publishing so that anyone with a story can tell it, regardless of income, gender, politics, language, or where they live in the world.', 'jetpack' ); ?>
+ </p>
+ <p>
+ <?php esc_html_e( 'We believe in Open Source and the vast majority of our work is available under the GPL.', 'jetpack' ); ?>
+ </p>
+ <p>
+ <?php
+ // Maybe use printf() because we'll want to escape the string but still allow for the link, so we can't use esc_html_e().
+ echo wp_kses(
+ __( 'We strive to live by the <a href="https://automattic.com/creed/" target="_blank" class="jptracks" data-jptracks-name="jetpack_about_creed" rel="noopener noreferrer">Automattic Creed</a>.', 'jetpack' ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'class' => array(),
+ 'target' => array(),
+ 'rel' => array(),
+ 'data-jptracks-name' => array(),
+ ),
+ )
+ );
+ ?>
+ </p>
+ <p>
+ <a href="https://automattic.com/work-with-us" target="_blank" rel="noopener noreferrer" class="jptracks" data-jptracks-name="jetpack_about_work_with_us">
+ <?php esc_html_e( 'Come work with us', 'jetpack' ); ?>
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="jetpack-about__colophon">
+ <h3><?php esc_html_e( 'Popular WordPress services by Automattic', 'jetpack' ); ?></h3>
+ <ul class="jetpack-about__services">
+ <?php $this->display_plugins(); ?>
+ </ul>
+
+ <p class="jetpack-about__services-more">
+ <?php
+ echo wp_kses(
+ __( 'For even more of our WordPress plugins, please <a href="https://profiles.wordpress.org/automattic/#content-plugins" target="_blank" rel="noopener noreferrer" class="jptracks" data-jptracks-name="jetpack_about_wporg_profile">take a look at our WordPress.org profile</a>.', 'jetpack' ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'target' => array(),
+ 'rel' => array(),
+ 'class' => array(),
+ 'data-jptracks-name' => array(),
+ ),
+ )
+ );
+ ?>
+ </p>
+ </div>
+ </div>
+ <?php
+ }
+
+ /**
+ * Add information cards for a8c plugins.
+ */
+ public function display_plugins() {
+ $plugins_allowedtags = array(
+ 'a' => array(
+ 'href' => array(),
+ 'title' => array(),
+ 'target' => array(),
+ ),
+ 'abbr' => array( 'title' => array() ),
+ 'acronym' => array( 'title' => array() ),
+ 'code' => array(),
+ 'pre' => array(),
+ 'em' => array(),
+ 'strong' => array(),
+ 'ul' => array(),
+ 'ol' => array(),
+ 'li' => array(),
+ 'p' => array(),
+ 'br' => array(),
+ );
+
+ // slugs for plugins we want to display.
+ $a8c_plugins = array(
+ 'woocommerce',
+ 'wp-super-cache',
+ 'wp-job-manager',
+ 'co-authors-plus',
+ );
+
+ // need this to access the plugins_api() function.
+ include_once ABSPATH . 'wp-admin/includes/plugin-install.php';
+
+ $plugins = array();
+ foreach ( $a8c_plugins as $slug ) {
+ $args = array(
+ 'slug' => $slug,
+ 'fields' => array(
+ 'added' => false,
+ 'author' => false,
+ 'author_profile' => false,
+ 'banners' => false,
+ 'contributors' => false,
+ 'donate_link' => false,
+ 'homepage' => false,
+ 'reviews' => false,
+ 'screenshots' => false,
+ 'support_threads' => false,
+ 'support_threads_resolved' => false,
+ 'sections' => false,
+ 'tags' => false,
+ 'versions' => false,
+
+ 'compatibility' => true,
+ 'downloaded' => true,
+ 'downloadlink' => true,
+ 'icons' => true,
+ 'last_updated' => true,
+ 'num_ratings' => true,
+ 'rating' => true,
+ 'requires' => true,
+ 'requires_php' => true,
+ 'short_description' => true,
+ 'tested' => true,
+ ),
+ );
+
+ // should probably add some error checking here too.
+ $api = plugins_api( 'plugin_information', $args );
+ $plugins[] = $api;
+ }
+
+ foreach ( $plugins as $plugin ) {
+ if ( is_object( $plugin ) ) {
+ $plugin = (array) $plugin;
+ }
+
+ $title = wp_kses( $plugin['name'], $plugins_allowedtags );
+ $version = wp_kses( $plugin['version'], $plugins_allowedtags );
+
+ $name = wp_strip_all_tags( $title . ' ' . $version );
+
+ // Remove any HTML from the description.
+ $description = wp_strip_all_tags( $plugin['short_description'] );
+
+ $wp_version = get_bloginfo( 'version' );
+
+ $compatible_php = ( empty( $plugin['requires_php'] ) || version_compare( phpversion(), $plugin['requires_php'], '>=' ) );
+ $compatible_wp = ( empty( $plugin['requires'] ) || version_compare( $wp_version, $plugin['requires'], '>=' ) );
+
+ $action_links = array();
+
+ // install button.
+ if ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) {
+ $status = install_plugin_install_status( $plugin );
+ switch ( $status['status'] ) {
+ case 'install':
+ if ( $status['url'] ) {
+ if ( $compatible_php && $compatible_wp ) {
+ $action_links[] = sprintf(
+ '<a class="install-now button jptracks" data-slug="%1$s" href="%2$s" aria-label="%3$s" data-name="%4$s" data-jptracks-name="jetpack_about_install_button" data-jptracks-prop="%4$s">%5$s</a>',
+ esc_attr( $plugin['slug'] ),
+ esc_url( $status['url'] ),
+ /* translators: %s: plugin name and version */
+ esc_attr( sprintf( __( 'Install %s now', 'jetpack' ), $name ) ),
+ esc_attr( $name ),
+ esc_html__( 'Install Now', 'jetpack' )
+ );
+ } else {
+ $action_links[] = sprintf(
+ '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
+ _x( 'Cannot Install', 'plugin', 'jetpack' )
+ );
+ }
+ }
+ break;
+
+ case 'update_available':
+ if ( $status['url'] ) {
+ $action_links[] = sprintf(
+ '<a class="update-now button aria-button-if-js jptracks" data-plugin="%1$s" data-slug="%2$s" href="%3$s" aria-label="%4$s" data-name="%5$s" data-jptracks-name="jetpack_about_update_button" data-jptracks-prop="%5$s">%6$s</a>',
+ esc_attr( $status['file'] ),
+ esc_attr( $plugin['slug'] ),
+ esc_url( $status['url'] ),
+ /* translators: %s: plugin name and version */
+ esc_attr( sprintf( __( 'Update %s now', 'jetpack' ), $name ) ),
+ esc_attr( $name ),
+ __( 'Update Now', 'jetpack' )
+ );
+ }
+ break;
+
+ case 'latest_installed':
+ case 'newer_installed':
+ if ( is_plugin_active( $status['file'] ) ) {
+ $action_links[] = sprintf(
+ '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
+ _x( 'Active', 'plugin', 'jetpack' )
+ );
+ } elseif ( current_user_can( 'activate_plugin', $status['file'] ) ) {
+ $button_text = __( 'Activate', 'jetpack' );
+ /* translators: %s: plugin name */
+ $button_label = _x( 'Activate %s', 'plugin', 'jetpack' );
+ $activate_url = add_query_arg(
+ array(
+ '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $status['file'] ),
+ 'action' => 'activate',
+ 'plugin' => $status['file'],
+ ),
+ network_admin_url( 'plugins.php' )
+ );
+
+ if ( is_network_admin() ) {
+ $button_text = __( 'Network Activate', 'jetpack' );
+ /* translators: %s: plugin name */
+ $button_label = _x( 'Network Activate %s', 'plugin', 'jetpack' );
+ $activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url );
+ }
+
+ $action_links[] = sprintf(
+ '<a href="%1$s" class="button activate-now" aria-label="%2$s" data-jptracks-name="jetpack_about_activate_button" data-jptracks-prop="%3$s">%4$s</a>',
+ esc_url( $activate_url ),
+ esc_attr( sprintf( $button_label, $plugin['name'] ) ),
+ esc_attr( $plugin['name'] ),
+ $button_text
+ );
+ } else {
+ $action_links[] = sprintf(
+ '<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
+ _x( 'Installed', 'plugin', 'jetpack' )
+ );
+ }
+ break;
+ }
+ }
+
+ $plugin_install = "plugin-install.php?tab=plugin-information&amp;plugin={$plugin['slug']}&amp;TB_iframe=true&amp;width=600&amp;height=550";
+ $details_link = is_multisite()
+ ? network_admin_url( $plugin_install )
+ : admin_url( $plugin_install );
+
+ if ( ! empty( $plugin['icons']['svg'] ) ) {
+ $plugin_icon_url = $plugin['icons']['svg'];
+ } elseif ( ! empty( $plugin['icons']['2x'] ) ) {
+ $plugin_icon_url = $plugin['icons']['2x'];
+ } elseif ( ! empty( $plugin['icons']['1x'] ) ) {
+ $plugin_icon_url = $plugin['icons']['1x'];
+ } else {
+ $plugin_icon_url = $plugin['icons']['default'];
+ }
+ ?>
+
+ <li class="jetpack-about__plugin plugin-card-<?php echo sanitize_html_class( $plugin['slug'] ); ?>">
+ <?php
+ if ( ! $compatible_php || ! $compatible_wp ) {
+ echo '<div class="notice inline notice-error notice-alt"><p>';
+ if ( ! $compatible_php && ! $compatible_wp ) {
+ esc_html_e( 'This plugin doesn&#8217;t work with your versions of WordPress and PHP.', 'jetpack' );
+ if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
+ printf(
+ /* translators: 1: "Update WordPress" screen URL, 2: "Update PHP" page URL */
+ ' ' . wp_kses( __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.', 'jetpack' ), array( 'a' => array( 'href' => true ) ) ),
+ esc_url( self_admin_url( 'update-core.php' ) ),
+ esc_url( $this->jp_get_update_php_url() )
+ );
+ $this->jp_update_php_annotation();
+ } elseif ( current_user_can( 'update_core' ) ) {
+ printf(
+ /* translators: %s: "Update WordPress" screen URL */
+ ' ' . wp_kses( __( '<a href="%s">Please update WordPress</a>.', 'jetpack' ), array( 'a' => array( 'href' => true ) ) ),
+ esc_url( self_admin_url( 'update-core.php' ) )
+ );
+ } elseif ( current_user_can( 'update_php' ) ) {
+ printf(
+ /* translators: %s: "Update PHP" page URL */
+ ' ' . wp_kses( __( '<a href="%s">Learn more about updating PHP</a>.', 'jetpack' ), array( 'a' => array( 'href' => true ) ) ),
+ esc_url( $this->jp_get_update_php_url() )
+ );
+ $this->jp_update_php_annotation();
+ }
+ } elseif ( ! $compatible_wp ) {
+ esc_html_e( 'This plugin doesn&#8217;t work with your version of WordPress.', 'jetpack' );
+ if ( current_user_can( 'update_core' ) ) {
+ printf(
+ /* translators: %s: "Update WordPress" screen URL */
+ ' ' . wp_kses( __( '<a href="%s">Please update WordPress</a>.', 'jetpack' ), array( 'a' => array( 'href' => true ) ) ),
+ esc_url( self_admin_url( 'update-core.php' ) )
+ );
+ }
+ } elseif ( ! $compatible_php ) {
+ esc_html_e( 'This plugin doesn&#8217;t work with your version of PHP.', 'jetpack' );
+ if ( current_user_can( 'update_php' ) ) {
+ printf(
+ /* translators: %s: "Update PHP" page URL */
+ ' ' . wp_kses( __( '<a href="%s">Learn more about updating PHP</a>.', 'jetpack' ), array( 'a' => array( 'href' => true ) ) ),
+ esc_url( $this->jp_get_update_php_url() )
+ );
+ $this->jp_update_php_annotation();
+ }
+ }
+ echo '</p></div>';
+ }
+ ?>
+
+ <div class="plugin-card-top">
+ <div class="name column-name">
+ <h3>
+ <a href="<?php echo esc_url( $details_link ); ?>" class="jptracks thickbox open-plugin-details-modal" data-jptracks-name="jetpack_about_plugin_modal" data-jptracks-prop="<?php echo esc_attr( $plugin['slug'] ); ?>">
+ <?php echo esc_html( $title ); ?>
+ <img src="<?php echo esc_attr( $plugin_icon_url ); ?>" class="plugin-icon" alt="">
+ </a>
+ </h3>
+ </div>
+ <div class="desc column-description">
+ <p><?php echo esc_html( $description ); ?></p>
+ </div>
+
+ <div class="details-link">
+ <a class="jptracks thickbox open-plugin-details-modal" href="<?php echo esc_url( $details_link ); ?>" data-jptracks-name="jetpack_about_plugin_details_modal" data-jptracks-prop="<?php echo esc_attr( $plugin['slug'] ); ?>"><?php esc_html_e( 'More Details', 'jetpack' ); ?></a>
+ </div>
+ </div>
+
+ <div class="plugin-card-bottom">
+ <div class="meta">
+ <?php
+ wp_star_rating(
+ array(
+ 'rating' => $plugin['rating'],
+ 'type' => 'percent',
+ 'number' => $plugin['num_ratings'],
+ )
+ );
+ ?>
+ <span class="num-ratings" aria-hidden="true">(<?php echo esc_html( number_format_i18n( $plugin['num_ratings'] ) ); ?> <?php esc_html_e( 'ratings', 'jetpack' ); ?>)</span>
+ <div class="downloaded">
+ <?php
+ if ( $plugin['active_installs'] >= 1000000 ) {
+ $active_installs_millions = floor( $plugin['active_installs'] / 1000000 );
+ $active_installs_text = sprintf(
+ /* translators: number of millions of installs. */
+ _nx( '%s+ Million', '%s+ Million', $active_installs_millions, 'Active plugin installations', 'jetpack' ),
+ number_format_i18n( $active_installs_millions )
+ );
+ } elseif ( 0 === $plugin['active_installs'] ) {
+ $active_installs_text = _x( 'Less Than 10', 'Active plugin installations', 'jetpack' );
+ } else {
+ $active_installs_text = number_format_i18n( $plugin['active_installs'] ) . '+';
+ }
+ /* translators: number of active installs */
+ printf( esc_html__( '%s Active Installations', 'jetpack' ), esc_html( $active_installs_text ) );
+ ?>
+ </div>
+ </div>
+
+ <div class="action-links">
+ <?php
+ if ( $action_links ) {
+ // The var simply collects strings that have already been sanitized.
+ // phpcs:ignore WordPress.Security.EscapeOutput
+ echo '<ul class="action-buttons"><li>' . implode( '</li><li>', $action_links ) . '</li></ul>';
+ }
+ ?>
+ </div>
+ </div>
+ </li>
+ <?php
+
+ }
+
+ }
+
+ /**
+ * Fetch Gravatar hashes for public A12s from wpcom and display them as a list.
+ *
+ * @since 7.3
+ */
+ public function display_gravatars() {
+ $hashes = array(
+ 'https://1.gravatar.com/avatar/d2ab03dbab0c97740be75f290a2e3190',
+ 'https://2.gravatar.com/avatar/b0b357b291ac72bc7da81b4d74430fe6',
+ 'https://2.gravatar.com/avatar/9e149207a0e0818abed0edbb1fb2d0bf',
+ 'https://2.gravatar.com/avatar/9f376366854d750124dffe057dda99c9',
+ 'https://1.gravatar.com/avatar/1c75d26ad0d38624f02b15accc1f20cd',
+ 'https://1.gravatar.com/avatar/c510e69d83c7d10be4df64feeff4e46a',
+ 'https://0.gravatar.com/avatar/88ec0dcadea38adf5f30a17e54e9b248',
+ 'https://1.gravatar.com/avatar/bc45834430c5b0936d76e3f468f9ca57',
+ 'https://0.gravatar.com/avatar/032677e4115f3a38dc7785529e8cc4d9',
+ 'https://0.gravatar.com/avatar/72a638c2520ea177976e8eafb201a82f',
+ 'https://0.gravatar.com/avatar/b3618d70c63bbc5cc7caee0beded5ff0',
+ 'https://1.gravatar.com/avatar/4d346581a3340e32cf93703c9ce46bd4',
+ 'https://2.gravatar.com/avatar/9c2f6b95a00dfccfadc6a912a2b859ba',
+ 'https://1.gravatar.com/avatar/1a33e7a69df4f675fcd799edca088ac2',
+ 'https://2.gravatar.com/avatar/d5dc443845c134f365519568d5d80e62',
+ 'https://0.gravatar.com/avatar/c0ccdd53794779bcc07fcae7b79c4d80',
+ );
+ $output = '';
+ foreach ( $hashes as $hash ) {
+ $output .= '<li><img src="' . esc_url( $hash ) . '?s=150"></li>' . "\n";
+ }
+ echo wp_kses(
+ $output,
+ array(
+ 'li' => true,
+ 'img' => array(
+ 'src' => true,
+ ),
+ )
+ );
+ }
+
+ // The following methods jp_get_update_php_url, jp_get_default_update_php_url, and jp_update_php_annotation,
+ // are copies of functions introduced in WP 5.1
+ // At the time of releasing this, we're still supporting WP 5.0, so we needed
+ // to have them here to avoid fatal errors in old installations.
+
+ /**
+ * Gets the URL to learn more about updating the PHP version the site is running on.
+ *
+ * This URL can be overridden by specifying an environment variable `WP_UPDATE_PHP_URL` or by using the
+ * {@see 'wp_update_php_url'} filter. Providing an empty string is not allowed and will result in the
+ * default URL being used. Furthermore the page the URL links to should preferably be localized in the
+ * site language.
+ *
+ * @todo: Remove when 5.1 is minimum WP version.
+ * @since 5.1.0
+ *
+ * @return string URL to learn more about updating PHP.
+ */
+ private function jp_get_update_php_url() {
+ $default_url = $this->jp_get_default_update_php_url();
+
+ $update_url = $default_url;
+ if ( false !== getenv( 'WP_UPDATE_PHP_URL' ) ) {
+ $update_url = getenv( 'WP_UPDATE_PHP_URL' );
+ }
+
+ /**
+ * Filters the URL to learn more about updating the PHP version the site is running on.
+ *
+ * Providing an empty string is not allowed and will result in the default URL being used. Furthermore
+ * the page the URL links to should preferably be localized in the site language.
+ *
+ * @since 5.1.0
+ *
+ * @param string $update_url URL to learn more about updating PHP.
+ */
+ $update_url = apply_filters( 'wp_update_php_url', $update_url );
+
+ if ( empty( $update_url ) ) {
+ $update_url = $default_url;
+ }
+
+ return $update_url;
+ }
+
+ /**
+ * Gets the default URL to learn more about updating the PHP version the site is running on.
+ *
+ * Do not use this function to retrieve this URL. Instead, use {@see wp_get_update_php_url()} when relying on the URL.
+ * This function does not allow modifying the returned URL, and is only used to compare the actually used URL with the
+ * default one.
+ *
+ * @todo: Remove when 5.1 is minimum WP version.
+ * @since 5.1.0
+ * @access private
+ *
+ * @return string Default URL to learn more about updating PHP.
+ */
+ private function jp_get_default_update_php_url() {
+ return _x( 'https://wordpress.org/support/update-php/', 'localized PHP upgrade information page', 'jetpack' );
+ }
+
+ /**
+ * Prints the default annotation for the web host altering the "Update PHP" page URL.
+ *
+ * This function is to be used after {@see wp_get_update_php_url()} to display a consistent
+ * annotation if the web host has altered the default "Update PHP" page URL.
+ *
+ * @todo: Remove when 5.1 is minimum WP version.
+ * @since 5.1.0
+ */
+ private function jp_update_php_annotation() {
+ $update_url = $this->jp_get_update_php_url();
+ $default_url = $this->jp_get_default_update_php_url();
+
+ if ( $update_url === $default_url ) {
+ return;
+ }
+
+ echo '<p class="description">';
+ printf(
+ wp_kses(
+ /* translators: %s: default Update PHP page URL */
+ __( 'This resource is provided by your web host, and is specific to your site. For more information, <a href="%s" target="_blank" rel="noopener noreferrer">see the official WordPress documentation</a>.', 'jetpack' ),
+ array(
+ 'a' => array(
+ 'href' => true,
+ 'rel' => true,
+ ),
+ )
+ ),
+ esc_url( $default_url )
+ );
+ echo '</p>';
+ }
+
+}
diff --git a/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-admin-page.php b/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-admin-page.php
new file mode 100644
index 00000000..a6822678
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-admin-page.php
@@ -0,0 +1,353 @@
+<?php
+
+// Shared logic between Jetpack admin pages
+abstract class Jetpack_Admin_Page {
+ // Add page specific actions given the page hook
+ abstract function add_page_actions( $hook );
+
+ // Create a menu item for the page and returns the hook
+ abstract function get_page_hook();
+
+ // Enqueue and localize page specific scripts
+ abstract function page_admin_scripts();
+
+ // Render page specific HTML
+ abstract function page_render();
+
+ /**
+ * Should we block the page rendering because the site is in IDC?
+ *
+ * @var bool
+ */
+ static $block_page_rendering_for_idc;
+
+ /**
+ * Function called after admin_styles to load any additional needed styles.
+ *
+ * @since 4.3.0
+ */
+ function additional_styles() {}
+
+ function __construct() {
+ $this->jetpack = Jetpack::init();
+ self::$block_page_rendering_for_idc = (
+ Jetpack::validate_sync_error_idc_option() && ! Jetpack_Options::get_option( 'safe_mode_confirmed' )
+ );
+ }
+
+ function add_actions() {
+ global $pagenow;
+
+ // If user is not an admin and site is in Dev Mode, don't do anything
+ if ( ! current_user_can( 'manage_options' ) && Jetpack::is_development_mode() ) {
+ return;
+ }
+
+ // Don't add in the modules page unless modules are available!
+ if ( $this->dont_show_if_not_active && ! Jetpack::is_active() && ! Jetpack::is_development_mode() ) {
+ return;
+ }
+
+ // Initialize menu item for the page in the admin
+ $hook = $this->get_page_hook();
+
+ // Attach hooks common to all Jetpack admin pages based on the created
+ // hook
+ add_action( "load-$hook", array( $this, 'admin_help' ) );
+ add_action( "load-$hook", array( $this, 'admin_page_load' ) );
+ add_action( "admin_print_styles-$hook", array( $this, 'admin_styles' ) );
+ add_action( "admin_print_scripts-$hook", array( $this, 'admin_scripts' ) );
+
+ if ( ! self::$block_page_rendering_for_idc ) {
+ add_action( "admin_print_styles-$hook", array( $this, 'additional_styles' ) );
+ }
+ // If someone just activated Jetpack, let's show them a fullscreen connection banner.
+ if (
+ ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'jetpack' === $_GET['page'] )
+ && ! Jetpack::is_active()
+ && current_user_can( 'jetpack_connect' )
+ && ! Jetpack::is_development_mode()
+ ) {
+ add_action( 'admin_enqueue_scripts', array( 'Jetpack_Connection_Banner', 'enqueue_banner_scripts' ) );
+ add_action( 'admin_print_styles', array( Jetpack::init(), 'admin_banner_styles' ) );
+ add_action( 'admin_notices', array( 'Jetpack_Connection_Banner', 'render_connect_prompt_full_screen' ) );
+ delete_transient( 'activated_jetpack' );
+ }
+
+ // Check if the site plan changed and deactivate modules accordingly.
+ add_action( 'current_screen', array( $this, 'check_plan_deactivate_modules' ) );
+
+ // Attach page specific actions in addition to the above
+ $this->add_page_actions( $hook );
+ }
+
+ // Render the page with a common top and bottom part, and page specific content
+ function render() {
+ // We're in an IDC: we need a decision made before we show the UI again.
+ if ( self::$block_page_rendering_for_idc ) {
+ return;
+ }
+
+ // Check if we are looking at the main dashboard
+ if ( isset( $_GET['page'] ) && 'jetpack' === $_GET['page'] ) {
+ $this->page_render();
+ return;
+ }
+ self::wrap_ui( array( $this, 'page_render' ) );
+ }
+
+ function admin_help() {
+ $this->jetpack->admin_help();
+ }
+
+ function admin_page_load() {
+ // This is big. For the moment, just call the existing one.
+ $this->jetpack->admin_page_load();
+ }
+
+ // Add page specific scripts and jetpack stats for all menu pages
+ function admin_scripts() {
+ $this->page_admin_scripts(); // Delegate to inheriting class
+ add_action( 'admin_footer', array( $this->jetpack, 'do_stats' ) );
+ }
+
+ // Enqueue the Jetpack admin stylesheet
+ function admin_styles() {
+ $min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
+
+ wp_enqueue_style( 'jetpack-admin', plugins_url( "css/jetpack-admin{$min}.css", JETPACK__PLUGIN_FILE ), array( 'genericons' ), JETPACK__VERSION . '-20121016' );
+ wp_style_add_data( 'jetpack-admin', 'rtl', 'replace' );
+ wp_style_add_data( 'jetpack-admin', 'suffix', $min );
+ }
+
+ /**
+ * Checks if REST API is enabled.
+ *
+ * @since 4.4.2
+ *
+ * @return bool
+ */
+ function is_rest_api_enabled() {
+ return /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
+ apply_filters( 'rest_enabled', true ) &&
+ /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
+ apply_filters( 'rest_jsonp_enabled', true ) &&
+ /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
+ apply_filters( 'rest_authentication_errors', true );
+ }
+
+ /**
+ * Checks the site plan and deactivates modules that were active but are no longer included in the plan.
+ *
+ * @since 4.4.0
+ *
+ * @param $page
+ *
+ * @return array
+ */
+ function check_plan_deactivate_modules( $page ) {
+ if (
+ Jetpack::is_development_mode()
+ || ! in_array(
+ $page->base,
+ array(
+ 'toplevel_page_jetpack',
+ 'admin_page_jetpack_modules',
+ 'jetpack_page_vaultpress',
+ 'jetpack_page_stats',
+ 'jetpack_page_akismet-key-config',
+ )
+ )
+ ) {
+ return false;
+ }
+
+ $current = Jetpack_Plan::get();
+
+ $to_deactivate = array();
+ if ( isset( $current['product_slug'] ) ) {
+ $active = Jetpack::get_active_modules();
+ switch ( $current['product_slug'] ) {
+ case 'jetpack_free':
+ $to_deactivate = array( 'seo-tools', 'videopress', 'google-analytics', 'wordads', 'search' );
+ break;
+ case 'jetpack_personal':
+ case 'jetpack_personal_monthly':
+ $to_deactivate = array( 'seo-tools', 'videopress', 'google-analytics', 'wordads', 'search' );
+ break;
+ case 'jetpack_premium':
+ case 'jetpack_premium_monthly':
+ $to_deactivate = array( 'seo-tools', 'google-analytics', 'search' );
+ break;
+ }
+ $to_deactivate = array_intersect( $active, $to_deactivate );
+
+ $to_leave_enabled = array();
+ foreach ( $to_deactivate as $feature ) {
+ if ( Jetpack_Plan::supports( $feature ) ) {
+ $to_leave_enabled [] = $feature;
+ }
+ }
+ $to_deactivate = array_diff( $to_deactivate, $to_leave_enabled );
+
+ if ( ! empty( $to_deactivate ) ) {
+ Jetpack::update_active_modules( array_filter( array_diff( $active, $to_deactivate ) ) );
+ }
+ }
+ return array(
+ 'current' => $current,
+ 'deactivate' => $to_deactivate,
+ );
+ }
+
+ static function load_wrapper_styles() {
+ $rtl = is_rtl() ? '.rtl' : '';
+ wp_enqueue_style( 'dops-css', plugins_url( "_inc/build/admin.dops-style{$rtl}.css", JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
+ wp_enqueue_style( 'components-css', plugins_url( "_inc/build/style.min{$rtl}.css", JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
+ $custom_css = '
+ #wpcontent {
+ padding-left: 0 !important;
+ }
+ #wpbody-content {
+ background-color: #f6f6f6;
+ }
+
+ #jp-plugin-container .wrap {
+ margin: 0 auto;
+ max-width:45rem;
+ padding: 0 1.5rem;
+ }
+ #jp-plugin-container.is-wide .wrap {
+ max-width: 1040px;
+ }
+ #jp-plugin-container .wrap .jetpack-wrap-container {
+ margin-top: 1em;
+ }
+ .wp-admin #dolly {
+ float: none;
+ position: relative;
+ right: 0;
+ left: 0;
+ top: 0;
+ padding: .625rem;
+ text-align: right;
+ background: #fff;
+ font-size: .75rem;
+ font-style: italic;
+ color: #87a6bc;
+ border-bottom: 1px #e9eff3 solid;
+ }
+ ';
+ wp_add_inline_style( 'dops-css', $custom_css );
+ }
+
+ public static function wrap_ui( $callback, $args = array() ) {
+ $defaults = array(
+ 'is-wide' => false,
+ 'show-nav' => true,
+ );
+ $args = wp_parse_args( $args, $defaults );
+ $jetpack_admin_url = admin_url( 'admin.php?page=jetpack' );
+
+ ?>
+ <div id="jp-plugin-container" class="
+ <?php
+ if ( $args['is-wide'] ) {
+ echo 'is-wide'; }
+ ?>
+ ">
+
+ <div class="jp-masthead">
+ <div class="jp-masthead__inside-container">
+ <div class="jp-masthead__logo-container">
+ <a class="jp-masthead__logo-link" href="<?php echo esc_url( $jetpack_admin_url ); ?>">
+ <svg class="jetpack-logo__masthead" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" height="32" viewBox="0 0 118 32"><path fill="#00BE28" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M15,19H7l8-16V19z M17,29V13h8L17,29z"></path><path d="M41.3,26.6c-0.5-0.7-0.9-1.4-1.3-2.1c2.3-1.4,3-2.5,3-4.6V8h-3V6h6v13.4C46,22.8,45,24.8,41.3,26.6z"></path><path d="M65,18.4c0,1.1,0.8,1.3,1.4,1.3c0.5,0,2-0.2,2.6-0.4v2.1c-0.9,0.3-2.5,0.5-3.7,0.5c-1.5,0-3.2-0.5-3.2-3.1V12H60v-2h2.1V7.1 H65V10h4v2h-4V18.4z"></path><path d="M71,10h3v1.3c1.1-0.8,1.9-1.3,3.3-1.3c2.5,0,4.5,1.8,4.5,5.6s-2.2,6.3-5.8,6.3c-0.9,0-1.3-0.1-2-0.3V28h-3V10z M76.5,12.3 c-0.8,0-1.6,0.4-2.5,1.2v5.9c0.6,0.1,0.9,0.2,1.8,0.2c2,0,3.2-1.3,3.2-3.9C79,13.4,78.1,12.3,76.5,12.3z"></path><path d="M93,22h-3v-1.5c-0.9,0.7-1.9,1.5-3.5,1.5c-1.5,0-3.1-1.1-3.1-3.2c0-2.9,2.5-3.4,4.2-3.7l2.4-0.3v-0.3c0-1.5-0.5-2.3-2-2.3 c-0.7,0-2.3,0.5-3.7,1.1L84,11c1.2-0.4,3-1,4.4-1c2.7,0,4.6,1.4,4.6,4.7L93,22z M90,16.4l-2.2,0.4c-0.7,0.1-1.4,0.5-1.4,1.6 c0,0.9,0.5,1.4,1.3,1.4s1.5-0.5,2.3-1V16.4z"></path><path d="M104.5,21.3c-1.1,0.4-2.2,0.6-3.5,0.6c-4.2,0-5.9-2.4-5.9-5.9c0-3.7,2.3-6,6.1-6c1.4,0,2.3,0.2,3.2,0.5V13 c-0.8-0.3-2-0.6-3.2-0.6c-1.7,0-3.2,0.9-3.2,3.6c0,2.9,1.5,3.8,3.3,3.8c0.9,0,1.9-0.2,3.2-0.7V21.3z"></path><path d="M110,15.2c0.2-0.3,0.2-0.8,3.8-5.2h3.7l-4.6,5.7l5,6.3h-3.7l-4.2-5.8V22h-3V6h3V15.2z"></path><path d="M58.5,21.3c-1.5,0.5-2.7,0.6-4.2,0.6c-3.6,0-5.8-1.8-5.8-6c0-3.1,1.9-5.9,5.5-5.9s4.9,2.5,4.9,4.9c0,0.8,0,1.5-0.1,2h-7.3 c0.1,2.5,1.5,2.8,3.6,2.8c1.1,0,2.2-0.3,3.4-0.7C58.5,19,58.5,21.3,58.5,21.3z M56,15c0-1.4-0.5-2.9-2-2.9c-1.4,0-2.3,1.3-2.4,2.9 C51.6,15,56,15,56,15z"></path></svg>
+ </a>
+ </div>
+ <?php
+ if ( $args['show-nav'] ) :
+ ?>
+ <div class="jp-masthead__nav">
+ <?php
+ if ( is_network_admin() ) {
+ $current_screen = get_current_screen();
+
+ $highlight_current_sites = ( 'toplevel_page_jetpack-network' === $current_screen->id ? 'is-primary' : '' );
+ $highlight_current_settings = ( 'jetpack_page_jetpack-settings-network' === $current_screen->id ? 'is-primary' : '' );
+ ?>
+ <span class="dops-button-group">
+ <?php
+ if ( current_user_can( 'jetpack_network_sites_page' ) ) {
+ ?>
+ <a href="<?php echo esc_url( network_admin_url( 'admin.php?page=jetpack' ) ); ?>" type="button" class="<?php echo esc_attr( $highlight_current_sites ); ?> dops-button is-compact" title="<?php esc_html_e( "Manage your network's Jetpack Sites.", 'jetpack' ); ?>"><?php echo esc_html_x( 'Sites', 'Navigation item', 'jetpack' ); ?></a>
+ <?php
+ } if ( current_user_can( 'jetpack_network_settings_page' ) ) {
+ ?>
+ <a href="<?php echo esc_url( network_admin_url( 'admin.php?page=jetpack-settings' ) ); ?>" type="button" class="<?php echo esc_attr( $highlight_current_settings ); ?> dops-button is-compact" title="<?php esc_html_e( "Manage your network's Jetpack Sites.", 'jetpack' ); ?>"><?php echo esc_html_x( 'Network Settings', 'Navigation item', 'jetpack' ); ?></a>
+ <?php
+ }
+ ?>
+ </span>
+ <?php } else { ?>
+ <span class="dops-button-group">
+ <a href="<?php echo esc_url( $jetpack_admin_url ); ?>" type="button" class="dops-button is-compact"><?php esc_html_e( 'Dashboard', 'jetpack' ); ?></a>
+ <?php
+ if ( current_user_can( 'jetpack_manage_modules' ) ) {
+ ?>
+ <a href="<?php echo esc_url( $jetpack_admin_url . '#/settings' ); ?>" type="button" class="dops-button is-compact"><?php esc_html_e( 'Settings', 'jetpack' ); ?></a>
+ <?php
+ }
+ ?>
+ </span>
+ <?php } ?>
+ </div>
+ <?php endif; ?>
+ </div>
+ </div>
+ <div class="wrap"><div id="jp-admin-notices" aria-live="polite"></div></div>
+ <!-- START OF CALLBACK -->
+ <?php
+ ob_start();
+ call_user_func( $callback );
+ $callback_ui = ob_get_contents();
+ ob_end_clean();
+ echo $callback_ui;
+ ?>
+ <!-- END OF CALLBACK -->
+ <div class="jp-footer">
+ <div class="jp-footer__a8c-attr-container"><a href="https://automattic.com" target="_blank" rel="noopener noreferrer"><svg role="img" class="jp-footer__a8c-attr" x="0" y="0" viewBox="0 0 935 38.2" enable-background="new 0 0 935 38.2" aria-labelledby="a8c-svg-title"><title id="a8c-svg-title">An Automattic Airline</title><path d="M317.1 38.2c-12.6 0-20.7-9.1-20.7-18.5v-1.2c0-9.6 8.2-18.5 20.7-18.5 12.6 0 20.8 8.9 20.8 18.5v1.2C337.9 29.1 329.7 38.2 317.1 38.2zM331.2 18.6c0-6.9-5-13-14.1-13s-14 6.1-14 13v0.9c0 6.9 5 13.1 14 13.1s14.1-6.2 14.1-13.1V18.6zM175 36.8l-4.7-8.8h-20.9l-4.5 8.8h-7L157 1.3h5.5L182 36.8H175zM159.7 8.2L152 23.1h15.7L159.7 8.2zM212.4 38.2c-12.7 0-18.7-6.9-18.7-16.2V1.3h6.6v20.9c0 6.6 4.3 10.5 12.5 10.5 8.4 0 11.9-3.9 11.9-10.5V1.3h6.7V22C231.4 30.8 225.8 38.2 212.4 38.2zM268.6 6.8v30h-6.7v-30h-15.5V1.3h37.7v5.5H268.6zM397.3 36.8V8.7l-1.8 3.1 -14.9 25h-3.3l-14.7-25 -1.8-3.1v28.1h-6.5V1.3h9.2l14 24.4 1.7 3 1.7-3 13.9-24.4h9.1v35.5H397.3zM454.4 36.8l-4.7-8.8h-20.9l-4.5 8.8h-7l19.2-35.5h5.5l19.5 35.5H454.4zM439.1 8.2l-7.7 14.9h15.7L439.1 8.2zM488.4 6.8v30h-6.7v-30h-15.5V1.3h37.7v5.5H488.4zM537.3 6.8v30h-6.7v-30h-15.5V1.3h37.7v5.5H537.3zM569.3 36.8V4.6c2.7 0 3.7-1.4 3.7-3.4h2.8v35.5L569.3 36.8 569.3 36.8zM628 11.3c-3.2-2.9-7.9-5.7-14.2-5.7 -9.5 0-14.8 6.5-14.8 13.3v0.7c0 6.7 5.4 13 15.3 13 5.9 0 10.8-2.8 13.9-5.7l4 4.2c-3.9 3.8-10.5 7.1-18.3 7.1 -13.4 0-21.6-8.7-21.6-18.3v-1.2c0-9.6 8.9-18.7 21.9-18.7 7.5 0 14.3 3.1 18 7.1L628 11.3zM321.5 12.4c1.2 0.8 1.5 2.4 0.8 3.6l-6.1 9.4c-0.8 1.2-2.4 1.6-3.6 0.8l0 0c-1.2-0.8-1.5-2.4-0.8-3.6l6.1-9.4C318.7 11.9 320.3 11.6 321.5 12.4L321.5 12.4z"></path><path d="M37.5 36.7l-4.7-8.9H11.7l-4.6 8.9H0L19.4 0.8H25l19.7 35.9H37.5zM22 7.8l-7.8 15.1h15.9L22 7.8zM82.8 36.7l-23.3-24 -2.3-2.5v26.6h-6.7v-36H57l22.6 24 2.3 2.6V0.8h6.7v35.9H82.8z"></path><path d="M719.9 37l-4.8-8.9H694l-4.6 8.9h-7.1l19.5-36h5.6l19.8 36H719.9zM704.4 8l-7.8 15.1h15.9L704.4 8zM733 37V1h6.8v36H733zM781 37c-1.8 0-2.6-2.5-2.9-5.8l-0.2-3.7c-0.2-3.6-1.7-5.1-8.4-5.1h-12.8V37H750V1h19.6c10.8 0 15.7 4.3 15.7 9.9 0 3.9-2 7.7-9 9 7 0.5 8.5 3.7 8.6 7.9l0.1 3c0.1 2.5 0.5 4.3 2.2 6.1V37H781zM778.5 11.8c0-2.6-2.1-5.1-7.9-5.1h-13.8v10.8h14.4c5 0 7.3-2.4 7.3-5.2V11.8zM794.8 37V1h6.8v30.4h28.2V37H794.8zM836.7 37V1h6.8v36H836.7zM886.2 37l-23.4-24.1 -2.3-2.5V37h-6.8V1h6.5l22.7 24.1 2.3 2.6V1h6.8v36H886.2zM902.3 37V1H935v5.6h-26v9.2h20v5.5h-20v10.1h26V37H902.3z"></path></svg></a></div>
+ <ul class="jp-footer__links">
+ <li class="jp-footer__link-item">
+ <a href="https://jetpack.com" target="_blank" rel="noopener noreferrer" class="jp-footer__link" title="<?php esc_html_e( 'Jetpack version', 'jetpack' ); ?>">Jetpack <?php echo JETPACK__VERSION; ?></a>
+ </li>
+ <li class="jp-footer__link-item">
+ <a href="https://wordpress.com/tos/" target="_blank" rel="noopener noreferrer" title="<?php esc_html__( 'WordPress.com Terms of Service', 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Terms', 'Navigation item', 'jetpack' ); ?></a>
+ </li>
+ <li class="jp-footer__link-item">
+ <a href="<?php echo esc_url( $jetpack_admin_url . '#/privacy' ); ?>" rel="noopener noreferrer" title="<?php esc_html_e( "Automattic's Privacy Policy", 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Privacy', 'Navigation item', 'jetpack' ); ?></a>
+ </li>
+ <?php if ( is_multisite() && current_user_can( 'jetpack_network_sites_page' ) ) { ?>
+ <li class="jp-footer__link-item">
+ <a href="<?php echo esc_url( network_admin_url( 'admin.php?page=jetpack' ) ); ?>" title="<?php esc_html_e( "Manage your network's Jetpack Sites.", 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Network Sites', 'Navigation item', 'jetpack' ); ?></a>
+ </li>
+ <?php } ?>
+ <?php if ( is_multisite() && current_user_can( 'jetpack_network_settings_page' ) ) { ?>
+ <li class="jp-footer__link-item">
+ <a href="<?php echo esc_url( network_admin_url( 'admin.php?page=jetpack-settings' ) ); ?>" title="<?php esc_html_e( "Manage your network's Jetpack Sites.", 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Network Settings', 'Navigation item', 'jetpack' ); ?></a>
+ </li>
+ <?php } ?>
+ <?php if ( current_user_can( 'manage_options' ) ) { ?>
+ <li class="jp-footer__link-item">
+ <a href="<?php echo esc_url( admin_url() . 'admin.php?page=jetpack_modules' ); ?>" title="<?php esc_html_e( "Access the full list of Jetpack modules available on your site.", 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Modules', 'Navigation item', 'jetpack' ); ?></a>
+ </li>
+ <li class="jp-footer__link-item">
+ <a href="<?php echo esc_url( admin_url() . 'admin.php?page=jetpack-debugger' ); ?>" title="<?php esc_html_e( "Test your site's compatibility with Jetpack.", 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Debug', 'Navigation item', 'jetpack' ); ?></a>
+ </li>
+ <?php } ?>
+ </ul>
+ </div>
+ </div>
+ <?php
+ return;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-landing-page.php b/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-landing-page.php
new file mode 100644
index 00000000..5c06c284
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-landing-page.php
@@ -0,0 +1,3 @@
+<?php
+// This is intentionally left empty as a stub because some sites were caching the require()
+// @see https://github.com/Automattic/jetpack/issues/5091
diff --git a/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-react-page.php b/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-react-page.php
new file mode 100644
index 00000000..e5a423f0
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-react-page.php
@@ -0,0 +1,399 @@
+<?php
+include_once( 'class.jetpack-admin-page.php' );
+
+// Builds the landing page and its menu
+class Jetpack_React_Page extends Jetpack_Admin_Page {
+
+ protected $dont_show_if_not_active = false;
+
+ protected $is_redirecting = false;
+
+ function get_page_hook() {
+ // Add the main admin Jetpack menu
+ return add_menu_page( 'Jetpack', 'Jetpack', 'jetpack_admin_page', 'jetpack', array( $this, 'render' ), 'div' );
+ }
+
+ function add_page_actions( $hook ) {
+ /** This action is documented in class.jetpack.php */
+ do_action( 'jetpack_admin_menu', $hook );
+
+ // Place the Jetpack menu item on top and others in the order they appear
+ add_filter( 'custom_menu_order', '__return_true' );
+ add_filter( 'menu_order', array( $this, 'jetpack_menu_order' ) );
+
+ if ( ! isset( $_GET['page'] ) || 'jetpack' !== $_GET['page'] ) {
+ return; // No need to handle the fallback redirection if we are not on the Jetpack page
+ }
+
+ // Adding a redirect meta tag if the REST API is disabled
+ if ( ! $this->is_rest_api_enabled() ) {
+ $this->is_redirecting = true;
+ add_action( 'admin_head', array( $this, 'add_fallback_head_meta' ) );
+ }
+
+ // Adding a redirect meta tag wrapped in noscript tags for all browsers in case they have JavaScript disabled
+ add_action( 'admin_head', array( $this, 'add_noscript_head_meta' ) );
+
+ // If this is the first time the user is viewing the admin, don't show JITMs.
+ // This filter is added just in time because this function is called on admin_menu
+ // and JITMs are initialized on admin_init
+ if ( Jetpack::is_active() && ! Jetpack_Options::get_option( 'first_admin_view', false ) ) {
+ Jetpack_Options::update_option( 'first_admin_view', true );
+ add_filter( 'jetpack_just_in_time_msgs', '__return_false' );
+ }
+ }
+
+ /**
+ * Add Jetpack Dashboard sub-link and point it to AAG if the user can view stats, manage modules or if Protect is active.
+ *
+ * Works in Dev Mode or when user is connected.
+ *
+ * @since 4.3.0
+ */
+ function jetpack_add_dashboard_sub_nav_item() {
+ if ( Jetpack::is_development_mode() || Jetpack::is_active() ) {
+ global $submenu;
+ if ( current_user_can( 'jetpack_admin_page' ) ) {
+ $submenu['jetpack'][] = array( __( 'Dashboard', 'jetpack' ), 'jetpack_admin_page', 'admin.php?page=jetpack#/dashboard' );
+ }
+ }
+ }
+
+ /**
+ * If user is allowed to see the Jetpack Admin, add Settings sub-link.
+ *
+ * @since 4.3.0
+ */
+ function jetpack_add_settings_sub_nav_item() {
+ if ( ( Jetpack::is_development_mode() || Jetpack::is_active() ) && current_user_can( 'jetpack_admin_page' ) && current_user_can( 'edit_posts' ) ) {
+ global $submenu;
+ $submenu['jetpack'][] = array( __( 'Settings', 'jetpack' ), 'jetpack_admin_page', 'admin.php?page=jetpack#/settings' );
+ }
+ }
+
+ function add_fallback_head_meta() {
+ echo '<meta http-equiv="refresh" content="0; url=?page=jetpack_modules">';
+ }
+
+ function add_noscript_head_meta() {
+ echo '<noscript>';
+ $this->add_fallback_head_meta();
+ echo '</noscript>';
+ }
+
+ function jetpack_menu_order( $menu_order ) {
+ $jp_menu_order = array();
+
+ foreach ( $menu_order as $index => $item ) {
+ if ( $item != 'jetpack' )
+ $jp_menu_order[] = $item;
+
+ if ( $index == 0 )
+ $jp_menu_order[] = 'jetpack';
+ }
+
+ return $jp_menu_order;
+ }
+
+ function page_render() {
+ /** This action is already documented in views/admin/admin-page.php */
+ do_action( 'jetpack_notices' );
+
+ // Try fetching by patch
+ $static_html = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static.html' );
+
+ if ( false === $static_html ) {
+
+ // If we still have nothing, display an error
+ echo '<p>';
+ esc_html_e( 'Error fetching static.html. Try running: ', 'jetpack' );
+ echo '<code>yarn distclean && yarn build</code>';
+ echo '</p>';
+ } else {
+
+ // We got the static.html so let's display it
+ echo $static_html;
+ }
+ }
+
+ /**
+ * Gets array of any Jetpack notices that have been dismissed.
+ *
+ * @since 4.0.1
+ * @return mixed|void
+ */
+ function get_dismissed_jetpack_notices() {
+ $jetpack_dismissed_notices = get_option( 'jetpack_dismissed_notices', array() );
+ /**
+ * Array of notices that have been dismissed.
+ *
+ * @since 4.0.1
+ *
+ * @param array $jetpack_dismissed_notices If empty, will not show any Jetpack notices.
+ */
+ $dismissed_notices = apply_filters( 'jetpack_dismissed_notices', $jetpack_dismissed_notices );
+ return $dismissed_notices;
+ }
+
+ function additional_styles() {
+ Jetpack_Admin_Page::load_wrapper_styles();
+ }
+
+ function page_admin_scripts() {
+ if ( $this->is_redirecting ) {
+ return; // No need for scripts on a fallback page
+ }
+
+ $script_deps_path = JETPACK__PLUGIN_DIR . '_inc/build/admin.deps.json';
+ $script_dependencies = file_exists( $script_deps_path )
+ ? json_decode( file_get_contents( $script_deps_path ) )
+ : array();
+ $script_dependencies[] = 'wp-polyfill';
+
+ wp_enqueue_script(
+ 'react-plugin',
+ plugins_url( '_inc/build/admin.js', JETPACK__PLUGIN_FILE ),
+ $script_dependencies,
+ JETPACK__VERSION,
+ true
+ );
+
+ if ( ! Jetpack::is_development_mode() && Jetpack::is_active() ) {
+ // Required for Analytics.
+ wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
+ }
+
+ // Add objects to be passed to the initial state of the app.
+ wp_localize_script( 'react-plugin', 'Initial_State', $this->get_initial_state() );
+ }
+
+ function get_initial_state() {
+ // Load API endpoint base classes and endpoints for getting the module list fed into the JS Admin Page
+ require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php';
+ require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php';
+ $moduleListEndpoint = new Jetpack_Core_API_Module_List_Endpoint();
+ $modules = $moduleListEndpoint->get_modules();
+
+ // Preparing translated fields for JSON encoding by transforming all HTML entities to
+ // respective characters.
+ foreach( $modules as $slug => $data ) {
+ $modules[ $slug ]['name'] = html_entity_decode( $data['name'] );
+ $modules[ $slug ]['description'] = html_entity_decode( $data['description'] );
+ $modules[ $slug ]['short_description'] = html_entity_decode( $data['short_description'] );
+ $modules[ $slug ]['long_description'] = html_entity_decode( $data['long_description'] );
+ }
+
+ // Collecting roles that can view site stats.
+ $stats_roles = array();
+ $enabled_roles = function_exists( 'stats_get_option' ) ? stats_get_option( 'roles' ) : array( 'administrator' );
+
+ if ( ! function_exists( 'get_editable_roles' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/user.php';
+ }
+ foreach ( get_editable_roles() as $slug => $role ) {
+ $stats_roles[ $slug ] = array(
+ 'name' => translate_user_role( $role['name'] ),
+ 'canView' => is_array( $enabled_roles ) ? in_array( $slug, $enabled_roles, true ) : false,
+ );
+ }
+
+ // Get information about current theme.
+ $current_theme = wp_get_theme();
+
+ // Get all themes that Infinite Scroll provides support for natively.
+ $inf_scr_support_themes = array();
+ foreach ( Jetpack::glob_php( JETPACK__PLUGIN_DIR . 'modules/infinite-scroll/themes' ) as $path ) {
+ if ( is_readable( $path ) ) {
+ $inf_scr_support_themes[] = basename( $path, '.php' );
+ }
+ }
+
+ // Get last post, to build the link to Customizer in the Related Posts module.
+ $last_post = get_posts( array( 'posts_per_page' => 1 ) );
+ $last_post = isset( $last_post[0] ) && $last_post[0] instanceof WP_Post
+ ? get_permalink( $last_post[0]->ID )
+ : get_home_url();
+
+ // Ensure that class to get the affiliate code is loaded
+ if ( ! class_exists( 'Jetpack_Affiliate' ) ) {
+ require_once JETPACK__PLUGIN_DIR . 'class.jetpack-affiliate.php';
+ }
+
+ return array(
+ 'WP_API_root' => esc_url_raw( rest_url() ),
+ 'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
+ 'pluginBaseUrl' => plugins_url( '', JETPACK__PLUGIN_FILE ),
+ 'connectionStatus' => array(
+ 'isActive' => Jetpack::is_active(),
+ 'isStaging' => Jetpack::is_staging_site(),
+ 'devMode' => array(
+ 'isActive' => Jetpack::is_development_mode(),
+ 'constant' => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
+ 'url' => site_url() && false === strpos( site_url(), '.' ),
+ 'filter' => apply_filters( 'jetpack_development_mode', false ),
+ ),
+ 'isPublic' => '1' == get_option( 'blog_public' ),
+ 'isInIdentityCrisis' => Jetpack::validate_sync_error_idc_option(),
+ 'sandboxDomain' => JETPACK__SANDBOX_DOMAIN,
+ ),
+ 'connectUrl' => Jetpack::init()->build_connect_url( true, false, false ),
+ 'dismissedNotices' => $this->get_dismissed_jetpack_notices(),
+ 'isDevVersion' => Jetpack::is_development_version(),
+ 'currentVersion' => JETPACK__VERSION,
+ 'is_gutenberg_available' => true,
+ 'getModules' => $modules,
+ 'showJumpstart' => jetpack_show_jumpstart(),
+ 'rawUrl' => Jetpack::build_raw_urls( get_home_url() ),
+ 'adminUrl' => esc_url( admin_url() ),
+ 'stats' => array(
+ // data is populated asynchronously on page load
+ 'data' => array(
+ 'general' => false,
+ 'day' => false,
+ 'week' => false,
+ 'month' => false,
+ ),
+ 'roles' => $stats_roles,
+ ),
+ 'aff' => Jetpack_Affiliate::init()->get_affiliate_code(),
+ 'settings' => $this->get_flattened_settings( $modules ),
+ 'userData' => array(
+// 'othersLinked' => Jetpack::get_other_linked_admins(),
+ 'currentUser' => jetpack_current_user_data(),
+ ),
+ 'siteData' => array(
+ 'icon' => has_site_icon()
+ ? apply_filters( 'jetpack_photon_url', get_site_icon_url(), array( 'w' => 64 ) )
+ : '',
+ 'siteVisibleToSearchEngines' => '1' == get_option( 'blog_public' ),
+ /**
+ * Whether promotions are visible or not.
+ *
+ * @since 4.8.0
+ *
+ * @param bool $are_promotions_active Status of promotions visibility. True by default.
+ */
+ 'showPromotions' => apply_filters( 'jetpack_show_promotions', true ),
+ 'isAtomicSite' => jetpack_is_atomic_site(),
+ 'plan' => Jetpack_Plan::get(),
+ 'showBackups' => Jetpack::show_backups_ui(),
+ ),
+ 'themeData' => array(
+ 'name' => $current_theme->get( 'Name' ),
+ 'hasUpdate' => (bool) get_theme_update_available( $current_theme ),
+ 'support' => array(
+ 'infinite-scroll' => current_theme_supports( 'infinite-scroll' ) || in_array( $current_theme->get_stylesheet(), $inf_scr_support_themes ),
+ ),
+ ),
+ 'locale' => Jetpack::get_i18n_data_json(),
+ 'localeSlug' => join( '-', explode( '_', get_user_locale() ) ),
+ 'jetpackStateNotices' => array(
+ 'messageCode' => Jetpack::state( 'message' ),
+ 'errorCode' => Jetpack::state( 'error' ),
+ 'errorDescription' => Jetpack::state( 'error_description' ),
+ ),
+ 'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(),
+ 'currentIp' => function_exists( 'jetpack_protect_get_ip' ) ? jetpack_protect_get_ip() : false,
+ 'lastPostUrl' => esc_url( $last_post ),
+ 'externalServicesConnectUrls' => $this->get_external_services_connect_urls()
+ );
+ }
+
+ function get_external_services_connect_urls() {
+ $connect_urls = array();
+ jetpack_require_lib( 'class.jetpack-keyring-service-helper' );
+ foreach ( Jetpack_Keyring_Service_Helper::$SERVICES as $service_name => $service_info ) {
+ $connect_urls[ $service_name ] = Jetpack_Keyring_Service_Helper::connect_url( $service_name, $service_info[ 'for' ] );
+ }
+ return $connect_urls;
+ }
+
+ /**
+ * Returns an array of modules and settings both as first class members of the object.
+ *
+ * @param array $modules the result of an API request to get all modules.
+ *
+ * @return array flattened settings with modules.
+ */
+ function get_flattened_settings( $modules ) {
+ $core_api_endpoint = new Jetpack_Core_API_Data();
+ $settings = $core_api_endpoint->get_all_options();
+ return $settings->data;
+ }
+}
+
+/*
+ * Only show Jump Start on first activation.
+ * Any option 'jumpstart' other than 'new connection' will hide it.
+ *
+ * The option can be of 4 things, and will be stored as such:
+ * new_connection : Brand new connection - Show
+ * jumpstart_activated : Jump Start has been activated - dismiss
+ * jumpstart_dismissed : Manual dismissal of Jump Start - dismiss
+ * jetpack_action_taken: Deprecated since 7.3 But still listed here to respect behaviour for old versions.
+ * Manual activation of a module already happened - dismiss.
+ *
+ * @todo move to functions.global.php when available
+ * @since 3.6
+ * @return bool | show or hide
+ */
+function jetpack_show_jumpstart() {
+ if ( ! Jetpack::is_active() ) {
+ return false;
+ }
+ $jumpstart_option = Jetpack_Options::get_option( 'jumpstart' );
+
+ $hide_options = array(
+ 'jumpstart_activated',
+ 'jetpack_action_taken',
+ 'jumpstart_dismissed'
+ );
+
+ if ( ! $jumpstart_option || in_array( $jumpstart_option, $hide_options ) ) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Gather data about the current user.
+ *
+ * @since 4.1.0
+ *
+ * @return array
+ */
+function jetpack_current_user_data() {
+ $current_user = wp_get_current_user();
+ $is_master_user = $current_user->ID == Jetpack_Options::get_option( 'master_user' );
+ $dotcom_data = Jetpack::get_connected_user_data();
+ // Add connected user gravatar to the returned dotcom_data.
+ $dotcom_data['avatar'] = get_avatar_url( $dotcom_data['email'], array( 'size' => 64, 'default' => 'mysteryman' ) );
+
+ $current_user_data = array(
+ 'isConnected' => Jetpack::is_user_connected( $current_user->ID ),
+ 'isMaster' => $is_master_user,
+ 'username' => $current_user->user_login,
+ 'id' => $current_user->ID,
+ 'wpcomUser' => $dotcom_data,
+ 'gravatar' => get_avatar( $current_user->ID, 40, 'mm', '', array( 'force_display' => true ) ),
+ 'permissions' => array(
+ 'admin_page' => current_user_can( 'jetpack_admin_page' ),
+ 'connect' => current_user_can( 'jetpack_connect' ),
+ 'disconnect' => current_user_can( 'jetpack_disconnect' ),
+ 'manage_modules' => current_user_can( 'jetpack_manage_modules' ),
+ 'network_admin' => current_user_can( 'jetpack_network_admin_page' ),
+ 'network_sites_page' => current_user_can( 'jetpack_network_sites_page' ),
+ 'edit_posts' => current_user_can( 'edit_posts' ),
+ 'publish_posts' => current_user_can( 'publish_posts' ),
+ 'manage_options' => current_user_can( 'manage_options' ),
+ 'view_stats' => current_user_can( 'view_stats' ),
+ 'manage_plugins' => current_user_can( 'install_plugins' )
+ && current_user_can( 'activate_plugins' )
+ && current_user_can( 'update_plugins' )
+ && current_user_can( 'delete_plugins' ),
+ ),
+ );
+
+ return $current_user_data;
+}
diff --git a/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-settings-page.php b/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-settings-page.php
new file mode 100644
index 00000000..69991c70
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/admin-pages/class.jetpack-settings-page.php
@@ -0,0 +1,141 @@
+<?php
+include_once( 'class.jetpack-admin-page.php' );
+include_once( JETPACK__PLUGIN_DIR . 'class.jetpack-modules-list-table.php' );
+
+// Builds the settings page and its menu
+class Jetpack_Settings_Page extends Jetpack_Admin_Page {
+
+ // Show the settings page only when Jetpack is connected or in dev mode
+ protected $dont_show_if_not_active = true;
+
+ function add_page_actions( $hook ) {}
+
+ // Adds the Settings sub menu
+ function get_page_hook() {
+ return add_submenu_page(
+ null,
+ __( 'Jetpack Settings', 'jetpack' ),
+ __( 'Settings', 'jetpack' ),
+ 'jetpack_manage_modules',
+ 'jetpack_modules',
+ array( $this, 'render' )
+ );
+ }
+
+ // Renders the module list table where you can use bulk action or row
+ // actions to activate/deactivate and configure modules
+ function page_render() {
+ $list_table = new Jetpack_Modules_List_Table;
+
+ // We have static.html so let's continue trying to fetch the others
+ $noscript_notice = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static-noscript-notice.html' );
+ $rest_api_notice = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static-version-notice.html' );
+
+ $noscript_notice = str_replace(
+ '#HEADER_TEXT#',
+ esc_html__( 'You have JavaScript disabled', 'jetpack' ),
+ $noscript_notice
+ );
+ $noscript_notice = str_replace(
+ '#TEXT#',
+ esc_html__( "Turn on JavaScript to unlock Jetpack's full potential!", 'jetpack' ),
+ $noscript_notice
+ );
+
+ $rest_api_notice = str_replace(
+ '#HEADER_TEXT#',
+ esc_html( __( 'WordPress REST API is disabled', 'jetpack' ) ),
+ $rest_api_notice
+ );
+ $rest_api_notice = str_replace(
+ '#TEXT#',
+ esc_html( __( "Enable WordPress REST API to unlock Jetpack's full potential!", 'jetpack' ) ),
+ $rest_api_notice
+ );
+
+ if ( ! $this->is_rest_api_enabled() ) {
+ echo $rest_api_notice;
+ }
+ echo $noscript_notice;
+ ?>
+
+ <div class="page-content configure">
+ <div class="frame top hide-if-no-js">
+ <div class="wrap">
+ <div class="manage-left">
+ <table class="table table-bordered fixed-top">
+ <thead>
+ <tr>
+ <th class="check-column"><input type="checkbox" class="checkall"></th>
+ <th colspan="2">
+ <?php $list_table->unprotected_display_tablenav( 'top' ); ?>
+ <span class="filter-search">
+ <button type="button" class="button">Filter</button>
+ </span>
+ </th>
+ </tr>
+ </thead>
+ </table>
+ </div>
+ </div><!-- /.wrap -->
+ </div><!-- /.frame -->
+ <div class="frame bottom">
+ <div class="wrap">
+ <div class="manage-right" style="display: none;">
+ <div class="bumper">
+ <form class="navbar-form" role="search">
+ <input type="hidden" name="page" value="jetpack_modules" />
+ <?php $list_table->search_box( __( 'Search', 'jetpack' ), 'srch-term' ); ?>
+ <p><?php esc_html_e( 'View:', 'jetpack' ); ?></p>
+ <div class="button-group filter-active">
+ <button type="button" class="button <?php if ( empty( $_GET['activated'] ) ) echo 'active'; ?>"><?php esc_html_e( 'All', 'jetpack' ); ?></button>
+ <button type="button" class="button <?php if ( ! empty( $_GET['activated'] ) && 'true' == $_GET['activated'] ) echo 'active'; ?>" data-filter-by="activated" data-filter-value="true"><?php esc_html_e( 'Active', 'jetpack' ); ?></button>
+ <button type="button" class="button <?php if ( ! empty( $_GET['activated'] ) && 'false' == $_GET['activated'] ) echo 'active'; ?>" data-filter-by="activated" data-filter-value="false"><?php esc_html_e( 'Inactive', 'jetpack' ); ?></button>
+ </div>
+ <p><?php esc_html_e( 'Sort by:', 'jetpack' ); ?></p>
+ <div class="button-group sort">
+ <button type="button" class="button <?php if ( empty( $_GET['sort_by'] ) ) echo 'active'; ?>" data-sort-by="name"><?php esc_html_e( 'Alphabetical', 'jetpack' ); ?></button>
+ <button type="button" class="button <?php if ( ! empty( $_GET['sort_by'] ) && 'introduced' == $_GET['sort_by'] ) echo 'active'; ?>" data-sort-by="introduced" data-sort-order="reverse"><?php esc_html_e( 'Newest', 'jetpack' ); ?></button>
+ <button type="button" class="button <?php if ( ! empty( $_GET['sort_by'] ) && 'sort' == $_GET['sort_by'] ) echo 'active'; ?>" data-sort-by="sort"><?php esc_html_e( 'Popular', 'jetpack' ); ?></button>
+ </div>
+ <p><?php esc_html_e( 'Show:', 'jetpack' ); ?></p>
+ <?php $list_table->views(); ?>
+ </form>
+ </div>
+ </div>
+ <div class="manage-left" style="width: 100%;">
+ <form class="jetpack-modules-list-table-form" onsubmit="return false;">
+ <table class="<?php echo implode( ' ', $list_table->get_table_classes() ); ?>">
+ <tbody id="the-list">
+ <?php $list_table->display_rows_or_placeholder(); ?>
+ </tbody>
+ </table>
+ </form>
+ </div>
+ </div><!-- /.wrap -->
+ </div><!-- /.frame -->
+ </div><!-- /.content -->
+ <?php
+
+ JetpackTracking::record_user_event( 'wpa_page_view', array( 'path' => 'old_settings' ) );
+ }
+
+ /**
+ * Load styles for static page.
+ *
+ * @since 4.3.0
+ */
+ function additional_styles() {
+ Jetpack_Admin_Page::load_wrapper_styles();
+ }
+
+ // Javascript logic specific to the list table
+ function page_admin_scripts() {
+ wp_enqueue_script(
+ 'jetpack-admin-js',
+ Jetpack::get_file_url_for_environment( '_inc/build/jetpack-admin.min.js', '_inc/jetpack-admin.js' ),
+ array( 'jquery' ),
+ JETPACK__VERSION
+ );
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/class.color.php b/plugins/jetpack/_inc/lib/class.color.php
new file mode 100644
index 00000000..a57f2009
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.color.php
@@ -0,0 +1,755 @@
+<?php
+/**
+ * Color utility and conversion
+ *
+ * Represents a color value, and converts between RGB/HSV/XYZ/Lab/HSL
+ *
+ * Example:
+ * $color = new Jetpack_Color(0xFFFFFF);
+ *
+ * @author Harold Asbridge <hasbridge@gmail.com>
+ * @author Matt Wiebe <wiebe@automattic.com>
+ * @license http://www.opensource.org/licenses/MIT
+ */
+
+class Jetpack_Color {
+ /**
+ * @var int
+ */
+ protected $color = 0;
+
+ /**
+ * Initialize object
+ *
+ * @param string|array $color A color of the type $type
+ * @param string $type The type of color we will construct from.
+ * One of hex (default), rgb, hsl, int
+ */
+ public function __construct( $color = null, $type = 'hex' ) {
+ if ( $color ) {
+ switch ( $type ) {
+ case 'hex':
+ $this->fromHex( $color );
+ break;
+ case 'rgb':
+ if ( is_array( $color ) && count( $color ) == 3 ) {
+ list( $r, $g, $b ) = array_values( $color );
+ $this->fromRgbInt( $r, $g, $b );
+ }
+ break;
+ case 'hsl':
+ if ( is_array( $color ) && count( $color ) == 3 ) {
+ list( $h, $s, $l ) = array_values( $color );
+ $this->fromHsl( $h, $s, $l );
+ }
+ break;
+ case 'int':
+ $this->fromInt( $color );
+ break;
+ default:
+ // there is no default.
+ break;
+ }
+ }
+ }
+
+ /**
+ * Init color from hex value
+ *
+ * @param string $hexValue
+ *
+ * @return Jetpack_Color
+ */
+ public function fromHex($hexValue) {
+ $hexValue = str_replace( '#', '', $hexValue );
+ // handle short hex codes like #fff
+ if ( 3 === strlen( $hexValue ) ) {
+ $short = $hexValue;
+ $i = 0;
+ $hexValue = '';
+ while ( $i < 3 ) {
+ $chunk = substr($short, $i, 1 );
+ $hexValue .= $chunk . $chunk;
+ $i++;
+ }
+ }
+ $intValue = hexdec( $hexValue );
+
+ if ( $intValue < 0 || $intValue > 16777215 ) {
+ throw new RangeException( $hexValue . " out of valid color code range" );
+ }
+
+ $this->color = $intValue;
+
+ return $this;
+ }
+
+ /**
+ * Init color from integer RGB values
+ *
+ * @param int $red
+ * @param int $green
+ * @param int $blue
+ *
+ * @return Jetpack_Color
+ */
+ public function fromRgbInt($red, $green, $blue)
+ {
+ if ( $red < 0 || $red > 255 )
+ throw new RangeException( "Red value " . $red . " out of valid color code range" );
+
+ if ( $green < 0 || $green > 255 )
+ throw new RangeException( "Green value " . $green . " out of valid color code range" );
+
+ if ( $blue < 0 || $blue > 255 )
+ throw new RangeException( "Blue value " . $blue . " out of valid color code range" );
+
+ $this->color = (int)(($red << 16) + ($green << 8) + $blue);
+
+ return $this;
+ }
+
+ /**
+ * Init color from hex RGB values
+ *
+ * @param string $red
+ * @param string $green
+ * @param string $blue
+ *
+ * @return Jetpack_Color
+ */
+ public function fromRgbHex($red, $green, $blue)
+ {
+ return $this->fromRgbInt(hexdec($red), hexdec($green), hexdec($blue));
+ }
+
+ /**
+ * Converts an HSL color value to RGB. Conversion formula
+ * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+ * @param int $h Hue. [0-360]
+ * @param in $s Saturation [0, 100]
+ * @param int $l Lightness [0, 100]
+ */
+ public function fromHsl( $h, $s, $l ) {
+ $h /= 360; $s /= 100; $l /= 100;
+
+ if ( $s == 0 ) {
+ $r = $g = $b = $l; // achromatic
+ }
+ else {
+ $q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s;
+ $p = 2 * $l - $q;
+ $r = $this->hue2rgb( $p, $q, $h + 1/3 );
+ $g = $this->hue2rgb( $p, $q, $h );
+ $b = $this->hue2rgb( $p, $q, $h - 1/3 );
+ }
+
+ return $this->fromRgbInt( $r * 255, $g * 255, $b * 255 );
+ }
+
+ /**
+ * Helper function for Jetpack_Color::fromHsl()
+ */
+ private function hue2rgb( $p, $q, $t ) {
+ if ( $t < 0 ) $t += 1;
+ if ( $t > 1 ) $t -= 1;
+ if ( $t < 1/6 ) return $p + ( $q - $p ) * 6 * $t;
+ if ( $t < 1/2 ) return $q;
+ if ( $t < 2/3 ) return $p + ( $q - $p ) * ( 2/3 - $t ) * 6;
+ return $p;
+ }
+
+ /**
+ * Init color from integer value
+ *
+ * @param int $intValue
+ *
+ * @return Jetpack_Color
+ */
+ public function fromInt($intValue)
+ {
+ if ( $intValue < 0 || $intValue > 16777215 )
+ throw new RangeException( $intValue . " out of valid color code range" );
+
+ $this->color = $intValue;
+
+ return $this;
+ }
+
+ /**
+ * Convert color to hex
+ *
+ * @return string
+ */
+ public function toHex()
+ {
+ return str_pad(dechex($this->color), 6, '0', STR_PAD_LEFT);
+ }
+
+ /**
+ * Convert color to RGB array (integer values)
+ *
+ * @return array
+ */
+ public function toRgbInt()
+ {
+ return array(
+ 'red' => (int)(255 & ($this->color >> 16)),
+ 'green' => (int)(255 & ($this->color >> 8)),
+ 'blue' => (int)(255 & ($this->color))
+ );
+ }
+
+ /**
+ * Convert color to RGB array (hex values)
+ *
+ * @return array
+ */
+ public function toRgbHex()
+ {
+ $r = array();
+ foreach ($this->toRgbInt() as $item) {
+ $r[] = dechex($item);
+ }
+ return $r;
+ }
+
+ /**
+ * Get Hue/Saturation/Value for the current color
+ * (float values, slow but accurate)
+ *
+ * @return array
+ */
+ public function toHsvFloat()
+ {
+ $rgb = $this->toRgbInt();
+
+ $rgbMin = min($rgb);
+ $rgbMax = max($rgb);
+
+ $hsv = array(
+ 'hue' => 0,
+ 'sat' => 0,
+ 'val' => $rgbMax
+ );
+
+ // If v is 0, color is black
+ if ($hsv['val'] == 0) {
+ return $hsv;
+ }
+
+ // Normalize RGB values to 1
+ $rgb['red'] /= $hsv['val'];
+ $rgb['green'] /= $hsv['val'];
+ $rgb['blue'] /= $hsv['val'];
+ $rgbMin = min($rgb);
+ $rgbMax = max($rgb);
+
+ // Calculate saturation
+ $hsv['sat'] = $rgbMax - $rgbMin;
+ if ($hsv['sat'] == 0) {
+ $hsv['hue'] = 0;
+ return $hsv;
+ }
+
+ // Normalize saturation to 1
+ $rgb['red'] = ($rgb['red'] - $rgbMin) / ($rgbMax - $rgbMin);
+ $rgb['green'] = ($rgb['green'] - $rgbMin) / ($rgbMax - $rgbMin);
+ $rgb['blue'] = ($rgb['blue'] - $rgbMin) / ($rgbMax - $rgbMin);
+ $rgbMin = min($rgb);
+ $rgbMax = max($rgb);
+
+ // Calculate hue
+ if ($rgbMax == $rgb['red']) {
+ $hsv['hue'] = 0.0 + 60 * ($rgb['green'] - $rgb['blue']);
+ if ($hsv['hue'] < 0) {
+ $hsv['hue'] += 360;
+ }
+ } else if ($rgbMax == $rgb['green']) {
+ $hsv['hue'] = 120 + (60 * ($rgb['blue'] - $rgb['red']));
+ } else {
+ $hsv['hue'] = 240 + (60 * ($rgb['red'] - $rgb['green']));
+ }
+
+ return $hsv;
+ }
+
+ /**
+ * Get HSV values for color
+ * (integer values from 0-255, fast but less accurate)
+ *
+ * @return int
+ */
+ public function toHsvInt()
+ {
+ $rgb = $this->toRgbInt();
+
+ $rgbMin = min($rgb);
+ $rgbMax = max($rgb);
+
+ $hsv = array(
+ 'hue' => 0,
+ 'sat' => 0,
+ 'val' => $rgbMax
+ );
+
+ // If value is 0, color is black
+ if ($hsv['val'] == 0) {
+ return $hsv;
+ }
+
+ // Calculate saturation
+ $hsv['sat'] = round(255 * ($rgbMax - $rgbMin) / $hsv['val']);
+ if ($hsv['sat'] == 0) {
+ $hsv['hue'] = 0;
+ return $hsv;
+ }
+
+ // Calculate hue
+ if ($rgbMax == $rgb['red']) {
+ $hsv['hue'] = round(0 + 43 * ($rgb['green'] - $rgb['blue']) / ($rgbMax - $rgbMin));
+ } else if ($rgbMax == $rgb['green']) {
+ $hsv['hue'] = round(85 + 43 * ($rgb['blue'] - $rgb['red']) / ($rgbMax - $rgbMin));
+ } else {
+ $hsv['hue'] = round(171 + 43 * ($rgb['red'] - $rgb['green']) / ($rgbMax - $rgbMin));
+ }
+ if ($hsv['hue'] < 0) {
+ $hsv['hue'] += 255;
+ }
+
+ return $hsv;
+ }
+
+ /**
+ * Converts an RGB color value to HSL. Conversion formula
+ * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes r, g, and b are contained in the set [0, 255] and
+ * returns h in [0, 360], s in [0, 100], l in [0, 100]
+ *
+ * @return Array The HSL representation
+ */
+ public function toHsl() {
+ list( $r, $g, $b ) = array_values( $this->toRgbInt() );
+ $r /= 255; $g /= 255; $b /= 255;
+ $max = max( $r, $g, $b );
+ $min = min( $r, $g, $b );
+ $h = $s = $l = ( $max + $min ) / 2;
+ #var_dump( array( compact('max', 'min', 'r', 'g', 'b')) );
+ if ( $max == $min ) {
+ $h = $s = 0; // achromatic
+ }
+ else {
+ $d = $max - $min;
+ $s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min );
+ switch ( $max ) {
+ case $r:
+ $h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 );
+ break;
+ case $g:
+ $h = ( $b - $r ) / $d + 2;
+ break;
+ case $b:
+ $h = ( $r - $g ) / $d + 4;
+ break;
+ }
+ $h /= 6;
+ }
+ $h = (int) round( $h * 360 );
+ $s = (int) round( $s * 100 );
+ $l = (int) round( $l * 100 );
+ return compact( 'h', 's', 'l' );
+ }
+
+ public function toCSS( $type = 'hex', $alpha = 1 ) {
+ switch ( $type ) {
+ case 'hex':
+ return $this->toString();
+ break;
+ case 'rgb':
+ case 'rgba':
+ list( $r, $g, $b ) = array_values( $this->toRgbInt() );
+ if ( is_numeric( $alpha ) && $alpha < 1 ) {
+ return "rgba( {$r}, {$g}, {$b}, $alpha )";
+ }
+ else {
+ return "rgb( {$r}, {$g}, {$b} )";
+ }
+ break;
+ case 'hsl':
+ case 'hsla':
+ list( $h, $s, $l ) = array_values( $this->toHsl() );
+ if ( is_numeric( $alpha ) && $alpha < 1 ) {
+ return "hsla( {$h}, {$s}, {$l}, $alpha )";
+ }
+ else {
+ return "hsl( {$h}, {$s}, {$l} )";
+ }
+ break;
+ default:
+ return $this->toString();
+ break;
+ }
+ }
+
+ /**
+ * Get current color in XYZ format
+ *
+ * @return array
+ */
+ public function toXyz()
+ {
+ $rgb = $this->toRgbInt();
+
+ // Normalize RGB values to 1
+
+ $rgb_new = array();
+ foreach ($rgb as $item) {
+ $rgb_new[] = $item / 255;
+ }
+ $rgb = $rgb_new;
+
+ $rgb_new = array();
+ foreach ($rgb as $item) {
+ if ($item > 0.04045) {
+ $item = pow((($item + 0.055) / 1.055), 2.4);
+ } else {
+ $item = $item / 12.92;
+ }
+ $rgb_new[] = $item * 100;
+ }
+ $rgb = $rgb_new;
+
+ // Observer. = 2°, Illuminant = D65
+ $xyz = array(
+ 'x' => ($rgb['red'] * 0.4124) + ($rgb['green'] * 0.3576) + ($rgb['blue'] * 0.1805),
+ 'y' => ($rgb['red'] * 0.2126) + ($rgb['green'] * 0.7152) + ($rgb['blue'] * 0.0722),
+ 'z' => ($rgb['red'] * 0.0193) + ($rgb['green'] * 0.1192) + ($rgb['blue'] * 0.9505)
+ );
+
+ return $xyz;
+ }
+
+ /**
+ * Get color CIE-Lab values
+ *
+ * @return array
+ */
+ public function toLabCie()
+ {
+ $xyz = $this->toXyz();
+
+ //Ovserver = 2*, Iluminant=D65
+ $xyz['x'] /= 95.047;
+ $xyz['y'] /= 100;
+ $xyz['z'] /= 108.883;
+
+ $xyz_new = array();
+ foreach ($xyz as $item) {
+ if ($item > 0.008856) {
+ $xyz_new[] = pow($item, 1/3);
+ } else {
+ $xyz_new[] = (7.787 * $item) + (16 / 116);
+ }
+ }
+ $xyz = $xyz_new;
+
+ $lab = array(
+ 'l' => (116 * $xyz['y']) - 16,
+ 'a' => 500 * ($xyz['x'] - $xyz['y']),
+ 'b' => 200 * ($xyz['y'] - $xyz['z'])
+ );
+
+ return $lab;
+ }
+
+ /**
+ * Convert color to integer
+ *
+ * @return int
+ */
+ public function toInt()
+ {
+ return $this->color;
+ }
+
+ /**
+ * Alias of toString()
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->toString();
+ }
+
+ /**
+ * Get color as string
+ *
+ * @return string
+ */
+ public function toString()
+ {
+ $str = $this->toHex();
+ return strtoupper("#{$str}");
+ }
+
+ /**
+ * Get the distance between this color and the given color
+ *
+ * @param Jetpack_Color $color
+ *
+ * @return int
+ */
+ public function getDistanceRgbFrom(Jetpack_Color $color)
+ {
+ $rgb1 = $this->toRgbInt();
+ $rgb2 = $color->toRgbInt();
+
+ $rDiff = abs($rgb1['red'] - $rgb2['red']);
+ $gDiff = abs($rgb1['green'] - $rgb2['green']);
+ $bDiff = abs($rgb1['blue'] - $rgb2['blue']);
+
+ // Sum of RGB differences
+ $diff = $rDiff + $gDiff + $bDiff;
+ return $diff;
+ }
+
+ /**
+ * Get distance from the given color using the Delta E method
+ *
+ * @param Jetpack_Color $color
+ *
+ * @return float
+ */
+ public function getDistanceLabFrom(Jetpack_Color $color)
+ {
+ $lab1 = $this->toLabCie();
+ $lab2 = $color->toLabCie();
+
+ $lDiff = abs($lab2['l'] - $lab1['l']);
+ $aDiff = abs($lab2['a'] - $lab1['a']);
+ $bDiff = abs($lab2['b'] - $lab1['b']);
+
+ $delta = sqrt($lDiff + $aDiff + $bDiff);
+
+ return $delta;
+ }
+
+ public function toLuminosity() {
+ $lum = array();
+ foreach( $this->toRgbInt() as $slot => $value ) {
+ $chan = $value / 255;
+ $lum[ $slot ] = ( $chan <= 0.03928 ) ? $chan / 12.92 : pow( ( ( $chan + 0.055 ) / 1.055 ), 2.4 );
+ }
+ return 0.2126 * $lum['red'] + 0.7152 * $lum['green'] + 0.0722 * $lum['blue'];
+ }
+
+ /**
+ * Get distance between colors using luminance.
+ * Should be more than 5 for readable contrast
+ *
+ * @param Jetpack_Color $color Another color
+ * @return float
+ */
+ public function getDistanceLuminosityFrom( Jetpack_Color $color ) {
+ $L1 = $this->toLuminosity();
+ $L2 = $color->toLuminosity();
+ if ( $L1 > $L2 ) {
+ return ( $L1 + 0.05 ) / ( $L2 + 0.05 );
+ }
+ else{
+ return ( $L2 + 0.05 ) / ( $L1 + 0.05 );
+ }
+ }
+
+ public function getMaxContrastColor() {
+ $withBlack = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#000') );
+ $withWhite = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#fff') );
+ $color = new Jetpack_Color;
+ $hex = ( $withBlack >= $withWhite ) ? '#000000' : '#ffffff';
+ return $color->fromHex( $hex );
+ }
+
+ public function getGrayscaleContrastingColor( $contrast = false ) {
+ if ( ! $contrast ) {
+ return $this->getMaxContrastColor();
+ }
+ // don't allow less than 5
+ $target_contrast = ( $contrast < 5 ) ? 5 : $contrast;
+ $color = $this->getMaxContrastColor();
+ $contrast = $color->getDistanceLuminosityFrom( $this );
+
+ // if current max contrast is less than the target contrast, we had wishful thinking.
+ if ( $contrast <= $target_contrast ) {
+ return $color;
+ }
+
+ $incr = ( '#000000' === $color->toString() ) ? 1 : -1;
+ while ( $contrast > $target_contrast ) {
+ $color = $color->incrementLightness( $incr );
+ $contrast = $color->getDistanceLuminosityFrom( $this );
+ }
+
+ return $color;
+ }
+
+ /**
+ * Gets a readable contrasting color. $this is assumed to be the text and $color the background color.
+ * @param object $bg_color A Color object that will be compared against $this
+ * @param integer $min_contrast The minimum contrast to achieve, if possible.
+ * @return object A Color object, an increased contrast $this compared against $bg_color
+ */
+ public function getReadableContrastingColor( $bg_color = false, $min_contrast = 5 ) {
+ if ( ! $bg_color || ! is_a( $bg_color, 'Jetpack_Color' ) ) {
+ return $this;
+ }
+ // you shouldn't use less than 5, but you might want to.
+ $target_contrast = $min_contrast;
+ // working things
+ $contrast = $bg_color->getDistanceLuminosityFrom( $this );
+ $max_contrast_color = $bg_color->getMaxContrastColor();
+ $max_contrast = $max_contrast_color->getDistanceLuminosityFrom( $bg_color );
+
+ // if current max contrast is less than the target contrast, we had wishful thinking.
+ // still, go max
+ if ( $max_contrast <= $target_contrast ) {
+ return $max_contrast_color;
+ }
+ // or, we might already have sufficient contrast
+ if ( $contrast >= $target_contrast ) {
+ return $this;
+ }
+
+ $incr = ( 0 === $max_contrast_color->toInt() ) ? -1 : 1;
+ while ( $contrast < $target_contrast ) {
+ $this->incrementLightness( $incr );
+ $contrast = $bg_color->getDistanceLuminosityFrom( $this );
+ // infininite loop prevention: you never know.
+ if ( $this->color === 0 || $this->color === 16777215 ) {
+ break;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Detect if color is grayscale
+ *
+ * @param int @threshold
+ *
+ * @return bool
+ */
+ public function isGrayscale($threshold = 16)
+ {
+ $rgb = $this->toRgbInt();
+
+ // Get min and max rgb values, then difference between them
+ $rgbMin = min($rgb);
+ $rgbMax = max($rgb);
+ $diff = $rgbMax - $rgbMin;
+
+ return $diff < $threshold;
+ }
+
+ /**
+ * Get the closest matching color from the given array of colors
+ *
+ * @param array $colors array of integers or Jetpack_Color objects
+ *
+ * @return mixed the array key of the matched color
+ */
+ public function getClosestMatch(array $colors)
+ {
+ $matchDist = 10000;
+ $matchKey = null;
+ foreach($colors as $key => $color) {
+ if (false === ($color instanceof Jetpack_Color)) {
+ $c = new Jetpack_Color($color);
+ }
+ $dist = $this->getDistanceLabFrom($c);
+ if ($dist < $matchDist) {
+ $matchDist = $dist;
+ $matchKey = $key;
+ }
+ }
+
+ return $matchKey;
+ }
+
+ /* TRANSFORMS */
+
+ public function darken( $amount = 5 ) {
+ return $this->incrementLightness( - $amount );
+ }
+
+ public function lighten( $amount = 5 ) {
+ return $this->incrementLightness( $amount );
+ }
+
+ public function incrementLightness( $amount ) {
+ $hsl = $this->toHsl();
+ extract( $hsl );
+ $l += $amount;
+ if ( $l < 0 ) $l = 0;
+ if ( $l > 100 ) $l = 100;
+ return $this->fromHsl( $h, $s, $l );
+ }
+
+ public function saturate( $amount = 15 ) {
+ return $this->incrementSaturation( $amount );
+ }
+
+ public function desaturate( $amount = 15 ) {
+ return $this->incrementSaturation( - $amount );
+ }
+
+ public function incrementSaturation( $amount ) {
+ $hsl = $this->toHsl();
+ extract( $hsl );
+ $s += $amount;
+ if ( $s < 0 ) $s = 0;
+ if ( $s > 100 ) $s = 100;
+ return $this->fromHsl( $h, $s, $l );
+ }
+
+ public function toGrayscale() {
+ $hsl = $this->toHsl();
+ extract( $hsl );
+ $s = 0;
+ return $this->fromHsl( $h, $s, $l );
+ }
+
+ public function getComplement() {
+ return $this->incrementHue( 180 );
+ }
+
+ public function getSplitComplement( $step = 1 ) {
+ $incr = 180 + ( $step * 30 );
+ return $this->incrementHue( $incr );
+ }
+
+ public function getAnalog( $step = 1 ) {
+ $incr = $step * 30;
+ return $this->incrementHue( $incr );
+ }
+
+ public function getTetrad( $step = 1 ) {
+ $incr = $step * 60;
+ return $this->incrementHue( $incr );
+ }
+
+ public function getTriad( $step = 1 ) {
+ $incr = $step * 120;
+ return $this->incrementHue( $incr );
+ }
+
+ public function incrementHue( $amount ) {
+ $hsl = $this->toHsl();
+ extract( $hsl );
+ $h = ( $h + $amount ) % 360;
+ if ( $h < 0 ) $h = 360 - $h;
+ return $this->fromHsl( $h, $s, $l );
+ }
+
+} // class Jetpack_Color
diff --git a/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php b/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php
new file mode 100644
index 00000000..a0f0bf44
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php
@@ -0,0 +1,3196 @@
+<?php
+/**
+ * Register WP REST API endpoints for Jetpack.
+ *
+ * @author Automattic
+ */
+
+/**
+ * Disable direct access.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+// Load WP_Error for error messages.
+require_once ABSPATH . '/wp-includes/class-wp-error.php';
+
+// Register endpoints when WP REST API is initialized.
+add_action( 'rest_api_init', array( 'Jetpack_Core_Json_Api_Endpoints', 'register_endpoints' ) );
+// Load API endpoints that are synced with WP.com
+// Each of these is a class that will register its own routes on 'rest_api_init'.
+require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/load-wpcom-endpoints.php';
+
+/**
+ * Class Jetpack_Core_Json_Api_Endpoints
+ *
+ * @since 4.3.0
+ */
+class Jetpack_Core_Json_Api_Endpoints {
+
+ /**
+ * @var string Generic error message when user is not allowed to perform an action.
+ */
+ public static $user_permissions_error_msg;
+
+ /**
+ * @var array Roles that can access Stats once they're granted access.
+ */
+ public static $stats_roles;
+
+ /**
+ * Declare the Jetpack REST API endpoints.
+ *
+ * @since 4.3.0
+ */
+ public static function register_endpoints() {
+
+ // Load API endpoint base classes
+ require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php';
+
+ // Load API endpoints
+ require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php';
+ require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php';
+ require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php';
+
+ self::$user_permissions_error_msg = esc_html__(
+ 'You do not have the correct user permissions to perform this action.
+ Please contact your site admin if you think this is a mistake.',
+ 'jetpack'
+ );
+
+ self::$stats_roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
+
+ Jetpack::load_xml_rpc_client();
+ $ixr_client = new Jetpack_IXR_Client( array( 'user_id' => get_current_user_id() ) );
+ $core_api_endpoint = new Jetpack_Core_API_Data( $ixr_client );
+ $module_list_endpoint = new Jetpack_Core_API_Module_List_Endpoint();
+ $module_data_endpoint = new Jetpack_Core_API_Module_Data_Endpoint();
+ $module_toggle_endpoint = new Jetpack_Core_API_Module_Toggle_Endpoint( new Jetpack_IXR_Client() );
+ $site_endpoint = new Jetpack_Core_API_Site_Endpoint();
+ $widget_endpoint = new Jetpack_Core_API_Widget_Endpoint();
+
+ register_rest_route( 'jetpack/v4', 'plans', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_plans',
+ 'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
+
+ ) );
+
+ register_rest_route( 'jetpack/v4', '/jitm', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_jitm_message',
+ ) );
+
+ register_rest_route( 'jetpack/v4', '/jitm', array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => __CLASS__ . '::delete_jitm_message'
+ ) );
+
+ // Register a site
+ register_rest_route( 'jetpack/v4', '/verify_registration', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::verify_registration',
+ ) );
+
+ // Authorize a remote user
+ register_rest_route( 'jetpack/v4', '/remote_authorize', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::remote_authorize',
+ ) );
+
+ // Get current connection status of Jetpack
+ register_rest_route( 'jetpack/v4', '/connection', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::jetpack_connection_status',
+ ) );
+
+ // Test current connection status of Jetpack
+ register_rest_route( 'jetpack/v4', '/connection/test', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::jetpack_connection_test',
+ 'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
+ ) );
+
+ // Endpoint specific for privileged servers to request detailed debug information.
+ register_rest_route( 'jetpack/v4', '/connection/test-wpcom/', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::jetpack_connection_test_for_external',
+ 'permission_callback' => __CLASS__ . '::view_jetpack_connection_test_check',
+ ) );
+
+ register_rest_route( 'jetpack/v4', '/rewind', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_rewind_data',
+ 'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
+ ) );
+
+ // Fetches a fresh connect URL
+ register_rest_route( 'jetpack/v4', '/connection/url', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::build_connect_url',
+ 'permission_callback' => __CLASS__ . '::connect_url_permission_callback',
+ ) );
+
+ // Get current user connection data
+ register_rest_route( 'jetpack/v4', '/connection/data', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_user_connection_data',
+ 'permission_callback' => __CLASS__ . '::get_user_connection_data_permission_callback',
+ ) );
+
+ // Set the connection owner
+ register_rest_route( 'jetpack/v4', '/connection/owner', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::set_connection_owner',
+ 'permission_callback' => __CLASS__ . '::set_connection_owner_permission_callback',
+ ) );
+
+ // Current user: get or set tracking settings.
+ register_rest_route( 'jetpack/v4', '/tracking/settings', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_user_tracking_settings',
+ 'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::update_user_tracking_settings',
+ 'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
+ 'args' => array(
+ 'tracks_opt_out' => array( 'type' => 'boolean' ),
+ ),
+ ),
+ ) );
+
+ // Disconnect site from WordPress.com servers
+ register_rest_route( 'jetpack/v4', '/connection', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::disconnect_site',
+ 'permission_callback' => __CLASS__ . '::disconnect_site_permission_callback',
+ ) );
+
+ // Disconnect/unlink user from WordPress.com servers
+ register_rest_route( 'jetpack/v4', '/connection/user', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::unlink_user',
+ 'permission_callback' => __CLASS__ . '::unlink_user_permission_callback',
+ ) );
+
+ // Get current site data
+ register_rest_route( 'jetpack/v4', '/site', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_site_data',
+ 'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
+ ) );
+
+ // Get current site data
+ register_rest_route( 'jetpack/v4', '/site/features', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $site_endpoint, 'get_features' ),
+ 'permission_callback' => array( $site_endpoint , 'can_request' ),
+ ) );
+
+ // Confirm that a site in identity crisis should be in staging mode
+ register_rest_route( 'jetpack/v4', '/identity-crisis/confirm-safe-mode', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::confirm_safe_mode',
+ 'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
+ ) );
+
+ // IDC resolve: create an entirely new shadow site for this URL.
+ register_rest_route( 'jetpack/v4', '/identity-crisis/start-fresh', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::start_fresh_connection',
+ 'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
+ ) );
+
+ // Handles the request to migrate stats and subscribers during an identity crisis.
+ register_rest_route( 'jetpack/v4', 'identity-crisis/migrate', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::migrate_stats_and_subscribers',
+ 'permissison_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
+ ) );
+
+ // Return all modules
+ register_rest_route( 'jetpack/v4', '/module/all', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $module_list_endpoint, 'process' ),
+ 'permission_callback' => array( $module_list_endpoint, 'can_request' ),
+ ) );
+
+ // Activate many modules
+ register_rest_route( 'jetpack/v4', '/module/all/active', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $module_list_endpoint, 'process' ),
+ 'permission_callback' => array( $module_list_endpoint, 'can_request' ),
+ 'args' => array(
+ 'modules' => array(
+ 'default' => '',
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ 'required' => true,
+ 'validate_callback' => __CLASS__ . '::validate_module_list',
+ ),
+ 'active' => array(
+ 'default' => true,
+ 'type' => 'boolean',
+ 'required' => false,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ ),
+ )
+ ) );
+
+ // Return a single module and update it when needed
+ register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $core_api_endpoint, 'process' ),
+ 'permission_callback' => array( $core_api_endpoint, 'can_request' ),
+ ) );
+
+ // Activate and deactivate a module
+ register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)/active', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $module_toggle_endpoint, 'process' ),
+ 'permission_callback' => array( $module_toggle_endpoint, 'can_request' ),
+ 'args' => array(
+ 'active' => array(
+ 'default' => true,
+ 'type' => 'boolean',
+ 'required' => true,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ ),
+ )
+ ) );
+
+ // Update a module
+ register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $core_api_endpoint, 'process' ),
+ 'permission_callback' => array( $core_api_endpoint, 'can_request' ),
+ 'args' => self::get_updateable_parameters( 'any' )
+ ) );
+
+ // Get data for a specific module, i.e. Protect block count, WPCOM stats,
+ // Akismet spam count, etc.
+ register_rest_route( 'jetpack/v4', '/module/(?P<slug>[a-z\-]+)/data', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $module_data_endpoint, 'process' ),
+ 'permission_callback' => array( $module_data_endpoint, 'can_request' ),
+ 'args' => array(
+ 'range' => array(
+ 'default' => 'day',
+ 'type' => 'string',
+ 'required' => false,
+ 'validate_callback' => __CLASS__ . '::validate_string',
+ ),
+ )
+ ) );
+
+ // Check if the API key for a specific service is valid or not
+ register_rest_route( 'jetpack/v4', '/module/(?P<service>[a-z\-]+)/key/check', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $module_data_endpoint, 'key_check' ),
+ 'permission_callback' => __CLASS__ . '::update_settings_permission_check',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ) );
+
+ register_rest_route( 'jetpack/v4', '/module/(?P<service>[a-z\-]+)/key/check', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $module_data_endpoint, 'key_check' ),
+ 'permission_callback' => __CLASS__ . '::update_settings_permission_check',
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'args' => array(
+ 'api_key' => array(
+ 'default' => '',
+ 'type' => 'string',
+ 'validate_callback' => __CLASS__ . '::validate_alphanum',
+ ),
+ )
+ ) );
+
+ // Update any Jetpack module option or setting
+ register_rest_route( 'jetpack/v4', '/settings', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $core_api_endpoint, 'process' ),
+ 'permission_callback' => array( $core_api_endpoint, 'can_request' ),
+ 'args' => self::get_updateable_parameters( 'any' )
+ ) );
+
+ // Update a module
+ register_rest_route( 'jetpack/v4', '/settings/(?P<slug>[a-z\-]+)', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $core_api_endpoint, 'process' ),
+ 'permission_callback' => array( $core_api_endpoint, 'can_request' ),
+ 'args' => self::get_updateable_parameters()
+ ) );
+
+ // Return all module settings
+ register_rest_route( 'jetpack/v4', '/settings/', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $core_api_endpoint, 'process' ),
+ 'permission_callback' => array( $core_api_endpoint, 'can_request' ),
+ ) );
+
+ // Reset all Jetpack options
+ register_rest_route( 'jetpack/v4', '/options/(?P<options>[a-z\-]+)', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::reset_jetpack_options',
+ 'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
+ ) );
+
+ // Return current Jumpstart status
+ register_rest_route( 'jetpack/v4', '/jumpstart', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::jumpstart_status',
+ 'permission_callback' => __CLASS__ . '::update_settings_permission_check',
+ ) );
+
+ // Update Jumpstart
+ register_rest_route( 'jetpack/v4', '/jumpstart', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::jumpstart_toggle',
+ 'permission_callback' => __CLASS__ . '::manage_modules_permission_check',
+ 'args' => array(
+ 'active' => array(
+ 'required' => true,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ ),
+ ),
+ ) );
+
+ // Updates: get number of plugin updates available
+ register_rest_route( 'jetpack/v4', '/updates/plugins', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_plugin_update_count',
+ 'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
+ ) );
+
+ // Dismiss Jetpack Notices
+ register_rest_route( 'jetpack/v4', '/notice/(?P<notice>[a-z\-_]+)', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::dismiss_notice',
+ 'permission_callback' => __CLASS__ . '::view_admin_page_permission_check',
+ ) );
+
+ // Plugins: get list of all plugins.
+ register_rest_route( 'jetpack/v4', '/plugins', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_plugins',
+ 'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
+ ) );
+
+ // Plugins: check if the plugin is active.
+ register_rest_route( 'jetpack/v4', '/plugin/(?P<plugin>[a-z\/\.\-_]+)', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_plugin',
+ 'permission_callback' => __CLASS__ . '::activate_plugins_permission_check',
+ ) );
+
+ // Widgets: get information about a widget that supports it.
+ register_rest_route( 'jetpack/v4', '/widgets/(?P<id>[0-9a-z\-_]+)', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $widget_endpoint, 'process' ),
+ 'permission_callback' => array( $widget_endpoint, 'can_request' ),
+ ) );
+
+ // Site Verify: check if the site is verified, and a get verification token if not
+ register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::is_site_verified_and_token',
+ 'permission_callback' => __CLASS__ . '::update_settings_permission_check',
+ ) );
+
+ register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)/(?<keyring_id>[0-9]+)', array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::is_site_verified_and_token',
+ 'permission_callback' => __CLASS__ . '::update_settings_permission_check',
+ ) );
+
+ // Site Verify: tell a service to verify the site
+ register_rest_route( 'jetpack/v4', '/verify-site/(?P<service>[a-z\-_]+)', array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::verify_site',
+ 'permission_callback' => __CLASS__ . '::update_settings_permission_check',
+ 'args' => array(
+ 'keyring_id' => array(
+ 'required' => true,
+ 'type' => 'integer',
+ 'validate_callback' => __CLASS__ . '::validate_posint',
+ ),
+ )
+ ) );
+
+ // Get and set API keys.
+ // Note: permission_callback intentionally omitted from the GET method.
+ // Map block requires open access to API keys on the front end.
+ register_rest_route(
+ 'jetpack/v4',
+ '/service-api-keys/(?P<service>[a-z\-_]+)',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::get_service_api_key',
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::update_service_api_key',
+ 'permission_callback' => array( 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys','edit_others_posts_check' ),
+ 'args' => array(
+ 'service_api_key' => array(
+ 'required' => true,
+ 'type' => 'text',
+ ),
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => __CLASS__ . '::delete_service_api_key',
+ 'permission_callback' => array( 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys','edit_others_posts_check' ),
+ ),
+ )
+ );
+ }
+
+ public static function get_plans( $request ) {
+ $request = Jetpack_Client::wpcom_json_api_request_as_user(
+ '/plans?_locale=' . get_user_locale(),
+ '2',
+ array(
+ 'method' => 'GET',
+ 'headers' => array(
+ 'X-Forwarded-For' => Jetpack::current_user_ip( true ),
+ ),
+ )
+ );
+
+ $body = wp_remote_retrieve_body( $request );
+ if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
+ $data = $body;
+ } else {
+ // something went wrong so we'll just return the response without caching
+ return $body;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Asks for a jitm, unless they've been disabled, in which case it returns an empty array
+ *
+ * @param $request WP_REST_Request
+ *
+ * @return array An array of jitms
+ */
+ public static function get_jitm_message( $request ) {
+ require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-jitm.php' );
+
+ $jitm = Jetpack_JITM::init();
+
+ if ( ! $jitm ) {
+ return array();
+ }
+
+ return $jitm->get_messages( $request['message_path'], urldecode_deep( $request['query'] ) );
+ }
+
+ /**
+ * Dismisses a jitm
+ * @param $request WP_REST_Request The request
+ *
+ * @return bool Always True
+ */
+ public static function delete_jitm_message( $request ) {
+ require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-jitm.php' );
+
+ $jitm = Jetpack_JITM::init();
+
+ if ( ! $jitm ) {
+ return true;
+ }
+
+ return $jitm->dismiss( $request['id'], $request['feature_class'] );
+ }
+
+ /**
+ * Handles verification that a site is registered
+ *
+ * @since 5.4.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return array|wp-error
+ */
+ public static function verify_registration( $request ) {
+ require_once JETPACK__PLUGIN_DIR . 'class.jetpack-xmlrpc-server.php';
+ $xmlrpc_server = new Jetpack_XMLRPC_Server();
+ $result = $xmlrpc_server->verify_registration( array( $request['secret_1'], $request['state'] ) );
+
+ if ( is_a( $result, 'IXR_Error' ) ) {
+ $result = new WP_Error( $result->code, $result->message );
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Checks if this site has been verified using a service - only 'google' supported at present - and a specfic
+ * keyring to use to get the token if it is not
+ *
+ * Returns 'verified' = true/false, and a token if 'verified' is false and site is ready for verification
+ *
+ * @since 6.6.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return array|wp-error
+ */
+ public static function is_site_verified_and_token( $request ) {
+ /**
+ * Return an error if the site uses a Maintenance / Coming Soon plugin
+ * and if the plugin is configured to make the site private.
+ *
+ * We currently handle the following plugins:
+ * - https://github.com/mojoness/mojo-marketplace-wp-plugin (used by bluehost)
+ * - https://wordpress.org/plugins/mojo-under-construction
+ * - https://wordpress.org/plugins/under-construction-page
+ * - https://wordpress.org/plugins/ultimate-under-construction
+ * - https://wordpress.org/plugins/coming-soon
+ *
+ * You can handle this in your own plugin thanks to the `jetpack_is_under_construction_plugin` filter.
+ * If the filter returns true, we will consider the site as under construction.
+ */
+ $mm_coming_soon = get_option( 'mm_coming_soon', null );
+ $under_construction_activation_status = get_option( 'underConstructionActivationStatus', null );
+ $ucp_options = get_option( 'ucp_options', array() );
+ $uuc_settings = get_option( 'uuc_settings', array() );
+ $csp4 = get_option( 'seed_csp4_settings_content', array() );
+ if (
+ ( Jetpack::is_plugin_active( 'mojo-marketplace-wp-plugin/mojo-marketplace.php' ) && 'true' === $mm_coming_soon )
+ || Jetpack::is_plugin_active( 'mojo-under-construction/mojo-contruction.php' ) && 1 == $under_construction_activation_status // WPCS: loose comparison ok.
+ || ( Jetpack::is_plugin_active( 'under-construction-page/under-construction.php' ) && isset( $ucp_options['status'] ) && 1 == $ucp_options['status'] ) // WPCS: loose comparison ok.
+ || ( Jetpack::is_plugin_active( 'ultimate-under-construction/ultimate-under-construction.php' ) && isset( $uuc_settings['enable'] ) && 1 == $uuc_settings['enable'] ) // WPCS: loose comparison ok.
+ || ( Jetpack::is_plugin_active( 'coming-soon/coming-soon.php' ) && isset( $csp4['status'] ) && ( 1 == $csp4['status'] || 2 == $csp4['status'] ) ) // WPCS: loose comparison ok.
+ /**
+ * Allow plugins to mark a site as "under construction".
+ *
+ * @since 6.7.0
+ *
+ * @param false bool Is the site under construction? Default to false.
+ */
+ || true === apply_filters( 'jetpack_is_under_construction_plugin', false )
+ ) {
+ return new WP_Error( 'forbidden', __( 'Site is under construction and cannot be verified', 'jetpack' ) );
+ }
+
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id(),
+ ) );
+
+ $args = array(
+ 'user_id' => get_current_user_id(),
+ 'service' => $request[ 'service' ],
+ );
+
+ if ( isset( $request[ 'keyring_id' ] ) ) {
+ $args[ 'keyring_id' ] = $request[ 'keyring_id' ];
+ }
+
+ $xml->query( 'jetpack.isSiteVerified', $args );
+
+ if ( $xml->isError() ) {
+ return new WP_Error( 'error_checking_if_site_verified_google', sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
+ } else {
+ return $xml->getResponse();
+ }
+ }
+
+
+
+ public static function verify_site( $request ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id(),
+ ) );
+
+ $params = $request->get_json_params();
+
+ $xml->query( 'jetpack.verifySite', array(
+ 'user_id' => get_current_user_id(),
+ 'service' => $request[ 'service' ],
+ 'keyring_id' => $params[ 'keyring_id' ],
+ )
+ );
+
+ if ( $xml->isError() ) {
+ return new WP_Error( 'error_verifying_site_google', sprintf( '%s: %s', $xml->getErrorCode(), $xml->getErrorMessage() ) );
+ } else {
+ $response = $xml->getResponse();
+
+ if ( ! empty( $response['errors'] ) ) {
+ $error = new WP_Error;
+ $error->errors = $response['errors'];
+ return $error;
+ }
+
+ return $response;
+ }
+ }
+
+ /**
+ * Handles verification that a site is registered
+ *
+ * @since 5.4.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return array|wp-error
+ */
+ public static function remote_authorize( $request ) {
+ require_once JETPACK__PLUGIN_DIR . 'class.jetpack-xmlrpc-server.php';
+ $xmlrpc_server = new Jetpack_XMLRPC_Server();
+ $result = $xmlrpc_server->remote_authorize( $request );
+
+ if ( is_a( $result, 'IXR_Error' ) ) {
+ $result = new WP_Error( $result->code, $result->message );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Handles dismissing of Jetpack Notices
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return array|wp-error
+ */
+ public static function dismiss_notice( $request ) {
+ $notice = $request['notice'];
+
+ if ( ! isset( $request['dismissed'] ) || $request['dismissed'] !== true ) {
+ return new WP_Error( 'invalid_param', esc_html__( 'Invalid parameter "dismissed".', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ if ( isset( $notice ) && ! empty( $notice ) ) {
+ switch( $notice ) {
+ case 'feedback_dash_request':
+ case 'welcome':
+ $notices = get_option( 'jetpack_dismissed_notices', array() );
+ $notices[ $notice ] = true;
+ update_option( 'jetpack_dismissed_notices', $notices );
+ return rest_ensure_response( get_option( 'jetpack_dismissed_notices', array() ) );
+
+ default:
+ return new WP_Error( 'invalid_param', esc_html__( 'Invalid parameter "notice".', 'jetpack' ), array( 'status' => 404 ) );
+ }
+ }
+
+ return new WP_Error( 'required_param', esc_html__( 'Missing parameter "notice".', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ /**
+ * Verify that the user can disconnect the site.
+ *
+ * @since 4.3.0
+ *
+ * @return bool|WP_Error True if user is able to disconnect the site.
+ */
+ public static function disconnect_site_permission_callback() {
+ if ( current_user_can( 'jetpack_disconnect' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+
+ }
+
+ /**
+ * Verify that the user can get a connect/link URL
+ *
+ * @since 4.3.0
+ *
+ * @return bool|WP_Error True if user is able to disconnect the site.
+ */
+ public static function connect_url_permission_callback() {
+ if ( current_user_can( 'jetpack_connect_user' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+
+ }
+
+ /**
+ * Verify that a user can get the data about the current user.
+ * Only those who can connect.
+ *
+ * @since 4.3.0
+ *
+ * @uses Jetpack::is_user_connected();
+ *
+ * @return bool|WP_Error True if user is able to unlink.
+ */
+ public static function get_user_connection_data_permission_callback() {
+ if ( current_user_can( 'jetpack_connect_user' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_user_connection_data', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Check that user has permission to change the master user.
+ *
+ * @since 6.2.0
+ *
+ * @return bool|WP_Error True if user is able to change master user.
+ */
+ public static function set_connection_owner_permission_callback() {
+ if ( get_current_user_id() === Jetpack_Options::get_option( 'master_user' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_set_connection_owner', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
+ *
+ * @since 4.3.0
+ *
+ * @uses Jetpack::is_user_connected();
+ *
+ * @return bool|WP_Error True if user is able to unlink.
+ */
+ public static function unlink_user_permission_callback() {
+ if ( current_user_can( 'jetpack_connect_user' ) && Jetpack::is_user_connected( get_current_user_id() ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_unlink_user', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Verify that user can manage Jetpack modules.
+ *
+ * @since 4.3.0
+ *
+ * @return bool Whether user has the capability 'jetpack_manage_modules'.
+ */
+ public static function manage_modules_permission_check() {
+ if ( current_user_can( 'jetpack_manage_modules' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_manage_modules', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Verify that user can update Jetpack modules.
+ *
+ * @since 4.3.0
+ *
+ * @return bool Whether user has the capability 'jetpack_configure_modules'.
+ */
+ public static function configure_modules_permission_check() {
+ if ( current_user_can( 'jetpack_configure_modules' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_configure_modules', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Verify that user can view Jetpack admin page.
+ *
+ * @since 4.3.0
+ *
+ * @return bool Whether user has the capability 'jetpack_admin_page'.
+ */
+ public static function view_admin_page_permission_check() {
+ if ( current_user_can( 'jetpack_admin_page' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_view_admin', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Verify that user can mitigate an identity crisis.
+ *
+ * @since 4.4.0
+ *
+ * @return bool Whether user has capability 'jetpack_disconnect'.
+ */
+ public static function identity_crisis_mitigation_permission_check() {
+ if ( current_user_can( 'jetpack_disconnect' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_identity_crisis', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Verify that user can update Jetpack general settings.
+ *
+ * @since 4.3.0
+ *
+ * @return bool Whether user has the capability 'update_settings_permission_check'.
+ */
+ public static function update_settings_permission_check() {
+ if ( current_user_can( 'jetpack_configure_modules' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_manage_settings', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Verify that user can view Jetpack admin page and can activate plugins.
+ *
+ * @since 4.3.0
+ *
+ * @return bool Whether user has the capability 'jetpack_admin_page' and 'activate_plugins'.
+ */
+ public static function activate_plugins_permission_check() {
+ if ( current_user_can( 'jetpack_admin_page' ) && current_user_can( 'activate_plugins' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_activate_plugins', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Verify that user can edit other's posts (Editors and Administrators).
+ *
+ * @return bool Whether user has the capability 'edit_others_posts'.
+ */
+ public static function edit_others_posts_check() {
+ if ( current_user_can( 'edit_others_posts' ) ) {
+ return true;
+ }
+
+ return new WP_Error( 'invalid_user_permission_edit_others_posts', self::$user_permissions_error_msg, array( 'status' => self::rest_authorization_required_code() ) );
+ }
+
+ /**
+ * Contextual HTTP error code for authorization failure.
+ *
+ * Taken from rest_authorization_required_code() in WP-API plugin until is added to core.
+ * @see https://github.com/WP-API/WP-API/commit/7ba0ae6fe4f605d5ffe4ee85b1cd5f9fb46900a6
+ *
+ * @since 4.3.0
+ *
+ * @return int
+ */
+ public static function rest_authorization_required_code() {
+ return is_user_logged_in() ? 403 : 401;
+ }
+
+ /**
+ * Get connection status for this Jetpack site.
+ *
+ * @since 4.3.0
+ *
+ * @return bool True if site is connected
+ */
+ public static function jetpack_connection_status() {
+ return rest_ensure_response( array(
+ 'isActive' => Jetpack::is_active(),
+ 'isStaging' => Jetpack::is_staging_site(),
+ 'devMode' => array(
+ 'isActive' => Jetpack::is_development_mode(),
+ 'constant' => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
+ 'url' => site_url() && false === strpos( site_url(), '.' ),
+ 'filter' => apply_filters( 'jetpack_development_mode', false ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Test connection status for this Jetpack site.
+ *
+ * @since 6.8.0
+ *
+ * @return array|WP_Error WP_Error returned if connection test does not succeed.
+ */
+ public static function jetpack_connection_test() {
+ jetpack_require_lib( 'debugger' );
+ $cxntests = new Jetpack_Cxn_Tests();
+
+ if ( $cxntests->pass() ) {
+ return rest_ensure_response(
+ array(
+ 'code' => 'success',
+ 'message' => __( 'All connection tests passed.', 'jetpack' ),
+ )
+ );
+ } else {
+ return $cxntests->output_fails_as_wp_error();
+ }
+ }
+
+ /**
+ * Test connection permission check method.
+ *
+ * @since 7.1.0
+ *
+ * @return bool
+ */
+ public static function view_jetpack_connection_test_check() {
+ if ( ! isset( $_GET['signature'], $_GET['timestamp'], $_GET['url'] ) ) {
+ return false;
+ }
+ $signature = base64_decode( $_GET['signature'] );
+
+ $signature_data = wp_json_encode(
+ array(
+ 'rest_route' => $_GET['rest_route'],
+ 'timestamp' => intval( $_GET['timestamp'] ),
+ 'url' => wp_unslash( $_GET['url'] ),
+ )
+ );
+
+ if (
+ ! function_exists( 'openssl_verify' )
+ || ! openssl_verify(
+ $signature_data,
+ $signature,
+ JETPACK__DEBUGGER_PUBLIC_KEY
+ )
+ ) {
+ return false;
+ }
+
+ // signature timestamp must be within 5min of current time
+ if ( abs( time() - intval( $_GET['timestamp'] ) ) > 300 ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Test connection status for this Jetpack site, encrypt the results for decryption by a third-party.
+ *
+ * @since 7.1.0
+ *
+ * @return array|mixed|object|WP_Error
+ */
+ public static function jetpack_connection_test_for_external() {
+ // Since we are running this test for inclusion in the WP.com testing suite, let's not try to run them as part of these results.
+ add_filter( 'jetpack_debugger_run_self_test', '__return_false' );
+ jetpack_require_lib( 'debugger' );
+ $cxntests = new Jetpack_Cxn_Tests();
+
+ if ( $cxntests->pass() ) {
+ $result = array(
+ 'code' => 'success',
+ 'message' => __( 'All connection tests passed.', 'jetpack' ),
+ );
+ } else {
+ $error = $cxntests->output_fails_as_wp_error(); // Using this so the output is similar both ways.
+ $errors = array();
+
+ // Borrowed from WP_REST_Server::error_to_response().
+ foreach ( (array) $error->errors as $code => $messages ) {
+ foreach ( (array) $messages as $message ) {
+ $errors[] = array(
+ 'code' => $code,
+ 'message' => $message,
+ 'data' => $error->get_error_data( $code ),
+ );
+ }
+ }
+
+ $result = $errors[0];
+ if ( count( $errors ) > 1 ) {
+ // Remove the primary error.
+ array_shift( $errors );
+ $result['additional_errors'] = $errors;
+ }
+ }
+
+ $result = wp_json_encode( $result );
+
+ $encrypted = $cxntests->encrypt_string_for_wpcom( $result );
+
+ if ( ! $encrypted || ! is_array( $encrypted ) ) {
+ return rest_ensure_response(
+ array(
+ 'code' => 'action_required',
+ 'message' => 'Please request results from the in-plugin debugger',
+ )
+ );
+ }
+
+ return rest_ensure_response(
+ array(
+ 'code' => 'response',
+ 'debug' => array(
+ 'data' => $encrypted['data'],
+ 'key' => $encrypted['key'],
+ ),
+ )
+ );
+ }
+
+ public static function rewind_data() {
+ $site_id = Jetpack_Options::get_option( 'id' );
+
+ if ( ! $site_id ) {
+ return new WP_Error( 'site_id_missing' );
+ }
+
+ $response = Jetpack_Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/rewind', $site_id ) .'?force=wpcom', '2', array(), null, 'wpcom' );
+
+ if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ return new WP_Error( 'rewind_data_fetch_failed' );
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+
+ return json_decode( $body );
+ }
+
+ /**
+ * Get rewind data
+ *
+ * @since 5.7.0
+ *
+ * @return array Array of rewind properties.
+ */
+ public static function get_rewind_data() {
+ $rewind_data = self::rewind_data();
+
+ if ( ! is_wp_error( $rewind_data ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Backup & Scan data correctly received.', 'jetpack' ),
+ 'data' => wp_json_encode( $rewind_data ),
+ )
+ );
+ }
+
+ if ( $rewind_data->get_error_code() === 'rewind_data_fetch_failed' ) {
+ return new WP_Error( 'rewind_data_fetch_failed', esc_html__( 'Failed fetching rewind data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ if ( $rewind_data->get_error_code() === 'site_id_missing' ) {
+ return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ return new WP_Error(
+ 'error_get_rewind_data',
+ esc_html__( 'Could not retrieve Backup & Scan data.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ /**
+ * Disconnects Jetpack from the WordPress.com Servers
+ *
+ * @uses Jetpack::disconnect();
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return bool|WP_Error True if Jetpack successfully disconnected.
+ */
+ public static function disconnect_site( $request ) {
+
+ if ( ! isset( $request['isActive'] ) || $request['isActive'] !== false ) {
+ return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ if ( Jetpack::is_active() ) {
+ Jetpack::disconnect();
+ return rest_ensure_response( array( 'code' => 'success' ) );
+ }
+
+ return new WP_Error( 'disconnect_failed', esc_html__( 'Was not able to disconnect the site. Please try again.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ /**
+ * Gets a new connect raw URL with fresh nonce.
+ *
+ * @uses Jetpack::disconnect();
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return string|WP_Error A raw URL if the connection URL could be built; error message otherwise.
+ */
+ public static function build_connect_url() {
+ $url = Jetpack::init()->build_connect_url( true, false, false );
+ if ( $url ) {
+ return rest_ensure_response( $url );
+ }
+
+ return new WP_Error( 'build_connect_url_failed', esc_html__( 'Unable to build the connect URL. Please reload the page and try again.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ /**
+ * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
+ * Information about the master/primary user.
+ * Information about the current user.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return object
+ */
+ public static function get_user_connection_data() {
+ require_once( JETPACK__PLUGIN_DIR . '_inc/lib/admin-pages/class.jetpack-react-page.php' );
+
+ $response = array(
+// 'othersLinked' => Jetpack::get_other_linked_admins(),
+ 'currentUser' => jetpack_current_user_data(),
+ );
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Change the master user.
+ *
+ * @since 6.2.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return bool|WP_Error True if owner successfully changed.
+ */
+ public static function set_connection_owner( $request ) {
+ if ( ! isset( $request['owner'] ) ) {
+ return new WP_Error(
+ 'invalid_param',
+ esc_html__( 'Invalid Parameter', 'jetpack' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ $new_owner_id = $request['owner'];
+ if ( ! user_can( $new_owner_id, 'administrator' ) ) {
+ return new WP_Error(
+ 'new_owner_not_admin',
+ esc_html__( 'New owner is not admin', 'jetpack' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ if ( $new_owner_id === get_current_user_id() ) {
+ return new WP_Error(
+ 'new_owner_is_current_user',
+ esc_html__( 'New owner is same as current user', 'jetpack' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ if ( ! Jetpack::is_user_connected( $new_owner_id ) ) {
+ return new WP_Error(
+ 'new_owner_not_connected',
+ esc_html__( 'New owner is not connected', 'jetpack' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Update the master user in Jetpack
+ $updated = Jetpack_Options::update_option( 'master_user', $new_owner_id );
+
+ // Notify WPCOM about the master user change
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client( array(
+ 'user_id' => get_current_user_id(),
+ ) );
+ $xml->query( 'jetpack.switchBlogOwner', array(
+ 'new_blog_owner' => $new_owner_id,
+ ) );
+
+ if ( $updated && ! $xml->isError() ) {
+ return rest_ensure_response(
+ array(
+ 'code' => 'success',
+ )
+ );
+ }
+ return new WP_Error(
+ 'error_setting_new_owner',
+ esc_html__( 'Could not confirm new owner.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ /**
+ * Unlinks current user from the WordPress.com Servers.
+ *
+ * @since 4.3.0
+ * @uses Jetpack::unlink_user
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return bool|WP_Error True if user successfully unlinked.
+ */
+ public static function unlink_user( $request ) {
+
+ if ( ! isset( $request['linked'] ) || $request['linked'] !== false ) {
+ return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ if ( Jetpack::unlink_user() ) {
+ return rest_ensure_response(
+ array(
+ 'code' => 'success'
+ )
+ );
+ }
+
+ return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ /**
+ * Gets current user's tracking settings.
+ *
+ * @since 6.0.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return WP_REST_Response|WP_Error Response, else error.
+ */
+ public static function get_user_tracking_settings( $request ) {
+ if ( ! Jetpack::is_user_connected() ) {
+ $response = array(
+ 'tracks_opt_out' => true, // Default to opt-out if not connected to wp.com.
+ );
+ } else {
+ $response = Jetpack_Client::wpcom_json_api_request_as_user(
+ '/jetpack-user-tracking',
+ 'v2',
+ array(
+ 'method' => 'GET',
+ 'headers' => array(
+ 'X-Forwarded-For' => Jetpack::current_user_ip( true ),
+ ),
+ )
+ );
+ if ( ! is_wp_error( $response ) ) {
+ $response = json_decode( wp_remote_retrieve_body( $response ), true );
+ }
+ }
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Updates current user's tracking settings.
+ *
+ * @since 6.0.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return WP_REST_Response|WP_Error Response, else error.
+ */
+ public static function update_user_tracking_settings( $request ) {
+ if ( ! Jetpack::is_user_connected() ) {
+ $response = array(
+ 'tracks_opt_out' => true, // Default to opt-out if not connected to wp.com.
+ );
+ } else {
+ $response = Jetpack_Client::wpcom_json_api_request_as_user(
+ '/jetpack-user-tracking',
+ 'v2',
+ array(
+ 'method' => 'PUT',
+ 'headers' => array(
+ 'Content-Type' => 'application/json',
+ 'X-Forwarded-For' => Jetpack::current_user_ip( true ),
+ ),
+ ),
+ wp_json_encode( $request->get_params() )
+ );
+ if ( ! is_wp_error( $response ) ) {
+ $response = json_decode( wp_remote_retrieve_body( $response ), true );
+ }
+ }
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Fetch site data from .com including the site's current plan.
+ *
+ * @since 5.5.0
+ *
+ * @return array Array of site properties.
+ */
+ public static function site_data() {
+ $site_id = Jetpack_Options::get_option( 'id' );
+
+ if ( ! $site_id ) {
+ new WP_Error( 'site_id_missing' );
+ }
+
+ $response = Jetpack_Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d', $site_id ) .'?force=wpcom', '1.1' );
+
+ if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ return new WP_Error( 'site_data_fetch_failed' );
+ }
+
+ Jetpack_Plan::update_from_sites_response( $response );
+
+ $body = wp_remote_retrieve_body( $response );
+
+ return json_decode( $body );
+ }
+ /**
+ * Get site data, including for example, the site's current plan.
+ *
+ * @since 4.3.0
+ *
+ * @return array Array of site properties.
+ */
+ public static function get_site_data() {
+ $site_data = self::site_data();
+
+ if ( ! is_wp_error( $site_data ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Site data correctly received.', 'jetpack' ),
+ 'data' => json_encode( $site_data ),
+ )
+ );
+ }
+ if ( $site_data->get_error_code() === 'site_data_fetch_failed' ) {
+ return new WP_Error( 'site_data_fetch_failed', esc_html__( 'Failed fetching site data. Try again later.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ if ( $site_data->get_error_code() === 'site_id_missing' ) {
+ return new WP_Error( 'site_id_missing', esc_html__( 'The ID of this site does not exist.', 'jetpack' ), array( 'status' => 404 ) );
+ }
+ }
+
+ /**
+ * Handles identity crisis mitigation, confirming safe mode for this site.
+ *
+ * @since 4.4.0
+ *
+ * @return bool | WP_Error True if option is properly set.
+ */
+ public static function confirm_safe_mode() {
+ $updated = Jetpack_Options::update_option( 'safe_mode_confirmed', true );
+ if ( $updated ) {
+ return rest_ensure_response(
+ array(
+ 'code' => 'success'
+ )
+ );
+ }
+ return new WP_Error(
+ 'error_setting_jetpack_safe_mode',
+ esc_html__( 'Could not confirm safe mode.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ /**
+ * Handles identity crisis mitigation, migrating stats and subscribers from old url to this, new url.
+ *
+ * @since 4.4.0
+ *
+ * @return bool | WP_Error True if option is properly set.
+ */
+ public static function migrate_stats_and_subscribers() {
+ if ( Jetpack_Options::get_option( 'sync_error_idc' ) && ! Jetpack_Options::delete_option( 'sync_error_idc' ) ) {
+ return new WP_Error(
+ 'error_deleting_sync_error_idc',
+ esc_html__( 'Could not delete sync error option.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ if ( Jetpack_Options::get_option( 'migrate_for_idc' ) || Jetpack_Options::update_option( 'migrate_for_idc', true ) ) {
+ return rest_ensure_response(
+ array(
+ 'code' => 'success'
+ )
+ );
+ }
+ return new WP_Error(
+ 'error_setting_jetpack_migrate',
+ esc_html__( 'Could not confirm migration.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ /**
+ * This IDC resolution will disconnect the site and re-connect to a completely new
+ * and separate shadow site than the original.
+ *
+ * It will first will disconnect the site without phoning home as to not disturb the production site.
+ * It then builds a fresh connection URL and sends it back along with the response.
+ *
+ * @since 4.4.0
+ * @return bool|WP_Error
+ */
+ public static function start_fresh_connection() {
+ // First clear the options / disconnect.
+ Jetpack::disconnect();
+ return self::build_connect_url();
+ }
+
+ /**
+ * Reset Jetpack options
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $options Available options to reset are options|modules
+ * }
+ *
+ * @return bool|WP_Error True if options were reset. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public static function reset_jetpack_options( $request ) {
+
+ if ( ! isset( $request['reset'] ) || $request['reset'] !== true ) {
+ return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ if ( isset( $request['options'] ) ) {
+ $data = $request['options'];
+
+ switch( $data ) {
+ case ( 'options' ) :
+ $options_to_reset = Jetpack::get_jetpack_options_for_reset();
+
+ // Reset the Jetpack options
+ foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
+ Jetpack_Options::delete_option( $option_to_reset );
+ }
+
+ foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
+ delete_option( $option_to_reset );
+ }
+
+ // Reset to default modules
+ $default_modules = Jetpack::get_default_modules();
+ Jetpack::update_active_modules( $default_modules );
+
+ // Jumpstart option is special
+ Jetpack_Options::update_option( 'jumpstart', 'new_connection' );
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Jetpack options reset.', 'jetpack' ),
+ ) );
+ break;
+
+ case 'modules':
+ $default_modules = Jetpack::get_default_modules();
+ Jetpack::update_active_modules( $default_modules );
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Modules reset to default.', 'jetpack' ),
+ ) );
+ break;
+
+ default:
+ return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack' ), array( 'status' => 404 ) );
+ }
+ }
+
+ return new WP_Error( 'required_param', esc_html__( 'Missing parameter "type".', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ /**
+ * Retrieves the current status of Jumpstart.
+ *
+ * @since 4.5.0
+ *
+ * @return bool
+ */
+ public static function jumpstart_status() {
+ return array(
+ 'status' => Jetpack_Options::get_option( 'jumpstart' )
+ );
+ }
+
+ /**
+ * Toggles activation or deactivation of the JumpStart
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return bool|WP_Error True if toggling Jumpstart succeeded. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public static function jumpstart_toggle( $request ) {
+
+ if ( $request[ 'active' ] ) {
+ return self::jumpstart_activate( $request );
+ } else {
+ return self::jumpstart_deactivate( $request );
+ }
+ }
+
+ /**
+ * Activates a series of valid Jetpack modules and initializes some options.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return bool|WP_Error True if Jumpstart succeeded. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public static function jumpstart_activate( $request ) {
+ $modules = Jetpack::get_available_modules();
+ $activate_modules = array();
+ foreach ( $modules as $module ) {
+ $module_info = Jetpack::get_module( $module );
+ if ( isset( $module_info['feature'] ) && is_array( $module_info['feature'] ) && in_array( 'Jumpstart', $module_info['feature'] ) ) {
+ $activate_modules[] = $module;
+ }
+ }
+
+ // Collect success/error messages like modules that are properly activated.
+ $result = array(
+ 'activated_modules' => array(),
+ 'failed_modules' => array(),
+ );
+
+ // Update the jumpstart option
+ if ( 'new_connection' === Jetpack_Options::get_option( 'jumpstart' ) ) {
+ $result['jumpstart_activated'] = Jetpack_Options::update_option( 'jumpstart', 'jumpstart_activated' );
+ }
+
+ // Check for possible conflicting plugins
+ $module_slugs_filtered = Jetpack::init()->filter_default_modules( $activate_modules );
+
+ foreach ( $module_slugs_filtered as $module_slug ) {
+ Jetpack::log( 'activate', $module_slug );
+ if ( Jetpack::activate_module( $module_slug, false, false ) ) {
+ $result['activated_modules'][] = $module_slug;
+ } else {
+ $result['failed_modules'][] = $module_slug;
+ }
+ }
+
+ // Set the default sharing buttons and set to display on posts if none have been set.
+ $sharing_services = get_option( 'sharing-services' );
+ $sharing_options = get_option( 'sharing-options' );
+ if ( empty( $sharing_services['visible'] ) ) {
+ // Default buttons to set
+ $visible = array(
+ 'twitter',
+ 'facebook',
+ );
+ $hidden = array();
+
+ // Set some sharing settings
+ if ( class_exists( 'Sharing_Service' ) ) {
+ $sharing = new Sharing_Service();
+ $sharing_options['global'] = array(
+ 'button_style' => 'icon',
+ 'sharing_label' => $sharing->default_sharing_label,
+ 'open_links' => 'same',
+ 'show' => array( 'post' ),
+ 'custom' => isset( $sharing_options['global']['custom'] ) ? $sharing_options['global']['custom'] : array()
+ );
+
+ $result['sharing_options'] = update_option( 'sharing-options', $sharing_options );
+ $result['sharing_services'] = update_option( 'sharing-services', array( 'visible' => $visible, 'hidden' => $hidden ) );
+ }
+ }
+
+ // If all Jumpstart modules were activated
+ if ( empty( $result['failed_modules'] ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Jumpstart done.', 'jetpack' ),
+ 'data' => $result,
+ ) );
+ }
+
+ return new WP_Error( 'jumpstart_failed', esc_html( sprintf( _n( 'Jumpstart failed activating this module: %s.', 'Jumpstart failed activating these modules: %s.', count( $result['failed_modules'] ), 'jetpack' ), join( ', ', $result['failed_modules'] ) ) ), array( 'status' => 400 ) );
+ }
+
+ /**
+ * Dismisses Jumpstart so user is not prompted to go through it again.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return bool|WP_Error True if Jumpstart was disabled or was nothing to dismiss. Otherwise, a WP_Error instance with a message.
+ */
+ public static function jumpstart_deactivate( $request ) {
+
+ // If dismissed, flag the jumpstart option as such.
+ if ( 'new_connection' === Jetpack_Options::get_option( 'jumpstart' ) ) {
+ if ( Jetpack_Options::update_option( 'jumpstart', 'jumpstart_dismissed' ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Jumpstart dismissed.', 'jetpack' ),
+ ) );
+ } else {
+ return new WP_Error( 'jumpstart_failed_dismiss', esc_html__( 'Jumpstart could not be dismissed.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+ }
+
+ // If this was not a new connection and there was nothing to dismiss, don't fail.
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Nothing to dismiss. This was not a new connection.', 'jetpack' ),
+ ) );
+ }
+
+ /**
+ * Get the query parameters to update module options or general settings.
+ *
+ * @since 4.3.0
+ * @since 4.4.0 Accepts a $selector parameter.
+ *
+ * @param string $selector Selects a set of options to update, Can be empty, a module slug or 'any'.
+ *
+ * @return array
+ */
+ public static function get_updateable_parameters( $selector = '' ) {
+ $parameters = array(
+ 'context' => array(
+ 'default' => 'edit',
+ ),
+ );
+
+ return array_merge( $parameters, self::get_updateable_data_list( $selector ) );
+ }
+
+ /**
+ * Returns a list of module options or general settings that can be updated.
+ *
+ * @since 4.3.0
+ * @since 4.4.0 Accepts 'any' as a parameter which will make it return the entire list.
+ *
+ * @param string|array $selector Module slug, 'any', or an array of parameters.
+ * If empty, it's assumed we're updating a module and we'll try to get its slug.
+ * If 'any' the full list is returned.
+ * If it's an array of parameters, includes the elements by matching keys.
+ *
+ * @return array
+ */
+ public static function get_updateable_data_list( $selector = '' ) {
+
+ $options = array(
+
+ // Carousel
+ 'carousel_background_color' => array(
+ 'description' => esc_html__( 'Color scheme.', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => 'black',
+ 'enum' => array(
+ 'black',
+ 'white',
+ ),
+ 'enum_labels' => array(
+ 'black' => esc_html__( 'Black', 'jetpack' ),
+ 'white' => esc_html__( 'White', 'jetpack' ),
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_list_item',
+ 'jp_group' => 'carousel',
+ ),
+ 'carousel_display_exif' => array(
+ 'description' => wp_kses( sprintf( __( 'Show photo metadata (<a href="http://en.wikipedia.org/wiki/Exchangeable_image_file_format" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) ), array( 'a' => array( 'href' => true, 'target' => true ) ) ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'carousel',
+ ),
+
+ // Comments
+ 'highlander_comment_form_prompt' => array(
+ 'description' => esc_html__( 'Greeting Text', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => esc_html__( 'Leave a Reply', 'jetpack' ),
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'jp_group' => 'comments',
+ ),
+ 'jetpack_comment_form_color_scheme' => array(
+ 'description' => esc_html__( "Color scheme", 'jetpack' ),
+ 'type' => 'string',
+ 'default' => 'light',
+ 'enum' => array(
+ 'light',
+ 'dark',
+ 'transparent',
+ ),
+ 'enum_labels' => array(
+ 'light' => esc_html__( 'Light', 'jetpack' ),
+ 'dark' => esc_html__( 'Dark', 'jetpack' ),
+ 'transparent' => esc_html__( 'Transparent', 'jetpack' ),
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_list_item',
+ 'jp_group' => 'comments',
+ ),
+
+ // Custom Content Types
+ 'jetpack_portfolio' => array(
+ 'description' => esc_html__( 'Enable or disable Jetpack portfolio post type.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'custom-content-types',
+ ),
+ 'jetpack_portfolio_posts_per_page' => array(
+ 'description' => esc_html__( 'Number of entries to show at most in Portfolio pages.', 'jetpack' ),
+ 'type' => 'integer',
+ 'default' => 10,
+ 'validate_callback' => __CLASS__ . '::validate_posint',
+ 'jp_group' => 'custom-content-types',
+ ),
+ 'jetpack_testimonial' => array(
+ 'description' => esc_html__( 'Enable or disable Jetpack testimonial post type.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'custom-content-types',
+ ),
+ 'jetpack_testimonial_posts_per_page' => array(
+ 'description' => esc_html__( 'Number of entries to show at most in Testimonial pages.', 'jetpack' ),
+ 'type' => 'integer',
+ 'default' => 10,
+ 'validate_callback' => __CLASS__ . '::validate_posint',
+ 'jp_group' => 'custom-content-types',
+ ),
+
+ // Galleries
+ 'tiled_galleries' => array(
+ 'description' => esc_html__( 'Display all your gallery pictures in a cool mosaic.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'tiled-gallery',
+ ),
+
+ 'gravatar_disable_hovercards' => array(
+ 'description' => esc_html__( "View people's profiles when you mouse over their Gravatars", 'jetpack' ),
+ 'type' => 'string',
+ 'default' => 'enabled',
+ // Not visible. This is used as the checkbox value.
+ 'enum' => array(
+ 'enabled',
+ 'disabled',
+ ),
+ 'enum_labels' => array(
+ 'enabled' => esc_html__( 'Enabled', 'jetpack' ),
+ 'disabled' => esc_html__( 'Disabled', 'jetpack' ),
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_list_item',
+ 'jp_group' => 'gravatar-hovercards',
+ ),
+
+ // Infinite Scroll
+ 'infinite_scroll' => array(
+ 'description' => esc_html__( 'To infinity and beyond', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'infinite-scroll',
+ ),
+ 'infinite_scroll_google_analytics' => array(
+ 'description' => esc_html__( 'Use Google Analytics with Infinite Scroll', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'infinite-scroll',
+ ),
+
+ // Likes
+ 'wpl_default' => array(
+ 'description' => esc_html__( 'WordPress.com Likes are', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => 'on',
+ 'enum' => array(
+ 'on',
+ 'off',
+ ),
+ 'enum_labels' => array(
+ 'on' => esc_html__( 'On for all posts', 'jetpack' ),
+ 'off' => esc_html__( 'Turned on per post', 'jetpack' ),
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_list_item',
+ 'jp_group' => 'likes',
+ ),
+ 'social_notifications_like' => array(
+ 'description' => esc_html__( 'Send email notification when someone likes a post', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'likes',
+ ),
+
+ // Markdown
+ 'wpcom_publish_comments_with_markdown' => array(
+ 'description' => esc_html__( 'Use Markdown for comments.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'markdown',
+ ),
+ 'wpcom_publish_posts_with_markdown' => array(
+ 'description' => esc_html__( 'Use Markdown for posts.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'markdown',
+ ),
+
+ // Mobile Theme
+ 'wp_mobile_excerpt' => array(
+ 'description' => esc_html__( 'Excerpts', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'minileven',
+ ),
+ 'wp_mobile_featured_images' => array(
+ 'description' => esc_html__( 'Featured Images', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'minileven',
+ ),
+ 'wp_mobile_app_promos' => array(
+ 'description' => esc_html__( 'Show a promo for the WordPress mobile apps in the footer of the mobile theme.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'minileven',
+ ),
+
+ // Monitor
+ 'monitor_receive_notifications' => array(
+ 'description' => esc_html__( 'Receive Monitor Email Notifications.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'monitor',
+ ),
+
+ // Post by Email
+ 'post_by_email_address' => array(
+ 'description' => esc_html__( 'Email Address', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => 'noop',
+ 'enum' => array(
+ 'noop',
+ 'create',
+ 'regenerate',
+ 'delete',
+ ),
+ 'enum_labels' => array(
+ 'noop' => '',
+ 'create' => esc_html__( 'Create Post by Email address', 'jetpack' ),
+ 'regenerate' => esc_html__( 'Regenerate Post by Email address', 'jetpack' ),
+ 'delete' => esc_html__( 'Delete Post by Email address', 'jetpack' ),
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_list_item',
+ 'jp_group' => 'post-by-email',
+ ),
+
+ // Protect
+ 'jetpack_protect_key' => array(
+ 'description' => esc_html__( 'Protect API key', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_alphanum',
+ 'jp_group' => 'protect',
+ ),
+ 'jetpack_protect_global_whitelist' => array(
+ 'description' => esc_html__( 'Protect global whitelist', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_string',
+ 'sanitize_callback' => 'esc_textarea',
+ 'jp_group' => 'protect',
+ ),
+
+ // Sharing
+ 'sharing_services' => array(
+ 'description' => esc_html__( 'Enabled Services and those hidden behind a button', 'jetpack' ),
+ 'type' => 'object',
+ 'default' => array(
+ 'visible' => array( 'twitter', 'facebook', 'google-plus-1' ),
+ 'hidden' => array(),
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_services',
+ 'jp_group' => 'sharedaddy',
+ ),
+ 'button_style' => array(
+ 'description' => esc_html__( 'Button Style', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => 'icon',
+ 'enum' => array(
+ 'icon-text',
+ 'icon',
+ 'text',
+ 'official',
+ ),
+ 'enum_labels' => array(
+ 'icon-text' => esc_html__( 'Icon + text', 'jetpack' ),
+ 'icon' => esc_html__( 'Icon only', 'jetpack' ),
+ 'text' => esc_html__( 'Text only', 'jetpack' ),
+ 'official' => esc_html__( 'Official buttons', 'jetpack' ),
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_list_item',
+ 'jp_group' => 'sharedaddy',
+ ),
+ 'sharing_label' => array(
+ 'description' => esc_html__( 'Sharing Label', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_string',
+ 'sanitize_callback' => 'esc_html',
+ 'jp_group' => 'sharedaddy',
+ ),
+ 'show' => array(
+ 'description' => esc_html__( 'Views where buttons are shown', 'jetpack' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string'
+ ),
+ 'default' => array( 'post' ),
+ 'validate_callback' => __CLASS__ . '::validate_sharing_show',
+ 'jp_group' => 'sharedaddy',
+ ),
+ 'jetpack-twitter-cards-site-tag' => array(
+ 'description' => esc_html__( "The Twitter username of the owner of this site's domain.", 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_twitter_username',
+ 'sanitize_callback' => 'esc_html',
+ 'jp_group' => 'sharedaddy',
+ ),
+ 'sharedaddy_disable_resources' => array(
+ 'description' => esc_html__( 'Disable CSS and JS', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'sharedaddy',
+ ),
+ 'custom' => array(
+ 'description' => esc_html__( 'Custom sharing services added by user.', 'jetpack' ),
+ 'type' => 'object',
+ 'default' => array(
+ 'sharing_name' => '',
+ 'sharing_url' => '',
+ 'sharing_icon' => '',
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_custom_service',
+ 'jp_group' => 'sharedaddy',
+ ),
+ // Not an option, but an action that can be perfomed on the list of custom services passing the service ID.
+ 'sharing_delete_service' => array(
+ 'description' => esc_html__( 'Delete custom sharing service.', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_custom_service_id',
+ 'jp_group' => 'sharedaddy',
+ ),
+
+ // SSO
+ 'jetpack_sso_require_two_step' => array(
+ 'description' => esc_html__( 'Require Two-Step Authentication', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'sso',
+ ),
+ 'jetpack_sso_match_by_email' => array(
+ 'description' => esc_html__( 'Match by Email', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'sso',
+ ),
+
+ // Subscriptions
+ 'stb_enabled' => array(
+ 'description' => esc_html__( "Show a <em>'follow blog'</em> option in the comment form", 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'subscriptions',
+ ),
+ 'stc_enabled' => array(
+ 'description' => esc_html__( "Show a <em>'follow comments'</em> option in the comment form", 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'subscriptions',
+ ),
+
+ // Related Posts
+ 'show_headline' => array(
+ 'description' => esc_html__( 'Highlight related content with a heading', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'related-posts',
+ ),
+ 'show_thumbnails' => array(
+ 'description' => esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'related-posts',
+ ),
+
+ // Verification Tools
+ 'google' => array(
+ 'description' => esc_html__( 'Google Search Console', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_verification_service',
+ 'jp_group' => 'verification-tools',
+ ),
+ 'bing' => array(
+ 'description' => esc_html__( 'Bing Webmaster Center', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_verification_service',
+ 'jp_group' => 'verification-tools',
+ ),
+ 'pinterest' => array(
+ 'description' => esc_html__( 'Pinterest Site Verification', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_verification_service',
+ 'jp_group' => 'verification-tools',
+ ),
+ 'yandex' => array(
+ 'description' => esc_html__( 'Yandex Site Verification', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_verification_service',
+ 'jp_group' => 'verification-tools',
+ ),
+ 'enable_header_ad' => array(
+ 'description' => esc_html__( 'Display an ad unit at the top of each page.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'wordads',
+ ),
+ 'wordads_approved' => array(
+ 'description' => esc_html__( 'Is site approved for WordAds?', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'wordads',
+ ),
+ 'wordads_second_belowpost' => array(
+ 'description' => esc_html__( 'Display second ad below post?', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'wordads',
+ ),
+ 'wordads_display_front_page' => array(
+ 'description' => esc_html__( 'Display ads on the front page?', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'wordads',
+ ),
+ 'wordads_display_post' => array(
+ 'description' => esc_html__( 'Display ads on posts?', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'wordads',
+ ),
+ 'wordads_display_page' => array(
+ 'description' => esc_html__( 'Display ads on pages?', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'wordads',
+ ),
+ 'wordads_display_archive' => array(
+ 'description' => esc_html__( 'Display ads on archive pages?', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'wordads',
+ ),
+ 'wordads_custom_adstxt' => array(
+ 'description' => esc_html__( 'Custom ads.txt entries', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_string',
+ 'sanitize_callback' => 'sanitize_textarea_field',
+ 'jp_group' => 'wordads',
+ ),
+
+ // Google Analytics
+ 'google_analytics_tracking_id' => array(
+ 'description' => esc_html__( 'Google Analytics', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_alphanum',
+ 'jp_group' => 'google-analytics',
+ ),
+
+ // Stats
+ 'admin_bar' => array(
+ 'description' => esc_html__( 'Put a chart showing 48 hours of views in the admin bar.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'stats',
+ ),
+ 'roles' => array(
+ 'description' => esc_html__( 'Select the roles that will be able to view stats reports.', 'jetpack' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string'
+ ),
+ 'default' => array( 'administrator' ),
+ 'validate_callback' => __CLASS__ . '::validate_stats_roles',
+ 'sanitize_callback' => __CLASS__ . '::sanitize_stats_allowed_roles',
+ 'jp_group' => 'stats',
+ ),
+ 'count_roles' => array(
+ 'description' => esc_html__( 'Count the page views of registered users who are logged in.', 'jetpack' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string'
+ ),
+ 'default' => array( 'administrator' ),
+ 'validate_callback' => __CLASS__ . '::validate_stats_roles',
+ 'jp_group' => 'stats',
+ ),
+ 'blog_id' => array(
+ 'description' => esc_html__( 'Blog ID.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'stats',
+ ),
+ 'do_not_track' => array(
+ 'description' => esc_html__( 'Do not track.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'stats',
+ ),
+ 'hide_smile' => array(
+ 'description' => esc_html__( 'Hide the stats smiley face image.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 1,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'stats',
+ ),
+ 'version' => array(
+ 'description' => esc_html__( 'Version.', 'jetpack' ),
+ 'type' => 'integer',
+ 'default' => 9,
+ 'validate_callback' => __CLASS__ . '::validate_posint',
+ 'jp_group' => 'stats',
+ ),
+
+ // Akismet - Not a module, but a plugin. The options can be passed and handled differently.
+ 'akismet_show_user_comments_approved' => array(
+ 'description' => '',
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'settings',
+ ),
+
+ 'wordpress_api_key' => array(
+ 'description' => '',
+ 'type' => 'string',
+ 'default' => '',
+ 'validate_callback' => __CLASS__ . '::validate_alphanum',
+ 'jp_group' => 'settings',
+ ),
+
+ // Apps card on dashboard
+ 'dismiss_dash_app_card' => array(
+ 'description' => '',
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'settings',
+ ),
+
+ // Empty stats card dismiss
+ 'dismiss_empty_stats_card' => array(
+ 'description' => '',
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'settings',
+ ),
+
+ 'lang_id' => array(
+ 'description' => esc_html__( 'Primary language for the site.', 'jetpack' ),
+ 'type' => 'string',
+ 'default' => 'en_US',
+ 'jp_group' => 'settings',
+ ),
+
+ 'onboarding' => array(
+ 'description' => '',
+ 'type' => 'object',
+ 'default' => array(
+ 'siteTitle' => '',
+ 'siteDescription' => '',
+ 'siteType' => 'personal',
+ 'homepageFormat' => 'posts',
+ 'addContactForm' => 0,
+ 'businessAddress' => array(
+ 'name' => '',
+ 'street' => '',
+ 'city' => '',
+ 'state' => '',
+ 'zip' => '',
+ ),
+ 'installWooCommerce' => false,
+ ),
+ 'validate_callback' => __CLASS__ . '::validate_onboarding',
+ 'jp_group' => 'settings',
+ ),
+
+ );
+
+ // Add modules to list so they can be toggled
+ $modules = Jetpack::get_available_modules();
+ if ( is_array( $modules ) && ! empty( $modules ) ) {
+ $module_args = array(
+ 'description' => '',
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'modules',
+ );
+ foreach( $modules as $module ) {
+ $options[ $module ] = $module_args;
+ }
+ }
+
+ if ( is_array( $selector ) ) {
+
+ // Return only those options whose keys match $selector keys
+ return array_intersect_key( $options, $selector );
+ }
+
+ if ( 'any' === $selector ) {
+
+ // Toggle module or update any module option or any general setting
+ return $options;
+ }
+
+ // We're updating the options for a single module.
+ if ( empty( $selector ) ) {
+ $selector = self::get_module_requested();
+ }
+ $selected = array();
+ foreach ( $options as $option => $attributes ) {
+
+ // Not adding an isset( $attributes['jp_group'] ) because if it's not set, it must be fixed, otherwise options will fail.
+ if ( $selector === $attributes['jp_group'] ) {
+ $selected[ $option ] = $attributes;
+ }
+ }
+ return $selected;
+ }
+
+ /**
+ * Validates that the parameters are proper values that can be set during Jetpack onboarding.
+ *
+ * @since 5.4.0
+ *
+ * @param array $onboarding_data Values 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 validate_onboarding( $onboarding_data, $request, $param ) {
+ if ( ! is_array( $onboarding_data ) ) {
+ return new WP_Error( 'invalid_param', esc_html__( 'Not valid onboarding data.', 'jetpack' ) );
+ }
+ foreach ( $onboarding_data as $value ) {
+ if ( is_string( $value ) ) {
+ $onboarding_choice = self::validate_string( $value, $request, $param );
+ } elseif ( is_array( $value ) ) {
+ $onboarding_choice = self::validate_onboarding( $value, $request, $param );
+ } else {
+ $onboarding_choice = self::validate_boolean( $value, $request, $param );
+ }
+ if ( is_wp_error( $onboarding_choice ) ) {
+ return $onboarding_choice;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is either a pure boolean or a numeric string that can be mapped to a boolean.
+ *
+ * @since 4.3.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 validate_boolean( $value, $request, $param ) {
+ if ( ! is_bool( $value ) && ! ( ( ctype_digit( $value ) || is_numeric( $value ) ) && in_array( $value, array( 0, 1 ) ) ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be true, false, 0 or 1.', 'jetpack' ), $param ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is a positive integer.
+ *
+ * @since 4.3.0
+ *
+ * @param int $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 validate_posint( $value = 0, $request, $param ) {
+ if ( ! is_numeric( $value ) || $value <= 0 ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a positive integer.', 'jetpack' ), $param ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter belongs to a list of admitted values.
+ *
+ * @since 4.3.0
+ *
+ * @param string $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 validate_list_item( $value = '', $request, $param ) {
+ $attributes = $request->get_attributes();
+ if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s not recognized', 'jetpack' ), $param ) );
+ }
+ $args = $attributes['args'][ $param ];
+ if ( ! empty( $args['enum'] ) ) {
+
+ // If it's an associative array, use the keys to check that the value is among those admitted.
+ $enum = ( count( array_filter( array_keys( $args['enum'] ), 'is_string' ) ) > 0 ) ? array_keys( $args['enum'] ) : $args['enum'];
+ if ( ! in_array( $value, $enum ) ) {
+ return new WP_Error( 'invalid_param_value', sprintf(
+ /* Translators: first variable is the parameter passed to endpoint that holds the list item, the second is a list of admitted values. */
+ esc_html__( '%1$s must be one of %2$s', 'jetpack' ), $param, implode( ', ', $enum )
+ ) );
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter belongs to a list of admitted values.
+ *
+ * @since 4.3.0
+ *
+ * @param string $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 validate_module_list( $value = '', $request, $param ) {
+ if ( ! is_array( $value ) ) {
+ return new WP_Error( 'invalid_param_value', sprintf( esc_html__( '%s must be an array', 'jetpack' ), $param ) );
+ }
+
+ $modules = Jetpack::get_available_modules();
+
+ if ( count( array_intersect( $value, $modules ) ) != count( $value ) ) {
+ return new WP_Error( 'invalid_param_value', sprintf( esc_html__( '%s must be a list of valid modules', 'jetpack' ), $param ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is an alphanumeric or empty string (to be able to clear the field).
+ *
+ * @since 4.3.0
+ *
+ * @param string $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 validate_alphanum( $value = '', $request, $param ) {
+ if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^[a-z0-9]+$/i', $value ) ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string.', 'jetpack' ), $param ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is a tag or id for a verification service, or an empty string (to be able to clear the field).
+ *
+ * @since 4.6.0
+ *
+ * @param string $value Value to check.
+ * @param WP_REST_Request $request
+ * @param string $param Name of the parameter passed to endpoint holding $value.
+ *
+ * @return bool|WP_Error
+ */
+ public static function validate_verification_service( $value = '', $request, $param ) {
+ if ( ! empty( $value ) && ! ( is_string( $value ) && ( preg_match( '/^[a-z0-9_-]+$/i', $value ) || jetpack_verification_get_code( $value ) !== false ) ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string or a verification tag.', 'jetpack' ), $param ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is among the roles allowed for Stats.
+ *
+ * @since 4.3.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 validate_stats_roles( $value, $request, $param ) {
+ if ( ! empty( $value ) && ! array_intersect( self::$stats_roles, $value ) ) {
+ return new WP_Error( 'invalid_param', sprintf(
+ /* Translators: first variable is the name of a parameter passed to endpoint holding the role that will be checked, the second is a list of roles allowed to see stats. The parameter is checked against this list. */
+ esc_html__( '%1$s must be %2$s.', 'jetpack' ), $param, join( ', ', self::$stats_roles )
+ ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is among the views where the Sharing can be displayed.
+ *
+ * @since 4.3.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 validate_sharing_show( $value, $request, $param ) {
+ $views = array( 'index', 'post', 'page', 'attachment', 'jetpack-portfolio' );
+ if ( ! is_array( $value ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array of post types.', 'jetpack' ), $param ) );
+ }
+ if ( ! array_intersect( $views, $value ) ) {
+ return new WP_Error( 'invalid_param', sprintf(
+ /* Translators: first variable is the name of a parameter passed to endpoint holding the post type where Sharing will be displayed, the second is a list of post types where Sharing can be displayed */
+ esc_html__( '%1$s must be %2$s.', 'jetpack' ), $param, join( ', ', $views )
+ ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is among the views where the Sharing can be displayed.
+ *
+ * @since 4.3.0
+ *
+ * @param string|bool $value {
+ * Value to check received by request.
+ *
+ * @type array $visible List of slug of services to share to that are displayed directly in the page.
+ * @type array $hidden List of slug of services to share to that are concealed in a folding menu.
+ * }
+ * @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 validate_services( $value, $request, $param ) {
+ if ( ! is_array( $value ) || ! isset( $value['visible'] ) || ! isset( $value['hidden'] ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array with visible and hidden items.', 'jetpack' ), $param ) );
+ }
+
+ // Allow to clear everything.
+ if ( empty( $value['visible'] ) && empty( $value['hidden'] ) ) {
+ return true;
+ }
+
+ if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
+ return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
+ }
+ $sharer = new Sharing_Service();
+ $services = array_keys( $sharer->get_all_services() );
+
+ if (
+ ( ! empty( $value['visible'] ) && ! array_intersect( $value['visible'], $services ) )
+ ||
+ ( ! empty( $value['hidden'] ) && ! array_intersect( $value['hidden'], $services ) ) )
+ {
+ return new WP_Error( 'invalid_param', sprintf(
+ /* Translators: placeholder 1 is a parameter holding the services passed to endpoint, placeholder 2 is a list of all Jetpack Sharing services */
+ esc_html__( '%1$s visible and hidden items must be a list of %2$s.', 'jetpack' ), $param, join( ', ', $services )
+ ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter has enough information to build a custom sharing button.
+ *
+ * @since 4.3.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 validate_custom_service( $value, $request, $param ) {
+ if ( ! is_array( $value ) || ! isset( $value['sharing_name'] ) || ! isset( $value['sharing_url'] ) || ! isset( $value['sharing_icon'] ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an array with sharing name, url and icon.', 'jetpack' ), $param ) );
+ }
+
+ // Allow to clear everything.
+ if ( empty( $value['sharing_name'] ) && empty( $value['sharing_url'] ) && empty( $value['sharing_icon'] ) ) {
+ return true;
+ }
+
+ if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
+ return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
+ }
+
+ if ( ( ! empty( $value['sharing_name'] ) && ! is_string( $value['sharing_name'] ) )
+ || ( ! empty( $value['sharing_url'] ) && ! is_string( $value['sharing_url'] ) )
+ || ( ! empty( $value['sharing_icon'] ) && ! is_string( $value['sharing_icon'] ) ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s needs sharing name, url and icon.', 'jetpack' ), $param ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is a custom sharing service ID like 'custom-1461976264'.
+ *
+ * @since 4.3.0
+ *
+ * @param string $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 validate_custom_service_id( $value = '', $request, $param ) {
+ if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/custom\-[0-1]+/i', $value ) ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( "%s must be a string prefixed with 'custom-' and followed by a numeric ID.", 'jetpack' ), $param ) );
+ }
+
+ if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
+ return new WP_Error( 'invalid_param', esc_html__( 'Failed loading required dependency Sharing_Service.', 'jetpack' ) );
+ }
+ $sharer = new Sharing_Service();
+ $services = array_keys( $sharer->get_all_services() );
+
+ if ( ! empty( $value ) && ! in_array( $value, $services ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s is not a registered custom sharing service.', 'jetpack' ), $param ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is a Twitter username or empty string (to be able to clear the field).
+ *
+ * @since 4.3.0
+ *
+ * @param string $value Value to check.
+ * @param WP_REST_Request $request
+ * @param string $param Name of the parameter passed to endpoint holding $value.
+ *
+ * @return bool|WP_Error
+ */
+ public static function validate_twitter_username( $value = '', $request, $param ) {
+ if ( ! empty( $value ) && ( ! is_string( $value ) || ! preg_match( '/^@?\w{1,15}$/i', $value ) ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a Twitter username.', 'jetpack' ), $param ) );
+ }
+ return true;
+ }
+
+ /**
+ * Validates that the parameter is a string.
+ *
+ * @since 4.3.0
+ *
+ * @param string $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 validate_string( $value = '', $request, $param ) {
+ if ( ! is_string( $value ) ) {
+ return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a string.', 'jetpack' ), $param ) );
+ }
+ return true;
+ }
+
+ /**
+ * If for some reason the roles allowed to see Stats are empty (for example, user tampering with checkboxes),
+ * return an array with only 'administrator' as the allowed role and save it for 'roles' option.
+ *
+ * @since 4.3.0
+ *
+ * @param string|bool $value Value to check.
+ *
+ * @return bool|array
+ */
+ public static function sanitize_stats_allowed_roles( $value ) {
+ if ( empty( $value ) ) {
+ return array( 'administrator' );
+ }
+ return $value;
+ }
+
+ /**
+ * Get the currently accessed route and return the module slug in it.
+ *
+ * @since 4.3.0
+ *
+ * @param string $route Regular expression for the endpoint with the module slug to return.
+ *
+ * @return array|string
+ */
+ public static function get_module_requested( $route = '/module/(?P<slug>[a-z\-]+)' ) {
+
+ if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
+ return '';
+ }
+
+ preg_match( "#$route#", $GLOBALS['wp']->query_vars['rest_route'], $module );
+
+ if ( empty( $module['slug'] ) ) {
+ return '';
+ }
+
+ return $module['slug'];
+ }
+
+ /**
+ * Adds extra information for modules.
+ *
+ * @since 4.3.0
+ *
+ * @param string|array $modules Can be a single module or a list of modules.
+ * @param null|string $slug Slug of the module in the first parameter.
+ *
+ * @return array|string
+ */
+ public static function prepare_modules_for_response( $modules = '', $slug = null ) {
+ global $wp_rewrite;
+
+ /** This filter is documented in modules/sitemaps/sitemaps.php */
+ $location = apply_filters( 'jetpack_sitemap_location', '' );
+
+ if ( $wp_rewrite->using_index_permalinks() ) {
+ $sitemap_url = home_url( '/index.php' . $location . '/sitemap.xml' );
+ $news_sitemap_url = home_url( '/index.php' . $location . '/news-sitemap.xml' );
+ } else if ( $wp_rewrite->using_permalinks() ) {
+ $sitemap_url = home_url( $location . '/sitemap.xml' );
+ $news_sitemap_url = home_url( $location . '/news-sitemap.xml' );
+ } else {
+ $sitemap_url = home_url( $location . '/?jetpack-sitemap=sitemap.xml' );
+ $news_sitemap_url = home_url( $location . '/?jetpack-sitemap=news-sitemap.xml' );
+ }
+
+ if ( is_null( $slug ) && isset( $modules['sitemaps'] ) ) {
+ // Is a list of modules
+ $modules['sitemaps']['extra']['sitemap_url'] = $sitemap_url;
+ $modules['sitemaps']['extra']['news_sitemap_url'] = $news_sitemap_url;
+ } elseif ( 'sitemaps' == $slug ) {
+ // It's a single module
+ $modules['extra']['sitemap_url'] = $sitemap_url;
+ $modules['extra']['news_sitemap_url'] = $news_sitemap_url;
+ }
+ return $modules;
+ }
+
+ /**
+ * Remove 'validate_callback' item from options available for module.
+ * Fetch current option value and add to array of module options.
+ * Prepare values of module options that need special handling, like those saved in wpcom.
+ *
+ * @since 4.3.0
+ *
+ * @param string $module Module slug.
+ * @return array
+ */
+ public static function prepare_options_for_response( $module = '' ) {
+ $options = self::get_updateable_data_list( $module );
+
+ if ( ! is_array( $options ) || empty( $options ) ) {
+ return $options;
+ }
+
+ // Some modules need special treatment.
+ switch ( $module ) {
+
+ case 'monitor':
+ // Status of user notifications
+ $options['monitor_receive_notifications']['current_value'] = self::cast_value( self::get_remote_value( 'monitor', 'monitor_receive_notifications' ), $options['monitor_receive_notifications'] );
+ break;
+
+ case 'post-by-email':
+ // Email address
+ $options['post_by_email_address']['current_value'] = self::cast_value( self::get_remote_value( 'post-by-email', 'post_by_email_address' ), $options['post_by_email_address'] );
+ break;
+
+ case 'protect':
+ // Protect
+ $options['jetpack_protect_key']['current_value'] = get_site_option( 'jetpack_protect_key', false );
+ if ( ! function_exists( 'jetpack_protect_format_whitelist' ) ) {
+ include_once( JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php' );
+ }
+ $options['jetpack_protect_global_whitelist']['current_value'] = jetpack_protect_format_whitelist();
+ break;
+
+ case 'related-posts':
+ // It's local, but it must be broken apart since it's saved as an array.
+ $options = self::split_options( $options, Jetpack_Options::get_option( 'relatedposts' ) );
+ break;
+
+ case 'verification-tools':
+ // It's local, but it must be broken apart since it's saved as an array.
+ $options = self::split_options( $options, get_option( 'verification_services_codes' ) );
+ break;
+
+ case 'google-analytics':
+ $wga = get_option( 'jetpack_wga' );
+ $code = '';
+ if ( is_array( $wga ) && array_key_exists( 'code', $wga ) ) {
+ $code = $wga[ 'code' ];
+ }
+ $options[ 'google_analytics_tracking_id' ][ 'current_value' ] = $code;
+ break;
+
+ case 'sharedaddy':
+ // It's local, but it must be broken apart since it's saved as an array.
+ if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
+ break;
+ }
+ $sharer = new Sharing_Service();
+ $options = self::split_options( $options, $sharer->get_global_options() );
+ $options['sharing_services']['current_value'] = $sharer->get_blog_services();
+ $other_sharedaddy_options = array( 'jetpack-twitter-cards-site-tag', 'sharedaddy_disable_resources', 'sharing_delete_service' );
+ foreach ( $other_sharedaddy_options as $key ) {
+ $default_value = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
+ $current_value = get_option( $key, $default_value );
+ $options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
+ }
+ break;
+
+ case 'stats':
+ // It's local, but it must be broken apart since it's saved as an array.
+ if ( ! function_exists( 'stats_get_options' ) ) {
+ include_once( JETPACK__PLUGIN_DIR . 'modules/stats.php' );
+ }
+ $options = self::split_options( $options, stats_get_options() );
+ break;
+ default:
+ // These option are just stored as plain WordPress options.
+ foreach ( $options as $key => $value ) {
+ $default_value = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
+ $current_value = get_option( $key, $default_value );
+ $options[ $key ]['current_value'] = self::cast_value( $current_value, $options[ $key ] );
+ }
+ }
+ // At this point some options have current_value not set because they're options
+ // that only get written on update, so we set current_value to the default one.
+ foreach ( $options as $key => $value ) {
+ // We don't need validate_callback in the response
+ if ( isset( $options[ $key ]['validate_callback'] ) ) {
+ unset( $options[ $key ]['validate_callback'] );
+ }
+ $default_value = isset( $options[ $key ]['default'] ) ? $options[ $key ]['default'] : '';
+ if ( ! array_key_exists( 'current_value', $options[ $key ] ) ) {
+ $options[ $key ]['current_value'] = self::cast_value( $default_value, $options[ $key ] );
+ }
+ }
+ return $options;
+ }
+
+ /**
+ * Splits module options saved as arrays like relatedposts or verification_services_codes into separate options to be returned in the response.
+ *
+ * @since 4.3.0
+ *
+ * @param array $separate_options Array of options admitted by the module.
+ * @param array $grouped_options Option saved as array to be splitted.
+ * @param string $prefix Optional prefix for the separate option keys.
+ *
+ * @return array
+ */
+ public static function split_options( $separate_options, $grouped_options, $prefix = '' ) {
+ if ( is_array( $grouped_options ) ) {
+ foreach ( $grouped_options as $key => $value ) {
+ $option_key = $prefix . $key;
+ if ( isset( $separate_options[ $option_key ] ) ) {
+ $separate_options[ $option_key ]['current_value'] = self::cast_value( $grouped_options[ $key ], $separate_options[ $option_key ] );
+ }
+ }
+ }
+ return $separate_options;
+ }
+
+ /**
+ * Perform a casting to the value specified in the option definition.
+ *
+ * @since 4.3.0
+ *
+ * @param mixed $value Value to cast to the proper type.
+ * @param array $definition Type to cast the value to.
+ *
+ * @return bool|float|int|string
+ */
+ public static function cast_value( $value, $definition ) {
+ if ( $value === 'NULL' ) {
+ return null;
+ }
+
+ if ( isset( $definition['type'] ) ) {
+ switch ( $definition['type'] ) {
+ case 'boolean':
+ if ( 'true' === $value ) {
+ return true;
+ } elseif ( 'false' === $value ) {
+ return false;
+ }
+ return (bool) $value;
+ break;
+
+ case 'integer':
+ return (int) $value;
+ break;
+
+ case 'float':
+ return (float) $value;
+ break;
+
+ case 'string':
+ return (string) $value;
+ break;
+ }
+ }
+ return $value;
+ }
+
+ /**
+ * Get a value not saved locally.
+ *
+ * @since 4.3.0
+ *
+ * @param string $module Module slug.
+ * @param string $option Option name.
+ *
+ * @return bool Whether user is receiving notifications or not.
+ */
+ public static function get_remote_value( $module, $option ) {
+
+ if ( in_array( $module, array( 'post-by-email' ), true ) ) {
+ $option .= get_current_user_id();
+ }
+
+ // If option doesn't exist, 'does_not_exist' will be returned.
+ $value = get_option( $option, 'does_not_exist' );
+
+ // If option exists, just return it.
+ if ( 'does_not_exist' !== $value ) {
+ return $value;
+ }
+
+ // Only check a remote option if Jetpack is connected.
+ if ( ! Jetpack::is_active() ) {
+ return false;
+ }
+
+ // Do what is necessary for each module.
+ switch ( $module ) {
+ case 'monitor':
+ // Load the class to use the method. If class can't be found, do nothing.
+ if ( ! class_exists( 'Jetpack_Monitor' ) && ! include_once( Jetpack::get_module_path( $module ) ) ) {
+ return false;
+ }
+ $value = Jetpack_Monitor::user_receives_notifications( false );
+ break;
+
+ case 'post-by-email':
+ // Load the class to use the method. If class can't be found, do nothing.
+ if ( ! class_exists( 'Jetpack_Post_By_Email' ) && ! include_once( Jetpack::get_module_path( $module ) ) ) {
+ return false;
+ }
+ $post_by_email = new Jetpack_Post_By_Email();
+ $value = $post_by_email->get_post_by_email_address();
+ if ( $value === null ) {
+ $value = 'NULL'; // sentinel value so it actually gets set
+ }
+ break;
+ }
+
+ // Normalize value to boolean.
+ if ( is_wp_error( $value ) || is_null( $value ) ) {
+ $value = false;
+ }
+
+ // Save option to use it next time.
+ update_option( $option, $value );
+
+ return $value;
+ }
+
+ /**
+ * Get number of plugin updates available.
+ *
+ * @since 4.3.0
+ *
+ * @return mixed|WP_Error Number of plugin updates available. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public static function get_plugin_update_count() {
+ $updates = wp_get_update_data();
+ if ( isset( $updates['counts'] ) && isset( $updates['counts']['plugins'] ) ) {
+ $count = $updates['counts']['plugins'];
+ if ( 0 == $count ) {
+ $response = array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'All plugins are up-to-date. Keep up the good work!', 'jetpack' ),
+ 'count' => 0,
+ );
+ } else {
+ $response = array(
+ 'code' => 'updates-available',
+ 'message' => esc_html( sprintf( _n( '%s plugin need updating.', '%s plugins need updating.', $count, 'jetpack' ), $count ) ),
+ 'count' => $count,
+ );
+ }
+ return rest_ensure_response( $response );
+ }
+
+ return new WP_Error( 'not_found', esc_html__( 'Could not check updates for plugins on this site.', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+
+ /**
+ * Returns a list of all plugins in the site.
+ *
+ * @since 4.2.0
+ * @uses get_plugins()
+ *
+ * @return array
+ */
+ private static function core_get_plugins() {
+ if ( ! function_exists( 'get_plugins' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+ /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
+ $plugins = apply_filters( 'all_plugins', get_plugins() );
+
+ if ( is_array( $plugins ) && ! empty( $plugins ) ) {
+ foreach ( $plugins as $plugin_slug => $plugin_data ) {
+ $plugins[ $plugin_slug ]['active'] = self::core_is_plugin_active( $plugin_slug );
+ }
+ return $plugins;
+ }
+
+ return array();
+ }
+
+ /**
+ * Deprecated - Get third party plugin API keys.
+ * @deprecated
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function get_service_api_key( $request ) {
+ _deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::get_service_api_key' );
+ return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::get_service_api_key( $request );
+ }
+
+ /**
+ * Deprecated - Update third party plugin API keys.
+ * @deprecated
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function update_service_api_key( $request ) {
+ _deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::update_service_api_key' );
+ return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::update_service_api_key( $request ) ;
+ }
+
+ /**
+ * Deprecated - Delete a third party plugin API key.
+ * @deprecated
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function delete_service_api_key( $request ) {
+ _deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::delete_service_api_key' );
+ return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::delete_service_api_key( $request );
+ }
+
+ /**
+ * Deprecated - Validate the service provided in /service-api-keys/ endpoints.
+ * To add a service to these endpoints, add the service name to $valid_services
+ * and add '{service name}_api_key' to the non-compact return array in get_option_names(),
+ * in class-jetpack-options.php
+ * @deprecated
+ *
+ * @param string $service The service the API key is for.
+ * @return string Returns the service name if valid, null if invalid.
+ */
+ public static function validate_service_api_service( $service = null ) {
+ _deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_service' );
+ return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_service( $service );
+ }
+
+ /**
+ * Error response for invalid service API key requests with an invalid service.
+ */
+ public static function service_api_invalid_service_response() {
+ _deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::service_api_invalid_service_response' );
+ return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::service_api_invalid_service_response();
+ }
+
+ /**
+ * Deprecated - Validate API Key
+ * @deprecated
+ *
+ * @param string $key The API key to be validated.
+ * @param string $service The service the API key is for.
+ *
+ */
+ public static function validate_service_api_key( $key = null, $service = null ) {
+ _deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_key' );
+ return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_key( $key , $service );
+ }
+
+ /**
+ * Deprecated - Validate Mapbox API key
+ * Based loosely on https://github.com/mapbox/geocoding-example/blob/master/php/MapboxTest.php
+ * @deprecated
+ *
+ * @param string $key The API key to be validated.
+ */
+ public static function validate_service_api_key_mapbox( $key ) {
+ _deprecated_function( __METHOD__, 'jetpack-6.9.0', 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_key' );
+ return WPCOM_REST_API_V2_Endpoint_Service_API_Keys::validate_service_api_key_mapbox( $key );
+
+ }
+
+ /**
+ * Checks if the queried plugin is active.
+ *
+ * @since 4.2.0
+ * @uses is_plugin_active()
+ *
+ * @return bool
+ */
+ private static function core_is_plugin_active( $plugin ) {
+ if ( ! function_exists( 'is_plugin_active' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+
+ return is_plugin_active( $plugin );
+ }
+
+ /**
+ * Get plugins data in site.
+ *
+ * @since 4.2.0
+ *
+ * @return WP_REST_Response|WP_Error List of plugins in the site. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public static function get_plugins() {
+ $plugins = self::core_get_plugins();
+
+ if ( ! empty( $plugins ) ) {
+ return rest_ensure_response( $plugins );
+ }
+
+ return new WP_Error( 'not_found', esc_html__( 'Unable to list plugins.', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ /**
+ * Get data about the queried plugin. Currently it only returns whether the plugin is active or not.
+ *
+ * @since 4.2.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ *
+ * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public static function get_plugin( $request ) {
+
+ $plugins = self::core_get_plugins();
+
+ if ( empty( $plugins ) ) {
+ return new WP_Error( 'no_plugins_found', esc_html__( 'This site has no plugins.', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ $plugin = stripslashes( $request['plugin'] );
+
+ if ( ! in_array( $plugin, array_keys( $plugins ) ) ) {
+ return new WP_Error( 'plugin_not_found', esc_html( sprintf( __( 'Plugin %s is not installed.', 'jetpack' ), $plugin ) ), array( 'status' => 404 ) );
+ }
+
+ $plugin_data = $plugins[ $plugin ];
+
+ $plugin_data['active'] = self::core_is_plugin_active( $plugin );
+
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Plugin found.', 'jetpack' ),
+ 'data' => $plugin_data
+ ) );
+ }
+
+} // class end
diff --git a/plugins/jetpack/_inc/lib/class.jetpack-automatic-install-skin.php b/plugins/jetpack/_inc/lib/class.jetpack-automatic-install-skin.php
new file mode 100644
index 00000000..228c6b2c
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.jetpack-automatic-install-skin.php
@@ -0,0 +1,111 @@
+<?php
+
+include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+include_once ABSPATH . 'wp-admin/includes/file.php';
+
+/**
+ * Allows us to capture that the site doesn't have proper file system access.
+ * In order to update the plugin.
+ */
+class Jetpack_Automatic_Install_Skin extends Automatic_Upgrader_Skin {
+ /**
+ * Stores the last error key;
+ **/
+ protected $main_error_code = 'install_error';
+
+ /**
+ * Stores the last error message.
+ **/
+ protected $main_error_message = 'An unknown error occurred during installation';
+
+ /**
+ * Overwrites the set_upgrader to be able to tell if we e ven have the ability to write to the files.
+ *
+ * @param WP_Upgrader $upgrader
+ *
+ */
+ public function set_upgrader( &$upgrader ) {
+ parent::set_upgrader( $upgrader );
+
+ // Check if we even have permission to.
+ $result = $upgrader->fs_connect( array( WP_CONTENT_DIR, WP_PLUGIN_DIR ) );
+ if ( ! $result ) {
+ // set the string here since they are not available just yet
+ $upgrader->generic_strings();
+ $this->feedback( 'fs_unavailable' );
+ }
+ }
+
+ /**
+ * Overwrites the error function
+ */
+ public function error( $error ) {
+ if ( is_wp_error( $error ) ) {
+ $this->feedback( $error );
+ }
+ }
+
+ private function set_main_error_code( $code ) {
+ // Don't set the process_failed as code since it is not that helpful unless we don't have one already set.
+ $this->main_error_code = ( $code === 'process_failed' && $this->main_error_code ? $this->main_error_code : $code );
+ }
+
+ private function set_main_error_message( $message, $code ) {
+ // Don't set the process_failed as message since it is not that helpful unless we don't have one already set.
+ $this->main_error_message = ( $code === 'process_failed' && $this->main_error_code ? $this->main_error_code : $message );
+ }
+
+ public function get_main_error_code() {
+ return $this->main_error_code;
+ }
+
+ public function get_main_error_message() {
+ return $this->main_error_message;
+ }
+
+ /**
+ * Overwrites the feedback function
+ */
+ public function feedback( $data ) {
+
+ $current_error = null;
+ if ( is_wp_error( $data ) ) {
+ $this->set_main_error_code( $data->get_error_code() );
+ $string = $data->get_error_message();
+ } elseif ( is_array( $data ) ) {
+ return;
+ } else {
+ $string = $data;
+ }
+
+ if ( ! empty( $this->upgrader->strings[$string] ) ) {
+ $this->set_main_error_code( $string );
+
+ $current_error = $string;
+ $string = $this->upgrader->strings[$string];
+ }
+
+ if ( strpos( $string, '%' ) !== false ) {
+ $args = func_get_args();
+ $args = array_splice( $args, 1 );
+ if ( ! empty( $args ) ) {
+ $string = vsprintf( $string, $args );
+ }
+ }
+
+ $string = trim( $string );
+ $string = wp_kses(
+ $string, array(
+ 'a' => array(
+ 'href' => true
+ ),
+ 'br' => true,
+ 'em' => true,
+ 'strong' => true,
+ )
+ );
+
+ $this->set_main_error_message( $string, $current_error );
+ $this->messages[] = $string;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/class.jetpack-iframe-embed.php b/plugins/jetpack/_inc/lib/class.jetpack-iframe-embed.php
new file mode 100644
index 00000000..4445cb65
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.jetpack-iframe-embed.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Tweak the preview when rendered in an iframe
+ */
+
+class Jetpack_Iframe_Embed {
+ static function init() {
+ if ( ! self::is_embedding_in_iframe() ) {
+ return;
+ }
+
+ // Disable the admin bar
+ if ( ! defined( 'IFRAME_REQUEST' ) ) {
+ define( 'IFRAME_REQUEST', true );
+ }
+
+ // Prevent canonical redirects
+ remove_filter( 'template_redirect', 'redirect_canonical' );
+
+ add_action( 'wp_head', array( 'Jetpack_Iframe_Embed', 'noindex' ), 1 );
+ add_action( 'wp_head', array( 'Jetpack_Iframe_Embed', 'base_target_blank' ), 1 );
+
+ add_filter( 'shortcode_atts_video', array( 'Jetpack_Iframe_Embed', 'disable_autoplay' ) );
+ add_filter( 'shortcode_atts_audio', array( 'Jetpack_Iframe_Embed', 'disable_autoplay' ) );
+
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ wp_enqueue_script( 'jetpack-iframe-embed', WPMU_PLUGIN_URL . '/jetpack-iframe-embed/jetpack-iframe-embed.js', array( 'jquery' ) );
+ } else {
+ $ver = sprintf( '%s-%s', gmdate( 'oW' ), defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : '' );
+ wp_enqueue_script( 'jetpack-iframe-embed', '//s0.wp.com/wp-content/mu-plugins/jetpack-iframe-embed/jetpack-iframe-embed.js', array( 'jquery' ), $ver );
+ }
+ wp_localize_script( 'jetpack-iframe-embed', '_previewSite', array( 'siteURL' => get_site_url() ) );
+ }
+
+ static function is_embedding_in_iframe() {
+ return (
+ self::has_iframe_get_param() && (
+ self::has_preview_get_param() ||
+ self::has_preview_theme_preview_param()
+ )
+ );
+ }
+
+ private static function has_iframe_get_param() {
+ return isset( $_GET['iframe'] ) && $_GET['iframe'] === 'true';
+ }
+
+ private static function has_preview_get_param() {
+ return isset( $_GET['preview'] ) && $_GET['preview'] === 'true';
+ }
+
+ private static function has_preview_theme_preview_param() {
+ return isset( $_GET['theme_preview'] ) && $_GET['theme_preview'] === 'true';
+ }
+
+ /**
+ * Disable `autoplay` shortcode attribute in context of an iframe
+ * Added via `shortcode_atts_video` & `shortcode_atts_audio` in `init`
+ *
+ * @param array $atts The output array of shortcode attributes.
+ *
+ * @return array The output array of shortcode attributes.
+ */
+ static function disable_autoplay( $atts ) {
+ return array_merge( $atts, array( 'autoplay' => false ) );
+ }
+
+ /**
+ * We don't want search engines to index iframe previews
+ * Added via `wp_head` action in `init`
+ */
+ static function noindex() {
+ echo '<meta name="robots" content="noindex,nofollow" />';
+ }
+
+ /**
+ * Make sure all links and forms open in a new window by default
+ * (unless overridden on client-side by JS)
+ * Added via `wp_head` action in `init`
+ */
+ static function base_target_blank() {
+ echo '<base target="_blank" />';
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/class.jetpack-keyring-service-helper.php b/plugins/jetpack/_inc/lib/class.jetpack-keyring-service-helper.php
new file mode 100644
index 00000000..c8005ea1
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.jetpack-keyring-service-helper.php
@@ -0,0 +1,204 @@
+<?php
+
+class Jetpack_Keyring_Service_Helper {
+ /**
+ * @var Jetpack_Keyring_Service_Helper
+ **/
+ private static $instance = null;
+
+ static function init() {
+ if ( is_null( self::$instance ) ) {
+ self::$instance = new Jetpack_Keyring_Service_Helper;
+ }
+
+ return self::$instance;
+ }
+
+ public static $SERVICES = array(
+ 'facebook' => array(
+ 'for' => 'publicize'
+ ),
+ 'twitter' => array(
+ 'for' => 'publicize'
+ ),
+ 'linkedin' => array(
+ 'for' => 'publicize'
+ ),
+ 'tumblr' => array(
+ 'for' => 'publicize'
+ ),
+ 'path' => array(
+ 'for' => 'publicize'
+ ),
+ 'google_plus' => array(
+ 'for' => 'publicize'
+ ),
+ 'google_site_verification' => array(
+ 'for' => 'other'
+ )
+ );
+
+ private function __construct() {
+ add_action( 'load-settings_page_sharing', array( __CLASS__, 'admin_page_load' ), 9 );
+ }
+
+ function get_services( $filter = 'all' ) {
+ $services = array(
+
+ );
+
+ if ( 'all' == $filter ) {
+ return $services;
+ } else {
+ $connected_services = array();
+ foreach ( $services as $service => $empty ) {
+ $connections = $this->get_connections( $service );
+ if ( $connections ) {
+ $connected_services[ $service ] = $connections;
+ }
+ }
+ return $connected_services;
+ }
+ }
+
+ /**
+ * Gets a URL to the public-api actions. Works like WP's admin_url
+ *
+ * @param string $service Shortname of a specific service.
+ *
+ * @return URL to specific public-api process
+ */
+ // on WordPress.com this is/calls Keyring::admin_url
+ static function api_url( $service = false, $params = array() ) {
+ /**
+ * Filters the API URL used to interact with WordPress.com.
+ *
+ * @since 2.0.0
+ *
+ * @param string https://public-api.wordpress.com/connect/?jetpack=publicize Default Publicize API URL.
+ */
+ $url = apply_filters( 'publicize_api_url', 'https://public-api.wordpress.com/connect/?jetpack=publicize' );
+
+ if ( $service ) {
+ $url = add_query_arg( array( 'service' => $service ), $url );
+ }
+
+ if ( count( $params ) ) {
+ $url = add_query_arg( $params, $url );
+ }
+
+ return $url;
+ }
+
+ static function connect_url( $service_name, $for ) {
+ return add_query_arg( array(
+ 'action' => 'request',
+ 'service' => $service_name,
+ 'kr_nonce' => wp_create_nonce( 'keyring-request' ),
+ 'nonce' => wp_create_nonce( "keyring-request-$service_name" ),
+ 'for' => $for,
+ ), menu_page_url( 'sharing', false ) );
+ }
+
+ static function refresh_url( $service_name, $for ) {
+ return add_query_arg( array(
+ 'action' => 'request',
+ 'service' => $service_name,
+ 'kr_nonce' => wp_create_nonce( 'keyring-request' ),
+ 'refresh' => 1,
+ 'for' => $for,
+ 'nonce' => wp_create_nonce( "keyring-request-$service_name" ),
+ ), admin_url( 'options-general.php?page=sharing' ) );
+ }
+
+ static function disconnect_url( $service_name, $id ) {
+ return add_query_arg( array(
+ 'action' => 'delete',
+ 'service' => $service_name,
+ 'id' => $id,
+ 'kr_nonce' => wp_create_nonce( 'keyring-request' ),
+ 'nonce' => wp_create_nonce( "keyring-request-$service_name" ),
+ ), menu_page_url( 'sharing', false ) );
+ }
+
+ static function admin_page_load() {
+ if ( isset( $_GET['action'] ) ) {
+ if ( isset( $_GET['service'] ) ) {
+ $service_name = $_GET['service'];
+ }
+
+ switch ( $_GET['action'] ) {
+
+ case 'request':
+ check_admin_referer( 'keyring-request', 'kr_nonce' );
+ check_admin_referer( "keyring-request-$service_name", 'nonce' );
+
+ $verification = Jetpack::generate_secrets( 'publicize' );
+ if ( ! $verification ) {
+ $url = Jetpack::admin_url( 'jetpack#/settings' );
+ wp_die( sprintf( __( "Jetpack is not connected. Please connect Jetpack by visiting <a href='%s'>Settings</a>.", 'jetpack' ), $url ) );
+
+ }
+ $stats_options = get_option( 'stats_options' );
+ $wpcom_blog_id = Jetpack_Options::get_option( 'id' );
+ $wpcom_blog_id = ! empty( $wpcom_blog_id ) ? $wpcom_blog_id : $stats_options['blog_id'];
+
+ $user = wp_get_current_user();
+ $redirect = Jetpack_Keyring_Service_Helper::api_url( $service_name, urlencode_deep( array(
+ 'action' => 'request',
+ 'redirect_uri' => add_query_arg( array( 'action' => 'done' ), menu_page_url( 'sharing', false ) ),
+ 'for' => 'publicize',
+ // required flag that says this connection is intended for publicize
+ 'siteurl' => site_url(),
+ 'state' => $user->ID,
+ 'blog_id' => $wpcom_blog_id,
+ 'secret_1' => $verification['secret_1'],
+ 'secret_2' => $verification['secret_2'],
+ 'eol' => $verification['exp'],
+ ) ) );
+ wp_redirect( $redirect );
+ exit;
+ break;
+
+ case 'completed':
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client();
+ $xml->query( 'jetpack.fetchPublicizeConnections' );
+
+ if ( ! $xml->isError() ) {
+ $response = $xml->getResponse();
+ Jetpack_Options::update_option( 'publicize_connections', $response );
+ }
+
+ break;
+
+ case 'delete':
+ $id = $_GET['id'];
+
+ check_admin_referer( 'keyring-request', 'kr_nonce' );
+ check_admin_referer( "keyring-request-$service_name", 'nonce' );
+
+ Jetpack_Keyring_Service_Helper::disconnect( $service_name, $id );
+
+ do_action( 'connection_disconnected', $service_name );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Remove a Publicize connection
+ */
+ static function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ) {
+ Jetpack::load_xml_rpc_client();
+ $xml = new Jetpack_IXR_Client();
+ $xml->query( 'jetpack.deletePublicizeConnection', $connection_id );
+
+ if ( ! $xml->isError() ) {
+ Jetpack_Options::update_option( 'publicize_connections', $xml->getResponse() );
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/plugins/jetpack/_inc/lib/class.jetpack-password-checker.php b/plugins/jetpack/_inc/lib/class.jetpack-password-checker.php
new file mode 100644
index 00000000..14de6053
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.jetpack-password-checker.php
@@ -0,0 +1,1288 @@
+<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * The password strength checker.
+ *
+ * @package jetpack
+ */
+
+/**
+ * Checks passwords strength.
+ */
+class Jetpack_Password_Checker {
+
+ /**
+ * Minimum entropy bits a password should contain. 36 bits of entropy is considered
+ * to be a reasonable password, 28 stands for a weak one.
+ *
+ * @const Integer
+ */
+ const MINIMUM_BITS = 28;
+
+ /**
+ * Currently tested password.
+ *
+ * @var String
+ */
+ public $password = '';
+
+ /**
+ * Test results array.
+ *
+ * @var Array
+ */
+ public $test_results = '';
+
+ /**
+ * Current password score.
+ *
+ * @var Integer
+ */
+ public $score = 0;
+
+ /**
+ * Current multiplier affecting the score.
+ *
+ * @var Integer
+ */
+ public $multiplier = 4;
+
+ /**
+ * A common password blacklist, which on match will immediately disqualify the password.
+ *
+ * @var Array
+ */
+ public $common_passwords = array();
+
+ /**
+ * Minimum password length setting.
+ *
+ * @var Integer
+ */
+ public $min_password_length = 6;
+
+ /**
+ * User defined strings that passwords need to be tested for a match against.
+ *
+ * @var Array
+ */
+ private $user_strings_to_test = array();
+
+ /**
+ * The user object for whom the password is being tested.
+ *
+ * @var WP_User
+ */
+ protected $user;
+
+ /**
+ * The user identifier for whom the password is being tested, used if there's no user object.
+ *
+ * @var WP_User
+ */
+ protected $user_id;
+
+ /**
+ * Creates an instance of the password checker class for the specified user, or
+ * defaults to the currently logged in user.
+ *
+ * @param Mixed $user can be an integer ID, or a WP_User object.
+ */
+ public function __construct( $user = null ) {
+
+ /**
+ * Filters Jetpack's password strength enforcement settings. You can supply your own passwords
+ * that should not be used for authenticating in addition to weak and easy to guess strings for
+ * each user. For example, you can add passwords from known password databases to avoid compromised
+ * password usage.
+ *
+ * @since 7.2.0
+ *
+ * @param Array $restricted_passwords strings that are forbidden for use as passwords.
+ */
+ $this->common_passwords = apply_filters( 'jetpack_password_checker_restricted_strings', array() );
+
+ if ( is_null( $user ) ) {
+ $this->user_id = get_current_user_id();
+ } elseif ( is_object( $user ) && isset( $user->ID ) ) {
+
+ // Existing user, using their ID.
+ $this->user_id = $user->ID;
+
+ } elseif ( is_object( $user ) ) {
+
+ // Newly created user, using existing data.
+ $this->user = $user;
+ $this->user_id = 'new_user';
+
+ } else {
+ $this->user_id = $user;
+ }
+ $this->min_password_length = apply_filters( 'better_password_min_length', $this->min_password_length );
+ }
+
+ /**
+ * Run tests against a password.
+ *
+ * @param String $password the tested string.
+ * @param Boolean $required_only only test against required conditions, defaults to false.
+ * @return Array $results an array containing failed and passed test results.
+ */
+ public function test( $password, $required_only = false ) {
+
+ $this->password = $password;
+ $results = $this->run_tests( $this->list_tests(), $required_only );
+
+ // If we've failed on the required tests, return now.
+ if ( ! empty( $results['failed'] ) ) {
+ return array(
+ 'passed' => false,
+ 'test_results' => $results,
+ );
+ }
+
+ /**
+ * Filters Jetpack's password strength enforcement settings. You can modify the minimum
+ * entropy bits requirement using this filter.
+ *
+ * @since 7.2.0
+ *
+ * @param Array $minimum_entropy_bits minimum entropy bits requirement.
+ */
+ $bits = apply_filters( 'jetpack_password_checker_minimum_entropy_bits', self::MINIMUM_BITS );
+ $entropy_bits = $this->calculate_entropy_bits( $this->password );
+
+ // If we have failed the entropy bits test, run the regex tests so we can suggest improvements.
+ if ( $entropy_bits < $bits ) {
+ $results['failed']['entropy_bits'] = $entropy_bits;
+ $results = array_merge(
+ $results,
+ $this->run_tests( $this->list_tests( 'preg_match' ), false )
+ );
+ }
+
+ return( array(
+ 'passed' => empty( $results['failed'] ),
+ 'test_results' => $results,
+ ) );
+ }
+
+ /**
+ * Run the tests using the currently set up object values.
+ *
+ * @param Array $tests tests to run.
+ * @param Boolean $required_only whether to run only required tests.
+ * @return Array test results.
+ */
+ protected function run_tests( $tests, $required_only = false ) {
+
+ $results = array(
+ 'passed' => array(),
+ 'failed' => array(),
+ );
+
+ foreach ( $tests as $test_type => $section_tests ) {
+ foreach ( $section_tests as $test_name => $test_data ) {
+
+ // Skip non-required tests if required_only param is set.
+ if ( $required_only && ! $test_data['required'] ) {
+ continue;
+ }
+
+ $test_function = 'test_' . $test_type;
+
+ $result = call_user_func( array( $this, $test_function ), $test_data );
+
+ if ( $result ) {
+ $results['passed'][] = array( 'test_name' => $test_name );
+ } else {
+ $results['failed'][] = array(
+ 'test_name' => $test_name,
+ 'explanation' => $test_data['error'],
+ );
+
+ if ( isset( $test_data['fail_immediately'] ) ) {
+ return $results;
+ }
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Returns a list of tests that need to be run on password strings.
+ *
+ * @param Array $sections only return specific sections with the passed keys, defaults to all.
+ * @return Array test descriptions.
+ */
+ protected function list_tests( $sections = false ) {
+ // Note: these should be in order of priority.
+ $tests = array(
+ 'preg_match' => array(
+ 'no_backslashes' => array(
+ 'pattern' => '^[^\\\\]*$',
+ 'error' => __( 'Passwords may not contain the character "\".', 'jetpack' ),
+ 'required' => true,
+ 'fail_immediately' => true,
+ ),
+ 'minimum_length' => array(
+ 'pattern' => '^.{' . $this->min_password_length . ',}',
+ /* translators: %d is a number of characters in the password. */
+ 'error' => sprintf( __( 'Password must be at least %d characters.', 'jetpack' ), $this->min_password_length ),
+ 'required' => true,
+ 'fail_immediately' => true,
+ ),
+ 'has_mixed_case' => array(
+ 'pattern' => '([a-z].*?[A-Z]|[A-Z].*?[a-z])',
+ 'error' => __( 'This password is too easy to guess: you can improve it by adding additional uppercase letters, lowercase letters, or numbers.', 'jetpack' ),
+ 'trim' => true,
+ 'required' => false,
+ ),
+ 'has_digit' => array(
+ 'pattern' => '\d',
+ 'error' => __( 'This password is too easy to guess: you can improve it by mixing both letters and numbers.', 'jetpack' ),
+ 'trim' => false,
+ 'required' => false,
+ ),
+ 'has_special_char' => array(
+ 'pattern' => '[^a-zA-Z\d]',
+ 'error' => __( 'This password is too easy to guess: you can improve it by including special characters such as !#=?*&.', 'jetpack' ),
+ 'required' => false,
+ ),
+ ),
+ 'compare_to_list' => array(
+ 'not_a_common_password' => array(
+ 'list_callback' => 'get_common_passwords',
+ 'compare_callback' => 'negative_in_array',
+ 'error' => __( 'This is a very common password. Choose something that will be harder for others to guess.', 'jetpack' ),
+ 'required' => true,
+ ),
+ 'not_same_as_other_user_data' => array(
+ 'list_callback' => 'get_other_user_data',
+ 'compare_callback' => 'test_not_same_as_other_user_data',
+ 'error' => __( 'Your password is too weak: Looks like you\'re including easy to guess information about yourself. Try something a little more unique.', 'jetpack' ),
+ 'required' => true,
+ ),
+ ),
+ );
+
+ /**
+ * Filters Jetpack's password strength enforcement settings. You can determine the tests run
+ * and their order based on whatever criteria you wish to specify.
+ *
+ * @since 7.2.0
+ *
+ * @param Array $minimum_entropy_bits minimum entropy bits requirement.
+ */
+ $tests = apply_filters( 'jetpack_password_checker_tests', $tests );
+
+ if ( ! $sections ) {
+ return $tests;
+ }
+
+ $sections = (array) $sections;
+ return array_intersect_key( $tests, array_flip( $sections ) );
+ }
+
+ /**
+ * Provides the regular expression tester functionality.
+ *
+ * @param Array $test_data the current test data.
+ * @return Boolean does the test pass?
+ */
+ protected function test_preg_match( $test_data ) {
+ $password = stripslashes( $this->password );
+
+ if ( isset( $test_data['trim'] ) ) {
+ $password = substr( $password, 1, -1 );
+ }
+
+ if ( ! preg_match( '/' . $test_data['pattern'] . '/u', $password ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Provides the comparison tester functionality.
+ *
+ * @param Array $test_data the current test data.
+ * @return Boolean does the test pass?
+ */
+ protected function test_compare_to_list( $test_data ) {
+ $list_callback = $test_data['list_callback'];
+ $compare_callback = $test_data['compare_callback'];
+
+ if (
+ ! is_callable( array( $this, $list_callback ) )
+ || ! is_callable( array( $this, $compare_callback ) )
+ ) {
+ return false;
+ }
+
+ $list = call_user_func( array( $this, $list_callback ) );
+ if ( empty( $list ) ) {
+ return true;
+ }
+
+ return call_user_func( array( $this, $compare_callback ), $this->password, $list );
+ }
+
+ /**
+ * Getter for the common password list.
+ *
+ * @return Array common passwords.
+ */
+ protected function get_common_passwords() {
+ return $this->common_passwords;
+ }
+
+ /**
+ * Returns the widely known user data that can not be used in the password to avoid
+ * predictable strings.
+ *
+ * @return Array user data.
+ */
+ protected function get_other_user_data() {
+
+ if ( ! isset( $this->user ) ) {
+ $user_data = get_userdata( $this->user_id );
+
+ $first_name = get_user_meta( $user_data->ID, 'first_name', true );
+ $last_name = get_user_meta( $user_data->ID, 'last_name', true );
+ $nickname = get_user_meta( $user_data->ID, 'nickname', true );
+
+ $this->add_user_strings_to_test( $nickname );
+ $this->add_user_strings_to_test( $user_data->user_nicename );
+ $this->add_user_strings_to_test( $user_data->display_name );
+ } else {
+ $user_data = $this->user;
+
+ $first_name = $user_data->first_name;
+ $last_name = $user_data->last_name;
+ }
+ $email_username = substr( $user_data->user_email, 0, strpos( $user_data->user_email, '@' ) );
+
+ $this->add_user_strings_to_test( $user_data->user_email );
+ $this->add_user_strings_to_test( $email_username, '.' );
+ $this->add_user_strings_to_test( $first_name );
+ $this->add_user_strings_to_test( $last_name );
+
+ return $this->user_strings_to_test;
+ }
+
+ /**
+ * Compare the password for matches with known user data.
+ *
+ * @param String $password the string to be tested.
+ * @param Array $strings_to_test known user data.
+ * @return Boolean does the test pass?
+ */
+ protected function test_not_same_as_other_user_data( $password, $strings_to_test ) {
+ $password_lowercase = strtolower( $password );
+ foreach ( array_unique( $strings_to_test ) as $string ) {
+ if ( empty( $string ) ) {
+ continue;
+ }
+
+ $string = strtolower( $string );
+ $string_reversed = strrev( $string );
+
+ if ( $password_lowercase === $string || $password_lowercase === $string_reversed ) {
+ return false;
+ }
+
+ // Also check for the string or reversed string with any numbers just stuck to the end to catch things like bob123 as passwords.
+ if (
+ preg_match( '/^' . preg_quote( $string, '/' ) . '\d+$/', $password_lowercase )
+ || preg_match( '/^' . preg_quote( $string_reversed, '/' ) . '\d+$/', $password_lowercase )
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A shorthand for the not in array construct.
+ *
+ * @param Mixed $needle the needle.
+ * @param Array $haystack the haystack.
+ * @return is the needle not in the haystack?
+ */
+ protected function negative_in_array( $needle, $haystack ) {
+ if ( in_array( $needle, $haystack, true ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * A helper function used to break a single string into its constituents so
+ * that both the full string and its constituents and any variants thereof
+ * can be tested against the password.
+ *
+ * @param String $string the string to be broken down.
+ * @param String $explode_delimiter delimiter.
+ * @return NULL|Array array of fragments, or NULL on empty string.
+ */
+ protected function add_user_strings_to_test( $string, $explode_delimiter = ' ' ) {
+
+ // Don't check against empty strings.
+ if ( empty( $string ) ) {
+ return;
+ }
+
+ $strings = explode( $explode_delimiter, $string );
+
+ // Remove any non alpha numeric characters from the strings to check against.
+ foreach ( $strings as $key => $_string ) {
+ $strings[ $key ] = preg_replace( '/[^a-zA-Z0-9]/', '', $_string );
+ }
+
+ // Check the original too.
+ $strings[] = $string;
+
+ // Check the original minus non alpha numeric characters.
+ $strings[] = preg_replace( '/[^a-zA-Z0-9]/', '', $string );
+
+ // Remove any empty strings.
+ $strings = array_filter( $strings );
+ $this->user_strings_to_test = array_merge( $this->user_strings_to_test, $strings );
+ }
+
+ /**
+ * Return a character set size that is used in the string.
+ *
+ * @param String $password the password.
+ * @return Integer number of different character sets in use.
+ */
+ protected function get_charset_size( $password ) {
+ $size = 0;
+
+ // Lowercase a-z.
+ if ( preg_match( '/[a-z]/', $password ) ) {
+ $size += 26;
+ }
+
+ // Uppercase A-Z.
+ if ( preg_match( '/[A-Z]/', substr( $password, 1, -1 ) ) ) {
+ $size += 26;
+ }
+
+ // Digits.
+ if ( preg_match( '/\d/', substr( $password, 1, -1 ) ) ) {
+ $size += 10;
+ }
+
+ // Over digits symbols.
+ if ( preg_match( '/[!|@|#|$|%|^|&|*|(|)]/', $password ) ) {
+ $size += 10;
+ }
+
+ // Other symbols.
+ if ( preg_match( '#[`|~|-|_|=|+|\[|{|\]|}|\\|\|;:\'",<\.>/\?]#', $password ) ) {
+ $size += 20;
+ }
+
+ // Spaces.
+ if ( strpos( $password, ' ' ) ) {
+ $size++;
+ }
+
+ return $size;
+ }
+
+ /**
+ * Shorthand for getting a character index.
+ *
+ * @param String $char character.
+ * @return Integer the character code.
+ */
+ protected function get_char_index( $char ) {
+ $char = strtolower( $char[0] );
+ if ( $char < 'a' || $char > 'z' ) {
+ return 0;
+ } else {
+ return ord( $char[0] ) - ord( 'a' ) + 1;
+ }
+ }
+
+ /**
+ * This is the password strength calculation algorithm, based on the formula H = L(logN/log2).
+ *
+ * H = Entropy
+ * L = String length (the for iterator)
+ * N = Our charset size, via get_charset_size()
+ *
+ * @see http://en.wikipedia.org/wiki/Password_strength#Random_passwords
+ *
+ * On top of the base formula, we're also multiplying the bits of entropy for every char
+ * by 1 - (the probabily of it following the previous char)
+ * i.e.: the probablity of U following Q is ~0.84. If our password contains this pair of characters,
+ * the u char will only add ( 0.16^2 * charset_score ) to our total of entropy bits.
+ *
+ * @param String $password the password.
+ */
+ protected function calculate_entropy_bits( $password ) {
+ $bits = 0;
+ $charset_score = log( $this->get_charset_size( $password ) ) / log( 2 );
+
+ $aidx = $this->get_char_index( $password[0] );
+ $length = strlen( $password );
+
+ for ( $b = 1; $b < $length; $b++ ) {
+ $bidx = $this->get_char_index( $password[ $b ] );
+
+ // 27 = number of chars in the index (a-z,' ').
+ $c = 1.0 - $this->frequency_table[ $aidx * 27 + $bidx ];
+ $bits += $charset_score * $c * $c;
+
+ // Move on to next pair.
+ $aidx = $bidx;
+ }
+
+ return $bits;
+ }
+
+ /**
+ * A frequency table of character pairs, starting with ' ' then ' a', ' b' [...] , 'a ', 'aa' etc.
+ *
+ * @see http://rumkin.com/tools/password/passchk.php
+ * @var Array
+ */
+ public $frequency_table = array(
+ 0.23653710453418866,
+ 0.04577693541332556,
+ 0.03449832337075375,
+ 0.042918209651552706,
+ 0.037390873305146524,
+ 0.028509112115468728,
+ 0.02350896632162123,
+ 0.022188657238664526,
+ 0.028429800262428927,
+ 0.04357019973757107,
+ 0.00913602565971716,
+ 0.03223093745443942,
+ 0.02235311269864412,
+ 0.04438081352966905,
+ 0.04512377897652719,
+ 0.020055401662049863,
+ 0.055903192885260244,
+ 0.0024388394809739026,
+ 0.035207464644991984,
+ 0.07355941099285611,
+ 0.036905671380667734,
+ 0.026134421927394666,
+ 0.023787724158040528,
+ 0.011352092141711621,
+ 0.0032354570637119114,
+ 0.005986878553725033,
+ 0.008861933226417843,
+ 0.11511532293337222,
+ 0.027556203528211108,
+ 0.024331243621519172,
+ 0.039266365359381834,
+ 0.031599941682461,
+ 0.014403265782183991,
+ 0.015480973902901297,
+ 0.027770812071730572,
+ 0.00942761335471643,
+ 0.039872867764980315,
+ 0.0078122175244204695,
+ 0.02808456043154979,
+ 0.08429100451960927,
+ 0.04688963405744277,
+ 0.13831170724595424,
+ 0.002540311998833649,
+ 0.025211838460416972,
+ 0.001543082081936142,
+ 0.09519638431258201,
+ 0.061845750109345385,
+ 0.08907071001603732,
+ 0.02137571074500656,
+ 0.027093162268552268,
+ 0.005521504592506197,
+ 0.003023181221752442,
+ 0.007086747339262283,
+ 0.010262720513194342,
+ 0.08785070710016038,
+ 0.14617757690625455,
+ 0.03417291150313457,
+ 0.0059635515381250915,
+ 0.006146668610584633,
+ 0.195202799241872,
+ 0.002774748505613063,
+ 0.004715556203528212,
+ 0.0044776206444088066,
+ 0.11205481848665985,
+ 0.005654468581425864,
+ 0.0028820527773727946,
+ 0.07383000437381543,
+ 0.005516839189386207,
+ 0.006496573844583759,
+ 0.09843067502551392,
+ 0.0027140982650532145,
+ 0.0006893133109782768,
+ 0.08425368129464937,
+ 0.021325557661466685,
+ 0.006493074792243767,
+ 0.07023414491908442,
+ 0.002077270739174807,
+ 0.0024633328473538415,
+ 0.0007744569179180639,
+ 0.015413325557661468,
+ 0.0011990086018370024,
+ 0.13162851727657093,
+ 0.10115993585070711,
+ 0.0026989357049132527,
+ 0.03319317684793702,
+ 0.002946202070272634,
+ 0.0783216212275842,
+ 0.0018358361277154103,
+ 0.00258813238081353,
+ 0.2141688292754046,
+ 0.09853681294649366,
+ 0.0032482869222918796,
+ 0.04359352675317102,
+ 0.01993526753171016,
+ 0.0036880011663507797,
+ 0.008011663507799971,
+ 0.12014696019827964,
+ 0.0029846916460125384,
+ 0.0017553579238956116,
+ 0.029470185158186325,
+ 0.010413179763813967,
+ 0.030699518880303252,
+ 0.03508499781309229,
+ 0.002021285901734947,
+ 0.0010613792097973467,
+ 0.0005295232541186761,
+ 0.009677212421635807,
+ 0.010585799679253535,
+ 0.17101734946785244,
+ 0.07968625164018078,
+ 0.007839043592360402,
+ 0.005438693687126403,
+ 0.0183606939787141,
+ 0.2732701559994168,
+ 0.004953491762647616,
+ 0.007259367254701851,
+ 0.008104971570199739,
+ 0.13274588132380813,
+ 0.004210526315789474,
+ 0.004997813092287506,
+ 0.017006560723137484,
+ 0.007442484327161393,
+ 0.016789619478058026,
+ 0.08477737279486806,
+ 0.005106283714827234,
+ 0.0005026971861787433,
+ 0.04040355736987899,
+ 0.037535500801866156,
+ 0.00885960052485785,
+ 0.0336410555474559,
+ 0.007066919376002332,
+ 0.005344219273946639,
+ 0.0006333284735384167,
+ 0.010684939495553289,
+ 0.0063064586674442345,
+ 0.15386849394955532,
+ 0.015049424114302375,
+ 0.012162705933809595,
+ 0.020425134859308938,
+ 0.037366379938766583,
+ 0.02157165767604607,
+ 0.009373961218836564,
+ 0.0173214754337367,
+ 0.009616562181075958,
+ 0.029522670943286193,
+ 0.010154249890654615,
+ 0.018600962239393497,
+ 0.06362210234728094,
+ 0.03157078291296107,
+ 0.151603440734801,
+ 0.0062329785683044175,
+ 0.014775331681003062,
+ 0.0020854351946347867,
+ 0.1826342032366234,
+ 0.0878017203674005,
+ 0.054190989940224525,
+ 0.010329202507654177,
+ 0.012763376585508092,
+ 0.0064872430383437815,
+ 0.006381105117364048,
+ 0.005388540603586529,
+ 0.0090800408222773,
+ 0.09611196967487973,
+ 0.09940691062837148,
+ 0.01033969966467415,
+ 0.004034407348009914,
+ 0.008826942703017933,
+ 0.11474675608689314,
+ 0.07132584924916169,
+ 0.012388977985129028,
+ 0.005435194634786413,
+ 0.1417174515235457,
+ 0.0037066627788307337,
+ 0.0045802595130485495,
+ 0.060800699810468,
+ 0.005341886572386646,
+ 0.005683627350925791,
+ 0.12434932205860913,
+ 0.004596588423968508,
+ 0.0007534626038781163,
+ 0.07107041842834232,
+ 0.022361277154104096,
+ 0.04784720804782038,
+ 0.06277533168100306,
+ 0.003441901151771395,
+ 0.005828254847645429,
+ 0.0009669047966175828,
+ 0.009470768333576322,
+ 0.002077270739174807,
+ 0.12797667298440007,
+ 0.08797783933518005,
+ 0.005388540603586529,
+ 0.0024913252660737715,
+ 0.007550954949701123,
+ 0.2786866890217233,
+ 0.002509986878553725,
+ 0.029002478495407494,
+ 0.0303204548768042,
+ 0.07576614666861058,
+ 0.00246799825047383,
+ 0.00592389561160519,
+ 0.039574281965301064,
+ 0.00706808572678233,
+ 0.03304505029887739,
+ 0.05474150750838315,
+ 0.0028633911648928414,
+ 0.0005073625892987316,
+ 0.07293541332555767,
+ 0.053528502697186175,
+ 0.022566554891383584,
+ 0.038151334013704616,
+ 0.002716430966613209,
+ 0.005049132526607377,
+ 0.0009902318122175246,
+ 0.008997229916897508,
+ 0.0011861787432570347,
+ 0.1666377022889634,
+ 0.14414462749671964,
+ 0.003374252806531564,
+ 0.005169266656947077,
+ 0.008468873013558828,
+ 0.16337541915731155,
+ 0.002873888321912815,
+ 0.004305000728969237,
+ 0.0031141565825922144,
+ 0.1241172182533897,
+ 0.0052800699810468,
+ 0.008969237498177577,
+ 0.024094474413179766,
+ 0.017029887738737422,
+ 0.01722700102055693,
+ 0.10618457501093455,
+ 0.006147834961364631,
+ 0.0008269427030179326,
+ 0.03303571949263741,
+ 0.024188948826359528,
+ 0.05213937891820965,
+ 0.04505846333284735,
+ 0.0035270447587111824,
+ 0.006799825047383001,
+ 0.0008199445983379502,
+ 0.02206735675754483,
+ 0.001010059775477475,
+ 0.11971191135734072,
+ 0.04656538854060359,
+ 0.011243621519171892,
+ 0.06513019390581717,
+ 0.032375564951159064,
+ 0.06347047674588133,
+ 0.013678961947805804,
+ 0.03309870243475726,
+ 0.006982942119842543,
+ 0.009726199154395685,
+ 0.010121592068814697,
+ 0.032514360693978714,
+ 0.04986032949409535,
+ 0.039734072022160664,
+ 0.15690683773144773,
+ 0.03949963551538125,
+ 0.014790494241143023,
+ 0.002722262720513194,
+ 0.02614375273363464,
+ 0.10753637556495116,
+ 0.06764834523983088,
+ 0.006221315060504448,
+ 0.021317393206006705,
+ 0.0030826651115322934,
+ 0.002399183554454002,
+ 0.0019069835252952323,
+ 0.015595276279341012,
+ 0.0925126111678087,
+ 0.18437906400349907,
+ 0.006538562472663654,
+ 0.008719638431258201,
+ 0.02116693395538708,
+ 0.18241376293920394,
+ 0.007290858725761773,
+ 0.005976381396705059,
+ 0.005629975215045925,
+ 0.09721300481119698,
+ 0.004810030616707975,
+ 0.024303251202799244,
+ 0.012954658113427612,
+ 0.011057005394372358,
+ 0.02733459688001166,
+ 0.10135121737862662,
+ 0.012016912086309959,
+ 0.001055547455897361,
+ 0.009027555037177431,
+ 0.07162326869806095,
+ 0.01007143898527482,
+ 0.07297623560285756,
+ 0.006741507508383147,
+ 0.0036891675171307776,
+ 0.0008409389123778977,
+ 0.011272780288671819,
+ 0.007020265344802449,
+ 0.1030389269572824,
+ 0.15350809155853623,
+ 0.004232686980609419,
+ 0.004353987461729115,
+ 0.0023385333138941536,
+ 0.14450386353695874,
+ 0.002546143752733635,
+ 0.0024470039364338824,
+ 0.01200758128006998,
+ 0.0981227584195947,
+ 0.003161976964572095,
+ 0.040695145064878264,
+ 0.03460446129173349,
+ 0.003908441463770229,
+ 0.01598483743986004,
+ 0.13107216795451232,
+ 0.003129319142732177,
+ 0.00032307916605919226,
+ 0.04050386353695874,
+ 0.05452689896486368,
+ 0.03589677795597026,
+ 0.07087097244496282,
+ 0.006143169558244642,
+ 0.008684647907858289,
+ 0.0004607085580988482,
+ 0.022010205569324977,
+ 0.0009097536083977258,
+ 0.07328765126111678,
+ 0.14751421490013122,
+ 0.008015162560139961,
+ 0.006601545414783497,
+ 0.025279486805656802,
+ 0.1682449336637994,
+ 0.008313748359819215,
+ 0.007010934538562473,
+ 0.005886572386645284,
+ 0.16889575739903775,
+ 0.004123050007289692,
+ 0.011925936725470185,
+ 0.10007289692374982,
+ 0.013380376148126549,
+ 0.009021723283277445,
+ 0.08650823735238372,
+ 0.007756232686980609,
+ 0.0007243038343781893,
+ 0.0026791077416533026,
+ 0.02797492345823006,
+ 0.032384895757399036,
+ 0.04187432570345531,
+ 0.00882461000145794,
+ 0.0032401224668318998,
+ 0.00033357632307916605,
+ 0.027878116343490307,
+ 0.0022277299897944304,
+ 0.14333518005540166,
+ 0.1725534334451086,
+ 0.02781629975215046,
+ 0.006909462020702727,
+ 0.005264907420906838,
+ 0.16661437527336345,
+ 0.004325995043009185,
+ 0.003334596880011664,
+ 0.005312727802886718,
+ 0.14024668318996938,
+ 0.0013261408368566844,
+ 0.003504884093891238,
+ 0.006375273363464061,
+ 0.04964922000291588,
+ 0.008290421344219274,
+ 0.09536783787724158,
+ 0.05394372357486515,
+ 0.005505175681586237,
+ 0.005339553870826651,
+ 0.01782067356757545,
+ 0.006710016037323225,
+ 0.05105933809593235,
+ 0.002983525295232541,
+ 0.002940370316372649,
+ 0.0004548768041988629,
+ 0.01208456043154979,
+ 0.000915585362297711,
+ 0.20146260387811635,
+ 0.067196967487972,
+ 0.006158332118384605,
+ 0.025438110511736407,
+ 0.07753783350342616,
+ 0.1273876658405015,
+ 0.009337804344656656,
+ 0.07683452398308792,
+ 0.0070412596588423975,
+ 0.08747164309666132,
+ 0.0038827817466102928,
+ 0.018116926665694706,
+ 0.005017641055547455,
+ 0.004567429654468581,
+ 0.028277008310249308,
+ 0.05271555620352821,
+ 0.004394809739029013,
+ 0.0013343052923166642,
+ 0.00411605190260971,
+ 0.059621519171890944,
+ 0.09073859163143316,
+ 0.01446858142586383,
+ 0.006770666277883074,
+ 0.003425572240851436,
+ 0.0004455459979588861,
+ 0.010401516256013998,
+ 0.005825922146085436,
+ 0.10833882490158916,
+ 0.007584779122321038,
+ 0.016903921854497742,
+ 0.02719580113719201,
+ 0.0304814112844438,
+ 0.02206385770520484,
+ 0.013064295086747339,
+ 0.02696369733197259,
+ 0.009581571657676046,
+ 0.026761918647033093,
+ 0.006510570053943724,
+ 0.021941390873305145,
+ 0.07042659279778393,
+ 0.05437410701268406,
+ 0.1425175681586237,
+ 0.027802303542790494,
+ 0.037690625455605774,
+ 0.0019606356611750987,
+ 0.1095623268698061,
+ 0.06157748942994606,
+ 0.044618749088788455,
+ 0.04955124653739612,
+ 0.03608689313310978,
+ 0.018381688292754043,
+ 0.003404577926811489,
+ 0.015036594255722409,
+ 0.009600233270156,
+ 0.10794693103951014,
+ 0.12447528794284882,
+ 0.0031981338387520046,
+ 0.0074716430966613205,
+ 0.003202799241871993,
+ 0.13437643971424407,
+ 0.006655197550663361,
+ 0.0036693395538708266,
+ 0.049338970695436656,
+ 0.09486863974340283,
+ 0.0015990669193760023,
+ 0.0026604461291733486,
+ 0.051775477474850555,
+ 0.0041347135150896636,
+ 0.005450357194926374,
+ 0.12030325120279925,
+ 0.04581309228750547,
+ 0.0004537104534188657,
+ 0.12425601399620935,
+ 0.025981629975215047,
+ 0.023926519900860182,
+ 0.04423385333138941,
+ 0.0017950138504155123,
+ 0.002661612479953346,
+ 0.0006333284735384167,
+ 0.008449045050298877,
+ 0.000653156436798367,
+ 0.04816678816153958,
+ 0.008625164018078437,
+ 0.0039037760606502403,
+ 0.005228750546726928,
+ 0.004531272780288672,
+ 0.0056672984400058316,
+ 0.00359585945473101,
+ 0.0032179618020119548,
+ 0.0038093016474704767,
+ 0.011452398308791368,
+ 0.002519317684793702,
+ 0.00280390727511299,
+ 0.005572824026826068,
+ 0.004554599795888614,
+ 0.004531272780288672,
+ 0.0035841959469310393,
+ 0.004400641492928998,
+ 0.0036670068523108326,
+ 0.004839189386207902,
+ 0.006258638285464354,
+ 0.004897506925207757,
+ 0.840776789619478,
+ 0.004968654322787578,
+ 0.002886718180492783,
+ 0.0019757982213150604,
+ 0.0018568304417553576,
+ 0.001691208630995772,
+ 0.09009243329931477,
+ 0.14030150167662925,
+ 0.013242746756086894,
+ 0.013746610293045632,
+ 0.027342761335471644,
+ 0.16938912377897652,
+ 0.006607377168683481,
+ 0.01661933226417845,
+ 0.008173786266219566,
+ 0.13297448607668758,
+ 0.0034675608689313307,
+ 0.016641492928998396,
+ 0.011722991689750693,
+ 0.021493512173786266,
+ 0.03430820819361423,
+ 0.10099548039072752,
+ 0.00873596734217816,
+ 0.0018323370753754193,
+ 0.020103222044029742,
+ 0.047197550663362,
+ 0.040833940807697915,
+ 0.03361189677795597,
+ 0.010844729552412887,
+ 0.005544831608106138,
+ 0.0007522962530981193,
+ 0.01525120279924187,
+ 0.00815512465373961,
+ 0.2109648636827526,
+ 0.058258055110074355,
+ 0.007181221752442048,
+ 0.043560868931331105,
+ 0.004058900714389853,
+ 0.10618107595859454,
+ 0.0062399766729844,
+ 0.004835690333867911,
+ 0.02679224376731302,
+ 0.08414637702288964,
+ 0.0030698352529523252,
+ 0.03637498177576906,
+ 0.01592885260242018,
+ 0.017413617145356466,
+ 0.008430383437818923,
+ 0.037231083248286924,
+ 0.03290275550371775,
+ 0.007538125091121154,
+ 0.004500947660008748,
+ 0.05932409972299169,
+ 0.16006764834523984,
+ 0.03309636973319726,
+ 0.007766729844000583,
+ 0.005225251494386936,
+ 0.0006321621227584196,
+ 0.012989648636827526,
+ 0.005274238227146815,
+ 0.1254503571949264,
+ 0.12852719055255868,
+ 0.0035433736696311416,
+ 0.005203090829566993,
+ 0.0019314768916751715,
+ 0.20520775623268697,
+ 0.002509986878553725,
+ 0.00343606939787141,
+ 0.027138649948972155,
+ 0.13926578218399185,
+ 0.004565096952908587,
+ 0.005614812654905963,
+ 0.00874413179763814,
+ 0.004109053797929727,
+ 0.008300918501239247,
+ 0.08270943286193323,
+ 0.002912377897652719,
+ 0.0037066627788307337,
+ 0.06909578655780726,
+ 0.03242805073625893,
+ 0.05237614812654906,
+ 0.04723487388832191,
+ 0.0038991106575302524,
+ 0.006299460562764251,
+ 0.00043388249015891526,
+ 0.020029741944889927,
+ 0.005311561452106721,
+ 0.09334072022160665,
+ 0.022940953491762648,
+ 0.024658988190698353,
+ 0.02901297565242747,
+ 0.03531593526753171,
+ 0.0758023035427905,
+ 0.013711619769645722,
+ 0.021597317393206007,
+ 0.009670214316955824,
+ 0.044728386062108175,
+ 0.010596296836273509,
+ 0.03264382563055839,
+ 0.0604822860475288,
+ 0.05489546581134276,
+ 0.11501851581863246,
+ 0.01837585653885406,
+ 0.026237060796034405,
+ 0.0011255285026971862,
+ 0.08704125965884241,
+ 0.10156349322058608,
+ 0.06660562764251349,
+ 0.023434319871701415,
+ 0.010777081207173057,
+ 0.005409534917626476,
+ 0.003123487388832191,
+ 0.0028762210234728096,
+ 0.0089995626184575,
+ 0.07518297127861205,
+ 0.2314868056568013,
+ 0.002226563639014434,
+ 0.003285610147251786,
+ 0.0027455897361131363,
+ 0.2724537104534189,
+ 0.0016655489138358362,
+ 0.0019209797346551977,
+ 0.0022137337804344656,
+ 0.17690392185449774,
+ 0.0014532730718763668,
+ 0.0024994897215337513,
+ 0.015302522233561744,
+ 0.003441901151771395,
+ 0.015303688584341741,
+ 0.09314593964134713,
+ 0.0017833503426155418,
+ 0.0005108616416387229,
+ 0.017828838023035427,
+ 0.010385187345094037,
+ 0.003168975069252078,
+ 0.01902901297565243,
+ 0.005525003644846187,
+ 0.0010088934246974776,
+ 0.0009272488700976819,
+ 0.036282840064149294,
+ 0.0022977110365942554,
+ 0.0766805656801283,
+ 0.22270418428342326,
+ 0.005283569033386791,
+ 0.007155562035282111,
+ 0.01173582154833066,
+ 0.1715620352821111,
+ 0.003925936725470185,
+ 0.004425134859308937,
+ 0.020040239101909902,
+ 0.14243242455168392,
+ 0.0016737133692958156,
+ 0.0066808572678232975,
+ 0.011980755212130047,
+ 0.012638577052048404,
+ 0.07206065024055984,
+ 0.08115701997375711,
+ 0.00710424260096224,
+ 0.0007278028867181805,
+ 0.02347630849978131,
+ 0.04595538708266512,
+ 0.01481965301064295,
+ 0.013925061962385188,
+ 0.0018125091121154687,
+ 0.00529173348884677,
+ 0.0016340574427759146,
+ 0.03072401224668319,
+ 0.0023746901880740633,
+ 0.25174165330223064,
+ 0.06673392622831317,
+ 0.00878378772415804,
+ 0.03956261845750109,
+ 0.010077270739174807,
+ 0.0844787869951888,
+ 0.00985216503863537,
+ 0.004973319725907567,
+ 0.01893220586091267,
+ 0.11200583175389998,
+ 0.0028715556203528212,
+ 0.004095057588569762,
+ 0.01202391019098994,
+ 0.01756757544831608,
+ 0.014825484764542934,
+ 0.05312961073042717,
+ 0.06746872721971132,
+ 0.003845458521650386,
+ 0.0210806239976673,
+ 0.019443067502551394,
+ 0.08017028721387957,
+ 0.01825572240851436,
+ 0.005365213587986587,
+ 0.01959702580551101,
+ 0.026184575010934536,
+ 0.02474879720075813,
+ 0.002171745152354571,
+ 0.25827321767021433,
+ 0.048050153083539875,
+ 0.01043184137629392,
+ 0.03930485493512174,
+ 0.027640180784370902,
+ 0.03294007872867765,
+ 0.006474413179763814,
+ 0.018314039947514214,
+ 0.015119405161102202,
+ 0.014706516984983233,
+ 0.005494678524566263,
+ 0.03309870243475726,
+ 0.043864120134130345,
+ 0.058996355153812505,
+ 0.06265986295378335,
+ 0.04633328473538417,
+ 0.03790756670068523,
+ 0.0004642076104388394,
+ 0.037849249161685375,
+ 0.08369966467415076,
+ 0.04999679253535501,
+ 0.02392768625164018,
+ 0.010998687855372504,
+ 0.009881323808135296,
+ 0.003867619186470331,
+ 0.012434465665548913,
+ 0.007253535500801866,
+ 0.11106225397288234,
+ 0.17624726636535937,
+ 0.008209943140399476,
+ 0.008390727511299025,
+ 0.012682898381688294,
+ 0.1825653885406036,
+ 0.001538416678816154,
+ 0.004590756670068524,
+ 0.008710307625018223,
+ 0.1299513048549351,
+ 0.002677941390873305,
+ 0.012309666132089225,
+ 0.014087184720804781,
+ 0.01199941682461,
+ 0.031246537396121883,
+ 0.07206648199445984,
+ 0.008254264470039366,
+ 0.0007033095203382417,
+ 0.007034261554162415,
+ 0.006599212713223502,
+ 0.013906400349905234,
+ 0.050098265053214755,
+ 0.007133401370462167,
+ 0.017750692520775622,
+ 0.0008257763522379356,
+ 0.03918821985712203,
+ 0.06015454147834961,
+ );
+}
diff --git a/plugins/jetpack/_inc/lib/class.jetpack-photon-image-sizes.php b/plugins/jetpack/_inc/lib/class.jetpack-photon-image-sizes.php
new file mode 100644
index 00000000..8dc22d19
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.jetpack-photon-image-sizes.php
@@ -0,0 +1,182 @@
+<?php
+/**
+ * The Image Sizes library.
+ *
+ * @package jetpack
+ */
+
+jetpack_require_lib( 'class.jetpack-photon-image' );
+
+/**
+ * Class Jetpack_Photon_ImageSizes
+ *
+ * Manages image resizing via Jetpack CDN Service.
+ */
+class Jetpack_Photon_ImageSizes {
+
+ /**
+ * @var array $data Attachment metadata.
+ */
+ public $data;
+
+ /**
+ * @var Image Image to be resized.
+ */
+ public $image;
+
+ /**
+ * @var null|array $sizes Intermediate sizes.
+ */
+ public static $sizes = null;
+
+ /**
+ * Construct new sizes meta
+ *
+ * @param int $attachment_id Attachment ID.
+ * @param array $data Attachment metadata.
+ */
+ public function __construct( $attachment_id, $data ) {
+ $this->data = $data;
+ $this->image = new Jetpack_Photon_Image( $data, get_post_mime_type( $attachment_id ) );
+ $this->generate_sizes();
+ }
+
+ /**
+ * Generate sizes for attachment.
+ *
+ * @return array Array of sizes; empty array as failure fallback.
+ */
+ protected function generate_sizes() {
+
+ // There is no need to generate the sizes a new for every single image.
+ if ( null !== self::$sizes ) {
+ return self::$sizes;
+ }
+
+ /*
+ * The following logic is copied over from wp_generate_attachment_metadata
+ */
+ $_wp_additional_image_sizes = wp_get_additional_image_sizes();
+
+ $sizes = array();
+
+ $intermediate_image_sizes = get_intermediate_image_sizes();
+
+ foreach ( $intermediate_image_sizes as $s ) {
+ $sizes[ $s ] = array(
+ 'width' => '',
+ 'height' => '',
+ 'crop' => false,
+ );
+ if ( isset( $_wp_additional_image_sizes[ $s ]['width'] ) ) {
+ // For theme-added sizes.
+ $sizes[ $s ]['width'] = intval( $_wp_additional_image_sizes[ $s ]['width'] );
+ } else {
+ // For default sizes set in options.
+ $sizes[ $s ]['width'] = get_option( "{$s}_size_w" );
+ }
+
+ if ( isset( $_wp_additional_image_sizes[ $s ]['height'] ) ) {
+ // For theme-added sizes.
+ $sizes[ $s ]['height'] = intval( $_wp_additional_image_sizes[ $s ]['height'] );
+ } else {
+ // For default sizes set in options.
+ $sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
+ }
+
+ if ( isset( $_wp_additional_image_sizes[ $s ]['crop'] ) ) {
+ // For theme-added sizes.
+ $sizes[ $s ]['crop'] = $_wp_additional_image_sizes[ $s ]['crop'];
+ } else {
+ // For default sizes set in options.
+ $sizes[ $s ]['crop'] = get_option( "{$s}_crop" );
+ }
+ }
+
+ self::$sizes = $sizes;
+
+ return $sizes;
+ }
+
+ /**
+ * @return array
+ */
+ public function filtered_sizes() {
+ // Remove filter preventing the creation of advanced sizes.
+ remove_filter(
+ 'intermediate_image_sizes_advanced',
+ array( 'Jetpack_Photon', 'filter_photon_noresize_intermediate_sizes' )
+ );
+
+ /** This filter is documented in wp-admin/includes/image.php */
+ $sizes = apply_filters( 'intermediate_image_sizes_advanced', self::$sizes, $this->data );
+
+ // Re-add the filter removed above.
+ add_filter(
+ 'intermediate_image_sizes_advanced',
+ array( 'Jetpack_Photon', 'filter_photon_noresize_intermediate_sizes' )
+ );
+
+ return (array) $sizes;
+ }
+
+ /**
+ * Standardises and validates the size_data array.
+ *
+ * @param array $size_data Size data array - at least containing height or width key. Can contain crop as well.
+ *
+ * @return array Array with populated width, height and crop keys; empty array if no width and height are provided.
+ */
+ public function standardize_size_data( $size_data ) {
+ $has_at_least_width_or_height = ( isset( $size_data['width'] ) || isset( $size_data['height'] ) );
+ if ( ! $has_at_least_width_or_height ) {
+ return array();
+ }
+
+ $defaults = array(
+ 'width' => null,
+ 'height' => null,
+ 'crop' => false,
+ );
+
+ return array_merge( $defaults, $size_data );
+ }
+
+ /**
+ * Get sizes for attachment post meta.
+ *
+ * @return array ImageSizes for attachment postmeta.
+ */
+ public function generate_sizes_meta() {
+
+ $metadata = array();
+
+ foreach ( $this->filtered_sizes() as $size => $size_data ) {
+
+ $size_data = $this->standardize_size_data( $size_data );
+
+ if ( true === empty( $size_data ) ) {
+ continue;
+ }
+
+ $resized_image = $this->resize( $size_data );
+
+ if ( true === is_array( $resized_image ) ) {
+ $metadata[ $size ] = $resized_image;
+ }
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * @param array $size_data
+ *
+ * @return array|\WP_Error Array for usage in $metadata['sizes']; WP_Error on failure.
+ */
+ protected function resize( $size_data ) {
+
+ return $this->image->get_size( $size_data );
+
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/class.jetpack-photon-image.php b/plugins/jetpack/_inc/lib/class.jetpack-photon-image.php
new file mode 100644
index 00000000..d364f2a3
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.jetpack-photon-image.php
@@ -0,0 +1,243 @@
+<?php
+/**
+ * The Image Class.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Represents a resizable image, exposing properties necessary for properly generating srcset.
+ */
+class Jetpack_Photon_Image {
+
+ /**
+ * @var string $filename Attachment's Filename.
+ */
+ public $filename;
+
+ /**
+ * @var string/WP_Erorr $mime_type Attachment's mime-type, WP_Error on failure when recalculating the dimensions.
+ */
+ private $mime_type;
+
+ /**
+ * @var int $original_width Image original width.
+ */
+ private $original_width;
+
+ /**
+ * @var int $original_width Image original height.
+ */
+ private $original_height;
+
+ /**
+ * @var int $width Current attachment's width.
+ */
+ private $width;
+
+ /**
+ * @var int $height Current attachment's height.
+ */
+ private $height;
+
+ /**
+ * @var bool $is_resized Whether the attachment has been resized yet, or not.
+ */
+ private $is_resized = false;
+
+ /**
+ * Constructs the image object.
+ *
+ * The $data array should provide at least
+ * file : string Image file path
+ * width : int Image width
+ * height : int Image height
+ *
+ * @param array $data Array of attachment metadata, typically value of _wp_attachment_metadata postmeta
+ * @param string|\WP_Error $mime_type Typically value returned from get_post_mime_type function.
+ */
+ public function __construct( $data, $mime_type ) {
+ $this->filename = $data['file'];
+ $this->width = $this->original_width = $data['width'];
+ $this->height = $this->original_height = $data['height'];
+ $this->mime_type = $mime_type;
+ }
+
+ /**
+ * Resizes the image to given size.
+ *
+ * @param array $size_data Array of width, height, and crop properties of a size.
+ *
+ * @return bool|\WP_Error True if resize was successful, WP_Error on failure.
+ */
+ public function resize( $size_data ) {
+
+ $dimensions = $this->image_resize_dimensions( $size_data['width'], $size_data['height'], $size_data['crop'] );
+
+ if ( true === is_wp_error( $dimensions ) ) {
+ return $dimensions; // Returns \WP_Error.
+ }
+
+ if ( true === is_wp_error( $this->mime_type ) ) {
+ return $this->mime_type; // Returns \WP_Error.
+ }
+
+ $this->set_width_height( $dimensions );
+
+ return $this->is_resized = true;
+ }
+
+ /**
+ * Generates size data for usage in $metadata['sizes'];.
+ *
+ * @param array $size_data Array of width, height, and crop properties of a size.
+ *
+ * @return array|\WP_Error An array containing file, width, height, and mime-type keys and it's values. WP_Error on failure.
+ */
+ public function get_size( $size_data ) {
+
+ $is_resized = $this->resize( $size_data );
+
+ if ( true === is_wp_error( $is_resized ) ) {
+ return $is_resized;
+ }
+
+ return array(
+ 'file' => $this->get_filename(),
+ 'width' => $this->get_width(),
+ 'height' => $this->get_height(),
+ 'mime-type' => $this->get_mime_type(),
+ );
+ }
+
+ /**
+ * Resets the image to it's original dimensions.
+ *
+ * @return bool True on successful reset to original dimensions.
+ */
+ public function reset_to_original() {
+ $this->width = $this->original_width;
+ $this->height = $this->original_height;
+ $this->is_resized = false;
+
+ return true;
+ }
+
+ /**
+ * Return the basename filename. If the image has been resized, including
+ * the resizing params for Jetpack CDN.
+ *
+ * @return string Basename of the filename.
+ */
+ public function get_filename() {
+
+ if ( true === $this->is_resized() ) {
+ $filename = $this->get_resized_filename();
+ } else {
+ $filename = $this->filename;
+ }
+
+ return wp_basename( $filename );
+ }
+
+ /**
+ * Returns current image width. Either original, or after resize.
+ *
+ * @return int
+ */
+ public function get_width() {
+ return (int) $this->width;
+ }
+
+ /**
+ * Returns current image height. Either original, or after resize.
+ *
+ * @return int
+ */
+ public function get_height() {
+ return (int) $this->height;
+ }
+
+ /**
+ * Returns image mime type.
+ *
+ * @return string|WP_Error Image's mime type or WP_Error if it was not determined.
+ */
+ public function get_mime_type() {
+ return $this->mime_type;
+ }
+
+ /**
+ * Checks the resize status of the image.
+ *
+ * @return bool If the image has been resized.
+ */
+ public function is_resized() {
+ return ( true === $this->is_resized );
+ }
+
+ /**
+ * Get filename with proper args for the Photon service.
+ *
+ * @return string Filename with query args for Photon service
+ */
+ protected function get_resized_filename() {
+ $query_args = array(
+ 'resize' => join(
+ ',',
+ array(
+ $this->get_width(),
+ $this->get_height(),
+ )
+ ),
+ );
+
+ return add_query_arg( $query_args, $this->filename );
+ }
+
+ /**
+ * Get resize dimensions used for the Jetpack CDN service.
+ *
+ * Converts the list of values returned from `image_resize_dimensions()` to
+ * associative array for the sake of more readable code no relying on index
+ * nor `list`.
+ *
+ * @param int $max_width
+ * @param int $max_height
+ * @param bool|array $crop
+ *
+ * @return array|\WP_Error Array of dimensions matching the parameters to imagecopyresampled. WP_Error on failure.
+ */
+ protected function image_resize_dimensions( $max_width, $max_height, $crop ) {
+ $dimensions = image_resize_dimensions( $this->original_width, $this->original_height, $max_width, $max_height, $crop );
+ if ( ! $dimensions ) {
+ return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ), $this->filename );
+ }
+
+ return array_combine(
+ array(
+ 'dst_x',
+ 'dst_y',
+ 'src_x',
+ 'src_y',
+ 'dst_w',
+ 'dst_h',
+ 'src_w',
+ 'src_h',
+ ),
+ $dimensions
+ );
+ }
+
+ /**
+ * Sets proper width and height from dimensions.
+ *
+ * @param Array $dimensions an array of image dimensions.
+ * @return void
+ */
+ protected function set_width_height( $dimensions ) {
+ $this->width = (int) $dimensions['dst_w'];
+ $this->height = (int) $dimensions['dst_h'];
+ }
+
+}
diff --git a/plugins/jetpack/_inc/lib/class.jetpack-search-performance-logger.php b/plugins/jetpack/_inc/lib/class.jetpack-search-performance-logger.php
new file mode 100644
index 00000000..f8de70ee
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.jetpack-search-performance-logger.php
@@ -0,0 +1,83 @@
+<?php
+
+class Jetpack_Search_Performance_Logger {
+ /**
+ * @var Jetpack_Search_Performance_Logger
+ **/
+ private static $instance = null;
+
+ private $current_query = null;
+ private $query_started = null;
+ private $stats = null;
+
+ static function init() {
+ if ( is_null( self::$instance ) ) {
+ self::$instance = new Jetpack_Search_Performance_Logger;
+ }
+
+ return self::$instance;
+ }
+
+ private function __construct() {
+ $this->stats = array();
+ add_action( 'pre_get_posts', array( $this, 'begin_log_query' ), 10, 1 );
+ add_action( 'did_jetpack_search_query', array( $this, 'log_jetpack_search_query' ) );
+ add_filter( 'found_posts', array( $this, 'log_mysql_query' ), 10, 2 );
+ add_action( 'wp_footer', array( $this, 'print_stats' ) );
+ }
+
+ public function begin_log_query( $query ) {
+ if ( $this->should_log_query( $query ) ) {
+ $this->query_started = microtime( true );
+ $this->current_query = $query;
+ }
+ }
+
+ public function log_mysql_query( $found_posts, $query ) {
+ if ( $this->current_query === $query ) {
+ $duration = microtime( true ) - $this->query_started;
+ if ( $duration < 60 ) { // eliminate outliers, likely tracking errors
+ $this->record_query_time( $duration, false );
+ }
+ $this->reset_query_state();
+ }
+
+ return $found_posts;
+ }
+
+ public function log_jetpack_search_query() {
+ $duration = microtime( true ) - $this->query_started;
+ if ( $duration < 60 ) { // eliminate outliers, likely tracking errors
+ $this->record_query_time( $duration, true );
+ }
+ $this->reset_query_state();
+ }
+
+ private function reset_query_state() {
+ $this->query_started = null;
+ $this->current_query = null;
+ }
+
+ private function should_log_query( $query ) {
+ return $query->is_main_query() && $query->is_search();
+ }
+
+ private function record_query_time( $duration, $was_jetpack_search ) {
+ $this->stats[] = array( $was_jetpack_search, intval( $duration * 1000 ) );
+ }
+
+ public function print_stats() {
+ $beacons = array();
+ if ( ! empty( $this->stats ) ) {
+ foreach( $this->stats as $stat ) {
+ $search_type = $stat[0] ? 'es' : 'mysql';
+ $beacons[] = "%22jetpack.search.{$search_type}.duration:{$stat[1]}|ms%22";
+ }
+
+ $encoded_json = '{%22beacons%22:[' . implode(',', $beacons ) . ']}';
+ $encoded_site_url = urlencode( site_url() );
+ $url = "https://pixel.wp.com/boom.gif?v=0.9&u={$encoded_site_url}&json={$encoded_json}";
+ echo '<img src="' . $url . '" width="1" height="1" style="display:none;" alt=":)"/>';
+ }
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/class.media-extractor.php b/plugins/jetpack/_inc/lib/class.media-extractor.php
new file mode 100644
index 00000000..6acf34db
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.media-extractor.php
@@ -0,0 +1,436 @@
+<?php
+/**
+ * Class with methods to extract metadata from a post/page about videos, images, links, mentions embedded
+ * in or attached to the post/page.
+ *
+ * @todo Additionally, have some filters on number of items in each field
+ */
+class Jetpack_Media_Meta_Extractor {
+
+ // Some consts for what to extract
+ const ALL = 255;
+ const LINKS = 1;
+ const MENTIONS = 2;
+ const IMAGES = 4;
+ const SHORTCODES = 8; // Only the keeper shortcodes below
+ const EMBEDS = 16;
+ const HASHTAGS = 32;
+
+ // For these, we try to extract some data from the shortcode, rather than just recording its presence (which we do for all)
+ // There should be a function get_{shortcode}_id( $atts ) or static method SomethingShortcode::get_{shortcode}_id( $atts ) for these.
+ private static $KEEPER_SHORTCODES = array(
+ 'youtube',
+ 'vimeo',
+ 'hulu',
+ 'ted',
+ 'wpvideo',
+ 'videopress',
+ );
+
+ /**
+ * Gets the specified media and meta info from the given post.
+ * NOTE: If you have the post's HTML content already and don't need image data, use extract_from_content() instead.
+ *
+ * @param $blog_id The ID of the blog
+ * @param $post_id The ID of the post
+ * @param $what_to_extract (int) A mask of things to extract, e.g. Jetpack_Media_Meta_Extractor::IMAGES | Jetpack_Media_Meta_Extractor::MENTIONS
+ * @returns a structure containing metadata about the embedded things, or empty array if nothing found, or WP_Error on error
+ */
+ static public function extract( $blog_id, $post_id, $what_to_extract = self::ALL ) {
+
+ // multisite?
+ if ( function_exists( 'switch_to_blog') )
+ switch_to_blog( $blog_id );
+
+ $post = get_post( $post_id );
+ $content = $post->post_title . "\n\n" . $post->post_content;
+ $char_cnt = strlen( $content );
+
+ //prevent running extraction on really huge amounts of content
+ if ( $char_cnt > 100000 ) //about 20k English words
+ $content = substr( $content, 0, 100000 );
+
+ $extracted = array();
+
+ // Get images first, we need the full post for that
+ if ( self::IMAGES & $what_to_extract ) {
+ $extracted = self::get_image_fields( $post );
+
+ // Turn off images so we can safely call extract_from_content() below
+ $what_to_extract = $what_to_extract - self::IMAGES;
+ }
+
+ if ( function_exists( 'switch_to_blog') )
+ restore_current_blog();
+
+ // All of the other things besides images can be extracted from just the content
+ $extracted = self::extract_from_content( $content, $what_to_extract, $extracted );
+
+ return $extracted;
+ }
+
+ /**
+ * Gets the specified meta info from the given post content.
+ * NOTE: If you want IMAGES, call extract( $blog_id, $post_id, ...) which will give you more/better image extraction
+ * This method will give you an error if you ask for IMAGES.
+ *
+ * @param $content The HTML post_content of a post
+ * @param $what_to_extract (int) A mask of things to extract, e.g. Jetpack_Media_Meta_Extractor::IMAGES | Jetpack_Media_Meta_Extractor::MENTIONS
+ * @param $already_extracted (array) Previously extracted things, e.g. images from extract(), which can be used for x-referencing here
+ * @returns a structure containing metadata about the embedded things, or empty array if nothing found, or WP_Error on error
+ */
+ static public function extract_from_content( $content, $what_to_extract = self::ALL, $already_extracted = array() ) {
+ $stripped_content = self::get_stripped_content( $content );
+
+ // Maybe start with some previously extracted things (e.g. images from extract()
+ $extracted = $already_extracted;
+
+ // Embedded media objects will have already been converted to shortcodes by pre_kses hooks on save.
+
+ if ( self::IMAGES & $what_to_extract ) {
+ $images = Jetpack_Media_Meta_Extractor::extract_images_from_content( $stripped_content, array() );
+ $extracted = array_merge( $extracted, $images );
+ }
+
+ // ----------------------------------- MENTIONS ------------------------------
+
+ if ( self::MENTIONS & $what_to_extract ) {
+ if ( preg_match_all( '/(^|\s)@(\w+)/u', $stripped_content, $matches ) ) {
+ $mentions = array_values( array_unique( $matches[2] ) ); //array_unique() retains the keys!
+ $mentions = array_map( 'strtolower', $mentions );
+ $extracted['mention'] = array( 'name' => $mentions );
+ if ( !isset( $extracted['has'] ) )
+ $extracted['has'] = array();
+ $extracted['has']['mention'] = count( $mentions );
+ }
+ }
+
+ // ----------------------------------- HASHTAGS ------------------------------
+ /** Some hosts may not compile with --enable-unicode-properties and kick a warning:
+ * Warning: preg_match_all() [function.preg-match-all]: Compilation failed: support for \P, \p, and \X has not been compiled
+ * Therefore, we only run this code block on wpcom, not in Jetpack.
+ */
+ if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) && ( self::HASHTAGS & $what_to_extract ) ) {
+ //This regex does not exactly match Twitter's
+ // if there are problems/complaints we should implement this:
+ // https://github.com/twitter/twitter-text/blob/master/java/src/com/twitter/Regex.java
+ if ( preg_match_all( '/(?:^|\s)#(\w*\p{L}+\w*)/u', $stripped_content, $matches ) ) {
+ $hashtags = array_values( array_unique( $matches[1] ) ); //array_unique() retains the keys!
+ $hashtags = array_map( 'strtolower', $hashtags );
+ $extracted['hashtag'] = array( 'name' => $hashtags );
+ if ( !isset( $extracted['has'] ) )
+ $extracted['has'] = array();
+ $extracted['has']['hashtag'] = count( $hashtags );
+ }
+ }
+
+ // ----------------------------------- SHORTCODES ------------------------------
+
+ // Always look for shortcodes.
+ // If we don't want them, we'll just remove them, so we don't grab them as links below
+ $shortcode_pattern = '/' . get_shortcode_regex() . '/s';
+ if ( preg_match_all( $shortcode_pattern, $content, $matches ) ) {
+
+ $shortcode_total_count = 0;
+ $shortcode_type_counts = array();
+ $shortcode_types = array();
+ $shortcode_details = array();
+
+ if ( self::SHORTCODES & $what_to_extract ) {
+
+ foreach( $matches[2] as $key => $shortcode ) {
+ //Elasticsearch (and probably other things) doesn't deal well with some chars as key names
+ $shortcode_name = preg_replace( '/[.,*"\'\/\\\\#+ ]/', '_', $shortcode );
+
+ $attr = shortcode_parse_atts( $matches[3][ $key ] );
+
+ $shortcode_total_count++;
+ if ( ! isset( $shortcode_type_counts[$shortcode_name] ) )
+ $shortcode_type_counts[$shortcode_name] = 0;
+ $shortcode_type_counts[$shortcode_name]++;
+
+ // Store (uniquely) presence of all shortcode regardless of whether it's a keeper (for those, get ID below)
+ // @todo Store number of occurrences?
+ if ( ! in_array( $shortcode_name, $shortcode_types ) )
+ $shortcode_types[] = $shortcode_name;
+
+ // For keeper shortcodes, also store the id/url of the object (e.g. youtube video, TED talk, etc.)
+ if ( in_array( $shortcode, self::$KEEPER_SHORTCODES ) ) {
+ unset( $id ); // Clear shortcode ID data left from the last shortcode
+ // We'll try to get the salient ID from the function jetpack_shortcode_get_xyz_id()
+ // If the shortcode is a class, we'll call XyzShortcode::get_xyz_id()
+ $shortcode_get_id_func = "jetpack_shortcode_get_{$shortcode}_id";
+ $shortcode_class_name = ucfirst( $shortcode ) . 'Shortcode';
+ $shortcode_get_id_method = "get_{$shortcode}_id";
+ if ( function_exists( $shortcode_get_id_func ) ) {
+ $id = call_user_func( $shortcode_get_id_func, $attr );
+ } else if ( method_exists( $shortcode_class_name, $shortcode_get_id_method ) ) {
+ $id = call_user_func( array( $shortcode_class_name, $shortcode_get_id_method ), $attr );
+ }
+ if ( ! empty( $id )
+ && ( ! isset( $shortcode_details[$shortcode_name] ) || ! in_array( $id, $shortcode_details[$shortcode_name] ) ) )
+ $shortcode_details[$shortcode_name][] = $id;
+ }
+ }
+
+ if ( $shortcode_total_count > 0 ) {
+ // Add the shortcode info to the $extracted array
+ if ( !isset( $extracted['has'] ) )
+ $extracted['has'] = array();
+ $extracted['has']['shortcode'] = $shortcode_total_count;
+ $extracted['shortcode'] = array();
+ foreach ( $shortcode_type_counts as $type => $count )
+ $extracted['shortcode'][$type] = array( 'count' => $count );
+ if ( ! empty( $shortcode_types ) )
+ $extracted['shortcode_types'] = $shortcode_types;
+ foreach ( $shortcode_details as $type => $id )
+ $extracted['shortcode'][$type]['id'] = $id;
+ }
+ }
+
+ // Remove the shortcodes form our copy of $content, so we don't count links in them as links below.
+ $content = preg_replace( $shortcode_pattern, ' ', $content );
+ }
+
+ // ----------------------------------- LINKS ------------------------------
+
+ if ( self::LINKS & $what_to_extract ) {
+
+ // To hold the extracted stuff we find
+ $links = array();
+
+ // @todo Get the text inside the links?
+
+ // Grab any links, whether in <a href="..." or not, but subtract those from shortcodes and images
+ // (we treat embed links as just another link)
+ if ( preg_match_all( '#(?:^|\s|"|\')(https?://([^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))))#', $content, $matches ) ) {
+
+ foreach ( $matches[1] as $link_raw ) {
+ $url = parse_url( $link_raw );
+
+ // Data URI links
+ if ( isset( $url['scheme'] ) && 'data' === $url['scheme'] )
+ continue;
+
+ // Remove large (and likely invalid) links
+ if ( 4096 < strlen( $link_raw ) )
+ continue;
+
+ // Build a simple form of the URL so we can compare it to ones we found in IMAGES or SHORTCODES and exclude those
+ $simple_url = $url['scheme'] . '://' . $url['host'] . ( ! empty( $url['path'] ) ? $url['path'] : '' );
+ if ( isset( $extracted['image']['url'] ) ) {
+ if ( in_array( $simple_url, (array) $extracted['image']['url'] ) )
+ continue;
+ }
+
+ list( $proto, $link_all_but_proto ) = explode( '://', $link_raw );
+
+ // Build a reversed hostname
+ $host_parts = array_reverse( explode( '.', $url['host'] ) );
+ $host_reversed = '';
+ foreach ( $host_parts as $part ) {
+ $host_reversed .= ( ! empty( $host_reversed ) ? '.' : '' ) . $part;
+ }
+
+ $link_analyzed = '';
+ if ( !empty( $url['path'] ) ) {
+ // The whole path (no query args or fragments)
+ $path = substr( $url['path'], 1 ); // strip the leading '/'
+ $link_analyzed .= ( ! empty( $link_analyzed ) ? ' ' : '' ) . $path;
+
+ // The path split by /
+ $path_split = explode( '/', $path );
+ if ( count( $path_split ) > 1 ) {
+ $link_analyzed .= ' ' . implode( ' ', $path_split );
+ }
+
+ // The fragment
+ if ( ! empty( $url['fragment'] ) )
+ $link_analyzed .= ( ! empty( $link_analyzed ) ? ' ' : '' ) . $url['fragment'];
+ }
+
+ // @todo Check unique before adding
+ $links[] = array(
+ 'url' => $link_all_but_proto,
+ 'host_reversed' => $host_reversed,
+ 'host' => $url['host'],
+ );
+ }
+
+ }
+
+ $link_count = count( $links );
+ if ( $link_count ) {
+ $extracted[ 'link' ] = $links;
+ if ( !isset( $extracted['has'] ) )
+ $extracted['has'] = array();
+ $extracted['has']['link'] = $link_count;
+ }
+ }
+
+ // ----------------------------------- EMBEDS ------------------------------
+
+ //Embeds are just individual links on their own line
+ if ( self::EMBEDS & $what_to_extract ) {
+
+ if ( !function_exists( '_wp_oembed_get_object' ) )
+ include( ABSPATH . WPINC . '/class-oembed.php' );
+
+ // get an oembed object
+ $oembed = _wp_oembed_get_object();
+
+ // Grab any links on their own lines that may be embeds
+ if ( preg_match_all( '|^\s*(https?://[^\s"]+)\s*$|im', $content, $matches ) ) {
+
+ // To hold the extracted stuff we find
+ $embeds = array();
+
+ foreach ( $matches[1] as $link_raw ) {
+ $url = parse_url( $link_raw );
+
+ list( $proto, $link_all_but_proto ) = explode( '://', $link_raw );
+
+ // Check whether this "link" is really an embed.
+ foreach ( $oembed->providers as $matchmask => $data ) {
+ list( $providerurl, $regex ) = $data;
+
+ // Turn the asterisk-type provider URLs into regex
+ if ( !$regex ) {
+ $matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i';
+ $matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask );
+ }
+
+ if ( preg_match( $matchmask, $link_raw ) ) {
+ $provider = str_replace( '{format}', 'json', $providerurl ); // JSON is easier to deal with than XML
+ $embeds[] = $link_all_but_proto; // @todo Check unique before adding
+
+ // @todo Try to get ID's for the ones we care about (shortcode_keepers)
+ break;
+ }
+ }
+ }
+
+ if ( ! empty( $embeds ) ) {
+ if ( !isset( $extracted['has'] ) )
+ $extracted['has'] = array();
+ $extracted['has']['embed'] = count( $embeds );
+ $extracted['embed'] = array( 'url' => array() );
+ foreach ( $embeds as $e )
+ $extracted['embed']['url'][] = $e;
+ }
+ }
+ }
+
+ return $extracted;
+ }
+
+ /**
+ * @param $post A post object
+ * @param $args (array) Optional args, see defaults list for details
+ * @returns array Returns an array of all images meeting the specified criteria in $args
+ *
+ * Uses Jetpack Post Images
+ */
+ private static function get_image_fields( $post, $args = array() ) {
+
+ $defaults = array(
+ 'width' => 200, // Required minimum width (if possible to determine)
+ 'height' => 200, // Required minimum height (if possible to determine)
+ );
+
+ $args = wp_parse_args( $args, $defaults );
+
+ $image_list = array();
+ $image_booleans = array();
+ $image_booleans['gallery'] = 0;
+
+ $from_featured_image = Jetpack_PostImages::from_thumbnail( $post->ID, $args['width'], $args['height'] );
+ if ( !empty( $from_featured_image ) ) {
+ $srcs = wp_list_pluck( $from_featured_image, 'src' );
+ $image_list = array_merge( $image_list, $srcs );
+ }
+
+ $from_slideshow = Jetpack_PostImages::from_slideshow( $post->ID, $args['width'], $args['height'] );
+ if ( !empty( $from_slideshow ) ) {
+ $srcs = wp_list_pluck( $from_slideshow, 'src' );
+ $image_list = array_merge( $image_list, $srcs );
+ }
+
+ $from_gallery = Jetpack_PostImages::from_gallery( $post->ID );
+ if ( !empty( $from_gallery ) ) {
+ $srcs = wp_list_pluck( $from_gallery, 'src' );
+ $image_list = array_merge( $image_list, $srcs );
+ $image_booleans['gallery']++; // @todo This count isn't correct, will only every count 1
+ }
+
+ // @todo Can we check width/height of these efficiently? Could maybe use query args at least, before we strip them out
+ $image_list = Jetpack_Media_Meta_Extractor::get_images_from_html( $post->post_content, $image_list );
+
+ return Jetpack_Media_Meta_Extractor::build_image_struct( $image_list, $image_booleans );
+ }
+
+ public static function extract_images_from_content( $content, $image_list ) {
+ $image_list = Jetpack_Media_Meta_Extractor::get_images_from_html( $content, $image_list );
+ return Jetpack_Media_Meta_Extractor::build_image_struct( $image_list, array() );
+ }
+
+ public static function build_image_struct( $image_list, $image_booleans ) {
+ if ( ! empty( $image_list ) ) {
+ $retval = array( 'image' => array() );
+ $image_list = array_unique( $image_list );
+ foreach ( $image_list as $img ) {
+ $retval['image'][] = array( 'url' => $img );
+ }
+ $image_booleans['image'] = count( $retval['image'] );
+ if ( ! empty( $image_booleans ) )
+ $retval['has'] = $image_booleans;
+ return $retval;
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ *
+ * @param string $html Some markup, possibly containing image tags
+ * @param array $images_already_extracted (just an array of image URLs without query strings, no special structure), used for de-duplication
+ * @return array Image URLs extracted from the HTML, stripped of query params and de-duped
+ */
+ public static function get_images_from_html( $html, $images_already_extracted ) {
+ $image_list = $images_already_extracted;
+ $from_html = Jetpack_PostImages::from_html( $html );
+ if ( !empty( $from_html ) ) {
+ $srcs = wp_list_pluck( $from_html, 'src' );
+ foreach( $srcs as $image_url ) {
+ if ( ( $src = parse_url( $image_url ) ) && isset( $src['scheme'], $src['host'], $src['path'] ) ) {
+ // Rebuild the URL without the query string
+ $queryless = $src['scheme'] . '://' . $src['host'] . $src['path'];
+ } elseif ( $length = strpos( $image_url, '?' ) ) {
+ // If parse_url() didn't work, strip off the query string the old fashioned way
+ $queryless = substr( $image_url, 0, $length );
+ } else {
+ // Failing that, there was no spoon! Err ... query string!
+ $queryless = $image_url;
+ }
+
+ // Discard URLs that are longer then 4KB, these are likely data URIs or malformed HTML.
+ if ( 4096 < strlen( $queryless ) ) {
+ continue;
+ }
+
+ if ( ! in_array( $queryless, $image_list ) ) {
+ $image_list[] = $queryless;
+ }
+ }
+ }
+ return $image_list;
+ }
+
+ private static function get_stripped_content( $content ) {
+ $clean_content = strip_tags( $content );
+ $clean_content = html_entity_decode( $clean_content );
+ //completely strip shortcodes and any content they enclose
+ $clean_content = strip_shortcodes( $clean_content );
+ return $clean_content;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/class.media-summary.php b/plugins/jetpack/_inc/lib/class.media-summary.php
new file mode 100644
index 00000000..732706d4
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.media-summary.php
@@ -0,0 +1,369 @@
+<?php
+/**
+ * Class Jetpack_Media_Summary
+ *
+ * embed [video] > gallery > image > text
+ */
+class Jetpack_Media_Summary {
+
+ private static $cache = array();
+
+ static function get( $post_id, $blog_id = 0, $args = array() ) {
+
+ $defaults = array(
+ 'max_words' => 16,
+ 'max_chars' => 256,
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ $switched = false;
+ if ( !empty( $blog_id ) && $blog_id != get_current_blog_id() && function_exists( 'switch_to_blog' ) ) {
+ switch_to_blog( $blog_id );
+ $switched = true;
+ } else {
+ $blog_id = get_current_blog_id();
+ }
+
+ $cache_key = "{$blog_id}_{$post_id}_{$args['max_words']}_{$args['max_chars']}";
+ if ( isset( self::$cache[ $cache_key ] ) ) {
+ return self::$cache[ $cache_key ];
+ }
+
+ if ( ! class_exists( 'Jetpack_Media_Meta_Extractor' ) ) {
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ jetpack_require_lib( 'class.wpcom-media-meta-extractor' );
+ } else {
+ jetpack_require_lib( 'class.media-extractor' );
+ }
+ }
+
+ $post = get_post( $post_id );
+ $permalink = get_permalink( $post_id );
+
+ $return = array(
+ 'type' => 'standard',
+ 'permalink' => $permalink,
+ 'image' => '',
+ 'excerpt' => '',
+ 'word_count' => 0,
+ 'secure' => array(
+ 'image' => '',
+ ),
+ 'count' => array(
+ 'image' => 0,
+ 'video' => 0,
+ 'word' => 0,
+ 'link' => 0,
+ ),
+ );
+
+ if ( empty( $post->post_password ) ) {
+ $return['excerpt'] = self::get_excerpt( $post->post_content, $post->post_excerpt, $args['max_words'], $args['max_chars'] , $post);
+ $return['count']['word'] = self::get_word_count( $post->post_content );
+ $return['count']['word_remaining'] = self::get_word_remaining_count( $post->post_content, $return['excerpt'] );
+ $return['count']['link'] = self::get_link_count( $post->post_content );
+ }
+
+ $extract = Jetpack_Media_Meta_Extractor::extract( $blog_id, $post_id, Jetpack_Media_Meta_Extractor::ALL );
+
+ if ( empty( $extract['has'] ) )
+ return $return;
+
+ // Prioritize [some] video embeds
+ if ( !empty( $extract['has']['shortcode'] ) ) {
+ foreach ( $extract['shortcode'] as $type => $data ) {
+ switch ( $type ) {
+ case 'videopress':
+ case 'wpvideo':
+ if ( 0 == $return['count']['video'] ) {
+ // If there is no id on the video, then let's just skip this
+ if ( ! isset ( $data['id'][0] ) ) {
+ break;
+ }
+
+ $guid = $data['id'][0];
+ $video_info = videopress_get_video_details( $guid );
+
+ // Only add the video tags if the guid returns a valid videopress object.
+ if ( $video_info instanceof stdClass ) {
+ // Continue early if we can't find a Video slug.
+ if ( empty( $video_info->files->std->mp4 ) ) {
+ break;
+ }
+
+ $url = sprintf(
+ 'https://videos.files.wordpress.com/%1$s/%2$s',
+ $guid,
+ $video_info->files->std->mp4
+ );
+
+ $thumbnail = $video_info->poster;
+ if ( ! empty( $thumbnail ) ) {
+ $return['image'] = $thumbnail;
+ $return['secure']['image'] = $thumbnail;
+ }
+
+ $return['type'] = 'video';
+ $return['video'] = esc_url_raw( $url );
+ $return['video_type'] = 'video/mp4';
+ $return['secure']['video'] = $return['video'];
+ }
+
+ }
+ $return['count']['video']++;
+ break;
+ case 'youtube':
+ if ( 0 == $return['count']['video'] ) {
+ $return['type'] = 'video';
+ $return['video'] = esc_url_raw( 'http://www.youtube.com/watch?feature=player_embedded&v=' . $extract['shortcode']['youtube']['id'][0] );
+ $return['image'] = self::get_video_poster( 'youtube', $extract['shortcode']['youtube']['id'][0] );
+ $return['secure']['video'] = self::https( $return['video'] );
+ $return['secure']['image'] = self::https( $return['image'] );
+ }
+ $return['count']['video']++;
+ break;
+ case 'vimeo':
+ if ( 0 == $return['count']['video'] ) {
+ $return['type'] = 'video';
+ $return['video'] = esc_url_raw( 'http://vimeo.com/' . $extract['shortcode']['vimeo']['id'][0] );
+ $return['secure']['video'] = self::https( $return['video'] );
+
+ $poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
+ if ( !empty( $poster_image ) ) {
+ $return['image'] = $poster_image;
+ $poster_url_parts = parse_url( $poster_image );
+ $return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
+ }
+ }
+ $return['count']['video']++;
+ break;
+ }
+ }
+
+ }
+
+ if ( !empty( $extract['has']['embed'] ) ) {
+ foreach( $extract['embed']['url'] as $embed ) {
+ if ( preg_match( '/((youtube|vimeo|dailymotion)\.com|youtu.be)/', $embed ) ) {
+ if ( 0 == $return['count']['video'] ) {
+ $return['type'] = 'video';
+ $return['video'] = 'http://' . $embed;
+ $return['secure']['video'] = self::https( $return['video'] );
+ if ( false !== strpos( $embed, 'youtube' ) ) {
+ $return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
+ $return['secure']['image'] = self::https( $return['image'] );
+ } else if ( false !== strpos( $embed, 'youtu.be' ) ) {
+ $youtube_id = jetpack_get_youtube_id( $return['video'] );
+ $return['video'] = 'http://youtube.com/watch?v=' . $youtube_id . '&feature=youtu.be';
+ $return['secure']['video'] = self::https( $return['video'] );
+ $return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
+ $return['secure']['image'] = self::https( $return['image'] );
+ } else if ( false !== strpos( $embed, 'vimeo' ) ) {
+ $poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
+ if ( !empty( $poster_image ) ) {
+ $return['image'] = $poster_image;
+ $poster_url_parts = parse_url( $poster_image );
+ $return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
+ }
+ } else if ( false !== strpos( $embed, 'dailymotion' ) ) {
+ $return['image'] = str_replace( 'dailymotion.com/video/','dailymotion.com/thumbnail/video/', $embed );
+ $return['image'] = parse_url( $return['image'], PHP_URL_SCHEME ) === null ? 'http://' . $return['image'] : $return['image'];
+ $return['secure']['image'] = self::https( $return['image'] );
+ }
+
+ }
+ $return['count']['video']++;
+ }
+ }
+ }
+
+ // Do we really want to make the video the primary focus of the post?
+ if ( 'video' == $return['type'] ) {
+ $content = wpautop( strip_tags( $post->post_content ) );
+ $paragraphs = explode( '</p>', $content );
+ $number_of_paragraphs = 0;
+
+ foreach ( $paragraphs as $i => $paragraph ) {
+ // Don't include blank lines as a paragraph
+ if ( '' == trim( $paragraph ) ) {
+ unset( $paragraphs[$i] );
+ continue;
+ }
+ $number_of_paragraphs++;
+ }
+
+ $number_of_paragraphs = $number_of_paragraphs - $return['count']['video']; // subtract amount for videos..
+
+ // More than 2 paragraph? The video is not the primary focus so we can do some more analysis
+ if ( $number_of_paragraphs > 2 )
+ $return['type'] = 'standard';
+ }
+
+ // If we don't have any prioritized embed...
+ if ( 'standard' == $return['type'] ) {
+ if ( ( ! empty( $extract['has']['gallery'] ) || ! empty( $extract['shortcode']['gallery']['count'] ) ) && ! empty( $extract['image'] ) ) {
+ //... Then we prioritize galleries first (multiple images returned)
+ $return['type'] = 'gallery';
+ $return['images'] = $extract['image'];
+ foreach ( $return['images'] as $image ) {
+ $return['secure']['images'][] = array( 'url' => self::ssl_img( $image['url'] ) );
+ $return['count']['image']++;
+ }
+ } else if ( ! empty( $extract['has']['image'] ) ) {
+ // ... Or we try and select a single image that would make sense
+ $content = wpautop( strip_tags( $post->post_content ) );
+ $paragraphs = explode( '</p>', $content );
+ $number_of_paragraphs = 0;
+
+ foreach ( $paragraphs as $i => $paragraph ) {
+ // Don't include 'actual' captions as a paragraph
+ if ( false !== strpos( $paragraph, '[caption' ) ) {
+ unset( $paragraphs[$i] );
+ continue;
+ }
+ // Don't include blank lines as a paragraph
+ if ( '' == trim( $paragraph ) ) {
+ unset( $paragraphs[$i] );
+ continue;
+ }
+ $number_of_paragraphs++;
+ }
+
+ $return['image'] = $extract['image'][0]['url'];
+ $return['secure']['image'] = self::ssl_img( $return['image'] );
+ $return['count']['image']++;
+
+ if ( $number_of_paragraphs <= 2 && 1 == count( $extract['image'] ) ) {
+ // If we have lots of text or images, let's not treat it as an image post, but return its first image
+ $return['type'] = 'image';
+ }
+ }
+ }
+
+ if ( $switched ) {
+ restore_current_blog();
+ }
+
+ /**
+ * Allow a theme or plugin to inspect and ultimately change the media summary.
+ *
+ * @since 4.4.0
+ *
+ * @param array $data The calculated media summary data.
+ * @param int $post_id The id of the post this data applies to.
+ */
+ $return = apply_filters( 'jetpack_media_summary_output', $return, $post_id );
+
+ self::$cache[ $cache_key ] = $return;
+
+ return $return;
+ }
+
+ static function https( $str ) {
+ return str_replace( 'http://', 'https://', $str );
+ }
+
+ static function ssl_img( $url ) {
+ if ( false !== strpos( $url, 'files.wordpress.com' ) ) {
+ return self::https( $url );
+ } else {
+ return self::https( jetpack_photon_url( $url ) );
+ }
+ }
+
+ static function get_video_poster( $type, $id ) {
+ if ( 'videopress' == $type ) {
+ if ( function_exists( 'video_get_highest_resolution_image_url' ) ) {
+ return video_get_highest_resolution_image_url( $id );
+ } else if ( class_exists( 'VideoPress_Video' ) ) {
+ $video = new VideoPress_Video( $id );
+ return $video->poster_frame_uri;
+ }
+ } else if ( 'youtube' == $type ) {
+ return 'http://img.youtube.com/vi/'.$id.'/0.jpg';
+ }
+ }
+
+ static function clean_text( $text ) {
+ return trim(
+ preg_replace(
+ '/[\s]+/',
+ ' ',
+ preg_replace(
+ '@https?://[\S]+@',
+ '',
+ strip_shortcodes(
+ strip_tags(
+ $text
+ )
+ )
+ )
+ )
+ );
+ }
+
+ /**
+ * Retrieve an excerpt for the post summary.
+ *
+ * This function works around a suspected problem with Core. If resolved, this function should be simplified.
+ * @link https://github.com/Automattic/jetpack/pull/8510
+ * @link https://core.trac.wordpress.org/ticket/42814
+ *
+ * @param string $post_content The post's content.
+ * @param string $post_excerpt The post's excerpt. Empty if none was explicitly set.
+ * @param int $max_words Maximum number of words for the excerpt. Used on wp.com. Default 16.
+ * @param int $max_chars Maximum characters in the excerpt. Used on wp.com. Default 256.
+ * @param WP_Post $requested_post The post object.
+ * @return string Post excerpt.
+ **/
+ static function get_excerpt( $post_content, $post_excerpt, $max_words = 16, $max_chars = 256, $requested_post = null ) {
+ global $post;
+ $original_post = $post; // Saving the global for later use.
+ if ( function_exists( 'wpcom_enhanced_excerpt_extract_excerpt' ) ) {
+ return self::clean_text( wpcom_enhanced_excerpt_extract_excerpt( array(
+ 'text' => $post_content,
+ 'excerpt_only' => true,
+ 'show_read_more' => false,
+ 'max_words' => $max_words,
+ 'max_chars' => $max_chars,
+ 'read_more_threshold' => 25,
+ ) ) );
+ } elseif ( $requested_post instanceof WP_Post ) {
+ $post = $requested_post; // setup_postdata does not set the global.
+ setup_postdata( $post );
+ /** This filter is documented in core/src/wp-includes/post-template.php */
+ $post_excerpt = apply_filters( 'get_the_excerpt', $post_excerpt, $post );
+ $post = $original_post; // wp_reset_postdata uses the $post global.
+ wp_reset_postdata();
+ return self::clean_text( $post_excerpt );
+ }
+ return '';
+ }
+
+ /**
+ * Split a string into an array of words.
+ *
+ * @param string $text Post content or excerpt.
+ */
+ static function split_content_in_words( $text ) {
+ $words = preg_split( '/[\s!?;,.]+/', $text, null, PREG_SPLIT_NO_EMPTY );
+
+ // Return an empty array if the split above fails.
+ return $words ? $words : array();
+ }
+
+ static function get_word_count( $post_content ) {
+ return (int) count( self::split_content_in_words( self::clean_text( $post_content ) ) );
+ }
+
+ static function get_word_remaining_count( $post_content, $excerpt_content ) {
+ $content_word_count = count( self::split_content_in_words( self::clean_text( $post_content ) ) );
+ $excerpt_word_count = count( self::split_content_in_words( self::clean_text( $excerpt_content ) ) );
+
+ return (int) $content_word_count - $excerpt_word_count;
+ }
+
+ static function get_link_count( $post_content ) {
+ return preg_match_all( '/\<a[\> ]/', $post_content, $matches );
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/class.media.php b/plugins/jetpack/_inc/lib/class.media.php
new file mode 100644
index 00000000..e48c4aad
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/class.media.php
@@ -0,0 +1,505 @@
+<?php
+
+require_once( JETPACK__PLUGIN_DIR . 'sal/class.json-api-date.php' );
+
+/**
+ * Class to handle different actions related to media.
+ */
+class Jetpack_Media {
+ public static $WP_ORIGINAL_MEDIA = '_wp_original_post_media';
+ public static $WP_REVISION_HISTORY = '_wp_revision_history';
+ public static $REVISION_HISTORY_MAXIMUM_AMOUNT = 0;
+ public static $WP_ATTACHMENT_IMAGE_ALT = '_wp_attachment_image_alt';
+
+ /**
+ * Generate a filename in function of the original filename of the media.
+ * The returned name has the `{basename}-{hash}-{random-number}.{ext}` shape.
+ * The hash is built according to the filename trying to avoid name collisions
+ * with other media files.
+ *
+ * @param number $media_id - media post ID
+ * @param string $new_filename - the new filename
+ * @return string A random filename.
+ */
+ public static function generate_new_filename( $media_id, $new_filename ) {
+ // get the right filename extension
+ $new_filename_paths = pathinfo( $new_filename );
+ $new_file_ext = $new_filename_paths['extension'];
+
+ // take out filename from the original file or from the current attachment
+ $original_media = (array) self::get_original_media( $media_id );
+
+ if ( ! empty( $original_media ) ) {
+ $original_file_parts = pathinfo( $original_media['file'] );
+ $filename_base = $original_file_parts['filename'];
+ } else {
+ $current_file = get_attached_file( $media_id );
+ $current_file_parts = pathinfo( $current_file );
+ $current_file_ext = $current_file_parts['filename'];
+ $filename_base = $current_file_parts['filename'];
+ }
+
+ // add unique seed based on the filename
+ $filename_base .= '-' . crc32( $filename_base ) . '-';
+
+ $number_suffix = time() . rand( 100, 999 );
+
+ do {
+ $filename = $filename_base;
+ $filename .= $number_suffix;
+ $file_ext = $new_file_ext ? $new_file_ext : $current_file_ext;
+
+ $new_filename = "{$filename}.{$file_ext}";
+ $new_path = "{$current_file_parts['dirname']}/$new_filename";
+ $number_suffix++;
+ } while( file_exists( $new_path ) );
+
+ return $new_filename;
+ }
+
+ /**
+ * File urls use the post (image item) date to generate a folder path.
+ * Post dates can change, so we use the original date used in the `guid`
+ * url so edits can remain in the same folder. In the following function
+ * we capture a string in the format of `YYYY/MM` from the guid.
+ *
+ * For example with a guid of
+ * "http://test.files.wordpress.com/2016/10/test.png" the resulting string
+ * would be: "2016/10"
+ *
+ * @param number $media_id
+ * @return string
+ */
+ private function get_time_string_from_guid( $media_id ) {
+ $time = date( "Y/m", strtotime( current_time( 'mysql' ) ) );
+
+ if ( $media = get_post( $media_id ) ) {
+ $pattern = '/\/(\d{4}\/\d{2})\//';
+ preg_match( $pattern, $media->guid, $matches );
+ if ( count( $matches ) > 1 ) {
+ $time = $matches[1];
+ }
+ }
+ return $time;
+ }
+
+ /**
+ * Return an array of allowed mime_type items used to upload a media file.
+ *
+ * @return array mime_type array
+ */
+ static function get_allowed_mime_types( $default_mime_types ) {
+ return array_unique( array_merge( $default_mime_types, array(
+ 'application/msword', // .doc
+ 'application/vnd.ms-powerpoint', // .ppt, .pps
+ 'application/vnd.ms-excel', // .xls
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
+ 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', // .ppsx
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
+ 'application/vnd.oasis.opendocument.text', // .odt
+ 'application/pdf', // .pdf
+ ) ) );
+ }
+
+ /**
+ * Checks that the mime type of the file
+ * is among those in a filterable list of mime types.
+ *
+ * @param string $file Path to file to get its mime type.
+ * @return bool
+ */
+ protected static function is_file_supported_for_sideloading( $file ) {
+ if ( class_exists( 'finfo' ) ) { // php 5.3+
+ // phpcs:ignore PHPCompatibility.PHP.NewClasses.finfoFound
+ $finfo = new finfo( FILEINFO_MIME );
+ $mime = explode( '; ', $finfo->file( $file ) );
+ $type = $mime[0];
+
+ } elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
+ $type = mime_content_type( $file );
+
+ } else {
+ return false;
+ }
+
+ /**
+ * Filter the list of supported mime types for media sideloading.
+ *
+ * @since 4.0
+ *
+ * @module json-api
+ *
+ * @param array $supported_mime_types Array of the supported mime types for media sideloading.
+ */
+ $supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
+ 'image/png',
+ 'image/jpeg',
+ 'image/gif',
+ 'image/bmp',
+ 'video/quicktime',
+ 'video/mp4',
+ 'video/mpeg',
+ 'video/ogg',
+ 'video/3gpp',
+ 'video/3gpp2',
+ 'video/h261',
+ 'video/h262',
+ 'video/h264',
+ 'video/x-msvideo',
+ 'video/x-ms-wmv',
+ 'video/x-ms-asf',
+ ) );
+
+ // If the type returned was not an array as expected, then we know we don't have a match.
+ if ( ! is_array( $supported_mime_types ) ) {
+ return false;
+ }
+
+ return in_array( $type, $supported_mime_types );
+ }
+
+ /**
+ * Try to remove the temporal file from the given file array.
+ *
+ * @param array $file_array Array with data about the temporal file
+ * @return bool `true` if the file has been removed. `false` either the file doesn't exist or it couldn't be removed.
+ */
+ private static function remove_tmp_file( $file_array ) {
+ if ( ! file_exists ( $file_array['tmp_name'] ) ) {
+ return false;
+ }
+ return @unlink( $file_array['tmp_name'] );
+ }
+
+ /**
+ * Save the given temporal file considering file type,
+ * correct location according to the original file path, etc.
+ * The file type control is done through of `jetpack_supported_media_sideload_types` filter,
+ * which allows define to the users their own file types list.
+ *
+ * @param array $file_array file to save
+ * @param number $media_id
+ * @return array|WP_Error an array with information about the new file saved or a WP_Error is something went wrong.
+ */
+ public static function save_temporary_file( $file_array, $media_id ) {
+ $tmp_filename = $file_array['tmp_name'];
+
+ if ( ! file_exists( $tmp_filename ) ) {
+ return new WP_Error( 'invalid_input', 'No media provided in input.' );
+ }
+
+ // add additional mime_types through of the `jetpack_supported_media_sideload_types` filter
+ $mime_type_static_filter = array(
+ 'Jetpack_Media',
+ 'get_allowed_mime_types'
+ );
+
+ add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
+ if (
+ ! self::is_file_supported_for_sideloading( $tmp_filename ) &&
+ ! file_is_displayable_image( $tmp_filename )
+ ) {
+ @unlink( $tmp_filename );
+ return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
+ }
+ remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
+
+ // generate a new file name
+ $tmp_new_filename = self::generate_new_filename( $media_id, $file_array[ 'name' ] );
+
+ // start to create the parameters to move the temporal file
+ $overrides = array( 'test_form' => false );
+
+ // get time according to the original filaname
+ $time = self::get_time_string_from_guid( $media_id );
+
+ $file_array['name'] = $tmp_new_filename;
+ $file = wp_handle_sideload( $file_array, $overrides, $time );
+
+ self::remove_tmp_file( $file_array );
+
+ if ( isset( $file['error'] ) ) {
+ return new WP_Error( 'upload_error', $file['error'] );
+ }
+
+ return $file;
+ }
+
+ /**
+ * Return an object with an snapshot of a revision item.
+ *
+ * @param object $media_item - media post object
+ * @return object a revision item
+ */
+ public static function get_snapshot( $media_item ) {
+ $current_file = get_attached_file( $media_item->ID );
+ $file_paths = pathinfo( $current_file );
+
+ $snapshot = array(
+ 'date' => (string) WPCOM_JSON_API_Date::format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
+ 'URL' => (string) wp_get_attachment_url( $media_item->ID ),
+ 'file' => (string) $file_paths['basename'],
+ 'extension' => (string) $file_paths['extension'],
+ 'mime_type' => (string) $media_item->post_mime_type,
+ 'size' => (int) filesize( $current_file )
+ );
+
+ return (object) $snapshot;
+ }
+
+ /**
+ * Add a new item into revision_history array.
+ *
+ * @param object $media_item - media post object
+ * @param file $file - file recently added
+ * @param bool $has_original_media - condition is the original media has been already added
+ * @return bool `true` if the item has been added. Otherwise `false`.
+ */
+ public static function register_revision( $media_item, $file, $has_original_media ) {
+ if ( is_wp_error( $file ) || ! $has_original_media ) {
+ return false;
+ }
+
+ add_post_meta( $media_item->ID, self::$WP_REVISION_HISTORY, self::get_snapshot( $media_item ) );
+ }
+ /**
+ * Return the `revision_history` of the given media.
+ *
+ * @param number $media_id - media post ID
+ * @return array `revision_history` array
+ */
+ public static function get_revision_history( $media_id ) {
+ return array_reverse( get_post_meta( $media_id, self::$WP_REVISION_HISTORY ) );
+ }
+
+ /**
+ * Return the original media data
+ */
+ public static function get_original_media( $media_id ) {
+ $original = get_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, true );
+ $original = $original ? $original : array();
+ return $original;
+ }
+
+ public static function delete_file( $pathname ) {
+ if ( ! file_exists( $pathname ) || ! is_file( $pathname ) ) {
+ // let's touch a fake file to try to `really` remove the media file
+ touch( $pathname );
+ }
+
+ return wp_delete_file( $pathname );
+ }
+
+ /**
+ * Try to delete a file according to the dirname of
+ * the media attached file and the filename.
+ *
+ * @param number $media_id - media post ID
+ * @param string $filename - basename of the file ( name-of-file.ext )
+ * @return bool `true` is the file has been removed, `false` if not.
+ */
+ private static function delete_media_history_file( $media_id, $filename ) {
+ $attached_path = get_attached_file( $media_id );
+ $attached_parts = pathinfo( $attached_path );
+ $dirname = $attached_parts['dirname'];
+
+ $pathname = $dirname . '/' . $filename;
+
+ // remove thumbnails
+ $metadata = wp_generate_attachment_metadata( $media_id, $pathname );
+
+ if ( isset( $metadata ) && isset( $metadata['sizes'] ) ) {
+ foreach ( $metadata['sizes'] as $size => $properties ) {
+ self::delete_file( $dirname . '/' . $properties['file'] );
+ }
+ }
+
+ // remove primary file
+ self::delete_file( $pathname );
+ }
+
+ /**
+ * Remove specific items from the `revision history` array
+ * depending on the given criteria: array(
+ * 'from' => (int) <from>,
+ * 'to' => (int) <to>,
+ * )
+ *
+ * Also, it removes the file defined in each item.
+ *
+ * @param number $media_id - media post ID
+ * @param object $criteria - criteria to remove the items
+ * @param array [$revision_history] - revision history array
+ * @return array `revision_history` array updated.
+ */
+ public static function remove_items_from_revision_history( $media_id, $criteria = array(), $revision_history ) {
+ if ( ! isset ( $revision_history ) ) {
+ $revision_history = self::get_revision_history( $media_id );
+ }
+
+ $from = $criteria['from'];
+ $to = $criteria['to'] ? $criteria['to'] : ( $from + 1 );
+
+ for ( $i = $from; $i < $to; $i++ ) {
+ $removed_item = array_slice( $revision_history, $from, 1 );
+ if ( ! $removed_item ) {
+ break;
+ }
+
+ array_splice( $revision_history, $from, 1 );
+ self::delete_media_history_file( $media_id, $removed_item[0]->file );
+ }
+
+ // override all history items
+ delete_post_meta( $media_id, self::$WP_REVISION_HISTORY );
+ $revision_history = array_reverse( $revision_history );
+ foreach ( $revision_history as &$item ) {
+ add_post_meta( $media_id, self::$WP_REVISION_HISTORY, $item );
+ }
+
+ return $revision_history;
+ }
+
+ /**
+ * Limit the number of items of the `revision_history` array.
+ * When the stack is overflowing the oldest item is remove from there (FIFO).
+ *
+ * @param number $media_id - media post ID
+ * @param number [$limit] - maximun amount of items. 20 as default.
+ * @return array items removed from `revision_history`
+ */
+ public static function limit_revision_history( $media_id, $limit = null) {
+ if ( is_null( $limit ) ) {
+ $limit = self::$REVISION_HISTORY_MAXIMUM_AMOUNT;
+ }
+
+ $revision_history = self::get_revision_history( $media_id );
+
+ $total = count( $revision_history );
+
+ if ( $total < $limit ) {
+ return array();
+ }
+
+ self::remove_items_from_revision_history(
+ $media_id,
+ array( 'from' => $limit, 'to' => $total ),
+ $revision_history
+ );
+
+ return self::get_revision_history( $media_id );
+ }
+
+ /**
+ * Remove the original file and clean the post metadata.
+ *
+ * @param number $media_id - media post ID
+ */
+ public static function clean_original_media( $media_id ) {
+ $original_file = self::get_original_media( $media_id );
+
+ if ( ! $original_file ) {
+ return null;
+ }
+
+ self::delete_media_history_file( $media_id, $original_file->file );
+ return delete_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA );
+ }
+
+ /**
+ * Clean `revision_history` of the given $media_id. it means:
+ * - remove all media files tied to the `revision_history` items.
+ * - clean `revision_history` meta data.
+ * - remove and clean the `original_media`
+ *
+ * @param number $media_id - media post ID
+ * @return array results of removing these files
+ */
+ public static function clean_revision_history( $media_id ) {
+ self::clean_original_media( $media_id );
+
+ $revision_history = self::get_revision_history( $media_id );
+ $total = count( $revision_history );
+ $updated_history = array();
+
+ if ( $total < 1 ) {
+ return $updated_history;
+ }
+
+ $updated_history = self::remove_items_from_revision_history(
+ $media_id,
+ array( 'from' => 0, 'to' => $total ),
+ $revision_history
+ );
+
+ return $updated_history;
+ }
+
+ /**
+ * Edit media item process:
+ *
+ * - update attachment file
+ * - preserve original media file
+ * - trace revision history
+ *
+ * @param number $media_id - media post ID
+ * @param array $file_array - temporal file
+ * @return {Post|WP_Error} Updated media item or a WP_Error is something went wrong.
+ */
+ public static function edit_media_file( $media_id, $file_array ) {
+ $media_item = get_post( $media_id );
+ $has_original_media = self::get_original_media( $media_id );
+
+ if ( ! $has_original_media ) {
+ // The first time that the media is updated
+ // the original media is stored into the revision_history
+ $snapshot = self::get_snapshot( $media_item );
+ add_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, $snapshot, true );
+ }
+
+ // save temporary file in the correct location
+ $uploaded_file = self::save_temporary_file( $file_array, $media_id );
+
+ if ( is_wp_error( $uploaded_file ) ) {
+ self::remove_tmp_file( $file_array );
+ return $uploaded_file;
+ }
+
+ // revision_history control
+ self::register_revision( $media_item, $uploaded_file, $has_original_media );
+
+ $uploaded_path = $uploaded_file['file'];
+ $udpated_mime_type = $uploaded_file['type'];
+ $was_updated = update_attached_file( $media_id, $uploaded_path );
+
+ if ( ! $was_updated ) {
+ return WP_Error( 'update_error', 'Media update error' );
+ }
+
+ $new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
+ wp_update_attachment_metadata( $media_id, $new_metadata );
+
+ // check maximum amount of revision_history
+ self::limit_revision_history( $media_id );
+
+ $edited_action = wp_update_post( (object) array(
+ 'ID' => $media_id,
+ 'post_mime_type' => $udpated_mime_type
+ ), true );
+
+ if ( is_wp_error( $edited_action ) ) {
+ return $edited_action;
+ }
+
+ return $media_item;
+ }
+}
+
+// hook: clean revision history when the media item is deleted
+function clean_revision_history( $media_id ) {
+ Jetpack_Media::clean_revision_history( $media_id );
+};
+
+add_action( 'delete_attachment', 'clean_revision_history' );
+
diff --git a/plugins/jetpack/_inc/lib/core-api/class-wpcom-rest-field-controller.php b/plugins/jetpack/_inc/lib/core-api/class-wpcom-rest-field-controller.php
new file mode 100644
index 00000000..2a2245e4
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class-wpcom-rest-field-controller.php
@@ -0,0 +1,333 @@
+<?php
+
+// @todo - nicer API for array values?
+
+/**
+ * `WP_REST_Controller` is basically a wrapper for `register_rest_route()`
+ * `WPCOM_REST_API_V2_Field_Controller` is a mostly-analogous wrapper for `register_rest_field()`
+ */
+abstract class WPCOM_REST_API_V2_Field_Controller {
+ /**
+ * @var string|string[] $object_type The REST Object Type(s) to which the field should be added.
+ */
+ protected $object_type;
+
+ /**
+ * @var string $field_name The name of the REST API field to add.
+ */
+ protected $field_name;
+
+ public function __construct() {
+ if ( ! $this->object_type ) {
+ /* translators: %s: object_type */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::$object_type', sprintf( __( "Property '%s' must be overridden.", 'jetpack' ), 'object_type' ), 'Jetpack 6.8' );
+ return;
+ }
+
+ if ( ! $this->field_name ) {
+ /* translators: %s: field_name */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::$field_name', sprintf( __( "Property '%s' must be overridden.", 'jetpack' ), 'field_name' ), 'Jetpack 6.8' );
+ return;
+ }
+
+ add_action( 'rest_api_init', array( $this, 'register_fields' ) );
+
+ // do this again later to collect any CPTs that get registered later
+ add_action( 'restapi_theme_init', array( $this, 'register_fields' ), 20 );
+ }
+
+ /**
+ * Registers the field with the appropriate schema and callbacks.
+ */
+ public function register_fields() {
+ foreach ( (array) $this->object_type as $object_type ) {
+ register_rest_field(
+ $object_type,
+ $this->field_name,
+ array(
+ 'get_callback' => array( $this, 'get_for_response' ),
+ 'update_callback' => array( $this, 'update_from_request' ),
+ 'schema' => $this->get_schema(),
+ )
+ );
+ }
+ }
+
+ /**
+ * Ensures the response matches the schema and request context.
+ *
+ * @param mixed $value
+ * @param WP_REST_Request $request
+ * @return mixed
+ */
+ private function prepare_for_response( $value, $request ) {
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $schema = $this->get_schema();
+
+ $is_valid = rest_validate_value_from_schema( $value, $schema, $this->field_name );
+ if ( is_wp_error( $is_valid ) ) {
+ return $is_valid;
+ }
+
+ return $this->filter_response_by_context( $value, $schema, $context );
+ }
+
+ /**
+ * Returns the schema's default value
+ *
+ * If there is no default, returns the type's falsey value.
+ *
+ * @param array $schema
+ * @return mixed
+ */
+ final public function get_default_value( $schema ) {
+ if ( isset( $schema['default'] ) ) {
+ return $schema['default'];
+ }
+
+ // If you have something more complicated, use $schema['default'];
+ switch ( isset( $schema['type'] ) ? $schema['type'] : 'null' ) {
+ case 'string':
+ return '';
+ case 'integer':
+ case 'number':
+ return 0;
+ case 'object':
+ return (object) array();
+ case 'array':
+ return array();
+ case 'boolean':
+ return false;
+ case 'null':
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * The field's wrapped getter. Does permission checks and output preparation.
+ *
+ * This cannot be extended: implement `->get()` instead.
+ *
+ * @param mixed $object_data Probably an array. Whatever the endpoint returns.
+ * @param string $field_name Should always match `->field_name`
+ * @param WP_REST_Request $request
+ * @param string $object_type Should always match `->object_type`
+ * @return mixed
+ */
+ final public function get_for_response( $object_data, $field_name, $request, $object_type ) {
+ $permission_check = $this->get_permission_check( $object_data, $request );
+
+ if ( ! $permission_check ) {
+ /* translators: %s: get_permission_check() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error.", 'jetpack' ), 'get_permission_check' ), 'Jetpack 6.8' );
+ return $this->get_default_value( $this->get_schema() );
+ }
+
+ if ( is_wp_error( $permission_check ) ) {
+ return $this->get_default_value( $this->get_schema() );
+ }
+
+ $value = $this->get( $object_data, $request );
+
+ return $this->prepare_for_response( $value, $request );
+ }
+
+ /**
+ * The field's wrapped setter. Does permission checks.
+ *
+ * This cannot be extended: implement `->update()` instead.
+ *
+ * @param mixed $value The new value for the field.
+ * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
+ * @param string $field_name Should always match `->field_name`
+ * @param WP_REST_Request $request
+ * @param string $object_type Should always match `->object_type`
+ * @return void|WP_Error
+ */
+ final public function update_from_request( $value, $object_data, $field_name, $request, $object_type ) {
+ $permission_check = $this->update_permission_check( $value, $object_data, $request );
+
+ if ( ! $permission_check ) {
+ /* translators: %s: update_permission_check() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error.", 'jetpack' ), 'update_permission_check' ), 'Jetpack 6.8' );
+ /* translators: %s: the name of an API response field */
+ return new WP_Error( 'invalid_user_permission', sprintf( __( "You are not allowed to access the '%s' field.", 'jetpack' ), $this->field_name ) );
+ }
+
+ if ( is_wp_error( $permission_check ) ) {
+ return $permission_check;
+ }
+
+ $updated = $this->update( $value, $object_data, $request );
+
+ if ( is_wp_error( $updated ) ) {
+ return $updated;
+ }
+ }
+
+ /**
+ * Permission Check for the field's getter. Must be implemented in the inheriting class.
+ *
+ * @param mixed $object_data Whatever the endpoint would return for its response.
+ * @param WP_REST_Request $request
+ * @return true|WP_Error
+ */
+ public function get_permission_check( $object_data, $request ) {
+ /* translators: %s: get_permission_check() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * The field's "raw" getter. Must be implemented in the inheriting class.
+ *
+ * @param mixed $object_data Whatever the endpoint would return for its response.
+ * @param WP_REST_Request $request
+ * @return mixed
+ */
+ public function get( $object_data, $request ) {
+ /* translators: %s: get() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * Permission Check for the field's setter. Must be implemented in the inheriting class.
+ *
+ * @param mixed $value The new value for the field.
+ * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
+ * @param WP_REST_Request $request
+ * @return true|WP_Error
+ */
+ public function update_permission_check( $value, $object_data, $request ) {
+ /* translators: %s: update_permission_check() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * The field's "raw" setter. Must be implemented in the inheriting class.
+ *
+ * @param mixed $value The new value for the field.
+ * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
+ * @param WP_REST_Request $request
+ * @return mixed
+ */
+ public function update( $value, $object_data, $request ) {
+ /* translators: %s: update() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * The JSON Schema for the field
+ *
+ * @link https://json-schema.org/understanding-json-schema/
+ * As of WordPress 5.0, Core currently understands:
+ * * type
+ * * string - not minLength, not maxLength, not pattern
+ * * integer - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
+ * * number - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
+ * * boolean
+ * * null
+ * * object - properties, additionalProperties, not propertyNames, not dependencies, not patternProperties, not required
+ * * array: only lists, not tuples - items, not minItems, not maxItems, not uniqueItems, not contains
+ * * enum
+ * * format
+ * * date-time
+ * * email
+ * * ip
+ * * uri
+ * As of WordPress 5.0, Core does not support:
+ * * Multiple type: `type: [ 'string', 'integer' ]`
+ * * $ref, allOf, anyOf, oneOf, not, const
+ *
+ * @return array
+ */
+ public function get_schema() {
+ /* translators: %s: get_schema() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_schema', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * @param array $schema
+ * @param string $context REST API Request context
+ * @return bool
+ */
+ private function is_valid_for_context( $schema, $context ) {
+ return empty( $schema['context'] ) || in_array( $context, $schema['context'], true );
+ }
+
+ /**
+ * Removes properties that should not appear in the current
+ * request's context
+ *
+ * $context is a Core REST API Framework request attribute that is
+ * always one of:
+ * * view (what you see on the blog)
+ * * edit (what you see in an editor)
+ * * embed (what you see in, e.g., an oembed)
+ *
+ * Fields (and sub-fields, and sub-sub-...) can be flagged for a
+ * set of specific contexts via the field's schema.
+ *
+ * The Core API will filter out top-level fields with the wrong
+ * context, but will not recurse deeply enough into arrays/objects
+ * to remove all levels of sub-fields with the wrong context.
+ *
+ * This function handles that recursion.
+ *
+ * @param mixed $value
+ * @param array $schema
+ * @param string $context REST API Request context
+ * @return mixed Filtered $value
+ */
+ final public function filter_response_by_context( $value, $schema, $context ) {
+ if ( ! $this->is_valid_for_context( $schema, $context ) ) {
+ // We use this intentionally odd looking WP_Error object
+ // internally only in this recursive function (see below
+ // in the `object` case). It will never be output by the REST API.
+ // If we return this for the top level object, Core
+ // correctly remove the top level object from the response
+ // for us.
+ return new WP_Error( '__wrong-context__' );
+ }
+
+ switch ( $schema['type'] ) {
+ case 'array':
+ if ( ! isset( $schema['items'] ) ) {
+ return $value;
+ }
+
+ // Shortcircuit if we know none of the items are valid for this context.
+ // This would only happen in a strangely written schema.
+ if ( ! $this->is_valid_for_context( $schema['items'], $context ) ) {
+ return array();
+ }
+
+ // Recurse to prune sub-properties of each item.
+ foreach ( $value as $key => $item ) {
+ $value[ $key ] = $this->filter_response_by_context( $item, $schema['items'], $context );
+ }
+
+ return $value;
+ case 'object':
+ if ( ! isset( $schema['properties'] ) ) {
+ return $value;
+ }
+
+ foreach ( $value as $field_name => $field_value ) {
+ if ( isset( $schema['properties'][ $field_name ] ) ) {
+ $field_value = $this->filter_response_by_context( $field_value, $schema['properties'][ $field_name ], $context );
+ if ( is_wp_error( $field_value ) && '__wrong-context__' === $field_value->get_error_code() ) {
+ unset( $value[ $field_name ] );
+ } else {
+ // Respect recursion that pruned sub-properties of each property.
+ $value[ $field_name ] = $field_value;
+ }
+ }
+ }
+
+ return (object) $value;
+ }
+
+ return $value;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php
new file mode 100644
index 00000000..96a47a08
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php
@@ -0,0 +1,1708 @@
+<?php
+/**
+ * This is the base class for every Core API endpoint Jetpack uses.
+ *
+ */
+class Jetpack_Core_API_Module_Toggle_Endpoint
+ extends Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
+
+ /**
+ * Check if the module requires the site to be publicly accessible from WPCOM.
+ * If the site meets this requirement, the module is activated. Otherwise an error is returned.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Module slug.
+ * @type bool $active should module be activated.
+ * }
+ *
+ * @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
+ */
+ public function process( $request ) {
+ if ( $request['active'] ) {
+ return $this->activate_module( $request );
+ } else {
+ return $this->deactivate_module( $request );
+ }
+ }
+
+ /**
+ * If it's a valid Jetpack module, activate it.
+ *
+ * @since 4.3.0
+ *
+ * @param string|WP_REST_Request $request It's a WP_REST_Request when called from endpoint /module/<slug>/*
+ * and a string when called from Jetpack_Core_API_Data->update_data.
+ * {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Module slug.
+ * }
+ *
+ * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public function activate_module( $request ) {
+ $module_slug = '';
+
+ if (
+ (
+ is_array( $request )
+ || is_object( $request )
+ )
+ && isset( $request['slug'] )
+ ) {
+ $module_slug = $request['slug'];
+ } else {
+ $module_slug = $request;
+ }
+
+ if ( ! Jetpack::is_module( $module_slug ) ) {
+ return new WP_Error(
+ 'not_found',
+ esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ if ( ! Jetpack_Plan::supports( $module_slug ) ) {
+ return new WP_Error(
+ 'not_supported',
+ esc_html__( 'The requested Jetpack module is not supported by your plan.', 'jetpack' ),
+ array( 'status' => 424 )
+ );
+ }
+
+ if ( Jetpack::activate_module( $module_slug, false, false ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'The requested Jetpack module was activated.', 'jetpack' ),
+ ) );
+ }
+
+ return new WP_Error(
+ 'activation_failed',
+ esc_html__( 'The requested Jetpack module could not be activated.', 'jetpack' ),
+ array( 'status' => 424 )
+ );
+ }
+
+ /**
+ * If it's a valid Jetpack module, deactivate it.
+ *
+ * @since 4.3.0
+ *
+ * @param string|WP_REST_Request $request It's a WP_REST_Request when called from endpoint /module/<slug>/*
+ * and a string when called from Jetpack_Core_API_Data->update_data.
+ * {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Module slug.
+ * }
+ *
+ * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public function deactivate_module( $request ) {
+ $module_slug = '';
+
+ if (
+ (
+ is_array( $request )
+ || is_object( $request )
+ )
+ && isset( $request['slug'] )
+ ) {
+ $module_slug = $request['slug'];
+ } else {
+ $module_slug = $request;
+ }
+
+ if ( ! Jetpack::is_module( $module_slug ) ) {
+ return new WP_Error(
+ 'not_found',
+ esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ if ( ! Jetpack::is_module_active( $module_slug ) ) {
+ return new WP_Error(
+ 'already_inactive',
+ esc_html__( 'The requested Jetpack module was already inactive.', 'jetpack' ),
+ array( 'status' => 409 )
+ );
+ }
+
+ if ( Jetpack::deactivate_module( $module_slug ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'The requested Jetpack module was deactivated.', 'jetpack' ),
+ ) );
+ }
+ return new WP_Error(
+ 'deactivation_failed',
+ esc_html__( 'The requested Jetpack module could not be deactivated.', 'jetpack' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ /**
+ * Check that the current user has permissions to manage Jetpack modules.
+ *
+ * @since 4.3.0
+ *
+ * @return bool
+ */
+ public function can_request() {
+ return current_user_can( 'jetpack_manage_modules' );
+ }
+}
+
+class Jetpack_Core_API_Module_List_Endpoint {
+
+ /**
+ * A WordPress REST API callback method that accepts a request object and decides what to do with it.
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @since 4.3.0
+ *
+ * @return bool|Array|WP_Error a resulting value or object, or an error.
+ */
+ public function process( $request ) {
+ if ( 'GET' === $request->get_method() ) {
+ return $this->get_modules( $request );
+ } else {
+ return $this->activate_modules( $request );
+ }
+ }
+
+ /**
+ * Get a list of all Jetpack modules and their information.
+ *
+ * @since 4.3.0
+ *
+ * @return array Array of Jetpack modules.
+ */
+ public function get_modules() {
+ require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php' );
+
+ $modules = Jetpack_Admin::init()->get_modules();
+ foreach ( $modules as $slug => $properties ) {
+ $modules[ $slug ]['options'] =
+ Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $slug );
+ if (
+ isset( $modules[ $slug ]['requires_connection'] )
+ && $modules[ $slug ]['requires_connection']
+ && Jetpack::is_development_mode()
+ ) {
+ $modules[ $slug ]['activated'] = false;
+ }
+ }
+
+ $modules = Jetpack::get_translated_modules( $modules );
+
+ return Jetpack_Core_Json_Api_Endpoints::prepare_modules_for_response( $modules );
+ }
+
+ /**
+ * Activate a list of valid Jetpack modules.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Module slug.
+ * }
+ *
+ * @return bool|WP_Error True if modules were activated. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public static function activate_modules( $request ) {
+
+ if (
+ ! isset( $request['modules'] )
+ || ! is_array( $request['modules'] )
+ ) {
+ return new WP_Error(
+ 'not_found',
+ esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $activated = array();
+ $failed = array();
+
+ foreach ( $request['modules'] as $module ) {
+ if ( Jetpack::activate_module( $module, false, false ) ) {
+ $activated[] = $module;
+ } else {
+ $failed[] = $module;
+ }
+ }
+
+ if ( empty( $failed ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'All modules activated.', 'jetpack' ),
+ ) );
+ }
+
+ $error = '';
+
+ $activated_count = count( $activated );
+ if ( $activated_count > 0 ) {
+ $activated_last = array_pop( $activated );
+ $activated_text = $activated_count > 1 ? sprintf(
+ /* Translators: first variable is a list followed by the last item, which is the second variable. Example: dog, cat and bird. */
+ __( '%1$s and %2$s', 'jetpack' ),
+ join( ', ', $activated ), $activated_last ) : $activated_last;
+
+ $error = sprintf(
+ /* Translators: the variable is a module name. */
+ _n( 'The module %s was activated.', 'The modules %s were activated.', $activated_count, 'jetpack' ),
+ $activated_text ) . ' ';
+ }
+
+ $failed_count = count( $failed );
+ if ( count( $failed ) > 0 ) {
+ $failed_last = array_pop( $failed );
+ $failed_text = $failed_count > 1 ? sprintf(
+ /* Translators: first variable is a list followed by the last item, which is the second variable. Example: dog, cat and bird. */
+ __( '%1$s and %2$s', 'jetpack' ),
+ join( ', ', $failed ), $failed_last ) : $failed_last;
+
+ $error = sprintf(
+ /* Translators: the variable is a module name. */
+ _n( 'The module %s failed to be activated.', 'The modules %s failed to be activated.', $failed_count, 'jetpack' ),
+ $failed_text ) . ' ';
+ }
+
+ return new WP_Error(
+ 'activation_failed',
+ esc_html( $error ),
+ array( 'status' => 424 )
+ );
+ }
+
+ /**
+ * A WordPress REST API permission callback method that accepts a request object and decides
+ * if the current user has enough privileges to act.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return bool does the current user have enough privilege.
+ */
+ public function can_request( $request ) {
+ if ( 'GET' === $request->get_method() ) {
+ return current_user_can( 'jetpack_admin_page' );
+ } else {
+ return current_user_can( 'jetpack_manage_modules' );
+ }
+ }
+}
+
+/**
+ * Class that manages updating of Jetpack module options and general Jetpack settings or retrieving module data.
+ * If no module is specified, all module settings are retrieved/updated.
+ *
+ * @since 4.3.0
+ * @since 4.4.0 Renamed Jetpack_Core_API_Module_Endpoint from to Jetpack_Core_API_Data.
+ *
+ * @author Automattic
+ */
+class Jetpack_Core_API_Data extends Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
+
+ /**
+ * Process request by returning the module or updating it.
+ * If no module is specified, settings for all modules are assumed.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request
+ *
+ * @return bool|mixed|void|WP_Error
+ */
+ public function process( $request ) {
+ if ( 'GET' === $request->get_method() ) {
+ if ( isset( $request['slug'] ) ) {
+ return $this->get_module( $request );
+ }
+
+ return $this->get_all_options();
+ } else {
+ return $this->update_data( $request );
+ }
+ }
+
+ /**
+ * Get information about a specific and valid Jetpack module.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Module slug.
+ * }
+ *
+ * @return mixed|void|WP_Error
+ */
+ public function get_module( $request ) {
+ if ( Jetpack::is_module( $request['slug'] ) ) {
+
+ $module = Jetpack::get_module( $request['slug'] );
+
+ $module['options'] = Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $request['slug'] );
+
+ if (
+ isset( $module['requires_connection'] )
+ && $module['requires_connection']
+ && Jetpack::is_development_mode()
+ ) {
+ $module['activated'] = false;
+ }
+
+ $i18n = jetpack_get_module_i18n( $request['slug'] );
+ if ( isset( $module['name'] ) ) {
+ $module['name'] = $i18n['name'];
+ }
+ if ( isset( $module['description'] ) ) {
+ $module['description'] = $i18n['description'];
+ $module['short_description'] = $i18n['description'];
+ }
+
+ return Jetpack_Core_Json_Api_Endpoints::prepare_modules_for_response( $module );
+ }
+
+ return new WP_Error(
+ 'not_found',
+ esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ /**
+ * Get information about all Jetpack module options and settings.
+ *
+ * @since 4.6.0
+ *
+ * @return WP_REST_Response $response
+ */
+ public function get_all_options() {
+ $response = array();
+
+ $modules = Jetpack::get_available_modules();
+ if ( is_array( $modules ) && ! empty( $modules ) ) {
+ foreach ( $modules as $module ) {
+ // Add all module options
+ $options = Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $module );
+ foreach ( $options as $option_name => $option ) {
+ $response[ $option_name ] = $option['current_value'];
+ }
+
+ // Add the module activation state
+ $response[ $module ] = Jetpack::is_module_active( $module );
+ }
+ }
+
+ $settings = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( 'settings' );
+
+ if ( ! function_exists( 'is_plugin_active' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+
+ foreach ( $settings as $setting => $properties ) {
+ switch ( $setting ) {
+ case 'lang_id':
+ if ( defined( 'WPLANG' ) ) {
+ // We can't affect this setting, so warn the client
+ $response[ $setting ] = 'error_const';
+ break;
+ }
+
+ if ( ! current_user_can( 'install_languages' ) ) {
+ // The user doesn't have caps to install language packs, so warn the client
+ $response[ $setting ] = 'error_cap';
+ break;
+ }
+
+ $value = get_option( 'WPLANG' );
+ $response[ $setting ] = empty( $value ) ? 'en_US' : $value;
+ break;
+
+ case 'wordpress_api_key':
+ // When field is clear, return empty. Otherwise it would return "false".
+ if ( '' === get_option( 'wordpress_api_key', '' ) ) {
+ $response[ $setting ] = '';
+ } else {
+ if ( ! class_exists( 'Akismet' ) ) {
+ if ( is_readable( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
+ require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
+ }
+ }
+ $response[ $setting ] = class_exists( 'Akismet' ) ? Akismet::get_api_key() : '';
+ }
+ break;
+
+ case 'onboarding':
+ $business_address = get_option( 'jpo_business_address' );
+ $business_address = is_array( $business_address ) ? array_map( array( $this, 'decode_special_characters' ), $business_address ) : $business_address;
+
+ $response[ $setting ] = array(
+ 'siteTitle' => $this->decode_special_characters( get_option( 'blogname' ) ),
+ 'siteDescription' => $this->decode_special_characters( get_option( 'blogdescription' ) ),
+ 'siteType' => get_option( 'jpo_site_type' ),
+ 'homepageFormat' => get_option( 'jpo_homepage_format' ),
+ 'addContactForm' => intval( get_option( 'jpo_contact_page' ) ),
+ 'businessAddress' => $business_address,
+ 'installWooCommerce' => is_plugin_active( 'woocommerce/woocommerce.php' ),
+ 'stats' => Jetpack::is_active() && Jetpack::is_module_active( 'stats' ),
+ );
+ break;
+
+ default:
+ $response[ $setting ] = Jetpack_Core_Json_Api_Endpoints::cast_value( get_option( $setting ), $settings[ $setting ] );
+ break;
+ }
+ }
+
+ $response['akismet'] = is_plugin_active( 'akismet/akismet.php' );
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Decode the special HTML characters in a certain value.
+ *
+ * @since 5.8
+ *
+ * @param string $value Value to decode.
+ *
+ * @return string Value with decoded HTML characters.
+ */
+ private function decode_special_characters( $value ) {
+ return (string) htmlspecialchars_decode( $value, ENT_QUOTES );
+ }
+
+ /**
+ * If it's a valid Jetpack module and configuration parameters have been sent, update it.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Module slug.
+ * }
+ *
+ * @return bool|WP_Error True if module was updated. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public function update_data( $request ) {
+
+ // If it's null, we're trying to update many module options from different modules.
+ if ( is_null( $request['slug'] ) ) {
+
+ // Value admitted by Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list that will make it return all module options.
+ // It will not be passed. It's just checked in this method to pass that method a string or array.
+ $request['slug'] = 'any';
+ } else {
+ if ( ! Jetpack::is_module( $request['slug'] ) ) {
+ return new WP_Error( 'not_found', esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ if ( ! Jetpack::is_module_active( $request['slug'] ) ) {
+ return new WP_Error( 'inactive', esc_html__( 'The requested Jetpack module is inactive.', 'jetpack' ), array( 'status' => 409 ) );
+ }
+ }
+
+ // Get parameters to update the module. We can not simply use $request->get_params() because when we registered
+ // this route, we are adding the entire output of Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list() to
+ // the current request object's params. We are interested in body of the actual request.
+ // This may be JSON:
+ $params = $request->get_json_params();
+ if ( ! is_array( $params ) ) {
+ // Or it may be standard POST key-value pairs:
+ $params = $request->get_body_params();
+ }
+
+ // Exit if no parameters were passed.
+ if ( ! is_array( $params ) ) {
+ return new WP_Error( 'missing_options', esc_html__( 'Missing options.', 'jetpack' ), array( 'status' => 404 ) );
+ }
+
+ // If $params was set via `get_body_params()` there may be some additional variables in the request that can
+ // cause validation to fail. This method verifies that each param was in fact updated and will throw a `some_updated`
+ // error if unused variables are included in the request.
+ foreach ( array_keys( $params ) as $key ) {
+ if ( is_int( $key ) || 'slug' === $key || 'context' === $key ) {
+ unset( $params[ $key ] );
+ }
+ }
+
+ // Get available module options.
+ $options = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( 'any' === $request['slug']
+ ? $params
+ : $request['slug']
+ );
+
+ // Prepare to toggle module if needed
+ $toggle_module = new Jetpack_Core_API_Module_Toggle_Endpoint( new Jetpack_IXR_Client() );
+
+ // Options that are invalid or failed to update.
+ $invalid = array_keys( array_diff_key( $params, $options ) );
+ $not_updated = array();
+
+ // Remove invalid options
+ $params = array_intersect_key( $params, $options );
+
+ // Used if response is successful. The message can be overwritten and additional data can be added here.
+ $response = array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'The requested Jetpack data updates were successful.', 'jetpack' ),
+ );
+
+ // If there are modules to activate, activate them first so they're ready when their options are set.
+ foreach ( $params as $option => $value ) {
+ if ( 'modules' === $options[ $option ]['jp_group'] ) {
+
+ // Used if there was an error. Can be overwritten with specific error messages.
+ $error = '';
+
+ // Set to true if the module toggling was successful.
+ $updated = false;
+
+ // Check if user can toggle the module.
+ if ( $toggle_module->can_request() ) {
+
+ // Activate or deactivate the module according to the value passed.
+ $toggle_result = $value
+ ? $toggle_module->activate_module( $option )
+ : $toggle_module->deactivate_module( $option );
+
+ if (
+ is_wp_error( $toggle_result )
+ && 'already_inactive' === $toggle_result->get_error_code()
+ ) {
+
+ // If the module is already inactive, we don't fail
+ $updated = true;
+ } elseif ( is_wp_error( $toggle_result ) ) {
+ $error = $toggle_result->get_error_message();
+ } else {
+ $updated = true;
+ }
+ } else {
+ $error = Jetpack_Core_Json_Api_Endpoints::$user_permissions_error_msg;
+ }
+
+ // The module was not toggled.
+ if ( ! $updated ) {
+ $not_updated[ $option ] = $error;
+ }
+
+ // Remove module from list so we don't go through it again.
+ unset( $params[ $option ] );
+ }
+ }
+
+ foreach ( $params as $option => $value ) {
+
+ // Used if there was an error. Can be overwritten with specific error messages.
+ $error = '';
+
+ // Set to true if the option update was successful.
+ $updated = false;
+
+ // Get option attributes, including the group it belongs to.
+ $option_attrs = $options[ $option ];
+
+ // If this is a module option and the related module isn't active for any reason, continue with the next one.
+ if ( 'settings' !== $option_attrs['jp_group'] ) {
+ if ( ! Jetpack::is_module( $option_attrs['jp_group'] ) ) {
+ $not_updated[ $option ] = esc_html__( 'The requested Jetpack module was not found.', 'jetpack' );
+ continue;
+ }
+
+ if (
+ 'any' !== $request['slug']
+ && ! Jetpack::is_module_active( $option_attrs['jp_group'] )
+ ) {
+
+ // We only take note of skipped options when updating one module
+ $not_updated[ $option ] = esc_html__( 'The requested Jetpack module is inactive.', 'jetpack' );
+ continue;
+ }
+ }
+
+ // Properly cast value based on its type defined in endpoint accepted args.
+ $value = Jetpack_Core_Json_Api_Endpoints::cast_value( $value, $option_attrs );
+
+ switch ( $option ) {
+ case 'lang_id':
+ if ( defined( 'WPLANG' ) || ! current_user_can( 'install_languages' ) ) {
+ // We can't affect this setting
+ $updated = false;
+ break;
+ }
+
+ if ( $value === 'en_US' || empty( $value ) ) {
+ return delete_option( 'WPLANG' );
+ }
+
+ if ( ! function_exists( 'request_filesystem_credentials' ) ) {
+ require_once( ABSPATH . 'wp-admin/includes/file.php' );
+ }
+
+ if ( ! function_exists( 'wp_download_language_pack' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/translation-install.php';
+ }
+
+ // `wp_download_language_pack` only tries to download packs if they're not already available
+ $language = wp_download_language_pack( $value );
+ if ( $language === false ) {
+ // The language pack download failed.
+ $updated = false;
+ break;
+ }
+ $updated = get_option( 'WPLANG' ) === $language ? true : update_option( 'WPLANG', $language );
+ break;
+
+ case 'monitor_receive_notifications':
+ $monitor = new Jetpack_Monitor();
+
+ // If we got true as response, consider it done.
+ $updated = true === $monitor->update_option_receive_jetpack_monitor_notification( $value );
+ break;
+
+ case 'post_by_email_address':
+ if ( 'create' == $value ) {
+ $result = $this->_process_post_by_email(
+ 'jetpack.createPostByEmailAddress',
+ esc_html__( 'Unable to create the Post by Email address. Please try again later.', 'jetpack' )
+ );
+ } elseif ( 'regenerate' == $value ) {
+ $result = $this->_process_post_by_email(
+ 'jetpack.regeneratePostByEmailAddress',
+ esc_html__( 'Unable to regenerate the Post by Email address. Please try again later.', 'jetpack' )
+ );
+ } elseif ( 'delete' == $value ) {
+ $result = $this->_process_post_by_email(
+ 'jetpack.deletePostByEmailAddress',
+ esc_html__( 'Unable to delete the Post by Email address. Please try again later.', 'jetpack' )
+ );
+ } else {
+ $result = false;
+ }
+
+ // If we got an email address (create or regenerate) or 1 (delete), consider it done.
+ if ( is_string( $result ) && preg_match( '/[a-z0-9]+@post.wordpress.com/', $result ) ) {
+ $response[$option] = $result;
+ $updated = true;
+ } elseif ( 1 == $result ) {
+ $updated = true;
+ } elseif ( is_array( $result ) && isset( $result['message'] ) ) {
+ $error = $result['message'];
+ }
+ break;
+
+ case 'jetpack_protect_key':
+ $protect = Jetpack_Protect_Module::instance();
+ if ( 'create' == $value ) {
+ $result = $protect->get_protect_key();
+ } else {
+ $result = false;
+ }
+
+ // If we got one of Protect keys, consider it done.
+ if ( preg_match( '/[a-z0-9]{40,}/i', $result ) ) {
+ $response[$option] = $result;
+ $updated = true;
+ }
+ break;
+
+ case 'jetpack_protect_global_whitelist':
+ $updated = jetpack_protect_save_whitelist( explode( PHP_EOL, str_replace( array( ' ', ',' ), array( '', "\n" ), $value ) ) );
+ if ( is_wp_error( $updated ) ) {
+ $error = $updated->get_error_message();
+ }
+ break;
+
+ case 'show_headline':
+ case 'show_thumbnails':
+ $grouped_options = $grouped_options_current = (array) Jetpack_Options::get_option( 'relatedposts' );
+ $grouped_options[$option] = $value;
+
+ // If option value was the same, consider it done.
+ $updated = $grouped_options_current != $grouped_options ? Jetpack_Options::update_option( 'relatedposts', $grouped_options ) : true;
+ break;
+
+ case 'google':
+ case 'bing':
+ case 'pinterest':
+ case 'yandex':
+ $grouped_options = $grouped_options_current = (array) get_option( 'verification_services_codes' );
+
+ // Extracts the content attribute from the HTML meta tag if needed
+ if ( preg_match( '#.*<meta name="(?:[^"]+)" content="([^"]+)" />.*#i', $value, $matches ) ) {
+ $grouped_options[ $option ] = $matches[1];
+ } else {
+ $grouped_options[ $option ] = $value;
+ }
+
+ // If option value was the same, consider it done.
+ $updated = $grouped_options_current != $grouped_options ? update_option( 'verification_services_codes', $grouped_options ) : true;
+ break;
+
+ case 'sharing_services':
+ if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
+ break;
+ }
+
+ $sharer = new Sharing_Service();
+
+ // If option value was the same, consider it done.
+ $updated = $value != $sharer->get_blog_services() ? $sharer->set_blog_services( $value['visible'], $value['hidden'] ) : true;
+ break;
+
+ case 'button_style':
+ case 'sharing_label':
+ case 'show':
+ if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
+ break;
+ }
+
+ $sharer = new Sharing_Service();
+ $grouped_options = $sharer->get_global_options();
+ $grouped_options[ $option ] = $value;
+ $updated = $sharer->set_global_options( $grouped_options );
+ break;
+
+ case 'custom':
+ if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
+ break;
+ }
+
+ $sharer = new Sharing_Service();
+ $updated = $sharer->new_service( stripslashes( $value['sharing_name'] ), stripslashes( $value['sharing_url'] ), stripslashes( $value['sharing_icon'] ) );
+
+ // Return new custom service
+ $response[$option] = $updated;
+ break;
+
+ case 'sharing_delete_service':
+ if ( ! class_exists( 'Sharing_Service' ) && ! include_once( JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) ) {
+ break;
+ }
+
+ $sharer = new Sharing_Service();
+ $updated = $sharer->delete_service( $value );
+ break;
+
+ case 'jetpack-twitter-cards-site-tag':
+ $value = trim( ltrim( strip_tags( $value ), '@' ) );
+ $updated = get_option( $option ) !== $value ? update_option( $option, $value ) : true;
+ break;
+
+ case 'admin_bar':
+ case 'roles':
+ case 'count_roles':
+ case 'blog_id':
+ case 'do_not_track':
+ case 'hide_smile':
+ case 'version':
+ $grouped_options = $grouped_options_current = (array) get_option( 'stats_options' );
+ $grouped_options[$option] = $value;
+
+ // If option value was the same, consider it done.
+ $updated = $grouped_options_current != $grouped_options ? update_option( 'stats_options', $grouped_options ) : true;
+ break;
+
+ case 'akismet_show_user_comments_approved':
+
+ // Save Akismet option '1' or '0' like it's done in akismet/class.akismet-admin.php
+ $updated = get_option( $option ) != $value ? update_option( $option, (bool) $value ? '1' : '0' ) : true;
+ break;
+
+ case 'wordpress_api_key':
+
+ if ( ! file_exists( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
+ $error = esc_html__( 'Please install Akismet.', 'jetpack' );
+ $updated = false;
+ break;
+ }
+
+ if ( ! defined( 'AKISMET_VERSION' ) ) {
+ $error = esc_html__( 'Please activate Akismet.', 'jetpack' );
+ $updated = false;
+ break;
+ }
+
+ // Allow to clear the API key field
+ if ( '' === $value ) {
+ $updated = get_option( $option ) != $value ? update_option( $option, $value ) : true;
+ break;
+ }
+
+ require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
+ require_once WP_PLUGIN_DIR . '/akismet/class.akismet-admin.php';
+
+ if ( class_exists( 'Akismet_Admin' ) && method_exists( 'Akismet_Admin', 'save_key' ) ) {
+ if ( Akismet::verify_key( $value ) === 'valid' ) {
+ $akismet_user = Akismet_Admin::get_akismet_user( $value );
+ if ( $akismet_user ) {
+ if ( in_array( $akismet_user->status, array( 'active', 'active-dunning', 'no-sub' ) ) ) {
+ $updated = get_option( $option ) != $value ? update_option( $option, $value ) : true;
+ break;
+ } else {
+ $error = esc_html__( "Akismet user status doesn't allow to update the key", 'jetpack' );
+ }
+ } else {
+ $error = esc_html__( 'Invalid Akismet user', 'jetpack' );
+ }
+ } else {
+ $error = esc_html__( 'Invalid Akismet key', 'jetpack' );
+ }
+ } else {
+ $error = esc_html__( 'Akismet is not installed or active', 'jetpack' );
+ }
+ $updated = false;
+ break;
+
+ case 'google_analytics_tracking_id':
+ $grouped_options = $grouped_options_current = (array) get_option( 'jetpack_wga' );
+ $grouped_options[ 'code' ] = $value;
+
+ // If option value was the same, consider it done.
+ $updated = $grouped_options_current != $grouped_options ? update_option( 'jetpack_wga', $grouped_options ) : true;
+ break;
+
+ case 'dismiss_dash_app_card':
+ case 'dismiss_empty_stats_card':
+ // If option value was the same, consider it done.
+ $updated = get_option( $option ) != $value ? update_option( $option, (bool) $value ) : true;
+ break;
+
+ case 'onboarding':
+ jetpack_require_lib( 'widgets' );
+ // Break apart and set Jetpack onboarding options.
+ $result = $this->_process_onboarding( (array) $value );
+ if ( empty( $result ) ) {
+ $updated = true;
+ } else {
+ $error = sprintf( esc_html__( 'Onboarding failed to process: %s', 'jetpack' ), $result );
+ $updated = false;
+ }
+ break;
+
+ default:
+ // If option value was the same, consider it done.
+ $updated = get_option( $option ) != $value ? update_option( $option, $value ) : true;
+ break;
+ }
+
+ // The option was not updated.
+ if ( ! $updated ) {
+ $not_updated[ $option ] = $error;
+ }
+ }
+
+ if ( empty( $invalid ) && empty( $not_updated ) ) {
+ // The option was updated.
+ return rest_ensure_response( $response );
+ } else {
+ $invalid_count = count( $invalid );
+ $not_updated_count = count( $not_updated );
+ $error = '';
+ if ( $invalid_count > 0 ) {
+ $error = sprintf(
+ /* Translators: the plural variable is a comma-separated list. Example: dog, cat, bird. */
+ _n( 'Invalid option: %s.', 'Invalid options: %s.', $invalid_count, 'jetpack' ),
+ join( ', ', $invalid )
+ );
+ }
+ if ( $not_updated_count > 0 ) {
+ $not_updated_messages = array();
+ foreach ( $not_updated as $not_updated_option => $not_updated_message ) {
+ if ( ! empty( $not_updated_message ) ) {
+ $not_updated_messages[] = sprintf(
+ /* Translators: the first variable is a module option or slug, or setting. The second is the error message . */
+ __( '%1$s: %2$s', 'jetpack' ),
+ $not_updated_option, $not_updated_message );
+ }
+ }
+ if ( ! empty( $error ) ) {
+ $error .= ' ';
+ }
+ if ( ! empty( $not_updated_messages ) ) {
+ $error .= ' ' . join( '. ', $not_updated_messages );
+ }
+
+ }
+ // There was an error because some options were updated but others were invalid or failed to update.
+ return new WP_Error( 'some_updated', esc_html( $error ), array( 'status' => 400 ) );
+ }
+
+ }
+
+ /**
+ * Perform tasks in the site based on onboarding choices.
+ *
+ * @since 5.4.0
+ *
+ * @param array $data Onboarding choices made by user.
+ *
+ * @return string Result of onboarding processing and, if there is one, an error message.
+ */
+ private function _process_onboarding( $data ) {
+ if ( isset( $data['end'] ) && $data['end'] ) {
+ return Jetpack::invalidate_onboarding_token()
+ ? ''
+ : esc_html__( "The onboarding token couldn't be deleted.", 'jetpack' );
+ }
+
+ $error = array();
+
+ if ( ! empty( $data['siteTitle'] ) ) {
+ // If option value was the same, consider it done.
+ if ( ! ( update_option( 'blogname', $data['siteTitle'] ) || get_option( 'blogname' ) == $data['siteTitle'] ) ) {
+ $error[] = 'siteTitle';
+ }
+ }
+
+ if ( isset( $data['siteDescription'] ) ) {
+ // If option value was the same, consider it done.
+ if ( ! ( update_option( 'blogdescription', $data['siteDescription'] ) || get_option( 'blogdescription' ) == $data['siteDescription'] ) ) {
+ $error[] = 'siteDescription';
+ }
+ }
+
+ $site_title = get_option( 'blogname' );
+ $author = get_current_user_id() || 1;
+
+ if ( ! empty( $data['siteType'] ) ) {
+ if ( ! ( update_option( 'jpo_site_type', $data['siteType'] ) || get_option( 'jpo_site_type' ) == $data['siteType'] ) ) {
+ $error[] = 'siteType';
+ }
+ }
+
+ if ( isset( $data['homepageFormat'] ) ) {
+ // If $data['homepageFormat'] is 'posts', we have nothing to do since it's WordPress' default
+ // if it exists, just update
+ $homepage_format = get_option( 'jpo_homepage_format' );
+ if ( ! $homepage_format || $homepage_format !== $data['homepageFormat'] ) {
+ if ( 'page' === $data['homepageFormat'] ) {
+ if ( ! ( update_option( 'show_on_front', 'page' ) || get_option( 'show_on_front' ) == 'page' ) ) {
+ $error[] = 'homepageFormat';
+ }
+
+ $home = wp_insert_post( array(
+ 'post_type' => 'page',
+ /* translators: this references the home page of a site, also called front page. */
+ 'post_title' => esc_html_x( 'Home Page', 'The home page of a website.', 'jetpack' ),
+ 'post_content' => sprintf( esc_html__( 'Welcome to %s.', 'jetpack' ), $site_title ),
+ 'post_status' => 'publish',
+ 'post_author' => $author,
+ ) );
+ if ( 0 == $home ) {
+ $error[] = 'home insert: 0';
+ } elseif ( is_wp_error( $home ) ) {
+ $error[] = 'home creation: '. $home->get_error_message();
+ }
+ if ( ! ( update_option( 'page_on_front', $home ) || get_option( 'page_on_front' ) == $home ) ) {
+
+ $error[] = 'home set';
+ }
+
+ $blog = wp_insert_post( array(
+ 'post_type' => 'page',
+ /* translators: this references the page where blog posts are listed. */
+ 'post_title' => esc_html_x( 'Blog', 'The blog of a website.', 'jetpack' ),
+ 'post_content' => sprintf( esc_html__( 'These are the latest posts in %s.', 'jetpack' ), $site_title ),
+ 'post_status' => 'publish',
+ 'post_author' => $author,
+ ) );
+ if ( 0 == $blog ) {
+ $error[] = 'blog insert: 0';
+ } elseif ( is_wp_error( $blog ) ) {
+ $error[] = 'blog creation: '. $blog->get_error_message();
+ }
+ if ( ! ( update_option( 'page_for_posts', $blog ) || get_option( 'page_for_posts' ) == $blog ) ) {
+ $error[] = 'blog set';
+ }
+ } else {
+ $front_page = get_option( 'page_on_front' );
+ $posts_page = get_option( 'page_for_posts' );
+ if ( $posts_page && get_post( $posts_page ) ) {
+ wp_delete_post( $posts_page );
+ }
+ if ( $front_page && get_post( $front_page ) ) {
+ wp_delete_post( $front_page );
+ }
+ update_option( 'show_on_front', 'posts' );
+ }
+ }
+ update_option( 'jpo_homepage_format', $data['homepageFormat'] );
+ }
+
+ // Setup contact page and add a form and/or business info
+ $contact_page = '';
+ if ( ! empty( $data['addContactForm'] ) && ! get_option( 'jpo_contact_page' ) ) {
+ $contact_form_module_active = Jetpack::is_module_active( 'contact-form' );
+ if ( ! $contact_form_module_active ) {
+ $contact_form_module_active = Jetpack::activate_module( 'contact-form', false, false );
+ }
+
+ if ( $contact_form_module_active ) {
+ $contact_page = '[contact-form][contact-field label="' . esc_html__( 'Name', 'jetpack' ) . '" type="name" required="true" /][contact-field label="' . esc_html__( 'Email', 'jetpack' ) . '" type="email" required="true" /][contact-field label="' . esc_html__( 'Website', 'jetpack' ) . '" type="url" /][contact-field label="' . esc_html__( 'Message', 'jetpack' ) . '" type="textarea" /][/contact-form]';
+ } else {
+ $error[] = 'contact-form activate';
+ }
+ }
+
+ if ( isset( $data['businessPersonal'] ) && 'business' === $data['businessPersonal'] ) {
+ $contact_page .= "\n" . join( "\n", $data['businessInfo'] );
+ }
+
+ if ( ! empty( $contact_page ) ) {
+ $form = wp_insert_post( array(
+ 'post_type' => 'page',
+ /* translators: this references a page with contact details and possibly a form. */
+ 'post_title' => esc_html_x( 'Contact us', 'Contact page for your website.', 'jetpack' ),
+ 'post_content' => esc_html__( 'Send us a message!', 'jetpack' ) . "\n" . $contact_page,
+ 'post_status' => 'publish',
+ 'post_author' => $author,
+ ) );
+ if ( 0 == $form ) {
+ $error[] = 'form insert: 0';
+ } elseif ( is_wp_error( $form ) ) {
+ $error[] = 'form creation: '. $form->get_error_message();
+ } else {
+ update_option( 'jpo_contact_page', $form );
+ }
+ }
+
+ if ( isset( $data['businessAddress'] ) ) {
+ $handled_business_address = self::handle_business_address( $data['businessAddress'] );
+ if ( is_wp_error( $handled_business_address ) ) {
+ $error[] = 'BusinessAddress';
+ }
+ }
+
+ if ( ! empty( $data['installWooCommerce'] ) ) {
+ jetpack_require_lib( 'plugins' );
+ $wc_install_result = Jetpack_Plugins::install_and_activate_plugin( 'woocommerce' );
+ delete_transient( '_wc_activation_redirect' ); // Redirecting to WC setup would kill our users' flow
+ if ( is_wp_error( $wc_install_result ) ) {
+ $error[] = 'woocommerce installation';
+ }
+ }
+
+ if ( ! empty( $data['stats'] ) ) {
+ if ( Jetpack::is_active() ) {
+ $stats_module_active = Jetpack::is_module_active( 'stats' );
+ if ( ! $stats_module_active ) {
+ $stats_module_active = Jetpack::activate_module( 'stats', false, false );
+ }
+
+ if ( ! $stats_module_active ) {
+ $error[] = 'stats activate';
+ }
+ } else {
+ $error[] = 'stats not connected';
+ }
+ }
+
+ return empty( $error )
+ ? ''
+ : join( ', ', $error );
+ }
+
+ /**
+ * Add or update Business Address widget.
+ *
+ * @param array $address Array of business address fields.
+ *
+ * @return WP_Error|true True if the data was saved correctly.
+ */
+ static function handle_business_address( $address ) {
+ $first_sidebar = Jetpack_Widgets::get_first_sidebar();
+
+ $widgets_module_active = Jetpack::is_module_active( 'widgets' );
+ if ( ! $widgets_module_active ) {
+ $widgets_module_active = Jetpack::activate_module( 'widgets', false, false );
+ }
+ if ( ! $widgets_module_active ) {
+ return new WP_Error( 'module_activation_failed', 'Failed to activate the widgets module.', 400 );
+ }
+
+ if ( $first_sidebar ) {
+ $title = isset( $address['name'] ) ? sanitize_text_field( $address['name'] ) : '';
+ $street = isset( $address['street'] ) ? sanitize_text_field( $address['street'] ) : '';
+ $city = isset( $address['city'] ) ? sanitize_text_field( $address['city'] ) : '';
+ $state = isset( $address['state'] ) ? sanitize_text_field( $address['state'] ) : '';
+ $zip = isset( $address['zip'] ) ? sanitize_text_field( $address['zip'] ) : '';
+ $country = isset( $address['country'] ) ? sanitize_text_field( $address['country'] ) : '';
+
+ $full_address = implode( ' ', array_filter( array( $street, $city, $state, $zip, $country ) ) );
+
+ $widget_options = array(
+ 'title' => $title,
+ 'address' => $full_address,
+ 'phone' => '',
+ 'hours' => '',
+ 'showmap' => false,
+ 'email' => ''
+ );
+
+ $widget_updated = '';
+ if ( ! self::has_business_address_widget( $first_sidebar ) ) {
+ $widget_updated = Jetpack_Widgets::insert_widget_in_sidebar( 'widget_contact_info', $widget_options, $first_sidebar );
+ } else {
+ $widget_updated = Jetpack_Widgets::update_widget_in_sidebar( 'widget_contact_info', $widget_options, $first_sidebar );
+ }
+ if ( is_wp_error( $widget_updated ) ) {
+ return new WP_Error( 'widget_update_failed', 'Widget could not be updated.', 400 );
+ }
+
+ $address_save = array(
+ 'name' => $title,
+ 'street' => $street,
+ 'city' => $city,
+ 'state' => $state,
+ 'zip' => $zip,
+ 'country' => $country
+ );
+ update_option( 'jpo_business_address', $address_save );
+ return true;
+ }
+
+ // No sidebar to place the widget
+ return new WP_Error( 'sidebar_not_found', 'No sidebar.', 400 );
+ }
+
+ /**
+ * Check whether "Contact Info & Map" widget is present in a given sidebar.
+ *
+ * @param string $sidebar ID of the sidebar to which the widget will be added.
+ *
+ * @return bool Whether the widget is present in a given sidebar.
+ */
+ static function has_business_address_widget( $sidebar ) {
+ $sidebars_widgets = get_option( 'sidebars_widgets', array() );
+ if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
+ return false;
+ }
+ foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
+ if ( strpos( $widget, 'widget_contact_info' ) !== false ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Calls WPCOM through authenticated request to create, regenerate or delete the Post by Email address.
+ * @todo: When all settings are updated to use endpoints, move this to the Post by Email module and replace __process_ajax_proxy_request.
+ *
+ * @since 4.3.0
+ *
+ * @param string $endpoint Process to call on WPCOM to create, regenerate or delete the Post by Email address.
+ * @param string $error Error message to return.
+ *
+ * @return array
+ */
+ private function _process_post_by_email( $endpoint, $error ) {
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ return array( 'message' => $error );
+ }
+
+ $this->xmlrpc->query( $endpoint );
+
+ if ( $this->xmlrpc->isError() ) {
+ return array( 'message' => $error );
+ }
+
+ $response = $this->xmlrpc->getResponse();
+ if ( empty( $response ) ) {
+ return array( 'message' => $error );
+ }
+
+ // Used only in Jetpack_Core_Json_Api_Endpoints::get_remote_value.
+ update_option( 'post_by_email_address' . get_current_user_id(), $response );
+
+ return $response;
+ }
+
+ /**
+ * Check if user is allowed to perform the update.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_REST_Request $request The request sent to the WP REST API.
+ *
+ * @return bool
+ */
+ public function can_request( $request ) {
+ $req_params = $request->get_params();
+ if ( ! empty( $req_params['onboarding']['token'] ) && isset( $req_params['rest_route'] ) ) {
+ return Jetpack::validate_onboarding_token_action( $req_params['onboarding']['token'], $req_params['rest_route'] );
+ }
+
+ if ( 'GET' === $request->get_method() ) {
+ return current_user_can( 'jetpack_admin_page' );
+ } else {
+ $module = Jetpack_Core_Json_Api_Endpoints::get_module_requested();
+ if ( empty( $module ) ) {
+ $params = $request->get_json_params();
+ if ( ! is_array( $params ) ) {
+ $params = $request->get_body_params();
+ }
+ $options = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( $params );
+ foreach ( $options as $option => $definition ) {
+ if ( in_array( $options[ $option ]['jp_group'], array( 'post-by-email' ) ) ) {
+ $module = $options[ $option ]['jp_group'];
+ break;
+ }
+ }
+ }
+ // User is trying to create, regenerate or delete its PbE.
+ if ( 'post-by-email' === $module ) {
+ return current_user_can( 'edit_posts' ) && current_user_can( 'jetpack_admin_page' );
+ }
+ return current_user_can( 'jetpack_configure_modules' );
+ }
+ }
+}
+
+class Jetpack_Core_API_Module_Data_Endpoint {
+
+ public function process( $request ) {
+ switch( $request['slug'] ) {
+ case 'protect':
+ return $this->get_protect_data();
+ case 'stats':
+ return $this->get_stats_data( $request );
+ case 'akismet':
+ return $this->get_akismet_data();
+ case 'monitor':
+ return $this->get_monitor_data();
+ case 'verification-tools':
+ return $this->get_verification_tools_data();
+ case 'vaultpress':
+ return $this->get_vaultpress_data();
+ }
+ }
+
+ /**
+ * Decide against which service to check the key.
+ *
+ * @since 4.8.0
+ *
+ * @param WP_REST_Request $request
+ *
+ * @return bool
+ */
+ public function key_check( $request ) {
+ switch( $request['service'] ) {
+ case 'akismet':
+ $params = $request->get_json_params();
+ if ( isset( $params['api_key'] ) && ! empty( $params['api_key'] ) ) {
+ return $this->check_akismet_key( $params['api_key'] );
+ }
+ return $this->check_akismet_key();
+ }
+ return false;
+ }
+
+ /**
+ * Get number of blocked intrusion attempts.
+ *
+ * @since 4.3.0
+ *
+ * @return mixed|WP_Error Number of blocked attempts if protection is enabled. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public function get_protect_data() {
+ if ( Jetpack::is_module_active( 'protect' ) ) {
+ return get_site_option( 'jetpack_protect_blocked_attempts' );
+ }
+
+ return new WP_Error(
+ 'not_active',
+ esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ /**
+ * Get number of spam messages blocked by Akismet.
+ *
+ * @since 4.3.0
+ *
+ * @return int|string Number of spam blocked by Akismet. Otherwise, an error message.
+ */
+ public function get_akismet_data() {
+ if ( ! is_wp_error( $status = $this->akismet_is_active_and_registered() ) ) {
+ return rest_ensure_response( Akismet_Admin::get_stats( Akismet::get_api_key() ) );
+ } else {
+ return $status->get_error_code();
+ }
+ }
+
+ /**
+ * Verify the Akismet API key.
+ *
+ * @since 4.8.0
+ *
+ * @param string $api_key Optional API key to check.
+ *
+ * @return array Information about the key. 'validKey' is true if key is valid, false otherwise.
+ */
+ public function check_akismet_key( $api_key = '' ) {
+ $akismet_status = $this->akismet_class_exists();
+ if ( is_wp_error( $akismet_status ) ) {
+ return rest_ensure_response( array(
+ 'validKey' => false,
+ 'invalidKeyCode' => $akismet_status->get_error_code(),
+ 'invalidKeyMessage' => $akismet_status->get_error_message(),
+ ) );
+ }
+
+ $key_status = Akismet::check_key_status( empty( $api_key ) ? Akismet::get_api_key() : $api_key );
+
+ if ( ! $key_status || 'invalid' === $key_status || 'failed' === $key_status ) {
+ return rest_ensure_response( array(
+ 'validKey' => false,
+ 'invalidKeyCode' => 'invalid_key',
+ 'invalidKeyMessage' => esc_html__( 'Invalid Akismet key. Please contact support.', 'jetpack' ),
+ ) );
+ }
+
+ return rest_ensure_response( array(
+ 'validKey' => isset( $key_status[1] ) && 'valid' === $key_status[1]
+ ) );
+ }
+
+ /**
+ * Check if Akismet class file exists and if class is loaded.
+ *
+ * @since 4.8.0
+ *
+ * @return bool|WP_Error Returns true if class file exists and class is loaded, WP_Error otherwise.
+ */
+ private function akismet_class_exists() {
+ if ( ! file_exists( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
+ return new WP_Error( 'not_installed', esc_html__( 'Please install Akismet.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ if ( ! class_exists( 'Akismet' ) ) {
+ return new WP_Error( 'not_active', esc_html__( 'Please activate Akismet.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Is Akismet registered and active?
+ *
+ * @since 4.3.0
+ *
+ * @return bool|WP_Error True if Akismet is active and registered. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ private function akismet_is_active_and_registered() {
+ if ( is_wp_error( $akismet_exists = $this->akismet_class_exists() ) ) {
+ return $akismet_exists;
+ }
+
+ // What about if Akismet is put in a sub-directory or maybe in mu-plugins?
+ require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
+ require_once WP_PLUGIN_DIR . '/akismet/class.akismet-admin.php';
+ $akismet_key = Akismet::verify_key( Akismet::get_api_key() );
+
+ if ( ! $akismet_key || 'invalid' === $akismet_key || 'failed' === $akismet_key ) {
+ return new WP_Error( 'invalid_key', esc_html__( 'Invalid Akismet key. Please contact support.', 'jetpack' ), array( 'status' => 400 ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Get stats data for this site
+ *
+ * @since 4.1.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $date Date range to restrict results to.
+ * }
+ *
+ * @return WP_Error|WP_HTTP_Response|WP_REST_Response Stats information relayed from WordPress.com.
+ */
+ public function get_stats_data( WP_REST_Request $request ) {
+ // Get parameters to fetch Stats data.
+ $range = $request->get_param( 'range' );
+
+ // If no parameters were passed.
+ if (
+ empty ( $range )
+ || ! in_array( $range, array( 'day', 'week', 'month' ), true )
+ ) {
+ $range = 'day';
+ }
+
+ if ( ! function_exists( 'stats_get_from_restapi' ) ) {
+ require_once( JETPACK__PLUGIN_DIR . 'modules/stats.php' );
+ }
+
+ switch ( $range ) {
+
+ // This is always called first on page load
+ case 'day':
+ $initial_stats = stats_get_from_restapi();
+ return rest_ensure_response( array(
+ 'general' => $initial_stats,
+
+ // Build data for 'day' as if it was stats_get_from_restapi( array(), 'visits?unit=day&quantity=30' );
+ 'day' => isset( $initial_stats->visits )
+ ? $initial_stats->visits
+ : array(),
+ ) );
+ case 'week':
+ return rest_ensure_response( array(
+ 'week' => stats_get_from_restapi( array(), 'visits?unit=week&quantity=14' ),
+ ) );
+ case 'month':
+ return rest_ensure_response( array(
+ 'month' => stats_get_from_restapi( array(), 'visits?unit=month&quantity=12&' ),
+ ) );
+ }
+ }
+
+ /**
+ * Get date of last downtime.
+ *
+ * @since 4.3.0
+ *
+ * @return mixed|WP_Error Number of days since last downtime. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public function get_monitor_data() {
+ if ( ! Jetpack::is_module_active( 'monitor' ) ) {
+ return new WP_Error(
+ 'not_active',
+ esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $monitor = new Jetpack_Monitor();
+ $last_downtime = $monitor->monitor_get_last_downtime();
+ if ( is_wp_error( $last_downtime ) ) {
+ return $last_downtime;
+ } else if ( false === strtotime( $last_downtime ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'date' => null,
+ ) );
+ } else {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'date' => human_time_diff( strtotime( $last_downtime ), strtotime( 'now' ) ),
+ ) );
+ }
+ }
+
+ /**
+ * Get services that this site is verified with.
+ *
+ * @since 4.3.0
+ *
+ * @return mixed|WP_Error List of services that verified this site. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public function get_verification_tools_data() {
+ if ( ! Jetpack::is_module_active( 'verification-tools' ) ) {
+ return new WP_Error(
+ 'not_active',
+ esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $verification_services_codes = get_option( 'verification_services_codes' );
+ if (
+ ! is_array( $verification_services_codes )
+ || empty( $verification_services_codes )
+ ) {
+ return new WP_Error(
+ 'empty',
+ esc_html__( 'Site not verified with any service.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $services = array();
+ foreach ( jetpack_verification_services() as $name => $service ) {
+ if ( is_array( $service ) && ! empty( $verification_services_codes[ $name ] ) ) {
+ switch ( $name ) {
+ case 'google':
+ $services[] = 'Google';
+ break;
+ case 'bing':
+ $services[] = 'Bing';
+ break;
+ case 'pinterest':
+ $services[] = 'Pinterest';
+ break;
+ case 'yandex':
+ $services[] = 'Yandex';
+ break;
+ }
+ }
+ }
+
+ if ( empty( $services ) ) {
+ return new WP_Error(
+ 'empty',
+ esc_html__( 'Site not verified with any service.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ if ( 2 > count( $services ) ) {
+ $message = esc_html(
+ sprintf(
+ /* translators: %s is a service name like Google, Bing, Pinterest, etc. */
+ __( 'Your site is verified with %s.', 'jetpack' ),
+ $services[0]
+ )
+ );
+ } else {
+ $copy_services = $services;
+ $last = count( $copy_services ) - 1;
+ $last_service = $copy_services[ $last ];
+ unset( $copy_services[ $last ] );
+ $message = esc_html(
+ sprintf(
+ /* translators: %1$s is a comma separated list of services, and %2$s is a single service name like Google, Bing, Pinterest, etc. */
+ __( 'Your site is verified with %1$s and %2$s.', 'jetpack' ),
+ join( ', ', $copy_services ),
+ $last_service
+ )
+ );
+ }
+
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => $message,
+ 'services' => $services,
+ ) );
+ }
+
+ /**
+ * Get VaultPress site data including, among other things, the date of the last backup if it was completed.
+ *
+ * @since 4.3.0
+ *
+ * @return mixed|WP_Error VaultPress site data. Otherwise, a WP_Error instance with the corresponding error.
+ */
+ public function get_vaultpress_data() {
+ if ( ! class_exists( 'VaultPress' ) ) {
+ return new WP_Error(
+ 'not_active',
+ esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $vaultpress = new VaultPress();
+ if ( ! $vaultpress->is_registered() ) {
+ return rest_ensure_response( array(
+ 'code' => 'not_registered',
+ 'message' => esc_html__( 'You need to register for VaultPress.', 'jetpack' )
+ ) );
+ }
+
+ $data = json_decode( base64_decode( $vaultpress->contact_service( 'plugin_data' ) ) );
+ if ( false == $data ) {
+ return rest_ensure_response( array(
+ 'code' => 'not_registered',
+ 'message' => esc_html__( 'Could not connect to VaultPress.', 'jetpack' )
+ ) );
+ } else if ( is_wp_error( $data ) || ! isset( $data->backups->last_backup ) ) {
+ return $data;
+ } else if ( empty( $data->backups->last_backup ) ) {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'VaultPress is active and will back up your site soon.', 'jetpack' ),
+ 'data' => $data,
+ ) );
+ } else {
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html(
+ sprintf(
+ __( 'Your site was successfully backed-up %s ago.', 'jetpack' ),
+ human_time_diff(
+ $data->backups->last_backup,
+ current_time( 'timestamp' )
+ )
+ )
+ ),
+ 'data' => $data,
+ ) );
+ }
+ }
+
+ /**
+ * A WordPress REST API permission callback method that accepts a request object and
+ * decides if the current user has enough privileges to act.
+ *
+ * @since 4.3.0
+ *
+ * @return bool does a current user have enough privileges.
+ */
+ public function can_request() {
+ return current_user_can( 'jetpack_admin_page' );
+ }
+}
+
+/**
+ * Actions performed only when Gravatar Hovercards is activated through the endpoint call.
+ *
+ * @since 4.3.1
+ */
+function jetpack_do_after_gravatar_hovercards_activation() {
+
+ // When Gravatar Hovercards is activated, enable them automatically.
+ update_option( 'gravatar_disable_hovercards', 'enabled' );
+}
+add_action( 'jetpack_activate_module_gravatar-hovercards', 'jetpack_do_after_gravatar_hovercards_activation' );
+
+/**
+ * Actions performed only when Gravatar Hovercards is activated through the endpoint call.
+ *
+ * @since 4.3.1
+ */
+function jetpack_do_after_gravatar_hovercards_deactivation() {
+
+ // When Gravatar Hovercards is deactivated, disable them automatically.
+ update_option( 'gravatar_disable_hovercards', 'disabled' );
+}
+add_action( 'jetpack_deactivate_module_gravatar-hovercards', 'jetpack_do_after_gravatar_hovercards_deactivation' );
+
+/**
+ * Actions performed only when Markdown is activated through the endpoint call.
+ *
+ * @since 4.7.0
+ */
+function jetpack_do_after_markdown_activation() {
+
+ // When Markdown is activated, enable support for post editing automatically.
+ update_option( 'wpcom_publish_posts_with_markdown', true );
+}
+add_action( 'jetpack_activate_module_markdown', 'jetpack_do_after_markdown_activation' );
diff --git a/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php
new file mode 100644
index 00000000..68327f51
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * This is the endpoint class for `/site` endpoints.
+ *
+ */
+class Jetpack_Core_API_Site_Endpoint {
+
+ /**
+ * Returns the result of `/sites/%s/features` endpoint call.
+ * @return object $features has 'active' and 'available' properties each of which contain feature slugs.
+ * 'active' is a simple array of slugs that are active on the current plan.
+ * 'available' is an object with keys that represent feature slugs and values are arrays
+ * of plan slugs that enable these features
+ */
+ public static function get_features() {
+
+ // Make the API request
+ $request = sprintf( '/sites/%d/features', Jetpack_Options::get_option( 'id' ) );
+ $response = Jetpack_Client::wpcom_json_api_request_as_blog( $request, '1.1' );
+
+ // Bail if there was an error or malformed response
+ if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
+ return new WP_Error(
+ 'failed_to_fetch_data',
+ esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ // Decode the results
+ $results = json_decode( $response['body'], true );
+
+ // Bail if there were no results or plan details returned
+ if ( ! is_array( $results ) ) {
+ return new WP_Error(
+ 'failed_to_fetch_data',
+ esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Site features correctly received.', 'jetpack' ),
+ 'data' => wp_remote_retrieve_body( $response ),
+ )
+ );
+ }
+
+ /**
+ * Check that the current user has permissions to request information about this site.
+ *
+ * @since 5.1.0
+ *
+ * @return bool
+ */
+ public static function can_request() {
+ return current_user_can( 'jetpack_manage_modules' );
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php
new file mode 100644
index 00000000..ffd62bb3
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Widget information getter endpoint.
+ *
+ */
+class Jetpack_Core_API_Widget_Endpoint {
+
+ /**
+ * @since 5.5.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $id Widget id.
+ * }
+ *
+ * @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
+ */
+ public function process( $request ) {
+ $widget_base = _get_widget_id_base( $request['id'] );
+ $widget_id = (int) substr( $request['id'], strlen( $widget_base ) + 1 );
+
+ switch( $widget_base ) {
+ case 'milestone_widget':
+ $instances = get_option( 'widget_milestone_widget', array() );
+
+ if (
+ class_exists( 'Milestone_Widget' )
+ && is_active_widget( false, $widget_base . '-' . $widget_id, $widget_base )
+ && isset( $instances[ $widget_id ] )
+ ) {
+ $instance = $instances[ $widget_id ];
+ $widget = new Milestone_Widget();
+ return $widget->get_widget_data( $instance );
+ }
+ }
+
+ return new WP_Error(
+ 'not_found',
+ esc_html__( 'The requested widget was not found.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ /**
+ * Check that the current user has permissions to view widget information.
+ * For the currently supported widget there are no permissions required.
+ *
+ * @since 5.5.0
+ *
+ * @return bool
+ */
+ public function can_request() {
+ return true;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php
new file mode 100644
index 00000000..abfc8627
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * This is the base class for every Core API endpoint that needs an XMLRPC client.
+ *
+ */
+abstract class Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
+
+ /**
+ * An instance of the Jetpack XMLRPC client to make WordPress.com requests
+ *
+ * @private
+ * @var Jetpack_IXR_Client
+ */
+ protected $xmlrpc;
+
+ /**
+ *
+ * @since 4.3.0
+ *
+ * @param Jetpack_IXR_Client $xmlrpc
+ */
+ public function __construct( $xmlrpc = null ) {
+ $this->xmlrpc = $xmlrpc;
+ }
+
+ /**
+ * Checks if the site is public and returns the result.
+ *
+ * @since 4.3.0
+ *
+ * @return Boolean $is_public
+ */
+ protected function is_site_public() {
+ if ( $this->xmlrpc->query( 'jetpack.isSitePubliclyAccessible', home_url() ) ) {
+ return $this->xmlrpc->getResponse();
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php b/plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php
new file mode 100644
index 00000000..2b26f78c
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php
@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * Loader for WP REST API endpoints that are synced with WP.com.
+ *
+ * On WP.com see:
+ * - wp-content/mu-plugins/rest-api.php
+ * - wp-content/rest-api-plugins/jetpack-endpoints/
+ */
+
+function wpcom_rest_api_v2_load_plugin_files( $file_pattern ) {
+ $plugins = glob( dirname( __FILE__ ) . '/' . $file_pattern );
+
+ if ( ! is_array( $plugins ) ) {
+ return;
+ }
+
+ foreach ( array_filter( $plugins, 'is_file' ) as $plugin ) {
+ require_once $plugin;
+ }
+}
+
+// API v2 plugins: define a class, then call this function.
+function wpcom_rest_api_v2_load_plugin( $class_name ) {
+ global $wpcom_rest_api_v2_plugins;
+
+ if ( ! isset( $wpcom_rest_api_v2_plugins ) ) {
+ $_GLOBALS['wpcom_rest_api_v2_plugins'] = $wpcom_rest_api_v2_plugins = array();
+ }
+
+ if ( ! isset( $wpcom_rest_api_v2_plugins[ $class_name ] ) ) {
+ $wpcom_rest_api_v2_plugins[ $class_name ] = new $class_name;
+ }
+}
+
+require dirname( __FILE__ ) . '/class-wpcom-rest-field-controller.php';
+
+// Now load the endpoint files.
+wpcom_rest_api_v2_load_plugin_files( 'wpcom-endpoints/*.php' );
+wpcom_rest_api_v2_load_plugin_files( 'wpcom-fields/*.php' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/business-hours.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/business-hours.php
new file mode 100644
index 00000000..2bf80939
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/business-hours.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Business Hours: Localized week
+ *
+ * @since 7.1
+ */
+class WPCOM_REST_API_V2_Endpoint_Business_Hours extends WP_REST_Controller {
+ function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'business-hours';
+ // This endpoint *does not* need to connect directly to Jetpack sites.
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ // GET /sites/<blog_id>/business-hours/localized-week - Return the localized
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/localized-week', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_localized_week' ),
+ )
+ ) );
+ }
+
+ /**
+ * Retreives localized business hours
+ *
+ * @return array data object containing information about business hours
+ */
+ public function get_localized_week() {
+ global $wp_locale;
+
+ return array(
+ 'days' => array(
+ 'Sun' => $wp_locale->get_weekday( 0 ),
+ 'Mon' => $wp_locale->get_weekday( 1 ),
+ 'Tue' => $wp_locale->get_weekday( 2 ),
+ 'Wed' => $wp_locale->get_weekday( 3 ),
+ 'Thu' => $wp_locale->get_weekday( 4 ),
+ 'Fri' => $wp_locale->get_weekday( 5 ),
+ 'Sat' => $wp_locale->get_weekday( 6 ),
+ ),
+ 'startOfWeek' => (int) get_option( 'start_of_week', 0 ),
+ );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Business_Hours' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php
new file mode 100644
index 00000000..354880ed
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * Mailchimp: Get Mailchimp Status.
+ * API to determine if current site has linked Mailchimp account and mailing list selected.
+ * This API is meant to be used in Jetpack and on WPCOM.
+ *
+ * @since 7.1
+ */
+class WPCOM_REST_API_V2_Endpoint_Mailchimp extends WP_REST_Controller {
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'mailchimp';
+ $this->wpcom_is_wpcom_only_endpoint = true;
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_mailchimp_status' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Check if MailChimp is set up properly.
+ *
+ * @return bool
+ */
+ private function is_connected() {
+ $option = get_option( 'jetpack_mailchimp' );
+ if ( ! $option ) {
+ return false;
+ }
+ $data = json_decode( $option, true );
+ if ( ! $data ) {
+ return false;
+ }
+ return isset( $data['follower_list_id'], $data['keyring_id'] );
+ }
+
+ /**
+ * Get the status of current blog's Mailchimp connection
+ *
+ * @return mixed
+ * code:string (connected|unconnected),
+ * connect_url:string
+ * site_id:int
+ */
+ public function get_mailchimp_status() {
+ $is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM );
+ $site_id = $is_wpcom ? get_current_blog_id() : Jetpack_Options::get_option( 'id' );
+ if ( ! $site_id ) {
+ return new WP_Error(
+ 'unavailable_site_id',
+ __( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack' ),
+ 403
+ );
+ }
+ $connect_url = sprintf( 'https://wordpress.com/marketing/connections/%s', rawurlencode( $site_id ) );
+ return array(
+ 'code' => $this->is_connected() ? 'connected' : 'not_connected',
+ 'connect_url' => $connect_url,
+ 'site_id' => $site_id,
+ );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Mailchimp' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php
new file mode 100644
index 00000000..a10a4056
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php
@@ -0,0 +1,71 @@
+<?php
+
+/*
+ * Gutenberg: List Available Gutenberg Extensions (Blocks and Plugins)
+ *
+ * [
+ * { # Availabilty Object. See schema for more detail.
+ * available: (boolean) Whether the extension is available
+ * unavailable_reason: (string) Reason for the extension not being available
+ * },
+ * ...
+ * ]
+ *
+ * @since 6.9
+ */
+class WPCOM_REST_API_V2_Endpoint_Gutenberg_Available_Extensions extends WP_REST_Controller {
+ function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'gutenberg';
+ $this->wpcom_is_site_specific_endpoint = true;
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ register_rest_route( $this->namespace, $this->rest_base . '/available-extensions', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( 'Jetpack_Gutenberg', 'get_availability' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ ),
+ 'schema' => array( $this, 'get_item_schema' ),
+ ) );
+ }
+
+ /**
+ * Return the available Gutenberg extensions schema
+ *
+ * @return array Available Gutenberg extensions schema
+ */
+ public function get_public_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'gutenberg-available-extensions',
+ 'type' => 'object',
+ 'properties' => array(
+ 'available' => array(
+ 'description' => __( 'Whether the extension is available', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ 'unavailable_reason' => array(
+ 'description' => __( 'Reason for the extension not being available', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Ensure the user has proper permissions
+ *
+ * @return boolean
+ */
+ public function get_items_permission_check() {
+ return current_user_can( 'edit_posts' );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Gutenberg_Available_Extensions' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/hello.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/hello.php
new file mode 100644
index 00000000..a05769b2
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/hello.php
@@ -0,0 +1,22 @@
+<?php
+
+class WPCOM_REST_API_V2_Endpoint_Hello {
+ public function __construct() {
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ register_rest_route( 'wpcom/v2', '/hello', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_data' ),
+ ),
+ ) );
+ }
+
+ public function get_data( $request ) {
+ return array( 'hello' => 'world' );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Hello' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php
new file mode 100644
index 00000000..ec997739
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php
@@ -0,0 +1,187 @@
+<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Memberships: API to communicate with "product" database.
+ *
+ * @package Jetpack
+ * @since 7.3.0
+ */
+
+/**
+ * Class WPCOM_REST_API_V2_Endpoint_Memberships
+ * This introduces V2 endpoints.
+ */
+class WPCOM_REST_API_V2_Endpoint_Memberships extends WP_REST_Controller {
+
+ /**
+ * WPCOM_REST_API_V2_Endpoint_Memberships constructor.
+ */
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'memberships';
+ $this->wpcom_is_wpcom_only_endpoint = true;
+ $this->wpcom_is_site_specific_endpoint = true;
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ $this->rest_base . '/status',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_status' ),
+ 'permission_callback' => array( $this, 'get_status_permission_check' ),
+ ),
+ )
+ );
+ register_rest_route(
+ $this->namespace,
+ $this->rest_base . '/product',
+ array(
+ array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'create_product' ),
+ 'permission_callback' => array( $this, 'get_status_permission_check' ),
+ 'args' => array(
+ 'title' => array(
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ 'price' => array(
+ 'type' => 'float',
+ 'required' => true,
+ ),
+ 'currency' => array(
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ 'interval' => array(
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Ensure the user has proper permissions
+ *
+ * @return boolean
+ */
+ public function get_status_permission_check() {
+ return current_user_can( 'edit_posts' );
+ }
+
+ /**
+ * Do create a product based on data, or pass request to wpcom.
+ *
+ * @param object $request - request passed from WP.
+ *
+ * @return array|WP_Error
+ */
+ public function create_product( $request ) {
+ if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
+ require_lib( 'memberships' );
+ $connected_destination_account_id = Jetpack_Memberships::get_connected_account_id();
+ if ( ! $connected_destination_account_id ) {
+ return new WP_Error( 'no-destination-account', __( 'Please set up a Stripe account for this site first', 'jetpack' ) );
+ }
+ $product = Memberships_Product::create(
+ get_current_blog_id(),
+ array(
+ 'title' => $request['title'],
+ 'price' => $request['price'],
+ 'currency' => $request['currency'],
+ 'interval' => $request['interval'],
+ 'connected_destination_account_id' => $connected_destination_account_id,
+ )
+ );
+ if ( is_wp_error( $product ) ) {
+ return new WP_Error( $product->get_error_code(), __( 'Creating product has failed.', 'jetpack' ) );
+ }
+ return $product->to_array();
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $response = Jetpack_Client::wpcom_json_api_request_as_user(
+ "/sites/$blog_id/{$this->rest_base}/product",
+ 'v2',
+ array(
+ 'method' => 'POST',
+ ),
+ array(
+ 'title' => $request['title'],
+ 'price' => $request['price'],
+ 'currency' => $request['currency'],
+ 'interval' => $request['interval'],
+ )
+ );
+ if ( is_wp_error( $response ) ) {
+ if ( $response->get_error_code() === 'missing_token' ) {
+ return new WP_Error( 'missing_token', __( 'Please connect your user account to WordPress.com', 'jetpack' ), 404 );
+ }
+ return new WP_Error( 'wpcom_connection_error', __( 'Could not connect to WordPress.com', 'jetpack' ), 404 );
+ }
+ $data = isset( $response['body'] ) ? json_decode( $response['body'], true ) : null;
+ // If endpoint returned error, we have to detect it.
+ if ( 200 !== $response['response']['code'] && $data['code'] && $data['message'] ) {
+ return new WP_Error( $data['code'], $data['message'], 401 );
+ }
+ return $data;
+ }
+
+ return $request;
+ }
+
+ /**
+ * Get a status of connection for the site. If this is Jetpack, pass the request to wpcom.
+ *
+ * @return array|WP_Error
+ */
+ public function get_status() {
+ $connected_account_id = Jetpack_Memberships::get_connected_account_id();
+ $connect_url = '';
+ if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
+ require_lib( 'memberships' );
+ $blog_id = get_current_blog_id();
+ if ( ! $connected_account_id ) {
+ $connect_url = get_memberships_connected_account_redirect( get_current_user_id(), $blog_id );
+ }
+ $products = get_memberships_plans( $blog_id );
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $response = Jetpack_Client::wpcom_json_api_request_as_user(
+ "/sites/$blog_id/{$this->rest_base}/status",
+ 'v2',
+ array(),
+ null
+ );
+ if ( is_wp_error( $response ) ) {
+ if ( $response->get_error_code() === 'missing_token' ) {
+ return new WP_Error( 'missing_token', __( 'Please connect your user account to WordPress.com', 'jetpack' ), 404 );
+ }
+ return new WP_Error( 'wpcom_connection_error', __( 'Could not connect to WordPress.com', 'jetpack' ), 404 );
+ }
+ $data = isset( $response['body'] ) ? json_decode( $response['body'], true ) : null;
+ if ( ! $connected_account_id ) {
+ $connect_url = empty( $data['connect_url'] ) ? '' : $data['connect_url'];
+ }
+ $products = empty( $data['products'] ) ? array() : $data['products'];
+ }
+ return array(
+ 'connected_account_id' => $connected_account_id,
+ 'connect_url' => $connect_url,
+ 'products' => $products,
+ );
+ }
+}
+
+if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_active() ) {
+ wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Memberships' );
+}
+
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php
new file mode 100644
index 00000000..6e04a289
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php
@@ -0,0 +1,121 @@
+<?php
+
+require_once dirname( __FILE__ ) . '/publicize-connections.php';
+
+/**
+ * Publicize: List Connection Test Result Data
+ *
+ * All the same data as the Publicize Connections Endpoint, plus test results.
+ *
+ * @since 6.8
+ */
+class WPCOM_REST_API_V2_Endpoint_List_Publicize_Connection_Test_Results extends WPCOM_REST_API_V2_Endpoint_List_Publicize_Connections {
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'publicize/connection-test-results';
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Adds the test results properties to the Connection schema.
+ *
+ * @return array
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-connection-test-results',
+ 'type' => 'object',
+ 'properties' => $this->get_connection_schema_properties() + array(
+ 'test_success' => array(
+ 'description' => __( 'Did the Publicize Connection test pass?', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ 'test_message' => array(
+ 'description' => __( 'Publicize Connection success or error message', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'can_refresh' => array(
+ 'description' => __( 'Can the current user refresh the Publicize Connection?', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ 'refresh_text' => array(
+ 'description' => __( 'Message instructing the user to refresh their Connection to the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'refresh_url' => array(
+ 'description' => __( 'URL for refreshing the Connection to the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * @param WP_REST_Request
+ * @see Publicize::get_publicize_conns_test_results()
+ * @return WP_REST_Response suitable for 1-page collection
+ */
+ public function get_items( $request ) {
+ global $publicize;
+
+ $items = $this->get_connections();
+
+ $test_results = $publicize->get_publicize_conns_test_results();
+ $test_results_by_unique_id = array();
+ foreach ( $test_results as $test_result ) {
+ $test_results_by_unique_id[ $test_result['unique_id'] ] = $test_result;
+ }
+
+ $mapping = array(
+ 'test_success' => 'connectionTestPassed',
+ 'test_message' => 'connectionTestMessage',
+ 'can_refresh' => 'userCanRefresh',
+ 'refresh_text' => 'refreshText',
+ 'refresh_url' => 'refreshURL',
+ );
+
+ foreach ( $items as &$item ) {
+ $test_result = $test_results_by_unique_id[ $item['id'] ];
+
+ foreach ( $mapping as $field => $test_result_field ) {
+ $item[ $field ] = $test_result[ $test_result_field ];
+ }
+ }
+
+ if ( 'linkedin' === $item['id'] && 'must_reauth' === $test_result['connectionTestPassed'] ) {
+ $item['test_success'] = 'must_reauth';
+ }
+
+ $response = rest_ensure_response( $items );
+
+ $response->header( 'X-WP-Total', count( $items ) );
+ $response->header( 'X-WP-TotalPages', 1 );
+
+ return $response;
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_List_Publicize_Connection_Test_Results' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connections.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connections.php
new file mode 100644
index 00000000..34d6b2a6
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connections.php
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * Publicize: List Connections
+ *
+ * [
+ * { # Connnection Object. See schema for more detail.
+ * id: (string) Connection unique_id
+ * service_name: (string) Service slug
+ * display_name: (string) User name/display name of user/connection on Service
+ * global: (boolean) Is the Connection available to all users of the site?
+ * },
+ * ...
+ * ]
+ *
+ * @since 6.8
+ */
+class WPCOM_REST_API_V2_Endpoint_List_Publicize_Connections extends WP_REST_Controller {
+ /**
+ * Flag to help WordPress.com decide where it should look for
+ * Publicize data. Ignored for direct requests to Jetpack sites.
+ *
+ * @var bool $wpcom_is_wpcom_only_endpoint
+ */
+ public $wpcom_is_wpcom_only_endpoint = true;
+
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'publicize/connections';
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Helper for generating schema. Used by this endpoint and by the
+ * Connection Test Result endpoint.
+ *
+ * @internal
+ * @return array
+ */
+ protected function get_connection_schema_properties() {
+ return array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the Publicize Connection', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'service_name' => array(
+ 'description' => __( 'Alphanumeric identifier for the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'display_name' => array(
+ 'description' => __( 'Username of the connected account', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'global' => array(
+ 'description' => __( 'Is this connection available to all users?', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-connection',
+ 'type' => 'object',
+ 'properties' => $this->get_connection_schema_properties(),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Helper for retrieving Connections. Used by this endpoint and by
+ * the Connection Test Result endpoint.
+ *
+ * @internal
+ * @return array
+ */
+ protected function get_connections() {
+ global $publicize;
+
+ $items = array();
+
+ foreach ( (array) $publicize->get_services( 'connected' ) as $service_name => $connections ) {
+ foreach ( $connections as $connection ) {
+ $connection_meta = $publicize->get_connection_meta( $connection );
+ $connection_data = $connection_meta['connection_data'];
+
+ $items[] = array(
+ 'id' => (string) $publicize->get_connection_unique_id( $connection ),
+ 'service_name' => $service_name,
+ 'display_name' => $publicize->get_display_name( $service_name, $connection ),
+ // We expect an integer, but do loose comparison below in case some other type is stored
+ 'global' => 0 == $connection_data['user_id'],
+ );
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * @param WP_REST_Request $request
+ * @return WP_REST_Response suitable for 1-page collection
+ */
+ public function get_items( $request ) {
+ $items = array();
+
+ foreach ( $this->get_connections() as $item ) {
+ $items[] = $this->prepare_item_for_response( $item, $request );
+ }
+
+ $response = rest_ensure_response( $items );
+ $response->header( 'X-WP-Total', count( $items ) );
+ $response->header( 'X-WP-TotalPages', 1 );
+
+ return $response;
+ }
+
+ /**
+ * Filters out data based on ?_fields= request parameter
+ *
+ * @param array $connection
+ * @param WP_REST_Request $request
+ * @return array filtered $connection
+ */
+ public function prepare_item_for_response( $connection, $request ) {
+ if ( ! is_callable( array( $this, 'get_fields_for_response' ) ) ) {
+ return $connection;
+ }
+
+ $fields = $this->get_fields_for_response( $request );
+
+ $response_data = array();
+ foreach ( $connection as $field => $value ) {
+ if ( in_array( $field, $fields, true ) ) {
+ $response_data[ $field ] = $value;
+ }
+ }
+
+ return $response_data;
+ }
+
+ /**
+ * Verify that user can access Publicize data
+ *
+ * @return true|WP_Error
+ */
+ public function get_items_permission_check() {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return new WP_Error(
+ 'publicize_not_available',
+ __( 'Sorry, Publicize is not available on your site right now.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ if ( $publicize->current_user_can_access_publicize_data() ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'invalid_user_permission_publicize',
+ __( 'Sorry, you are not allowed to access Publicize data on this site.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_List_Publicize_Connections' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-services.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-services.php
new file mode 100644
index 00000000..4641b218
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-services.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * Publicize: List Publicize Services
+ *
+ * [
+ * { # Service Object. See schema for more detail.
+ * name: (string) Service slug
+ * label: (string) Human readable label for the Service
+ * url: (string) Connect URL
+ * },
+ * ...
+ * ]
+ *
+ * @since 6.8
+ */
+class WPCOM_REST_API_V2_Endpoint_List_Publicize_Services extends WP_REST_Controller {
+ /**
+ * Flag to help WordPress.com decide where it should look for
+ * Publicize data. Ignored for direct requests to Jetpack sites.
+ *
+ * @var bool $wpcom_is_wpcom_only_endpoint
+ */
+ public $wpcom_is_wpcom_only_endpoint = true;
+
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'publicize/services';
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-service',
+ 'type' => 'object',
+ 'properties' => array(
+ 'name' => array(
+ 'description' => __( 'Alphanumeric identifier for the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'label' => array(
+ 'description' => __( 'Human readable label for the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'url' => array(
+ 'description' => __( 'The URL used to connect to the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Retrieves available Publicize Services.
+ *
+ * @see Publicize::get_available_service_data()
+ *
+ * @param WP_REST_Request $request
+ * @return WP_REST_Response suitable for 1-page collection
+ */
+ public function get_items( $request ) {
+ global $publicize;
+ /**
+ * We need this because Publicize::get_available_service_data() uses `Jetpack_Keyring_Service_Helper`
+ * and `Jetpack_Keyring_Service_Helper` relies on `menu_page_url()`.
+ *
+ * We also need add_submenu_page(), as the URLs for connecting each service
+ * rely on the `sharing` menu subpage being present.
+ */
+ include_once ABSPATH . 'wp-admin/includes/plugin.php';
+
+ // The `sharing` submenu page must exist for service connect URLs to be correct.
+ add_submenu_page( 'options-general.php', '', '', 'manage_options', 'sharing', '__return_empty_string' );
+
+ $services_data = $publicize->get_available_service_data();
+
+ $services = array();
+ foreach ( $services_data as $service_data ) {
+ $services[] = $this->prepare_item_for_response( $service_data, $request );
+ }
+
+ $response = rest_ensure_response( $services );
+ $response->header( 'X-WP-Total', count( $services ) );
+ $response->header( 'X-WP-TotalPages', 1 );
+
+ return $response;
+ }
+
+ /**
+ * Filters out data based on ?_fields= request parameter
+ *
+ * @param array $service
+ * @param WP_REST_Request $request
+ * @return array filtered $service
+ */
+ public function prepare_item_for_response( $service, $request ) {
+ if ( ! is_callable( array( $this, 'get_fields_for_response' ) ) ) {
+ return $service;
+ }
+
+ $fields = $this->get_fields_for_response( $request );
+
+ $response_data = array();
+ foreach ( $service as $field => $value ) {
+ if ( in_array( $field, $fields, true ) ) {
+ $response_data[ $field ] = $value;
+ }
+ }
+
+ return $response_data;
+ }
+
+ /**
+ * Verify that user can access Publicize data
+ *
+ * @return true|WP_Error
+ */
+ public function get_items_permission_check() {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return new WP_Error(
+ 'publicize_not_available',
+ __( 'Sorry, Publicize is not available on your site right now.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ if ( $publicize->current_user_can_access_publicize_data() ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'invalid_user_permission_publicize',
+ __( 'Sorry, you are not allowed to access Publicize data on this site.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_List_Publicize_Services' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/service-api-keys.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/service-api-keys.php
new file mode 100644
index 00000000..05d0ddd3
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/service-api-keys.php
@@ -0,0 +1,281 @@
+<?php
+/*
+ * Service API Keys: Exposes 3rd party api keys that are used on a site.
+ *
+ * [
+ * { # Availabilty Object. See schema for more detail.
+ * code: (string) Displays success if the operation was successfully executed and an error code if it was not
+ * service: (string) The name of the service in question
+ * service_api_key: (string) The API key used by the service empty if one is not set yet
+ * message: (string) User friendly message
+ * },
+ * ...
+ * ]
+ *
+ * @since 6.9
+ */
+class WPCOM_REST_API_V2_Endpoint_Service_API_Keys extends WP_REST_Controller {
+
+ function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'service-api-keys';
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ register_rest_route(
+ 'wpcom/v2',
+ '/service-api-keys/(?P<service>[a-z\-_]+)',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( __CLASS__, 'get_service_api_key' ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( __CLASS__, 'update_service_api_key' ),
+ 'permission_callback' => array( __CLASS__, 'edit_others_posts_check' ),
+ 'args' => array(
+ 'service_api_key' => array(
+ 'required' => true,
+ 'type' => 'text',
+ ),
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => array( __CLASS__, 'delete_service_api_key' ),
+ 'permission_callback' => array( __CLASS__, 'edit_others_posts_check' ),
+ ),
+ )
+ );
+ }
+
+ public static function edit_others_posts_check() {
+ if ( current_user_can( 'edit_others_posts' ) ) {
+ return true;
+ }
+
+ $user_permissions_error_msg = esc_html__(
+ 'You do not have the correct user permissions to perform this action.
+ Please contact your site admin if you think this is a mistake.',
+ 'jetpack'
+ );
+
+ return new WP_Error( 'invalid_user_permission_edit_others_posts', $user_permissions_error_msg, rest_authorization_required_code() );
+ }
+
+ /**
+ * Return the available Gutenberg extensions schema
+ *
+ * @return array Service API Key schema
+ */
+ public function get_public_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'service-api-keys',
+ 'type' => 'object',
+ 'properties' => array(
+ 'code' => array(
+ 'description' => __( 'Displays success if the operation was successfully executed and an error code if it was not', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'service' => array(
+ 'description' => __( 'The name of the service in question', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'service_api_key' => array(
+ 'description' => __( 'The API key used by the service. Empty if none has been set yet', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'message' => array(
+ 'description' => __( 'User friendly message', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Get third party plugin API keys.
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function get_service_api_key( $request ) {
+
+ $service = self::validate_service_api_service( $request['service'] );
+ if ( ! $service ) {
+ return self::service_api_invalid_service_response();
+ }
+ $option = self::key_for_api_service( $service );
+ $message = esc_html__( 'API key retrieved successfully.', 'jetpack' );
+ return array(
+ 'code' => 'success',
+ 'service' => $service,
+ 'service_api_key' => Jetpack_Options::get_option( $option, '' ),
+ 'message' => $message,
+ );
+ }
+
+ /**
+ * Update third party plugin API keys.
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function update_service_api_key( $request ) {
+ $service = self::validate_service_api_service( $request['service'] );
+ if ( ! $service ) {
+ return self::service_api_invalid_service_response();
+ }
+ $json_params = $request->get_json_params();
+ $params = ! empty( $json_params ) ? $json_params : $request->get_body_params();
+ $service_api_key = trim( $params['service_api_key'] );
+ $option = self::key_for_api_service( $service );
+
+ $validation = self::validate_service_api_key( $service_api_key, $service, $params );
+ if ( ! $validation['status'] ) {
+ return new WP_Error( 'invalid_key', esc_html__( 'Invalid API Key', 'jetpack' ), array( 'status' => 404 ) );
+ }
+ $message = esc_html__( 'API key updated successfully.', 'jetpack' );
+ Jetpack_Options::update_option( $option, $service_api_key );
+ return array(
+ 'code' => 'success',
+ 'service' => $service,
+ 'service_api_key' => Jetpack_Options::get_option( $option, '' ),
+ 'message' => $message,
+ );
+ }
+
+ /**
+ * Delete a third party plugin API key.
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function delete_service_api_key( $request ) {
+ $service = self::validate_service_api_service( $request['service'] );
+ if ( ! $service ) {
+ return self::service_api_invalid_service_response();
+ }
+ $option = self::key_for_api_service( $service );
+ Jetpack_Options::delete_option( $option );
+ $message = esc_html__( 'API key deleted successfully.', 'jetpack' );
+ return array(
+ 'code' => 'success',
+ 'service' => $service,
+ 'service_api_key' => Jetpack_Options::get_option( $option, '' ),
+ 'message' => $message,
+ );
+ }
+
+ /**
+ * Validate the service provided in /service-api-keys/ endpoints.
+ * To add a service to these endpoints, add the service name to $valid_services
+ * and add '{service name}_api_key' to the non-compact return array in get_option_names(),
+ * in class-jetpack-options.php
+ *
+ * @param string $service The service the API key is for.
+ * @return string Returns the service name if valid, null if invalid.
+ */
+ public static function validate_service_api_service( $service = null ) {
+ $valid_services = array(
+ 'mapbox',
+ );
+ return in_array( $service, $valid_services, true ) ? $service : null;
+ }
+
+ /**
+ * Error response for invalid service API key requests with an invalid service.
+ */
+ public static function service_api_invalid_service_response() {
+ return new WP_Error(
+ 'invalid_service',
+ esc_html__( 'Invalid Service', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ /**
+ * Validate API Key
+ *
+ * @param string $key The API key to be validated.
+ * @param string $service The service the API key is for.
+ */
+ public static function validate_service_api_key( $key = null, $service = null ) {
+ $validation = false;
+ switch ( $service ) {
+ case 'mapbox':
+ $validation = self::validate_service_api_key_mapbox( $key );
+ break;
+ }
+ return $validation;
+ }
+
+ /**
+ * Validate Mapbox API key
+ * Based loosely on https://github.com/mapbox/geocoding-example/blob/master/php/MapboxTest.php
+ *
+ * @param string $key The API key to be validated.
+ */
+ public static function validate_service_api_key_mapbox( $key ) {
+ $status = true;
+ $msg = null;
+ $mapbox_url = sprintf(
+ 'https://api.mapbox.com?%s',
+ $key
+ );
+ $mapbox_response = wp_safe_remote_get( esc_url_raw( $mapbox_url ) );
+ $mapbox_body = wp_remote_retrieve_body( $mapbox_response );
+ if ( '{"api":"mapbox"}' !== $mapbox_body ) {
+ $status = false;
+ $msg = esc_html__( 'Can\'t connect to Mapbox', 'jetpack' );
+ return array(
+ 'status' => $status,
+ 'error_message' => $msg,
+ );
+ }
+ $mapbox_geocode_url = esc_url_raw(
+ sprintf(
+ 'https://api.mapbox.com/geocoding/v5/mapbox.places/%s.json?access_token=%s',
+ '1+broadway+new+york+ny+usa',
+ $key
+ )
+ );
+ $mapbox_geocode_response = wp_safe_remote_get( esc_url_raw( $mapbox_geocode_url ) );
+ $mapbox_geocode_body = wp_remote_retrieve_body( $mapbox_geocode_response );
+ $mapbox_geocode_json = json_decode( $mapbox_geocode_body );
+ if ( isset( $mapbox_geocode_json->message ) && ! isset( $mapbox_geocode_json->query ) ) {
+ $status = false;
+ $msg = $mapbox_geocode_json->message;
+ }
+ return array(
+ 'status' => $status,
+ 'error_message' => $msg,
+ );
+ }
+
+ /**
+ * Create site option key for service
+ *
+ * @param string $service The service to create key for.
+ */
+ private static function key_for_api_service( $service ) {
+ return $service . '_api_key';
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/sites-posts-featured-media-url.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/sites-posts-featured-media-url.php
new file mode 100644
index 00000000..4c34161c
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/sites-posts-featured-media-url.php
@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * Plugin Name: WPCOM Add Featured Media URL
+ *
+ * Adds `jetpack_featured_media_url` to post responses
+ */
+
+class WPCOM_REST_API_V2_Sites_Posts_Add_Featured_Media_URL {
+ function __construct() {
+ add_action( 'rest_api_init', array( $this, 'add_featured_media_url' ) );
+ }
+
+ function add_featured_media_url() {
+ register_rest_field( 'post', 'jetpack_featured_media_url',
+ array(
+ 'get_callback' => array( $this, 'get_featured_media_url' ),
+ 'update_callback' => null,
+ 'schema' => null,
+ )
+ );
+ }
+
+ function get_featured_media_url( $object, $field_name, $request ) {
+ $featured_media_url = '';
+ $image_attributes = wp_get_attachment_image_src(
+ get_post_thumbnail_id( $object['id'] ),
+ 'full'
+ );
+ if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) {
+ $featured_media_url = (string) $image_attributes[0];
+ }
+ return $featured_media_url;
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Sites_Posts_Add_Featured_Media_URL' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php
new file mode 100644
index 00000000..c1a712bd
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Subscribers: Get subscriber count
+ *
+ * @since 6.9
+ */
+class WPCOM_REST_API_V2_Endpoint_Subscribers extends WP_REST_Controller {
+ function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'subscribers';
+ // This endpoint *does not* need to connect directly to Jetpack sites.
+ $this->wpcom_is_wpcom_only_endpoint = true;
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ // GET /sites/<blog_id>/subscribers/count - Return number of subscribers for this site.
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/count', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_subscriber_count' ),
+ 'permission_callback' => array( $this, 'readable_permission_check' ),
+ )
+ ) );
+ }
+
+ public function readable_permission_check() {
+ if ( ! current_user_can_for_blog( get_current_blog_id(), 'edit_posts' ) ) {
+ return new WP_Error( 'authorization_required', 'Only users with the permission to edit posts can see the subscriber count.', array( 'status' => 401 ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Retrieves subscriber count
+ *
+ * @param WP_REST_Request $request incoming API request info
+ * @return array data object containing subscriber count
+ */
+ public function get_subscriber_count( $request ) {
+ // Get the most up to date subscriber count when request is not a test
+ if ( ! Jetpack_Constants::is_defined( 'TESTING_IN_JETPACK' ) ) {
+ delete_transient( 'wpcom_subscribers_total' );
+ }
+
+ $subscriber_info = Jetpack_Subscriptions_Widget::fetch_subscriber_count();
+ $subscriber_count = $subscriber_info['value'];
+
+ return array(
+ 'count' => $subscriber_count
+ );
+ }
+}
+
+if (
+ Jetpack::is_module_active( 'subscriptions' ) ||
+ ( Jetpack_Constants::is_defined( 'TESTING_IN_JETPACK' ) && Jetpack_Constants::get_constant( 'TESTING_IN_JETPACK' ) )
+) {
+ wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Subscribers' );
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-fields/attachment-fields-videopress.php b/plugins/jetpack/_inc/lib/core-api/wpcom-fields/attachment-fields-videopress.php
new file mode 100644
index 00000000..b615c4e6
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-fields/attachment-fields-videopress.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ * Extend the REST API functionality for VideoPress users.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Add per-attachment VideoPress data.
+ *
+ * { # Attachment Object
+ * ...
+ * jetpack_videopress_guid: (string) VideoPress identifier
+ * ...
+ * }
+ *
+ * @since 7.1.0
+ */
+class WPCOM_REST_API_V2_Attachment_VideoPress_Field extends WPCOM_REST_API_V2_Field_Controller {
+ /**
+ * The REST Object Type to which the jetpack_videopress_guid field will be added.
+ *
+ * @var string
+ */
+ protected $object_type = 'attachment';
+
+ /**
+ * The name of the REST API field to add.
+ *
+ * @var string $field_name
+ */
+ protected $field_name = 'jetpack_videopress_guid';
+
+ /**
+ * Registers the jetpack_videopress field and adds a filter to remove it for attachments that are not videos.
+ */
+ public function register_fields() {
+ parent::register_fields();
+
+ add_filter( 'rest_prepare_attachment', array( $this, 'remove_field_for_non_videos' ), 10, 2 );
+ }
+
+ /**
+ * Defines data structure and what elements are visible in which contexts
+ */
+ public function get_schema() {
+ return array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->field_name,
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ 'description' => __( 'Unique VideoPress ID', 'jetpack' ),
+ );
+ }
+
+ /**
+ * Getter: Retrieve current VideoPress data for a given attachment.
+ *
+ * @param array $attachment Response from the attachment endpoint.
+ * @param WP_REST_Request $request Request to the attachment endpoint.
+ *
+ * @return string
+ */
+ public function get( $attachment, $request ) {
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $blog_id = get_current_blog_id();
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ }
+
+ $post_id = absint( $attachment['id'] );
+
+ $videopress_guid = $this->get_videopress_guid( $post_id, $blog_id );
+
+ if ( ! $videopress_guid ) {
+ return '';
+ }
+
+ return $videopress_guid;
+ }
+
+ /**
+ * Gets the VideoPress GUID for a given attachment.
+ *
+ * This is pulled out into a separate method to support unit test mocking.
+ *
+ * @param int $attachment_id Attachment ID.
+ * @param int $blog_id Blog ID.
+ *
+ * @return string
+ */
+ public function get_videopress_guid( $attachment_id, $blog_id ) {
+ return video_get_info_by_blogpostid( $blog_id, $attachment_id )->guid;
+ }
+
+ /**
+ * Checks if the given attachment is a video.
+ *
+ * @param object $attachment The attachment object.
+ *
+ * @return false|int
+ */
+ public function is_video( $attachment ) {
+ return wp_startswith( $attachment->post_mime_type, 'video/' );
+ }
+
+ /**
+ * Removes the jetpack_videopress_guid field from the response if the
+ * given attachment is not a video.
+ *
+ * @param WP_REST_Response $response Response from the attachment endpoint.
+ * @param WP_Post $attachment The original attachment object.
+ *
+ * @return mixed
+ */
+ public function remove_field_for_non_videos( $response, $attachment ) {
+ if ( ! $this->is_video( $attachment ) ) {
+ unset( $response->data[ $this->field_name ] );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Setter: It does nothing since `jetpack_videopress` is a read-only field.
+ *
+ * @param mixed $value The new value for the field.
+ * @param WP_Post $object The attachment object.
+ * @param WP_REST_Request $request The request object.
+ *
+ * @return null
+ */
+ public function update( $value, $object, $request ) {
+ return null;
+ }
+
+ /**
+ * Permission Check for the field's getter. Delegate the responsibility to the
+ * attachment endpoint, so it always returns true.
+ *
+ * @param mixed $object Response from the attachment endpoint.
+ * @param WP_REST_Request $request Request to the attachment endpoint.
+ *
+ * @return true
+ */
+ public function get_permission_check( $object, $request ) {
+ return true;
+ }
+
+ /**
+ * Permission Check for the field's setter. Delegate the responsibility to the
+ * attachment endpoint, so it always returns true.
+ *
+ * @param mixed $value The new value for the field.
+ * @param WP_Post $object The attachment object.
+ * @param WP_REST_Request $request Request to the attachment endpoint.
+ *
+ * @return true
+ */
+ public function update_permission_check( $value, $object, $request ) {
+ return true;
+ }
+}
+
+if (
+ ( method_exists( 'Jetpack', 'is_active' ) && Jetpack::is_active() ) ||
+ ( defined( 'IS_WPCOM' ) && IS_WPCOM )
+) {
+ wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Attachment_VideoPress_Field' );
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-fields/post-fields-publicize-connections.php b/plugins/jetpack/_inc/lib/core-api/wpcom-fields/post-fields-publicize-connections.php
new file mode 100644
index 00000000..c4254a9d
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-fields/post-fields-publicize-connections.php
@@ -0,0 +1,353 @@
+<?php
+
+/**
+ * Add per-post Publicize Connection data.
+ *
+ * { # Post Object
+ * ...
+ * jetpack_publicize_connections: { # Defined below in this file. See schema for more detail.
+ * id: (string) Connection unique_id
+ * service_name: (string) Service slug
+ * display_name: (string) User name/display name of user/connection on Service
+ * enabled: (boolean) Is this connection slated to be shared to? context=edit only
+ * done: (boolean) Is this post (or connection) done sharing? context=edit only
+ * toggleable: (boolean) Can the current user change the `enabled` setting for this Connection+Post? context=edit only
+ * }
+ * ...
+ * meta: { # Not defined in this file. Handled in modules/publicize/publicize.php via `register_meta()`
+ * jetpack_publicize_message: (string) The message to use instead of the post's title when sharing.
+ * }
+ * ...
+ * }
+ *
+ * @since 6.8.0
+ */
+class WPCOM_REST_API_V2_Post_Publicize_Connections_Field extends WPCOM_REST_API_V2_Field_Controller {
+ protected $object_type = 'post';
+ protected $field_name = 'jetpack_publicize_connections';
+
+ public $memoized_updates = array();
+
+ /**
+ * Registers the jetpack_publicize_connections field. Called
+ * automatically on `rest_api_init()`.
+ */
+ public function register_fields() {
+ $this->object_type = get_post_types_by_support( 'publicize' );
+
+ foreach ( $this->object_type as $post_type ) {
+ // Adds meta support for those post types that don't already have it.
+ // Only runs during REST API requests, so it doesn't impact UI.
+ if ( ! post_type_supports( $post_type, 'custom-fields' ) ) {
+ add_post_type_support( $post_type, 'custom-fields' );
+ }
+
+ add_filter( 'rest_pre_insert_' . $post_type, array( $this, 'rest_pre_insert' ), 10, 2 );
+ add_action( 'rest_insert_' . $post_type, array( $this, 'rest_insert' ), 10, 3 );
+ }
+
+ parent::register_fields();
+ }
+
+ /**
+ * Defines data structure and what elements are visible in which contexts
+ */
+ public function get_schema() {
+ return array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-post-connections',
+ 'type' => 'array',
+ 'context' => array( 'view', 'edit' ),
+ 'items' => $this->post_connection_schema(),
+ 'default' => array(),
+ );
+ }
+
+ private function post_connection_schema() {
+ return array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-post-connection',
+ 'type' => 'object',
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the Publicize Connection', 'jetpack' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'service_name' => array(
+ 'description' => __( 'Alphanumeric identifier for the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'display_name' => array(
+ 'description' => __( 'Username of the connected account', 'jetpack' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'enabled' => array(
+ 'description' => __( 'Whether to share to this connection', 'jetpack' ),
+ 'type' => 'boolean',
+ 'context' => array( 'edit' ),
+ ),
+ 'done' => array(
+ 'description' => __( 'Whether Publicize has already finished sharing for this post', 'jetpack' ),
+ 'type' => 'boolean',
+ 'context' => array( 'edit' ),
+ 'readonly' => true,
+ ),
+ 'toggleable' => array(
+ 'description' => __( 'Whether `enable` can be changed for this post/connection', 'jetpack' ),
+ 'type' => 'boolean',
+ 'context' => array( 'edit' ),
+ 'readonly' => true,
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @param int $post_id
+ * @return true|WP_Error
+ */
+ function permission_check( $post_id ) {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return new WP_Error(
+ 'publicize_not_available',
+ __( 'Sorry, Publicize is not available on your site right now.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ if ( $publicize->current_user_can_access_publicize_data( $post_id ) ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'invalid_user_permission_publicize',
+ __( 'Sorry, you are not allowed to access Publicize data for this post.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ /**
+ * Getter permission check
+ *
+ * @param array $post_array Response data from Post Endpoint
+ * @return true|WP_Error
+ */
+ function get_permission_check( $post_array, $request ) {
+ return $this->permission_check( isset( $post_array['id'] ) ? $post_array['id'] : 0 );
+
+ }
+
+ /**
+ * Setter permission check
+ *
+ * @param WP_Post $post
+ * @return true|WP_Error
+ */
+ public function update_permission_check( $value, $post, $request ) {
+ return $this->permission_check( isset( $post->ID ) ? $post->ID : 0 );
+ }
+
+ /**
+ * Getter: Retrieve current list of connected social accounts for a given post.
+ *
+ * @see Publicize::get_filtered_connection_data()
+ *
+ * @param array $post_array Response from Post Endpoint
+ * @param WP_REST_Request
+ *
+ * @return array List of connections
+ */
+ public function get( $post_array, $request ) {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return array();
+ }
+
+ $schema = $this->post_connection_schema();
+ $properties = array_keys( $schema['properties'] );
+
+ $connections = $publicize->get_filtered_connection_data( $post_array['id'] );
+
+ $output_connections = array();
+ foreach ( $connections as $connection ) {
+ $output_connection = array();
+ foreach ( $properties as $property ) {
+ if ( isset( $connection[ $property ] ) ) {
+ $output_connection[ $property ] = $connection[ $property ];
+ }
+ }
+
+ $output_connection['id'] = (string) $connection['unique_id'];
+
+ $output_connections[] = $output_connection;
+ }
+
+ return $output_connections;
+ }
+
+ /**
+ * Prior to updating the post, first calculate which Services to
+ * Publicize to and which to skip.
+ *
+ * @param object $post Post data to insert/update.
+ * @param WP_REST_Request $request
+ * @return Filtered $post
+ */
+ public function rest_pre_insert( $post, $request ) {
+ if ( ! isset( $request['jetpack_publicize_connections'] ) ) {
+ return $post;
+ }
+
+ $permission_check = $this->update_permission_check( $request['jetpack_publicize_connections'], $post, $request );
+
+ if ( is_wp_error( $permission_check ) ) {
+ return $permission_check;
+ }
+
+ // memoize
+ $this->get_meta_to_update( $request['jetpack_publicize_connections'], isset( $post->ID ) ? $post->ID : 0 );
+
+ return $post;
+ }
+
+ /**
+ * After creating a new post, update our cached data to reflect
+ * the new post ID.
+ *
+ * @param WP_Post $post
+ * @param WP_REST_Request $request
+ * @param bool $is_new
+ */
+ public function rest_insert( $post, $request, $is_new ) {
+ if ( ! $is_new ) {
+ // An existing post was edited - no need to update
+ // our cache - we started out knowing the correct
+ // post ID.
+ return;
+ }
+
+ if ( ! isset( $request['jetpack_publicize_connections'] ) ) {
+ return;
+ }
+
+ if ( ! isset( $this->memoized_updates[0] ) ) {
+ return;
+ }
+
+ $this->memoized_updates[ $post->ID ] = $this->memoized_updates[0];
+ unset( $this->memoized_updates[0] );
+ }
+
+ protected function get_meta_to_update( $requested_connections, $post_id = 0 ) {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return array();
+ }
+
+ if ( isset( $this->memoized_updates[$post_id] ) ) {
+ return $this->memoized_updates[$post_id];
+ }
+
+ $available_connections = $publicize->get_filtered_connection_data( $post_id );
+
+ $changed_connections = array();
+
+ // Build lookup mappings
+ $available_connections_by_unique_id = array();
+ $available_connections_by_service_name = array();
+ foreach ( $available_connections as $available_connection ) {
+ $available_connections_by_unique_id[ $available_connection['unique_id'] ] = $available_connection;
+
+ if ( ! isset( $available_connections_by_service_name[ $available_connection['service_name'] ] ) ) {
+ $available_connections_by_service_name[ $available_connection['service_name'] ] = array();
+ }
+ $available_connections_by_service_name[ $available_connection['service_name'] ][] = $available_connection;
+ }
+
+ // Handle { service_name: $service_name, enabled: (bool) }
+ foreach ( $requested_connections as $requested_connection ) {
+ if ( ! isset( $requested_connection['service_name'] ) ) {
+ continue;
+ }
+
+ if ( ! isset( $available_connections_by_service_name[ $requested_connection['service_name'] ] ) ) {
+ continue;
+ }
+
+ foreach ( $available_connections_by_service_name[ $requested_connection['service_name'] ] as $available_connection ) {
+ $changed_connections[ $available_connection['unique_id'] ] = $requested_connection['enabled'];
+ }
+ }
+
+ // Handle { id: $id, enabled: (bool) }
+ // These override the service_name settings
+ foreach ( $requested_connections as $requested_connection ) {
+ if ( ! isset( $requested_connection['id'] ) ) {
+ continue;
+ }
+
+ if ( ! isset( $available_connections_by_unique_id[ $requested_connection['id'] ] ) ) {
+ continue;
+ }
+
+ $changed_connections[ $requested_connection['id'] ] = $requested_connection['enabled'];
+ }
+
+ // Set all changed connections to their new value
+ foreach ( $changed_connections as $unique_id => $enabled ) {
+ $connection = $available_connections_by_unique_id[ $unique_id ];
+
+ if ( $connection['done'] || ! $connection['toggleable'] ) {
+ continue;
+ }
+
+ $available_connections_by_unique_id[ $unique_id ]['enabled'] = $enabled;
+ }
+
+ $meta_to_update = array();
+ // For all connections, ensure correct post_meta
+ foreach ( $available_connections_by_unique_id as $unique_id => $available_connection ) {
+ if ( $available_connection['enabled'] ) {
+ $meta_to_update[$publicize->POST_SKIP . $unique_id] = null;
+ } else {
+ $meta_to_update[$publicize->POST_SKIP . $unique_id] = 1;
+ }
+ }
+
+ $this->memoized_updates[$post_id] = $meta_to_update;
+
+ return $meta_to_update;
+ }
+
+ /**
+ * Update the connections slated to be shared to.
+ *
+ * @param array $requested_connections
+ * Items are either `{ id: (string) }` or `{ service_name: (string) }`
+ * @param WP_Post $post
+ * @param WP_REST_Request
+ */
+ public function update( $requested_connections, $post, $request ) {
+ foreach ( $this->get_meta_to_update( $requested_connections, $post->ID ) as $meta_key => $meta_value ) {
+ if ( is_null( $meta_value ) ) {
+ delete_post_meta( $post->ID, $meta_key );
+ } else {
+ update_post_meta( $post->ID, $meta_key, $meta_value );
+ }
+ }
+ }
+}
+
+if ( Jetpack::is_module_active( 'publicize' ) ) {
+ wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Post_Publicize_Connections_Field' );
+}
diff --git a/plugins/jetpack/_inc/lib/debugger/0-load.php b/plugins/jetpack/_inc/lib/debugger/0-load.php
new file mode 100644
index 00000000..a3eaf4e6
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/debugger/0-load.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Loading the various functions used for Jetpack Debugging.
+ *
+ * @package Jetpack.
+ */
+
+global $wp_version;
+
+/* Jetpack Connection Testing Framework */
+require_once 'class-jetpack-cxn-test-base.php';
+/* Jetpack Connection Tests */
+require_once 'class-jetpack-cxn-tests.php';
+/* Jetpack Debug Data */
+require_once 'class-jetpack-debug-data.php';
+/* The "In-Plugin Debugger" admin page. */
+require_once 'class-jetpack-debugger.php';
+
+if ( version_compare( $wp_version, '5.2-alpha', 'ge' ) ) {
+ require_once 'debug-functions-for-php53.php';
+ add_filter( 'debug_information', array( 'Jetpack_Debug_Data', 'core_debug_data' ) );
+ add_filter( 'site_status_tests', 'jetpack_debugger_site_status_tests' );
+ add_action( 'wp_ajax_health-check-jetpack-local_testing_suite', 'jetpack_debugger_ajax_local_testing_suite' );
+}
diff --git a/plugins/jetpack/_inc/lib/debugger/class-jetpack-cxn-test-base.php b/plugins/jetpack/_inc/lib/debugger/class-jetpack-cxn-test-base.php
new file mode 100644
index 00000000..56f21bc4
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/debugger/class-jetpack-cxn-test-base.php
@@ -0,0 +1,471 @@
+<?php
+/**
+ * Jetpack Connection Testing
+ *
+ * Framework for various "unit tests" against the Jetpack connection.
+ *
+ * Individual tests should be added to the class-jetpack-cxn-tests.php file.
+ *
+ * @author Brandon Kraft
+ * @package Jetpack
+ */
+
+/**
+ * "Unit Tests" for the Jetpack connection.
+ *
+ * @since 7.1.0
+ */
+class Jetpack_Cxn_Test_Base {
+
+ /**
+ * Tests to run on the Jetpack connection.
+ *
+ * @var array $tests
+ */
+ protected $tests = array();
+
+ /**
+ * Results of the Jetpack connection tests.
+ *
+ * @var array $results
+ */
+ protected $results = array();
+
+ /**
+ * Status of the testing suite.
+ *
+ * Used internally to determine if a test should be skipped since the tests are already failing. Assume passing.
+ *
+ * @var bool $pass
+ */
+ protected $pass = true;
+
+ /**
+ * Jetpack_Cxn_Test constructor.
+ */
+ public function __construct() {
+ $this->tests = array();
+ $this->results = array();
+ }
+
+ /**
+ * Adds a new test to the Jetpack Connection Testing suite.
+ *
+ * @since 7.1.0
+ * @since 7.3.0 Adds name parameter and returns WP_Error on failure.
+ *
+ * @param callable $callable Test to add to queue.
+ * @param string $name Unique name for the test.
+ * @param string $type Optional. Core Site Health type: 'direct' if test can be run during initial load or 'async' if test should run async.
+ * @param array $groups Optional. Testing groups to add test to.
+ *
+ * @return mixed True if successfully added. WP_Error on failure.
+ */
+ public function add_test( $callable, $name, $type = 'direct', $groups = array( 'default' ) ) {
+ if ( is_array( $name ) ) {
+ // Pre-7.3.0 method passed the $groups parameter here.
+ return new WP_Error( __( 'add_test arguments changed in 7.3.0. Please reference inline documentation.', 'jetpack' ) );
+ }
+ if ( array_key_exists( $name, $this->tests ) ) {
+ return new WP_Error( __( 'Test names must be unique.', 'jetpack' ) );
+ }
+ if ( ! is_callable( $callable ) ) {
+ return new WP_Error( __( 'Tests must be valid PHP callables.', 'jetpack' ) );
+ }
+
+ $this->tests[ $name ] = array(
+ 'name' => $name,
+ 'test' => $callable,
+ 'group' => $groups,
+ 'type' => $type,
+ );
+ return true;
+ }
+
+ /**
+ * Lists all tests to run.
+ *
+ * @since 7.3.0
+ *
+ * @param string $type Optional. Core Site Health type: 'direct' or 'async'. All by default.
+ * @param string $group Optional. A specific testing group. All by default.
+ *
+ * @return array $tests Array of tests with test information.
+ */
+ public function list_tests( $type = 'all', $group = 'all' ) {
+ if ( ! ( 'all' === $type || 'direct' === $type || 'async' === $type ) ) {
+ _doing_it_wrong( 'Jetpack_Cxn_Test_Base->list_tests', 'Type must be all, direct, or async', '7.3.0' );
+ }
+
+ $tests = array();
+ foreach ( $this->tests as $name => $value ) {
+ // Get all valid tests by group staged.
+ if ( 'all' === $group || $group === $value['group'] ) {
+ $tests[ $name ] = $value;
+ }
+
+ // Next filter out any that do not match the type.
+ if ( 'all' !== $type && $type !== $value['type'] ) {
+ unset( $tests[ $name ] );
+ }
+ }
+
+ return $tests;
+ }
+
+ /**
+ * Run a specific test.
+ *
+ * @since 7.3.0
+ *
+ * @param string $name Name of test.
+ *
+ * @return mixed $result Test result array or WP_Error if invalid name. {
+ * @type string $name Test name
+ * @type mixed $pass True if passed, false if failed, 'skipped' if skipped.
+ * @type string $message Human-readable test result message.
+ * @type string $resolution Human-readable resolution steps.
+ * }
+ */
+ public function run_test( $name ) {
+ if ( array_key_exists( $name, $this->tests ) ) {
+ return call_user_func( $this->tests[ $name ]['test'] );
+ }
+ return new WP_Error( __( 'There is no test by that name: ', 'jetpack' ) . $name );
+ }
+
+ /**
+ * Runs the Jetpack connection suite.
+ */
+ public function run_tests() {
+ foreach ( $this->tests as $test ) {
+ $result = call_user_func( $test['test'] );
+ $result['group'] = $test['group'];
+ $result['type'] = $test['type'];
+ $this->results[] = $result;
+ if ( false === $result['pass'] ) {
+ $this->pass = false;
+ }
+ }
+ }
+
+ /**
+ * Returns the full results array.
+ *
+ * @since 7.1.0
+ * @since 7.3.0 Add 'type'
+ *
+ * @param string $type Test type, async or direct.
+ * @param string $group Testing group whose results we want. Defaults to all tests.
+ * @return array Array of test results.
+ */
+ public function raw_results( $type = 'all', $group = 'all' ) {
+ if ( ! $this->results ) {
+ $this->run_tests();
+ }
+
+ $results = $this->results;
+
+ if ( 'all' !== $group ) {
+ foreach ( $results as $test => $result ) {
+ if ( ! in_array( $group, $result['group'], true ) ) {
+ unset( $results[ $test ] );
+ }
+ }
+ }
+
+ if ( 'all' !== $type ) {
+ foreach ( $results as $test => $result ) {
+ if ( $type !== $result['type'] ) {
+ unset( $results[ $test ] );
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Returns the status of the connection suite.
+ *
+ * @since 7.1.0
+ * @since 7.3.0 Add 'type'
+ *
+ * @param string $type Test type, async or direct. Optional, direct all tests.
+ * @param string $group Testing group to check status of. Optional, default all tests.
+ *
+ * @return true|array True if all tests pass. Array of failed tests.
+ */
+ public function pass( $type = 'all', $group = 'all' ) {
+ $results = $this->raw_results( $type, $group );
+
+ foreach ( $results as $result ) {
+ // 'pass' could be true, false, or 'skipped'. We only want false.
+ if ( isset( $result['pass'] ) && false === $result['pass'] ) {
+ return false;
+ }
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Return array of failed test messages.
+ *
+ * @since 7.1.0
+ * @since 7.3.0 Add 'type'
+ *
+ * @param string $type Test type, direct or async.
+ * @param string $group Testing group whose failures we want. Defaults to "all".
+ *
+ * @return false|array False if no failed tests. Otherwise, array of failed tests.
+ */
+ public function list_fails( $type = 'all', $group = 'all' ) {
+ $results = $this->raw_results( $type, $group );
+
+ foreach ( $results as $test => $result ) {
+ // We do not want tests that passed or ones that are misconfigured (no pass status or no failure message).
+ if ( ! isset( $result['pass'] ) || false !== $result['pass'] || ! isset( $result['message'] ) ) {
+ unset( $results[ $test ] );
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Helper function to return consistent responses for a passing test.
+ *
+ * @param string $name Test name.
+ *
+ * @return array Test results.
+ */
+ public static function passing_test( $name = 'Unnamed' ) {
+ return array(
+ 'name' => $name,
+ 'pass' => true,
+ 'message' => __( 'Test Passed!', 'jetpack' ),
+ 'resolution' => false,
+ 'severity' => false,
+ );
+ }
+
+ /**
+ * Helper function to return consistent responses for a skipped test.
+ *
+ * @param string $name Test name.
+ * @param string $message Reason for skipping the test. Optional.
+ *
+ * @return array Test results.
+ */
+ public static function skipped_test( $name = 'Unnamed', $message = false ) {
+ return array(
+ 'name' => $name,
+ 'pass' => 'skipped',
+ 'message' => $message,
+ 'resolution' => false,
+ 'severity' => false,
+ );
+ }
+
+ /**
+ * Helper function to return consistent responses for a failing test.
+ *
+ * @since 7.1.0
+ * @since 7.3.0 Added $action for resolution action link, $severity for issue severity.
+ *
+ * @param string $name Test name.
+ * @param string $message Message detailing the failure.
+ * @param string $resolution Optional. Steps to resolve.
+ * @param string $action Optional. URL to direct users to self-resolve.
+ * @param string $severity Optional. "critical" or "recommended" for failure stats. "good" for passing.
+ *
+ * @return array Test results.
+ */
+ public static function failing_test( $name, $message, $resolution = false, $action = false, $severity = 'critical' ) {
+ // Provide standard resolutions steps, but allow pass-through of non-standard ones.
+ switch ( $resolution ) {
+ case 'cycle_connection':
+ $resolution = __( 'Please disconnect and reconnect Jetpack.', 'jetpack' ); // @todo: Link.
+ break;
+ case 'outbound_requests':
+ $resolution = __( 'Please ask your hosting provider to confirm your server can make outbound requests to jetpack.com.', 'jetpack' );
+ break;
+ case 'support':
+ case false:
+ $resolution = __( 'Please contact Jetpack support.', 'jetpack' ); // @todo: Link to support.
+ break;
+ }
+
+ return array(
+ 'name' => $name,
+ 'pass' => false,
+ 'message' => $message,
+ 'resolution' => $resolution,
+ 'action' => $action,
+ 'severity' => $severity,
+ );
+ }
+
+ /**
+ * Provide WP_CLI friendly testing results.
+ *
+ * @since 7.1.0
+ * @since 7.3.0 Add 'type'
+ *
+ * @param string $type Test type, direct or async.
+ * @param string $group Testing group whose results we are outputting. Default all tests.
+ */
+ public function output_results_for_cli( $type = 'all', $group = 'all' ) {
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
+ if ( Jetpack::is_development_mode() ) {
+ WP_CLI::line( __( 'Jetpack is in Development Mode:', 'jetpack' ) );
+ WP_CLI::line( Jetpack::development_mode_trigger_text() );
+ }
+ WP_CLI::line( __( 'TEST RESULTS:', 'jetpack' ) );
+ foreach ( $this->raw_results( $group ) as $test ) {
+ if ( true === $test['pass'] ) {
+ WP_CLI::log( WP_CLI::colorize( '%gPassed:%n ' . $test['name'] ) );
+ } elseif ( 'skipped' === $test['pass'] ) {
+ WP_CLI::log( WP_CLI::colorize( '%ySkipped:%n ' . $test['name'] ) );
+ if ( $test['message'] ) {
+ WP_CLI::log( ' ' . $test['message'] ); // Number of spaces to "tab indent" the reason.
+ }
+ } else { // Failed.
+ WP_CLI::log( WP_CLI::colorize( '%rFailed:%n ' . $test['name'] ) );
+ WP_CLI::log( ' ' . $test['message'] ); // Number of spaces to "tab indent" the reason.
+ }
+ }
+ }
+ }
+
+ /**
+ * Output results of failures in format expected by Core's Site Health tool for async tests.
+ *
+ * Specifically not asking for a testing group since we're opinionated that Site Heath should see all.
+ *
+ * @since 7.3.0
+ *
+ * @return array Array of test results
+ */
+ public function output_results_for_core_async_site_health() {
+ $result = array(
+ 'label' => __( 'Jetpack passed all async tests.', 'jetpack' ),
+ 'status' => 'good',
+ 'badge' => array(
+ 'label' => __( 'Jetpack', 'jetpack' ),
+ 'color' => 'green',
+ ),
+ 'description' => sprintf(
+ '<p>%s</p>',
+ __( "Jetpack's async local testing suite passed all tests!", 'jetpack' )
+ ),
+ 'actions' => '',
+ 'test' => 'jetpack_debugger_local_testing_suite_core',
+ );
+
+ if ( $this->pass() ) {
+ return $result;
+ }
+
+ $fails = $this->list_fails( 'async' );
+ $error = false;
+ foreach ( $fails as $fail ) {
+ if ( ! $error ) {
+ $error = true;
+ $result['label'] = $fail['message'];
+ $result['status'] = $fail['severity'];
+ $result['description'] = sprintf(
+ '<p>%s</p>',
+ $fail['resolution']
+ );
+ if ( ! empty( $fail['action'] ) ) {
+ $result['actions'] = sprintf(
+ '<a class="button button-primary" href="%1$s" target="_blank" rel="noopener noreferrer">%2$s <span class="screen-reader-text">%3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+ esc_url( $fail['action'] ),
+ __( 'Resolve', 'jetpack' ),
+ /* translators: accessibility text */
+ __( '(opens in a new tab)', 'jetpack' )
+ );
+ }
+ } else {
+ $result['description'] .= sprintf(
+ '<p>%s</p>',
+ __( 'There was another problem:', 'jetpack' )
+ ) . ' ' . $fail['message'] . ': ' . $fail['resolution'];
+ if ( 'critical' === $fail['severity'] ) { // In case the initial failure is only "recommended".
+ $result['status'] = 'critical';
+ }
+ }
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Provide single WP Error instance of all failures.
+ *
+ * @since 7.1.0
+ * @since 7.3.0 Add 'type'
+ *
+ * @param string $type Test type, direct or async.
+ * @param string $group Testing group whose failures we want converted. Default all tests.
+ *
+ * @return WP_Error|false WP_Error with all failed tests or false if there were no failures.
+ */
+ public function output_fails_as_wp_error( $type = 'all', $group = 'all' ) {
+ if ( $this->pass( $group ) ) {
+ return false;
+ }
+ $fails = $this->list_fails( $type, $group );
+ $error = false;
+
+ foreach ( $fails as $result ) {
+ $code = 'failed_' . $result['name'];
+ $message = $result['message'];
+ $data = array(
+ 'resolution' => $result['resolution'],
+ );
+ if ( ! $error ) {
+ $error = new WP_Error( $code, $message, $data );
+ } else {
+ $error->add( $code, $message, $data );
+ }
+ }
+
+ return $error;
+ }
+
+ /**
+ * Encrypt data for sending to WordPress.com.
+ *
+ * @todo When PHP minimum is 5.3+, add cipher detection to use an agreed better cipher than RC4. RC4 should be the last resort.
+ *
+ * @param string $data Data to encrypt with the WP.com Public Key.
+ *
+ * @return false|array False if functionality not available. Array of encrypted data, encryption key.
+ */
+ public function encrypt_string_for_wpcom( $data ) {
+ $return = false;
+ if ( ! function_exists( 'openssl_get_publickey' ) || ! function_exists( 'openssl_seal' ) ) {
+ return $return;
+ }
+
+ $public_key = openssl_get_publickey( JETPACK__DEBUGGER_PUBLIC_KEY );
+
+ if ( $public_key && openssl_seal( $data, $encrypted_data, $env_key, array( $public_key ) ) ) {
+ // We are returning base64-encoded values to ensure they're characters we can use in JSON responses without issue.
+ $return = array(
+ 'data' => base64_encode( $encrypted_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
+ 'key' => base64_encode( $env_key[0] ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
+ 'cipher' => 'RC4', // When Jetpack's minimum WP version is at PHP 5.3+, we will add in detecting and using a stronger one.
+ );
+ }
+
+ openssl_free_key( $public_key );
+
+ return $return;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/debugger/class-jetpack-cxn-tests.php b/plugins/jetpack/_inc/lib/debugger/class-jetpack-cxn-tests.php
new file mode 100644
index 00000000..274d032b
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/debugger/class-jetpack-cxn-tests.php
@@ -0,0 +1,340 @@
+<?php
+/**
+ * Collection of tests to run on the Jetpack connection locally.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Class Jetpack_Cxn_Tests contains all of the actual tests.
+ */
+class Jetpack_Cxn_Tests extends Jetpack_Cxn_Test_Base {
+
+ /**
+ * Jetpack_Cxn_Tests constructor.
+ */
+ public function __construct() {
+ parent::__construct();
+
+ $methods = get_class_methods( 'Jetpack_Cxn_Tests' );
+
+ foreach ( $methods as $method ) {
+ if ( false === strpos( $method, 'test__' ) ) {
+ continue;
+ }
+ $this->add_test( array( $this, $method ), $method, 'direct' );
+ }
+
+ /**
+ * Fires after loading default Jetpack Connection tests.
+ *
+ * @since 7.1.0
+ */
+ do_action( 'jetpack_connection_tests_loaded' );
+
+ /**
+ * Determines if the WP.com testing suite should be included.
+ *
+ * @since 7.1.0
+ *
+ * @param bool $run_test To run the WP.com testing suite. Default true.
+ */
+ if ( apply_filters( 'jetpack_debugger_run_self_test', true ) ) {
+ /**
+ * Intentionally added last as it checks for an existing failure state before attempting.
+ * Generally, any failed location condition would result in the WP.com check to fail too, so
+ * we will skip it to avoid confusing error messages.
+ *
+ * Note: This really should be an 'async' test.
+ */
+ $this->add_test( array( $this, 'last__wpcom_self_test' ), 'test__wpcom_self_test', 'direct' );
+ }
+ }
+
+ /**
+ * Helper function to look up the expected master user and return the local WP_User.
+ *
+ * @return WP_User Jetpack's expected master user.
+ */
+ protected function helper_retrieve_local_master_user() {
+ $master_user = Jetpack_Options::get_option( 'master_user' );
+ return new WP_User( $master_user );
+ }
+
+ /**
+ * Is Jetpack even connected and supposed to be talking to WP.com?
+ */
+ protected function helper_is_jetpack_connected() {
+ return ( Jetpack::is_active() && ! Jetpack::is_development_mode() );
+ }
+
+ /**
+ * Test if Jetpack is connected.
+ */
+ protected function test__check_if_connected() {
+ $name = __FUNCTION__;
+ if ( $this->helper_is_jetpack_connected() ) {
+ $result = self::passing_test( $name );
+ } elseif ( Jetpack::is_development_mode() ) {
+ $result = self::skipped_test( $name, __( 'Jetpack is in Development Mode:', 'jetpack' ) . ' ' . Jetpack::development_mode_trigger_text(), __( 'Disable development mode.', 'jetpack' ) );
+ } else {
+ $result = self::failing_test( $name, __( 'Jetpack is not connected.', 'jetpack' ), 'cycle_connection' );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Test that the master user still exists on this site.
+ *
+ * @return array Test results.
+ */
+ protected function test__master_user_exists_on_site() {
+ $name = __FUNCTION__;
+ if ( ! $this->helper_is_jetpack_connected() ) {
+ return self::skipped_test( $name, __( 'Jetpack is not connected. No master user to check.', 'jetpack' ) ); // Skip test.
+ }
+ $local_user = $this->helper_retrieve_local_master_user();
+
+ if ( $local_user->exists() ) {
+ $result = self::passing_test( $name );
+ } else {
+ $result = self::failing_test( $name, __( 'The user who setup the Jetpack connection no longer exists on this site.', 'jetpack' ), 'cycle_connection' );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Test that the master user has the manage options capability (e.g. is an admin).
+ *
+ * Generic calls from WP.com execute on Jetpack as the master user. If it isn't an admin, random things will fail.
+ *
+ * @return array Test results.
+ */
+ protected function test__master_user_can_manage_options() {
+ $name = __FUNCTION__;
+ if ( ! $this->helper_is_jetpack_connected() ) {
+ return self::skipped_test( $name, __( 'Jetpack is not connected.', 'jetpack' ) ); // Skip test.
+ }
+ $master_user = $this->helper_retrieve_local_master_user();
+
+ if ( user_can( $master_user, 'manage_options' ) ) {
+ $result = self::passing_test( $name );
+ } else {
+ /* translators: a WordPress username */
+ $result = self::failing_test( $name, sprintf( __( 'The user (%s) who setup the Jetpack connection is not an administrator.', 'jetpack' ), $master_user->user_login ), __( 'Either upgrade the user or disconnect and reconnect Jetpack.', 'jetpack' ) ); // @todo: Link to the right places.
+ }
+
+ return $result;
+ }
+
+ /**
+ * Test that the PHP's XML library is installed.
+ *
+ * While it should be installed by default, increasingly in PHP 7, some OSes require an additional php-xml package.
+ *
+ * @return array Test results.
+ */
+ protected function test__xml_parser_available() {
+ $name = __FUNCTION__;
+ if ( function_exists( 'xml_parser_create' ) ) {
+ $result = self::passing_test( $name );
+ } else {
+ $result = self::failing_test( $name, __( 'PHP XML manipluation libraries are not available.', 'jetpack' ), __( "Please ask your hosting provider to refer to our server requirements at https://jetpack.com/support/server-requirements/ and enable PHP's XML module.", 'jetpack' ) );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Test that the server is able to send an outbound http communication.
+ *
+ * @return array Test results.
+ */
+ protected function test__outbound_http() {
+ $name = __FUNCTION__;
+ $request = wp_remote_get( preg_replace( '/^https:/', 'http:', JETPACK__API_BASE ) . 'test/1/' );
+ $code = wp_remote_retrieve_response_code( $request );
+
+ if ( 200 === intval( $code ) ) {
+ $result = self::passing_test( $name );
+ } else {
+ $result = self::failing_test( $name, __( 'Your server did not successfully connect to the Jetpack server using HTTP', 'jetpack' ), 'outbound_requests' );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Test that the server is able to send an outbound https communication.
+ *
+ * @return array Test results.
+ */
+ protected function test__outbound_https() {
+ $name = __FUNCTION__;
+ $request = wp_remote_get( preg_replace( '/^http:/', 'https:', JETPACK__API_BASE ) . 'test/1/' );
+ $code = wp_remote_retrieve_response_code( $request );
+
+ if ( 200 === intval( $code ) ) {
+ $result = self::passing_test( $name );
+ } else {
+ $result = self::failing_test( $name, __( 'Your server did not successfully connect to the Jetpack server using HTTPS', 'jetpack' ), 'outbound_requests' );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Check for an IDC.
+ *
+ * @return array Test results.
+ */
+ protected function test__identity_crisis() {
+ $name = __FUNCTION__;
+ if ( ! $this->helper_is_jetpack_connected() ) {
+ return self::skipped_test( $name, __( 'Jetpack is not connected.', 'jetpack' ) ); // Skip test.
+ }
+ $identity_crisis = Jetpack::check_identity_crisis();
+
+ if ( ! $identity_crisis ) {
+ $result = self::passing_test( $name );
+ } else {
+ $message = sprintf(
+ /* translators: Two URLs. The first is the locally-recorded value, the second is the value as recorded on WP.com. */
+ __( 'Your url is set as `%1$s`, but your WordPress.com connection lists it as `%2$s`!', 'jetpack' ),
+ $identity_crisis['home'],
+ $identity_crisis['wpcom_home']
+ );
+ $result = self::failing_test( $name, $message, 'support' );
+ }
+ return $result;
+ }
+
+ /**
+ * Tests connection status against wp.com's test-connection endpoint
+ *
+ * @todo: Compare with the wpcom_self_test. We only need one of these.
+ *
+ * @return array Test results.
+ */
+ protected function test__wpcom_connection_test() {
+ $name = __FUNCTION__;
+
+ if ( ! Jetpack::is_active() || Jetpack::is_development_mode() || Jetpack::is_staging_site() || ! $this->pass ) {
+ return self::skipped_test( $name );
+ }
+
+ $response = Jetpack_Client::wpcom_json_api_request_as_blog(
+ sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
+ Jetpack_Client::WPCOM_JSON_API_VERSION
+ );
+
+ if ( is_wp_error( $response ) ) {
+ /* translators: %1$s is the error code, %2$s is the error message */
+ $message = sprintf( __( 'Connection test failed (#%1$s: %2$s)', 'jetpack' ), $response->get_error_code(), $response->get_error_message() );
+ return self::failing_test( $name, $message );
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+ if ( ! $body ) {
+ $message = __( 'Connection test failed (empty response body)', 'jetpack' ) . wp_remote_retrieve_response_code( $response );
+ return self::failing_test( $name, $message );
+ }
+
+ $result = json_decode( $body );
+ $is_connected = (bool) $result->connected;
+ $message = $result->message . ': ' . wp_remote_retrieve_response_code( $response );
+
+ if ( $is_connected ) {
+ return self::passing_test( $name );
+ } else {
+ return self::failing_test( $name, $message );
+ }
+ }
+
+ /**
+ * Tests the port number to ensure it is an expected value.
+ *
+ * We expect that sites on be on one of:
+ * port 80,
+ * port 443 (https sites only),
+ * the value of JETPACK_SIGNATURE__HTTP_PORT,
+ * unless the site is intentionally on a different port (e.g. example.com:8080 is the site's URL).
+ *
+ * If the value isn't one of those and the site's URL doesn't include a port, then the signature verification will fail.
+ *
+ * This happens most commonly on sites with reverse proxies, so the edge (e.g. Varnish) is running on 80/443, but nginx
+ * or Apache is responding internally on a different port (e.g. 81).
+ *
+ * @return array Test results
+ */
+ protected function test__server_port_value() {
+ $name = __FUNCTION__;
+ if ( ! isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) && ! isset( $_SERVER['SERVER_PORT'] ) ) {
+ $message = 'The server port values are not defined. This is most common when running PHP via a CLI.';
+ return self::skipped_test( $name, $message );
+ }
+ $site_port = wp_parse_url( home_url(), PHP_URL_PORT );
+ $server_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? (int) $_SERVER['HTTP_X_FORWARDED_PORT'] : (int) $_SERVER['SERVER_PORT'];
+ $http_ports = array( 80 );
+ $https_ports = array( 80, 443 );
+
+ if ( defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ) {
+ $http_ports[] = JETPACK_SIGNATURE__HTTP_PORT;
+ }
+
+ if ( defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ) {
+ $https_ports[] = JETPACK_SIGNATURE__HTTPS_PORT;
+ }
+
+ if ( $site_port ) {
+ return self::skipped_test( $name ); // Not currently testing for this situation.
+ }
+
+ if ( is_ssl() && in_array( $server_port, $https_ports, true ) ) {
+ return self::passing_test( $name );
+ } elseif ( in_array( $server_port, $http_ports, true ) ) {
+ return self::passing_test( $name );
+ } else {
+ if ( is_ssl() ) {
+ $needed_constant = 'JETPACK_SIGNATURE__HTTPS_PORT';
+ } else {
+ $needed_constant = 'JETPACK_SIGNATURE__HTTP_PORT';
+ }
+ $message = __( 'The server port value is unexpected.', 'jetpack' );
+ $resolution = __( 'Try adding the following to your wp-config.php file:', 'jetpack' ) . " define( '$needed_constant', $server_port );";
+ return self::failing_test( $name, $message, $resolution );
+ }
+ }
+
+ /**
+ * Calls to WP.com to run the connection diagnostic testing suite.
+ *
+ * Intentionally added last as it will be skipped if any local failed conditions exist.
+ *
+ * @return array Test results.
+ */
+ protected function last__wpcom_self_test() {
+ $name = 'test__wpcom_self_test';
+ if ( ! Jetpack::is_active() || Jetpack::is_development_mode() || Jetpack::is_staging_site() || ! $this->pass ) {
+ return self::skipped_test( $name );
+ }
+
+ $self_xml_rpc_url = site_url( 'xmlrpc.php' );
+
+ $testsite_url = Jetpack::fix_url_for_bad_hosts( JETPACK__API_BASE . 'testsite/1/?url=' );
+
+ add_filter( 'http_request_timeout', array( 'Jetpack_Debugger', 'jetpack_increase_timeout' ) );
+
+ $response = wp_remote_get( $testsite_url . $self_xml_rpc_url );
+
+ remove_filter( 'http_request_timeout', array( 'Jetpack_Debugger', 'jetpack_increase_timeout' ) );
+
+ if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
+ return self::passing_test( $name );
+ } else {
+ return self::failing_test( $name, __( 'Jetpack.com detected an error.', 'jetpack' ), __( 'Visit the Jetpack.com debugging page for more information or contact support.', 'jetpack' ) ); // @todo direct links.
+ }
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/debugger/class-jetpack-debug-data.php b/plugins/jetpack/_inc/lib/debugger/class-jetpack-debug-data.php
new file mode 100644
index 00000000..31e38790
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/debugger/class-jetpack-debug-data.php
@@ -0,0 +1,400 @@
+<?php
+/**
+ * Jetpack Debug Data for the legacy Jetpack debugger page and the WP 5.2-era Site Health sections.
+ *
+ * @package jetpack
+ */
+
+/**
+ * Class Jetpack_Debug_Data
+ *
+ * Collect and return debug data for Jetpack.
+ *
+ * @since 7.3.0
+ */
+class Jetpack_Debug_Data {
+ /**
+ * Determine the active plan and normalize it for the debugger results.
+ *
+ * @since 7.3.0
+ *
+ * @return string The plan slug.
+ */
+ public static function what_jetpack_plan() {
+ $plan = Jetpack_Plan::get();
+ return ! empty( $plan['class'] ) ? $plan['class'] : 'undefined';
+ }
+
+ /**
+ * Convert seconds to human readable time.
+ *
+ * A dedication function instead of using Core functionality to allow for output in seconds.
+ *
+ * @since 7.3.0
+ *
+ * @param int $seconds Number of seconds to convert to human time.
+ *
+ * @return string Human readable time.
+ */
+ public static function seconds_to_time( $seconds ) {
+ $seconds = intval( $seconds );
+ $units = array(
+ 'week' => WEEK_IN_SECONDS,
+ 'day' => DAY_IN_SECONDS,
+ 'hour' => HOUR_IN_SECONDS,
+ 'minute' => MINUTE_IN_SECONDS,
+ 'second' => 1,
+ );
+ // specifically handle zero.
+ if ( 0 === $seconds ) {
+ return '0 seconds';
+ }
+ $human_readable = '';
+ foreach ( $units as $name => $divisor ) {
+ $quot = intval( $seconds / $divisor );
+ if ( $quot ) {
+ $human_readable .= "$quot $name";
+ $human_readable .= ( abs( $quot ) > 1 ? 's' : '' ) . ', ';
+ $seconds -= $quot * $divisor;
+ }
+ }
+ return substr( $human_readable, 0, -2 );
+ }
+
+ /**
+ * Return debug data in the format expected by Core's Site Health Info tab.
+ *
+ * @since 7.3.0
+ *
+ * @param array $debug {
+ * The debug information already compiled by Core.
+ *
+ * @type string $label The title for this section of the debug output.
+ * @type string $description Optional. A description for your information section which may contain basic HTML
+ * markup: `em`, `strong` and `a` for linking to documentation or putting emphasis.
+ * @type boolean $show_count Optional. If set to `true` the amount of fields will be included in the title for
+ * this section.
+ * @type boolean $private Optional. If set to `true` the section and all associated fields will be excluded
+ * from the copy-paste text area.
+ * @type array $fields {
+ * An associative array containing the data to be displayed.
+ *
+ * @type string $label The label for this piece of information.
+ * @type string $value The output that is of interest for this field.
+ * @type boolean $private Optional. If set to `true` the field will not be included in the copy-paste text area
+ * on top of the page, allowing you to show, for example, API keys here.
+ * }
+ * }
+ *
+ * @return array $args Debug information in the same format as the initial argument.
+ */
+ public static function core_debug_data( $debug ) {
+ $jetpack = array(
+ 'jetpack' => array(
+ 'label' => __( 'Jetpack', 'jetpack' ),
+ 'description' => sprintf(
+ /* translators: %1$s is URL to jetpack.com's contact support page. %2$s accessibility text */
+ __(
+ 'Diagnostic information helpful to <a href="%1$s" target="_blank" rel="noopener noreferrer">your Jetpack Happiness team<span class="screen-reader-text">%2$s</span></a>',
+ 'jetpack'
+ ),
+ esc_html( 'https://jetpack.com/contact-support/' ),
+ __( '(opens in a new tab)', 'jetpack' )
+ ),
+ 'fields' => self::debug_data(),
+ ),
+ );
+ $debug = array_merge( $debug, $jetpack );
+ return $debug;
+ }
+
+ /**
+ * Compile and return array of debug information.
+ *
+ * @since 7.3.0
+ *
+ * @return array $args {
+ * Associated array of arrays with the following.
+ * @type string $label The label for this piece of information.
+ * @type string $value The output that is of interest for this field.
+ * @type boolean $private Optional. Set to true if data is sensitive (API keys, etc).
+ * }
+ */
+ public static function debug_data() {
+ $debug_info = array();
+
+ /* Add various important Jetpack options */
+ $debug_info['site_id'] = array(
+ 'label' => 'Jetpack Site ID',
+ 'value' => Jetpack_Options::get_option( 'id' ),
+ 'private' => false,
+ );
+ $debug_info['ssl_cert'] = array(
+ 'label' => 'Jetpack SSL Verfication Bypass',
+ 'value' => ( Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' ) ) ? 'Yes' : 'No',
+ 'private' => false,
+ );
+ $debug_info['time_diff'] = array(
+ 'label' => "Offset between Jetpack server's time and this server's time.",
+ 'value' => Jetpack_Options::get_option( 'time_diff' ),
+ 'private' => false,
+ );
+ $debug_info['version_option'] = array(
+ 'label' => 'Current Jetpack Version Option',
+ 'value' => Jetpack_Options::get_option( 'version' ),
+ 'private' => false,
+ );
+ $debug_info['old_version'] = array(
+ 'label' => 'Previous Jetpack Version',
+ 'value' => Jetpack_Options::get_option( 'old_version' ),
+ 'private' => false,
+ );
+ $debug_info['public'] = array(
+ 'label' => 'Jetpack Site Public',
+ 'value' => ( Jetpack_Options::get_option( 'public' ) ) ? 'Public' : 'Private',
+ 'private' => false,
+ );
+ $debug_info['master_user'] = array(
+ 'label' => 'Jetpack Master User',
+ 'value' => self::human_readable_master_user(),
+ 'private' => false,
+ );
+
+ /**
+ * Token information is private, but awareness if there one is set is helpful.
+ *
+ * To balance out information vs privacy, we only display and include the "key",
+ * which is a segment of the token prior to a period within the token and is
+ * technically not private.
+ *
+ * If a token does not contain a period, then it is malformed and we report it as such.
+ */
+ $user_id = get_current_user_id();
+ $user_tokens = Jetpack_Options::get_option( 'user_tokens' );
+ $blog_token = Jetpack_Options::get_option( 'blog_token' );
+ $user_token = null;
+ if ( is_array( $user_tokens ) && array_key_exists( $user_id, $user_tokens ) ) {
+ $user_token = $user_tokens[ $user_id ];
+ }
+ unset( $user_tokens );
+
+ $tokenset = '';
+ if ( $blog_token ) {
+ $tokenset = 'Blog ';
+ $blog_key = substr( $blog_token, 0, strpos( $blog_token, '.' ) );
+ // Intentionally not translated since this is helpful when sent to Happiness.
+ $blog_key = ( $blog_key ) ? $blog_key : 'Potentially Malformed Token.';
+ }
+ if ( $user_token ) {
+ $tokenset .= 'User';
+ $user_key = substr( $user_token, 0, strpos( $user_token, '.' ) );
+ // Intentionally not translated since this is helpful when sent to Happiness.
+ $user_key = ( $user_key ) ? $user_key : 'Potentially Malformed Token.';
+ }
+ if ( ! $tokenset ) {
+ $tokenset = 'None';
+ }
+
+ $debug_info['current_user'] = array(
+ 'label' => 'Current User',
+ 'value' => self::human_readable_user( $user_id ),
+ 'private' => false,
+ );
+ $debug_info['tokens_set'] = array(
+ 'label' => 'Tokens defined',
+ 'value' => $tokenset,
+ 'private' => false,
+ );
+ $debug_info['blog_token'] = array(
+ 'label' => 'Blog Public Key',
+ 'value' => ( $blog_token ) ? $blog_key : 'Not set.',
+ 'private' => false,
+ );
+ $debug_info['user_token'] = array(
+ 'label' => 'User Public Key',
+ 'value' => ( $user_token ) ? $user_key : 'Not set.',
+ 'private' => false,
+ );
+
+ /** Jetpack Environmental Information */
+ $debug_info['version'] = array(
+ 'label' => 'Jetpack Version',
+ 'value' => JETPACK__VERSION,
+ 'private' => false,
+ );
+ $debug_info['jp_plugin_dir'] = array(
+ 'label' => 'Jetpack Directory',
+ 'value' => JETPACK__PLUGIN_DIR,
+ 'private' => false,
+ );
+ $debug_info['plan'] = array(
+ 'label' => 'Plan Type',
+ 'value' => self::what_jetpack_plan(),
+ 'private' => false,
+ );
+
+ foreach ( array(
+ 'HTTP_HOST',
+ 'SERVER_PORT',
+ 'HTTPS',
+ 'GD_PHP_HANDLER',
+ 'HTTP_AKAMAI_ORIGIN_HOP',
+ 'HTTP_CF_CONNECTING_IP',
+ 'HTTP_CLIENT_IP',
+ 'HTTP_FASTLY_CLIENT_IP',
+ 'HTTP_FORWARDED',
+ 'HTTP_FORWARDED_FOR',
+ 'HTTP_INCAP_CLIENT_IP',
+ 'HTTP_TRUE_CLIENT_IP',
+ 'HTTP_X_CLIENTIP',
+ 'HTTP_X_CLUSTER_CLIENT_IP',
+ 'HTTP_X_FORWARDED',
+ 'HTTP_X_FORWARDED_FOR',
+ 'HTTP_X_IP_TRAIL',
+ 'HTTP_X_REAL_IP',
+ 'HTTP_X_VARNISH',
+ 'REMOTE_ADDR',
+ ) as $header ) {
+ if ( isset( $_SERVER[ $header ] ) ) {
+ $debug_info[ $header ] = array(
+ 'label' => 'Server Variable ' . $header,
+ 'value' => ( $_SERVER[ $header ] ) ? $_SERVER[ $header ] : 'false',
+ 'private' => false,
+ );
+ }
+ }
+
+ $debug_info['protect_header'] = array(
+ 'label' => 'Trusted IP',
+ 'value' => wp_json_encode( get_site_option( 'trusted_ip_header' ) ),
+ 'private' => false,
+ );
+
+ /** Sync Debug Information */
+ /** Load Sync modules */
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-modules.php';
+ /** Load Sync sender */
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-sender.php';
+ /** Load Sync functions */
+ require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-functions.php';
+
+ $sync_module = Jetpack_Sync_Modules::get_module( 'full-sync' );
+ if ( $sync_module ) {
+ $sync_statuses = $sync_module->get_status();
+ $human_readable_sync_status = array();
+ foreach ( $sync_statuses as $sync_status => $sync_status_value ) {
+ $human_readable_sync_status[ $sync_status ] =
+ in_array( $sync_status, array( 'started', 'queue_finished', 'send_started', 'finished' ), true )
+ ? date( 'r', $sync_status_value ) : $sync_status_value;
+ }
+ $debug_info['full_sync'] = array(
+ 'label' => 'Full Sync Status',
+ 'value' => wp_json_encode( $human_readable_sync_status ),
+ 'private' => false,
+ );
+ }
+
+ $queue = Jetpack_Sync_Sender::get_instance()->get_sync_queue();
+
+ $debug_info['sync_size'] = array(
+ 'label' => 'Sync Queue Size',
+ 'value' => $queue->size(),
+ 'private' => false,
+ );
+ $debug_info['sync_lag'] = array(
+ 'label' => 'Sync Queue Lag',
+ 'value' => self::seconds_to_time( $queue->lag() ),
+ 'private' => false,
+ );
+
+ $full_sync_queue = Jetpack_Sync_Sender::get_instance()->get_full_sync_queue();
+
+ $debug_info['full_sync_size'] = array(
+ 'label' => 'Full Sync Queue Size',
+ 'value' => $full_sync_queue->size(),
+ 'private' => false,
+ );
+ $debug_info['full_sync_lag'] = array(
+ 'label' => 'Full Sync Queue Lag',
+ 'value' => self::seconds_to_time( $full_sync_queue->lag() ),
+ 'private' => false,
+ );
+
+ /**
+ * IDC Information
+ *
+ * Must follow sync debug since it depends on sync functionality.
+ */
+ $idc_urls = array(
+ 'home' => Jetpack_Sync_Functions::home_url(),
+ 'siteurl' => Jetpack_Sync_Functions::site_url(),
+ 'WP_HOME' => Jetpack_Constants::is_defined( 'WP_HOME' ) ? Jetpack_Constants::get_constant( 'WP_HOME' ) : '',
+ 'WP_SITEURL' => Jetpack_Constants::is_defined( 'WP_SITEURL' ) ? Jetpack_Constants::get_constant( 'WP_SITEURL' ) : '',
+ );
+
+ $debug_info['idc_urls'] = array(
+ 'label' => 'IDC URLs',
+ 'value' => wp_json_encode( $idc_urls ),
+ 'private' => false,
+ );
+ $debug_info['idc_error_option'] = array(
+ 'label' => 'IDC Error Option',
+ 'value' => wp_json_encode( Jetpack_Options::get_option( 'sync_error_idc' ) ),
+ 'private' => false,
+ );
+ $debug_info['idc_optin'] = array(
+ 'label' => 'IDC Opt-in',
+ 'value' => Jetpack::sync_idc_optin(),
+ 'private' => false,
+ );
+
+ // @todo -- Add testing results?
+ $cxn_tests = new Jetpack_Cxn_Tests();
+ $debug_info['cxn_tests'] = array(
+ 'label' => 'Connection Tests',
+ 'value' => '',
+ 'private' => false,
+ );
+ if ( $cxn_tests->pass() ) {
+ $debug_info['cxn_tests']['value'] = 'All Pass.';
+ } else {
+ $debug_info['cxn_tests']['value'] = wp_json_encode( $cxn_tests->list_fails() );
+ }
+
+ return $debug_info;
+ }
+
+ /**
+ * Returns a human readable string for which user is the master user.
+ *
+ * @return string
+ */
+ private static function human_readable_master_user() {
+ $master_user = Jetpack_Options::get_option( 'master_user' );
+
+ if ( ! $master_user ) {
+ return __( 'No master user set.', 'jetpack' );
+ }
+
+ $user = new WP_User( $master_user );
+
+ if ( ! $user ) {
+ return __( 'Master user no longer exists. Please disconnect and reconnect Jetpack.', 'jetpack' );
+ }
+
+ return self::human_readable_user( $user );
+ }
+
+ /**
+ * Return human readable string for a given user object.
+ *
+ * @param WP_User|int $user Object or ID.
+ *
+ * @return string
+ */
+ private static function human_readable_user( $user ) {
+ $user = new WP_User( $user );
+
+ return sprintf( '#%1$d %2$s (%3$s)', $user->ID, $user->user_login, $user->user_email ); // Format: "#1 username (user@example.com)".
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/debugger/class-jetpack-debugger.php b/plugins/jetpack/_inc/lib/debugger/class-jetpack-debugger.php
new file mode 100644
index 00000000..e7038902
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/debugger/class-jetpack-debugger.php
@@ -0,0 +1,440 @@
+<?php
+/**
+ * Jetpack Debugger functionality allowing for self-service diagnostic information via the legacy jetpack debugger.
+ *
+ * @package jetpack
+ */
+
+/** Ensure the Jetpack_Debug_Data class is available. It should be via the library loaded, but defense is good. */
+require_once 'class-jetpack-debug-data.php';
+
+/**
+ * Class Jetpack_Debugger
+ *
+ * A namespacing class for functionality related to the legacy in-plugin diagnostic tooling.
+ */
+class Jetpack_Debugger {
+
+ /**
+ * Determine the active plan and normalize it for the debugger results.
+ *
+ * @return string The plan slug prepended with "JetpackPlan"
+ */
+ private static function what_jetpack_plan() {
+ // Specifically not deprecating this function since it modifies the output of the Jetpack_Debug_Data::what_jetpack_plan return.
+ return 'JetpackPlan' . Jetpack_Debug_Data::what_jetpack_plan();
+ }
+
+ /**
+ * Convert seconds to human readable time.
+ *
+ * A dedication function instead of using Core functionality to allow for output in seconds.
+ *
+ * @deprecated 7.3.0
+ *
+ * @param int $seconds Number of seconds to convert to human time.
+ *
+ * @return string Human readable time.
+ */
+ public static function seconds_to_time( $seconds ) {
+ _deprecated_function( 'Jetpack_Debugger::seconds_to_time', 'Jetpack 7.3.0', 'Jeptack_Debug_Data::seconds_to_time' );
+ return Jetpack_Debug_Data::seconds_to_time( $seconds );
+ }
+
+ /**
+ * Returns 30 for use with a filter.
+ *
+ * To allow time for WP.com to run upstream testing, this function exists to increase the http_request_timeout value
+ * to 30.
+ *
+ * @return int 30
+ */
+ public static function jetpack_increase_timeout() {
+ return 30; // seconds.
+ }
+
+ /**
+ * Disconnect Jetpack and redirect user to connection flow.
+ */
+ public static function disconnect_and_redirect() {
+ if ( ! ( isset( $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'], 'jp_disconnect' ) ) ) {
+ return;
+ }
+
+ if ( isset( $_GET['disconnect'] ) && $_GET['disconnect'] ) {
+ if ( Jetpack::is_active() ) {
+ Jetpack::disconnect();
+ wp_safe_redirect( Jetpack::admin_url() );
+ exit;
+ }
+ }
+ }
+
+ /**
+ * Handles output to the browser for the in-plugin debugger.
+ */
+ public static function jetpack_debug_display_handler() {
+ global $wp_version;
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'jetpack' ) );
+ }
+
+ $data = Jetpack_Debug_Data::debug_data();
+ $debug_info = '';
+ foreach ( $data as $datum ) {
+ $debug_info .= $datum['label'] . ': ' . $datum['value'] . "\r\n";
+ }
+
+ $debug_info .= "\r\n" . esc_html( 'PHP_VERSION: ' . PHP_VERSION );
+ $debug_info .= "\r\n" . esc_html( 'WORDPRESS_VERSION: ' . $GLOBALS['wp_version'] );
+ $debug_info .= "\r\n" . esc_html( 'SITE_URL: ' . site_url() );
+ $debug_info .= "\r\n" . esc_html( 'HOME_URL: ' . home_url() );
+
+ $debug_info .= "\r\n\r\nTEST RESULTS:\r\n\r\n";
+
+ $cxntests = new Jetpack_Cxn_Tests();
+ ?>
+ <div class="wrap">
+ <h2><?php esc_html_e( 'Debugging Center', 'jetpack' ); ?></h2>
+ <h3><?php esc_html_e( "Testing your site's compatibility with Jetpack...", 'jetpack' ); ?></h3>
+ <div class="jetpack-debug-test-container">
+ <?php
+ if ( $cxntests->pass() ) {
+ echo '<div class="jetpack-tests-succeed">' . esc_html__( 'Your Jetpack setup looks a-okay!', 'jetpack' ) . '</div>';
+ $debug_info .= "All tests passed.\r\n";
+ $debug_info .= print_r( $cxntests->raw_results(), true ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
+ } else {
+ $failures = $cxntests->list_fails();
+ foreach ( $failures as $fail ) {
+ echo '<div class="jetpack-test-error">';
+ echo '<p><a class="jetpack-test-heading" href="#">' . esc_html( $fail['message'] );
+ echo '<span class="noticon noticon-collapse"></span></a></p>';
+ echo '<p class="jetpack-test-details">' . esc_html( $fail['resolution'] ) . '</p>';
+ echo '</div>';
+
+ $debug_info .= "FAILED TESTS!\r\n";
+ $debug_info .= $fail['name'] . ': ' . $fail['message'] . "\r\n";
+ $debug_info .= print_r( $cxntests->raw_results(), true ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
+ }
+ }
+ ?>
+ </div>
+ <div class="entry-content">
+ <h3><?php esc_html_e( 'Trouble with Jetpack?', 'jetpack' ); ?></h3>
+ <h4><?php esc_html_e( 'It may be caused by one of these issues, which you can diagnose yourself:', 'jetpack' ); ?></h4>
+ <ol>
+ <li><b><em>
+ <?php
+ esc_html_e( 'A known issue.', 'jetpack' );
+ ?>
+ </em></b>
+ <?php
+ echo sprintf(
+ wp_kses(
+ /* translators: URLs to Jetpack support pages. */
+ __( 'Some themes and plugins have <a href="%1$s" target="_blank">known conflicts</a> with Jetpack – check the <a href="%2$s" target="_blank">list</a>. (You can also browse the <a href="%3$s" target="_blank">Jetpack support pages</a> or <a href="%4$s" target="_blank">Jetpack support forum</a> to see if others have experienced and solved the problem.)', 'jetpack' ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'target' => array(),
+ ),
+ )
+ ),
+ 'http://jetpack.com/support/getting-started-with-jetpack/known-issues/',
+ 'http://jetpack.com/support/getting-started-with-jetpack/known-issues/',
+ 'http://jetpack.com/support/',
+ 'https://wordpress.org/support/plugin/jetpack'
+ );
+ ?>
+ </li>
+ <li><b><em><?php esc_html_e( 'An incompatible plugin.', 'jetpack' ); ?></em></b> <?php esc_html_e( "Find out by disabling all plugins except Jetpack. If the problem persists, it's not a plugin issue. If the problem is solved, turn your plugins on one by one until the problem pops up again – there's the culprit! Let us know, and we'll try to help.", 'jetpack' ); ?></li>
+ <li>
+ <b><em><?php esc_html_e( 'A theme conflict.', 'jetpack' ); ?></em></b>
+ <?php
+ $default_theme = wp_get_theme( WP_DEFAULT_THEME );
+
+ if ( $default_theme->exists() ) {
+ /* translators: %s is the name of a theme */
+ echo esc_html( sprintf( __( "If your problem isn't known or caused by a plugin, try activating %s (the default WordPress theme).", 'jetpack' ), $default_theme->get( 'Name' ) ) );
+ } else {
+ esc_html_e( "If your problem isn't known or caused by a plugin, try activating the default WordPress theme.", 'jetpack' );
+ }
+ ?>
+ <?php esc_html_e( "If this solves the problem, something in your theme is probably broken – let the theme's author know.", 'jetpack' ); ?>
+ </li>
+ <li><b><em><?php esc_html_e( 'A problem with your XMLRPC file.', 'jetpack' ); ?></em></b>
+ <?php
+ echo sprintf(
+ wp_kses(
+ /* translators: The URL to the site's xmlrpc.php file. */
+ __( 'Load your <a href="%s">XMLRPC file</a>. It should say “XML-RPC server accepts POST requests only.” on a line by itself.', 'jetpack' ),
+ array( 'a' => array( 'href' => array() ) )
+ ),
+ esc_attr( site_url( 'xmlrpc.php' ) )
+ );
+ ?>
+ <ul>
+ <li>- <?php esc_html_e( "If it's not by itself, a theme or plugin is displaying extra characters. Try steps 2 and 3.", 'jetpack' ); ?></li>
+ <li>- <?php esc_html_e( 'If you get a 404 message, contact your web host. Their security may block XMLRPC.', 'jetpack' ); ?></li>
+ </ul>
+ </li>
+ <?php if ( current_user_can( 'jetpack_disconnect' ) && Jetpack::is_active() ) : ?>
+ <li>
+ <strong><em><?php esc_html_e( 'A connection problem with WordPress.com.', 'jetpack' ); ?></em></strong>
+ <?php
+ echo sprintf(
+ wp_kses(
+ /* translators: URL to disconnect and reconnect Jetpack. */
+ __( 'Jetpack works by connecting to WordPress.com for a lot of features. Sometimes, when the connection gets messed up, you need to disconnect and reconnect to get things working properly. <a href="%s">Disconnect from WordPress.com</a>', 'jetpack' ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'class' => array(),
+ ),
+ )
+ ),
+ esc_attr(
+ wp_nonce_url(
+ Jetpack::admin_url(
+ array(
+ 'page' => 'jetpack-debugger',
+ 'disconnect' => true,
+ )
+ ),
+ 'jp_disconnect',
+ 'nonce'
+ )
+ )
+ );
+ ?>
+ </li>
+ <?php endif; ?>
+ </ol>
+ <h4><?php esc_html_e( 'Still having trouble?', 'jetpack' ); ?></h4>
+ <p><b><em><?php esc_html_e( 'Ask us for help!', 'jetpack' ); ?></em></b>
+ <?php
+ /**
+ * Offload to new WordPress debug data in WP 5.2+
+ *
+ * @todo remove fallback when 5.2 is the minimum supported.
+ */
+ if ( version_compare( $wp_version, '5.2-alpha', '>=' ) ) {
+ echo sprintf(
+ wp_kses(
+ /* translators: URL for Jetpack support. URL for WordPress's Site Health */
+ __( '<a href="%1$s">Contact our Happiness team</a>. When you do, please include the <a href="%2$s">full debug information from your site</a>.', 'jetpack' ),
+ array( 'a' => array( 'href' => array() ) )
+ ),
+ 'https://jetpack.com/contact-support/',
+ esc_url( admin_url() . 'site-health.php?tab=debug' )
+ );
+ $hide_debug = true;
+ } else { // Versions before 5.2, fallback.
+ echo sprintf(
+ wp_kses(
+ /* translators: URL for Jetpack support. */
+ __( '<a href="%s">Contact our Happiness team</a>. When you do, please include the full debug information below.', 'jetpack' ),
+ array( 'a' => array( 'href' => array() ) )
+ ),
+ 'https://jetpack.com/contact-support/'
+ );
+ $hide_debug = false;
+ }
+ ?>
+ </p>
+ <hr />
+ <?php if ( Jetpack::is_active() ) : ?>
+ <div id="connected-user-details">
+ <h3><?php esc_html_e( 'More details about your Jetpack settings', 'jetpack' ); ?></h3>
+ <p>
+ <?php
+ printf(
+ wp_kses(
+ /* translators: %s is an e-mail address */
+ __( 'The primary connection is owned by <strong>%s</strong>\'s WordPress.com account.', 'jetpack' ),
+ array( 'strong' => array() )
+ ),
+ esc_html( Jetpack::get_master_user_email() )
+ );
+ ?>
+ </p>
+ </div>
+ <?php else : ?>
+ <div id="dev-mode-details">
+ <p>
+ <?php
+ printf(
+ wp_kses(
+ /* translators: Link to a Jetpack support page. */
+ __( 'Would you like to use Jetpack on your local development site? You can do so thanks to <a href="%s">Jetpack\'s development mode</a>.', 'jetpack' ),
+ array( 'a' => array( 'href' => array() ) )
+ ),
+ 'https://jetpack.com/support/development-mode/'
+ );
+ ?>
+ </p>
+ </div>
+ <?php endif; ?>
+ <?php
+ if (
+ current_user_can( 'jetpack_manage_modules' )
+ && ( Jetpack::is_development_mode() || Jetpack::is_active() )
+ ) {
+ printf(
+ wp_kses(
+ '<p><a href="%1$s">%2$s</a></p>',
+ array(
+ 'a' => array( 'href' => array() ),
+ 'p' => array(),
+ )
+ ),
+ esc_attr( Jetpack::admin_url( 'page=jetpack_modules' ) ),
+ esc_html__( 'Access the full list of Jetpack modules available on your site.', 'jetpack' )
+ );
+ }
+ ?>
+ </div>
+ <hr />
+ <?php
+ if ( ! $hide_debug ) {
+ ?>
+ <div id="toggle_debug_info"><?php esc_html_e( 'Advanced Debug Results', 'jetpack' ); ?></div>
+ <div id="debug_info_div">
+ <h4><?php esc_html_e( 'Debug Info', 'jetpack' ); ?></h4>
+ <div id="debug_info"><pre><?php echo esc_html( $debug_info ); ?></pre></div>
+ </div>
+ <?php
+ }
+ ?>
+ </div>
+ <?php
+ }
+
+ /**
+ * Outputs html needed within the <head> for the in-plugin debugger page.
+ */
+ public static function jetpack_debug_admin_head() {
+
+ Jetpack_Admin_Page::load_wrapper_styles();
+ ?>
+ <style type="text/css">
+
+ .jetpack-debug-test-container {
+ margin-top: 20px;
+ margin-bottom: 30px;
+ }
+
+ .jetpack-tests-succeed {
+ font-size: large;
+ color: #8BAB3E;
+ }
+
+ .jetpack-test-details {
+ margin: 4px 6px;
+ padding: 10px;
+ overflow: auto;
+ display: none;
+ }
+
+ .jetpack-test-error {
+ margin-bottom: 10px;
+ background: #FFEBE8;
+ border: solid 1px #C00;
+ border-radius: 3px;
+ }
+
+ .jetpack-test-error p {
+ margin: 0;
+ padding: 0;
+ }
+
+ p.jetpack-test-details {
+ margin: 4px 6px;
+ padding: 10px;
+ }
+
+ .jetpack-test-error a.jetpack-test-heading {
+ padding: 4px 6px;
+ display: block;
+ text-decoration: none;
+ color: inherit;
+ }
+
+ .jetpack-test-error .noticon {
+ float: right;
+ }
+
+ .formbox {
+ margin: 0 0 25px 0;
+ }
+
+ .formbox input[type="text"], .formbox input[type="email"], .formbox input[type="url"], .formbox textarea, #debug_info_div {
+ border: 1px solid #e5e5e5;
+ border-radius: 11px;
+ box-shadow: inset 0 1px 1px rgba(0,0,0,0.1);
+ color: #666;
+ font-size: 14px;
+ padding: 10px;
+ width: 97%;
+ }
+ #debug_info_div {
+ border-radius: 0;
+ margin-top: 16px;
+ background: #FFF;
+ padding: 16px;
+ }
+ .formbox .contact-support input[type="submit"] {
+ float: right;
+ margin: 0 !important;
+ border-radius: 20px !important;
+ cursor: pointer;
+ font-size: 13pt !important;
+ height: auto !important;
+ margin: 0 0 2em 10px !important;
+ padding: 8px 16px !important;
+ background-color: #ddd;
+ border: 1px solid rgba(0,0,0,0.05);
+ border-top-color: rgba(255,255,255,0.1);
+ border-bottom-color: rgba(0,0,0,0.15);
+ color: #333;
+ font-weight: 400;
+ display: inline-block;
+ text-align: center;
+ text-decoration: none;
+ }
+
+ .formbox span.errormsg {
+ margin: 0 0 10px 10px;
+ color: #d00;
+ display: none;
+ }
+
+ .formbox.error span.errormsg {
+ display: block;
+ }
+
+ #debug_info_div, #toggle_debug_info, #debug_info_div p {
+ font-size: 12px;
+ }
+
+ #category_div ul li {
+ list-style-type: none;
+ }
+
+ </style>
+ <script type="text/javascript">
+ jQuery( document ).ready( function($) {
+
+ $( '#debug_info' ).prepend( 'jQuery version: ' + jQuery.fn.jquery + "\r\n" );
+ $( '#debug_form_info' ).prepend( 'jQuery version: ' + jQuery.fn.jquery + "\r\n" );
+
+ $( '.jetpack-test-error .jetpack-test-heading' ).on( 'click', function() {
+ $( this ).parents( '.jetpack-test-error' ).find( '.jetpack-test-details' ).slideToggle();
+ return false;
+ } );
+
+ } );
+ </script>
+ <?php
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/debugger/debug-functions-for-php53.php b/plugins/jetpack/_inc/lib/debugger/debug-functions-for-php53.php
new file mode 100644
index 00000000..a32d9fee
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/debugger/debug-functions-for-php53.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * WP Site Health functionality temporarily stored in this file until all of Jetpack is PHP 5.3+
+ *
+ * @package Jetpack.
+ */
+
+/**
+ * Test runner for Core's Site Health module.
+ *
+ * @since 7.3.0
+ */
+function jetpack_debugger_ajax_local_testing_suite() {
+ check_ajax_referer( 'health-check-site-status' );
+ if ( ! current_user_can( 'jetpack_manage_modules' ) ) {
+ wp_send_json_error();
+ }
+ $tests = new Jetpack_Cxn_Tests();
+ wp_send_json_success( $tests->output_results_for_core_async_site_health() );
+}
+/**
+ * Adds the Jetpack Local Testing Suite to the Core Site Health system.
+ *
+ * @since 7.3.0
+ *
+ * @param array $core_tests Array of tests from Core's Site Health.
+ *
+ * @return array $core_tests Array of tests for Core's Site Health.
+ */
+function jetpack_debugger_site_status_tests( $core_tests ) {
+ $cxn_tests = new Jetpack_Cxn_Tests();
+ $tests = $cxn_tests->list_tests( 'direct' );
+ foreach ( $tests as $test ) {
+ $core_tests['direct'][ $test['name'] ] = array(
+ 'label' => __( 'Jetpack: ', 'jetpack' ) . $test['name'],
+ 'test' => function() use ( $test, $cxn_tests ) { // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.Found
+ $results = $cxn_tests->run_test( $test['name'] );
+ // Test names are, by default, `test__some_string_of_text`. Let's convert to "Some String Of Text" for humans.
+ $label = ucwords(
+ str_replace(
+ '_',
+ ' ',
+ str_replace( 'test__', '', $test['name'] )
+ )
+ );
+ $return = array(
+ 'label' => $label,
+ 'status' => 'good',
+ 'badge' => array(
+ 'label' => __( 'Jetpack', 'jetpack' ),
+ 'color' => 'green',
+ ),
+ 'description' => sprintf(
+ '<p>%s</p>',
+ __( 'This test successfully passed!', 'jetpack' )
+ ),
+ 'actions' => '',
+ 'test' => 'jetpack_' . $test['name'],
+ );
+ if ( is_wp_error( $results ) ) {
+ return;
+ }
+ if ( false === $results['pass'] ) {
+ $return['label'] = $results['message'];
+ $return['status'] = $results['severity'];
+ $return['description'] = sprintf(
+ '<p>%s</p>',
+ $results['resolution']
+ );
+ if ( ! empty( $results['action'] ) ) {
+ $return['actions'] = sprintf(
+ '<a class="button button-primary" href="%1$s" target="_blank" rel="noopener noreferrer">%2$s <span class="screen-reader-text">%3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+ esc_url( $results['action'] ),
+ __( 'Resolve', 'jetpack' ),
+ /* translators: accessibility text */
+ __( '(opens in a new tab)', 'jetpack' )
+ );
+ }
+ }
+
+ return $return;
+ },
+ );
+ }
+ $core_tests['async']['jetpack_test_suite'] = array(
+ 'label' => __( 'Jetpack Tests', 'jetpack' ),
+ 'test' => 'jetpack_local_testing_suite',
+ );
+
+ return $core_tests;
+}
+
diff --git a/plugins/jetpack/_inc/lib/functions.wp-notify.php b/plugins/jetpack/_inc/lib/functions.wp-notify.php
new file mode 100644
index 00000000..6be0c3ac
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/functions.wp-notify.php
@@ -0,0 +1,353 @@
+<?php
+
+if ( ! function_exists( 'wp_notify_postauthor' ) && Jetpack::is_active() ) :
+ /**
+ * Notify an author (and/or others) of a comment/trackback/pingback on a post.
+ *
+ * @since 1.0.0
+ *
+ * @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
+ * @param string $deprecated Not used
+ * @return bool True on completion. False if no email addresses were specified.
+ */
+ function wp_notify_postauthor( $comment_id, $deprecated = null ) {
+ if ( null !== $deprecated ) {
+ _deprecated_argument( __FUNCTION__, '3.8.0' );
+ }
+
+ $comment = get_comment( $comment_id );
+
+ if ( empty( $comment ) || empty( $comment->comment_post_ID ) ) {
+ return false;
+ }
+
+ $post = get_post( $comment->comment_post_ID );
+ $author = get_userdata( $post->post_author );
+
+ // Who to notify? By default, just the post author, but others can be added.
+ $emails = array();
+ if ( $author ) {
+ $emails[] = $author->user_email;
+ }
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $emails = apply_filters( 'comment_notification_recipients', $emails, $comment->comment_ID );
+ $emails = array_filter( $emails );
+
+ // If there are no addresses to send the comment to, bail.
+ if ( ! count( $emails ) ) {
+ return false;
+ }
+
+ // Facilitate unsetting below without knowing the keys.
+ $emails = array_flip( $emails );
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $notify_author = apply_filters( 'comment_notification_notify_author', false, $comment->comment_ID );
+
+ // The comment was left by the author
+ if ( $author && ! $notify_author && $comment->user_id == $post->post_author ) {
+ unset( $emails[ $author->user_email ] );
+ }
+
+ // The author moderated a comment on their own post
+ if ( $author && ! $notify_author && $post->post_author == get_current_user_id() ) {
+ unset( $emails[ $author->user_email ] );
+ }
+
+ // The post author is no longer a member of the blog
+ if ( $author && ! $notify_author && ! user_can( $post->post_author, 'read_post', $post->ID ) ) {
+ unset( $emails[ $author->user_email ] );
+ }
+
+ // If there's no email to send the comment to, bail, otherwise flip array back around for use below
+ if ( ! count( $emails ) ) {
+ return false;
+ } else {
+ $emails = array_flip( $emails );
+ }
+
+ $switched_locale = switch_to_locale( get_locale() );
+
+ $comment_author_domain = @gethostbyaddr( $comment->comment_author_IP );
+
+ // The blogname option is escaped with esc_html on the way into the database in sanitize_option
+ // we want to reverse this for the plain text arena of emails.
+ $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
+ $comment_content = wp_specialchars_decode( $comment->comment_content );
+
+ function is_user_connected( $email ) {
+ $user = get_user_by( 'email', $email );
+ return Jetpack::is_user_connected( $user->ID );
+ }
+
+ $moderate_on_wpcom = ! in_array( false, array_map( 'is_user_connected', $emails ) );
+
+ $primary_site_slug = Jetpack::build_raw_urls( get_home_url() );
+
+ switch ( $comment->comment_type ) {
+ case 'trackback':
+ /* translators: 1: Post title */
+ $notify_message = sprintf( __( 'New trackback on your post "%s"' ), $post->post_title ) . "\r\n";
+ /* translators: 1: Trackback/pingback website name, 2: website IP address, 3: website hostname */
+ $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
+ $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
+ $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
+ $notify_message .= __( 'You can see all trackbacks on this post here:' ) . "\r\n";
+ /* translators: 1: blog name, 2: post title */
+ $subject = sprintf( __( '[%1$s] Trackback: "%2$s"' ), $blogname, $post->post_title );
+ break;
+ case 'pingback':
+ /* translators: 1: Post title */
+ $notify_message = sprintf( __( 'New pingback on your post "%s"' ), $post->post_title ) . "\r\n";
+ /* translators: 1: Trackback/pingback website name, 2: website IP address, 3: website hostname */
+ $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
+ $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
+ $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
+ $notify_message .= __( 'You can see all pingbacks on this post here:' ) . "\r\n";
+ /* translators: 1: blog name, 2: post title */
+ $subject = sprintf( __( '[%1$s] Pingback: "%2$s"' ), $blogname, $post->post_title );
+ break;
+ default: // Comments
+ $notify_message = sprintf( __( 'New comment on your post "%s"' ), $post->post_title ) . "\r\n";
+ /* translators: 1: comment author, 2: comment author's IP address, 3: comment author's hostname */
+ $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
+ $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n";
+ $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
+ $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
+ $notify_message .= __( 'You can see all comments on this post here:' ) . "\r\n";
+ /* translators: 1: blog name, 2: post title */
+ $subject = sprintf( __( '[%1$s] Comment: "%2$s"' ), $blogname, $post->post_title );
+ break;
+ }
+
+ $notify_message .= $moderate_on_wpcom
+ ? "https://wordpress.com/comments/all/{$primary_site_slug}/{$comment->comment_post_ID}/\r\n\r\n"
+ : get_permalink( $comment->comment_post_ID ) . "#comments\r\n\r\n";
+
+ $notify_message .= sprintf( __( 'Permalink: %s' ), get_comment_link( $comment ) ) . "\r\n";
+
+ if ( user_can( $post->post_author, 'edit_comment', $comment->comment_ID ) ) {
+ if ( EMPTY_TRASH_DAYS ) {
+ $notify_message .= sprintf(
+ __( 'Trash it: %s' ), $moderate_on_wpcom
+ ? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=trash"
+ : admin_url( "comment.php?action=trash&c={$comment->comment_ID}#wpbody-content" )
+ ) . "\r\n";
+ } else {
+ $notify_message .= sprintf(
+ __( 'Delete it: %s' ), $moderate_on_wpcom
+ ? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=delete"
+ : admin_url( "comment.php?action=delete&c={$comment->comment_ID}#wpbody-content" )
+ ) . "\r\n";
+ }
+ $notify_message .= sprintf(
+ __( 'Spam it: %s' ), $moderate_on_wpcom ?
+ "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=spam"
+ : admin_url( "comment.php?action=spam&c={$comment->comment_ID}#wpbody-content" )
+ ) . "\r\n";
+ }
+
+ $wp_email = 'wordpress@' . preg_replace( '#^www\.#', '', strtolower( $_SERVER['SERVER_NAME'] ) );
+
+ if ( '' == $comment->comment_author ) {
+ $from = "From: \"$blogname\" <$wp_email>";
+ if ( '' != $comment->comment_author_email ) {
+ $reply_to = "Reply-To: $comment->comment_author_email";
+ }
+ } else {
+ $from = "From: \"$comment->comment_author\" <$wp_email>";
+ if ( '' != $comment->comment_author_email ) {
+ $reply_to = "Reply-To: \"$comment->comment_author_email\" <$comment->comment_author_email>";
+ }
+ }
+
+ $message_headers = "$from\n"
+ . 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . "\"\n";
+
+ if ( isset( $reply_to ) ) {
+ $message_headers .= $reply_to . "\n";
+ }
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $notify_message = apply_filters( 'comment_notification_text', $notify_message, $comment->comment_ID );
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $subject = apply_filters( 'comment_notification_subject', $subject, $comment->comment_ID );
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $message_headers = apply_filters( 'comment_notification_headers', $message_headers, $comment->comment_ID );
+
+ foreach ( $emails as $email ) {
+ @wp_mail( $email, wp_specialchars_decode( $subject ), $notify_message, $message_headers );
+ }
+
+ if ( $switched_locale ) {
+ restore_previous_locale();
+ }
+
+ return true;
+ }
+endif;
+
+if ( ! function_exists( 'wp_notify_moderator' ) && Jetpack::is_active() ) :
+ /**
+ * Notifies the moderator of the site about a new comment that is awaiting approval.
+ *
+ * @since 1.0.0
+ *
+ * @global wpdb $wpdb WordPress database abstraction object.
+ *
+ * Uses the {@see 'notify_moderator'} filter to determine whether the site moderator
+ * should be notified, overriding the site setting.
+ *
+ * @param int $comment_id Comment ID.
+ * @return true Always returns true.
+ */
+ function wp_notify_moderator( $comment_id ) {
+ global $wpdb;
+
+ $maybe_notify = get_option( 'moderation_notify' );
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $maybe_notify = apply_filters( 'notify_moderator', $maybe_notify, $comment_id );
+
+ if ( ! $maybe_notify ) {
+ return true;
+ }
+
+ $comment = get_comment( $comment_id );
+ $post = get_post( $comment->comment_post_ID );
+ $user = get_userdata( $post->post_author );
+ // Send to the administration and to the post author if the author can modify the comment.
+ $emails = array( get_option( 'admin_email' ) );
+ if ( $user && user_can( $user->ID, 'edit_comment', $comment_id ) && ! empty( $user->user_email ) ) {
+ if ( 0 !== strcasecmp( $user->user_email, get_option( 'admin_email' ) ) ) {
+ $emails[] = $user->user_email;
+ }
+ }
+
+ $switched_locale = switch_to_locale( get_locale() );
+
+ $comment_author_domain = @gethostbyaddr( $comment->comment_author_IP );
+ $comments_waiting = $wpdb->get_var( "SELECT count(comment_ID) FROM $wpdb->comments WHERE comment_approved = '0'" );
+
+ // The blogname option is escaped with esc_html on the way into the database in sanitize_option
+ // we want to reverse this for the plain text arena of emails.
+ $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
+ $comment_content = wp_specialchars_decode( $comment->comment_content );
+
+ switch ( $comment->comment_type ) {
+ case 'trackback':
+ /* translators: 1: Post title */
+ $notify_message = sprintf( __( 'A new trackback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
+ $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
+ /* translators: 1: Trackback/pingback website name, 2: website IP address, 3: website hostname */
+ $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
+ /* translators: 1: Trackback/pingback/comment author URL */
+ $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
+ $notify_message .= __( 'Trackback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n";
+ break;
+ case 'pingback':
+ /* translators: 1: Post title */
+ $notify_message = sprintf( __( 'A new pingback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
+ $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
+ /* translators: 1: Trackback/pingback website name, 2: website IP address, 3: website hostname */
+ $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
+ /* translators: 1: Trackback/pingback/comment author URL */
+ $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
+ $notify_message .= __( 'Pingback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n";
+ break;
+ default: // Comments
+ /* translators: 1: Post title */
+ $notify_message = sprintf( __( 'A new comment on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
+ $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
+ /* translators: 1: Comment author name, 2: comment author's IP address, 3: comment author's hostname */
+ $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
+ /* translators: 1: Comment author URL */
+ $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n";
+ /* translators: 1: Trackback/pingback/comment author URL */
+ $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
+ /* translators: 1: Comment text */
+ $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
+ break;
+ }
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $emails = apply_filters( 'comment_moderation_recipients', $emails, $comment_id );
+
+ function is_user_connected( $email ) {
+ $user = get_user_by( 'email', $email );
+ return Jetpack::is_user_connected( $user->ID );
+ }
+
+ $moderate_on_wpcom = ! in_array( false, array_map( 'is_user_connected', $emails ) );
+
+ $primary_site_slug = Jetpack::build_raw_urls( get_home_url() );
+
+ /* translators: Comment moderation. 1: Comment action URL */
+ $notify_message .= sprintf(
+ __( 'Approve it: %s' ), $moderate_on_wpcom
+ ? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=approve"
+ : admin_url( "comment.php?action=approve&c={$comment_id}#wpbody-content" )
+ ) . "\r\n";
+
+ if ( EMPTY_TRASH_DAYS ) {
+ /* translators: Comment moderation. 1: Comment action URL */
+ $notify_message .= sprintf(
+ __( 'Trash it: %s' ), $moderate_on_wpcom
+ ? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=trash"
+ : admin_url( "comment.php?action=trash&c={$comment_id}#wpbody-content" )
+ ) . "\r\n";
+ } else {
+ /* translators: Comment moderation. 1: Comment action URL */
+ $notify_message .= sprintf(
+ __( 'Delete it: %s' ), $moderate_on_wpcom
+ ? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=delete"
+ : admin_url( "comment.php?action=delete&c={$comment_id}#wpbody-content" )
+ ) . "\r\n";
+ }
+
+ /* translators: Comment moderation. 1: Comment action URL */
+ $notify_message .= sprintf(
+ __( 'Spam it: %s' ), $moderate_on_wpcom
+ ? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=spam"
+ : admin_url( "comment.php?action=spam&c={$comment_id}#wpbody-content" )
+ ) . "\r\n";
+
+ /* translators: Comment moderation. 1: Number of comments awaiting approval */
+ $notify_message .= sprintf(
+ _n(
+ 'Currently %s comment is waiting for approval. Please visit the moderation panel:',
+ 'Currently %s comments are waiting for approval. Please visit the moderation panel:', $comments_waiting
+ ), number_format_i18n( $comments_waiting )
+ ) . "\r\n";
+
+ $notify_message .= $moderate_on_wpcom
+ ? "https://wordpress.com/comments/pending/{$primary_site_slug}/"
+ : admin_url( 'edit-comments.php?comment_status=moderated#wpbody-content' ) . "\r\n";
+
+ /* translators: Comment moderation notification email subject. 1: Site name, 2: Post title */
+ $subject = sprintf( __( '[%1$s] Please moderate: "%2$s"' ), $blogname, $post->post_title );
+ $message_headers = '';
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $notify_message = apply_filters( 'comment_moderation_text', $notify_message, $comment_id );
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $subject = apply_filters( 'comment_moderation_subject', $subject, $comment_id );
+
+ /** This filter is documented in core/src/wp-includes/pluggable.php */
+ $message_headers = apply_filters( 'comment_moderation_headers', $message_headers, $comment_id );
+
+ foreach ( $emails as $email ) {
+ @wp_mail( $email, wp_specialchars_decode( $subject ), $notify_message, $message_headers );
+ }
+
+ if ( $switched_locale ) {
+ restore_previous_locale();
+ }
+
+ return true;
+ }
+endif;
diff --git a/plugins/jetpack/_inc/lib/icalendar-reader.php b/plugins/jetpack/_inc/lib/icalendar-reader.php
new file mode 100644
index 00000000..f7e047f9
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/icalendar-reader.php
@@ -0,0 +1,913 @@
+<?php
+
+/**
+ * Gets and renders iCal feeds for the Upcoming Events widget and shortcode
+ */
+
+class iCalendarReader {
+
+ public $todo_count = 0;
+ public $event_count = 0;
+ public $cal = array();
+ public $_lastKeyWord = '';
+ public $timezone = null;
+
+ /**
+ * Class constructor
+ *
+ * @return void
+ */
+ public function __construct() {}
+
+ /**
+ * Return an array of events
+ *
+ * @param string $url (default: '')
+ * @return array | false on failure
+ */
+ public function get_events( $url = '', $count = 5 ) {
+ $count = (int) $count;
+ $transient_id = 'icalendar_vcal_' . md5( $url ) . '_' . $count;
+
+ $vcal = get_transient( $transient_id );
+
+ if ( ! empty( $vcal ) ) {
+ if ( isset( $vcal['TIMEZONE'] ) )
+ $this->timezone = $this->timezone_from_string( $vcal['TIMEZONE'] );
+
+ if ( isset( $vcal['VEVENT'] ) ) {
+ $vevent = $vcal['VEVENT'];
+
+ if ( $count > 0 )
+ $vevent = array_slice( $vevent, 0, $count );
+
+ $this->cal['VEVENT'] = $vevent;
+
+ return $this->cal['VEVENT'];
+ }
+ }
+
+ if ( ! $this->parse( $url ) )
+ return false;
+
+ $vcal = array();
+
+ if ( $this->timezone ) {
+ $vcal['TIMEZONE'] = $this->timezone->getName();
+ } else {
+ $this->timezone = $this->timezone_from_string( '' );
+ }
+
+ if ( ! empty( $this->cal['VEVENT'] ) ) {
+ $vevent = $this->cal['VEVENT'];
+
+ // check for recurring events
+ // $vevent = $this->add_recurring_events( $vevent );
+
+ // remove before caching - no sense in hanging onto the past
+ $vevent = $this->filter_past_and_recurring_events( $vevent );
+
+ // order by soonest start date
+ $vevent = $this->sort_by_recent( $vevent );
+
+ $vcal['VEVENT'] = $vevent;
+ }
+
+ set_transient( $transient_id, $vcal, HOUR_IN_SECONDS );
+
+ if ( !isset( $vcal['VEVENT'] ) )
+ return false;
+
+ if ( $count > 0 )
+ return array_slice( $vcal['VEVENT'], 0, $count );
+
+ return $vcal['VEVENT'];
+ }
+
+ function apply_timezone_offset( $events ) {
+ if ( ! $events ) {
+ return $events;
+ }
+
+ // get timezone offset from the timezone name.
+ $timezone_name = get_option( 'timezone_string' );
+ if ( $timezone_name ) {
+ $timezone = new DateTimeZone( $timezone_name );
+ $timezone_offset_interval = false;
+ } else {
+ // If the timezone isn't set then the GMT offset must be set.
+ // generate a DateInterval object from the timezone offset
+ $gmt_offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
+ $timezone_offset_interval = date_interval_create_from_date_string( "{$gmt_offset} seconds" );
+ $timezone = new DateTimeZone( 'UTC' );
+ }
+
+ $offsetted_events = array();
+
+ foreach ( $events as $event ) {
+ // Don't handle all-day events
+ if ( 8 < strlen( $event['DTSTART'] ) ) {
+ $start_time = preg_replace( '/Z$/', '', $event['DTSTART'] );
+ $start_time = new DateTime( $start_time, $this->timezone );
+ $start_time->setTimeZone( $timezone );
+
+ $end_time = preg_replace( '/Z$/', '', $event['DTEND'] );
+ $end_time = new DateTime( $end_time, $this->timezone );
+ $end_time->setTimeZone( $timezone );
+
+ if ( $timezone_offset_interval ) {
+ $start_time->add( $timezone_offset_interval );
+ $end_time->add( $timezone_offset_interval );
+ }
+
+ $event['DTSTART'] = $start_time->format( 'YmdHis\Z' );
+ $event['DTEND'] = $end_time->format( 'YmdHis\Z' );
+ }
+
+ $offsetted_events[] = $event;
+ }
+
+ return $offsetted_events;
+ }
+
+ protected function filter_past_and_recurring_events( $events ) {
+ $upcoming = array();
+ $set_recurring_events = array();
+ $recurrences = array();
+ /**
+ * This filter allows any time to be passed in for testing or changing timezones, etc...
+ *
+ * @module widgets
+ *
+ * @since 3.4.0
+ *
+ * @param object time() A time object.
+ */
+ $current = apply_filters( 'ical_get_current_time', time() );
+
+ foreach ( $events as $event ) {
+
+ $date_from_ics = strtotime( $event['DTSTART'] );
+ if ( isset( $event['DTEND'] ) ) {
+ $duration = strtotime( $event['DTEND'] ) - strtotime( $event['DTSTART'] );
+ } else {
+ $duration = 0;
+ }
+
+ if ( isset( $event['RRULE'] ) && $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
+ try {
+ $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone('UTC') );
+ $adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
+ $event['DTSTART'] = $adjusted_time->format('Ymd\THis');
+ $date_from_ics = strtotime( $event['DTSTART'] );
+
+ $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
+ } catch ( Exception $e ) {
+ // Invalid argument to DateTime
+ }
+
+ if ( isset( $event['EXDATE'] ) ) {
+ $exdates = array();
+ foreach ( (array) $event['EXDATE'] as $exdate ) {
+ try {
+ $adjusted_time = new DateTime( $exdate, new DateTimeZone('UTC') );
+ $adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
+ if ( 8 == strlen( $event['DTSTART'] ) ) {
+ $exdates[] = $adjusted_time->format( 'Ymd' );
+ } else {
+ $exdates[] = $adjusted_time->format( 'Ymd\THis' );
+ }
+ } catch ( Exception $e ) {
+ // Invalid argument to DateTime
+ }
+ }
+ $event['EXDATE'] = $exdates;
+ } else {
+ $event['EXDATE'] = array();
+ }
+ }
+
+ if ( ! isset( $event['DTSTART'] ) ) {
+ continue;
+ }
+
+ // Process events with RRULE before other events
+ $rrule = isset( $event['RRULE'] ) ? $event['RRULE'] : false ;
+ $uid = $event['UID'];
+
+ if ( $rrule && ! in_array( $uid, $set_recurring_events ) ) {
+
+ // Break down the RRULE into digestible chunks
+ $rrule_array = array();
+
+ foreach ( explode( ";", $event['RRULE'] ) as $rline ) {
+ list( $rkey, $rvalue ) = explode( "=", $rline, 2 );
+ $rrule_array[$rkey] = $rvalue;
+ }
+
+ $interval = ( isset( $rrule_array['INTERVAL'] ) ) ? $rrule_array['INTERVAL'] : 1;
+ $rrule_count = ( isset( $rrule_array['COUNT'] ) ) ? $rrule_array['COUNT'] : 0;
+ $until = ( isset( $rrule_array['UNTIL'] ) ) ? strtotime( $rrule_array['UNTIL'] ) : strtotime( '+1 year', $current );
+
+ // Used to bound event checks
+ $echo_limit = 10;
+ $noop = false;
+
+ // Set bydays for the event
+ $weekdays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );
+ $bydays = $weekdays;
+
+ // Calculate a recent start date for incrementing depending on the frequency and interval
+ switch ( $rrule_array['FREQ'] ) {
+
+ case 'DAILY':
+ $frequency = 'day';
+ $echo_limit = 10;
+
+ if ( $date_from_ics >= $current ) {
+ $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
+ } else {
+ // Interval and count
+ $catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * DAY_IN_SECONDS ) );
+ if ( $rrule_count && $catchup > 0 ) {
+ if ( $catchup < $rrule_count ) {
+ $rrule_count = $rrule_count - $catchup;
+ $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ } else {
+ $noop = true;
+ }
+ } else {
+ $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ }
+ }
+ break;
+
+ case 'WEEKLY':
+ $frequency = 'week';
+ $echo_limit = 4;
+
+ // BYDAY exception to current date
+ $day = false;
+ if ( ! isset( $rrule_array['BYDAY'] ) ) {
+ $day = $rrule_array['BYDAY'] = strtoupper( substr( date( 'D', strtotime( $event['DTSTART'] ) ), 0, 2 ) );
+ }
+ $bydays = explode( ',', $rrule_array['BYDAY'] );
+
+ if ( $date_from_ics >= $current ) {
+ $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
+ } else {
+ // Interval and count
+ $catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * WEEK_IN_SECONDS ) );
+ if ( $rrule_count && $catchup > 0 ) {
+ if ( ( $catchup * count( $bydays ) ) < $rrule_count ) {
+ $rrule_count = $rrule_count - ( $catchup * count( $bydays ) ); // Estimate current event count
+ $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ } else {
+ $noop = true;
+ }
+ } else {
+ $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ }
+ }
+
+ // Set to Sunday start
+ if ( ! $noop && 'SU' !== strtoupper( substr( date( 'D', strtotime( $recurring_event_date_start ) ), 0, 2 ) ) ) {
+ $recurring_event_date_start = date( 'Ymd', strtotime( "last Sunday", strtotime( $recurring_event_date_start ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ }
+ break;
+
+ case 'MONTHLY':
+ $frequency = 'month';
+ $echo_limit = 1;
+
+ if ( $date_from_ics >= $current ) {
+ $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
+ } else {
+ // Describe the date in the month
+ if ( isset( $rrule_array['BYDAY'] ) ) {
+ $day_number = substr( $rrule_array['BYDAY'], 0, 1 );
+ $week_day = substr( $rrule_array['BYDAY'], 1 );
+ $day_cardinals = array( 1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth' );
+ $weekdays = array( 'SU' => 'Sunday', 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday' );
+ $event_date_desc = "{$day_cardinals[$day_number]} {$weekdays[$week_day]} of ";
+ } else {
+ $event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) );
+ }
+
+ // Interval only
+ if ( $interval > 1 ) {
+ $catchup = 0;
+ $maybe = strtotime( $event['DTSTART'] );
+ while ( $maybe < $current ) {
+ $maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) );
+ $catchup++;
+ }
+ $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * ( $catchup - 1 ) ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ } else {
+ $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ }
+
+ // Add one interval if necessary
+ if ( strtotime( $recurring_event_date_start ) < $current ) {
+ if ( $interval > 1 ) {
+ $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ } else {
+ try {
+ $adjustment = new DateTime( date( 'Y-m-d', $current ) );
+ $adjustment->modify( 'first day of next month' );
+ $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
+ } catch ( Exception $e ) {
+ // Invalid argument to DateTime
+ }
+ }
+ }
+ }
+ break;
+
+ case 'YEARLY':
+ $frequency = 'year';
+ $echo_limit = 1;
+
+ if ( $date_from_ics >= $current ) {
+ $recurring_event_date_start = date( "Ymd\THis", strtotime( $event['DTSTART'] ) );
+ } else {
+ $recurring_event_date_start = date( 'Y', $current ) . date( "md\THis", strtotime( $event['DTSTART'] ) );
+ if ( strtotime( $recurring_event_date_start ) < $current ) {
+ try {
+ $next = new DateTime( date( 'Y-m-d', $current ) );
+ $next->modify( 'first day of next year' );
+ $recurring_event_date_start = $next->format( 'Y' ) . date ( 'md\THis', strtotime( $event['DTSTART'] ) );
+ } catch ( Exception $e ) {
+ // Invalid argument to DateTime
+ }
+ }
+ }
+ break;
+
+ default:
+ $frequency = false;
+ }
+
+ if ( $frequency !== false && ! $noop ) {
+ $count_counter = 1;
+
+ // If no COUNT limit, go to 10
+ if ( empty( $rrule_count ) ) {
+ $rrule_count = 10;
+ }
+
+ // Set up EXDATE handling for the event
+ $exdates = ( isset( $event['EXDATE'] ) ) ? $event['EXDATE'] : array();
+
+ for ( $i = 1; $i <= $echo_limit; $i++ ) {
+
+ // Weeks need a daily loop and must check for inclusion in BYDAYS
+ if ( 'week' == $frequency ) {
+ $byday_event_date_start = strtotime( $recurring_event_date_start );
+
+ foreach ( $weekdays as $day ) {
+
+ $event_start_timestamp = $byday_event_date_start;
+ $start_time = date( 'His', $event_start_timestamp );
+ $event_end_timestamp = $event_start_timestamp + $duration;
+ $end_time = date( 'His', $event_end_timestamp );
+ if ( 8 == strlen( $event['DTSTART'] ) ) {
+ $exdate_compare = date( 'Ymd', $event_start_timestamp );
+ } else {
+ $exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
+ }
+
+ if ( in_array( $day, $bydays ) && $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) {
+ if ( 8 == strlen( $event['DTSTART'] ) ) {
+ $event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
+ $event['DTEND'] = date( 'Ymd', $event_end_timestamp );
+ } else {
+ $event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp );
+ $event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp );
+ }
+ if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
+ try {
+ $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
+ $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
+ $event['DTSTART'] = $adjusted_time->format('Ymd\THis');
+
+ $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
+ } catch ( Exception $e ) {
+ // Invalid argument to DateTime
+ }
+ }
+ $upcoming[] = $event;
+ $count_counter++;
+ }
+
+ // Move forward one day
+ $byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time );
+ }
+
+ // Restore first event timestamp
+ $event_start_timestamp = strtotime( $recurring_event_date_start );
+
+ } else {
+
+ $event_start_timestamp = strtotime( $recurring_event_date_start );
+ $start_time = date( 'His', $event_start_timestamp );
+ $event_end_timestamp = $event_start_timestamp + $duration;
+ $end_time = date( 'His', $event_end_timestamp );
+ if ( 8 == strlen( $event['DTSTART'] ) ) {
+ $exdate_compare = date( 'Ymd', $event_start_timestamp );
+ } else {
+ $exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
+ }
+
+ if ( $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) {
+ if ( 8 == strlen( $event['DTSTART'] ) ) {
+ $event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
+ $event['DTEND'] = date( 'Ymd', $event_end_timestamp );
+ } else {
+ $event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time;
+ $event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time;
+ }
+ if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
+ try {
+ $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
+ $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
+ $event['DTSTART'] = $adjusted_time->format('Ymd\THis');
+
+ $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
+ } catch ( Exception $e ) {
+ // Invalid argument to DateTime
+ }
+ }
+ $upcoming[] = $event;
+ $count_counter++;
+ }
+ }
+
+ // Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact
+ $next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp );
+ if ( 8 == strlen( $event['DTSTART'] ) ) {
+ $event['DTSTART'] = date( 'Ymd', $next_start_timestamp );
+ $event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration );
+ } else {
+ $event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp );
+ $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
+ }
+
+ // Move recurring event date forward
+ $recurring_event_date_start = $event['DTSTART'];
+ }
+ $set_recurring_events[] = $uid;
+
+ }
+
+ } else {
+ // Process normal events
+ if ( strtotime( isset( $event['DTEND'] ) ? $event['DTEND'] : $event['DTSTART'] ) >= $current ) {
+ $upcoming[] = $event;
+ }
+ }
+ }
+ return $upcoming;
+ }
+
+ /**
+ * Parse events from an iCalendar feed
+ *
+ * @param string $url (default: '')
+ * @return array | false on failure
+ */
+ public function parse( $url = '' ) {
+ $cache_group = 'icalendar_reader_parse';
+ $disable_get_key = 'disable:' . md5( $url );
+
+ // Check to see if previous attempts have failed
+ if ( false !== wp_cache_get( $disable_get_key, $cache_group ) )
+ return false;
+
+ // rewrite webcal: URI schem to HTTP
+ $url = preg_replace('/^webcal/', 'http', $url );
+ // try to fetch
+ $r = wp_remote_get( $url, array( 'timeout' => 3, 'sslverify' => false ) );
+ if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
+ // We were unable to fetch any content, so don't try again for another 60 seconds
+ wp_cache_set( $disable_get_key, 1, $cache_group, 60 );
+ return false;
+ }
+
+ $body = wp_remote_retrieve_body( $r );
+ if ( empty( $body ) )
+ return false;
+
+ $body = str_replace( "\r\n", "\n", $body );
+ $lines = preg_split( "/\n(?=[A-Z])/", $body );
+
+ if ( empty( $lines ) )
+ return false;
+
+ if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) )
+ return false;
+
+ foreach ( $lines as $line ) {
+ $add = $this->key_value_from_string( $line );
+ if ( ! $add ) {
+ $this->add_component( $type, false, $line );
+ continue;
+ }
+ list( $keyword, $value ) = $add;
+
+ switch ( $keyword ) {
+ case 'BEGIN':
+ case 'END':
+ switch ( $line ) {
+ case 'BEGIN:VTODO':
+ $this->todo_count++;
+ $type = 'VTODO';
+ break;
+ case 'BEGIN:VEVENT':
+ $this->event_count++;
+ $type = 'VEVENT';
+ break;
+ case 'BEGIN:VCALENDAR':
+ case 'BEGIN:DAYLIGHT':
+ case 'BEGIN:VTIMEZONE':
+ case 'BEGIN:STANDARD':
+ $type = $value;
+ break;
+ case 'END:VTODO':
+ case 'END:VEVENT':
+ case 'END:VCALENDAR':
+ case 'END:DAYLIGHT':
+ case 'END:VTIMEZONE':
+ case 'END:STANDARD':
+ $type = 'VCALENDAR';
+ break;
+ }
+ break;
+ case 'TZID':
+ if ( 'VTIMEZONE' == $type && ! $this->timezone )
+ $this->timezone = $this->timezone_from_string( $value );
+ break;
+ case 'X-WR-TIMEZONE':
+ if ( ! $this->timezone )
+ $this->timezone = $this->timezone_from_string( $value );
+ break;
+ default:
+ $this->add_component( $type, $keyword, $value );
+ break;
+ }
+ }
+
+ // Filter for RECURRENCE-IDs
+ $recurrences = array();
+ if ( array_key_exists( 'VEVENT', $this->cal ) ) {
+ foreach ( $this->cal['VEVENT'] as $event ) {
+ if ( isset( $event['RECURRENCE-ID'] ) ) {
+ $recurrences[] = $event;
+ }
+ }
+ foreach ( $recurrences as $recurrence ) {
+ for ( $i = 0; $i < count( $this->cal['VEVENT'] ); $i++ ) {
+ if ( $this->cal['VEVENT'][ $i ]['UID'] == $recurrence['UID'] && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] ) ) {
+ $this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID'];
+ break;
+ }
+ }
+ }
+ }
+
+ return $this->cal;
+ }
+
+ /**
+ * Parse key:value from a string
+ *
+ * @param string $text (default: '')
+ * @return array
+ */
+ public function key_value_from_string( $text = '' ) {
+ preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches );
+
+ if ( 0 == count( $matches ) )
+ return false;
+
+ return array( $matches[1], $matches[3] );
+ }
+
+ /**
+ * Convert a timezone name into a timezone object.
+ *
+ * @param string $text Timezone name. Example: America/Chicago
+ * @return object|null A DateTimeZone object if the conversion was successful.
+ */
+ private function timezone_from_string( $text ) {
+ try {
+ $timezone = new DateTimeZone( $text );
+ } catch ( Exception $e ) {
+ $blog_timezone = get_option( 'timezone_string' );
+ if ( ! $blog_timezone ) {
+ $blog_timezone = 'Etc/UTC';
+ }
+
+ $timezone = new DateTimeZone( $blog_timezone );
+ }
+
+ return $timezone;
+ }
+
+ /**
+ * Add a component to the calendar array
+ *
+ * @param string $component (default: '')
+ * @param string $keyword (default: '')
+ * @param string $value (default: '')
+ * @return void
+ */
+ public function add_component( $component = '', $keyword = '', $value = '' ) {
+ if ( false == $keyword ) {
+ $keyword = $this->last_keyword;
+ switch ( $component ) {
+ case 'VEVENT':
+ $value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value;
+ break;
+ case 'VTODO' :
+ $value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value;
+ break;
+ }
+ }
+
+ /*
+ * Some events have a specific timezone set in their start/end date,
+ * and it may or may not be different than the calendar timzeone.
+ * Valid formats include:
+ * DTSTART;TZID=Pacific Standard Time:20141219T180000
+ * DTEND;TZID=Pacific Standard Time:20141219T200000
+ * EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
+ * EXDATE;VALUE=DATE:2015050
+ * EXDATE;TZID=America/New_York:20150424T170000
+ * EXDATE;TZID=Pacific Standard Time:20120615T140000,20120629T140000,20120706T140000
+ */
+
+ // Always store EXDATE as an array
+ if ( stristr( $keyword, 'EXDATE' ) ) {
+ $value = explode( ',', $value );
+ }
+
+ // Adjust DTSTART, DTEND, and EXDATE according to their TZID if set
+ if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) {
+ $keyword = explode( ';', $keyword );
+
+ $tzid = false;
+ if ( 2 == count( $keyword ) ) {
+ $tparam = $keyword[1];
+
+ if ( strpos( $tparam, "TZID" ) !== false ) {
+ $tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) );
+ }
+ }
+
+ // Normalize all times to default UTC
+ if ( $tzid ) {
+ $adjusted_times = array();
+ foreach ( (array) $value as $v ) {
+ try {
+ $adjusted_time = new DateTime( $v, $tzid );
+ $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
+ $adjusted_times[] = $adjusted_time->format('Ymd\THis');
+ } catch ( Exception $e ) {
+ // Invalid argument to DateTime
+ return;
+ }
+ }
+ $value = $adjusted_times;
+ }
+
+ // Format for adding to event
+ $keyword = $keyword[0];
+ if ( 'EXDATE' != $keyword ) {
+ $value = implode( (array) $value );
+ }
+ }
+
+ foreach ( (array) $value as $v ) {
+ switch ($component) {
+ case 'VTODO':
+ if ( 'EXDATE' == $keyword ) {
+ $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v;
+ } else {
+ $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v;
+ }
+ break;
+ case 'VEVENT':
+ if ( 'EXDATE' == $keyword ) {
+ $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v;
+ } else {
+ $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v;
+ }
+ break;
+ default:
+ $this->cal[ $component ][ $keyword ] = $v;
+ break;
+ }
+ }
+ $this->last_keyword = $keyword;
+ }
+
+ /**
+ * Escape strings with wp_kses, allow links
+ *
+ * @param string $string (default: '')
+ * @return string
+ */
+ public function escape( $string = '' ) {
+ // Unfold content lines per RFC 5545
+ $string = str_replace( "\n\t", '', $string );
+ $string = str_replace( "\n ", '', $string );
+
+ $allowed_html = array(
+ 'a' => array(
+ 'href' => array(),
+ 'title' => array()
+ )
+ );
+
+ $allowed_tags = '';
+ foreach ( array_keys( $allowed_html ) as $tag ) {
+ $allowed_tags .= "<{$tag}>";
+ }
+
+ // Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc
+ // because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra
+ // safety and good measure.
+ return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html );
+ }
+
+ /**
+ * Render the events
+ *
+ * @param string $url (default: '')
+ * @param string $context (default: 'widget') or 'shortcode'
+ * @return mixed bool|string false on failure, rendered HTML string on success.
+ */
+ public function render( $url = '', $args = array() ) {
+
+ $args = wp_parse_args( $args, array(
+ 'context' => 'widget',
+ 'number' => 5
+ ) );
+
+ $events = $this->get_events( $url, $args['number'] );
+ $events = $this->apply_timezone_offset( $events );
+
+ if ( empty( $events ) )
+ return false;
+
+ ob_start();
+
+ if ( 'widget' == $args['context'] ) : ?>
+ <ul class="upcoming-events">
+ <?php foreach ( $events as $event ) : ?>
+ <li>
+ <strong class="event-summary"><?php echo $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></strong>
+ <span class="event-when"><?php echo $this->formatted_date( $event ); ?></span>
+ <?php if ( ! empty( $event['LOCATION'] ) ) : ?>
+ <span class="event-location"><?php echo $this->escape( stripslashes( $event['LOCATION'] ) ); ?></span>
+ <?php endif; ?>
+ <?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
+ <span class="event-description"><?php echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></span>
+ <?php endif; ?>
+ </li>
+ <?php endforeach; ?>
+ </ul>
+ <?php endif;
+
+ if ( 'shortcode' == $args['context'] ) : ?>
+ <table class="upcoming-events">
+ <thead>
+ <tr>
+ <th><?php esc_html_e( 'Location', 'jetpack' ); ?></th>
+ <th><?php esc_html_e( 'When', 'jetpack' ); ?></th>
+ <th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th>
+ <th><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ( $events as $event ) : ?>
+ <tr>
+ <td><?php echo empty( $event['LOCATION'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['LOCATION'] ) ); ?></td>
+ <td><?php echo $this->formatted_date( $event ); ?></td>
+ <td><?php echo empty( $event['SUMMARY'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></td>
+ <td><?php echo empty( $event['DESCRIPTION'] ) ? '&nbsp;' : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ <?php endif;
+
+ $rendered = ob_get_clean();
+
+ if ( empty( $rendered ) )
+ return false;
+
+ return $rendered;
+ }
+
+ public function formatted_date( $event ) {
+
+ $date_format = get_option( 'date_format' );
+ $time_format = get_option( 'time_format' );
+ $start = strtotime( $event['DTSTART'] );
+ $end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false;
+
+ $all_day = ( 8 == strlen( $event['DTSTART'] ) );
+
+ if ( !$all_day && $this->timezone ) {
+ try {
+ $start_time = new DateTime( $event['DTSTART'] );
+ $timezone_offset = $this->timezone->getOffset( $start_time );
+ $start += $timezone_offset;
+
+ if ( $end ) {
+ $end += $timezone_offset;
+ }
+ } catch ( Exception $e ) {
+ // Invalid argument to DateTime
+ }
+ }
+ $single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true;
+
+ /* translators: Date and time */
+ $date_with_time = __( '%1$s at %2$s' , 'jetpack' );
+ /* translators: Two dates with a separator */
+ $two_dates = __( '%1$s &ndash; %2$s' , 'jetpack' );
+
+ // we'll always have the start date. Maybe with time
+ if ( $all_day )
+ $date = date_i18n( $date_format, $start );
+ else
+ $date = sprintf( $date_with_time, date_i18n( $date_format, $start ), date_i18n( $time_format, $start ) );
+
+ // single day, timed
+ if ( $single_day && ! $all_day && false !== $end )
+ $date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) );
+
+ // multi-day
+ if ( ! $single_day ) {
+
+ if ( $all_day ) {
+ // DTEND for multi-day events represents "until", not "including", so subtract one minute
+ $end_date = date_i18n( $date_format, $end - 60 );
+ } else {
+ $end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) );
+ }
+
+ $date = sprintf( $two_dates, $date, $end_date );
+
+ }
+
+ return $date;
+ }
+
+ protected function sort_by_recent( $list ) {
+ $dates = $sorted_list = array();
+
+ foreach ( $list as $key => $row ) {
+ $date = $row['DTSTART'];
+ // pad some time onto an all day date
+ if ( 8 === strlen( $date ) )
+ $date .= 'T000000Z';
+ $dates[$key] = $date;
+ }
+ asort( $dates );
+ foreach( $dates as $key => $value ) {
+ $sorted_list[$key] = $list[$key];
+ }
+ unset($list);
+ return $sorted_list;
+ }
+
+}
+
+
+/**
+ * Wrapper function for iCalendarReader->get_events()
+ *
+ * @param string $url (default: '')
+ * @return array
+ */
+function icalendar_get_events( $url = '', $count = 5 ) {
+ // Find your calendar's address http://support.google.com/calendar/bin/answer.py?hl=en&answer=37103
+ $ical = new iCalendarReader();
+ return $ical->get_events( $url, $count );
+}
+
+/**
+ * Wrapper function for iCalendarReader->render()
+ *
+ * @param string $url (default: '')
+ * @param string $context (default: 'widget') or 'shortcode'
+ * @return mixed bool|string false on failure, rendered HTML string on success.
+ */
+function icalendar_render_events( $url = '', $args = array() ) {
+ $ical = new iCalendarReader();
+ return $ical->render( $url, $args );
+}
diff --git a/plugins/jetpack/_inc/lib/jetpack-wpes-query-builder/jetpack-wpes-query-builder.php b/plugins/jetpack/_inc/lib/jetpack-wpes-query-builder/jetpack-wpes-query-builder.php
new file mode 100644
index 00000000..d3481ce5
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/jetpack-wpes-query-builder/jetpack-wpes-query-builder.php
@@ -0,0 +1,341 @@
+<?php
+
+
+/**
+ * Provides an interface for easily building a complex search query that
+ * combines multiple ranking signals.
+ *
+ *
+ * $bldr = new Jetpack_WPES_Query_Builder();
+ * $bldr->add_filter( ... );
+ * $bldr->add_filter( ... );
+ * $bldr->add_query( ... );
+ * $es_query = $bldr->build_query();
+ *
+ *
+ * All ES queries take a standard form with main query (with some filters),
+ * wrapped in a function_score
+ *
+ * Bucketed queries use an aggregation to diversify results. eg a bunch
+ * of separate filters where to get different sets of results.
+ *
+ */
+
+class Jetpack_WPES_Query_Builder {
+
+ protected $es_filters = array();
+
+ // Custom boosting with function_score
+ protected $functions = array();
+ protected $decays = array();
+ protected $scripts = array();
+ protected $functions_max_boost = 2.0;
+ protected $functions_score_mode = 'multiply';
+ protected $query_bool_boost = null;
+
+ // General aggregations for buckets and metrics
+ protected $aggs_query = false;
+ protected $aggs = array();
+
+ // The set of top level text queries to combine
+ protected $must_queries = array();
+ protected $should_queries = array();
+ protected $dis_max_queries = array();
+
+ protected $diverse_buckets_query = false;
+ protected $bucket_filters = array();
+ protected $bucket_sub_aggs = array();
+
+ ////////////////////////////////////
+ // Methods for building a query
+
+ public function add_filter( $filter ) {
+ $this->es_filters[] = $filter;
+ }
+
+ public function add_query( $query, $type = 'must' ) {
+ switch ( $type ) {
+ case 'dis_max':
+ $this->dis_max_queries[] = $query;
+ break;
+
+ case 'should':
+ $this->should_queries[] = $query;
+ break;
+
+ case 'must':
+ default:
+ $this->must_queries[] = $query;
+ break;
+ }
+ }
+
+ /**
+ * Add a scoring function to the query
+ *
+ * NOTE: For decays (linear, exp, or gauss), use Jetpack_WPES_Query_Builder::add_decay() instead
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
+ *
+ * @param $function string name of the function
+ * @param $params array functions parameters
+ *
+ * @return void
+ */
+ public function add_function( $function, $params ) {
+ $this->functions[ $function ][] = $params;
+ }
+
+ /**
+ * Add a decay function to score results
+ *
+ * This method should be used instead of Jetpack_WPES_Query_Builder::add_function() for decays, as the internal ES structure
+ * is slightly different for them.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/decay-functions.html
+ *
+ * @param $function string name of the decay function - linear, exp, or gauss
+ * @param $params array The decay functions parameters, passed to ES directly
+ *
+ * @return void
+ */
+ public function add_decay( $function, $params ) {
+ $this->decays[ $function ][] = $params;
+ }
+
+ /**
+ * Add a scoring mode to the query
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
+ *
+ * @param $mode string name of how to score
+ *
+ * @return void
+ */
+ public function add_score_mode_to_functions( $mode='multiply' ) {
+ $this->functions_score_mode = $mode;
+ }
+
+ public function add_max_boost_to_functions( $boost ) {
+ $this->functions_max_boost = $boost;
+ }
+
+ public function add_boost_to_query_bool( $boost ) {
+ $this->query_bool_boost = $boost;
+ }
+
+ public function add_aggs( $aggs_name, $aggs ) {
+ $this->aggs_query = true;
+ $this->aggs[$aggs_name] = $aggs;
+ }
+
+ public function add_aggs_sub_aggs( $aggs_name, $sub_aggs ) {
+ if ( ! array_key_exists( 'aggs', $this->aggs[$aggs_name] ) ) {
+ $this->aggs[$aggs_name]['aggs'] = array();
+ }
+ $this->aggs[$aggs_name]['aggs'] = $sub_aggs;
+ }
+
+ public function add_bucketed_query( $name, $query ) {
+ $this->_add_bucket_filter( $name, $query );
+
+ $this->add_query( $query, 'dis_max' );
+ }
+
+ public function add_bucketed_terms( $name, $field, $terms, $boost = 1 ) {
+ if ( ! is_array( $terms ) ) {
+ $terms = array( $terms );
+ }
+
+ $this->_add_bucket_filter( $name, array(
+ 'terms' => array(
+ $field => $terms,
+ ),
+ ));
+
+ $this->add_query( array(
+ 'constant_score' => array(
+ 'filter' => array(
+ 'terms' => array(
+ $field => $terms,
+ ),
+ ),
+ 'boost' => $boost,
+ ),
+ ), 'dis_max' );
+ }
+
+ public function add_bucket_sub_aggs( $agg ) {
+ $this->bucket_sub_aggs = array_merge( $this->bucket_sub_aggs, $agg );
+ }
+
+ protected function _add_bucket_filter( $name, $filter ) {
+ $this->diverse_buckets_query = true;
+ $this->bucket_filters[ $name ] = $filter;
+ }
+
+ ////////////////////////////////////
+ // Building Final Query
+
+ /**
+ * Combine all the queries, functions, decays, scripts, and max_boost into an ES query
+ *
+ * @return array Array representation of the built ES query
+ */
+ public function build_query() {
+ $query = array();
+
+ //dis_max queries just become a single must query
+ if ( ! empty( $this->dis_max_queries ) ) {
+ $this->must_queries[] = array(
+ 'dis_max' => array(
+ 'queries' => $this->dis_max_queries,
+ ),
+ );
+ }
+
+ if ( empty( $this->must_queries ) ) {
+ $this->must_queries = array(
+ array(
+ 'match_all' => array(),
+ ),
+ );
+ }
+
+ if ( empty( $this->should_queries ) ) {
+ if ( 1 == count( $this->must_queries ) ) {
+ $query = $this->must_queries[0];
+ } else {
+ $query = array(
+ 'bool' => array(
+ 'must' => $this->must_queries,
+ ),
+ );
+ }
+ } else {
+ $query = array(
+ 'bool' => array(
+ 'must' => $this->must_queries,
+ 'should' => $this->should_queries,
+ ),
+ );
+ }
+
+ if ( ! is_null( $this->query_bool_boost ) && isset( $query['bool'] ) ) {
+ $query['bool']['boost'] = $this->query_bool_boost;
+ }
+
+ // If there are any function score adjustments, then combine those
+ if ( $this->functions || $this->decays || $this->scripts ) {
+ $weighting_functions = array();
+
+ if ( $this->functions ) {
+ foreach ( $this->functions as $function_type => $configs ) {
+ foreach ( $configs as $config ) {
+ foreach ( $config as $field => $params ) {
+ $func_arr = $params;
+
+ $func_arr['field'] = $field;
+
+ $weighting_functions[] = array(
+ $function_type => $func_arr,
+ );
+ }
+ }
+ }
+ }
+
+ if ( $this->decays ) {
+ foreach ( $this->decays as $decay_type => $configs ) {
+ foreach ( $configs as $config ) {
+ foreach ( $config as $field => $params ) {
+ $weighting_functions[] = array(
+ $decay_type => array(
+ $field => $params,
+ ),
+ );
+ }
+ }
+ }
+ }
+
+ if ( $this->scripts ) {
+ foreach ( $this->scripts as $script ) {
+ $weighting_functions[] = array(
+ 'script_score' => array(
+ 'script' => $script,
+ ),
+ );
+ }
+ }
+
+ $query = array(
+ 'function_score' => array(
+ 'query' => $query,
+ 'functions' => $weighting_functions,
+ 'max_boost' => $this->functions_max_boost,
+ 'score_mode' => $this->functions_score_mode,
+ ),
+ );
+ } // End if().
+
+ return $query;
+ }
+
+ /**
+ * Assemble the 'filter' portion of an ES query, from all registered filters
+ *
+ * @return array|null Combined ES filters, or null if none have been defined
+ */
+ public function build_filter() {
+ if ( empty( $this->es_filters ) ) {
+ $filter = null;
+ } elseif ( 1 == count( $this->es_filters ) ) {
+ $filter = $this->es_filters[0];
+ } else {
+ $filter = array(
+ 'and' => $this->es_filters,
+ );
+ }
+
+ return $filter;
+ }
+
+ /**
+ * Assemble the 'aggregation' portion of an ES query, from all general aggregations.
+ *
+ * @return array An aggregation query as an array of topics, filters, and bucket names
+ */
+ public function build_aggregation() {
+ if ( empty( $this->bucket_sub_aggs ) && empty( $this->aggs_query ) ) {
+ return array();
+ }
+
+ if ( ! $this->diverse_buckets_query && empty( $this->aggs_query ) ) {
+ return $this->bucket_sub_aggs;
+ }
+
+ $aggregations = array(
+ 'topics' => array(
+ 'filters' => array(
+ 'filters' => array(),
+ ),
+ ),
+ );
+
+ if ( ! empty( $this->bucket_sub_aggs ) ) {
+ $aggregations['topics']['aggs'] = $this->bucket_sub_aggs;
+ }
+
+ foreach ( $this->bucket_filters as $bucket_name => $filter ) {
+ $aggregations['topics']['filters']['filters'][ $bucket_name ] = $filter;
+ }
+
+ if ( ! empty( $this->aggs_query ) ) {
+ $aggregations = $this->aggs;
+ }
+
+ return $aggregations;
+ }
+
+}
diff --git a/plugins/jetpack/_inc/lib/jetpack-wpes-query-builder/jetpack-wpes-query-parser.php b/plugins/jetpack/_inc/lib/jetpack-wpes-query-builder/jetpack-wpes-query-parser.php
new file mode 100644
index 00000000..2b7710cb
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/jetpack-wpes-query-builder/jetpack-wpes-query-parser.php
@@ -0,0 +1,683 @@
+<?php
+
+/**
+ * Parse a pure text query into WordPress Elasticsearch query. This builds on
+ * the Jetpack_WPES_Query_Builder() to provide search query parsing.
+ *
+ * The key part of this parser is taking a user's query string typed into a box
+ * and converting it into an ES search query.
+ *
+ * This varies by application, but roughly it means extracting some parts of the query
+ * (authors, tags, and phrases) that are treated as a filter. Then taking the
+ * remaining words and building the correct query (possibly with prefix searching
+ * if we are doing search as you type)
+ *
+ * This class only supports ES 2.x+
+ *
+ * This parser builds queries of the form:
+ * bool:
+ * must:
+ * AND match of a single field (ideally an edgengram field)
+ * filter:
+ * filter clauses from context (eg @gibrown, #news, etc)
+ * should:
+ * boosting of results by various fields
+ *
+ * Features supported:
+ * - search as you type
+ * - phrases
+ * - supports querying across multiple languages at once
+ *
+ * Example usage (from Search on Reader Manage):
+ *
+ * require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-search-query-parser' );
+ * $parser = new WPES_Search_Query_Parser( $args['q'], array( $lang ) );
+ *
+ * //author
+ * $parser->author_field_filter( array(
+ * 'prefixes' => array( '@' ),
+ * 'wpcom_id_field' => 'author_id',
+ * 'must_query_fields' => array( 'author.engram', 'author_login.engram' ),
+ * 'boost_query_fields' => array( 'author^2', 'author_login^2', 'title.default.engram' ),
+ * ) );
+ *
+ * //remainder of query
+ * $match_content_fields = $parser->merge_ml_fields(
+ * array(
+ * 'all_content' => 0.1,
+ * ),
+ * array(
+ * 'all_content.default.engram^0.1',
+ * )
+ * );
+ * $boost_content_fields = $parser->merge_ml_fields(
+ * array(
+ * 'title' => 2,
+ * 'description' => 1,
+ * 'tags' => 1,
+ * ),
+ * array(
+ * 'author_login^2',
+ * 'author^2',
+ * )
+ * );
+ *
+ * $parser->phrase_filter( array(
+ * 'must_query_fields' => $match_content_fields,
+ * 'boost_query_fields' => $boost_content_fields,
+ * ) );
+ * $parser->remaining_query( array(
+ * 'must_query_fields' => $match_content_fields,
+ * 'boost_query_fields' => $boost_content_fields,
+ * ) );
+ *
+ * //Boost on phrases
+ * $parser->remaining_query( array(
+ * 'boost_query_fields' => $boost_content_fields,
+ * 'boost_query_type' => 'phrase',
+ * ) );
+ *
+ * //boosting
+ * $parser->add_max_boost_to_functions( 20 );
+ * $parser->add_function( 'field_value_factor', array(
+ * 'follower_count' => array(
+ * 'modifier' => 'sqrt',
+ * 'factor' => 1,
+ * 'missing' => 0,
+ * ) ) );
+ *
+ * //Filtering
+ * $parser->add_filter( array(
+ * 'exists' => array( 'field' => 'langs.' . $lang )
+ * ) );
+ *
+ * //run the query
+ * $es_query_args = array(
+ * 'name' => 'feeds',
+ * 'blog_id' => false,
+ * 'security_strategy' => 'a8c',
+ * 'type' => 'feed,blog',
+ * 'fields' => array( 'blog_id', 'feed_id' ),
+ * 'query' => $parser->build_query(),
+ * 'filter' => $parser->build_filter(),
+ * 'size' => $size,
+ * 'from' => $from
+ * );
+ * $es_results = es_api_search_index( $es_query_args, 'api-feed-find' );
+ *
+ */
+
+jetpack_require_lib( 'jetpack-wpes-query-builder' );
+
+class Jetpack_WPES_Search_Query_Parser extends Jetpack_WPES_Query_Builder {
+
+ protected $orig_query = '';
+ protected $current_query = '';
+ protected $langs;
+ protected $avail_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' );
+
+ public function __construct( $user_query, $langs ) {
+ $this->orig_query = $user_query;
+ $this->current_query = $this->orig_query;
+ $this->langs = $this->norm_langs( $langs );
+ }
+
+ protected $extracted_phrases = array();
+
+ ///////////////////////////////////////////////////////
+ // Methods for Building arrays of multilingual fields
+
+ /*
+ * Normalize language codes
+ */
+ public function norm_langs( $langs ) {
+ $lst = array();
+ foreach( $langs as $l ) {
+ $l = strtok( $l, '-_' );
+ if ( in_array( $l, $this->avail_langs ) ) {
+ $lst[$l] = true;
+ } else {
+ $lst['default'] = true;
+ }
+ }
+ return array_keys( $lst );
+ }
+
+ /*
+ * Take a list of field prefixes and expand them for multi-lingual
+ * with the provided boostings.
+ */
+ public function merge_ml_fields( $fields2boosts, $additional_fields ) {
+ $flds = array();
+ foreach( $fields2boosts as $f => $b ) {
+ foreach( $this->langs as $l ) {
+ $flds[] = $f . '.' . $l . '^' . $b;
+ }
+ }
+ foreach( $additional_fields as $f ) {
+ $flds[] = $f;
+ }
+ return $flds;
+ }
+
+ ////////////////////////////////////
+ // Extract Fields for Filtering on
+
+ /*
+ * Extract any @mentions from the user query
+ * use them as a filter if we can find a wp.com id
+ * otherwise use them as a
+ *
+ * args:
+ * wpcom_id_field: wp.com id field
+ * must_query_fields: array of fields to search for matching results (optional)
+ * boost_query_fields: array of fields to search in for boosting results (optional)
+ * prefixes: array of prefixes that the user can use to indicate an author
+ *
+ * returns true/false of whether any were found
+ *
+ * See also: https://github.com/twitter/twitter-text/blob/master/java/src/com/twitter/Regex.java
+ */
+ public function author_field_filter( $args ) {
+ $defaults = array(
+ 'wpcom_id_field' => 'author_id',
+ 'must_query_fields' => null,
+ 'boost_query_fields' => null,
+ 'prefixes' => array( '@' ),
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ $names = array();
+ foreach( $args['prefixes'] as $p ) {
+ $found = $this->get_fields( $p );
+ if ( $found ) {
+ foreach( $found as $f ) {
+ $names[] = $f;
+ }
+ }
+ }
+
+ if ( empty( $names ) ) {
+ return false;
+ }
+
+ foreach( $args['prefixes'] as $p ) {
+ $this->remove_fields( $p );
+ }
+
+ $user_ids = array();
+ $query_names = array();
+
+ //loop through the matches and separate into filters and queries
+ foreach( $names as $n ) {
+ //check for exact match on login
+ $userdata = get_user_by( 'login', strtolower( $n ) );
+ $filtering = false;
+ if ( $userdata ) {
+ $user_ids[ $userdata->ID ] = true;
+ $filtering = true;
+ }
+
+ $is_phrase = false;
+ if ( preg_match( '/"/', $n ) ) {
+ $is_phrase = true;
+ $n = preg_replace( '/"/', '', $n );
+ }
+
+ if ( !empty( $args['must_query_fields'] ) && !$filtering ) {
+ if ( $is_phrase ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['must_query_fields'],
+ 'query' => $n,
+ 'type' => 'phrase',
+ ) ) );
+ } else {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['must_query_fields'],
+ 'query' => $n,
+ ) ) );
+ }
+ }
+
+ if ( !empty( $args['boost_query_fields'] ) ) {
+ if ( $is_phrase ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $n,
+ 'type' => 'phrase',
+ ) ), 'should' );
+ } else {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $n,
+ ) ), 'should' );
+ }
+ }
+ }
+
+ if ( ! empty( $user_ids ) ) {
+ $user_ids = array_keys( $user_ids );
+ $this->add_filter( array( 'terms' => array( $args['wpcom_id_field'] => $user_ids ) ) );
+ }
+
+ return true;
+ }
+
+ /*
+ * Extract any prefix followed by text use them as a must clause,
+ * and optionally as a boost to the should query
+ * This can be used for hashtags. eg #News, or #"current events",
+ * but also works for any arbitrary field. eg from:Greg
+ *
+ * args:
+ * must_query_fields: array of fields that must match the tag (optional)
+ * boost_query_fields: array of fields to boost search on (optional)
+ * prefixes: array of prefixes that the user can use to indicate a tag
+ *
+ * returns true/false of whether any were found
+ *
+ */
+ public function text_field_filter( $args ) {
+ $defaults = array(
+ 'must_query_fields' => array( 'tag.name' ),
+ 'boost_query_fields' => array( 'tag.name' ),
+ 'prefixes' => array( '#' ),
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ $tags = array();
+ foreach( $args['prefixes'] as $p ) {
+ $found = $this->get_fields( $p );
+ if ( $found ) {
+ foreach( $found as $f ) {
+ $tags[] = $f;
+ }
+ }
+ }
+
+ if ( empty( $tags ) ) {
+ return false;
+ }
+
+ foreach( $args['prefixes'] as $p ) {
+ $this->remove_fields( $p );
+ }
+
+ foreach( $tags as $t ) {
+ $is_phrase = false;
+ if ( preg_match( '/"/', $t ) ) {
+ $is_phrase = true;
+ $t = preg_replace( '/"/', '', $t );
+ }
+
+ if ( ! empty( $args['must_query_fields'] ) ) {
+ if ( $is_phrase ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['must_query_fields'],
+ 'query' => $t,
+ 'type' => 'phrase',
+ ) ) );
+ } else {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['must_query_fields'],
+ 'query' => $t,
+ ) ) );
+ }
+ }
+
+ if ( ! empty( $args['boost_query_fields'] ) ) {
+ if ( $is_phrase ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $t,
+ 'type' => 'phrase',
+ ) ), 'should' );
+ } else {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $t,
+ ) ), 'should' );
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * Extract anything surrounded by quotes or if there is an opening quote
+ * that is not complete, and add them to the query as a phrase query.
+ * Quotes can be either '' or ""
+ *
+ * args:
+ * must_query_fields: array of fields that must match the phrases
+ * boost_query_fields: array of fields to boost the phrases on (optional)
+ *
+ * returns true/false of whether any were found
+ *
+ */
+ public function phrase_filter( $args ) {
+ $defaults = array(
+ 'must_query_fields' => array( 'all_content' ),
+ 'boost_query_fields' => array( 'title' ),
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ $phrases = array();
+ if ( preg_match_all( '/"([^"]+)"/', $this->current_query, $matches ) ) {
+ foreach ( $matches[1] as $match ) {
+ $phrases[] = $match;
+ }
+ $this->current_query = preg_replace( '/"([^"]+)"/', '', $this->current_query );
+ }
+
+ if ( preg_match_all( "/'([^']+)'/", $this->current_query, $matches ) ) {
+ foreach ( $matches[1] as $match ) {
+ $phrases[] = $match;
+ }
+ $this->current_query = preg_replace( "/'([^']+)'/", '', $this->current_query );
+ }
+
+ //look for a final, uncompleted phrase
+ $phrase_prefix = false;
+ if ( preg_match_all( '/"([^"]+)$/', $this->current_query, $matches ) ) {
+ $phrase_prefix = $matches[1][0];
+ $this->current_query = preg_replace( '/"([^"]+)$/', '', $this->current_query );
+ }
+ if ( preg_match_all( "/(?:'\B|\B')([^']+)$/", $this->current_query, $matches ) ) {
+ $phrase_prefix = $matches[1][0];
+ $this->current_query = preg_replace( "/(?:'\B|\B')([^']+)$/", '', $this->current_query );
+ }
+
+ if ( $phrase_prefix ) {
+ $phrases[] = $phrase_prefix;
+ }
+ if ( empty( $phrases ) ) {
+ return false;
+ }
+
+ foreach ( $phrases as $p ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['must_query_fields'],
+ 'query' => $p,
+ 'type' => 'phrase',
+ ) ) );
+
+ if ( ! empty( $args['boost_query_fields'] ) ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $p,
+ 'operator' => 'and',
+ ) ), 'should' );
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * Query fields based on the remaining parts of the query
+ * This could be the final AND part of the query terms to match, or it
+ * could be boosting certain elements of the query
+ *
+ * args:
+ * must_query_fields: array of fields that must match the remaining terms (optional)
+ * boost_query_fields: array of fields to boost the remaining terms on (optional)
+ *
+ */
+ public function remaining_query( $args ) {
+ $defaults = array(
+ 'must_query_fields' => null,
+ 'boost_query_fields' => null,
+ 'boost_operator' => 'and',
+ 'boost_query_type' => 'best_fields',
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ if ( empty( $this->current_query ) || ctype_space( $this->current_query ) ) {
+ return;
+ }
+
+ if ( ! empty( $args['must_query_fields'] ) ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['must_query_fields'],
+ 'query' => $this->current_query,
+ 'operator' => 'and',
+ ) ) );
+ }
+
+ if ( ! empty( $args['boost_query_fields'] ) ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $this->current_query,
+ 'operator' => $args['boost_operator'],
+ 'type' => $args['boost_query_type'],
+ ) ), 'should' );
+ }
+
+ }
+
+ /*
+ * Query fields using a prefix query (alphabetical expansions on the index).
+ * This is not recommended. Slower performance and worse relevancy.
+ *
+ * (UNTESTED! Copied from old prefix expansion code)
+ *
+ * args:
+ * must_query_fields: array of fields that must match the remaining terms (optional)
+ * boost_query_fields: array of fields to boost the remaining terms on (optional)
+ *
+ */
+ public function remaining_prefix_query( $args ) {
+ $defaults = array(
+ 'must_query_fields' => array( 'all_content' ),
+ 'boost_query_fields' => array( 'title' ),
+ 'boost_operator' => 'and',
+ 'boost_query_type' => 'best_fields',
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ if ( empty( $this->current_query ) || ctype_space( $this->current_query ) ) {
+ return;
+ }
+
+ //////////////////////////////////
+ // Example cases to think about:
+ // "elasticse"
+ // "elasticsearch"
+ // "elasticsearch "
+ // "elasticsearch lucen"
+ // "elasticsearch lucene"
+ // "the future" - note the stopword which will match nothing!
+ // "F1" - an exact match that also has tons of expansions
+ // "こんにちは" ja "hello"
+ // "こんにちは友人" ja "hello friend" - we just rely on the prefix phrase and ES to split words
+ // - this could still be better I bet. Maybe we need to analyze with ES first?
+ //
+
+ /////////////////////////////
+ //extract pieces of query
+ // eg: "PREFIXREMAINDER PREFIXWORD"
+ // "elasticsearch lucen"
+
+ $prefix_word = false;
+ $prefix_remainder = false;
+ if ( preg_match_all( '/([^ ]+)$/', $this->current_query, $matches ) ) {
+ $prefix_word = $matches[1][0];
+ }
+
+ $prefix_remainder = preg_replace( '/([^ ]+)$/', '', $this->current_query );
+ if ( ctype_space( $prefix_remainder ) ) {
+ $prefix_remainder = false;
+ }
+
+ if ( ! $prefix_word ) {
+ //Space at the end of the query, so skip using a prefix query
+ if ( ! empty( $args['must_query_fields'] ) ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['must_query_fields'],
+ 'query' => $this->current_query,
+ 'operator' => 'and',
+ ) ) );
+ }
+
+ if ( ! empty( $args['boost_query_fields'] ) ) {
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $this->current_query,
+ 'operator' => $args['boost_operator'],
+ 'type' => $args['boost_query_type'],
+ ) ), 'should' );
+ }
+ } else {
+
+ //must match the prefix word and the prefix remainder
+ if ( ! empty( $args['must_query_fields'] ) ) {
+ //need to do an OR across a few fields to handle all cases
+ $must_q = array( 'bool' => array( 'should' => array( ), 'minimum_should_match' => 1 ) );
+
+ //treat all words as an exact search (boosts complete word like "news"
+ //from prefixes of "newspaper")
+ $must_q['bool']['should'][] = array( 'multi_match' => array(
+ 'fields' => $this->all_fields,
+ 'query' => $full_text,
+ 'operator' => 'and',
+ 'type' => 'cross_fields',
+ ) );
+
+ //always optimistically try and match the full text as a phrase
+ //prefix "the futu" should try to match "the future"
+ //otherwise the first stopword kinda breaks
+ //This also works as the prefix match for a single word "elasticsea"
+ $must_q['bool']['should'][] = array( 'multi_match' => array(
+ 'fields' => $this->phrase_fields,
+ 'query' => $full_text,
+ 'operator' => 'and',
+ 'type' => 'phrase_prefix',
+ 'max_expansions' => 100,
+ ) );
+
+ if ( $prefix_remainder ) {
+ //Multiple words found, so treat each word on its own and not just as
+ //a part of a phrase
+ //"elasticsearch lucen" => "elasticsearch" exact AND "lucen" prefix
+ $q['bool']['should'][] = array( 'bool' => array(
+ 'must' => array(
+ array( 'multi_match' => array(
+ 'fields' => $this->phrase_fields,
+ 'query' => $prefix_word,
+ 'operator' => 'and',
+ 'type' => 'phrase_prefix',
+ 'max_expansions' => 100,
+ ) ),
+ array( 'multi_match' => array(
+ 'fields' => $this->all_fields,
+ 'query' => $prefix_remainder,
+ 'operator' => 'and',
+ 'type' => 'cross_fields',
+ ) ),
+ )
+ ) );
+ }
+
+ $this->add_query( $must_q );
+ }
+
+ //Now add any boosting of the query
+ if ( ! empty( $args['boost_query_fields'] ) ) {
+ //treat all words as an exact search (boosts complete word like "news"
+ //from prefixes of "newspaper")
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $this->current_query,
+ 'operator' => $args['boost_query_operator'],
+ 'type' => $args['boost_query_type'],
+ ) ), 'should' );
+
+ //optimistically boost the full phrase prefix match
+ $this->add_query( array(
+ 'multi_match' => array(
+ 'fields' => $args['boost_query_fields'],
+ 'query' => $this->current_query,
+ 'operator' => 'and',
+ 'type' => 'phrase_prefix',
+ 'max_expansions' => 100,
+ ) ) );
+ }
+ }
+ }
+
+ /*
+ * Boost results based on the lang probability overlaps
+ *
+ * args:
+ * langs2prob: list of languages to search in with associated boosts
+ */
+ public function boost_lang_probs( $langs2prob ) {
+ foreach( $langs2prob as $l => $p ) {
+ $this->add_function( 'field_value_factor', array(
+ 'modifier' => 'none',
+ 'factor' => $p,
+ 'missing' => 0.01, //1% chance doc did not have right lang detected
+ ) );
+ }
+ }
+
+ ////////////////////////////////////
+ // Helper Methods
+
+ //Get the text after some prefix. eg @gibrown, or @"Greg Brown"
+ protected function get_fields( $field_prefix ) {
+ $regex = '/' . $field_prefix . '(("[^"]+")|([^\\p{Z}]+))/';
+ if ( preg_match_all( $regex, $this->current_query, $match ) ) {
+ return $match[1];
+ }
+ return false;
+ }
+
+ //Remove the prefix and text from the query
+ protected function remove_fields( $field_name ) {
+ $regex = '/' . $field_name . '(("[^"]+")|([^\\p{Z}]+))/';
+ $this->current_query = preg_replace( $regex, '', $this->current_query );
+ }
+
+ //Best effort string truncation that splits on word breaks
+ protected function truncate_string( $string, $limit, $break=" " ) {
+ if ( mb_strwidth( $string ) <= $limit ) {
+ return $string;
+ }
+
+ // walk backwards from $limit to find first break
+ $breakpoint = $limit;
+ $broken = false;
+ while ( $breakpoint > 0 ) {
+ if ( $break === mb_strimwidth( $string, $breakpoint, 1 ) ) {
+ $string = mb_strimwidth( $string, 0, $breakpoint );
+ $broken = true;
+ break;
+ }
+ $breakpoint--;
+ }
+ // if we weren't able to find a break, need to chop mid-word
+ if ( !$broken ) {
+ $string = mb_strimwidth( $string, 0, $limit );
+ }
+ return $string;
+ }
+
+}
diff --git a/plugins/jetpack/_inc/lib/markdown/0-load.php b/plugins/jetpack/_inc/lib/markdown/0-load.php
new file mode 100644
index 00000000..bf5993e3
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/markdown/0-load.php
@@ -0,0 +1,6 @@
+<?php
+
+if ( ! class_exists( 'MarkdownExtra_Parser' ) )
+ jetpack_require_lib( 'markdown/extra' );
+
+jetpack_require_lib( 'markdown/gfm' );
diff --git a/plugins/jetpack/_inc/lib/markdown/README.md b/plugins/jetpack/_inc/lib/markdown/README.md
new file mode 100644
index 00000000..45f298d1
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/markdown/README.md
@@ -0,0 +1,19 @@
+# Markdown parsing library
+
+Contains two libraries:
+
+* `/extra`
+ - Gives you `MardownExtra_Parser` and `Markdown_Parser`
+ - Docs at http://michelf.ca/projects/php-markdown/extra/
+
+* `/gfm` -- Github Flavored Markdown
+ - Gives you `WPCom_GHF_Markdown_Parser`
+ - It has the same interface as `MarkdownExtra_Parser`
+ - Adds support for fenced code blocks: https://help.github.com/articles/creating-and-highlighting-code-blocks/#fenced-code-blocks
+ - By default it replaces them with a code shortcode
+ - You can change this using the `$use_code_shortcode` member variable
+ - You can change the code shortcode wrapping with `$shortcode_start` and `$shortcode_end` member variables
+ - The `$preserve_shortcodes` member variable will preserve all registered shortcodes untouched. Requires WordPress to be loaded for `get_shortcode_regex()`
+ - The `$preserve_latex` member variable will preserve oldskool $latex yer-latex$ codes untouched.
+ - The `$strip_paras` member variable will strip <p> tags because that's what WordPress likes.
+ - See `WPCom_GHF_Markdown_Parser::__construct()` for how the above member variable defaults are set.
diff --git a/plugins/jetpack/_inc/lib/markdown/extra.php b/plugins/jetpack/_inc/lib/markdown/extra.php
new file mode 100644
index 00000000..fd85a3c8
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/markdown/extra.php
@@ -0,0 +1,3207 @@
+<?php
+#
+# Markdown Extra - A text-to-HTML conversion tool for web writers
+#
+# PHP Markdown & Extra
+# Copyright (c) 2004-2013 Michel Fortin
+# <http://michelf.ca/projects/php-markdown/>
+#
+# Original Markdown
+# Copyright (c) 2004-2006 John Gruber
+# <http://daringfireball.net/projects/markdown/>
+#
+# Tweaked to remove WordPress interface
+
+
+define( 'MARKDOWN_VERSION', "1.0.2" ); # 29 Nov 2013
+define( 'MARKDOWNEXTRA_VERSION', "1.2.8" ); # 29 Nov 2013
+
+
+#
+# Global default settings:
+#
+
+# Change to ">" for HTML output
+@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />");
+
+# Define the width of a tab for code blocks.
+@define( 'MARKDOWN_TAB_WIDTH', 4 );
+
+# Optional title attribute for footnote links and backlinks.
+@define( 'MARKDOWN_FN_LINK_TITLE', "" );
+@define( 'MARKDOWN_FN_BACKLINK_TITLE', "" );
+
+# Optional class attribute for footnote links and backlinks.
+@define( 'MARKDOWN_FN_LINK_CLASS', "jetpack-footnote" );
+@define( 'MARKDOWN_FN_BACKLINK_CLASS', "" );
+
+# Optional class prefix for fenced code block.
+@define( 'MARKDOWN_CODE_CLASS_PREFIX', "language-" );
+
+# Class attribute for code blocks goes on the `code` tag;
+# setting this to true will put attributes on the `pre` tag instead.
+@define( 'MARKDOWN_CODE_ATTR_ON_PRE', false );
+
+
+
+### Standard Function Interface ###
+
+@define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser' );
+
+function Markdown($text) {
+#
+# Initialize the parser and return the result of its transform method.
+#
+ # Setup static parser variable.
+ static $parser;
+ if (!isset($parser)) {
+ $parser_class = MARKDOWN_PARSER_CLASS;
+ $parser = new $parser_class;
+ }
+
+ # Transform text using parser.
+ return $parser->transform($text);
+}
+
+/**
+ * Returns the length of $text loosely counting the number of UTF-8 characters with regular expression.
+ * Used by the Markdown_Parser class when mb_strlen is not available.
+ *
+ * @since 5.9
+ *
+ * @return string Length of the multibyte string
+ *
+ */
+function jetpack_utf8_strlen( $text ) {
+ return preg_match_all( "/[\\x00-\\xBF]|[\\xC0-\\xFF][\\x80-\\xBF]*/", $text, $m );
+}
+
+#
+# Markdown Parser Class
+#
+
+class Markdown_Parser {
+
+ ### Configuration Variables ###
+
+ # Change to ">" for HTML output.
+ public $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX;
+ public $tab_width = MARKDOWN_TAB_WIDTH;
+
+ # Change to `true` to disallow markup or entities.
+ public $no_markup = false;
+ public $no_entities = false;
+
+ # Predefined urls and titles for reference links and images.
+ public $predef_urls = array();
+ public $predef_titles = array();
+
+
+ ### Parser Implementation ###
+
+ # Regex to match balanced [brackets].
+ # Needed to insert a maximum bracked depth while converting to PHP.
+ public $nested_brackets_depth = 6;
+ public $nested_brackets_re;
+
+ public $nested_url_parenthesis_depth = 4;
+ public $nested_url_parenthesis_re;
+
+ # Table of hash values for escaped characters:
+ public $escape_chars = '\`*_{}[]()>#+-.!';
+ public $escape_chars_re;
+
+
+ function __construct() {
+ #
+ # Constructor function. Initialize appropriate member variables.
+ #
+ $this->_initDetab();
+ $this->prepareItalicsAndBold();
+
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
+ str_repeat('\])*', $this->nested_brackets_depth);
+
+ $this->nested_url_parenthesis_re =
+ str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
+ str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
+
+ $this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
+
+ # Sort document, block, and span gamut in ascendent priority order.
+ asort($this->document_gamut);
+ asort($this->block_gamut);
+ asort($this->span_gamut);
+ }
+
+
+ # Internal hashes used during transformation.
+ public $urls = array();
+ public $titles = array();
+ public $html_hashes = array();
+
+ # Status flag to avoid invalid nesting.
+ public $in_anchor = false;
+
+
+ function setup() {
+ #
+ # Called before the transformation process starts to setup parser
+ # states.
+ #
+ # Clear global hashes.
+ $this->urls = $this->predef_urls;
+ $this->titles = $this->predef_titles;
+ $this->html_hashes = array();
+
+ $this->in_anchor = false;
+ }
+
+ function teardown() {
+ #
+ # Called after the transformation process to clear any variable
+ # which may be taking up memory unnecessarly.
+ #
+ $this->urls = array();
+ $this->titles = array();
+ $this->html_hashes = array();
+ }
+
+
+ function transform($text) {
+ #
+ # Main function. Performs some preprocessing on the input text
+ # and pass it through the document gamut.
+ #
+ $this->setup();
+
+ # Remove UTF-8 BOM and marker character in input, if present.
+ $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
+
+ # Standardize line endings:
+ # DOS to Unix and Mac to Unix
+ $text = preg_replace('{\r\n?}', "\n", $text);
+
+ # Make sure $text ends with a couple of newlines:
+ $text .= "\n\n";
+
+ # Convert all tabs to spaces.
+ $text = $this->detab($text);
+
+ # Turn block-level HTML blocks into hash entries
+ $text = $this->hashHTMLBlocks($text);
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ ]*\n+/ .
+ $text = preg_replace('/^[ ]+$/m', '', $text);
+
+ # Run document gamut methods.
+ foreach ($this->document_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ $this->teardown();
+
+ return $text . "\n";
+ }
+
+ public $document_gamut = array(
+ # Strip link definitions, store in hashes.
+ "stripLinkDefinitions" => 20,
+
+ "runBasicBlockGamut" => 30,
+ );
+
+
+ function stripLinkDefinitions($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: ^[id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ [ ]*
+ (?:
+ <(.+?)> # url = $2
+ |
+ (\S+?) # url = $3
+ )
+ [ ]*
+ \n? # maybe one newline
+ [ ]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ ["(]
+ (.*?) # title = $4
+ [")]
+ [ ]*
+ )? # title is optional
+ (?:\n+|\Z)
+ }xm',
+ array(&$this, '_stripLinkDefinitions_callback'),
+ $text);
+ return $text;
+ }
+ function _stripLinkDefinitions_callback($matches) {
+ $link_id = strtolower($matches[1]);
+ $url = $matches[2] == '' ? $matches[3] : $matches[2];
+ $this->urls[$link_id] = $url;
+ $this->titles[$link_id] =& $matches[4];
+ return ''; # String that will replace the block
+ }
+
+
+ function hashHTMLBlocks($text) {
+ if ($this->no_markup) return $text;
+
+ $less_than_tab = $this->tab_width - 1;
+
+ # Hashify HTML blocks:
+ # We only want to do this for block-level HTML tags, such as headers,
+ # lists, and tables. That's because we still want to wrap <p>s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded:
+ #
+ # * List "a" is made of tags which can be both inline or block-level.
+ # These will be treated block-level when the start tag is alone on
+ # its line, otherwise they're not matched here and will be taken as
+ # inline later.
+ # * List "b" is made of tags which are always block-level;
+ #
+ $block_tags_a_re = 'ins|del';
+ $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
+ 'script|noscript|form|fieldset|iframe|math|svg|'.
+ 'article|section|nav|aside|hgroup|header|footer|'.
+ 'figure';
+
+ # Regular expression for the content of a block tag.
+ $nested_tags_level = 4;
+ $attr = '
+ (?> # optional tag attributes
+ \s # starts with whitespace
+ (?>
+ [^>"/]+ # text outside quotes
+ |
+ /+(?!>) # slash not followed by ">"
+ |
+ "[^"]*" # text inside double quotes (tolerate ">")
+ |
+ \'[^\']*\' # text inside single quotes (tolerate ">")
+ )*
+ )?
+ ';
+ $content =
+ str_repeat('
+ (?>
+ [^<]+ # content without tag
+ |
+ <\2 # nested opening tag
+ '.$attr.' # attributes
+ (?>
+ />
+ |
+ >', $nested_tags_level). # end of opening tag
+ '.*?'. # last level nested tag content
+ str_repeat('
+ </\2\s*> # closing nested tag
+ )
+ |
+ <(?!/\2\s*> # other tags with a different name
+ )
+ )*',
+ $nested_tags_level);
+ $content2 = str_replace('\2', '\3', $content);
+
+ # First, look for nested blocks, e.g.:
+ # <div>
+ # <div>
+ # tags for inner block must be indented.
+ # </div>
+ # </div>
+ #
+ # The outermost tags must start at the left margin for this to match, and
+ # the inner nested divs must be indented.
+ # We need to do this before the next, more liberal match, because the next
+ # match will start at the first `<div>` and stop at the first `</div>`.
+ $text = preg_replace_callback('{(?>
+ (?>
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+
+ # Match from `\n<tag>` to `</tag>\n`, handling nested tags
+ # in between.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_b_re.')# start tag = $2
+ '.$attr.'> # attributes followed by > and \n
+ '.$content.' # content, support nesting
+ </\2> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special version for tags of group a.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_a_re.')# start tag = $3
+ '.$attr.'>[ ]*\n # attributes followed by >
+ '.$content2.' # content, support nesting
+ </\3> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special case just for <hr />. It was easier to make a special
+ # case than to make the other regex more complicated.
+
+ [ ]{0,'.$less_than_tab.'}
+ <(hr) # start tag = $2
+ '.$attr.' # attributes
+ /?> # the matching end tag
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # Special case for standalone HTML comments:
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+ <!-- .*? -->
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # PHP and ASP-style processor instructions (<? and <%)
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+ <([?%]) # $2
+ .*?
+ \2>
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ )
+ )}Sxmi',
+ array(&$this, '_hashHTMLBlocks_callback'),
+ $text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_callback($matches) {
+ $text = $matches[1];
+ $key = $this->hashBlock($text);
+ return "\n\n$key\n\n";
+ }
+
+
+ function hashPart($text, $boundary = 'X') {
+ #
+ # Called whenever a tag must be hashed when a function insert an atomic
+ # element in the text stream. Passing $text to through this function gives
+ # a unique text-token which will be reverted back when calling unhash.
+ #
+ # The $boundary argument specify what character should be used to surround
+ # the token. By convension, "B" is used for block elements that needs not
+ # to be wrapped into paragraph tags at the end, ":" is used for elements
+ # that are word separators and "X" is used in the general case.
+ #
+ # Swap back any tag hash found in $text so we do not have to `unhash`
+ # multiple times at the end.
+ $text = $this->unhash($text);
+
+ # Then hash the block.
+ static $i = 0;
+ $key = "$boundary\x1A" . ++$i . $boundary;
+ $this->html_hashes[$key] = $text;
+ return $key; # String that will replace the tag.
+ }
+
+
+ function hashBlock($text) {
+ #
+ # Shortcut function for hashPart with block-level boundaries.
+ #
+ return $this->hashPart($text, 'B');
+ }
+
+
+ public $block_gamut = array(
+ #
+ # These are all the transformations that form block-level
+ # tags like paragraphs, headers, and list items.
+ #
+ "doHeaders" => 10,
+ "doHorizontalRules" => 20,
+
+ "doLists" => 40,
+ "doCodeBlocks" => 50,
+ "doBlockQuotes" => 60,
+ );
+
+ function runBlockGamut($text) {
+ #
+ # Run block gamut tranformations.
+ #
+ # We need to escape raw HTML in Markdown source before doing anything
+ # else. This need to be done for each block, and not only at the
+ # beginning in the Markdown function since hashed blocks can be part of
+ # list items and could have been indented. Indented blocks would have
+ # been seen as a code block in a previous pass of hashHTMLBlocks.
+ $text = $this->hashHTMLBlocks($text);
+
+ return $this->runBasicBlockGamut($text);
+ }
+
+ function runBasicBlockGamut($text) {
+ #
+ # Run block gamut tranformations, without hashing HTML blocks. This is
+ # useful when HTML blocks are known to be already hashed, like in the first
+ # whole-document pass.
+ #
+ foreach ($this->block_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ # Finally form paragraph and restore hashed blocks.
+ $text = $this->formParagraphs($text);
+
+ return $text;
+ }
+
+
+ function doHorizontalRules($text) {
+ # Do Horizontal Rules:
+ return preg_replace(
+ '{
+ ^[ ]{0,3} # Leading space
+ ([-*_]) # $1: First marker
+ (?> # Repeated marker group
+ [ ]{0,2} # Zero, one, or two spaces.
+ \1 # Marker character
+ ){2,} # Group repeated at least twice
+ [ ]* # Tailing spaces
+ $ # End of line.
+ }mx',
+ "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n",
+ $text);
+ }
+
+
+ public $span_gamut = array(
+ #
+ # These are all the transformations that occur *within* block-level
+ # tags like paragraphs, headers, and list items.
+ #
+ # Process character escapes, code spans, and inline HTML
+ # in one shot.
+ "parseSpan" => -30,
+
+ # Process anchor and image tags. Images must come first,
+ # because ![foo][f] looks like an anchor.
+ "doImages" => 10,
+ "doAnchors" => 20,
+
+ # Make links out of things like `<http://example.com/>`
+ # Must come after doAnchors, because you can use < and >
+ # delimiters in inline links like [this](<url>).
+ "doAutoLinks" => 30,
+ "encodeAmpsAndAngles" => 40,
+
+ "doItalicsAndBold" => 50,
+ "doHardBreaks" => 60,
+ );
+
+ function runSpanGamut($text) {
+ #
+ # Run span gamut tranformations.
+ #
+ foreach ($this->span_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ return $text;
+ }
+
+
+ function doHardBreaks($text) {
+ # Do hard breaks:
+ return preg_replace_callback('/ {2,}\n/',
+ array(&$this, '_doHardBreaks_callback'), $text);
+ }
+ function _doHardBreaks_callback($matches) {
+ return $this->hashPart("<br$this->empty_element_suffix\n");
+ }
+
+
+ function doAnchors($text) {
+ #
+ # Turn Markdown link shortcuts into XHTML <a> tags.
+ #
+ if ($this->in_anchor) return $text;
+ $this->in_anchor = true;
+
+ #
+ # First, handle reference-style links: [link text] [id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ #
+ # Next, inline-style links: [link text](url "optional title")
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+ \( # literal paren
+ [ \n]*
+ (?:
+ <(.+?)> # href = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # href = $4
+ )
+ [ \n]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # Title = $7
+ \6 # matching quote
+ [ \n]* # ignore any spaces/tabs between closing quote and )
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_doAnchors_inline_callback'), $text);
+
+ #
+ # Last, handle reference-style shortcuts: [link text]
+ # These must come last in case you've also got [link text][1]
+ # or [link text](/foo)
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ([^\[\]]+) # link text = $2; can\'t contain [ or ]
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ $this->in_anchor = false;
+ return $text;
+ }
+ function _doAnchors_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $matches[2];
+ $link_id =& $matches[3];
+
+ if ($link_id == "") {
+ # for shortcut links like [this][] or [this].
+ $link_id = $link_text;
+ }
+
+ # lower-case and turn embedded newlines into spaces
+ $link_id = strtolower($link_id);
+ $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
+
+ if (isset($this->urls[$link_id])) {
+ $url = $this->urls[$link_id];
+ $url = $this->encodeAttribute($url);
+
+ $result = "<a href=\"$url\"";
+ if ( isset( $this->titles[$link_id] ) ) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text</a>";
+ $result = $this->hashPart($result);
+ }
+ else {
+ $result = $whole_match;
+ }
+ return $result;
+ }
+ function _doAnchors_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $this->runSpanGamut($matches[2]);
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+
+ $url = $this->encodeAttribute($url);
+
+ $result = "<a href=\"$url\"";
+ if (isset($title)) {
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text</a>";
+
+ return $this->hashPart($result);
+ }
+
+
+ function doImages($text) {
+ #
+ # Turn Markdown image shortcuts into <img> tags.
+ #
+ #
+ # First, handle reference-style labeled images: ![alt text][id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+
+ )
+ }xs',
+ array(&$this, '_doImages_reference_callback'), $text);
+
+ #
+ # Next, handle inline images: ![alt text](url "optional title")
+ # Don't forget: encode * and _
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+ \s? # One optional whitespace character
+ \( # literal paren
+ [ \n]*
+ (?:
+ <(\S*)> # src url = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # src url = $4
+ )
+ [ \n]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # title = $7
+ \6 # matching quote
+ [ \n]*
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_doImages_inline_callback'), $text);
+
+ return $text;
+ }
+ function _doImages_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $link_id = strtolower($matches[3]);
+
+ if ($link_id == "") {
+ $link_id = strtolower($alt_text); # for shortcut links like ![this][].
+ }
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ if (isset($this->urls[$link_id])) {
+ $url = $this->encodeAttribute($this->urls[$link_id]);
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
+ if (isset($this->titles[$link_id])) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+ $result .= $this->empty_element_suffix;
+ $result = $this->hashPart($result);
+ }
+ else {
+ # If there's no such link ID, leave intact:
+ $result = $whole_match;
+ }
+
+ return $result;
+ }
+ function _doImages_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ $url = $this->encodeAttribute($url);
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
+ if (isset($title)) {
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\""; # $title already quoted
+ }
+ $result .= $this->empty_element_suffix;
+
+ return $this->hashPart($result);
+ }
+
+
+ function doHeaders($text) {
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ #
+ $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
+ array(&$this, '_doHeaders_callback_setext'), $text);
+
+ # atx-style headers:
+ # # Header 1
+ # ## Header 2
+ # ## Header 2 with closing hashes ##
+ # ...
+ # ###### Header 6
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #\'s (not counted)
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_callback_setext($matches) {
+ # Terrible hack to check we haven't found an empty list item.
+ if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
+ return $matches[0];
+
+ $level = $matches[2]{0} == '=' ? 1 : 2;
+ $block = "<h$level>".$this->runSpanGamut($matches[1])."</h$level>";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $block = "<h$level>".$this->runSpanGamut($matches[2])."</h$level>";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+
+
+ function doLists($text) {
+ #
+ # Form HTML ordered (numbered) and unordered (bulleted) lists.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[\.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $markers_relist = array(
+ $marker_ul_re => $marker_ol_re,
+ $marker_ol_re => $marker_ul_re,
+ );
+
+ foreach ($markers_relist as $marker_re => $other_marker_re) {
+ # Re-usable pattern to match any entirel ul or ol list:
+ $whole_list_re = '
+ ( # $1 = whole list
+ ( # $2
+ ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces
+ ('.$marker_re.') # $4 = first list item marker
+ [ ]+
+ )
+ (?s:.+?)
+ ( # $5
+ \z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another list item marker
+ [ ]*
+ '.$marker_re.'[ ]+
+ )
+ |
+ (?= # Lookahead for another kind of list
+ \n
+ \3 # Must have the same indentation
+ '.$other_marker_re.'[ ]+
+ )
+ )
+ )
+ '; // mx
+
+ # We use a different prefix before nested lists than top-level lists.
+ # See extended comment in _ProcessListItems().
+
+ if ($this->list_level) {
+ $text = preg_replace_callback('{
+ ^
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doLists_callback'), $text);
+ }
+ else {
+ $text = preg_replace_callback('{
+ (?:(?<=\n)\n|\A\n?) # Must eat the newline
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doLists_callback'), $text);
+ }
+ }
+
+ return $text;
+ }
+ function _doLists_callback($matches) {
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[\.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $list = $matches[1];
+ $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol";
+
+ $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
+
+ $list .= "\n";
+ $result = $this->processListItems($list, $marker_any_re);
+
+ $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>");
+ return "\n". $result ."\n\n";
+ }
+
+ public $list_level = 0;
+
+ function processListItems($list_str, $marker_any_re) {
+ #
+ # Process the contents of a single ordered or unordered list, splitting it
+ # into individual list items.
+ #
+ # The $this->list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+
+ $this->list_level++;
+
+ # trim trailing blank lines:
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+ $list_str = preg_replace_callback('{
+ (\n)? # leading line = $1
+ (^[ ]*) # leading whitespace = $2
+ ('.$marker_any_re.' # list marker and space = $3
+ (?:[ ]+|(?=\n)) # space only required if item is not empty
+ )
+ ((?s:.*?)) # list item text = $4
+ (?:(\n+(?=\n))|\n) # tailing blank line = $5
+ (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n))))
+ }xm',
+ array(&$this, '_processListItems_callback'), $list_str);
+
+ $this->list_level--;
+ return $list_str;
+ }
+ function _processListItems_callback($matches) {
+ $item = $matches[4];
+ $leading_line =& $matches[1];
+ $leading_space =& $matches[2];
+ $marker_space = $matches[3];
+ $tailing_blank_line =& $matches[5];
+
+ if ($leading_line || $tailing_blank_line ||
+ preg_match('/\n{2,}/', $item))
+ {
+ # Replace marker with the appropriate whitespace indentation
+ $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
+ $item = $this->runBlockGamut($this->outdent($item)."\n");
+ }
+ else {
+ # Recursion for sub-lists:
+ $item = $this->doLists($this->outdent($item));
+ $item = preg_replace('/\n+$/', '', $item);
+ $item = $this->runSpanGamut($item);
+ }
+
+ return "<li>" . $item . "</li>\n";
+ }
+
+
+ function doCodeBlocks($text) {
+ #
+ # Process Markdown `<pre><code>` blocks.
+ #
+ $text = preg_replace_callback('{
+ (?:\n\n|\A\n?)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?>
+ [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ }xm',
+ array(&$this, '_doCodeBlocks_callback'), $text);
+
+ return $text;
+ }
+ function _doCodeBlocks_callback($matches) {
+ $codeblock = $matches[1];
+
+ $codeblock = $this->outdent($codeblock);
+ $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
+
+ # trim leading newlines and trailing newlines
+ $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
+
+ $codeblock = "<pre><code>$codeblock\n</code></pre>";
+ return "\n\n".$this->hashBlock($codeblock)."\n\n";
+ }
+
+
+ function makeCodeSpan($code) {
+ #
+ # Create a code span markup for $code. Called from handleSpanToken.
+ #
+ $code = htmlspecialchars(trim($code), ENT_NOQUOTES);
+ return $this->hashPart("<code>$code</code>");
+ }
+
+
+ public $em_relist = array(
+ '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?=\S|$)(?![\.,:;]\s)',
+ '*' => '(?<=\S|^)(?<!\*)\*(?!\*)',
+ '_' => '(?<=\S|^)(?<!_)_(?!_)',
+ );
+ public $strong_relist = array(
+ '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?=\S|$)(?![\.,:;]\s)',
+ '**' => '(?<=\S|^)(?<!\*)\*\*(?!\*)',
+ '__' => '(?<=\S|^)(?<!_)__(?!_)',
+ );
+ public $em_strong_relist = array(
+ '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?=\S|$)(?![\.,:;]\s)',
+ '***' => '(?<=\S|^)(?<!\*)\*\*\*(?!\*)',
+ '___' => '(?<=\S|^)(?<!_)___(?!_)',
+ );
+ public $em_strong_prepared_relist;
+
+ function prepareItalicsAndBold() {
+ #
+ # Prepare regular expressions for searching emphasis tokens in any
+ # context.
+ #
+ foreach ($this->em_relist as $em => $em_re) {
+ foreach ($this->strong_relist as $strong => $strong_re) {
+ # Construct list of allowed token expressions.
+ $token_relist = array();
+ if (isset($this->em_strong_relist["$em$strong"])) {
+ $token_relist[] = $this->em_strong_relist["$em$strong"];
+ }
+ $token_relist[] = $em_re;
+ $token_relist[] = $strong_re;
+
+ # Construct master expression from list.
+ $token_re = '{('. implode('|', $token_relist) .')}';
+ $this->em_strong_prepared_relist["$em$strong"] = $token_re;
+ }
+ }
+ }
+
+ function doItalicsAndBold($text) {
+ $token_stack = array('');
+ $text_stack = array('');
+ $em = '';
+ $strong = '';
+ $tree_char_em = false;
+
+ while (1) {
+ #
+ # Get prepared regular expression for seraching emphasis tokens
+ # in current context.
+ #
+ $token_re = $this->em_strong_prepared_relist["$em$strong"];
+
+ #
+ # Each loop iteration search for the next emphasis token.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+ $text_stack[0] .= $parts[0];
+ $token =& $parts[1];
+ $text =& $parts[2];
+
+ if (empty($token)) {
+ # Reached end of text span: empty stack without emitting.
+ # any more emphasis.
+ while ($token_stack[0]) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ break;
+ }
+
+ $token_len = strlen($token);
+ if ($tree_char_em) {
+ # Reached closing marker while inside a three-char emphasis.
+ if ($token_len == 3) {
+ # Three-char closing marker, close em and strong.
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<strong><em>$span</em></strong>";
+ $text_stack[0] .= $this->hashPart($span);
+ $em = '';
+ $strong = '';
+ } else {
+ # Other closing marker: close one em or strong and
+ # change current token state to match the other
+ $token_stack[0] = str_repeat($token{0}, 3-$token_len);
+ $tag = $token_len == 2 ? "strong" : "em";
+ $span = $text_stack[0];
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span</$tag>";
+ $text_stack[0] = $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ $tree_char_em = false;
+ } else if ($token_len == 3) {
+ if ($em) {
+ # Reached closing marker for both em and strong.
+ # Closing strong marker:
+ for ($i = 0; $i < 2; ++$i) {
+ $shifted_token = array_shift($token_stack);
+ $tag = strlen($shifted_token) == 2 ? "strong" : "em";
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span</$tag>";
+ $text_stack[0] .= $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ } else {
+ # Reached opening three-char emphasis marker. Push on token
+ # stack; will be handled by the special condition above.
+ $em = $token{0};
+ $strong = "$em$em";
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $tree_char_em = true;
+ }
+ } else if ($token_len == 2) {
+ if ($strong) {
+ # Unwind any dangling emphasis marker:
+ if (strlen($token_stack[0]) == 1) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ # Closing strong marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<strong>$span</strong>";
+ $text_stack[0] .= $this->hashPart($span);
+ $strong = '';
+ } else {
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $strong = $token;
+ }
+ } else {
+ # Here $token_len == 1
+ if ($em) {
+ if (strlen($token_stack[0]) == 1) {
+ # Closing emphasis marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<em>$span</em>";
+ $text_stack[0] .= $this->hashPart($span);
+ $em = '';
+ } else {
+ $text_stack[0] .= $token;
+ }
+ } else {
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $em = $token;
+ }
+ }
+ }
+ return $text_stack[0];
+ }
+
+
+ function doBlockQuotes($text) {
+ $text = preg_replace_callback('/
+ ( # Wrap whole match in $1
+ (?>
+ ^[ ]*>[ ]? # ">" at the start of a line
+ .+\n # rest of the first line
+ (.+\n)* # subsequent consecutive lines
+ \n* # blanks
+ )+
+ )
+ /xm',
+ array(&$this, '_doBlockQuotes_callback'), $text);
+
+ return $text;
+ }
+ function _doBlockQuotes_callback($matches) {
+ $bq = $matches[1];
+ # trim one level of quoting - trim whitespace-only lines
+ $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
+ $bq = $this->runBlockGamut($bq); # recurse
+
+ $bq = preg_replace('/^/m', " ", $bq);
+ # These leading spaces cause problem with <pre> content,
+ # so we need to fix that:
+ $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
+ array(&$this, '_doBlockQuotes_callback2'), $bq);
+
+ return "\n". $this->hashBlock("<blockquote>\n$bq\n</blockquote>")."\n\n";
+ }
+ function _doBlockQuotes_callback2($matches) {
+ $pre = $matches[1];
+ $pre = preg_replace('/^ /m', '', $pre);
+ return $pre;
+ }
+
+
+ function formParagraphs($text) {
+ #
+ # Params:
+ # $text - string to process with html <p> tags
+ #
+ # Strip leading and trailing lines:
+ $text = preg_replace('/\A\n+|\n+\z/', '', $text);
+
+ $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+ #
+ # Wrap <p> tags and unhashify HTML blocks
+ #
+ foreach ($grafs as $key => $value) {
+ if (!preg_match('/^B\x1A[0-9]+B$/', $value)) {
+ # Is a paragraph.
+ $value = $this->runSpanGamut($value);
+ $value = preg_replace('/^([ ]*)/', "<p>", $value);
+ $value .= "</p>";
+ $grafs[$key] = $this->unhash($value);
+ }
+ else {
+ # Is a block.
+ # Modify elements of @grafs in-place...
+ $graf = $value;
+ $block = $this->html_hashes[$graf];
+ $graf = $block;
+// if (preg_match('{
+// \A
+// ( # $1 = <div> tag
+// <div \s+
+// [^>]*
+// \b
+// markdown\s*=\s* ([\'"]) # $2 = attr quote char
+// 1
+// \2
+// [^>]*
+// >
+// )
+// ( # $3 = contents
+// .*
+// )
+// (</div>) # $4 = closing tag
+// \z
+// }xs', $block, $matches))
+// {
+// list(, $div_open, , $div_content, $div_close) = $matches;
+//
+// # We can't call Markdown(), because that resets the hash;
+// # that initialization code should be pulled into its own sub, though.
+// $div_content = $this->hashHTMLBlocks($div_content);
+//
+// # Run document gamut methods on the content.
+// foreach ($this->document_gamut as $method => $priority) {
+// $div_content = $this->$method($div_content);
+// }
+//
+// $div_open = preg_replace(
+// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open);
+//
+// $graf = $div_open . "\n" . $div_content . "\n" . $div_close;
+// }
+ $grafs[$key] = $graf;
+ }
+ }
+
+ return implode("\n\n", $grafs);
+ }
+
+
+ function encodeAttribute($text) {
+ #
+ # Encode text for a double-quoted HTML attribute. This function
+ # is *not* suitable for attributes enclosed in single quotes.
+ #
+ $text = $this->encodeAmpsAndAngles($text);
+ $text = str_replace('"', '&quot;', $text);
+ return $text;
+ }
+
+
+ function encodeAmpsAndAngles($text) {
+ #
+ # Smart processing for ampersands and angle brackets that need to
+ # be encoded. Valid character entities are left alone unless the
+ # no-entities mode is set.
+ #
+ if ($this->no_entities) {
+ $text = str_replace('&', '&amp;', $text);
+ } else {
+ # Ampersand-encoding based entirely on Nat Irons's Amputator
+ # MT plugin: <http://bumppo.net/projects/amputator/>
+ $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
+ '&amp;', $text);;
+ }
+ # Encode remaining <'s
+ $text = str_replace('<', '&lt;', $text);
+
+ return $text;
+ }
+
+
+ function doAutoLinks($text) {
+ $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i',
+ array(&$this, '_doAutoLinks_url_callback'), $text);
+
+ # Email addresses: <address@domain.foo>
+ $text = preg_replace_callback('{
+ <
+ (?:mailto:)?
+ (
+ (?:
+ [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+
+ |
+ ".*?"
+ )
+ \@
+ (?:
+ [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
+ |
+ \[[\d.a-fA-F:]+\] # IPv4 & IPv6
+ )
+ )
+ >
+ }xi',
+ array(&$this, '_doAutoLinks_email_callback'), $text);
+ $text = preg_replace_callback('{<(tel:([^\'">\s]+))>}i',array(&$this, '_doAutoLinks_tel_callback'), $text);
+
+ return $text;
+ }
+ function _doAutoLinks_tel_callback($matches) {
+ $url = $this->encodeAttribute($matches[1]);
+ $tel = $this->encodeAttribute($matches[2]);
+ $link = "<a href=\"$url\">$tel</a>";
+ return $this->hashPart($link);
+ }
+ function _doAutoLinks_url_callback($matches) {
+ $url = $this->encodeAttribute($matches[1]);
+ $link = "<a href=\"$url\">$url</a>";
+ return $this->hashPart($link);
+ }
+ function _doAutoLinks_email_callback($matches) {
+ $address = $matches[1];
+ $link = $this->encodeEmailAddress($address);
+ return $this->hashPart($link);
+ }
+
+
+ function encodeEmailAddress($addr) {
+ #
+ # Input: an email address, e.g. "foo@example.com"
+ #
+ # Output: the email address as a mailto link, with each character
+ # of the address encoded as either a decimal or hex entity, in
+ # the hopes of foiling most address harvesting spam bots. E.g.:
+ #
+ # <p><a href="&#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
+ # &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
+ # &#x6d;">&#x66;o&#111;&#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;
+ # &#101;&#46;&#x63;&#111;&#x6d;</a></p>
+ #
+ # Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
+ # With some optimizations by Milian Wolff.
+ #
+ $addr = "mailto:" . $addr;
+ $chars = preg_split('/(?<!^)(?!$)/', $addr);
+ $seed = (int)abs(crc32($addr) / strlen($addr)); # Deterministic seed.
+
+ foreach ($chars as $key => $char) {
+ $ord = ord($char);
+ # Ignore non-ascii chars.
+ if ($ord < 128) {
+ $r = ($seed * (1 + $key)) % 100; # Pseudo-random function.
+ # roughly 10% raw, 45% hex, 45% dec
+ # '@' *must* be encoded. I insist.
+ if ($r > 90 && $char != '@') /* do nothing */;
+ else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';';
+ else $chars[$key] = '&#'.$ord.';';
+ }
+ }
+
+ $addr = implode('', $chars);
+ $text = implode('', array_slice($chars, 7)); # text without `mailto:`
+ $addr = "<a href=\"$addr\">$text</a>";
+
+ return $addr;
+ }
+
+
+ function parseSpan($str) {
+ #
+ # Take the string $str and parse it into tokens, hashing embedded HTML,
+ # escaped characters and handling code spans.
+ #
+ $output = '';
+
+ $span_re = '{
+ (
+ \\\\'.$this->escape_chars_re.'
+ |
+ (?<![`\\\\])
+ `+ # code span marker
+ '.( $this->no_markup ? '' : '
+ |
+ <!-- .*? --> # comment
+ |
+ <\?.*?\?> | <%.*?%> # processing instruction
+ |
+ <[!$]?[-a-zA-Z0-9:_]+ # regular tags
+ (?>
+ \s
+ (?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
+ )?
+ >
+ |
+ <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag
+ |
+ </[-a-zA-Z0-9:_]+\s*> # closing tag
+ ').'
+ )
+ }xs';
+
+ while (1) {
+ #
+ # Each loop iteration search for either the next tag, the next
+ # openning code span marker, or the next escaped character.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ # Create token from text preceding tag.
+ if ($parts[0] != "") {
+ $output .= $parts[0];
+ }
+
+ # Check if we reach the end.
+ if (isset($parts[1])) {
+ $output .= $this->handleSpanToken($parts[1], $parts[2]);
+ $str = $parts[2];
+ }
+ else {
+ break;
+ }
+ }
+
+ return $output;
+ }
+
+
+ function handleSpanToken($token, &$str) {
+ #
+ # Handle $token provided by parseSpan by determining its nature and
+ # returning the corresponding value that should replace it.
+ #
+ switch ($token{0}) {
+ case "\\":
+ return $this->hashPart("&#". ord($token{1}). ";");
+ case "`":
+ # Search for end marker in remaining text.
+ if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
+ $str, $matches))
+ {
+ $str = $matches[2];
+ $codespan = $this->makeCodeSpan($matches[1]);
+ return $this->hashPart($codespan);
+ }
+ return $token; // return as text since no ending marker found.
+ default:
+ return $this->hashPart($token);
+ }
+ }
+
+
+ function outdent($text) {
+ #
+ # Remove one level of line-leading tabs or spaces
+ #
+ return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text);
+ }
+
+
+ # String length function for detab. `_initDetab` will create a function to
+ # hanlde UTF-8 if the default function does not exist.
+ public $utf8_strlen = 'mb_strlen';
+
+ function detab($text) {
+ #
+ # Replace tabs with the appropriate amount of space.
+ #
+ # For each line we separate the line in blocks delemited by
+ # tab characters. Then we reconstruct every line by adding the
+ # appropriate number of space between each blocks.
+
+ $text = preg_replace_callback('/^.*\t.*$/m',
+ array(&$this, '_detab_callback'), $text);
+
+ return $text;
+ }
+ function _detab_callback($matches) {
+ $line = $matches[0];
+ $strlen = $this->utf8_strlen; # strlen function for UTF-8.
+
+ # Split in blocks.
+ $blocks = explode("\t", $line);
+ # Add each blocks to the line.
+ $line = $blocks[0];
+ unset($blocks[0]); # Do not add first block twice.
+ foreach ($blocks as $block) {
+ # Calculate amount of space, insert spaces, insert block.
+ $amount = $this->tab_width -
+ $strlen($line, 'UTF-8') % $this->tab_width;
+ $line .= str_repeat(" ", $amount) . $block;
+ }
+ return $line;
+ }
+ function _initDetab() {
+ #
+ # Check for the availability of the function in the `utf8_strlen` property
+ # (initially `mb_strlen`). If the function is not available, use jetpack_utf8_strlen
+ # that will loosely count the number of UTF-8 characters with a
+ # regular expression.
+ #
+ if ( function_exists( $this->utf8_strlen ) ) {
+ return;
+ }
+ $this->utf8_strlen = 'jetpack_utf8_strlen';
+ }
+
+
+ function unhash($text) {
+ #
+ # Swap back in all the tags hashed by _HashHTMLBlocks.
+ #
+ return preg_replace_callback('/(.)\x1A[0-9]+\1/',
+ array(&$this, '_unhash_callback'), $text);
+ }
+ function _unhash_callback($matches) {
+ return $this->html_hashes[$matches[0]];
+ }
+
+}
+
+
+#
+# Markdown Extra Parser Class
+#
+
+class MarkdownExtra_Parser extends Markdown_Parser {
+
+ ### Configuration Variables ###
+
+ # Prefix for footnote ids.
+ public $fn_id_prefix = "";
+
+ # Optional title attribute for footnote links and backlinks.
+ public $fn_link_title = MARKDOWN_FN_LINK_TITLE;
+ public $fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE;
+
+ # Optional class attribute for footnote links and backlinks.
+ public $fn_link_class = MARKDOWN_FN_LINK_CLASS;
+ public $fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS;
+
+ # Optional class prefix for fenced code block.
+ public $code_class_prefix = MARKDOWN_CODE_CLASS_PREFIX;
+ # Class attribute for code blocks goes on the `code` tag;
+ # setting this to true will put attributes on the `pre` tag instead.
+ public $code_attr_on_pre = MARKDOWN_CODE_ATTR_ON_PRE;
+
+ # Predefined abbreviations.
+ public $predef_abbr = array();
+
+
+ ### Parser Implementation ###
+
+ function __construct() {
+ #
+ # Constructor function. Initialize the parser object.
+ #
+ # Add extra escapable characters before parent constructor
+ # initialize the table.
+ $this->escape_chars .= ':|';
+
+ # Insert extra document, block, and span transformations.
+ # Parent constructor will do the sorting.
+ $this->document_gamut += array(
+ "doFencedCodeBlocks" => 5,
+ "stripFootnotes" => 15,
+ "stripAbbreviations" => 25,
+ "appendFootnotes" => 50,
+ );
+ $this->block_gamut += array(
+ "doFencedCodeBlocks" => 5,
+ "doTables" => 15,
+ "doDefLists" => 45,
+ );
+ $this->span_gamut += array(
+ "doFootnotes" => 5,
+ "doAbbreviations" => 70,
+ );
+
+ parent::__construct();
+ }
+
+
+ # Extra variables used during extra transformations.
+ public $footnotes = array();
+ public $footnotes_ordered = array();
+ public $footnotes_ref_count = array();
+ public $footnotes_numbers = array();
+ public $abbr_desciptions = array();
+ public $abbr_word_re = '';
+
+ # Give the current footnote number.
+ public $footnote_counter = 1;
+
+
+ function setup() {
+ #
+ # Setting up Extra-specific variables.
+ #
+ parent::setup();
+
+ $this->footnotes = array();
+ $this->footnotes_ordered = array();
+ $this->footnotes_ref_count = array();
+ $this->footnotes_numbers = array();
+ $this->abbr_desciptions = array();
+ $this->abbr_word_re = '';
+ $this->footnote_counter = 1;
+
+ foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
+ if ($this->abbr_word_re)
+ $this->abbr_word_re .= '|';
+ $this->abbr_word_re .= preg_quote($abbr_word);
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+ }
+ }
+
+ function teardown() {
+ #
+ # Clearing Extra-specific variables.
+ #
+ $this->footnotes = array();
+ $this->footnotes_ordered = array();
+ $this->footnotes_ref_count = array();
+ $this->footnotes_numbers = array();
+ $this->abbr_desciptions = array();
+ $this->abbr_word_re = '';
+
+ parent::teardown();
+ }
+
+
+ ### Extra Attribute Parser ###
+
+ # Expression to use to catch attributes (includes the braces)
+ public $id_class_attr_catch_re = '\{((?:[ ]*[#.][-_:a-zA-Z0-9]+){1,})[ ]*\}';
+ # Expression to use when parsing in a context when no capture is desired
+ public $id_class_attr_nocatch_re = '\{(?:[ ]*[#.][-_:a-zA-Z0-9]+){1,}[ ]*\}';
+
+ function doExtraAttributes($tag_name, $attr) {
+ #
+ # Parse attributes caught by the $this->id_class_attr_catch_re expression
+ # and return the HTML-formatted list of attributes.
+ #
+ # Currently supported attributes are .class and #id.
+ #
+ if (empty($attr)) return "";
+
+ # Split on components
+ preg_match_all('/[#.][-_:a-zA-Z0-9]+/', $attr, $matches);
+ $elements = $matches[0];
+
+ # handle classes and ids (only first id taken into account)
+ $classes = array();
+ $id = false;
+ foreach ($elements as $element) {
+ if ($element{0} == '.') {
+ $classes[] = substr($element, 1);
+ } else if ($element{0} == '#') {
+ if ($id === false) $id = substr($element, 1);
+ }
+ }
+
+ # compose attributes as string
+ $attr_str = "";
+ if (!empty($id)) {
+ $attr_str .= ' id="'.$id.'"';
+ }
+ if (!empty($classes)) {
+ $attr_str .= ' class="'.implode(" ", $classes).'"';
+ }
+ return $attr_str;
+ }
+
+
+ function stripLinkDefinitions($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: ^[id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ [ ]*
+ (?:
+ <(.+?)> # url = $2
+ |
+ (\S+?) # url = $3
+ )
+ [ ]*
+ \n? # maybe one newline
+ [ ]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ ["(]
+ (.*?) # title = $4
+ [")]
+ [ ]*
+ )? # title is optional
+ (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr
+ (?:\n+|\Z)
+ }xm',
+ array(&$this, '_stripLinkDefinitions_callback'),
+ $text);
+ return $text;
+ }
+ function _stripLinkDefinitions_callback($matches) {
+ $link_id = strtolower($matches[1]);
+ $url = $matches[2] == '' ? $matches[3] : $matches[2];
+ $this->urls[$link_id] = $url;
+ $this->titles[$link_id] =& $matches[4];
+ $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
+ return ''; # String that will replace the block
+ }
+
+
+ ### HTML Block Parser ###
+
+ # Tags that are always treated as block tags:
+ public $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption';
+
+ # Tags treated as block tags only if the opening tag is alone on its line:
+ public $context_block_tags_re = 'script|noscript|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
+
+ # Tags where markdown="1" default to span mode:
+ public $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
+
+ # Tags which must not have their contents modified, no matter where
+ # they appear:
+ public $clean_tags_re = 'script|math|svg';
+
+ # Tags that do not need to be closed.
+ public $auto_close_tags_re = 'hr|img|param|source|track';
+
+
+ function hashHTMLBlocks($text) {
+ #
+ # Hashify HTML Blocks and "clean tags".
+ #
+ # We only want to do this for block-level HTML tags, such as headers,
+ # lists, and tables. That's because we still want to wrap <p>s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded.
+ #
+ # This works by calling _HashHTMLBlocks_InMarkdown, which then calls
+ # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
+ # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
+ # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
+ # These two functions are calling each other. It's recursive!
+ #
+ if ($this->no_markup) return $text;
+
+ #
+ # Call the HTML-in-Markdown hasher.
+ #
+ list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
+ $enclosing_tag_re = '', $span = false)
+ {
+ #
+ # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
+ #
+ # * $indent is the number of space to be ignored when checking for code
+ # blocks. This is important because if we don't take the indent into
+ # account, something like this (which looks right) won't work as expected:
+ #
+ # <div>
+ # <div markdown="1">
+ # Hello World. <-- Is this a Markdown code block or text?
+ # </div> <-- Is this a Markdown code block or a real tag?
+ # <div>
+ #
+ # If you don't like this, just don't indent the tag on which
+ # you apply the markdown="1" attribute.
+ #
+ # * If $enclosing_tag_re is not empty, stops at the first unmatched closing
+ # tag with that name. Nested tags supported.
+ #
+ # * If $span is true, text inside must treated as span. So any double
+ # newline will be replaced by a single newline so that it does not create
+ # paragraphs.
+ #
+ # Returns an array of that form: ( processed text , remaining text )
+ #
+ if ($text === '') return array('', '');
+
+ # Regex to check for the presence of newlines around a block tag.
+ $newline_before_re = '/(?:^\n?|\n\n)*$/';
+ $newline_after_re =
+ '{
+ ^ # Start of text following the tag.
+ (?>[ ]*<!--.*?-->)? # Optional comment.
+ [ ]*\n # Must be followed by newline.
+ }xs';
+
+ # Regex to match any tag.
+ $block_tag_re =
+ '{
+ ( # $2: Capture whole tag.
+ </? # Any opening or closing tag.
+ (?> # Tag name.
+ '.$this->block_tags_re.' |
+ '.$this->context_block_tags_re.' |
+ '.$this->clean_tags_re.' |
+ (?!\s)'.$enclosing_tag_re.'
+ )
+ (?:
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
+ (?>
+ ".*?" | # Double quotes (can contain `>`)
+ \'.*?\' | # Single quotes (can contain `>`)
+ .+? # Anything but quotes and `>`.
+ )*?
+ )?
+ > # End of tag.
+ |
+ <!-- .*? --> # HTML Comment
+ |
+ <\?.*?\?> | <%.*?%> # Processing instruction
+ |
+ <!\[CDATA\[.*?\]\]> # CData Block
+ '. ( !$span ? ' # If not in span.
+ |
+ # Indented code block
+ (?: ^[ ]*\n | ^ | \n[ ]*\n )
+ [ ]{'.($indent+4).'}[^\n]* \n
+ (?>
+ (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n
+ )*
+ |
+ # Fenced code block marker
+ (?<= ^ | \n )
+ [ ]{0,'.($indent+3).'}(?:~{3,}|`{3,})
+ [ ]*
+ (?:
+ \.?[-_:a-zA-Z0-9]+ # standalone class name
+ |
+ '.$this->id_class_attr_nocatch_re.' # extra attributes
+ )?
+ [ ]*
+ (?= \n )
+ ' : '' ). ' # End (if not is span).
+ |
+ # Code span marker
+ # Note, this regex needs to go after backtick fenced
+ # code blocks but it should also be kept outside of the
+ # "if not in span" condition adding backticks to the parser
+ `+
+ )
+ }xs';
+
+
+ $depth = 0; # Current depth inside the tag tree.
+ $parsed = ""; # Parsed text that will be returned.
+
+ #
+ # Loop through every tag until we find the closing tag of the parent
+ # or loop until reaching the end of text if no parent tag specified.
+ #
+ do {
+ #
+ # Split the text using the first $tag_match pattern found.
+ # Text before pattern will be first in the array, text after
+ # pattern will be at the end, and between will be any catches made
+ # by the pattern.
+ #
+ $parts = preg_split($block_tag_re, $text, 2,
+ PREG_SPLIT_DELIM_CAPTURE);
+
+ # If in Markdown span mode, add a empty-string span-level hash
+ # after each newline to prevent triggering any block element.
+ if ($span) {
+ $void = $this->hashPart("", ':');
+ $newline = "$void\n";
+ $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
+ }
+
+ $parsed .= $parts[0]; # Text before current tag.
+
+ # If end of $text has been reached. Stop loop.
+ if (count($parts) < 3) {
+ $text = "";
+ break;
+ }
+
+ $tag = $parts[1]; # Tag to handle.
+ $text = $parts[2]; # Remaining text after current tag.
+ $tag_re = preg_quote($tag); # For use in a regular expression.
+
+ #
+ # Check for: Fenced code block marker.
+ # Note: need to recheck the whole tag to disambiguate backtick
+ # fences from code spans
+ #
+ if (preg_match('{^\n?([ ]{0,'.($indent+3).'})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+|'.$this->id_class_attr_nocatch_re.')?[ ]*\n?$}', $tag, $capture)) {
+ # Fenced code block marker: find matching end marker.
+ $fence_indent = strlen($capture[1]); # use captured indent in re
+ $fence_re = $capture[2]; # use captured fence in re
+ if (preg_match('{^(?>.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text,
+ $matches))
+ {
+ # End marker found: pass text unchanged until marker.
+ $parsed .= $tag . $matches[0];
+ $text = substr($text, strlen($matches[0]));
+ }
+ else {
+ # No end marker: just skip it.
+ $parsed .= $tag;
+ }
+ }
+ #
+ # Check for: Indented code block.
+ #
+ else if ($tag{0} == "\n" || $tag{0} == " ") {
+ # Indented code block: pass it unchanged, will be handled
+ # later.
+ $parsed .= $tag;
+ }
+ #
+ # Check for: Code span marker
+ # Note: need to check this after backtick fenced code blocks
+ #
+ else if ($tag{0} == "`") {
+ # Find corresponding end marker.
+ $tag_re = preg_quote($tag);
+ if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)'.$tag_re.'(?!`)}',
+ $text, $matches))
+ {
+ # End marker found: pass text unchanged until marker.
+ $parsed .= $tag . $matches[0];
+ $text = substr($text, strlen($matches[0]));
+ }
+ else {
+ # Unmatched marker: just skip it.
+ $parsed .= $tag;
+ }
+ }
+ #
+ # Check for: Opening Block level tag or
+ # Opening Context Block tag (like ins and del)
+ # used as a block tag (tag is alone on it's line).
+ #
+ else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) ||
+ ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) &&
+ preg_match($newline_before_re, $parsed) &&
+ preg_match($newline_after_re, $text) )
+ )
+ {
+ # Need to parse tag and following text using the HTML parser.
+ list($block_text, $text) =
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
+
+ # Make sure it stays outside of any paragraph by adding newlines.
+ $parsed .= "\n\n$block_text\n\n";
+ }
+ #
+ # Check for: Clean tag (like script, math)
+ # HTML Comments, processing instructions.
+ #
+ else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Need to parse tag and following text using the HTML parser.
+ # (don't check for markdown attribute)
+ list($block_text, $text) =
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
+
+ $parsed .= $block_text;
+ }
+ #
+ # Check for: Tag with same name as enclosing tag.
+ #
+ else if ($enclosing_tag_re !== '' &&
+ # Same name as enclosing tag.
+ preg_match('{^</?(?:'.$enclosing_tag_re.')\b}', $tag))
+ {
+ #
+ # Increase/decrease nested tag count.
+ #
+ if ($tag{1} == '/') $depth--;
+ else if ($tag{strlen($tag)-2} != '/') $depth++;
+
+ if ($depth < 0) {
+ #
+ # Going out of parent element. Clean up and break so we
+ # return to the calling function.
+ #
+ $text = $tag . $text;
+ break;
+ }
+
+ $parsed .= $tag;
+ }
+ else {
+ $parsed .= $tag;
+ }
+ } while ($depth >= 0);
+
+ return array($parsed, $text);
+ }
+ function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
+ #
+ # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
+ #
+ # * Calls $hash_method to convert any blocks.
+ # * Stops when the first opening tag closes.
+ # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
+ # (it is not inside clean tags)
+ #
+ # Returns an array of that form: ( processed text , remaining text )
+ #
+ if ($text === '') return array('', '');
+
+ # Regex to match `markdown` attribute inside of a tag.
+ $markdown_attr_re = '
+ {
+ \s* # Eat whitespace before the `markdown` attribute
+ markdown
+ \s*=\s*
+ (?>
+ (["\']) # $1: quote delimiter
+ (.*?) # $2: attribute value
+ \1 # matching delimiter
+ |
+ ([^\s>]*) # $3: unquoted attribute value
+ )
+ () # $4: make $3 always defined (avoid warnings)
+ }xs';
+
+ # Regex to match any tag.
+ $tag_re = '{
+ ( # $2: Capture whole tag.
+ </? # Any opening or closing tag.
+ [\w:$]+ # Tag name.
+ (?:
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
+ (?>
+ ".*?" | # Double quotes (can contain `>`)
+ \'.*?\' | # Single quotes (can contain `>`)
+ .+? # Anything but quotes and `>`.
+ )*?
+ )?
+ > # End of tag.
+ |
+ <!-- .*? --> # HTML Comment
+ |
+ <\?.*?\?> | <%.*?%> # Processing instruction
+ |
+ <!\[CDATA\[.*?\]\]> # CData Block
+ )
+ }xs';
+
+ $original_text = $text; # Save original text in case of faliure.
+
+ $depth = 0; # Current depth inside the tag tree.
+ $block_text = ""; # Temporary text holder for current text.
+ $parsed = ""; # Parsed text that will be returned.
+
+ #
+ # Get the name of the starting tag.
+ # (This pattern makes $base_tag_name_re safe without quoting.)
+ #
+ if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
+ $base_tag_name_re = $matches[1];
+
+ #
+ # Loop through every tag until we find the corresponding closing tag.
+ #
+ do {
+ #
+ # Split the text using the first $tag_match pattern found.
+ # Text before pattern will be first in the array, text after
+ # pattern will be at the end, and between will be any catches made
+ # by the pattern.
+ #
+ $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ if (count($parts) < 3) {
+ #
+ # End of $text reached with unbalenced tag(s).
+ # In that case, we return original text unchanged and pass the
+ # first character as filtered to prevent an infinite loop in the
+ # parent function.
+ #
+ return array($original_text{0}, substr($original_text, 1));
+ }
+
+ $block_text .= $parts[0]; # Text before current tag.
+ $tag = $parts[1]; # Tag to handle.
+ $text = $parts[2]; # Remaining text after current tag.
+
+ #
+ # Check for: Auto-close tag (like <hr/>)
+ # Comments and Processing Instructions.
+ #
+ if (preg_match('{^</?(?:'.$this->auto_close_tags_re.')\b}', $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Just add the tag to the block as if it was text.
+ $block_text .= $tag;
+ }
+ else {
+ #
+ # Increase/decrease nested tag count. Only do so if
+ # the tag's name match base tag's.
+ #
+ if (preg_match('{^</?'.$base_tag_name_re.'\b}', $tag)) {
+ if ($tag{1} == '/') $depth--;
+ else if ($tag{strlen($tag)-2} != '/') $depth++;
+ }
+
+ #
+ # Check for `markdown="1"` attribute and handle it.
+ #
+ if ($md_attr &&
+ preg_match($markdown_attr_re, $tag, $attr_m) &&
+ preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
+ {
+ # Remove `markdown` attribute from opening tag.
+ $tag = preg_replace($markdown_attr_re, '', $tag);
+
+ # Check if text inside this tag must be parsed in span mode.
+ $this->mode = $attr_m[2] . $attr_m[3];
+ $span_mode = $this->mode == 'span' || $this->mode != 'block' &&
+ preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag);
+
+ # Calculate indent before tag.
+ if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
+ $strlen = $this->utf8_strlen;
+ $indent = $strlen($matches[1], 'UTF-8');
+ } else {
+ $indent = 0;
+ }
+
+ # End preceding block with this tag.
+ $block_text .= $tag;
+ $parsed .= $this->$hash_method($block_text);
+
+ # Get enclosing tag name for the ParseMarkdown function.
+ # (This pattern makes $tag_name_re safe without quoting.)
+ preg_match('/^<([\w:$]*)\b/', $tag, $matches);
+ $tag_name_re = $matches[1];
+
+ # Parse the content using the HTML-in-Markdown parser.
+ list ($block_text, $text)
+ = $this->_hashHTMLBlocks_inMarkdown($text, $indent,
+ $tag_name_re, $span_mode);
+
+ # Outdent markdown text.
+ if ($indent > 0) {
+ $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
+ $block_text);
+ }
+
+ # Append tag content to parsed text.
+ if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
+ else $parsed .= "$block_text";
+
+ # Start over with a new block.
+ $block_text = "";
+ }
+ else $block_text .= $tag;
+ }
+
+ } while ($depth > 0);
+
+ #
+ # Hash last block text that wasn't processed inside the loop.
+ #
+ $parsed .= $this->$hash_method($block_text);
+
+ return array($parsed, $text);
+ }
+
+
+ function hashClean($text) {
+ #
+ # Called whenever a tag must be hashed when a function inserts a "clean" tag
+ # in $text, it passes through this function and is automaticaly escaped,
+ # blocking invalid nested overlap.
+ #
+ return $this->hashPart($text, 'C');
+ }
+
+
+ function doAnchors($text) {
+ #
+ # Turn Markdown link shortcuts into XHTML <a> tags.
+ #
+ if ($this->in_anchor) return $text;
+ $this->in_anchor = true;
+
+ #
+ # First, handle reference-style links: [link text] [id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ #
+ # Next, inline-style links: [link text](url "optional title")
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+ \( # literal paren
+ [ \n]*
+ (?:
+ <(.+?)> # href = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # href = $4
+ )
+ [ \n]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # Title = $7
+ \6 # matching quote
+ [ \n]* # ignore any spaces/tabs between closing quote and )
+ )? # title is optional
+ \)
+ (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes
+ )
+ }xs',
+ array(&$this, '_doAnchors_inline_callback'), $text);
+
+ #
+ # Last, handle reference-style shortcuts: [link text]
+ # These must come last in case you've also got [link text][1]
+ # or [link text](/foo)
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ([^\[\]]+) # link text = $2; can\'t contain [ or ]
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ $this->in_anchor = false;
+ return $text;
+ }
+ function _doAnchors_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $matches[2];
+ $link_id =& $matches[3];
+
+ if ($link_id == "") {
+ # for shortcut links like [this][] or [this].
+ $link_id = $link_text;
+ }
+
+ # lower-case and turn embedded newlines into spaces
+ $link_id = strtolower($link_id);
+ $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
+
+ if (isset($this->urls[$link_id])) {
+ $url = $this->urls[$link_id];
+ $url = $this->encodeAttribute($url);
+
+ $result = "<a href=\"$url\"";
+ if ( isset( $this->titles[$link_id] ) ) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+ if (isset($this->ref_attr[$link_id]))
+ $result .= $this->ref_attr[$link_id];
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text</a>";
+ $result = $this->hashPart($result);
+ }
+ else {
+ $result = $whole_match;
+ }
+ return $result;
+ }
+ function _doAnchors_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $this->runSpanGamut($matches[2]);
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+ $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]);
+
+
+ $url = $this->encodeAttribute($url);
+
+ $result = "<a href=\"$url\"";
+ if (isset($title)) {
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+ $result .= $attr;
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text</a>";
+
+ return $this->hashPart($result);
+ }
+
+
+ function doImages($text) {
+ #
+ # Turn Markdown image shortcuts into <img> tags.
+ #
+ #
+ # First, handle reference-style labeled images: ![alt text][id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+
+ )
+ }xs',
+ array(&$this, '_doImages_reference_callback'), $text);
+
+ #
+ # Next, handle inline images: ![alt text](url "optional title")
+ # Don't forget: encode * and _
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+ \s? # One optional whitespace character
+ \( # literal paren
+ [ \n]*
+ (?:
+ <(\S*)> # src url = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # src url = $4
+ )
+ [ \n]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # title = $7
+ \6 # matching quote
+ [ \n]*
+ )? # title is optional
+ \)
+ (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes
+ )
+ }xs',
+ array(&$this, '_doImages_inline_callback'), $text);
+
+ return $text;
+ }
+ function _doImages_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $link_id = strtolower($matches[3]);
+
+ if ($link_id == "") {
+ $link_id = strtolower($alt_text); # for shortcut links like ![this][].
+ }
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ if (isset($this->urls[$link_id])) {
+ $url = $this->encodeAttribute($this->urls[$link_id]);
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
+ if (isset($this->titles[$link_id])) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+ if (isset($this->ref_attr[$link_id]))
+ $result .= $this->ref_attr[$link_id];
+ $result .= $this->empty_element_suffix;
+ $result = $this->hashPart($result);
+ }
+ else {
+ # If there's no such link ID, leave intact:
+ $result = $whole_match;
+ }
+
+ return $result;
+ }
+ function _doImages_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+ $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]);
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ $url = $this->encodeAttribute($url);
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
+ if (isset($title)) {
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\""; # $title already quoted
+ }
+ $result .= $attr;
+ $result .= $this->empty_element_suffix;
+
+ return $this->hashPart($result);
+ }
+
+
+ function doHeaders($text) {
+ #
+ # Redefined to add id and class attribute support.
+ #
+ # Setext-style headers:
+ # Header 1 {#header1}
+ # ========
+ #
+ # Header 2 {#header2 .class1 .class2}
+ # --------
+ #
+ $text = preg_replace_callback(
+ '{
+ (^.+?) # $1: Header text
+ (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes
+ [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer
+ }mx',
+ array(&$this, '_doHeaders_callback_setext'), $text);
+
+ # atx-style headers:
+ # # Header 1 {#header1}
+ # ## Header 2 {#header2}
+ # ## Header 2 with closing hashes ## {#header3.class1.class2}
+ # ...
+ # ###### Header 6 {.class2}
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #\'s (not counted)
+ (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes
+ [ ]*
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_callback_setext($matches) {
+ if ($matches[3] == '-' && preg_match('{^- }', $matches[1]))
+ return $matches[0];
+ $level = $matches[3]{0} == '=' ? 1 : 2;
+ $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]);
+ $block = "<h$level$attr>".$this->runSpanGamut($matches[1])."</h$level>";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]);
+ $block = "<h$level$attr>".$this->runSpanGamut($matches[2])."</h$level>";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+
+
+ function doTables($text) {
+ #
+ # Form HTML tables.
+ #
+ $less_than_tab = $this->tab_width - 1;
+ #
+ # Find tables with leading pipe.
+ #
+ # | Header 1 | Header 2
+ # | -------- | --------
+ # | Cell 1 | Cell 2
+ # | Cell 3 | Cell 4
+ #
+ $text = preg_replace_callback('
+ {
+ ^ # Start of a line
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ [|] # Optional leading pipe (present)
+ (.+) \n # $1: Header row (at least one pipe)
+
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline
+
+ ( # $3: Cells
+ (?>
+ [ ]* # Allowed whitespace.
+ [|] .* \n # Row content.
+ )*
+ )
+ (?=\n|\Z) # Stop at final double newline.
+ }xm',
+ array(&$this, '_doTable_leadingPipe_callback'), $text);
+
+ #
+ # Find tables without leading pipe.
+ #
+ # Header 1 | Header 2
+ # -------- | --------
+ # Cell 1 | Cell 2
+ # Cell 3 | Cell 4
+ #
+ $text = preg_replace_callback('
+ {
+ ^ # Start of a line
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ (\S.*[|].*) \n # $1: Header row (at least one pipe)
+
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline
+
+ ( # $3: Cells
+ (?>
+ .* [|] .* \n # Row content
+ )*
+ )
+ (?=\n|\Z) # Stop at final double newline.
+ }xm',
+ array(&$this, '_DoTable_callback'), $text);
+
+ return $text;
+ }
+ function _doTable_leadingPipe_callback($matches) {
+ $head = $matches[1];
+ $underline = $matches[2];
+ $content = $matches[3];
+
+ # Remove leading pipe for each row.
+ $content = preg_replace('/^ *[|]/m', '', $content);
+
+ return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
+ }
+ function _doTable_callback($matches) {
+ $head = $matches[1];
+ $underline = $matches[2];
+ $content = $matches[3];
+
+ # Remove any tailing pipes for each line.
+ $head = preg_replace('/[|] *$/m', '', $head);
+ $underline = preg_replace('/[|] *$/m', '', $underline);
+ $content = preg_replace('/[|] *$/m', '', $content);
+
+ # Reading alignement from header underline.
+ $separators = preg_split('/ *[|] */', $underline);
+ foreach ($separators as $n => $s) {
+ if (preg_match('/^ *-+: *$/', $s)) $attr[$n] = ' align="right"';
+ else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"';
+ else if (preg_match('/^ *:-+ *$/', $s)) $attr[$n] = ' align="left"';
+ else $attr[$n] = '';
+ }
+
+ # Parsing span elements, including code spans, character escapes,
+ # and inline HTML tags, so that pipes inside those gets ignored.
+ $head = $this->parseSpan($head);
+ $headers = preg_split('/ *[|] */', $head);
+ $col_count = count($headers);
+ $attr = array_pad($attr, $col_count, '');
+
+ # Write column headers.
+ $text = "<table>\n";
+ $text .= "<thead>\n";
+ $text .= "<tr>\n";
+ foreach ($headers as $n => $header)
+ $text .= " <th$attr[$n]>".$this->runSpanGamut(trim($header))."</th>\n";
+ $text .= "</tr>\n";
+ $text .= "</thead>\n";
+
+ # Split content by row.
+ $rows = explode("\n", trim($content, "\n"));
+
+ $text .= "<tbody>\n";
+ foreach ($rows as $row) {
+ # Parsing span elements, including code spans, character escapes,
+ # and inline HTML tags, so that pipes inside those gets ignored.
+ $row = $this->parseSpan($row);
+
+ # Split row by cell.
+ $row_cells = preg_split('/ *[|] */', $row, $col_count);
+ $row_cells = array_pad($row_cells, $col_count, '');
+
+ $text .= "<tr>\n";
+ foreach ($row_cells as $n => $cell)
+ $text .= " <td$attr[$n]>".$this->runSpanGamut(trim($cell))."</td>\n";
+ $text .= "</tr>\n";
+ }
+ $text .= "</tbody>\n";
+ $text .= "</table>";
+
+ return $this->hashBlock($text) . "\n";
+ }
+
+
+ function doDefLists($text) {
+ #
+ # Form HTML definition lists.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Re-usable pattern to match any entire dl list:
+ $whole_list_re = '(?>
+ ( # $1 = whole list
+ ( # $2
+ [ ]{0,'.$less_than_tab.'}
+ ((?>.*\S.*\n)+) # $3 = defined term
+ \n?
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ (?s:.+?)
+ ( # $4
+ \z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another term
+ [ ]{0,'.$less_than_tab.'}
+ (?: \S.*\n )+? # defined term
+ \n?
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ (?! # Negative lookahead for another definition
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ )
+ )
+ )'; // mx
+
+ $text = preg_replace_callback('{
+ (?>\A\n?|(?<=\n\n))
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doDefLists_callback'), $text);
+
+ return $text;
+ }
+ function _doDefLists_callback($matches) {
+ # Re-usable patterns to match list item bullets and number markers:
+ $list = $matches[1];
+
+ # Turn double returns into triple returns, so that we can make a
+ # paragraph for the last item in a list, if necessary:
+ $result = trim($this->processDefListItems($list));
+ $result = "<dl>\n" . $result . "\n</dl>";
+ return $this->hashBlock($result) . "\n\n";
+ }
+
+
+ function processDefListItems($list_str) {
+ #
+ # Process the contents of a single definition list, splitting it
+ # into individual term and definition list items.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # trim trailing blank lines:
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+ # Process definition terms.
+ $list_str = preg_replace_callback('{
+ (?>\A\n?|\n\n+) # leading line
+ ( # definition terms = $1
+ [ ]{0,'.$less_than_tab.'} # leading whitespace
+ (?!\:[ ]|[ ]) # negative lookahead for a definition
+ # mark (colon) or more whitespace.
+ (?> \S.* \n)+? # actual term (not whitespace).
+ )
+ (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed
+ # with a definition mark.
+ }xm',
+ array(&$this, '_processDefListItems_callback_dt'), $list_str);
+
+ # Process actual definitions.
+ $list_str = preg_replace_callback('{
+ \n(\n+)? # leading line = $1
+ ( # marker space = $2
+ [ ]{0,'.$less_than_tab.'} # whitespace before colon
+ \:[ ]+ # definition mark (colon)
+ )
+ ((?s:.+?)) # definition text = $3
+ (?= \n+ # stop at next definition mark,
+ (?: # next term or end of text
+ [ ]{0,'.$less_than_tab.'} \:[ ] |
+ <dt> | \z
+ )
+ )
+ }xm',
+ array(&$this, '_processDefListItems_callback_dd'), $list_str);
+
+ return $list_str;
+ }
+ function _processDefListItems_callback_dt($matches) {
+ $terms = explode("\n", trim($matches[1]));
+ $text = '';
+ foreach ($terms as $term) {
+ $term = $this->runSpanGamut(trim($term));
+ $text .= "\n<dt>" . $term . "</dt>";
+ }
+ return $text . "\n";
+ }
+ function _processDefListItems_callback_dd($matches) {
+ $leading_line = $matches[1];
+ $marker_space = $matches[2];
+ $def = $matches[3];
+
+ if ($leading_line || preg_match('/\n{2,}/', $def)) {
+ # Replace marker with the appropriate whitespace indentation
+ $def = str_repeat(' ', strlen($marker_space)) . $def;
+ $def = $this->runBlockGamut($this->outdent($def . "\n\n"));
+ $def = "\n". $def ."\n";
+ }
+ else {
+ $def = rtrim($def);
+ $def = $this->runSpanGamut($this->outdent($def));
+ }
+
+ return "\n<dd>" . $def . "</dd>\n";
+ }
+
+
+ function doFencedCodeBlocks($text) {
+ #
+ # Adding the fenced code block syntax to regular Markdown:
+ #
+ # ~~~
+ # Code block
+ # ~~~
+ #
+ $less_than_tab = $this->tab_width;
+
+ $text = preg_replace_callback('{
+ (?:\n|\A)
+ # 1: Opening marker
+ (
+ (?:~{3,}|`{3,}) # 3 or more tildes/backticks.
+ )
+ [ ]*
+ (?:
+ \.?([-_:a-zA-Z0-9]+) # 2: standalone class name
+ |
+ '.$this->id_class_attr_catch_re.' # 3: Extra attributes
+ )?
+ [ ]* \n # Whitespace and newline following marker.
+
+ # 4: Content
+ (
+ (?>
+ (?!\1 [ ]* \n) # Not a closing marker.
+ .*\n+
+ )+
+ )
+
+ # Closing marker.
+ \1 [ ]* (?= \n )
+ }xm',
+ array(&$this, '_doFencedCodeBlocks_callback'), $text);
+
+ return $text;
+ }
+ function _doFencedCodeBlocks_callback($matches) {
+ $classname =& $matches[2];
+ $attrs =& $matches[3];
+ $codeblock = $matches[4];
+ $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
+ $codeblock = preg_replace_callback('/^\n+/',
+ array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock);
+
+ if ($classname != "") {
+ if ($classname{0} == '.')
+ $classname = substr($classname, 1);
+ $attr_str = ' class="'.$this->code_class_prefix.$classname.'"';
+ } else {
+ $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs);
+ }
+ $pre_attr_str = $this->code_attr_on_pre ? $attr_str : '';
+ $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
+ $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
+
+ return "\n\n".$this->hashBlock($codeblock)."\n\n";
+ }
+ function _doFencedCodeBlocks_newlines($matches) {
+ return str_repeat("<br$this->empty_element_suffix",
+ strlen($matches[0]));
+ }
+
+
+ #
+ # Redefining emphasis markers so that emphasis by underscore does not
+ # work in the middle of a word.
+ #
+ public $em_relist = array(
+ '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?=\S|$)(?![\.,:;]\s)',
+ '*' => '(?<=\S|^)(?<!\*)\*(?!\*)',
+ '_' => '(?<=\S|^)(?<!_)_(?![a-zA-Z0-9_])',
+ );
+ public $strong_relist = array(
+ '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?=\S|$)(?![\.,:;]\s)',
+ '**' => '(?<=\S|^)(?<!\*)\*\*(?!\*)',
+ '__' => '(?<=\S|^)(?<!_)__(?![a-zA-Z0-9_])',
+ );
+ public $em_strong_relist = array(
+ '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?=\S|$)(?![\.,:;]\s)',
+ '***' => '(?<=\S|^)(?<!\*)\*\*\*(?!\*)',
+ '___' => '(?<=\S|^)(?<!_)___(?![a-zA-Z0-9_])',
+ );
+
+
+ function formParagraphs($text) {
+ #
+ # Params:
+ # $text - string to process with html <p> tags
+ #
+ # Strip leading and trailing lines:
+ $text = preg_replace('/\A\n+|\n+\z/', '', $text);
+
+ $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+ #
+ # Wrap <p> tags and unhashify HTML blocks
+ #
+ foreach ($grafs as $key => $value) {
+ $value = trim($this->runSpanGamut($value));
+
+ # Check if this should be enclosed in a paragraph.
+ # Clean tag hashes & block tag hashes are left alone.
+ $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
+
+ if ($is_p) {
+ $value = "<p>$value</p>";
+ }
+ $grafs[$key] = $value;
+ }
+
+ # Join grafs in one text, then unhash HTML tags.
+ $text = implode("\n\n", $grafs);
+
+ # Finish by removing any tag hashes still present in $text.
+ $text = $this->unhash($text);
+
+ return $text;
+ }
+
+
+ ### Footnotes
+
+ function stripFootnotes($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: [^id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ ( # text = $2 (no blank lines allowed)
+ (?:
+ .+ # actual text
+ |
+ \n # newlines but
+ (?!\[\^.+?\]:\s)# negative lookahead for footnote marker.
+ (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
+ # by non-indented content
+ )*
+ )
+ }xm',
+ array(&$this, '_stripFootnotes_callback'),
+ $text);
+ return $text;
+ }
+ function _stripFootnotes_callback($matches) {
+ $note_id = $this->fn_id_prefix . $matches[1];
+ $this->footnotes[$note_id] = $this->outdent($matches[2]);
+ return ''; # String that will replace the block
+ }
+
+
+ function doFootnotes($text) {
+ #
+ # Replace footnote references in $text [^id] with a special text-token
+ # which will be replaced by the actual footnote marker in appendFootnotes.
+ #
+ if (!$this->in_anchor) {
+ $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
+ }
+ return $text;
+ }
+
+
+ function appendFootnotes($text) {
+ #
+ # Append footnote list to text.
+ #
+ $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
+ array(&$this, '_appendFootnotes_callback'), $text);
+
+ if (!empty($this->footnotes_ordered)) {
+ $text .= "\n\n";
+ $text .= "<div class=\"footnotes\">\n";
+ $text .= "<hr". $this->empty_element_suffix ."\n";
+ $text .= "<ol>\n\n";
+
+ $attr = "";
+ if ($this->fn_backlink_class != "") {
+ $class = $this->fn_backlink_class;
+ $class = $this->encodeAttribute($class);
+ $attr .= " class=\"$class\"";
+ }
+ if ($this->fn_backlink_title != "") {
+ $title = $this->fn_backlink_title;
+ $title = $this->encodeAttribute($title);
+ $attr .= " title=\"$title\"";
+ }
+ $num = 0;
+
+ while (!empty($this->footnotes_ordered)) {
+ $footnote = reset($this->footnotes_ordered);
+ $note_id = key($this->footnotes_ordered);
+ unset($this->footnotes_ordered[$note_id]);
+ $ref_count = $this->footnotes_ref_count[$note_id];
+ unset($this->footnotes_ref_count[$note_id]);
+ unset($this->footnotes[$note_id]);
+
+ $footnote .= "\n"; # Need to append newline before parsing.
+ $footnote = $this->runBlockGamut("$footnote\n");
+ $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
+ array(&$this, '_appendFootnotes_callback'), $footnote);
+
+ $attr = str_replace("%%", ++$num, $attr);
+ $note_id = $this->encodeAttribute($note_id);
+
+ # Prepare backlink, multiple backlinks if multiple references
+ $backlink = "<a href=\"#fnref:$note_id\"$attr>&#8617;</a>";
+ for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) {
+ $backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>&#8617;</a>";
+ }
+ # Add backlink to last paragraph; create new paragraph if needed.
+ if (preg_match('{</p>$}', $footnote)) {
+ $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
+ } else {
+ $footnote .= "\n\n<p>$backlink</p>";
+ }
+
+ $text .= "<li id=\"fn:$note_id\">\n";
+ $text .= $footnote . "\n";
+ $text .= "</li>\n\n";
+ }
+
+ $text .= "</ol>\n";
+ $text .= "</div>";
+ }
+ return $text;
+ }
+ function _appendFootnotes_callback($matches) {
+ $node_id = $this->fn_id_prefix . $matches[1];
+
+ # Create footnote marker only if it has a corresponding footnote *and*
+ # the footnote hasn't been used by another marker.
+ if (isset($this->footnotes[$node_id])) {
+ $num =& $this->footnotes_numbers[$node_id];
+ if (!isset($num)) {
+ # Transfer footnote content to the ordered list and give it its
+ # number
+ $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
+ $this->footnotes_ref_count[$node_id] = 1;
+ $num = $this->footnote_counter++;
+ $ref_count_mark = '';
+ } else {
+ $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
+ }
+
+ $attr = "";
+ if ($this->fn_link_class != "") {
+ $class = $this->fn_link_class;
+ $class = $this->encodeAttribute($class);
+ $attr .= " class=\"$class\"";
+ }
+ if ($this->fn_link_title != "") {
+ $title = $this->fn_link_title;
+ $title = $this->encodeAttribute($title);
+ $attr .= " title=\"$title\"";
+ }
+
+ $attr = str_replace("%%", $num, $attr);
+ $node_id = $this->encodeAttribute($node_id);
+
+ return
+ "<sup id=\"fnref$ref_count_mark:$node_id\">".
+ "<a href=\"#fn:$node_id\"$attr>$num</a>".
+ "</sup>";
+ }
+
+ return "[^".$matches[1]."]";
+ }
+
+
+ ### Abbreviations ###
+
+ function stripAbbreviations($text) {
+ #
+ # Strips abbreviations from text, stores titles in hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: [id]*: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1
+ (.*) # text = $2 (no blank lines allowed)
+ }xm',
+ array(&$this, '_stripAbbreviations_callback'),
+ $text);
+ return $text;
+ }
+ function _stripAbbreviations_callback($matches) {
+ $abbr_word = $matches[1];
+ $abbr_desc = $matches[2];
+ if ($this->abbr_word_re)
+ $this->abbr_word_re .= '|';
+ $this->abbr_word_re .= preg_quote($abbr_word);
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+ return ''; # String that will replace the block
+ }
+
+
+ function doAbbreviations($text) {
+ #
+ # Find defined abbreviations in text and wrap them in <abbr> elements.
+ #
+ if ($this->abbr_word_re) {
+ // cannot use the /x modifier because abbr_word_re may
+ // contain significant spaces:
+ $text = preg_replace_callback('{'.
+ '(?<![\w\x1A])'.
+ '(?:'.$this->abbr_word_re.')'.
+ '(?![\w\x1A])'.
+ '}',
+ array(&$this, '_doAbbreviations_callback'), $text);
+ }
+ return $text;
+ }
+ function _doAbbreviations_callback($matches) {
+ $abbr = $matches[0];
+ if (isset($this->abbr_desciptions[$abbr])) {
+ $desc = $this->abbr_desciptions[$abbr];
+ if (empty($desc)) {
+ return $this->hashPart("<abbr>$abbr</abbr>");
+ } else {
+ $desc = $this->encodeAttribute($desc);
+ return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
+ }
+ } else {
+ return $matches[0];
+ }
+ }
+
+}
+
+
+/*
+
+PHP Markdown Extra
+==================
+
+Description
+-----------
+
+This is a PHP port of the original Markdown formatter written in Perl
+by John Gruber. This special "Extra" version of PHP Markdown features
+further enhancements to the syntax for making additional constructs
+such as tables and definition list.
+
+Markdown is a text-to-HTML filter; it translates an easy-to-read /
+easy-to-write structured text format into HTML. Markdown's text format
+is mostly similar to that of plain text email, and supports features such
+as headers, *emphasis*, code blocks, blockquotes, and links.
+
+Markdown's syntax is designed not as a generic markup language, but
+specifically to serve as a front-end to (X)HTML. You can use span-level
+HTML tags anywhere in a Markdown document, and you can use block level
+HTML tags (like <div> and <table> as well).
+
+For more information about Markdown's syntax, see:
+
+<http://daringfireball.net/projects/markdown/>
+
+
+Bugs
+----
+
+To file bug reports please send email to:
+
+<michel.fortin@michelf.ca>
+
+Please include with your report: (1) the example input; (2) the output you
+expected; (3) the output Markdown actually produced.
+
+
+Version History
+---------------
+
+See the readme file for detailed release notes for this version.
+
+
+Copyright and License
+---------------------
+
+PHP Markdown & Extra
+Copyright (c) 2004-2013 Michel Fortin
+<http://michelf.ca/>
+All rights reserved.
+
+Based on Markdown
+Copyright (c) 2003-2006 John Gruber
+<http://daringfireball.net/>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
+
+*/
+?>
diff --git a/plugins/jetpack/_inc/lib/markdown/gfm.php b/plugins/jetpack/_inc/lib/markdown/gfm.php
new file mode 100644
index 00000000..081e1a11
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/markdown/gfm.php
@@ -0,0 +1,400 @@
+<?php
+/**
+ * GitHub-Flavoured Markdown. Inspired by Evan's plugin, but modified.
+ *
+ * @author Evan Solomon
+ * @author Matt Wiebe <wiebe@automattic.com>
+ * @link https://github.com/evansolomon/wp-github-flavored-markdown-comments
+ *
+ * Add a few extras from GitHub's Markdown implementation. Must be used in a WordPress environment.
+ */
+
+class WPCom_GHF_Markdown_Parser extends MarkdownExtra_Parser {
+
+ /**
+ * Hooray somewhat arbitrary numbers that are fearful of 1.0.x.
+ */
+ const WPCOM_GHF_MARDOWN_VERSION = '0.9.0';
+
+ /**
+ * Use a [code] shortcode when encountering a fenced code block
+ * @var boolean
+ */
+ public $use_code_shortcode = true;
+
+ /**
+ * Preserve shortcodes, untouched by Markdown.
+ * This requires use within a WordPress installation.
+ * @var boolean
+ */
+ public $preserve_shortcodes = true;
+
+ /**
+ * Preserve the legacy $latex your-latex-code-here$ style
+ * LaTeX markup
+ */
+ public $preserve_latex = true;
+
+ /**
+ * Preserve single-line <code> blocks.
+ * @var boolean
+ */
+ public $preserve_inline_code_blocks = true;
+
+ /**
+ * Strip paragraphs from the output. This is the right default for WordPress,
+ * which generally wants to create its own paragraphs with `wpautop`
+ * @var boolean
+ */
+ public $strip_paras = true;
+
+ // Will run through sprintf - you can supply your own syntax if you want
+ public $shortcode_start = '[code lang=%s]';
+ public $shortcode_end = '[/code]';
+
+ // Stores shortcodes we remove and then replace
+ protected $preserve_text_hash = array();
+
+ /**
+ * Set environment defaults based on presence of key functions/classes.
+ */
+ public function __construct() {
+ $this->use_code_shortcode = class_exists( 'SyntaxHighlighter' );
+ /**
+ * Allow processing shortcode contents.
+ *
+ * @module markdown
+ *
+ * @since 4.4.0
+ *
+ * @param boolean $preserve_shortcodes Defaults to $this->preserve_shortcodes.
+ */
+ $this->preserve_shortcodes = apply_filters( 'jetpack_markdown_preserve_shortcodes', $this->preserve_shortcodes ) && function_exists( 'get_shortcode_regex' );
+ $this->preserve_latex = function_exists( 'latex_markup' );
+ $this->strip_paras = function_exists( 'wpautop' );
+
+ parent::__construct();
+ }
+
+ /**
+ * Overload to specify heading styles only if the hash has space(s) after it. This is actually in keeping with
+ * the documentation and eases the semantic overload of the hash character.
+ * #Will Not Produce a Heading 1
+ * # This Will Produce a Heading 1
+ *
+ * @param string $text Markdown text
+ * @return string HTML-transformed text
+ */
+ public function transform( $text ) {
+ // Preserve anything inside a single-line <code> element
+ if ( $this->preserve_inline_code_blocks ) {
+ $text = $this->single_line_code_preserve( $text );
+ }
+ // Remove all shortcodes so their interiors are left intact
+ if ( $this->preserve_shortcodes ) {
+ $text = $this->shortcode_preserve( $text );
+ }
+ // Remove legacy LaTeX so it's left intact
+ if ( $this->preserve_latex ) {
+ $text = $this->latex_preserve( $text );
+ }
+
+ // escape line-beginning # chars that do not have a space after them.
+ $text = preg_replace_callback( '|^#{1,6}( )?|um', array( $this, '_doEscapeForHashWithoutSpacing' ), $text );
+
+ /**
+ * Allow third-party plugins to define custom patterns that won't be processed by Markdown.
+ *
+ * @module markdown
+ *
+ * @since 3.9.2
+ *
+ * @param array $custom_patterns Array of custom patterns to be ignored by Markdown.
+ */
+ $custom_patterns = apply_filters( 'jetpack_markdown_preserve_pattern', array() );
+ if ( is_array( $custom_patterns ) && ! empty( $custom_patterns ) ) {
+ foreach ( $custom_patterns as $pattern ) {
+ $text = preg_replace_callback( $pattern, array( $this, '_doRemoveText'), $text );
+ }
+ }
+
+ // run through core Markdown
+ $text = parent::transform( $text );
+
+ // Occasionally Markdown Extra chokes on a para structure, producing odd paragraphs.
+ $text = str_replace( "<p>&lt;</p>\n\n<p>p>", '<p>', $text );
+
+ // put start-of-line # chars back in place
+ $text = $this->restore_leading_hash( $text );
+
+ // Strip paras if set
+ if ( $this->strip_paras ) {
+ $text = $this->unp( $text );
+ }
+
+ // Restore preserved things like shortcodes/LaTeX
+ $text = $this->do_restore( $text );
+
+ return $text;
+ }
+
+ /**
+ * Prevents blocks like <code>__this__</code> from turning into <code><strong>this</strong></code>
+ * @param string $text Text that may need preserving
+ * @return string Text that was preserved if needed
+ */
+ public function single_line_code_preserve( $text ) {
+ return preg_replace_callback( '|<code\b[^>]*>(.*?)</code>|', array( $this, 'do_single_line_code_preserve' ), $text );
+ }
+
+ /**
+ * Regex callback for inline code presevation
+ * @param array $matches Regex matches
+ * @return string Hashed content for later restoration
+ */
+ public function do_single_line_code_preserve( $matches ) {
+ return '<code>' . $this->hash_block( $matches[1] ) . '</code>';
+ }
+
+ /**
+ * Preserve code block contents by HTML encoding them. Useful before getting to KSES stripping.
+ * @param string $text Markdown/HTML content
+ * @return string Markdown/HTML content with escaped code blocks
+ */
+ public function codeblock_preserve( $text ) {
+ return preg_replace_callback( "/^([`~]{3})([^`\n]+)?\n([^`~]+)(\\1)/m", array( $this, 'do_codeblock_preserve' ), $text );
+ }
+
+ /**
+ * Regex callback for code block preservation.
+ * @param array $matches Regex matches
+ * @return string Codeblock with escaped interior
+ */
+ public function do_codeblock_preserve( $matches ) {
+ $block = stripslashes( $matches[3] );
+ $block = esc_html( $block );
+ $block = str_replace( '\\', '\\\\', $block );
+ $open = $matches[1] . $matches[2] . "\n";
+ return $open . $block . $matches[4];
+ }
+
+ /**
+ * Restore previously preserved (i.e. escaped) code block contents.
+ * @param string $text Markdown/HTML content with escaped code blocks
+ * @return string Markdown/HTML content
+ */
+ public function codeblock_restore( $text ) {
+ return preg_replace_callback( "/^([`~]{3})([^`\n]+)?\n([^`~]+)(\\1)/m", array( $this, 'do_codeblock_restore' ), $text );
+ }
+
+ /**
+ * Regex callback for code block restoration (unescaping).
+ * @param array $matches Regex matches
+ * @return string Codeblock with unescaped interior
+ */
+ public function do_codeblock_restore( $matches ) {
+ $block = html_entity_decode( $matches[3], ENT_QUOTES );
+ $open = $matches[1] . $matches[2] . "\n";
+ return $open . $block . $matches[4];
+ }
+
+ /**
+ * Called to preserve legacy LaTeX like $latex some-latex-text $
+ * @param string $text Text in which to preserve LaTeX
+ * @return string Text with LaTeX replaced by a hash that will be restored later
+ */
+ protected function latex_preserve( $text ) {
+ // regex from latex_remove()
+ $regex = '%
+ \$latex(?:=\s*|\s+)
+ ((?:
+ [^$]+ # Not a dollar
+ |
+ (?<=(?<!\\\\)\\\\)\$ # Dollar preceded by exactly one slash
+ )+)
+ (?<!\\\\)\$ # Dollar preceded by zero slashes
+ %ix';
+ $text = preg_replace_callback( $regex, array( $this, '_doRemoveText'), $text );
+ return $text;
+ }
+
+ /**
+ * Called to preserve WP shortcodes from being formatted by Markdown in any way.
+ * @param string $text Text in which to preserve shortcodes
+ * @return string Text with shortcodes replaced by a hash that will be restored later
+ */
+ protected function shortcode_preserve( $text ) {
+ $text = preg_replace_callback( $this->get_shortcode_regex(), array( $this, '_doRemoveText' ), $text );
+ return $text;
+ }
+
+ /**
+ * Restores any text preserved by $this->hash_block()
+ * @param string $text Text that may have hashed preservation placeholders
+ * @return string Text with hashed preseravtion placeholders replaced by original text
+ */
+ protected function do_restore( $text ) {
+ // Reverse hashes to ensure nested blocks are restored.
+ $hashes = array_reverse( $this->preserve_text_hash, true );
+ foreach( $hashes as $hash => $value ) {
+ $placeholder = $this->hash_maker( $hash );
+ $text = str_replace( $placeholder, $value, $text );
+ }
+ // reset the hash
+ $this->preserve_text_hash = array();
+ return $text;
+ }
+
+ /**
+ * Regex callback for text preservation
+ * @param array $m Regex $matches array
+ * @return string A placeholder that will later be replaced by the original text
+ */
+ protected function _doRemoveText( $m ) {
+ return $this->hash_block( $m[0] );
+ }
+
+ /**
+ * Call this to store a text block for later restoration.
+ * @param string $text Text to preserve for later
+ * @return string Placeholder that will be swapped out later for the original text
+ */
+ protected function hash_block( $text ) {
+ $hash = md5( $text );
+ $this->preserve_text_hash[ $hash ] = $text;
+ $placeholder = $this->hash_maker( $hash );
+ return $placeholder;
+ }
+
+ /**
+ * Less glamorous than the Keymaker
+ * @param string $hash An md5 hash
+ * @return string A placeholder hash
+ */
+ protected function hash_maker( $hash ) {
+ return 'MARKDOWN_HASH' . $hash . 'MARKDOWN_HASH';
+ }
+
+ /**
+ * Remove bare <p> elements. <p>s with attributes will be preserved.
+ * @param string $text HTML content
+ * @return string <p>-less content
+ */
+ public function unp( $text ) {
+ return preg_replace( "#<p>(.*?)</p>(\n|$)#ums", '$1$2', $text );
+ }
+
+ /**
+ * A regex of all shortcodes currently registered by the current
+ * WordPress installation
+ * @uses get_shortcode_regex()
+ * @return string A regex for grabbing shortcodes.
+ */
+ protected function get_shortcode_regex() {
+ $pattern = get_shortcode_regex();
+
+ // don't match markdown link anchors that could be mistaken for shortcodes.
+ $pattern .= '(?!\()';
+
+ return "/$pattern/s";
+ }
+
+ /**
+ * Since we escape unspaced #Headings, put things back later.
+ * @param string $text text with a leading escaped hash
+ * @return string text with leading hashes unescaped
+ */
+ protected function restore_leading_hash( $text ) {
+ return preg_replace( "/^(<p>)?(&#35;|\\\\#)/um", "$1#", $text );
+ }
+
+ /**
+ * Overload to support ```-fenced code blocks for pre-Markdown Extra 1.2.8
+ * https://help.github.com/articles/github-flavored-markdown#fenced-code-blocks
+ */
+ public function doFencedCodeBlocks( $text ) {
+ // If we're at least at 1.2.8, native fenced code blocks are in.
+ // Below is just copied from it in case we somehow got loaded on
+ // top of someone else's Markdown Extra
+ if ( version_compare( MARKDOWNEXTRA_VERSION, '1.2.8', '>=' ) )
+ return parent::doFencedCodeBlocks( $text );
+
+ #
+ # Adding the fenced code block syntax to regular Markdown:
+ #
+ # ~~~
+ # Code block
+ # ~~~
+ #
+ $less_than_tab = $this->tab_width;
+
+ $text = preg_replace_callback('{
+ (?:\n|\A)
+ # 1: Opening marker
+ (
+ (?:~{3,}|`{3,}) # 3 or more tildes/backticks.
+ )
+ [ ]*
+ (?:
+ \.?([-_:a-zA-Z0-9]+) # 2: standalone class name
+ |
+ '.$this->id_class_attr_catch_re.' # 3: Extra attributes
+ )?
+ [ ]* \n # Whitespace and newline following marker.
+
+ # 4: Content
+ (
+ (?>
+ (?!\1 [ ]* \n) # Not a closing marker.
+ .*\n+
+ )+
+ )
+
+ # Closing marker.
+ \1 [ ]* (?= \n )
+ }xm',
+ array($this, '_doFencedCodeBlocks_callback'), $text);
+
+ return $text;
+ }
+
+ /**
+ * Callback for pre-processing start of line hashes to slyly escape headings that don't
+ * have a leading space
+ * @param array $m preg_match matches
+ * @return string possibly escaped start of line hash
+ */
+ public function _doEscapeForHashWithoutSpacing( $m ) {
+ if ( ! isset( $m[1] ) )
+ $m[0] = '\\' . $m[0];
+ return $m[0];
+ }
+
+ /**
+ * Overload to support Viper's [code] shortcode. Because awesome.
+ */
+ public function _doFencedCodeBlocks_callback( $matches ) {
+ // in case we have some escaped leading hashes right at the start of the block
+ $matches[4] = $this->restore_leading_hash( $matches[4] );
+ // just MarkdownExtra_Parser if we're not going ultra-deluxe
+ if ( ! $this->use_code_shortcode ) {
+ return parent::_doFencedCodeBlocks_callback( $matches );
+ }
+
+ // default to a "text" class if one wasn't passed. Helps with encoding issues later.
+ if ( empty( $matches[2] ) ) {
+ $matches[2] = 'text';
+ }
+
+ $classname =& $matches[2];
+ $codeblock = preg_replace_callback('/^\n+/', array( $this, '_doFencedCodeBlocks_newlines' ), $matches[4] );
+
+ if ( $classname{0} == '.' )
+ $classname = substr( $classname, 1 );
+
+ $codeblock = esc_html( $codeblock );
+ $codeblock = sprintf( $this->shortcode_start, $classname ) . "\n{$codeblock}" . $this->shortcode_end;
+ return "\n\n" . $this->hashBlock( $codeblock ). "\n\n";
+ }
+
+}
diff --git a/plugins/jetpack/_inc/lib/plugins.php b/plugins/jetpack/_inc/lib/plugins.php
new file mode 100644
index 00000000..9c8e3bc4
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/plugins.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Plugins Library
+ *
+ * Helper functions for installing and activating plugins.
+ *
+ * Used by the REST API
+ *
+ * @autounit api plugins
+ */
+
+include_once( 'class.jetpack-automatic-install-skin.php' );
+
+class Jetpack_Plugins {
+
+ /**
+ * Install and activate a plugin.
+ *
+ * @since 5.8.0
+ *
+ * @param string $slug Plugin slug.
+ *
+ * @return bool|WP_Error True if installation succeeded, error object otherwise.
+ */
+ public static function install_and_activate_plugin( $slug ) {
+ $plugin_id = self::get_plugin_id_by_slug( $slug );
+
+ if ( ! $plugin_id ) {
+ $installed = self::install_plugin( $slug );
+ if ( is_wp_error( $installed ) ) {
+ return $installed;
+ }
+ $plugin_id = self::get_plugin_id_by_slug( $slug );
+ } else if ( is_plugin_active( $plugin_id ) ) {
+ return true; // Already installed and active
+ }
+
+ if ( ! current_user_can( 'activate_plugins' ) ) {
+ return new WP_Error( 'not_allowed', __( 'You are not allowed to activate plugins on this site.', 'jetpack' ) );
+ }
+
+ $activated = activate_plugin( $plugin_id );
+ if ( is_wp_error( $activated ) ) {
+ return $activated;
+ }
+
+ return true;
+ }
+
+ /**
+ * Install a plugin.
+ *
+ * @since 5.8.0
+ *
+ * @param string $slug Plugin slug.
+ *
+ * @return bool|WP_Error True if installation succeeded, error object otherwise.
+ */
+ public static function install_plugin( $slug ) {
+ if ( is_multisite() && ! current_user_can( 'manage_network' ) ) {
+ return new WP_Error( 'not_allowed', __( 'You are not allowed to install plugins on this site.', 'jetpack' ) );
+ }
+
+ $skin = new Jetpack_Automatic_Install_Skin();
+ $upgrader = new Plugin_Upgrader( $skin );
+ $zip_url = self::generate_wordpress_org_plugin_download_link( $slug );
+
+ $result = $upgrader->install( $zip_url );
+
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+
+ $plugin = Jetpack_Plugins::get_plugin_id_by_slug( $slug );
+ $error_code = 'install_error';
+ if ( ! $plugin ) {
+ $error = __( 'There was an error installing your plugin', 'jetpack' );
+ }
+
+ if ( ! $result ) {
+ $error_code = $upgrader->skin->get_main_error_code();
+ $message = $upgrader->skin->get_main_error_message();
+ $error = $message ? $message : __( 'An unknown error occurred during installation', 'jetpack' );
+ }
+
+ if ( ! empty( $error ) ) {
+ if ( 'download_failed' === $error_code ) {
+ // For backwards compatibility: versions prior to 3.9 would return no_package instead of download_failed.
+ $error_code = 'no_package';
+ }
+
+ return new WP_Error( $error_code, $error, 400 );
+ }
+
+ return (array) $upgrader->skin->get_upgrade_messages();
+ }
+
+ protected static function generate_wordpress_org_plugin_download_link( $plugin_slug ) {
+ return "https://downloads.wordpress.org/plugin/$plugin_slug.latest-stable.zip";
+ }
+
+ public static function get_plugin_id_by_slug( $slug ) {
+ // Check if get_plugins() function exists. This is required on the front end of the
+ // site, since it is in a file that is normally only loaded in the admin.
+ if ( ! function_exists( 'get_plugins' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+
+ /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
+ $plugins = apply_filters( 'all_plugins', get_plugins() );
+ if ( ! is_array( $plugins ) ) {
+ return false;
+ }
+ foreach ( $plugins as $plugin_file => $plugin_data ) {
+ if ( self::get_slug_from_file_path( $plugin_file ) === $slug ) {
+ return $plugin_file;
+ }
+ }
+
+ return false;
+ }
+
+ protected static function get_slug_from_file_path( $plugin_file ) {
+ // Similar to get_plugin_slug() method.
+ $slug = dirname( $plugin_file );
+ if ( '.' === $slug ) {
+ $slug = preg_replace( "/(.+)\.php$/", "$1", $plugin_file );
+ }
+
+ return $slug;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/tonesque.php b/plugins/jetpack/_inc/lib/tonesque.php
new file mode 100644
index 00000000..17158e3d
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/tonesque.php
@@ -0,0 +1,237 @@
+<?php
+/*
+Plugin Name: Tonesque
+Plugin URI: http://automattic.com/
+Description: Grab an average color representation from an image.
+Version: 1.0
+Author: Automattic, Matias Ventura
+Author URI: http://automattic.com/
+License: GNU General Public License v2 or later
+License URI: http://www.gnu.org/licenses/gpl-2.0.html
+*/
+
+class Tonesque {
+
+ private $image_url = '';
+ private $image_obj = NULL;
+ private $color = '';
+
+ function __construct( $image_url ) {
+ if ( ! class_exists( 'Jetpack_Color' ) ) {
+ jetpack_require_lib( 'class.color' );
+ }
+
+ $this->image_url = esc_url_raw( $image_url );
+ $this->image_url = trim( $this->image_url );
+ /**
+ * Allows any image URL to be passed in for $this->image_url.
+ *
+ * @module theme-tools
+ *
+ * @since 2.5.0
+ *
+ * @param string $image_url The URL to any image
+ */
+ $this->image_url = apply_filters( 'tonesque_image_url', $this->image_url );
+
+ $this->image_obj = self::imagecreatefromurl( $this->image_url );
+ }
+
+ public static function imagecreatefromurl( $image_url ) {
+ $data = null;
+
+ // If it's a URL:
+ if ( preg_match( '#^https?://#i', $image_url ) ) {
+ // If it's a url pointing to a local media library url:
+ $content_url = content_url();
+ $_image_url = set_url_scheme( $image_url );
+ if ( wp_startswith( $_image_url, $content_url ) ) {
+ $_image_path = str_replace( $content_url, WP_CONTENT_DIR, $_image_url );
+ if ( file_exists( $_image_path ) ) {
+ $filetype = wp_check_filetype( $_image_path );
+ $ext = $filetype['ext'];
+ $type = $filetype['type'];
+
+ if ( wp_startswith( $type, 'image/' ) ) {
+ $data = file_get_contents( $_image_path );
+ }
+ }
+ }
+
+ if ( empty( $data ) ) {
+ $response = wp_remote_get( $image_url );
+ if ( is_wp_error( $response ) ) {
+ return false;
+ }
+ $data = wp_remote_retrieve_body( $response );
+ }
+ }
+
+ // If it's a local path in our WordPress install:
+ if ( file_exists( $image_url ) ) {
+ $filetype = wp_check_filetype( $image_url );
+ $ext = $filetype['ext'];
+ $type = $filetype['type'];
+
+ if ( wp_startswith( $type, 'image/' ) ) {
+ $data = file_get_contents( $image_url );
+ }
+ }
+
+ // Now turn it into an image and return it.
+ return imagecreatefromstring( $data );
+ }
+
+ /**
+ *
+ * Construct object from image.
+ *
+ * @param optional $type (hex, rgb, hsv)
+ * @return color as a string formatted as $type
+ *
+ */
+ function color( $type = 'hex' ) {
+ // Bail if there is no image to work with
+ if ( ! $this->image_obj )
+ return false;
+
+ // Finds dominant color
+ $color = self::grab_color();
+ // Passes value to Color class
+ $color = self::get_color( $color, $type );
+ return $color;
+ }
+
+ /**
+ *
+ * Grabs the color index for each of five sample points of the image
+ *
+ * @param $image
+ * @param $type can be 'index' or 'hex'
+ * @return array() with color indices
+ *
+ */
+ function grab_points( $type = 'index' ) {
+ $img = $this->image_obj;
+ if ( ! $img )
+ return false;
+
+ $height = imagesy( $img );
+ $width = imagesx( $img );
+
+ // Sample five points in the image
+ // Based on rule of thirds and center
+ $topy = round( $height / 3 );
+ $bottomy = round( ( $height / 3 ) * 2 );
+ $leftx = round( $width / 3 );
+ $rightx = round( ( $width / 3 ) * 2 );
+ $centery = round( $height / 2 );
+ $centerx = round( $width / 2 );
+
+ // Cast those colors into an array
+ $points = array(
+ imagecolorat( $img, $leftx, $topy ),
+ imagecolorat( $img, $rightx, $topy ),
+ imagecolorat( $img, $leftx, $bottomy ),
+ imagecolorat( $img, $rightx, $bottomy ),
+ imagecolorat( $img, $centerx, $centery ),
+ );
+
+ if ( 'hex' == $type ) {
+ foreach ( $points as $i => $p ) {
+ $c = imagecolorsforindex( $img, $p );
+ $points[ $i ] = self::get_color( array(
+ 'r' => $c['red'],
+ 'g' => $c['green'],
+ 'b' => $c['blue'],
+ ), 'hex' );
+ }
+ }
+
+ return $points;
+ }
+
+ /**
+ *
+ * Finds the average color of the image based on five sample points
+ *
+ * @param $image
+ * @return array() with rgb color
+ *
+ */
+ function grab_color() {
+ $img = $this->image_obj;
+ if ( ! $img )
+ return false;
+
+ $rgb = self::grab_points();
+
+ // Process the color points
+ // Find the average representation
+ foreach ( $rgb as $color ) {
+ $index = imagecolorsforindex( $img, $color );
+ $r[] = $index['red'];
+ $g[] = $index['green'];
+ $b[] = $index['blue'];
+
+ $red = round( array_sum( $r ) / 5 );
+ $green = round( array_sum( $g ) / 5 );
+ $blue = round( array_sum( $b ) / 5 );
+ }
+
+ // The average color of the image as rgb array
+ $color = array(
+ 'r' => $red,
+ 'g' => $green,
+ 'b' => $blue,
+ );
+
+ return $color;
+ }
+
+ /**
+ *
+ * Get a Color object using /lib class.color
+ * Convert to appropriate type
+ *
+ * @return string
+ *
+ */
+ function get_color( $color, $type ) {
+ $c = new Jetpack_Color( $color, 'rgb' );
+ $this->color = $c;
+
+ switch ( $type ) {
+ case 'rgb' :
+ $color = implode( $c->toRgbInt(), ',' );
+ break;
+ case 'hex' :
+ $color = $c->toHex();
+ break;
+ case 'hsv' :
+ $color = implode( $c->toHsvInt(), ',' );
+ break;
+ default:
+ return $color = $c->toHex();
+ }
+
+ return $color;
+ }
+
+ /**
+ *
+ * Checks contrast against main color
+ * Gives either black or white for using with opacity
+ *
+ * @return string
+ *
+ */
+ function contrast() {
+ if ( ! $this->color )
+ return false;
+
+ $c = $this->color->getMaxContrastColor();
+ return implode( $c->toRgbInt(), ',' );
+ }
+
+};
diff --git a/plugins/jetpack/_inc/lib/tracks/class.tracks-client.php b/plugins/jetpack/_inc/lib/tracks/class.tracks-client.php
new file mode 100644
index 00000000..b83c94f1
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/tracks/class.tracks-client.php
@@ -0,0 +1,191 @@
+<?php
+
+/**
+ * Jetpack_Tracks_Client
+ * @autounit nosara tracks-client
+ *
+ * Send Tracks events on behalf of a user
+ *
+ * Example Usage:
+```php
+ require( dirname(__FILE__).'path/to/tracks/class.tracks-client' );
+
+ $result = Jetpack_Tracks_Client::record_event( array(
+ '_en' => $event_name, // required
+ '_ui' => $user_id, // required unless _ul is provided
+ '_ul' => $user_login, // required unless _ui is provided
+
+ // Optional, but recommended
+ '_ts' => $ts_in_ms, // Default: now
+ '_via_ip' => $client_ip, // we use it for geo, etc.
+
+ // Possibly useful to set some context for the event
+ '_via_ua' => $client_user_agent,
+ '_via_url' => $client_url,
+ '_via_ref' => $client_referrer,
+
+ // For user-targeted tests
+ 'abtest_name' => $abtest_name,
+ 'abtest_variation' => $abtest_variation,
+
+ // Your application-specific properties
+ 'custom_property' => $some_value,
+ ) );
+
+ if ( is_wp_error( $result ) ) {
+ // Handle the error in your app
+ }
+```
+ */
+
+require_once( dirname(__FILE__).'/class.tracks-client.php' );
+
+class Jetpack_Tracks_Client {
+ const PIXEL = 'https://pixel.wp.com/t.gif';
+ const BROWSER_TYPE = 'php-agent';
+ const USER_AGENT_SLUG = 'tracks-client';
+ const VERSION = '0.3';
+
+ /**
+ * record_event
+ * @param mixed $event Event object to send to Tracks. An array will be cast to object. Required.
+ * Properties are included directly in the pixel query string after light validation.
+ * @return mixed True on success, WP_Error on failure
+ */
+ static function record_event( $event ) {
+ if ( ! Jetpack::jetpack_tos_agreed() || ! empty( $_COOKIE['tk_opt-out'] ) ) {
+ return false;
+ }
+
+ if ( ! $event instanceof Jetpack_Tracks_Event ) {
+ $event = new Jetpack_Tracks_Event( $event );
+ }
+ if ( is_wp_error( $event ) ) {
+ return $event;
+ }
+
+ $pixel = $event->build_pixel_url( $event );
+
+ if ( ! $pixel ) {
+ return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
+ }
+
+ return self::record_pixel( $pixel );
+ }
+
+ /**
+ * Synchronously request the pixel
+ */
+ static function record_pixel( $pixel ) {
+ // Add the Request Timestamp and URL terminator just before the HTTP request.
+ $pixel .= '&_rt=' . self::build_timestamp() . '&_=_';
+
+ $response = wp_remote_get( $pixel, array(
+ 'blocking' => true, // The default, but being explicit here :)
+ 'timeout' => 1,
+ 'redirection' => 2,
+ 'httpversion' => '1.1',
+ 'user-agent' => self::get_user_agent(),
+ ) );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $code = isset( $response['response']['code'] ) ? $response['response']['code'] : 0;
+
+ if ( $code !== 200 ) {
+ return new WP_Error( 'request_failed', 'Tracks pixel request failed', $code );
+ }
+
+ return true;
+ }
+
+ static function get_user_agent() {
+ return Jetpack_Tracks_Client::USER_AGENT_SLUG . '-v' . Jetpack_Tracks_Client::VERSION;
+ }
+
+ /**
+ * Build an event and return its tracking URL
+ * @deprecated Call the `build_pixel_url` method on a Jetpack_Tracks_Event object instead.
+ * @param array $event Event keys and values
+ * @return string URL of a tracking pixel
+ */
+ static function build_pixel_url( $event ) {
+ $_event = new Jetpack_Tracks_Event( $event );
+ return $_event->build_pixel_url();
+ }
+
+ /**
+ * Validate input for a tracks event.
+ * @deprecated Instantiate a Jetpack_Tracks_Event object instead
+ * @param array $event Event keys and values
+ * @return mixed Validated keys and values or WP_Error on failure
+ */
+ private static function validate_and_sanitize( $event ) {
+ $_event = new Jetpack_Tracks_Event( $event );
+ if ( is_wp_error( $_event ) ) {
+ return $_event;
+ }
+ return get_object_vars( $_event );
+ }
+
+ // Milliseconds since 1970-01-01
+ static function build_timestamp() {
+ $ts = round( microtime( true ) * 1000 );
+ return number_format( $ts, 0, '', '' );
+ }
+
+ /**
+ * Grabs the user's anon id from cookies, or generates and sets a new one
+ *
+ * @return string An anon id for the user
+ */
+ static function get_anon_id() {
+ static $anon_id = null;
+
+ if ( ! isset( $anon_id ) ) {
+
+ // Did the browser send us a cookie?
+ if ( isset( $_COOKIE[ 'tk_ai' ] ) && preg_match( '#^[A-Za-z0-9+/=]{24}$#', $_COOKIE[ 'tk_ai' ] ) ) {
+ $anon_id = $_COOKIE[ 'tk_ai' ];
+ } else {
+
+ $binary = '';
+
+ // Generate a new anonId and try to save it in the browser's cookies
+ // Note that base64-encoding an 18 character string generates a 24-character anon id
+ for ( $i = 0; $i < 18; ++$i ) {
+ $binary .= chr( mt_rand( 0, 255 ) );
+ }
+
+ $anon_id = 'jetpack:' . base64_encode( $binary );
+
+ if ( ! headers_sent()
+ && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST )
+ && ! ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
+ ) {
+ setcookie( 'tk_ai', $anon_id );
+ }
+ }
+ }
+
+ return $anon_id;
+ }
+
+ /**
+ * Gets the WordPress.com user's Tracks identity, if connected.
+ *
+ * @return array|bool
+ */
+ static function get_connected_user_tracks_identity() {
+ if ( ! $user_data = Jetpack::get_connected_user_data() ) {
+ return false;
+ }
+
+ return array(
+ 'userid' => $user_data['ID'],
+ 'username' => $user_data['login'],
+ );
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/tracks/class.tracks-event.php b/plugins/jetpack/_inc/lib/tracks/class.tracks-event.php
new file mode 100644
index 00000000..fb86e0ba
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/tracks/class.tracks-event.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * @autounit nosara tracks-client
+ *
+ * Example Usage:
+```php
+ require_once( dirname(__FILE__) . 'path/to/tracks/class.tracks-event' );
+
+ $event = new Jetpack_Tracks_Event( array(
+ '_en' => $event_name, // required
+ '_ui' => $user_id, // required unless _ul is provided
+ '_ul' => $user_login, // required unless _ui is provided
+
+ // Optional, but recommended
+ '_via_ip' => $client_ip, // for geo, etc.
+
+ // Possibly useful to set some context for the event
+ '_via_ua' => $client_user_agent,
+ '_via_url' => $client_url,
+ '_via_ref' => $client_referrer,
+
+ // For user-targeted tests
+ 'abtest_name' => $abtest_name,
+ 'abtest_variation' => $abtest_variation,
+
+ // Your application-specific properties
+ 'custom_property' => $some_value,
+ ) );
+
+ if ( is_wp_error( $event->error ) ) {
+ // Handle the error in your app
+ }
+
+ $bump_and_redirect_pixel = $event->build_signed_pixel_url();
+```
+ */
+
+require_once( dirname(__FILE__) . '/class.tracks-client.php' );
+
+class Jetpack_Tracks_Event {
+ const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/';
+ const PROP_NAME_REGEX = '/^[a-z_][a-z0-9_]*$/';
+ public $error;
+
+ function __construct( $event ) {
+ $_event = self::validate_and_sanitize( $event );
+ if ( is_wp_error( $_event ) ) {
+ $this->error = $_event;
+ return;
+ }
+
+ foreach( $_event as $key => $value ) {
+ $this->{$key} = $value;
+ }
+ }
+
+ function record() {
+ return Jetpack_Tracks_Client::record_event( $this );
+ }
+
+ /**
+ * Annotate the event with all relevant info.
+ * @param mixed $event Object or (flat) array
+ * @return mixed The transformed event array or WP_Error on failure.
+ */
+ static function validate_and_sanitize( $event ) {
+ $event = (object) $event;
+
+ // Required
+ if ( ! $event->_en ) {
+ return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 );
+ }
+
+ // delete non-routable addresses otherwise geoip will discard the record entirely
+ if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) {
+ unset($event->_via_ip);
+ }
+
+ $validated = array(
+ 'browser_type' => Jetpack_Tracks_Client::BROWSER_TYPE,
+ '_aua' => Jetpack_Tracks_Client::get_user_agent(),
+ );
+
+ $_event = (object) array_merge( (array) $event, $validated );
+
+ // If you want to blacklist property names, do it here.
+
+ // Make sure we have an event timestamp.
+ if ( ! isset( $_event->_ts ) ) {
+ $_event->_ts = Jetpack_Tracks_Client::build_timestamp();
+ }
+
+ return $_event;
+ }
+
+ /**
+ * Build a pixel URL that will send a Tracks event when fired.
+ * On error, returns an empty string ('').
+ *
+ * @return string A pixel URL or empty string ('') if there were invalid args.
+ */
+ function build_pixel_url() {
+ if ( $this->error ) {
+ return '';
+ }
+
+ $args = get_object_vars( $this );
+
+ // Request Timestamp and URL Terminator must be added just before the HTTP request or not at all.
+ unset( $args['_rt'] );
+ unset( $args['_'] );
+
+ $validated = self::validate_and_sanitize( $args );
+
+ if ( is_wp_error( $validated ) )
+ return '';
+
+ return Jetpack_Tracks_Client::PIXEL . '?' . http_build_query( $validated );
+ }
+
+ static function event_name_is_valid( $name ) {
+ return preg_match( Jetpack_Tracks_Event::EVENT_NAME_REGEX, $name );
+ }
+
+ static function prop_name_is_valid( $name ) {
+ return preg_match( Jetpack_Tracks_Event::PROP_NAME_REGEX, $name );
+ }
+
+ static function scrutinize_event_names( $event ) {
+ if ( ! Jetpack_Tracks_Event::event_name_is_valid( $event->_en ) ) {
+ return;
+ }
+
+ $whitelisted_key_names = array(
+ 'anonId',
+ 'Browser_Type',
+ );
+
+ foreach ( array_keys( (array) $event ) as $key ) {
+ if ( in_array( $key, $whitelisted_key_names ) ) {
+ continue;
+ }
+ if ( ! Jetpack_Tracks_Event::prop_name_is_valid( $key ) ) {
+ return;
+ }
+ }
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/tracks/client.php b/plugins/jetpack/_inc/lib/tracks/client.php
new file mode 100644
index 00000000..bd92d272
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/tracks/client.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * PHP Tracks Client
+ * @autounit nosara tracks-client
+ * Example Usage:
+ *
+```php
+ include( plugin_dir_path( __FILE__ ) . 'lib/tracks/client.php');
+ $result = jetpack_tracks_record_event( $user, $event_name, $properties );
+
+ if ( is_wp_error( $result ) ) {
+ // Handle the error in your app
+ }
+```
+ */
+
+// Load the client classes
+require_once( dirname(__FILE__) . '/class.tracks-event.php' );
+require_once( dirname(__FILE__) . '/class.tracks-client.php' );
+
+// Now, let's export a sprinkling of syntactic sugar!
+
+/**
+ * Procedurally (vs. Object-oriented), track an event object (or flat array)
+ * NOTE: Use this only when the simpler jetpack_tracks_record_event() function won't work for you.
+ * @param \Jetpack_Tracks_Event $event The event object.
+ * @return \Jetpack_Tracks_Event|\WP_Error
+ */
+function jetpack_tracks_record_event_raw( $event ) {
+ return Jetpack_Tracks_Client::record_event( $event );
+}
+
+/**
+ * Procedurally build a Tracks Event Object.
+ * NOTE: Use this only when the simpler jetpack_tracks_record_event() function won't work for you.
+ * @param $identity WP_user object
+ * @param string $event_name The name of the event
+ * @param array $properties Custom properties to send with the event
+ * @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred
+ * @return \Jetpack_Tracks_Event|\WP_Error
+ */
+function jetpack_tracks_build_event_obj( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
+
+ $identity = jetpack_tracks_get_identity( $user->ID );
+
+ $properties['user_lang'] = $user->get( 'WPLANG' );
+
+ $blog_details = array(
+ 'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' )
+ );
+
+ $timestamp = ( $event_timestamp_millis !== false ) ? $event_timestamp_millis : round( microtime( true ) * 1000 );
+ $timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' );
+
+ return new Jetpack_Tracks_Event( array_merge( $blog_details, (array) $properties, $identity, array(
+ '_en' => $event_name,
+ '_ts' => $timestamp_string
+ ) ) );
+}
+
+/*
+ * Get the identity to send to tracks.
+ *
+ * @param int $user_id The user id of the local user
+ * @return array $identity
+ */
+function jetpack_tracks_get_identity( $user_id ) {
+
+ // Meta is set, and user is still connected. Use WPCOM ID
+ $wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true );
+ if ( $wpcom_id && Jetpack::is_user_connected( $user_id ) ) {
+ return array(
+ '_ut' => 'wpcom:user_id',
+ '_ui' => $wpcom_id
+ );
+ }
+
+ // User is connected, but no meta is set yet. Use WPCOM ID and set meta.
+ if ( Jetpack::is_user_connected( $user_id ) ) {
+ $wpcom_user_data = Jetpack::get_connected_user_data( $user_id );
+ update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'] );
+
+ return array(
+ '_ut' => 'wpcom:user_id',
+ '_ui' => $wpcom_user_data['ID']
+ );
+ }
+
+ // User isn't linked at all. Fall back to anonymous ID.
+ $anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true );
+ if ( ! $anon_id ) {
+ $anon_id = Jetpack_Tracks_Client::get_anon_id();
+ add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false );
+ }
+
+ if ( ! isset( $_COOKIE[ 'tk_ai' ] ) && ! headers_sent() ) {
+ setcookie( 'tk_ai', $anon_id );
+ }
+
+ return array(
+ '_ut' => 'anon',
+ '_ui' => $anon_id
+ );
+
+}
+
+/**
+ * Record an event in Tracks - this is the preferred way to record events from PHP.
+ *
+ * @param mixed $identity username, user_id, or WP_user object
+ * @param string $event_name The name of the event
+ * @param array $properties Custom properties to send with the event
+ * @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred
+ * @return bool true for success | \WP_Error if the event pixel could not be fired
+ */
+function jetpack_tracks_record_event( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
+
+ // We don't want to track user events during unit tests/CI runs.
+ if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) {
+ return false;
+ }
+
+ $event_obj = jetpack_tracks_build_event_obj( $user, $event_name, $properties, $event_timestamp_millis );
+
+ if ( is_wp_error( $event_obj->error ) ) {
+ return $event_obj->error;
+ }
+
+ return $event_obj->record();
+}
diff --git a/plugins/jetpack/_inc/lib/tracks/tracks-ajax.js b/plugins/jetpack/_inc/lib/tracks/tracks-ajax.js
new file mode 100644
index 00000000..911275bd
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/tracks/tracks-ajax.js
@@ -0,0 +1,62 @@
+/* global jpTracksAJAX, jQuery */
+( function( $, jpTracksAJAX ) {
+ window.jpTracksAJAX = window.jpTracksAJAX || {};
+ const debugSet = localStorage.getItem( 'debug' ) === 'dops:analytics';
+
+ window.jpTracksAJAX.record_ajax_event = function( eventName, eventType, eventProp ) {
+ var data = {
+ tracksNonce: jpTracksAJAX.jpTracksAJAX_nonce,
+ action: 'jetpack_tracks',
+ tracksEventType: eventType,
+ tracksEventName: eventName,
+ tracksEventProp: eventProp || false,
+ };
+
+ return $.ajax( {
+ type: 'POST',
+ url: jpTracksAJAX.ajaxurl,
+ data: data,
+ success: function( response ) {
+ if ( debugSet ) {
+ // eslint-disable-next-line
+ console.log( 'AJAX tracks event recorded: ', data, response );
+ }
+ },
+ } );
+ };
+
+ $( document ).ready( function() {
+ $( 'body' ).on( 'click', '.jptracks a, a.jptracks', function( event ) {
+ var $this = $( event.target );
+ // We know that the jptracks element is either this, or its ancestor
+ var $jptracks = $this.closest( '.jptracks' );
+ // We need an event name at least
+ var eventName = $jptracks.attr( 'data-jptracks-name' );
+ if ( undefined === eventName ) {
+ return;
+ }
+
+ var eventProp = $jptracks.attr( 'data-jptracks-prop' ) || false;
+
+ var url = $this.attr( 'href' );
+ var target = $this.get( 0 ).target;
+ if ( url && target && '_self' !== target ) {
+ var newTabWindow = window.open( '', target );
+ newTabWindow.opener = null;
+ }
+
+ event.preventDefault();
+
+ window.jpTracksAJAX.record_ajax_event( eventName, 'click', eventProp ).always( function() {
+ // Continue on to whatever url they were trying to get to.
+ if ( url && ! $this.hasClass( 'thickbox' ) ) {
+ if ( newTabWindow ) {
+ newTabWindow.location = url;
+ return;
+ }
+ window.location = url;
+ }
+ } );
+ } );
+ } );
+} )( jQuery, jpTracksAJAX );
diff --git a/plugins/jetpack/_inc/lib/tracks/tracks-callables.js b/plugins/jetpack/_inc/lib/tracks/tracks-callables.js
new file mode 100644
index 00000000..d4e53af6
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/tracks/tracks-callables.js
@@ -0,0 +1,76 @@
+/**
+ * This was abstracted from wp-calypso's analytics lib: https://github.com/Automattic/wp-calypso/blob/master/client/lib/analytics/README.md
+ * Some stuff was removed like GA tracking and other things not necessary for Jetpack tracking.
+ *
+ * This library should only be used and loaded if the Jetpack site is connected.
+ */
+
+// Load tracking scripts
+window._tkq = window._tkq || [];
+
+function buildQuerystring( group, name ) {
+ var uriComponent = '';
+
+ if ( 'object' === typeof group ) {
+ for ( var key in group ) {
+ uriComponent += '&x_' + encodeURIComponent( key ) + '=' + encodeURIComponent( group[ key ] );
+ }
+ } else {
+ uriComponent = '&x_' + encodeURIComponent( group ) + '=' + encodeURIComponent( name );
+ }
+
+ return uriComponent;
+}
+
+var analytics = {
+ initialize: function( userId, username ) {
+ analytics.setUser( userId, username );
+ analytics.identifyUser();
+ },
+
+ mc: {
+ bumpStat: function( group, name ) {
+ var uriComponent = buildQuerystring( group, name ); // prints debug info
+ new Image().src =
+ document.location.protocol +
+ '//pixel.wp.com/g.gif?v=wpcom-no-pv' +
+ uriComponent +
+ '&t=' +
+ Math.random();
+ },
+ },
+
+ tracks: {
+ recordEvent: function( eventName, eventProperties ) {
+ eventProperties = eventProperties || {};
+
+ if ( eventName.indexOf( 'jetpack_' ) !== 0 ) {
+ debug( '- Event name must be prefixed by "jetpack_"' );
+ return;
+ }
+
+ window._tkq.push( [ 'recordEvent', eventName, eventProperties ] );
+ },
+
+ recordPageView: function( urlPath ) {
+ analytics.tracks.recordEvent( 'jetpack_page_view', {
+ path: urlPath,
+ } );
+ },
+ },
+
+ setUser: function( userId, username ) {
+ _user = { ID: userId, username: username };
+ },
+
+ identifyUser: function() {
+ // Don't identify the user if we don't have one
+ if ( _user ) {
+ window._tkq.push( [ 'identifyUser', _user.ID, _user.username ] );
+ }
+ },
+
+ clearedIdentity: function() {
+ window._tkq.push( [ 'clearIdentity' ] );
+ },
+};
diff --git a/plugins/jetpack/_inc/lib/widgets.php b/plugins/jetpack/_inc/lib/widgets.php
new file mode 100644
index 00000000..3f072b75
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/widgets.php
@@ -0,0 +1,776 @@
+<?php
+/**
+ * Widgets and Sidebars Library
+ *
+ * Helper functions for manipulating widgets on a per-blog basis.
+ * Only helpful on `wp_loaded` or later (currently requires widgets to be registered and the theme context to already be loaded).
+ *
+ * Used by the REST API
+ *
+ * @autounit api widgets
+ */
+
+class Jetpack_Widgets {
+
+ /**
+ * Returns the `sidebars_widgets` option with the `array_version` element removed.
+ *
+ * @return array The current value of sidebars_widgets
+ */
+ public static function get_sidebars_widgets() {
+ $sidebars = get_option( 'sidebars_widgets', array() );
+ if ( isset( $sidebars['array_version'] ) ) {
+ unset( $sidebars['array_version'] );
+ }
+ return $sidebars;
+ }
+
+ /**
+ * Format widget data for output and for use by other widget functions.
+ *
+ * The output looks like:
+ *
+ * array(
+ * 'id' => 'text-3',
+ * 'sidebar' => 'sidebar-1',
+ * 'position' => '0',
+ * 'settings' => array(
+ * 'title' => 'hello world'
+ * )
+ * )
+ *
+ *
+ * @param string|integer $position The position of the widget in its sidebar.
+ * @param string $widget_id The widget's id (eg: 'text-3').
+ * @param string $sidebar The widget's sidebar id (eg: 'sidebar-1').
+ * @param array (Optional) $settings The settings for the widget.
+ *
+ * @return array A normalized array representing this widget.
+ */
+ public static function format_widget( $position, $widget_id, $sidebar, $settings = null ) {
+ if ( ! $settings ) {
+ $all_settings = get_option( self::get_widget_option_name( $widget_id ) );
+ $instance = self::get_widget_instance_key( $widget_id );
+ $settings = $all_settings[$instance];
+ }
+ $widget = array();
+
+ $widget['id'] = $widget_id;
+ $widget['id_base'] = self::get_widget_id_base( $widget_id );
+ $widget['settings'] = $settings;
+ $widget['sidebar'] = $sidebar;
+ $widget['position'] = $position;
+
+ return $widget;
+ }
+
+ /**
+ * Return a widget's id_base from its id.
+ *
+ * @param string $widget_id The id of a widget. (eg: 'text-3')
+ *
+ * @return string The id_base of a widget (eg: 'text').
+ */
+ public static function get_widget_id_base( $widget_id ) {
+ // Grab what's before the hyphen.
+ return substr( $widget_id, 0, strrpos( $widget_id, '-' ) );
+ }
+
+ /**
+ * Determine a widget's option name (the WP option where the widget's settings
+ * are stored - generally `widget_` + the widget's id_base).
+ *
+ * @param string $widget_id The id of a widget. (eg: 'text-3')
+ *
+ * @return string The option name of the widget's settings. (eg: 'widget_text')
+ */
+ public static function get_widget_option_name( $widget_id ) {
+ return 'widget_' . self::get_widget_id_base( $widget_id );
+ }
+
+ /**
+ * Determine a widget instance key from its ID. (eg: 'text-3' becomes '3').
+ * Used to access the widget's settings.
+ *
+ * @param string $widget_id The id of a widget.
+ *
+ * @return integer The instance key of that widget.
+ */
+ public static function get_widget_instance_key( $widget_id ) {
+ // Grab all numbers from the end of the id.
+ preg_match('/(\d+)$/', $widget_id, $matches );
+
+ return intval( $matches[0] );
+ }
+
+ /**
+ * Return a widget by ID (formatted for output) or null if nothing is found.
+ *
+ * @param string $widget_id The id of a widget to look for.
+ *
+ * @return array|null The matching formatted widget (see format_widget).
+ */
+ public static function get_widget_by_id( $widget_id ) {
+ $found = null;
+ foreach ( self::get_all_widgets() as $widget ) {
+ if ( $widget['id'] === $widget_id ) {
+ $found = $widget;
+ }
+ }
+ return $found;
+ }
+
+ /**
+ * Return an array of all widgets (active and inactive) formatted for output.
+ *
+ * @return array An array of all widgets (see format_widget).
+ */
+ public static function get_all_widgets() {
+ $all_widgets = array();
+ $sidebars_widgets = self::get_all_sidebars();
+
+ foreach ( $sidebars_widgets as $sidebar => $widgets ) {
+ if ( ! is_array( $widgets ) ) {
+ continue;
+ }
+ foreach ( $widgets as $key => $widget_id ) {
+ array_push( $all_widgets, self::format_widget( $key, $widget_id, $sidebar ) );
+ }
+ }
+
+ return $all_widgets;
+ }
+
+ /**
+ * Return an array of all active widgets formatted for output.
+ *
+ * @return array An array of all active widgets (see format_widget).
+ */
+ public static function get_active_widgets() {
+ $active_widgets = array();
+ $all_widgets = self::get_all_widgets();
+ foreach( $all_widgets as $widget ) {
+ if ( 'wp_inactive_widgets' === $widget['sidebar'] ) {
+ continue;
+ }
+ array_push( $active_widgets, $widget );
+ }
+ return $active_widgets;
+ }
+
+ /**
+ * Return an array of all widget IDs (active and inactive)
+ *
+ * @return array An array of all widget IDs.
+ */
+ public static function get_all_widget_ids() {
+ $all_widgets = array();
+ $sidebars_widgets = self::get_all_sidebars();
+ foreach ( array_values( $sidebars_widgets ) as $widgets ) {
+ if ( ! is_array( $widgets ) ) {
+ continue;
+ }
+ foreach ( array_values( $widgets ) as $widget_id ) {
+ array_push( $all_widgets, $widget_id );
+ }
+ }
+ return $all_widgets;
+ }
+
+ /**
+ * Return an array of widgets with a specific id_base (eg: `text`).
+ *
+ * @param string $id_base The id_base of a widget type.
+ *
+ * @return array All the formatted widgets matching that widget type (see format_widget).
+ */
+ public static function get_widgets_with_id_base( $id_base ) {
+ $matching_widgets = array();
+ foreach ( self::get_all_widgets() as $widget ) {
+ if ( self::get_widget_id_base( $widget['id'] ) === $id_base ) {
+ array_push( $matching_widgets, $widget );
+ }
+ }
+ return $matching_widgets;
+ }
+
+ /**
+ * Return the array of widget IDs in a sidebar or null if that sidebar does
+ * not exist. Will return an empty array for an existing empty sidebar.
+ *
+ * @param string $sidebar The id of a sidebar.
+ *
+ * @return array|null The array of widget IDs in the sidebar.
+ */
+ public static function get_widgets_in_sidebar( $sidebar ) {
+ $sidebars = self::get_all_sidebars();
+
+
+ if ( ! $sidebars || ! is_array( $sidebars ) ) {
+ return null;
+ }
+ if ( ! $sidebars[ $sidebar ] && array_key_exists( $sidebar, $sidebars ) ) {
+ return array();
+ }
+ return $sidebars[ $sidebar ];
+ }
+
+ /**
+ * Return an associative array of all registered sidebars for this theme,
+ * active and inactive, including the hidden disabled widgets sidebar (keyed
+ * by `wp_inactive_widgets`). Each sidebar is keyed by the ID of the sidebar
+ * and its value is an array of widget IDs for that sidebar.
+ *
+ * @return array An associative array of all sidebars and their widget IDs.
+ */
+ public static function get_all_sidebars() {
+ $sidebars_widgets = self::get_sidebars_widgets();
+
+ if ( ! is_array( $sidebars_widgets ) ) {
+ return array();
+ }
+ return $sidebars_widgets;
+ }
+
+ /**
+ * Return an associative array of all active sidebars for this theme, Each
+ * sidebar is keyed by the ID of the sidebar and its value is an array of
+ * widget IDs for that sidebar.
+ *
+ * @return array An associative array of all active sidebars and their widget IDs.
+ */
+ public static function get_active_sidebars() {
+ $sidebars = array();
+ foreach ( self::get_all_sidebars() as $sidebar => $widgets ) {
+ if ( 'wp_inactive_widgets' === $sidebar || ! isset( $widgets ) || ! is_array( $widgets ) ) {
+ continue;
+ }
+ $sidebars[ $sidebar ] = $widgets;
+ }
+ return $sidebars;
+ }
+
+ /**
+ * Activates a widget in a sidebar. Does not validate that the sidebar exists,
+ * so please do that first. Also does not save the widget's settings. Please
+ * do that with `set_widget_settings`.
+ *
+ * If position is not set, it will be set to the next available position.
+ *
+ * @param string $widget_id The newly-formed id of the widget to be added.
+ * @param string $sidebar The id of the sidebar where the widget will be added.
+ * @param string|integer $position (Optional) The position within the sidebar where the widget will be added.
+ *
+ * @return bool
+ */
+ public static function add_widget_to_sidebar( $widget_id, $sidebar, $position ) {
+ return self::move_widget_to_sidebar( array( 'id' => $widget_id ), $sidebar, $position );
+ }
+
+ /**
+ * Removes a widget from a sidebar. Does not validate that the sidebar exists
+ * or remove any settings from the widget, so please do that separately.
+ *
+ * @param array $widget The widget to be removed.
+ */
+ public static function remove_widget_from_sidebar( $widget ) {
+ $sidebars_widgets = self::get_sidebars_widgets();
+ // Remove the widget from its old location and reflow the positions of the remaining widgets.
+ array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
+
+ update_option( 'sidebars_widgets', $sidebars_widgets );
+ }
+
+ /**
+ * Moves a widget to a sidebar. Does not validate that the sidebar exists,
+ * so please do that first. Also does not save the widget's settings. Please
+ * do that with `set_widget_settings`. The first argument should be a
+ * widget as returned by `format_widget` including `id`, `sidebar`, and
+ * `position`.
+ *
+ * If $position is not set, it will be set to the next available position.
+ *
+ * Can be used to add a new widget to a sidebar if
+ * $widget['sidebar'] === NULL
+ *
+ * Can be used to move a widget within a sidebar as well if
+ * $widget['sidebar'] === $sidebar.
+ *
+ * @param array $widget The widget to be moved (see format_widget).
+ * @param string $sidebar The sidebar where this widget will be moved.
+ * @param string|integer $position (Optional) The position where this widget will be moved in the sidebar.
+ *
+ * @return bool
+ */
+ public static function move_widget_to_sidebar( $widget, $sidebar, $position ) {
+ $sidebars_widgets = self::get_sidebars_widgets();
+
+ // If a position is passed and the sidebar isn't empty,
+ // splice the widget into the sidebar, update the sidebar option, and return the result
+ if ( isset( $widget['sidebar'] ) && isset( $widget['position'] ) ) {
+ array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
+ }
+
+ // Sometimes an existing empty sidebar is NULL, so initialize it.
+ if ( array_key_exists( $sidebar, $sidebars_widgets ) && ! is_array( $sidebars_widgets[ $sidebar ] ) ) {
+ $sidebars_widgets[ $sidebar ] = array();
+ }
+
+ // If no position is passed, set one from items in sidebar
+ if ( ! isset( $position ) ) {
+ $position = 0;
+ $last_position = self::get_last_position_in_sidebar( $sidebar );
+ if ( isset( $last_position ) && is_numeric( $last_position ) ) {
+ $position = $last_position + 1;
+ }
+ }
+
+ // Add the widget to the sidebar and reflow the positions of the other widgets.
+ if ( empty( $sidebars_widgets[ $sidebar ] ) ) {
+ $sidebars_widgets[ $sidebar ][] = $widget['id'];
+ } else {
+ array_splice( $sidebars_widgets[ $sidebar ], (int)$position, 0, $widget['id'] );
+ }
+
+ set_theme_mod( 'sidebars_widgets', array( 'time' => time(), 'data' => $sidebars_widgets ) );
+ return update_option( 'sidebars_widgets', $sidebars_widgets );
+ }
+
+ /**
+ * Return an integer containing the largest position number in a sidebar or
+ * null if there are no widgets in that sidebar.
+ *
+ * @param string $sidebar The id of a sidebar.
+ *
+ * @return integer|null The last index position of a widget in that sidebar.
+ */
+ public static function get_last_position_in_sidebar( $sidebar ) {
+ $widgets = self::get_widgets_in_sidebar( $sidebar );
+ if ( ! $widgets ) {
+ return null;
+ }
+ $last_position = 0;
+ foreach ( $widgets as $widget_id ) {
+ $widget = self::get_widget_by_id( $widget_id );
+ if ( intval( $widget['position'] ) > intval( $last_position ) ) {
+ $last_position = intval( $widget['position'] );
+ }
+ }
+ return $last_position;
+ }
+
+ /**
+ * Saves settings for a widget. Does not add that widget to a sidebar. Please
+ * do that with `move_widget_to_sidebar` first. Will merge the settings of
+ * any existing widget with the same `$widget_id`.
+ *
+ * @param string $widget_id The id of a widget.
+ * @param array $settings An associative array of settings to merge with any existing settings on this widget.
+ *
+ * @return boolean|WP_Error True if update was successful.
+ */
+ public static function set_widget_settings( $widget_id, $settings ) {
+ $widget_option_name = self::get_widget_option_name( $widget_id );
+ $widget_settings = get_option( $widget_option_name );
+ $instance_key = self::get_widget_instance_key( $widget_id );
+ $old_settings = $widget_settings[ $instance_key ];
+
+ if ( ! $settings = self::sanitize_widget_settings( $widget_id, $settings, $old_settings ) ) {
+ return new WP_Error( 'invalid_data', 'Update failed.', 500 );
+ }
+ if ( is_array( $old_settings ) ) {
+ // array_filter prevents empty arguments from replacing existing ones
+ $settings = wp_parse_args( array_filter( $settings ), $old_settings );
+ }
+
+ $widget_settings[ $instance_key ] = $settings;
+
+ return update_option( $widget_option_name, $widget_settings );
+ }
+
+ /**
+ * Sanitize an associative array for saving.
+ *
+ * @param string $widget_id The id of a widget.
+ * @param array $settings A widget settings array.
+ * @param array $old_settings The existing widget settings array.
+ *
+ * @return array|false The settings array sanitized by `WP_Widget::update` or false if sanitization failed.
+ */
+ private static function sanitize_widget_settings( $widget_id, $settings, $old_settings ) {
+ if ( ! $widget = self::get_registered_widget_object( self::get_widget_id_base( $widget_id ) ) ) {
+ return false;
+ }
+ $new_settings = $widget->update( $settings, $old_settings );
+ if ( ! is_array( $new_settings ) ) {
+ return false;
+ }
+ return $new_settings;
+ }
+
+ /**
+ * Deletes settings for a widget. Does not remove that widget to a sidebar. Please
+ * do that with `remove_widget_from_sidebar` first.
+ *
+ * @param array $widget The widget which will have its settings removed (see format_widget).
+ */
+ public static function remove_widget_settings( $widget ) {
+ $widget_option_name = self::get_widget_option_name( $widget['id'] );
+ $widget_settings = get_option( $widget_option_name );
+ unset( $widget_settings[ self::get_widget_instance_key( $widget['id'] ) ] );
+ update_option( $widget_option_name, $widget_settings );
+ }
+
+ /**
+ * Update a widget's settings, sidebar, and position. Returns the (updated)
+ * formatted widget if successful or a WP_Error if it fails.
+ *
+ * @param string $widget_id The id of a widget to update.
+ * @param string $sidebar (Optional) A sidebar to which this widget will be moved.
+ * @param string|integer (Optional) A new position to which this widget will be moved within its new or existing sidebar.
+ * @param array|object|string $settings Settings to merge with the existing settings of the widget (will be passed through `decode_settings`).
+ *
+ * @return array|WP_Error The newly added widget as an associative array with all the above properties.
+ */
+ public static function update_widget( $widget_id, $sidebar, $position, $settings ) {
+ $settings = self::decode_settings( $settings );
+ if ( isset( $settings ) && ! is_array( $settings ) ) {
+ return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
+ }
+ // Default to an empty array if nothing is specified.
+ if ( ! is_array( $settings ) ) {
+ $settings = array();
+ }
+ $widget = self::get_widget_by_id( $widget_id );
+ if ( ! $widget ) {
+ return new WP_Error( 'not_found', 'No widget found.', 400 );
+ }
+ if ( ! $sidebar ) {
+ $sidebar = $widget['sidebar'];
+ }
+ if ( ! isset( $position ) ) {
+ $position = $widget['position'];
+ }
+ if ( ! is_numeric( $position ) ) {
+ return new WP_Error( 'invalid_data', 'Invalid position', 400 );
+ }
+ $widgets_in_sidebar = self::get_widgets_in_sidebar( $sidebar );
+ if ( ! isset( $widgets_in_sidebar ) ) {
+ return new WP_Error( 'invalid_data', 'No such sidebar exists', 400 );
+ }
+ self::move_widget_to_sidebar( $widget, $sidebar, $position );
+ $widget_save_status = self::set_widget_settings( $widget_id, $settings );
+ if ( is_wp_error( $widget_save_status ) ) {
+ return $widget_save_status;
+ }
+ return self::get_widget_by_id( $widget_id );
+ }
+
+ /**
+ * Deletes a widget entirely including all its settings. Returns a WP_Error if
+ * the widget could not be found. Otherwise returns an empty array.
+ *
+ * @param string $widget_id The id of a widget to delete. (eg: 'text-2')
+ *
+ * @return array|WP_Error An empty array if successful.
+ */
+ public static function delete_widget( $widget_id ) {
+ $widget = self::get_widget_by_id( $widget_id );
+ if ( ! $widget ) {
+ return new WP_Error( 'not_found', 'No widget found.', 400 );
+ }
+ self::remove_widget_from_sidebar( $widget );
+ self::remove_widget_settings( $widget );
+ return array();
+ }
+
+ /**
+ * Return an array of settings. The input can be either an object, a JSON
+ * string, or an array.
+ *
+ * @param array|string|object $settings The settings of a widget as passed into the API.
+ *
+ * @return array Decoded associative array of settings.
+ */
+ public static function decode_settings( $settings ) {
+ // Treat as string in case JSON was passed
+ if ( is_object( $settings ) && property_exists( $settings, 'scalar' ) ) {
+ $settings = $settings->scalar;
+ }
+ if ( is_object( $settings ) ) {
+ $settings = (array) $settings;
+ }
+ // Attempt to decode JSON string
+ if ( is_string( $settings ) ) {
+ $settings = (array) json_decode( $settings );
+ }
+ return $settings;
+ }
+
+ /**
+ * Activate a new widget.
+ *
+ * @param string $id_base The id_base of the new widget (eg: 'text')
+ * @param string $sidebar The id of the sidebar where this widget will go. Dependent on theme. (eg: 'sidebar-1')
+ * @param string|integer $position (Optional) The position of the widget in the sidebar. Defaults to the last position.
+ * @param array|object|string $settings (Optional) An associative array of settings for this widget (will be passed through `decode_settings`). Varies by widget.
+ *
+ * @return array|WP_Error The newly added widget as an associative array with all the above properties except 'id_base' replaced with the generated 'id'.
+ */
+ public static function activate_widget( $id_base, $sidebar, $position, $settings ) {
+ if ( ! isset( $id_base ) || ! self::validate_id_base( $id_base ) ) {
+ return new WP_Error( 'invalid_data', 'Invalid ID base', 400 );
+ }
+
+ if ( ! isset( $sidebar ) ) {
+ return new WP_Error( 'invalid_data', 'No sidebar provided', 400 );
+ }
+
+ if ( isset( $position ) && ! is_numeric( $position ) ) {
+ return new WP_Error( 'invalid_data', 'Invalid position', 400 );
+ }
+
+ $settings = self::decode_settings( $settings );
+ if ( isset( $settings ) && ! is_array( $settings ) ) {
+ return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
+ }
+
+ // Default to an empty array if nothing is specified.
+ if ( ! is_array( $settings ) ) {
+ $settings = array();
+ }
+
+ $widget_counter = 1 + self::get_last_widget_instance_key_with_id_base( $id_base );
+ $widget_id = $id_base . '-' . $widget_counter;
+ if ( 0 >= $widget_counter ) {
+ return new WP_Error( 'invalid_data', 'Error creating widget ID' . $widget_id, 500 );
+ }
+ if ( self::get_widget_by_id( $widget_id ) ) {
+ return new WP_Error( 'invalid_data', 'Widget ID already exists', 500 );
+ }
+
+ self::add_widget_to_sidebar( $widget_id, $sidebar, $position );
+ $widget_save_status = self::set_widget_settings( $widget_id, $settings );
+ if ( is_wp_error( $widget_save_status ) ) {
+ return $widget_save_status;
+ }
+
+ // Add a Tracks event for non-Headstart activity.
+ if ( ! defined( 'HEADSTART' ) ) {
+ jetpack_require_lib( 'tracks/client' );
+ jetpack_tracks_record_event( wp_get_current_user(), 'wpcom_widgets_activate_widget', array(
+ 'widget' => $id_base,
+ 'settings' => json_encode( $settings ),
+ ) );
+ }
+
+ return self::get_widget_by_id( $widget_id );
+ }
+
+ /**
+ * Activate an array of new widgets. Like calling `activate_widget` multiple times.
+ *
+ * @param array $widgets An array of widget arrays. Each sub-array must be of the format required by `activate_widget`.
+ *
+ * @return array|WP_Error The newly added widgets in the form returned by `get_all_widgets`.
+ */
+ public static function activate_widgets( $widgets ) {
+ if ( ! is_array( $widgets ) ) {
+ return new WP_Error( 'invalid_data', 'Invalid widgets', 400 );
+ }
+
+ $added_widgets = array();
+
+ foreach( $widgets as $widget ) {
+ $added_widgets[] = self::activate_widget( $widget['id_base'], $widget['sidebar'], $widget['position'], $widget['settings'] );
+ }
+
+ return $added_widgets;
+ }
+
+ /**
+ * Return the last instance key (integer) of an existing widget matching
+ * `$id_base`. So if you pass in `text`, and there is a widget with the id
+ * `text-2`, this function will return `2`.
+ *
+ * @param string $id_base The id_base of a type of widget. (eg: 'rss')
+ *
+ * @return integer The last instance key of that type of widget.
+ */
+ public static function get_last_widget_instance_key_with_id_base( $id_base ) {
+ $similar_widgets = self::get_widgets_with_id_base( $id_base );
+
+ if ( ! empty( $similar_widgets ) ) {
+ // If the last widget with the same name is `text-3`, we want `text-4`
+ usort( $similar_widgets, __CLASS__ . '::sort_widgets' );
+
+ $last_widget = array_pop( $similar_widgets );
+ $last_val = intval( self::get_widget_instance_key( $last_widget['id'] ) );
+
+ return $last_val;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Method used to sort widgets
+ *
+ * @since 5.4
+ *
+ * @param array $a
+ * @param array $b
+ *
+ * @return int
+ */
+ public static function sort_widgets( $a, $b ) {
+ $a_val = intval( self::get_widget_instance_key( $a['id'] ) );
+ $b_val = intval( self::get_widget_instance_key( $b['id'] ) );
+ if ( $a_val > $b_val ) {
+ return 1;
+ }
+ if ( $a_val < $b_val ) {
+ return -1;
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve a given widget object instance by ID base (eg. 'text' or 'archives').
+ *
+ * @param string $id_base The id_base of a type of widget.
+ *
+ * @return WP_Widget|false The found widget object or false if the id_base was not found.
+ */
+ public static function get_registered_widget_object( $id_base ) {
+ if ( ! $id_base ) {
+ return false;
+ }
+
+ // Get all of the registered widgets.
+ global $wp_widget_factory;
+ if ( ! isset( $wp_widget_factory ) ) {
+ return false;
+ }
+
+ $registered_widgets = $wp_widget_factory->widgets;
+ if ( empty( $registered_widgets ) ) {
+ return false;
+ }
+
+ foreach ( array_values( $registered_widgets ) as $registered_widget_object ) {
+ if ( $registered_widget_object->id_base === $id_base ) {
+ return $registered_widget_object;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Validate a given widget ID base (eg. 'text' or 'archives').
+ *
+ * @param string $id_base The id_base of a type of widget.
+ *
+ * @return boolean True if the widget is of a known type.
+ */
+ public static function validate_id_base( $id_base ) {
+ return ( false !== self::get_registered_widget_object( $id_base ) );
+ }
+
+ /**
+ * Insert a new widget in a given sidebar.
+ *
+ * @param string $widget_id ID of the widget.
+ * @param array $widget_options Content of the widget.
+ * @param string $sidebar ID of the sidebar to which the widget will be added.
+ *
+ * @return WP_Error|true True when data has been saved correctly, error otherwise.
+ */
+ static function insert_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
+ // Retrieve sidebars, widgets and their instances
+ $sidebars_widgets = get_option( 'sidebars_widgets', array() );
+ $widget_instances = get_option( 'widget_' . $widget_id, array() );
+
+ // Retrieve the key of the next widget instance
+ $numeric_keys = array_filter( array_keys( $widget_instances ), 'is_int' );
+ $next_key = $numeric_keys ? max( $numeric_keys ) + 1 : 2;
+
+ // Add this widget to the sidebar
+ if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
+ $sidebars_widgets[ $sidebar ] = array();
+ }
+ $sidebars_widgets[ $sidebar ][] = $widget_id . '-' . $next_key;
+
+ // Add the new widget instance
+ $widget_instances[ $next_key ] = $widget_options;
+
+ // Store updated sidebars, widgets and their instances
+ if (
+ ! ( update_option( 'sidebars_widgets', $sidebars_widgets ) )
+ || ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) )
+ ) {
+ return new WP_Error( 'widget_update_failed', 'Failed to update widget or sidebar.', 400 );
+ };
+
+ return true;
+ }
+
+ /**
+ * Update the content of an existing widget in a given sidebar.
+ *
+ * @param string $widget_id ID of the widget.
+ * @param array $widget_options New content for the update.
+ * @param string $sidebar ID of the sidebar to which the widget will be added.
+ *
+ * @return WP_Error|true True when data has been updated correctly, error otherwise.
+ */
+ static function update_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
+ // Retrieve sidebars, widgets and their instances
+ $sidebars_widgets = get_option( 'sidebars_widgets', array() );
+ $widget_instances = get_option( 'widget_' . $widget_id, array() );
+
+ // Retrieve index of first widget instance in that sidebar
+ $widget_key = false;
+ foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
+ if ( strpos( $widget, $widget_id ) !== false ) {
+ $widget_key = absint( str_replace( $widget_id . '-', '', $widget ) );
+ break;
+ }
+ }
+
+ // There is no widget instance
+ if ( ! $widget_key ) {
+ return new WP_Error( 'invalid_data', 'No such widget.', 400 );
+ }
+
+ // Update the widget instance and option if the data has changed
+ if ( $widget_instances[ $widget_key ]['title'] !== $widget_options['title']
+ || $widget_instances[ $widget_key ]['address'] !== $widget_options['address']
+ ) {
+
+ $widget_instances[ $widget_key ] = array_merge( $widget_instances[ $widget_key ], $widget_options );
+
+ // Store updated widget instances and return Error when not successful
+ if ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) ) {
+ return new WP_Error( 'widget_update_failed', 'Failed to update widget.', 400 );
+ };
+ };
+ return true;
+ }
+
+ /**
+ * Retrieve the first active sidebar.
+ *
+ * @return string|WP_Error First active sidebar, error if none exists.
+ */
+ static function get_first_sidebar() {
+ $active_sidebars = get_option( 'sidebars_widgets', array() );
+ unset( $active_sidebars[ 'wp_inactive_widgets' ], $active_sidebars[ 'array_version' ] );
+
+ if ( empty( $active_sidebars ) ) {
+ return false;
+ }
+ $active_sidebars_keys = array_keys( $active_sidebars );
+ return array_shift( $active_sidebars_keys );
+ }
+}