Add upstream
This commit is contained in:
@@ -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*/
|
||||
__( 'We’re 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&plugin={$plugin['slug']}&TB_iframe=true&width=600&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’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’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’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,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
755
wp-content/plugins/jetpack/_inc/lib/class.color.php
Normal file
755
wp-content/plugins/jetpack/_inc/lib/class.color.php
Normal 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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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" />';
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 );
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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=":)"/>';
|
||||
}
|
||||
}
|
||||
}
|
||||
436
wp-content/plugins/jetpack/_inc/lib/class.media-extractor.php
Normal file
436
wp-content/plugins/jetpack/_inc/lib/class.media-extractor.php
Normal 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;
|
||||
}
|
||||
}
|
||||
369
wp-content/plugins/jetpack/_inc/lib/class.media-summary.php
Normal file
369
wp-content/plugins/jetpack/_inc/lib/class.media-summary.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
505
wp-content/plugins/jetpack/_inc/lib/class.media.php
Normal file
505
wp-content/plugins/jetpack/_inc/lib/class.media.php
Normal 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' );
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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' );
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
}
|
||||
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
@@ -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' );
|
||||
}
|
||||
@@ -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' );
|
||||
}
|
||||
@@ -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' );
|
||||
}
|
||||
24
wp-content/plugins/jetpack/_inc/lib/debugger/0-load.php
Normal file
24
wp-content/plugins/jetpack/_inc/lib/debugger/0-load.php
Normal 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' );
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)".
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
353
wp-content/plugins/jetpack/_inc/lib/functions.wp-notify.php
Normal file
353
wp-content/plugins/jetpack/_inc/lib/functions.wp-notify.php
Normal 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;
|
||||
913
wp-content/plugins/jetpack/_inc/lib/icalendar-reader.php
Normal file
913
wp-content/plugins/jetpack/_inc/lib/icalendar-reader.php
Normal 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'] ) ? ' ' : $this->escape( stripslashes( $event['LOCATION'] ) ); ?></td>
|
||||
<td><?php echo $this->formatted_date( $event ); ?></td>
|
||||
<td><?php echo empty( $event['SUMMARY'] ) ? ' ' : $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></td>
|
||||
<td><?php echo empty( $event['DESCRIPTION'] ) ? ' ' : 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 – %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 );
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
6
wp-content/plugins/jetpack/_inc/lib/markdown/0-load.php
Normal file
6
wp-content/plugins/jetpack/_inc/lib/markdown/0-load.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
if ( ! class_exists( 'MarkdownExtra_Parser' ) )
|
||||
jetpack_require_lib( 'markdown/extra' );
|
||||
|
||||
jetpack_require_lib( 'markdown/gfm' );
|
||||
19
wp-content/plugins/jetpack/_inc/lib/markdown/README.md
Normal file
19
wp-content/plugins/jetpack/_inc/lib/markdown/README.md
Normal 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.
|
||||
3207
wp-content/plugins/jetpack/_inc/lib/markdown/extra.php
Normal file
3207
wp-content/plugins/jetpack/_inc/lib/markdown/extra.php
Normal file
File diff suppressed because it is too large
Load Diff
400
wp-content/plugins/jetpack/_inc/lib/markdown/gfm.php
Normal file
400
wp-content/plugins/jetpack/_inc/lib/markdown/gfm.php
Normal 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><</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>)?(#|\\\\#)/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";
|
||||
}
|
||||
|
||||
}
|
||||
132
wp-content/plugins/jetpack/_inc/lib/plugins.php
Normal file
132
wp-content/plugins/jetpack/_inc/lib/plugins.php
Normal 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;
|
||||
}
|
||||
}
|
||||
237
wp-content/plugins/jetpack/_inc/lib/tonesque.php
Normal file
237
wp-content/plugins/jetpack/_inc/lib/tonesque.php
Normal 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(), ',' );
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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' );
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
/**
|
||||
* Deprecated since 7.5
|
||||
*/
|
||||
_deprecated_file( basename( __FILE__ ), 'jetpack-7.5' );
|
||||
5
wp-content/plugins/jetpack/_inc/lib/tracks/client.php
Normal file
5
wp-content/plugins/jetpack/_inc/lib/tracks/client.php
Normal 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' );
|
||||
62
wp-content/plugins/jetpack/_inc/lib/tracks/tracks-ajax.js
Normal file
62
wp-content/plugins/jetpack/_inc/lib/tracks/tracks-ajax.js
Normal 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 );
|
||||
@@ -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' ] );
|
||||
},
|
||||
};
|
||||
776
wp-content/plugins/jetpack/_inc/lib/widgets.php
Normal file
776
wp-content/plugins/jetpack/_inc/lib/widgets.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user