Add upstream

This commit is contained in:
root
2019-10-24 00:12:05 +02:00
parent 85d41e4216
commit ac980f592c
3504 changed files with 1049983 additions and 29971 deletions

View File

@@ -0,0 +1,631 @@
<?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;
/**
* Anonymous info about a12s. The method fetch_a8c_data() stores the response from wpcom here.
*
* @var array
*/
private $a8c_data = null;
/**
* 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(
null,
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' ) );
$this->a8c_data = $this->fetch_a8c_data();
}
/**
* 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
echo esc_html(
sprintf(
/* translators: first placeholder is the number of Automattic employees. The second is the number of countries of origin*/
__( 'Were a distributed company with over %1$s Automatticians in more than %2$s countries speaking at least %3$s 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' ),
$this->a8c_data['a12s'],
$this->a8c_data['countries'],
$this->a8c_data['languages']
)
);
?>
</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 = $this->a8c_data['featured_plugins'];
// 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( wp_get_update_php_url() )
);
wp_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( wp_get_update_php_url() )
);
wp_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( wp_get_update_php_url() )
);
wp_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_url( $plugin_icon_url ); ?>" class="plugin-icon" alt="<?php esc_attr_e( 'Plugin icon', 'jetpack' ); ?>" aria-hidden="true">
</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 anonymous data about A12s from wpcom: total count, number of countries, languages spoken.
*
* @since 7.4
*
* @return array $data
*/
private function fetch_a8c_data() {
$data = get_transient( 'jetpack_a8c_data' );
if ( false === $data ) {
$data = json_decode(
wp_remote_retrieve_body(
wp_remote_get( 'https://public-api.wordpress.com/wpcom/v2/jetpack-about' )
),
true
);
if ( ! empty( $data ) && is_array( $data ) ) {
set_transient( 'jetpack_a8c_data', $data, DAY_IN_SECONDS );
} else {
// Fallback if everything fails.
$data = array(
'a12s' => 888,
'countries' => 69,
'languages' => 83,
'featured_plugins' => array(
'woocommerce',
'wp-super-cache',
'wp-job-manager',
'co-authors-plus',
),
);
}
}
return $data;
}
/**
* Compile and display a list of avatars for A12s that gave their permission.
*
* @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/0619d4de8aef78c81b2194ff1d164d85',
'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,
),
)
);
}
}

View File

@@ -0,0 +1,363 @@
<?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{$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' );
$jetpack_about_url = ( Jetpack::is_active() || Jetpack::is_development_mode() )
? admin_url( 'admin.php?page=jetpack_about' )
: 'https://jetpack.com';
?>
<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="<?php echo esc_url( $jetpack_about_url ); ?>">
<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="<?php echo esc_url( $jetpack_about_url ); ?>" title="<?php esc_attr__( 'About Jetpack', 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html__( 'About', 'jetpack' ); ?></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;
}
}

View File

@@ -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

View File

@@ -0,0 +1,364 @@
<?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,
'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;
}
}
/**
* 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;
}

View File

@@ -0,0 +1,146 @@
<?php
use Automattic\Jetpack\Tracking;
use Automattic\Jetpack\Assets;
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
$tracking = new Tracking();
$tracking->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',
Assets::get_file_url_for_environment( '_inc/build/jetpack-admin.min.js', '_inc/jetpack-admin.js' ),
array( 'jquery' ),
JETPACK__VERSION
);
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
<?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 ) {
// phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection
$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;
}
}

View File

@@ -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" />';
}
}

View File

@@ -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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 );
}
}

View File

@@ -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'];
}
}

View File

@@ -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=":)"/>';
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 );
}
}

View File

@@ -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' );

View File

@@ -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;
}
}

View File

@@ -0,0 +1,63 @@
<?php
use Automattic\Jetpack\Connection\Client;
/**
* 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 = 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' );
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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' );

View File

@@ -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' );

View File

@@ -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' );

View File

@@ -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' );

View File

@@ -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' );

View File

@@ -0,0 +1,179 @@
<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName
/**
* Memberships: API to communicate with "product" database.
*
* @package Jetpack
* @since 7.3.0
*/
use Automattic\Jetpack\Connection\Client;
/**
* 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 = 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 WP_Error|array ['products','connected_account_id','connect_url','should_upgrade_to_access_memberships','upgrade_url']
*/
public function get_status() {
if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
require_lib( 'memberships' );
$blog_id = get_current_blog_id();
return (array) get_memberships_settings_for_site( $blog_id );
} else {
$blog_id = Jetpack_Options::get_option( 'id' );
$response = 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 ( 200 !== $response['response']['code'] && $data['code'] && $data['message'] ) {
return new WP_Error( $data['code'], $data['message'], 401 );
}
return $data;
}
}
}
if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_active() ) {
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Memberships' );
}

View File

@@ -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' );

View File

@@ -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' );

View File

@@ -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' );

View File

@@ -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' );

View File

@@ -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' );

View File

@@ -0,0 +1,64 @@
<?php
use Automattic\Jetpack\Constants;
/**
* 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 ( ! 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' ) ||
( Constants::is_defined( 'TESTING_IN_JETPACK' ) && Constants::get_constant( 'TESTING_IN_JETPACK' ) )
) {
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Subscribers' );
}

View File

@@ -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' );
}

View File

@@ -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' );
}

View File

@@ -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' );
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,346 @@
<?php
/**
* Collection of tests to run on the Jetpack connection locally.
*
* @package Jetpack
*/
use Automattic\Jetpack\Connection\Client;
/**
* 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 = Client::wpcom_json_api_request_as_blog(
sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
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 );
}
if ( 404 === wp_remote_retrieve_response_code( $response ) ) {
return self::skipped_test( $name, __( 'The WordPress.com API returned a 404 error.', 'jetpack' ) );
}
$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.
}
}
}

View File

@@ -0,0 +1,397 @@
<?php
/**
* Jetpack Debug Data for the legacy Jetpack debugger page and the WP 5.2-era Site Health sections.
*
* @package jetpack
*/
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Sync\Modules;
use Automattic\Jetpack\Sync\Functions;
use Automattic\Jetpack\Sync\Sender;
/**
* 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 ) {
$support_url = Jetpack::is_development_version()
? 'https://jetpack.com/contact-support/beta-group/'
: 'https://jetpack.com/contact-support/';
$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_url( $support_url ),
__( '(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();
$blog_token = Jetpack_Data::get_access_token();
$user_token = Jetpack_Data::get_access_token( $user_id );
$tokenset = '';
if ( $blog_token ) {
$tokenset = 'Blog ';
$blog_key = substr( $blog_token->secret, 0, strpos( $blog_token->secret, '.' ) );
// 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->secret, 0, strpos( $user_token->secret, '.' ) );
// 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 */
$sync_module = 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 = 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 = 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' => Functions::home_url(),
'siteurl' => Functions::site_url(),
'WP_HOME' => Constants::is_defined( 'WP_HOME' ) ? Constants::get_constant( 'WP_HOME' ) : '',
'WP_SITEURL' => Constants::is_defined( 'WP_SITEURL' ) ? 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)".
}
}

View File

@@ -0,0 +1,444 @@
<?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' ) );
}
$support_url = Jetpack::is_development_version()
? 'https://jetpack.com/contact-support/beta-group/'
: 'https://jetpack.com/contact-support/';
$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() ) )
),
esc_url( $support_url ),
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() ) )
),
esc_url( $support_url )
);
$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
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 );
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
<?php
if ( ! class_exists( 'MarkdownExtra_Parser' ) )
jetpack_require_lib( 'markdown/extra' );
jetpack_require_lib( 'markdown/gfm' );

View File

@@ -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.

File diff suppressed because it is too large Load Diff

View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -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(), ',' );
}
};

View File

@@ -0,0 +1,5 @@
<?php
/**
* Deprecated file since 7.5 - Jetpack_Tracks_Client is now autoloaded from 'vendor/automattic/jetpack-tracking/legacy/class.tracks-client.php'
*/
_deprecated_file( basename( __FILE__ ), 'jetpack-7.5' );

View File

@@ -0,0 +1,5 @@
<?php
/**
* Deprecated since 7.5
*/
_deprecated_file( basename( __FILE__ ), 'jetpack-7.5' );

View File

@@ -0,0 +1,5 @@
<?php
/**
* Deprecated file since 7.5 - Jetpack_Tracks_Client is now autoloaded from 'vendor/automattic/jetpack-tracking/legacy/class.tracks-client.php'
*/
_deprecated_file( basename( __FILE__ ), 'jetpack-7.5' );

View File

@@ -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 );

View File

@@ -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' ] );
},
};

View File

@@ -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' ) ) {
$tracking = new Automattic\Jetpack\Tracking();
$tracking->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 );
}
}