init
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for the Jetpack About Page within the wp-admin.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Disable direct access and execution.
|
||||
*/
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/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(
|
||||
'',
|
||||
esc_html__( 'About Jetpack', 'jetpack' ),
|
||||
'',
|
||||
'jetpack_admin_page',
|
||||
'jetpack_about',
|
||||
array( $this, 'render' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add page action
|
||||
*
|
||||
* @param string $hook Hook of current page.
|
||||
*/
|
||||
public function add_page_actions( $hook ) {
|
||||
if ( 'admin_page_jetpack_about' === $hook ) {
|
||||
$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 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the page content
|
||||
*/
|
||||
public function page_render() {
|
||||
?>
|
||||
<div class="jp-lower">
|
||||
<h1 class="screen-reader-text"><?php esc_html_e( 'About Jetpack', 'jetpack' ); ?></h1>
|
||||
<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>
|
||||
<svg class="gridicons-external" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19 13v6c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V7c0-1.105.895-2 2-2h6v2H5v12h12v-6h2zM13 3v2h4.586l-7.793 7.793 1.414 1.414L19 6.414V11h2V3h-8z"></path></g></svg>
|
||||
</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>
|
||||
<svg class="gridicons-external" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19 13v6c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V7c0-1.105.895-2 2-2h6v2H5v12h12v-6h2zM13 3v2h4.586l-7.793 7.793 1.414 1.414L19 6.414V11h2V3h-8z"></path></g></svg>
|
||||
</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><svg class="gridicons-external" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19 13v6c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V7c0-1.105.895-2 2-2h6v2H5v12h12v-6h2zM13 3v2h4.586l-7.793 7.793 1.414 1.414L19 6.414V11h2V3h-8z"></path></g></svg>', 'jetpack' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
'class' => array(),
|
||||
'target' => array(),
|
||||
'rel' => array(),
|
||||
'data-jptracks-name' => array(),
|
||||
),
|
||||
'svg' => array(
|
||||
'class' => array(),
|
||||
'height' => array(),
|
||||
'width' => array(),
|
||||
'xmlns' => array(),
|
||||
'viewbox' => array(),
|
||||
),
|
||||
'g' => array(),
|
||||
'path' => array(
|
||||
'd' => 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><svg class="gridicons-external" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19 13v6c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V7c0-1.105.895-2 2-2h6v2H5v12h12v-6h2zM13 3v2h4.586l-7.793 7.793 1.414 1.414L19 6.414V11h2V3h-8z"></path></g></svg>
|
||||
</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 take a look at <a href="https://profiles.wordpress.org/automattic/#content-plugins" target="_blank" rel="noopener noreferrer" class="jptracks" data-jptracks-name="jetpack_about_wporg_profile">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 is-rna" 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, WEEK_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/1ec3571e0201a990ceca5e365e780efa',
|
||||
'https://0.gravatar.com/avatar/0619d4de8aef78c81b2194ff1d164d85',
|
||||
'https://0.gravatar.com/avatar/7fdcad31a04def0ab9583af475c9036c',
|
||||
'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,515 @@
|
||||
<?php
|
||||
/**
|
||||
* A utility class that generates the initial state for Redux in wp-admin.
|
||||
* Modularized from `class.jetpack-react-page.php`.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Blaze;
|
||||
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Connection\Plugin_Storage as Connection_Plugin_Storage;
|
||||
use Automattic\Jetpack\Connection\REST_Connector;
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
|
||||
use Automattic\Jetpack\Device_Detection\User_Agent_Info;
|
||||
use Automattic\Jetpack\Identity_Crisis;
|
||||
use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
|
||||
use Automattic\Jetpack\Image_CDN\Image_CDN_Image;
|
||||
use Automattic\Jetpack\IP\Utils as IP_Utils;
|
||||
use Automattic\Jetpack\Licensing;
|
||||
use Automattic\Jetpack\Licensing\Endpoints as Licensing_Endpoints;
|
||||
use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
|
||||
use Automattic\Jetpack\My_Jetpack\Jetpack_Manage;
|
||||
use Automattic\Jetpack\Partner;
|
||||
use Automattic\Jetpack\Partner_Coupon as Jetpack_Partner_Coupon;
|
||||
use Automattic\Jetpack\Stats\Options as Stats_Options;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Responsible for populating the initial Redux state.
|
||||
*/
|
||||
class Jetpack_Redux_State_Helper {
|
||||
/**
|
||||
* Generate minimal state for React to fetch its own data asynchronously after load
|
||||
* This can improve user experience, reducing time spent on server requests before serving the page
|
||||
* e.g. used by React Disconnect Dialog on plugins page where the full initial state is not needed
|
||||
*/
|
||||
public static function get_minimal_state() {
|
||||
return array(
|
||||
'WP_API_root' => esc_url_raw( rest_url() ),
|
||||
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the initial state array to be used by the Redux store.
|
||||
*/
|
||||
public static function get_initial_state() {
|
||||
global $is_safari;
|
||||
|
||||
// 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';
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php';
|
||||
|
||||
$module_list_endpoint = new Jetpack_Core_API_Module_List_Endpoint();
|
||||
$modules = $module_list_endpoint->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'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
|
||||
$modules[ $slug ]['description'] = html_entity_decode( $data['description'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
|
||||
$modules[ $slug ]['short_description'] = html_entity_decode( $data['short_description'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
|
||||
$modules[ $slug ]['long_description'] = html_entity_decode( $data['long_description'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
|
||||
}
|
||||
|
||||
// "mock" a block module in order to get it searchable in the settings.
|
||||
$modules['blocks']['module'] = 'blocks';
|
||||
$modules['blocks']['additional_search_queries'] = esc_html_x( 'blocks, block, gutenberg', 'Search terms', 'jetpack' );
|
||||
|
||||
// "mock" an Earn module in order to get it searchable in the settings.
|
||||
$modules['earn']['module'] = 'earn';
|
||||
$modules['earn']['additional_search_queries'] = esc_html_x( 'earn, paypal, stripe, payments, pay', 'Search terms', 'jetpack' );
|
||||
|
||||
// Collecting roles that can view site stats.
|
||||
$stats_roles = array();
|
||||
$enabled_roles = Stats_Options::get_option( 'roles' );
|
||||
|
||||
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();
|
||||
|
||||
$current_user_data = jetpack_current_user_data();
|
||||
|
||||
/**
|
||||
* Adds information to the `connectionStatus` API field that is unique to the Jetpack React dashboard.
|
||||
*/
|
||||
$connection_status = array(
|
||||
'isInIdentityCrisis' => Identity_Crisis::validate_sync_error_idc_option(),
|
||||
'sandboxDomain' => JETPACK__SANDBOX_DOMAIN,
|
||||
|
||||
/**
|
||||
* Filter to add connection errors
|
||||
* Format: array( array( 'code' => '...', 'message' => '...', 'action' => '...' ), ... )
|
||||
*
|
||||
* @since 8.7.0
|
||||
*
|
||||
* @param array $errors Connection errors.
|
||||
*/
|
||||
'errors' => apply_filters( 'react_connection_errors_initial_state', array() ),
|
||||
);
|
||||
|
||||
$connection_status = array_merge( REST_Connector::connection_status( false ), $connection_status );
|
||||
|
||||
$host = new Host();
|
||||
|
||||
$speed_score_history = new Speed_Score_History( wp_parse_url( get_site_url(), PHP_URL_HOST ) );
|
||||
|
||||
$block_availability = Jetpack_Gutenberg::get_cached_availability();
|
||||
|
||||
return array(
|
||||
'WP_API_root' => esc_url_raw( rest_url() ),
|
||||
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
|
||||
'purchaseToken' => self::get_purchase_token(),
|
||||
'partnerCoupon' => Jetpack_Partner_Coupon::get_coupon(),
|
||||
'pluginBaseUrl' => plugins_url( '', JETPACK__PLUGIN_FILE ),
|
||||
'connectionStatus' => $connection_status,
|
||||
'connectedPlugins' => Connection_Plugin_Storage::get_all(),
|
||||
'connectUrl' => false == $current_user_data['isConnected'] // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
|
||||
? Jetpack::init()->build_connect_url( true, false, false )
|
||||
: '',
|
||||
'dismissedNotices' => self::get_dismissed_jetpack_notices(),
|
||||
'isDevVersion' => Jetpack::is_development_version(),
|
||||
'currentVersion' => JETPACK__VERSION,
|
||||
'is_gutenberg_available' => true,
|
||||
'getModules' => $modules,
|
||||
'rawUrl' => ( new Status() )->get_site_suffix(),
|
||||
'adminUrl' => esc_url( admin_url() ),
|
||||
'siteTitle' => (string) htmlspecialchars_decode( get_option( 'blogname' ), ENT_QUOTES ),
|
||||
'stats' => array(
|
||||
// data is populated asynchronously on page load.
|
||||
'data' => array(
|
||||
'general' => false,
|
||||
'day' => false,
|
||||
'week' => false,
|
||||
'month' => false,
|
||||
),
|
||||
'roles' => $stats_roles,
|
||||
),
|
||||
'aff' => Partner::init()->get_partner_code( Partner::AFFILIATE_CODE ),
|
||||
'partnerSubsidiaryId' => Partner::init()->get_partner_code( Partner::SUBSIDIARY_CODE ),
|
||||
'settings' => self::get_flattened_settings(),
|
||||
'userData' => array(
|
||||
'currentUser' => $current_user_data,
|
||||
),
|
||||
'siteData' => array(
|
||||
'blog_id' => Jetpack_Options::get_option( 'id', 0 ),
|
||||
'icon' => has_site_icon()
|
||||
? apply_filters( 'jetpack_photon_url', get_site_icon_url(), array( 'w' => 64 ) )
|
||||
: '',
|
||||
'siteVisibleToSearchEngines' => '1' == get_option( 'blog_public' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
|
||||
/**
|
||||
* 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' => $host->is_woa_site(),
|
||||
'isWoASite' => $host->is_woa_site(),
|
||||
'isAtomicPlatform' => $host->is_atomic_platform(),
|
||||
'plan' => Jetpack_Plan::get(),
|
||||
'showBackups' => Jetpack::show_backups_ui(),
|
||||
'showRecommendations' => Jetpack_Recommendations::is_enabled(),
|
||||
/** This filter is documented in my-jetpack/src/class-initializer.php */
|
||||
'showMyJetpack' => My_Jetpack_Initializer::should_initialize(),
|
||||
'isMultisite' => is_multisite(),
|
||||
'dateFormat' => get_option( 'date_format' ),
|
||||
'latestBoostSpeedScores' => $speed_score_history->latest(),
|
||||
'isSharingBlockAvailable' => (bool) isset( $block_availability['sharing-buttons'] )
|
||||
&& $block_availability['sharing-buttons']['available'],
|
||||
),
|
||||
'themeData' => array(
|
||||
'name' => $current_theme->get( 'Name' ),
|
||||
'stylesheet' => $current_theme->get_stylesheet(),
|
||||
'hasUpdate' => (bool) get_theme_update_available( $current_theme ),
|
||||
'isBlockTheme' => (bool) $current_theme->is_block_theme(),
|
||||
'support' => array(
|
||||
'infinite-scroll' => current_theme_supports( 'infinite-scroll' ) || in_array( $current_theme->get_stylesheet(), $inf_scr_support_themes, true ),
|
||||
'widgets' => current_theme_supports( 'widgets' ),
|
||||
'webfonts' => wp_theme_has_theme_json()
|
||||
&& ( function_exists( 'wp_register_webfont_provider' ) || function_exists( 'wp_register_webfonts' ) ),
|
||||
),
|
||||
),
|
||||
'jetpackStateNotices' => array(
|
||||
'messageCode' => Jetpack::state( 'message' ),
|
||||
'errorCode' => Jetpack::state( 'error' ),
|
||||
'errorDescription' => Jetpack::state( 'error_description' ),
|
||||
'messageContent' => Jetpack::state( 'display_update_modal' ) ? self::get_update_modal_data() : null,
|
||||
),
|
||||
'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(),
|
||||
'currentIp' => IP_Utils::get_ip(),
|
||||
'lastPostUrl' => esc_url( $last_post ),
|
||||
'externalServicesConnectUrls' => self::get_external_services_connect_urls(),
|
||||
'calypsoEnv' => ( new Host() )->get_calypso_env(),
|
||||
'products' => Jetpack::get_products_for_purchase(),
|
||||
'recommendationsStep' => Jetpack_Core_Json_Api_Endpoints::get_recommendations_step()['step'],
|
||||
'isSafari' => $is_safari || User_Agent_Info::is_opera_desktop(), // @todo Rename isSafari everywhere.
|
||||
'doNotUseConnectionIframe' => Constants::is_true( 'JETPACK_SHOULD_NOT_USE_CONNECTION_IFRAME' ),
|
||||
'licensing' => array(
|
||||
'error' => Licensing::instance()->last_error(),
|
||||
'showLicensingUi' => Licensing::instance()->is_licensing_input_enabled(),
|
||||
'userCounts' => Licensing_Endpoints::get_user_license_counts(),
|
||||
'activationNoticeDismiss' => Licensing::instance()->get_license_activation_notice_dismiss(),
|
||||
),
|
||||
'jetpackManage' => array(
|
||||
'isEnabled' => Jetpack_Manage::could_use_jp_manage(),
|
||||
'isAgencyAccount' => Jetpack_Manage::is_agency_account(),
|
||||
),
|
||||
'hasSeenWCConnectionModal' => Jetpack_Options::get_option( 'has_seen_wc_connection_modal', false ),
|
||||
'newRecommendations' => Jetpack_Recommendations::get_new_conditional_recommendations(),
|
||||
// Check if WooCommerce plugin is active (based on https://docs.woocommerce.com/document/create-a-plugin/).
|
||||
'isWooCommerceActive' => in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', Jetpack::get_active_plugins() ), true ),
|
||||
'useMyJetpackLicensingUI' => My_Jetpack_Initializer::is_licensing_ui_enabled(),
|
||||
'isOdysseyStatsEnabled' => Stats_Options::get_option( 'enable_odyssey_stats' ),
|
||||
'shouldInitializeBlaze' => Blaze::should_initialize(),
|
||||
'isBlazeDashboardEnabled' => Blaze::is_dashboard_enabled(),
|
||||
'socialInitialState' => self::get_publicize_initial_state(),
|
||||
'isSubscriptionSiteEnabled' => apply_filters( 'jetpack_subscription_site_enabled', false ),
|
||||
'newsletterDateExample' => gmdate( get_option( 'date_format' ), time() ),
|
||||
'subscriptionSiteEditSupported' => $current_theme->is_block_theme(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the initial state for the Publicize module.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get_publicize_initial_state() {
|
||||
$jetpack_social_settings = new Automattic\Jetpack\Publicize\Jetpack_Social_Settings\Settings();
|
||||
|
||||
$initial_state = $jetpack_social_settings->get_initial_state();
|
||||
|
||||
if ( empty( $initial_state ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $initial_state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets array of any Jetpack notices that have been dismissed.
|
||||
*
|
||||
* @return mixed|void
|
||||
*/
|
||||
public static function get_dismissed_jetpack_notices() {
|
||||
$jetpack_dismissed_notices = get_option( 'jetpack_dismissed_notices', array() );
|
||||
/**
|
||||
* Array of notices that have been dismissed.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of modules and settings both as first class members of the object.
|
||||
*
|
||||
* @return array flattened settings with modules.
|
||||
*/
|
||||
public static function get_flattened_settings() {
|
||||
$core_api_endpoint = new Jetpack_Core_API_Data();
|
||||
$settings = $core_api_endpoint->get_all_options();
|
||||
return $settings->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the release post content and image data as an associative array.
|
||||
* This data is used to create the update modal.
|
||||
*/
|
||||
public static function get_update_modal_data() {
|
||||
$post_data = self::get_release_post_data();
|
||||
|
||||
if ( ! isset( $post_data['posts'][0] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = $post_data['posts'][0];
|
||||
|
||||
if ( empty( $post['content'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This allows us to embed videopress videos into the release post.
|
||||
add_filter( 'wp_kses_allowed_html', array( __CLASS__, 'allow_post_embed_iframe' ), 10, 2 );
|
||||
$content = wp_kses_post( $post['content'] );
|
||||
remove_filter( 'wp_kses_allowed_html', array( __CLASS__, 'allow_post_embed_iframe' ), 10 );
|
||||
|
||||
$post_title = isset( $post['title'] ) ? $post['title'] : null;
|
||||
$title = wp_kses( $post_title, array() );
|
||||
|
||||
$post_thumbnail = isset( $post['post_thumbnail'] ) ? $post['post_thumbnail'] : null;
|
||||
if ( ! empty( $post_thumbnail ) ) {
|
||||
$photon_image = new Image_CDN_Image(
|
||||
array(
|
||||
'file' => Image_CDN_Core::cdn_url( $post_thumbnail['URL'] ),
|
||||
'width' => $post_thumbnail['width'],
|
||||
'height' => $post_thumbnail['height'],
|
||||
),
|
||||
$post_thumbnail['mime_type']
|
||||
);
|
||||
$photon_image->resize(
|
||||
array(
|
||||
'width' => 600,
|
||||
'height' => null,
|
||||
'crop' => false,
|
||||
)
|
||||
);
|
||||
$post_thumbnail_url = $photon_image->get_raw_filename();
|
||||
} else {
|
||||
$post_thumbnail_url = null;
|
||||
}
|
||||
|
||||
$post_array = array(
|
||||
'release_post_content' => $content,
|
||||
'release_post_featured_image' => $post_thumbnail_url,
|
||||
'release_post_title' => $title,
|
||||
);
|
||||
|
||||
return $post_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily allow post content to contain iframes, e.g. for videopress.
|
||||
*
|
||||
* @param string $tags The tags.
|
||||
* @param string $context The context.
|
||||
*/
|
||||
public static function allow_post_embed_iframe( $tags, $context ) {
|
||||
if ( 'post' === $context ) {
|
||||
$tags['iframe'] = array(
|
||||
'src' => true,
|
||||
'height' => true,
|
||||
'width' => true,
|
||||
'frameborder' => true,
|
||||
'allowfullscreen' => true,
|
||||
);
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the release post from the Jetpack release post blog. A release post will be displayed in the
|
||||
* update modal when a post has a tag equal to the Jetpack version number.
|
||||
*
|
||||
* The response parameters for the post array can be found here:
|
||||
* https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/%24post_ID/#apidoc-response
|
||||
*
|
||||
* @return array|null Returns an associative array containing the release post data at index ['posts'][0].
|
||||
* Returns null if the release post data is not available.
|
||||
*/
|
||||
public static function get_release_post_data() {
|
||||
if ( Constants::is_defined( 'TESTING_IN_JETPACK' ) && Constants::get_constant( 'TESTING_IN_JETPACK' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$release_post_src = add_query_arg(
|
||||
array(
|
||||
'order_by' => 'date',
|
||||
'tag' => JETPACK__VERSION,
|
||||
'number' => '1',
|
||||
),
|
||||
'https://public-api.wordpress.com/rest/v1/sites/' . JETPACK__RELEASE_POST_BLOG_SLUG . '/posts'
|
||||
);
|
||||
|
||||
$response = wp_remote_get( $release_post_src );
|
||||
|
||||
if ( ! is_array( $response ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external services connect URLs.
|
||||
*/
|
||||
public static function get_external_services_connect_urls() {
|
||||
$connect_urls = array();
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.jetpack-keyring-service-helper.php';
|
||||
// phpcs:disable
|
||||
foreach ( Jetpack_Keyring_Service_Helper::SERVICES as $service_name => $service_info ) {
|
||||
// phpcs:enable
|
||||
$connect_urls[ $service_name ] = Jetpack_Keyring_Service_Helper::connect_url( $service_name, $service_info['for'] );
|
||||
}
|
||||
return $connect_urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a purchase token that is used for Jetpack logged out visitor checkout.
|
||||
* The purchase token should be appended to all CTA url's that lead to checkout.
|
||||
*
|
||||
* @since 9.8.0
|
||||
* @return string|boolean
|
||||
*/
|
||||
public static function get_purchase_token() {
|
||||
if ( ! Jetpack::current_user_can_purchase() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$purchase_token = Jetpack_Options::get_option( 'purchase_token', false );
|
||||
|
||||
if ( $purchase_token ) {
|
||||
return $purchase_token;
|
||||
}
|
||||
// If the purchase token is not saved in the options table yet, then add it.
|
||||
Jetpack_Options::update_option( 'purchase_token', self::generate_purchase_token(), true );
|
||||
return Jetpack_Options::get_option( 'purchase_token', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a purchase token that is used for Jetpack logged out visitor checkout.
|
||||
*
|
||||
* @since 9.8.0
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_purchase_token() {
|
||||
return wp_generate_password( 12, false );
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
|
||||
|
||||
/**
|
||||
* Gather data about the current user.
|
||||
*
|
||||
* @since 4.1.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function jetpack_current_user_data() {
|
||||
$jetpack_connection = new Connection_Manager( 'jetpack' );
|
||||
|
||||
$current_user = wp_get_current_user();
|
||||
$is_user_connected = $jetpack_connection->is_user_connected( $current_user->ID );
|
||||
$is_master_user = $is_user_connected && (int) $current_user->ID && (int) Jetpack_Options::get_option( 'master_user' ) === (int) $current_user->ID;
|
||||
$dotcom_data = $jetpack_connection->get_connected_user_data();
|
||||
|
||||
// Add connected user gravatar to the returned dotcom_data.
|
||||
// Probably we shouldn't do this when $dotcom_data is false, but we have been since 2016 so
|
||||
// clients probably expect that by now.
|
||||
if ( false === $dotcom_data ) {
|
||||
$dotcom_data = array();
|
||||
}
|
||||
$dotcom_data['avatar'] = ( ! empty( $dotcom_data['email'] ) ?
|
||||
get_avatar_url(
|
||||
$dotcom_data['email'],
|
||||
array(
|
||||
'size' => 64,
|
||||
'default' => 'mysteryman',
|
||||
)
|
||||
)
|
||||
: false );
|
||||
|
||||
$current_user_data = array(
|
||||
'isConnected' => $is_user_connected,
|
||||
'isMaster' => $is_master_user,
|
||||
'username' => $current_user->user_login,
|
||||
'displayName' => $current_user->display_name,
|
||||
'email' => $current_user->user_email,
|
||||
'id' => $current_user->ID,
|
||||
'wpcomUser' => $dotcom_data,
|
||||
'gravatar' => get_avatar_url( $current_user->ID ),
|
||||
'permissions' => array(
|
||||
'admin_page' => current_user_can( 'jetpack_admin_page' ),
|
||||
'connect' => current_user_can( 'jetpack_connect' ),
|
||||
'connect_user' => current_user_can( 'jetpack_connect_user' ),
|
||||
'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,402 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Main class file for Jetpack Admin pages.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
|
||||
use Automattic\Jetpack\Identity_Crisis;
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Status;
|
||||
|
||||
/**
|
||||
* Shared logic between Jetpack admin pages.
|
||||
*/
|
||||
abstract class Jetpack_Admin_Page {
|
||||
/**
|
||||
* Add page specific actions given the page hook.
|
||||
*
|
||||
* @param string $hook Hook of current page.
|
||||
*/
|
||||
abstract public function add_page_actions( $hook );
|
||||
|
||||
/**
|
||||
* Create a menu item for the page and returns the hook.
|
||||
*
|
||||
* @return string|false Return value from WordPress's `add_menu_page()` or `add_submenu_page()`.
|
||||
*/
|
||||
abstract public function get_page_hook();
|
||||
|
||||
/**
|
||||
* Enqueue and localize page specific scripts.
|
||||
*/
|
||||
abstract public function page_admin_scripts();
|
||||
|
||||
/**
|
||||
* Render page specific HTML
|
||||
*/
|
||||
abstract public function page_render();
|
||||
|
||||
/**
|
||||
* Function called after admin_styles to load any additional needed styles.
|
||||
*
|
||||
* @since 4.3.0
|
||||
*/
|
||||
public function additional_styles() {}
|
||||
|
||||
/**
|
||||
* Add common page actions and attach page-specific actions.
|
||||
*/
|
||||
public function add_actions() {
|
||||
$is_offline_mode = ( new Status() )->is_offline_mode();
|
||||
|
||||
// If user is not an admin and site is in Offline Mode or not connected yet then don't do anything.
|
||||
if ( ! current_user_can( 'manage_options' ) && ( $is_offline_mode || ! Jetpack::is_connection_ready() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is Jetpack not connected and not offline?
|
||||
// True means that Jetpack is NOT connected and NOT in offline mode.
|
||||
// If Jetpack is connected OR in offline mode, this will be false.
|
||||
$connectable = ! Jetpack::is_connection_ready() && ! $is_offline_mode;
|
||||
|
||||
// Don't add in the modules page unless modules are available!
|
||||
if ( $this->dont_show_if_not_active && $connectable ) {
|
||||
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_page_load' ) );
|
||||
add_action( "admin_print_styles-$hook", array( $this, 'admin_styles' ) );
|
||||
add_action( "admin_print_scripts-$hook", array( $this, 'admin_scripts' ) );
|
||||
add_action( "admin_print_styles-$hook", array( $this, 'additional_styles' ) );
|
||||
|
||||
// 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.
|
||||
*/
|
||||
public function render() {
|
||||
/** This action is documented in class.jetpack.php */
|
||||
do_action( 'jetpack_initialize_tracking' );
|
||||
|
||||
// We're in an IDC: we need a decision made before we show the UI again.
|
||||
if ( $this->block_page_rendering_for_idc() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we are looking at the main dashboard.
|
||||
if ( isset( $_GET['page'] ) && 'jetpack' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- View logic.
|
||||
$this->page_render();
|
||||
return;
|
||||
}
|
||||
|
||||
$args = array();
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( isset( $_GET['page'] ) && 'jetpack_modules' === $_GET['page'] ) {
|
||||
$args['is-wide'] = true;
|
||||
}
|
||||
|
||||
self::wrap_ui( array( $this, 'page_render' ), $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the existing admin page events.
|
||||
*/
|
||||
public function admin_page_load() {
|
||||
Jetpack::init()->admin_page_load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add page specific scripts and jetpack stats for all menu pages.
|
||||
*/
|
||||
public function admin_scripts() {
|
||||
$this->page_admin_scripts(); // Delegate to inheriting class.
|
||||
add_action( 'admin_footer', array( Jetpack::init(), 'do_stats' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the Jetpack admin stylesheet.
|
||||
*/
|
||||
public 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-connection' ), 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
|
||||
*/
|
||||
public 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_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 WP_Screen $page Current WP_Screen object.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function check_plan_deactivate_modules( $page ) {
|
||||
if (
|
||||
( new Status() )->is_offline_mode()
|
||||
|| ! in_array(
|
||||
$page->base,
|
||||
array(
|
||||
'toplevel_page_jetpack',
|
||||
'admin_page_jetpack_modules',
|
||||
'jetpack_page_vaultpress',
|
||||
'jetpack_page_stats',
|
||||
'jetpack_page_akismet-key-config',
|
||||
),
|
||||
true
|
||||
)
|
||||
) {
|
||||
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':
|
||||
case 'jetpack_personal':
|
||||
case 'jetpack_personal_monthly':
|
||||
$to_deactivate = array( 'google-analytics', 'wordads', 'search' );
|
||||
break;
|
||||
case 'jetpack_premium':
|
||||
case 'jetpack_premium_monthly':
|
||||
$to_deactivate = array( '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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue inline wrapper styles for the main container.
|
||||
*/
|
||||
public 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( 'wp-components' ), JETPACK__VERSION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build header, content, and footer for admin page.
|
||||
*
|
||||
* @param callable $callback Callback to produce the content of the page. The callback is responsible for any needed escaping.
|
||||
* @param array $args Options for the wrapping. Also passed to the `jetpack_admin_pages_wrap_ui_after_callback` action.
|
||||
* - is-wide: (bool) Set the "is-wide" class on the wrapper div, which increases the max width. Default false.
|
||||
* - show-nav: (bool) Whether to show the navigation bar at the top of the page. Default true.
|
||||
*/
|
||||
public static function wrap_ui( $callback, $args = array() ) {
|
||||
$defaults = array(
|
||||
'is-wide' => false,
|
||||
'show-nav' => true,
|
||||
);
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
// Is Jetpack not connected and not offline?
|
||||
// True means that Jetpack is NOT connected and NOT in offline mode.
|
||||
// If Jetpack is connected OR in offline mode, this will be false.
|
||||
$connectable = ! Jetpack::is_connection_ready() && ! ( new Status() )->is_offline_mode();
|
||||
|
||||
$jetpack_admin_url = admin_url( 'admin.php?page=jetpack' );
|
||||
$jetpack_about_url = ! $connectable
|
||||
? admin_url( 'admin.php?page=jetpack_about' )
|
||||
: Redirect::get_url( 'jetpack' );
|
||||
|
||||
$jetpack_privacy_url = ! $connectable
|
||||
? $jetpack_admin_url . '#/privacy'
|
||||
: Redirect::get_url( 'a8c-privacy' );
|
||||
|
||||
$external_link_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="jp-footer__menu-item__icon" aria-hidden="true" focusable="false"><path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"></path></svg>';
|
||||
|
||||
?>
|
||||
<div id="jp-plugin-container" class="
|
||||
<?php
|
||||
if ( $args['is-wide'] ) {
|
||||
echo 'is-wide'; }
|
||||
?>
|
||||
">
|
||||
|
||||
<div class="jp-masthead jp-masthead-middle">
|
||||
<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="40" viewBox="0 0 118 32"><path fill="#069e08" 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;// phpcs:ignore WordPress.Security.EscapeOutput -- Callback is responsible for any needed escaping.
|
||||
?>
|
||||
<!-- END OF CALLBACK -->
|
||||
|
||||
<div id="jp-stats-report-bottom">
|
||||
<div class="wrap">
|
||||
<?php
|
||||
/**
|
||||
* Fires at the bottom of the Jetpack admin page template, after the dynamic content section.
|
||||
*
|
||||
* @since 10.0.0
|
||||
*
|
||||
* @param string $callback The callback sent to the Jetpack_Admin_Page::wrap_ui method.
|
||||
* @param array $args The arguments sent to the Jetpack_Admin_Page::wrap_ui method.
|
||||
*/
|
||||
do_action( 'jetpack_admin_pages_wrap_ui_after_callback', $callback, $args );
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="jp-footer jp-footer--static">
|
||||
<div class="jp-footer__container">
|
||||
<div class="jp-footer__logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 32 32" class="jetpack-logo jp-footer__jetpack-symbol" aria-labelledby="jetpack-logo-title" height="16" aria-label="<?php esc_html_e( 'Jetpack logo', 'jetpack' ); ?>">
|
||||
<desc id="jetpack-logo-title">
|
||||
<?php esc_html_e( 'Jetpack Logo', 'jetpack' ); ?>
|
||||
</desc>
|
||||
<path fill="#000" 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>
|
||||
</svg>
|
||||
<span class="jp-footer__module-name">
|
||||
<a href="<?php echo esc_url( Redirect::get_url( 'jetpack' ) ); ?>" target="_blank" rel="noopener noreferrer" aria-label="Jetpack 12.2-a.0">
|
||||
<?php esc_html_e( 'Jetpack', 'jetpack' ); ?>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="jp-footer__menu">
|
||||
<a href="<?php echo esc_url( $jetpack_about_url ); ?>" title="<?php esc_attr__( 'About Jetpack', 'jetpack' ); ?>" class="jp-footer__menu-item">
|
||||
<?php echo esc_html__( 'About', 'jetpack' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( $jetpack_privacy_url ); ?>" rel="noopener noreferrer" title="<?php esc_html_e( "Automattic's Privacy Policy", 'jetpack' ); ?>" class="jp-footer__menu-item <?php echo ! $connectable ? 'is-external' : ''; ?> ?>" target="<?php echo ! $connectable ? '_blank' : '_self'; ?>">
|
||||
<?php echo esc_html_x( 'Privacy', 'Navigation item', 'jetpack' ); ?>
|
||||
<?php echo ! $connectable ? $external_link_icon : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( Redirect::get_url( 'wpcom-tos' ) ); ?>" target="_blank" rel="noopener noreferrer" title="<?php esc_html__( 'WordPress.com Terms of Service', 'jetpack' ); ?>" class="jp-footer__menu-item is-external">
|
||||
<?php echo esc_html_x( 'Terms', 'Navigation item', 'jetpack' ); ?>
|
||||
<?php echo $external_link_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( Redirect::get_url( 'jetpack' ) ); ?>" target="_blank" rel="noopener noreferrer" aria-label="Jetpack 12.2-a.0" class="jp-footer__menu-item is-external">
|
||||
Version <?php echo esc_html( JETPACK__VERSION ); ?>
|
||||
<?php echo $external_link_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<?php if ( is_multisite() && current_user_can( 'jetpack_network_sites_page' ) ) { ?>
|
||||
<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__menu-item"><?php echo esc_html_x( 'Network Sites', 'Navigation item', 'jetpack' ); ?></a>
|
||||
<?php } ?>
|
||||
<?php if ( is_multisite() && current_user_can( 'jetpack_network_settings_page' ) ) { ?>
|
||||
<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__menu-item"><?php echo esc_html_x( 'Network Settings', 'Navigation item', 'jetpack' ); ?></a>
|
||||
<?php } ?>
|
||||
<?php if ( current_user_can( 'manage_options' ) ) { ?>
|
||||
<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__menu-item"><?php echo esc_html_x( 'Modules', 'Navigation item', 'jetpack' ); ?></a>
|
||||
<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__menu-item"><?php echo esc_html_x( 'Debug', 'Navigation item', 'jetpack' ); ?></a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<a class="jp-footer__a8c-logo" href="<?php echo esc_url( $jetpack_about_url ); ?>" aria-label="<?php echo esc_attr__( 'An Automattic Airline', 'jetpack' ); ?>">
|
||||
<svg role="img" x="0" y="0" viewBox="0 0 935 38.2" enable-background="new 0 0 935 38.2" aria-labelledby="jp-automattic-byline-logo-title" height="7" class="jp-automattic-byline-logo">
|
||||
<desc id="jp-automattic-byline-logo-title">
|
||||
<?php echo esc_attr__( 'An Automattic Airline', 'jetpack' ); ?>
|
||||
</desc>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we block the page rendering because the site is in IDC?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function block_page_rendering_for_idc() {
|
||||
return Jetpack::is_connection_ready() && Identity_Crisis::validate_sync_error_idc_option() && ! Jetpack_Options::get_option( 'safe_mode_confirmed' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase
|
||||
/** This is intentionally left empty as a stub because some sites were caching the require()
|
||||
*
|
||||
* @see https://github.com/Automattic/jetpack/issues/5091
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
use Automattic\Jetpack\Admin_UI\Admin_Menu;
|
||||
use Automattic\Jetpack\Assets\Logo;
|
||||
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Status;
|
||||
|
||||
require_once __DIR__ . '/class.jetpack-admin-page.php';
|
||||
require_once __DIR__ . '/class-jetpack-redux-state-helper.php';
|
||||
|
||||
/**
|
||||
* Builds the landing page and its menu.
|
||||
*/
|
||||
class Jetpack_React_Page extends Jetpack_Admin_Page {
|
||||
/**
|
||||
* Show the landing page only when Jetpack is connected.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $dont_show_if_not_active = false;
|
||||
|
||||
/**
|
||||
* Used for fallback when REST API is disabled.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $is_redirecting = false;
|
||||
|
||||
/**
|
||||
* Add the main admin Jetpack menu.
|
||||
*
|
||||
* @return string|false Return value from WordPress's `add_menu_page()`.
|
||||
*/
|
||||
public function get_page_hook() {
|
||||
$icon = ( new Logo() )->get_base64_logo();
|
||||
return add_menu_page( 'Jetpack', 'Jetpack', 'jetpack_admin_page', 'jetpack', array( $this, 'render' ), $icon, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add page action.
|
||||
*
|
||||
* @param string $hook Hook of current page.
|
||||
* @return void
|
||||
*/
|
||||
public function add_page_actions( $hook ) {
|
||||
/** This action is documented in class.jetpack-admin.php */
|
||||
do_action( 'jetpack_admin_menu', $hook );
|
||||
|
||||
if ( ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return;
|
||||
}
|
||||
$page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( 'jetpack' !== $page ) {
|
||||
if ( strpos( $page, 'jetpack/' ) === 0 ) {
|
||||
$section = substr( $page, 8 );
|
||||
wp_safe_redirect( admin_url( 'admin.php?page=jetpack#/' . $section ) );
|
||||
exit( 0 );
|
||||
}
|
||||
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_connection_ready() && ! 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' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the main Jetpack submenu if a site is in offline mode or connected.
|
||||
* At that point, admins can access the Jetpack Dashboard instead.
|
||||
*
|
||||
* @since 13.8
|
||||
*/
|
||||
public function remove_jetpack_menu() {
|
||||
if (
|
||||
( new Status() )->is_offline_mode()
|
||||
|| Jetpack::is_connection_ready()
|
||||
) {
|
||||
remove_submenu_page( 'jetpack', 'jetpack' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function jetpack_add_dashboard_sub_nav_item() {
|
||||
if ( ( new Status() )->is_offline_mode() || Jetpack::is_connection_ready() ) {
|
||||
Admin_Menu::add_menu(
|
||||
__( 'Dashboard', 'jetpack' ),
|
||||
__( 'Dashboard', 'jetpack' ),
|
||||
'jetpack_admin_page',
|
||||
Jetpack::admin_url( array( 'page' => 'jetpack#/dashboard' ) ),
|
||||
null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- See https://core.trac.wordpress.org/ticket/52539.
|
||||
14
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a user can access the Jetpack Settings page.
|
||||
*
|
||||
* Rules are:
|
||||
* - user is allowed to see the Jetpack Admin
|
||||
* - site is connected or in offline mode
|
||||
* - non-admins only need access to the settings when there are modules they can manage.
|
||||
*
|
||||
* @return bool $can_access_settings Can the user access settings.
|
||||
*/
|
||||
private function can_access_settings() {
|
||||
$connection = new Connection_Manager( 'jetpack' );
|
||||
$status = new Status();
|
||||
|
||||
// User must have the necessary permissions to see the Jetpack settings pages.
|
||||
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In offline mode, allow access to admins.
|
||||
if ( $status->is_offline_mode() && current_user_can( 'manage_options' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not in offline mode but site is not connected, bail.
|
||||
if ( ! Jetpack::is_connection_ready() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Additional checks for non-admins.
|
||||
*/
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
// If the site isn't connected at all, bail.
|
||||
if ( ! $connection->has_connected_owner() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* If they haven't connected their own account yet,
|
||||
* they have no use for the settings page.
|
||||
* They will not be able to manage any settings.
|
||||
*/
|
||||
if ( ! $connection->is_user_connected() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Non-admins only have access to settings
|
||||
* for the following modules:
|
||||
* - Publicize
|
||||
* - Post By Email
|
||||
* If those modules are not available, bail.
|
||||
*/
|
||||
if (
|
||||
! Jetpack::is_module_active( 'post-by-email' )
|
||||
&& ! Jetpack::is_module_active( 'publicize' )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jetpack Settings sub-link.
|
||||
*
|
||||
* @since 4.3.0
|
||||
* @since 9.7.0 If Connection does not have an owner, restrict it to admins
|
||||
*/
|
||||
public function jetpack_add_settings_sub_nav_item() {
|
||||
if ( $this->can_access_settings() ) {
|
||||
Admin_Menu::add_menu(
|
||||
__( 'Settings', 'jetpack' ),
|
||||
__( 'Settings', 'jetpack' ),
|
||||
'jetpack_admin_page',
|
||||
Jetpack::admin_url( array( 'page' => 'jetpack#/settings' ) ),
|
||||
null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- See https://core.trac.wordpress.org/ticket/52539.
|
||||
13
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback redirect meta tag if the REST API is disabled.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_fallback_head_meta() {
|
||||
echo '<meta http-equiv="refresh" content="0; url=?page=jetpack_modules">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback meta tag wrapped in noscript tags for all browsers in case they have JavaScript disabled.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_noscript_head_meta() {
|
||||
echo '<noscript>';
|
||||
$this->add_fallback_head_meta();
|
||||
echo '</noscript>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add action to render page specific HTML.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function page_render() {
|
||||
/** This action is already documented in class.jetpack-admin-page.php */
|
||||
do_action( 'jetpack_notices' );
|
||||
|
||||
// Fetch static.html.
|
||||
$static_html = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static.html' ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, Not fetching a remote file.
|
||||
|
||||
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>pnpm run distclean && pnpm jetpack build plugins/jetpack</code>';
|
||||
echo '</p>';
|
||||
} else {
|
||||
// We got the static.html so let's display it.
|
||||
echo $static_html; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Allow robust deep links to React.
|
||||
*
|
||||
* The Jetpack dashboard requires fragments/hash values to make
|
||||
* a deep link to it but passing fragments as part of a return URL
|
||||
* will most often be discarded throughout the process.
|
||||
* This logic aims to bridge this gap and reduce the chance of React
|
||||
* specific links being broken while passing them along.
|
||||
*/
|
||||
public function react_redirects() {
|
||||
global $pagenow;
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( 'admin.php' !== $pagenow || ! isset( $_GET['jp-react-redirect'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allowed_paths = array(
|
||||
'product-purchased' => admin_url( '/admin.php?page=jetpack#/recommendations/product-purchased' ),
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$target = sanitize_text_field( wp_unslash( $_GET['jp-react-redirect'] ) );
|
||||
if ( isset( $allowed_paths[ $target ] ) ) {
|
||||
wp_safe_redirect( $allowed_paths[ $target ] );
|
||||
exit( 0 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load styles for static page.
|
||||
*/
|
||||
public function additional_styles() {
|
||||
Jetpack_Admin_Page::load_wrapper_styles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load admin page scripts.
|
||||
*/
|
||||
public function page_admin_scripts() {
|
||||
if ( $this->is_redirecting ) {
|
||||
return; // No need for scripts on a fallback page.
|
||||
}
|
||||
|
||||
$status = new Status();
|
||||
$is_offline_mode = $status->is_offline_mode();
|
||||
$site_suffix = $status->get_site_suffix();
|
||||
$script_deps_path = JETPACK__PLUGIN_DIR . '_inc/build/admin.asset.php';
|
||||
$script_dependencies = array( 'jquery', 'wp-polyfill' );
|
||||
$version = JETPACK__VERSION;
|
||||
if ( file_exists( $script_deps_path ) ) {
|
||||
$asset_manifest = include $script_deps_path;
|
||||
$script_dependencies = $asset_manifest['dependencies'];
|
||||
$version = $asset_manifest['version'];
|
||||
}
|
||||
|
||||
$blog_id_prop = '';
|
||||
if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
|
||||
$blog_id = Connection_Manager::get_site_id( true );
|
||||
if ( $blog_id ) {
|
||||
$blog_id_prop = ', currentBlogID: "' . (int) $blog_id . '"';
|
||||
}
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'react-plugin',
|
||||
plugins_url( '_inc/build/admin.js', JETPACK__PLUGIN_FILE ),
|
||||
$script_dependencies,
|
||||
$version,
|
||||
true
|
||||
);
|
||||
|
||||
if ( ! $is_offline_mode && Jetpack::is_connection_ready() ) {
|
||||
// Required for Analytics.
|
||||
wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
|
||||
}
|
||||
|
||||
wp_set_script_translations( 'react-plugin', 'jetpack' );
|
||||
|
||||
// Add objects to be passed to the initial state of the app.
|
||||
// Use wp_add_inline_script instead of wp_localize_script, see https://core.trac.wordpress.org/ticket/25280.
|
||||
wp_add_inline_script( 'react-plugin', 'var Initial_State=JSON.parse(decodeURIComponent("' . rawurlencode( wp_json_encode( Jetpack_Redux_State_Helper::get_initial_state() ) ) . '"));', 'before' );
|
||||
|
||||
// This will set the default URL of the jp_redirects lib.
|
||||
wp_add_inline_script( 'react-plugin', 'var jetpack_redirects = { currentSiteRawUrl: "' . $site_suffix . '"' . $blog_id_prop . ' };', 'before' );
|
||||
|
||||
// Adds Connection package initial state.
|
||||
Connection_Initial_State::render_script( 'react-plugin' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
use Automattic\Jetpack\Assets;
|
||||
use Automattic\Jetpack\Tracking;
|
||||
|
||||
require_once __DIR__ . '/class.jetpack-admin-page.php';
|
||||
require_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.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $dont_show_if_not_active = true;
|
||||
|
||||
/**
|
||||
* Add page action.
|
||||
*
|
||||
* @param string $hook Hook of current page.
|
||||
* @return void
|
||||
*/
|
||||
public function add_page_actions( $hook ) {} //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
/**
|
||||
* Adds the Settings sub menu.
|
||||
*/
|
||||
public function get_page_hook() {
|
||||
return add_submenu_page(
|
||||
'',
|
||||
__( '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
|
||||
*/
|
||||
public 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' ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, Not fetching a remote file.
|
||||
$rest_api_notice = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static-version-notice.html' ); //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, Not fetching a remote file.
|
||||
|
||||
$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; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
echo $noscript_notice; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
?>
|
||||
|
||||
<div class="jetpack-module-list">
|
||||
<div class="wrap">
|
||||
<div class="manage-left jp-static-block">
|
||||
<table class="table table-bordered fixed-top jetpack-modules">
|
||||
<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>
|
||||
<form class="jetpack-modules-list-table-form" onsubmit="return false;">
|
||||
<table class="<?php echo esc_attr( implode( ' ', $list_table->get_table_classes() ) ); ?>">
|
||||
<tbody id="the-list">
|
||||
<?php $list_table->display_rows_or_placeholder(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<div class="manage-right">
|
||||
<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>
|
||||
<span class="dops-button-group button-group filter-active">
|
||||
<button type="button" class="dops-button is-compact button
|
||||
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is view logic.
|
||||
if ( empty( $_GET['activated'] ) ) {
|
||||
echo 'active';
|
||||
}
|
||||
?>
|
||||
">
|
||||
<?php esc_html_e( 'All', 'jetpack' ); ?></button>
|
||||
<button type="button" class="dops-button button
|
||||
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is view logic.
|
||||
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="dops-button button
|
||||
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is view logic.
|
||||
if ( ! empty( $_GET['activated'] ) && 'false' === $_GET['activated'] ) {
|
||||
echo 'active';
|
||||
}
|
||||
?>
|
||||
" data-filter-by="activated" data-filter-value="false"><?php esc_html_e( 'Inactive', 'jetpack' ); ?></button>
|
||||
</span>
|
||||
<p><?php esc_html_e( 'Sort by', 'jetpack' ); ?></p>
|
||||
<span class="dops-button-group button-group sort">
|
||||
<button type="button" class="dops-button button
|
||||
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is view logic.
|
||||
if ( empty( $_GET['sort_by'] ) ) {
|
||||
echo 'active';
|
||||
}
|
||||
?>
|
||||
" data-sort-by="name"><?php esc_html_e( 'Alphabetical', 'jetpack' ); ?></button>
|
||||
<button type="button" class="dops-button button
|
||||
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is view logic.
|
||||
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="dops-button button
|
||||
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is view logic.
|
||||
if ( ! empty( $_GET['sort_by'] ) && 'sort' === $_GET['sort_by'] ) {
|
||||
echo 'active';
|
||||
}
|
||||
?>
|
||||
" data-sort-by="sort"><?php esc_html_e( 'Popular', 'jetpack' ); ?></button>
|
||||
</span>
|
||||
<p><?php esc_html_e( 'Show', 'jetpack' ); ?></p>
|
||||
<?php $list_table->views(); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
*/
|
||||
public function additional_styles() {
|
||||
Jetpack_Admin_Page::load_wrapper_styles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Javascript logic specific to the list table
|
||||
*/
|
||||
public 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,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
<?php
|
||||
/**
|
||||
* API helper for the AI blocks.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 11.8
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\Jetpack\Search\Plan as Search_Plan;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Automattic\Jetpack\Status\Visitor;
|
||||
|
||||
/**
|
||||
* Class Jetpack_AI_Helper
|
||||
*
|
||||
* @since 11.8
|
||||
*/
|
||||
class Jetpack_AI_Helper {
|
||||
/**
|
||||
* Allow new completion every X seconds. Will return cached result otherwise.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $text_completion_cooldown_seconds = 15;
|
||||
|
||||
/**
|
||||
* Cache images for a prompt for a month.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $image_generation_cache_timeout = MONTH_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Cache AI-assistant feature for 60 seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $ai_assistant_feature_cache_timeout = 60;
|
||||
|
||||
/**
|
||||
* Cache AI-assistant errors for ten seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $ai_assistant_feature_error_cache_timeout = 10;
|
||||
|
||||
/**
|
||||
* Stores the number of JetpackAI calls in case we want to mark AI-assisted posts some way.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $post_meta_with_ai_generation_number = '_jetpack_ai_calls';
|
||||
|
||||
/**
|
||||
* Storing the error to prevent repeated requests to WPCOM after failure.
|
||||
*
|
||||
* @var null|WP_Error
|
||||
*/
|
||||
private static $ai_assistant_failed_request = null;
|
||||
|
||||
/**
|
||||
* Checks if a given request is allowed to get AI data from WordPress.com.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has access, WP_Error object otherwise.
|
||||
*/
|
||||
public static function get_status_permission_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
/*
|
||||
* This may need to be updated
|
||||
* to take into account the different ways we can make requests
|
||||
* (from a WordPress.com site, from a Jetpack site).
|
||||
*/
|
||||
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__( 'Sorry, you are not allowed to access Jetpack AI help on this site.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if these features should be active on the current site.
|
||||
* Currently, it's limited to WPCOM Simple and Atomic.
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
$default = false;
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$default = true;
|
||||
} elseif ( ( new Automattic\Jetpack\Status\Host() )->is_woa_site() ) {
|
||||
$default = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter whether the AI features are enabled in the Jetpack plugin.
|
||||
*
|
||||
* @since 11.8
|
||||
*
|
||||
* @param bool $default Are AI features enabled? Defaults to false.
|
||||
*/
|
||||
return apply_filters( 'jetpack_ai_enabled', $default );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the AI chat feature should be active on the current site.
|
||||
*
|
||||
* @todo IS_WPCOM (the endpoints need to be updated too).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_ai_chat_enabled() {
|
||||
$default = false;
|
||||
|
||||
$connection = new Manager();
|
||||
$plan = new Search_Plan();
|
||||
if ( $connection->is_connected() && $plan->supports_search() ) {
|
||||
$default = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter whether the AI chat feature is enabled in the Jetpack plugin.
|
||||
*
|
||||
* @since 12.6
|
||||
*
|
||||
* @param bool $default Is AI chat enabled? Defaults to false.
|
||||
*/
|
||||
return apply_filters( 'jetpack_ai_chat_enabled', $default );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the transient for image generation. Unique per prompt and allows for reuse of results for the same prompt across entire WPCOM.
|
||||
* I expext "puppy" to always be from cache.
|
||||
*
|
||||
* @param string $prompt - Supplied prompt.
|
||||
*/
|
||||
public static function transient_name_for_image_generation( $prompt ) {
|
||||
return 'jetpack_openai_image_' . md5( $prompt );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the transient for text completion. Unique per user, but not per text. Serves more as a cooldown.
|
||||
*/
|
||||
public static function transient_name_for_completion() {
|
||||
return 'jetpack_openai_completion_' . get_current_user_id(); // Cache for each user, so that other users dont get weird cached version from somebody else.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the transient for AI assistance feature. Unique per user.
|
||||
*
|
||||
* @param int $blog_id - Blog ID to get the transient name for.
|
||||
* @return string
|
||||
*/
|
||||
public static function transient_name_for_ai_assistance_feature( $blog_id ) {
|
||||
return 'jetpack_openai_ai_assistance_feature_' . $blog_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the edited post as "touched" by AI stuff.
|
||||
*
|
||||
* @param int $post_id Post ID for which the content is being generated.
|
||||
* @return void
|
||||
*/
|
||||
private static function mark_post_as_ai_assisted( $post_id ) {
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
$previous = get_post_meta( $post_id, self::$post_meta_with_ai_generation_number, true );
|
||||
if ( ! $previous ) {
|
||||
$previous = 0;
|
||||
} elseif ( ! is_numeric( $previous ) ) {
|
||||
// Data corrupted, nothing to do.
|
||||
return;
|
||||
}
|
||||
$new_value = intval( $previous ) + 1;
|
||||
update_post_meta( $post_id, self::$post_meta_with_ai_generation_number, $new_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text back from WordPress.com based off a starting text.
|
||||
*
|
||||
* @param string $content The content provided to send to the AI.
|
||||
* @param int $post_id Post ID for which the content is being generated.
|
||||
* @param bool $skip_cache Skip cache and force a new request.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_gpt_completion( $content, $post_id, $skip_cache = false ) {
|
||||
$content = wp_strip_all_tags( $content );
|
||||
$cache = get_transient( self::transient_name_for_completion() );
|
||||
if ( $cache && ! $skip_cache ) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
if ( ( new Status() )->is_offline_mode() ) {
|
||||
return new WP_Error(
|
||||
'dev_mode',
|
||||
__( 'Jetpack AI is not available in offline mode.', 'jetpack' )
|
||||
);
|
||||
}
|
||||
|
||||
$site_id = Manager::get_site_id();
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $site_id;
|
||||
}
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
if ( ! class_exists( 'OpenAI' ) ) {
|
||||
\require_lib( 'openai' );
|
||||
}
|
||||
|
||||
// Set the content for chatGPT endpoint
|
||||
$data = array(
|
||||
array(
|
||||
'role' => 'user',
|
||||
'content' => $content,
|
||||
),
|
||||
);
|
||||
|
||||
$openai = new OpenAI( 'openai', array( 'post_id' => $post_id ) );
|
||||
$moderation_result = $openai->moderate(
|
||||
implode(
|
||||
' ',
|
||||
array_map(
|
||||
function ( $msg ) {
|
||||
return $msg['role'] === 'user' ? $msg['content'] : '';
|
||||
},
|
||||
$data
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $moderation_result ) ) {
|
||||
return $moderation_result;
|
||||
}
|
||||
|
||||
$max_tokens = 480; // Default
|
||||
$result = $openai->request_chat_completion( $data, $max_tokens );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$response = $result->choices[0]->message->content;
|
||||
|
||||
// In case of Jetpack we are setting a transient on the WPCOM and not the remote site. I think the 'get_current_user_id' may default for the connection owner at this point but we'll deal with this later.
|
||||
set_transient( self::transient_name_for_completion(), $response, self::$text_completion_cooldown_seconds );
|
||||
self::mark_post_as_ai_assisted( $post_id );
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
sprintf( '/sites/%d/jetpack-ai/completions', $site_id ),
|
||||
2,
|
||||
array(
|
||||
'method' => 'post',
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
),
|
||||
wp_json_encode(
|
||||
array(
|
||||
'content' => $content,
|
||||
)
|
||||
),
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
if ( wp_remote_retrieve_response_code( $response ) >= 400 ) {
|
||||
return new WP_Error( $data->code, $data->message, $data->data );
|
||||
}
|
||||
|
||||
// Do not cache if it should be skipped.
|
||||
if ( ! $skip_cache ) {
|
||||
set_transient( self::transient_name_for_completion(), $data, self::$text_completion_cooldown_seconds );
|
||||
}
|
||||
self::mark_post_as_ai_assisted( $post_id );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of image objects back from WordPress.com based off a prompt.
|
||||
*
|
||||
* @param string $prompt The prompt to generate images for.
|
||||
* @param int $post_id Post ID for which the content is being generated.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_dalle_generation( $prompt, $post_id ) {
|
||||
$cache = get_transient( self::transient_name_for_image_generation( $prompt ) );
|
||||
if ( $cache ) {
|
||||
self::mark_post_as_ai_assisted( $post_id );
|
||||
return $cache;
|
||||
}
|
||||
|
||||
if ( ( new Status() )->is_offline_mode() ) {
|
||||
return new WP_Error(
|
||||
'dev_mode',
|
||||
__( 'Jetpack AI is not available in offline mode.', 'jetpack' )
|
||||
);
|
||||
}
|
||||
|
||||
$site_id = Manager::get_site_id();
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $site_id;
|
||||
}
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
if ( ! class_exists( 'OpenAI' ) ) {
|
||||
\require_lib( 'openai' );
|
||||
}
|
||||
|
||||
$result = ( new OpenAI( 'openai', array( 'post_id' => $post_id ) ) )->request_dalle_generation( $prompt );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
set_transient( self::transient_name_for_image_generation( $prompt ), $result, self::$image_generation_cache_timeout );
|
||||
self::mark_post_as_ai_assisted( $post_id );
|
||||
return $result;
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
sprintf( '/sites/%d/jetpack-ai/images/generations', $site_id ),
|
||||
2,
|
||||
array(
|
||||
'method' => 'post',
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
),
|
||||
wp_json_encode(
|
||||
array(
|
||||
'prompt' => $prompt,
|
||||
)
|
||||
),
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
if ( wp_remote_retrieve_response_code( $response ) >= 400 ) {
|
||||
return new WP_Error( $data->code, $data->message, $data->data );
|
||||
}
|
||||
set_transient( self::transient_name_for_image_generation( $prompt ), $data, self::$image_generation_cache_timeout );
|
||||
self::mark_post_as_ai_assisted( $post_id );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object with useful data about the requests made to the AI.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_ai_assistance_feature() {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
// On WPCOM, we can get the ID from the site.
|
||||
$blog_id = get_current_blog_id();
|
||||
$has_ai_assistant_feature = \wpcom_site_has_feature( 'ai-assistant', $blog_id );
|
||||
|
||||
if ( ! class_exists( 'WPCOM\Jetpack_AI\Usage\Helper' ) ) {
|
||||
if ( is_readable( WP_CONTENT_DIR . '/lib/jetpack-ai/usage/helper.php' ) ) {
|
||||
require_once WP_CONTENT_DIR . '/lib/jetpack-ai/usage/helper.php';
|
||||
} else {
|
||||
return new WP_Error(
|
||||
'jetpack_ai_usage_helper_not_found',
|
||||
__( 'WPCOM\Jetpack_AI\Usage\Helper class not found.', 'jetpack' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WPCOM\Jetpack_AI\Feature_Control' ) ) {
|
||||
if ( is_readable( WP_CONTENT_DIR . '/lib/jetpack-ai/feature-control.php' ) ) {
|
||||
require_once WP_CONTENT_DIR . '/lib/jetpack-ai/feature-control.php';
|
||||
} else {
|
||||
return new WP_Error(
|
||||
'jetpack_ai_feature_control_not_found',
|
||||
__( 'WPCOM\Jetpack_AI\Feature_Control class not found.', 'jetpack' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the upgrade type
|
||||
$upgrade_type = wpcom_is_vip( $blog_id ) ? 'vip' : 'default';
|
||||
|
||||
return array(
|
||||
'has-feature' => $has_ai_assistant_feature,
|
||||
'is-over-limit' => WPCOM\Jetpack_AI\Usage\Helper::is_over_limit( $blog_id ),
|
||||
'requests-count' => WPCOM\Jetpack_AI\Usage\Helper::get_all_time_requests_count( $blog_id ),
|
||||
'requests-limit' => WPCOM\Jetpack_AI\Usage\Helper::get_free_requests_limit( $blog_id ),
|
||||
'usage-period' => WPCOM\Jetpack_AI\Usage\Helper::get_period_data( $blog_id ),
|
||||
'site-require-upgrade' => WPCOM\Jetpack_AI\Usage\Helper::site_requires_upgrade( $blog_id ),
|
||||
'upgrade-type' => $upgrade_type,
|
||||
'upgrade-url' => WPCOM\Jetpack_AI\Usage\Helper::get_upgrade_url( $blog_id ),
|
||||
'current-tier' => WPCOM\Jetpack_AI\Usage\Helper::get_current_tier( $blog_id ),
|
||||
'next-tier' => WPCOM\Jetpack_AI\Usage\Helper::get_next_tier( $blog_id ),
|
||||
'tier-plans' => WPCOM\Jetpack_AI\Usage\Helper::get_tier_plans_list(),
|
||||
'tier-plans-enabled' => WPCOM\Jetpack_AI\Usage\Helper::ai_tier_plans_enabled(),
|
||||
'costs' => WPCOM\Jetpack_AI\Usage\Helper::get_costs(),
|
||||
'features-control' => WPCOM\Jetpack_AI\Feature_Control::get_features(),
|
||||
);
|
||||
}
|
||||
|
||||
// Outside of WPCOM, we need to fetch the data from the site.
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
|
||||
// Try to pick the AI Assistant feature from cache.
|
||||
$transient_name = self::transient_name_for_ai_assistance_feature( $blog_id );
|
||||
$cache = get_transient( $transient_name );
|
||||
if ( $cache ) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
if ( null !== static::$ai_assistant_failed_request ) {
|
||||
return static::$ai_assistant_failed_request;
|
||||
}
|
||||
|
||||
$request_path = sprintf( '/sites/%d/jetpack-ai/ai-assistant-feature', $blog_id );
|
||||
|
||||
$wpcom_request = Client::wpcom_json_api_request_as_user(
|
||||
$request_path,
|
||||
'v2',
|
||||
array(
|
||||
'method' => 'GET',
|
||||
'headers' => array(
|
||||
'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
|
||||
),
|
||||
'timeout' => 30,
|
||||
),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $wpcom_request );
|
||||
if ( 200 === $response_code ) {
|
||||
$ai_assistant_feature_data = json_decode( wp_remote_retrieve_body( $wpcom_request ), true );
|
||||
|
||||
// Cache the AI Assistant feature, for Jetpack sites.
|
||||
set_transient( $transient_name, $ai_assistant_feature_data, self::$ai_assistant_feature_cache_timeout );
|
||||
|
||||
return $ai_assistant_feature_data;
|
||||
} else {
|
||||
$error = new WP_Error(
|
||||
'failed_to_fetch_data',
|
||||
esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
|
||||
array(
|
||||
'status' => $response_code,
|
||||
'ts' => time(),
|
||||
)
|
||||
);
|
||||
|
||||
// Cache the AI Assistant feature error, for Jetpack sites, avoid API hammering.
|
||||
set_transient( $transient_name, $error, self::$ai_assistant_feature_error_cache_timeout );
|
||||
|
||||
static::$ai_assistant_failed_request = $error;
|
||||
|
||||
return $error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack_Currencies: Utils for displaying and managing currencies.
|
||||
*
|
||||
* @package Jetpack
|
||||
* @since 9.1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* General currencies specific functionality
|
||||
*/
|
||||
class Jetpack_Currencies {
|
||||
/**
|
||||
* Currencies definition
|
||||
*/
|
||||
const CURRENCIES = array(
|
||||
'USD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'GBP' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '£',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'JPY' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '¥',
|
||||
'decimal' => 0,
|
||||
),
|
||||
'BRL' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'R$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'EUR' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '€',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'NZD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'NZ$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'AUD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'A$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'CAD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'C$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'ILS' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '₪',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'RUB' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '₽',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'MXN' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'MX$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'MYR' => array(
|
||||
'format' => '%2$s%1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'RM',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'SEK' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Skr',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'HUF' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Ft',
|
||||
'decimal' => 0, // Decimals are supported by Stripe but not by PayPal.
|
||||
),
|
||||
'CHF' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'CHF',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'CZK' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Kč',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'DKK' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Dkr',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'HKD' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'HK$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'NOK' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Kr',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'PHP' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '₱',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'PLN' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'PLN',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'SGD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'S$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'TWD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'NT$',
|
||||
'decimal' => 0, // Decimals are supported by Stripe but not by PayPal.
|
||||
),
|
||||
'THB' => array(
|
||||
'format' => '%2$s%1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '฿',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'INR' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '₹',
|
||||
'decimal' => 0,
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Format a price with currency.
|
||||
*
|
||||
* Uses currency-aware formatting to output a formatted price with a simple fallback.
|
||||
*
|
||||
* Largely inspired by WordPress.com's Store_Price::display_currency
|
||||
*
|
||||
* @param string $price Price.
|
||||
* @param string $currency Currency.
|
||||
* @param bool $symbol Whether to display the currency symbol.
|
||||
* @return string Formatted price.
|
||||
*/
|
||||
public static function format_price( $price, $currency, $symbol = true ) {
|
||||
// Add some basic formatting for the price.
|
||||
$formatted_number = new NumberFormatter( get_locale(), NumberFormatter::DECIMAL );
|
||||
$price = (float) $formatted_number->parse( $price );
|
||||
|
||||
// Fall back to unspecified currency symbol like `¤1,234.05`.
|
||||
// @link https://en.wikipedia.org/wiki/Currency_sign_(typography).
|
||||
if ( ! array_key_exists( $currency, self::CURRENCIES ) ) {
|
||||
return ( $symbol ? '¤' : '' ) . number_format_i18n( $price, 2 );
|
||||
}
|
||||
|
||||
$currency_details = self::CURRENCIES[ $currency ];
|
||||
|
||||
// Ensure USD displays as 1234.56 even in non-US locales.
|
||||
$amount = 'USD' === $currency
|
||||
? number_format( $price, $currency_details['decimal'], '.', ',' )
|
||||
: number_format_i18n( $price, $currency_details['decimal'] );
|
||||
|
||||
return sprintf(
|
||||
$currency_details['format'],
|
||||
$symbol ? $currency_details['symbol'] : '',
|
||||
$amount
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Instagram Gallery block and API helper.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Instagram_Gallery_Helper
|
||||
*/
|
||||
class Jetpack_Instagram_Gallery_Helper {
|
||||
const TRANSIENT_KEY_PREFIX = 'jetpack_instagram_gallery_block_';
|
||||
|
||||
/**
|
||||
* Check whether an Instagram access token is valid,
|
||||
* or has been permanently deleted elsewhere.
|
||||
*
|
||||
* @param string $access_token_id The ID of the external access token for Instagram.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_instagram_access_token_valid( $access_token_id ) {
|
||||
$site_id = Manager::get_site_id();
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
if ( ! class_exists( 'WPCOM_Instagram_Gallery_Helper' ) ) {
|
||||
\require_lib( 'instagram-gallery-helper' );
|
||||
}
|
||||
$token = WPCOM_Instagram_Gallery_Helper::get_token( $access_token_id );
|
||||
return ! is_wp_error( $token );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/sites/%d/instagram/%d/check-token', $site_id, $access_token_id ),
|
||||
2,
|
||||
array( 'headers' => array( 'content-type' => 'application/json' ) ),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
return 200 === wp_remote_retrieve_response_code( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Instagram Gallery.
|
||||
*
|
||||
* @param string $access_token_id The ID of the external access token for Instagram.
|
||||
* @param int $count The number of Instagram posts to fetch.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_instagram_gallery( $access_token_id, $count ) {
|
||||
$site_id = Manager::get_site_id();
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $site_id;
|
||||
}
|
||||
|
||||
$transient_key = self::TRANSIENT_KEY_PREFIX . $access_token_id;
|
||||
|
||||
// Check if the connection exists before trying to retrieve the cached gallery.
|
||||
if ( ! self::is_instagram_access_token_valid( $access_token_id ) ) {
|
||||
delete_transient( $transient_key );
|
||||
return new WP_Error(
|
||||
'instagram_connection_unavailable',
|
||||
__( 'The requested Instagram connection is not available anymore.', 'jetpack' ),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
$cached_gallery = get_transient( $transient_key );
|
||||
if ( $cached_gallery ) {
|
||||
$decoded_cached_gallery = json_decode( $cached_gallery );
|
||||
// `images` can be an array of images or a string 'ERROR'.
|
||||
$cached_count = is_array( $decoded_cached_gallery->images ) ? count( $decoded_cached_gallery->images ) : 0;
|
||||
if ( $cached_count >= $count ) {
|
||||
return $decoded_cached_gallery;
|
||||
}
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/sites/%d/instagram/%d?count=%d', $site_id, $access_token_id, $count ),
|
||||
2,
|
||||
array( 'headers' => array( 'content-type' => 'application/json' ) ),
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$gallery = wp_remote_retrieve_body( $response );
|
||||
set_transient( $transient_key, $gallery, HOUR_IN_SECONDS );
|
||||
return json_decode( $gallery );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* Mapbox API helper.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Mapbox_Helper
|
||||
*/
|
||||
class Jetpack_Mapbox_Helper {
|
||||
/**
|
||||
* Site option key for the Mapbox service.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $site_option_key = 'mapbox_api_key';
|
||||
|
||||
/**
|
||||
* Transient key for the WordPress.com Mapbox access token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $transient_key = 'wpcom_mapbox_access_token';
|
||||
|
||||
/**
|
||||
* Get the site's own Mapbox access token if set, or the WordPress.com's one otherwise.
|
||||
*
|
||||
* @return array An array containing the key (if any) and its source ("site" or "wpcom").
|
||||
*/
|
||||
public static function get_access_token() {
|
||||
// If the site provides its own Mapbox access token, return it.
|
||||
$service_api_key = Jetpack_Options::get_option( self::$site_option_key );
|
||||
if ( $service_api_key ) {
|
||||
return self::format_access_token( $service_api_key );
|
||||
}
|
||||
|
||||
$site_id = self::get_wpcom_site_id();
|
||||
|
||||
// If on WordPress.com, try to return the access token straight away.
|
||||
if ( self::is_wpcom() && defined( 'WPCOM_MAPBOX_ACCESS_TOKEN' ) ) {
|
||||
require_lib( 'mapbox-blocklist' );
|
||||
return wpcom_is_site_blocked_from_map_block( $site_id )
|
||||
? self::format_access_token()
|
||||
: self::format_access_token( WPCOM_MAPBOX_ACCESS_TOKEN, 'wpcom' );
|
||||
}
|
||||
|
||||
// If not on WordPress.com or Atomic, return an empty access token.
|
||||
if ( ! $site_id || ( ! self::is_wpcom() && ! ( new Host() )->is_woa_site() ) ) {
|
||||
return self::format_access_token();
|
||||
}
|
||||
|
||||
// If there is a cached token, return it.
|
||||
$cached_token = get_transient( self::$transient_key );
|
||||
if ( $cached_token ) {
|
||||
return self::format_access_token( $cached_token, 'wpcom' );
|
||||
}
|
||||
|
||||
// Otherwise get it from the WordPress.com endpoint.
|
||||
$request_url = 'https://public-api.wordpress.com/wpcom/v2/sites/' . $site_id . '/mapbox';
|
||||
$response = wp_remote_get( esc_url_raw( $request_url ) );
|
||||
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
return self::format_access_token();
|
||||
}
|
||||
|
||||
$response_body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
$wpcom_mapbox_access_token = $response_body->wpcom_mapbox_access_token;
|
||||
|
||||
set_transient( self::$transient_key, $wpcom_mapbox_access_token, HOUR_IN_SECONDS );
|
||||
return self::format_access_token( $wpcom_mapbox_access_token, 'wpcom' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're in WordPress.com.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_wpcom() {
|
||||
return defined( 'IS_WPCOM' ) && IS_WPCOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current site's WordPress.com ID.
|
||||
*
|
||||
* @return mixed The site's WordPress.com ID.
|
||||
*/
|
||||
private static function get_wpcom_site_id() {
|
||||
if ( self::is_wpcom() ) {
|
||||
return get_current_blog_id();
|
||||
} elseif ( method_exists( 'Jetpack', 'is_connection_ready' ) && Jetpack::is_connection_ready() ) {
|
||||
return Jetpack_Options::get_option( 'id' );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an access token and its source into an array.
|
||||
*
|
||||
* @param string $key The API key.
|
||||
* @param string $source The key's source ("site" or "wpcom").
|
||||
* @return array
|
||||
*/
|
||||
private static function format_access_token( $key = '', $source = 'site' ) {
|
||||
return array(
|
||||
'key' => $key,
|
||||
'source' => $source,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* Extension of the SimplePie\Locator class, to detect podcast feeds
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
// Dummy comment to make phpcs happy.
|
||||
require_once __DIR__ . '/jp-simplepie-alias.php';
|
||||
|
||||
/**
|
||||
* Class Jetpack_Podcast_Feed_Locator
|
||||
*/
|
||||
class Jetpack_Podcast_Feed_Locator extends Jetpack\SimplePie\Locator {
|
||||
/**
|
||||
* Overrides the locator is_feed function to check for
|
||||
* appropriate podcast elements.
|
||||
*
|
||||
* @param Jetpack\SimplePie\File $file The file being checked.
|
||||
* @param boolean $check_html Adds text/html to the mimetypes checked.
|
||||
*/
|
||||
public function is_feed( $file, $check_html = false ) {
|
||||
return parent::is_feed( $file, $check_html ) && $this->is_podcast_feed( $file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the contents of the file for elements that make
|
||||
* it a podcast feed.
|
||||
*
|
||||
* @param Jetpack\SimplePie\File $file The file being checked.
|
||||
*/
|
||||
private function is_podcast_feed( $file ) {
|
||||
// If we can't read the DOM assume it's a podcast feed, we'll work
|
||||
// it out later.
|
||||
if ( ! class_exists( 'DOMDocument' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$feed_dom = $this->safely_load_xml( $file->body );
|
||||
|
||||
// Do this as either/or but prioritise the itunes namespace. It's pretty likely
|
||||
// that it's a podcast feed we've found if that namespace is present.
|
||||
return $feed_dom && $this->has_itunes_ns( $feed_dom ) && $this->has_audio_enclosures( $feed_dom );
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely loads an XML file
|
||||
*
|
||||
* @param string $xml A string of XML to load.
|
||||
* @return DOMDocument|false A restulting DOM document or `false` if there is an error.
|
||||
*/
|
||||
private function safely_load_xml( $xml ) {
|
||||
$disable_entity_loader = PHP_VERSION_ID < 80000;
|
||||
|
||||
if ( $disable_entity_loader ) {
|
||||
// This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading
|
||||
// is disabled by default, so this function is no longer needed to protect against XXE attacks.
|
||||
// phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated, PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated
|
||||
$loader = libxml_disable_entity_loader( true );
|
||||
}
|
||||
|
||||
$errors = libxml_use_internal_errors( true );
|
||||
|
||||
$return = new DOMDocument();
|
||||
if ( ! $return->loadXML( $xml ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors( $errors );
|
||||
|
||||
if ( $disable_entity_loader && isset( $loader ) ) {
|
||||
// phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated, PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated
|
||||
libxml_disable_entity_loader( $loader );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the RSS feed for the presence of the itunes podcast namespace.
|
||||
* It's pretty loose and just checks the URI for itunes.com
|
||||
*
|
||||
* @param DOMDocument $dom The XML document to check.
|
||||
* @return boolean Whether the itunes namespace is defined.
|
||||
*/
|
||||
private function has_itunes_ns( $dom ) {
|
||||
$xpath = new DOMXPath( $dom );
|
||||
foreach ( $xpath->query( 'namespace::*' ) as $node ) {
|
||||
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
// nodeValue is not valid, but it's part of the DOM API that we don't control.
|
||||
if ( strstr( $node->nodeValue, 'itunes.com' ) ) {
|
||||
return true;
|
||||
}
|
||||
// phpcs:enable
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the RSS feed for the presence of enclosures with an audio mimetype.
|
||||
*
|
||||
* @param DOMDocument $dom The XML document to check.
|
||||
* @return boolean Whether enclosures were found.
|
||||
*/
|
||||
private function has_audio_enclosures( $dom ) {
|
||||
$xpath = new DOMXPath( $dom );
|
||||
$enclosures = $xpath->query( "//enclosure[starts-with(@type,'audio/')]" );
|
||||
return ! $enclosures ? false : $enclosures->length > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
<?php
|
||||
/**
|
||||
* Helper to massage Podcast data to be used in the Podcast block.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
// Dummy comment to make phpcs happy.
|
||||
require_once __DIR__ . '/jp-simplepie-alias.php';
|
||||
|
||||
/**
|
||||
* Class Jetpack_Podcast_Helper
|
||||
*/
|
||||
class Jetpack_Podcast_Helper {
|
||||
/**
|
||||
* The RSS feed of the podcast.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $feed = null;
|
||||
|
||||
/**
|
||||
* The number of seconds to cache the podcast feed data.
|
||||
* This value defaults to 1 hour specifically for podcast feeds.
|
||||
* The value can be overridden specifically for podcasts using the
|
||||
* `jetpack_podcast_feed_cache_timeout` filter. Note that the cache timeout value
|
||||
* for all RSS feeds can be modified using the `wp_feed_cache_transient_lifetime`
|
||||
* filter from WordPress core.
|
||||
*
|
||||
* @see https://developer.wordpress.org/reference/hooks/wp_feed_cache_transient_lifetime/
|
||||
* @see WP_Feed_Cache_Transient
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $cache_timeout = HOUR_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Initialize class.
|
||||
*
|
||||
* @param string $feed The RSS feed of the podcast.
|
||||
*/
|
||||
public function __construct( $feed ) {
|
||||
$this->feed = esc_url_raw( $feed );
|
||||
|
||||
/**
|
||||
* Filter the number of seconds to cache a specific podcast URL for. The returned value will be ignored if it is null or not a valid integer.
|
||||
* Note that this timeout will only work if the site is using the default `WP_Feed_Cache_Transient` cache implementation for RSS feeds,
|
||||
* or their cache implementation relies on the `wp_feed_cache_transient_lifetime` filter.
|
||||
*
|
||||
* @since 11.3
|
||||
* @see https://developer.wordpress.org/reference/hooks/wp_feed_cache_transient_lifetime/
|
||||
*
|
||||
* @param int|null $cache_timeout The number of seconds to cache the podcast data. Default value is null, so we don't override any defaults from existing filters.
|
||||
* @param string $podcast_url The URL of the podcast feed.
|
||||
*/
|
||||
$podcast_cache_timeout = apply_filters( 'jetpack_podcast_feed_cache_timeout', $this->cache_timeout, $this->feed );
|
||||
|
||||
// Make sure we force new values for $this->cache_timeout to be integers.
|
||||
if ( is_numeric( $podcast_cache_timeout ) ) {
|
||||
$this->cache_timeout = (int) $podcast_cache_timeout;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tracks quantity.
|
||||
*
|
||||
* @return int number of tracks
|
||||
*/
|
||||
public static function get_tracks_quantity() {
|
||||
/**
|
||||
* Allow requesting a specific number of tracks from SimplePie's `get_items` call.
|
||||
* The default number of tracks is ten.
|
||||
*
|
||||
* @since 10.4.0
|
||||
*
|
||||
* @param int $number Number of tracks fetched. Default is 10.
|
||||
*/
|
||||
return (int) apply_filters( 'jetpack_podcast_helper_tracks_quantity', 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets podcast data formatted to be used by the Podcast Player block in both server-side
|
||||
* block rendering and in API `WPCOM_REST_API_V2_Endpoint_Podcast_Player`.
|
||||
*
|
||||
* The result is cached for one hour.
|
||||
*
|
||||
* @param array $args {
|
||||
* Optional array of arguments.
|
||||
* @type string|int $guid The ID of a specific episode to return rather than a list.
|
||||
* }
|
||||
*
|
||||
* @return array|WP_Error The player data or a error object.
|
||||
*/
|
||||
public function get_player_data( $args = array() ) {
|
||||
$guids = isset( $args['guids'] ) && $args['guids'] ? $args['guids'] : array();
|
||||
$episode_options = isset( $args['episode-options'] ) && $args['episode-options'];
|
||||
|
||||
// Try loading data from the cache.
|
||||
$transient_key = 'jetpack_podcast_' . md5( $this->feed . implode( ',', $guids ) . "-$episode_options" );
|
||||
$player_data = get_transient( $transient_key );
|
||||
|
||||
// Fetch data if we don't have any cached.
|
||||
if ( false === $player_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
|
||||
// Load feed.
|
||||
$rss = $this->load_feed();
|
||||
|
||||
if ( is_wp_error( $rss ) ) {
|
||||
return $rss;
|
||||
}
|
||||
|
||||
// Get a list of episodes by guid or all tracks in feed.
|
||||
if ( count( $guids ) ) {
|
||||
$tracks = array_map( array( $this, 'get_track_data' ), $guids );
|
||||
$tracks = array_filter(
|
||||
$tracks,
|
||||
function ( $track ) {
|
||||
return ! is_wp_error( $track );
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$tracks = $this->get_track_list();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $tracks ) ) {
|
||||
return $tracks;
|
||||
}
|
||||
|
||||
if ( empty( $tracks ) ) {
|
||||
return new WP_Error( 'no_tracks', __( 'Your Podcast couldn\'t be embedded as it doesn\'t contain any tracks. Please double check your URL.', 'jetpack' ) );
|
||||
}
|
||||
|
||||
// Get podcast meta.
|
||||
$title = $rss->get_title();
|
||||
$title = $this->get_plain_text( $title );
|
||||
|
||||
$description = $rss->get_description();
|
||||
$description = $this->get_plain_text( $description );
|
||||
|
||||
$cover = $rss->get_image_url();
|
||||
$cover = ! empty( $cover ) ? esc_url( $cover ) : null;
|
||||
|
||||
$link = $rss->get_link();
|
||||
$link = ! empty( $link ) ? esc_url( $link ) : null;
|
||||
|
||||
$player_data = array(
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'link' => $link,
|
||||
'cover' => $cover,
|
||||
'tracks' => $tracks,
|
||||
);
|
||||
|
||||
if ( $episode_options ) {
|
||||
$player_data['options'] = array();
|
||||
foreach ( $rss->get_items() as $episode ) {
|
||||
$enclosure = $this->get_audio_enclosure( $episode );
|
||||
// If the episode doesn't have playable audio, then don't include it.
|
||||
if ( is_wp_error( $enclosure ) ) {
|
||||
continue;
|
||||
}
|
||||
$player_data['options'][] = array(
|
||||
'label' => $this->get_plain_text( $episode->get_title() ),
|
||||
'value' => $episode->get_id(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for 1 hour.
|
||||
set_transient( $transient_key, $player_data, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $player_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific track from the supplied feed URL.
|
||||
*
|
||||
* @param string $guid The GUID of the track.
|
||||
* @param boolean $force_refresh Clear the feed cache.
|
||||
* @return array|WP_Error The track object or an error object.
|
||||
*/
|
||||
public function get_track_data( $guid, $force_refresh = false ) {
|
||||
// Get the cache key.
|
||||
$transient_key = 'jetpack_podcast_' . md5( "$this->feed::$guid" );
|
||||
|
||||
// Clear the cache if force_refresh param is true.
|
||||
if ( true === $force_refresh ) {
|
||||
delete_transient( $transient_key );
|
||||
}
|
||||
|
||||
// Try loading track data from the cache.
|
||||
$track_data = get_transient( $transient_key );
|
||||
|
||||
// Fetch data if we don't have any cached.
|
||||
if ( false === $track_data || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
|
||||
// Load feed.
|
||||
$rss = $this->load_feed( $force_refresh );
|
||||
|
||||
if ( is_wp_error( $rss ) ) {
|
||||
return $rss;
|
||||
}
|
||||
|
||||
// Loop over all tracks to find the one.
|
||||
foreach ( $rss->get_items() as $track ) {
|
||||
if ( $guid === $track->get_id() ) {
|
||||
$track_data = $this->setup_tracks_callback( $track );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( false === $track_data ) {
|
||||
return new WP_Error( 'no_track', __( 'The track was not found.', 'jetpack' ) );
|
||||
}
|
||||
|
||||
// Cache for 1 hour.
|
||||
set_transient( $transient_key, $track_data, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $track_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of tracks for the supplied RSS feed.
|
||||
*
|
||||
* @return array|WP_Error The feed's tracks or a error object.
|
||||
*/
|
||||
public function get_track_list() {
|
||||
$rss = $this->load_feed();
|
||||
|
||||
if ( is_wp_error( $rss ) ) {
|
||||
return $rss;
|
||||
}
|
||||
|
||||
$tracks_quantity = static::get_tracks_quantity();
|
||||
|
||||
/**
|
||||
* Allow requesting a specific number of tracks from SimplePie's `get_items` call.
|
||||
* The default number of tracks is ten.
|
||||
* Deprecated. Use jetpack_podcast_helper_tracks_quantity filter instead, which takes one less parameter.
|
||||
*
|
||||
* @since 9.5.0
|
||||
* @deprecated 10.4.0
|
||||
*
|
||||
* @param int $tracks_quantity Number of tracks fetched. Default is 10.
|
||||
* @param object $rss The SimplePie object built from core's `fetch_feed` call.
|
||||
*/
|
||||
$tracks_quantity = apply_filters_deprecated( 'jetpack_podcast_helper_list_quantity', array( $tracks_quantity, $rss ), '10.4.0', 'jetpack_podcast_helper_tracks_quantity' );
|
||||
|
||||
// Process the requested number of items from our feed.
|
||||
$track_list = array_map( array( __CLASS__, 'setup_tracks_callback' ), $rss->get_items( 0, $tracks_quantity ) );
|
||||
|
||||
// Filter out any tracks that are empty.
|
||||
// Reset the array indices.
|
||||
return array_values( array_filter( $track_list ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats string as pure plaintext, with no HTML tags or entities present.
|
||||
* This is ready to be used in React, innerText but needs to be escaped
|
||||
* using standard `esc_html` when generating markup on server.
|
||||
*
|
||||
* @param string $str Input string.
|
||||
* @return string Plain text string.
|
||||
*/
|
||||
protected function get_plain_text( $str ) {
|
||||
return $this->sanitize_and_decode_text( $str, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats strings as safe HTML.
|
||||
*
|
||||
* @param string $str Input string.
|
||||
* @return string HTML text string safe for post_content.
|
||||
*/
|
||||
protected function get_html_text( $str ) {
|
||||
return $this->sanitize_and_decode_text( $str, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip unallowed html tags and decode entities.
|
||||
*
|
||||
* @param string $str Input string.
|
||||
* @param boolean $strip_all_tags Strip all tags, otherwise allow post_content safe tags.
|
||||
* @return string Sanitized and decoded text.
|
||||
*/
|
||||
protected function sanitize_and_decode_text( $str, $strip_all_tags = true ) {
|
||||
// Trim string and return if empty.
|
||||
$str = trim( (string) $str );
|
||||
if ( empty( $str ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( $strip_all_tags ) {
|
||||
// Make sure there are no tags.
|
||||
$str = wp_strip_all_tags( $str );
|
||||
} else {
|
||||
$str = wp_kses_post( $str );
|
||||
}
|
||||
|
||||
// Replace all entities with their characters, including all types of quotes.
|
||||
$str = html_entity_decode( $str, ENT_QUOTES );
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an RSS feed using `fetch_feed`.
|
||||
*
|
||||
* @param boolean $force_refresh Clear the feed cache.
|
||||
* @return Jetpack\SimplePie\SimplePie|WP_Error The RSS object or error.
|
||||
*/
|
||||
public function load_feed( $force_refresh = false ) {
|
||||
// Add action: clear the SimplePie Cache if $force_refresh param is true.
|
||||
if ( true === $force_refresh ) {
|
||||
add_action( 'wp_feed_options', array( __CLASS__, 'reset_simplepie_cache' ) );
|
||||
}
|
||||
// Add action: detect the podcast feed from the provided feed URL.
|
||||
add_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) );
|
||||
|
||||
$cache_timeout_filter_added = false;
|
||||
if ( $this->cache_timeout !== null ) {
|
||||
// If we have a custom cache timeout, apply the custom timeout value.
|
||||
add_filter( 'wp_feed_cache_transient_lifetime', array( $this, 'filter_podcast_cache_timeout' ), 20 );
|
||||
$cache_timeout_filter_added = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow callers to set up any desired hooks when we fetch the content for a podcast.
|
||||
* The `jetpack_podcast_post_fetch` action can be used to perform cleanup.
|
||||
*
|
||||
* @param string $podcast_url URL for the podcast's RSS feed.
|
||||
*
|
||||
* @since 11.2
|
||||
*/
|
||||
do_action( 'jetpack_podcast_pre_fetch', $this->feed );
|
||||
|
||||
// Fetch the feed.
|
||||
$rss = fetch_feed( $this->feed );
|
||||
|
||||
// Remove added actions from wp_feed_options hook.
|
||||
remove_action( 'wp_feed_options', array( __CLASS__, 'set_podcast_locator' ) );
|
||||
if ( true === $force_refresh ) {
|
||||
remove_action( 'wp_feed_options', array( __CLASS__, 'reset_simplepie_cache' ) );
|
||||
}
|
||||
|
||||
if ( $cache_timeout_filter_added ) {
|
||||
// Remove the cache timeout filter we added.
|
||||
remove_filter( 'wp_feed_cache_transient_lifetime', array( $this, 'filter_podcast_cache_timeout' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow callers to identify when we have completed fetching a specified podcast feed.
|
||||
* This makes it possible to clean up any actions or filters that were set up using the
|
||||
* `jetpack_podcast_pre_fetch` action.
|
||||
*
|
||||
* Note that this action runs after other hooks added by Jetpack have been removed.
|
||||
*
|
||||
* @param string $podcast_url URL for the podcast's RSS feed.
|
||||
* @param SimplePie\SimplePie|SimplePie|WP_Error $rss Either the SimplePie RSS object or an error.
|
||||
*
|
||||
* @since 11.2
|
||||
*/
|
||||
do_action( 'jetpack_podcast_post_fetch', $this->feed, $rss );
|
||||
|
||||
if ( is_wp_error( $rss ) ) {
|
||||
return new WP_Error( 'invalid_url', __( 'Your podcast couldn\'t be embedded. Please double check your URL.', 'jetpack' ) );
|
||||
}
|
||||
|
||||
if ( ! $rss->get_item_quantity() ) {
|
||||
return new WP_Error( 'no_tracks', __( 'Podcast audio RSS feed has no tracks.', 'jetpack' ) );
|
||||
}
|
||||
|
||||
return $rss;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to override the default number of seconds to cache RSS feed data for the current feed.
|
||||
* Note that we don't use the feed's URL because some of the SimplePie feed caches trigger this
|
||||
* filter with a feed identifier and not a URL.
|
||||
*
|
||||
* @param int $cache_timeout_in_seconds Number of seconds to cache the podcast feed.
|
||||
*
|
||||
* @return int The number of seconds to cache the podcast feed.
|
||||
*/
|
||||
public function filter_podcast_cache_timeout( $cache_timeout_in_seconds ) {
|
||||
if ( $this->cache_timeout !== null ) {
|
||||
return $this->cache_timeout;
|
||||
}
|
||||
|
||||
return $cache_timeout_in_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action handler to set our podcast specific feed locator class on the SimplePie object.
|
||||
*
|
||||
* @param Jetpack\SimplePie\SimplePie $feed The SimplePie object, passed by reference.
|
||||
*/
|
||||
public static function set_podcast_locator( &$feed ) {
|
||||
if ( ! class_exists( 'Jetpack_Podcast_Feed_Locator' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-podcast-feed-locator.php';
|
||||
}
|
||||
|
||||
// @todo Once we drop support for WordPress 6.6, drop the class_exists check.
|
||||
// @phan-suppress-next-line PhanUndeclaredClassReference -- Being tested for. @phan-suppress-current-line UnusedPluginSuppression
|
||||
$feed->get_registry()->register( class_exists( SimplePie\Locator::class ) ? SimplePie\Locator::class : 'Locator', 'Jetpack_Podcast_Feed_Locator' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Action handler to reset the SimplePie cache for the podcast feed.
|
||||
*
|
||||
* Note this only resets the cache for the specified url. If the feed locator finds the podcast feed
|
||||
* within the markup of the that url, that feed itself may still be cached.
|
||||
*
|
||||
* @param Jetpack\SimplePie\SimplePie $feed The SimplePie object, passed by reference.
|
||||
* @return void
|
||||
*/
|
||||
public static function reset_simplepie_cache( &$feed ) {
|
||||
// Retrieve the cache object for a feed url. Based on:
|
||||
// https://github.com/WordPress/WordPress/blob/fd1c2cb4011845ceb7244a062b09b2506082b1c9/wp-includes/class-simplepie.php#L1412.
|
||||
// @todo This method of getting the cache is deprecated, and there doesn't seem to be a real replacement. `$feed->get_cache()` is private.
|
||||
// @phan-suppress-next-line PhanUndeclaredClassReference
|
||||
$cache = $feed->registry->call( 'Cache', 'get_handler', array( $feed->cache_location, call_user_func( $feed->cache_name_function, $feed->feed_url ), 'spc' ) );
|
||||
|
||||
if ( method_exists( $cache, 'unlink' ) ) {
|
||||
$cache->unlink();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares Episode data to be used by the Podcast Player block.
|
||||
*
|
||||
* @param Jetpack\SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
|
||||
* @return array
|
||||
*/
|
||||
protected function setup_tracks_callback( Jetpack\SimplePie\Item $episode ) {
|
||||
$enclosure = $this->get_audio_enclosure( $episode );
|
||||
|
||||
// If the audio enclosure is empty then it is not playable.
|
||||
// We therefore return an empty array for this track.
|
||||
// It will be filtered out later.
|
||||
if ( is_wp_error( $enclosure ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// If there is no link return an empty array. We will filter out later.
|
||||
if ( empty( $enclosure->link ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$publish_date = $episode->get_gmdate( DATE_ATOM );
|
||||
// Build track data.
|
||||
$track = array(
|
||||
'id' => wp_unique_id( 'podcast-track-' ),
|
||||
'link' => esc_url( $episode->get_link() ),
|
||||
'src' => esc_url( $enclosure->link ),
|
||||
'type' => esc_attr( $enclosure->type ),
|
||||
'description' => $this->get_plain_text( $episode->get_description() ),
|
||||
'description_html' => $this->get_html_text( $episode->get_description() ),
|
||||
'title' => $this->get_plain_text( $episode->get_title() ),
|
||||
'image' => esc_url( $this->get_episode_image_url( $episode ) ),
|
||||
'guid' => $this->get_plain_text( $episode->get_id() ),
|
||||
'publish_date' => $publish_date ? $publish_date : null,
|
||||
);
|
||||
|
||||
if ( empty( $track['title'] ) ) {
|
||||
$track['title'] = esc_html__( '(no title)', 'jetpack' );
|
||||
}
|
||||
|
||||
if ( ! empty( $enclosure->duration ) ) {
|
||||
$track['duration'] = esc_html( $this->format_track_duration( $enclosure->duration ) );
|
||||
}
|
||||
|
||||
return $track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an episode's image URL, if it's available.
|
||||
*
|
||||
* @param Jetpack\SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
|
||||
* @param string $itunes_ns The itunes namespace, defaulted to the standard 1.0 version.
|
||||
* @return string|null The image URL or null if not found.
|
||||
*/
|
||||
protected function get_episode_image_url( Jetpack\SimplePie\Item $episode, $itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ) {
|
||||
$image = $episode->get_item_tags( $itunes_ns, 'image' );
|
||||
if ( isset( $image[0]['attribs']['']['href'] ) ) {
|
||||
return $image[0]['attribs']['']['href'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an audio enclosure.
|
||||
*
|
||||
* @param Jetpack\SimplePie\Item $episode SimplePie Item object, representing a podcast episode.
|
||||
* @return Jetpack\SimplePie\Enclosure|null
|
||||
*/
|
||||
protected function get_audio_enclosure( Jetpack\SimplePie\Item $episode ) {
|
||||
foreach ( (array) $episode->get_enclosures() as $enclosure ) {
|
||||
if ( str_starts_with( $enclosure->type, 'audio/' ) ) {
|
||||
return $enclosure;
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error( 'invalid_audio', __( 'Podcast audio is an invalid type.', 'jetpack' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the track duration as a formatted string.
|
||||
*
|
||||
* @param int|float $duration of the track in seconds.
|
||||
* @return string
|
||||
*/
|
||||
protected function format_track_duration( $duration ) {
|
||||
$format = $duration > HOUR_IN_SECONDS ? 'H:i:s' : 'i:s';
|
||||
|
||||
return date_i18n( $format, $duration );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets podcast player data schema.
|
||||
*
|
||||
* Useful for json schema in REST API endpoints.
|
||||
*
|
||||
* @return array Player data json schema.
|
||||
*/
|
||||
public static function get_player_data_schema() {
|
||||
return array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'jetpack-podcast-player-data',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'title' => array(
|
||||
'description' => __( 'The title of the podcast.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'link' => array(
|
||||
'description' => __( 'The URL of the podcast website.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'cover' => array(
|
||||
'description' => __( 'The URL of the podcast cover image.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'tracks' => self::get_tracks_schema(),
|
||||
'options' => self::get_options_schema(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tracks data schema.
|
||||
*
|
||||
* Useful for json schema in REST API endpoints.
|
||||
*
|
||||
* @return array Tracks json schema.
|
||||
*/
|
||||
public static function get_tracks_schema() {
|
||||
return array(
|
||||
'description' => __( 'Latest episodes of the podcast.', 'jetpack' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'The episode id. Generated per request, not globally unique.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'link' => array(
|
||||
'description' => __( 'The external link for the episode.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'src' => array(
|
||||
'description' => __( 'The audio file URL of the episode.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'type' => array(
|
||||
'description' => __( 'The mime type of the episode.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'description' => array(
|
||||
'description' => __( 'The episode description, in plaintext.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'description_html' => array(
|
||||
'description' => __( 'The episode description with allowed html tags.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'title' => array(
|
||||
'description' => __( 'The episode title.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'publish_date' => array(
|
||||
'description' => __( 'The UTC publish date and time of the episode', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the episode options schema.
|
||||
*
|
||||
* Useful for json schema in REST API endpoints.
|
||||
*
|
||||
* @return array Tracks json schema.
|
||||
*/
|
||||
public static function get_options_schema() {
|
||||
return array(
|
||||
'description' => __( 'The options that will be displayed in the episode selection UI', 'jetpack' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'label' => array(
|
||||
'description' => __( 'The display label of the option, the episode title.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'value' => array(
|
||||
'description' => __( 'The value used for that option, the episode GUID', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
/**
|
||||
* Utilities related to the Jetpack Recommendations
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
|
||||
use Automattic\Jetpack\Plugins_Installer;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Contains utilities related to the Jetpack Recommendations.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Jetpack_Recommendations class
|
||||
*/
|
||||
class Jetpack_Recommendations {
|
||||
const PUBLICIZE_RECOMMENDATION = 'publicize';
|
||||
const PROTECT_RECOMMENDATION = 'protect';
|
||||
const ANTI_SPAM_RECOMMENDATION = 'anti-spam';
|
||||
const VIDEOPRESS_RECOMMENDATION = 'videopress';
|
||||
const BACKUP_PLAN_RECOMMENDATION = 'backup-plan';
|
||||
const BOOST_RECOMMENDATION = 'boost';
|
||||
|
||||
const CONDITIONAL_RECOMMENDATIONS_OPTION = 'recommendations_conditional';
|
||||
const CONDITIONAL_RECOMMENDATIONS = array(
|
||||
self::PUBLICIZE_RECOMMENDATION,
|
||||
self::PROTECT_RECOMMENDATION,
|
||||
self::ANTI_SPAM_RECOMMENDATION,
|
||||
self::VIDEOPRESS_RECOMMENDATION,
|
||||
self::BACKUP_PLAN_RECOMMENDATION,
|
||||
self::BOOST_RECOMMENDATION,
|
||||
);
|
||||
|
||||
const VIDEOPRESS_TIMED_ACTION = 'jetpack_recommend_videopress';
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating if the Jetpack Recommendations are enabled.
|
||||
*
|
||||
* @since 9.3.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
// Shortcircuit early if Jetpack is not active or we are in offline mode.
|
||||
if ( ! Jetpack::is_connection_ready() || ( new Status() )->is_offline_mode() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No recommendations for Atomic sites, they already get onboarded in Calypso.
|
||||
if ( ( new Host() )->is_woa_site() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self::initialize_jetpack_recommendations();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating if the Jetpack Banner is enabled.
|
||||
*
|
||||
* @since 9.3.0
|
||||
*
|
||||
* @deprecated 13.2
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_banner_enabled() {
|
||||
_deprecated_function( __METHOD__, 'jetpack-13.2' );
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up actions to monitor for things that trigger a recommendation.
|
||||
*
|
||||
* @return false|void
|
||||
*/
|
||||
public static function init_conditional_recommendation_actions() {
|
||||
// Check to make sure that recommendations are enabled.
|
||||
if ( ! self::is_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Monitor for the publishing of a new post.
|
||||
add_action( 'transition_post_status', array( static::class, 'post_transition' ), 10, 3 );
|
||||
add_action( 'jetpack_activate_module', array( static::class, 'jetpack_module_activated' ), 10, 2 );
|
||||
|
||||
// Monitor for activating a new plugin.
|
||||
add_action( 'activated_plugin', array( static::class, 'plugin_activated' ), 10 );
|
||||
|
||||
// Monitor for the addition of a new comment.
|
||||
add_action( 'comment_post', array( static::class, 'comment_added' ), 10, 3 );
|
||||
|
||||
// Monitor for Jetpack connection success.
|
||||
add_action( 'jetpack_authorize_ending_authorized', array( static::class, 'jetpack_connected' ) );
|
||||
add_action( self::VIDEOPRESS_TIMED_ACTION, array( static::class, 'recommend_videopress' ) );
|
||||
|
||||
// Monitor for changes in plugins that have auto-updates enabled
|
||||
add_action( 'update_site_option_auto_update_plugins', array( static::class, 'plugin_auto_update_settings_changed' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check when Jetpack modules are activated if some recommendations should be skipped.
|
||||
*
|
||||
* @param string $module Name of the module activated.
|
||||
* @param bool $success Whether the module activation was successful.
|
||||
*/
|
||||
public static function jetpack_module_activated( $module, $success ) {
|
||||
if ( 'publicize' === $module && $success ) {
|
||||
self::disable_conditional_recommendation( self::PUBLICIZE_RECOMMENDATION );
|
||||
} elseif ( 'videopress' === $module && $success ) {
|
||||
// If VideoPress is enabled and a recommendation for it is scheduled, cancel that recommendation.
|
||||
$recommendation_timestamp = wp_next_scheduled( self::VIDEOPRESS_TIMED_ACTION );
|
||||
if ( false !== $recommendation_timestamp ) {
|
||||
wp_unschedule_event( $recommendation_timestamp, self::VIDEOPRESS_TIMED_ACTION );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for transition_post_status that checks for the publishing of a new post or page.
|
||||
* Used to enable the publicize and boost recommendations.
|
||||
*
|
||||
* @param string $new_status new status of post.
|
||||
* @param string $old_status old status of post.
|
||||
* @param WP_Post $post the post object being updated.
|
||||
*/
|
||||
public static function post_transition( $new_status, $old_status, $post ) {
|
||||
// Check for condition when post has been published.
|
||||
if ( 'post' === $post->post_type && 'publish' === $new_status && 'publish' !== $old_status && ! Jetpack::is_module_active( 'publicize' ) ) {
|
||||
// Set the publicize recommendation to have met criteria to be shown.
|
||||
self::enable_conditional_recommendation( self::PUBLICIZE_RECOMMENDATION );
|
||||
return;
|
||||
}
|
||||
// A new page has been published
|
||||
// Check to see if the boost plugin is active
|
||||
if (
|
||||
'page' === $post->post_type &&
|
||||
'publish' === $new_status &&
|
||||
'publish' !== $old_status &&
|
||||
! Plugins_Installer::is_plugin_active( 'boost/jetpack-boost.php' ) &&
|
||||
! Plugins_Installer::is_plugin_active( 'jetpack-boost/jetpack-boost.php' )
|
||||
) {
|
||||
self::enable_conditional_recommendation( self::BOOST_RECOMMENDATION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when a plugin gets activated
|
||||
*
|
||||
* @param string $plugin Path to the plugins file relative to the plugins directory.
|
||||
*/
|
||||
public static function plugin_activated( $plugin ) {
|
||||
// If the plugin is in this list, don't enable the recommendation.
|
||||
$plugin_whitelist = array(
|
||||
'jetpack.php',
|
||||
'akismet.php',
|
||||
'creative-mail.php',
|
||||
'jetpack-backup.php',
|
||||
'jetpack-boost.php',
|
||||
'jetpack-protect.php',
|
||||
'crowdsignal.php',
|
||||
'vaultpress.php',
|
||||
'woocommerce.php',
|
||||
);
|
||||
|
||||
$path_parts = explode( '/', $plugin );
|
||||
$plugin_file = $path_parts ? array_pop( $path_parts ) : $plugin;
|
||||
|
||||
if ( ! in_array( $plugin_file, $plugin_whitelist, true ) ) {
|
||||
$products = array_column( Jetpack_Plan::get_products(), 'product_slug' );
|
||||
|
||||
// Check for a plan or product that enables scan.
|
||||
$plan_supports_scan = Jetpack_Plan::supports( 'scan' );
|
||||
$has_scan_product = count( array_intersect( array( 'jetpack_scan', 'jetpack_scan_monthly' ), $products ) ) > 0;
|
||||
$has_scan = $plan_supports_scan || $has_scan_product;
|
||||
|
||||
// Check if Jetpack Protect plugin is already active.
|
||||
$has_protect = Plugins_Installer::is_plugin_active( 'jetpack-protect/jetpack-protect.php' ) || Plugins_Installer::is_plugin_active( 'protect/jetpack-protect.php' );
|
||||
|
||||
if ( ! $has_scan && ! $has_protect ) {
|
||||
self::enable_conditional_recommendation( self::PROTECT_RECOMMENDATION );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the auto_update_plugins option has been changed
|
||||
*
|
||||
* @param string $option_name - the name of the option updated ( always auto_update_plugins ).
|
||||
* @param array $new_auto_update_plugins - plugins that have auto update enabled following the change.
|
||||
* @param array $old_auto_update_plugins - plugins that had auto update enabled before the most recent change.
|
||||
* @return void
|
||||
*/
|
||||
public static function plugin_auto_update_settings_changed( $option_name, $new_auto_update_plugins, $old_auto_update_plugins ) {
|
||||
if (
|
||||
is_multisite() ||
|
||||
self::is_conditional_recommendation_enabled( self::BACKUP_PLAN_RECOMMENDATION )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for plugins that have had auto-update enabled in this most recent update.
|
||||
$enabled_auto_updates = array_diff( $new_auto_update_plugins, $old_auto_update_plugins );
|
||||
if ( ! empty( $enabled_auto_updates ) ) {
|
||||
// Check the backup state.
|
||||
$rewind_state = get_transient( 'jetpack_rewind_state' );
|
||||
$has_backup = $rewind_state && in_array( $rewind_state->state, array( 'awaiting_credentials', 'provisioning', 'active' ), true );
|
||||
|
||||
if ( ! $has_backup ) {
|
||||
self::enable_conditional_recommendation( self::BACKUP_PLAN_RECOMMENDATION );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when a new comment is added.
|
||||
*
|
||||
* @param integer $comment_id The ID of the comment that was added.
|
||||
* @param bool $comment_approved Whether or not the comment is approved.
|
||||
* @param array $commentdata Comment data.
|
||||
*/
|
||||
public static function comment_added( $comment_id, $comment_approved, $commentdata ) {
|
||||
if ( self::is_conditional_recommendation_enabled( self::ANTI_SPAM_RECOMMENDATION ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Plugins_Installer::is_plugin_active( 'akismet/akismet.php' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The site has anti-spam features already.
|
||||
$site_products = array_column( Jetpack_Plan::get_products(), 'product_slug' );
|
||||
$has_anti_spam_product = count( array_intersect( array( 'jetpack_anti_spam', 'jetpack_anti_spam_monthly' ), $site_products ) ) > 0;
|
||||
|
||||
if ( Jetpack_Plan::supports( 'akismet' ) || Jetpack_Plan::supports( 'antispam' ) || $has_anti_spam_product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $commentdata['comment_post_ID'] ) ) {
|
||||
$post_id = $commentdata['comment_post_ID'];
|
||||
} else {
|
||||
$comment = get_comment( $comment_id );
|
||||
$post_id = $comment->comment_post_ID;
|
||||
}
|
||||
$comment_count = get_comments_number( $post_id );
|
||||
|
||||
if ( intval( $comment_count ) >= 5 ) {
|
||||
self::enable_conditional_recommendation( self::ANTI_SPAM_RECOMMENDATION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after a successful connection is made.
|
||||
*/
|
||||
public static function jetpack_connected() {
|
||||
// Schedule a recommendation for VideoPress in 2 weeks.
|
||||
if ( false === wp_next_scheduled( self::VIDEOPRESS_TIMED_ACTION ) ) {
|
||||
$date = new DateTime();
|
||||
$date->add( new DateInterval( 'P14D' ) );
|
||||
wp_schedule_single_event( $date->getTimestamp(), self::VIDEOPRESS_TIMED_ACTION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a recommendation for VideoPress.
|
||||
*/
|
||||
public static function recommend_videopress() {
|
||||
// Check to see if the VideoPress recommendation is already enabled.
|
||||
if ( self::is_conditional_recommendation_enabled( self::VIDEOPRESS_RECOMMENDATION ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$site_plan = Jetpack_Plan::get();
|
||||
$site_products = array_column( Jetpack_Plan::get_products(), 'product_slug' );
|
||||
|
||||
if ( self::should_recommend_videopress( $site_plan, $site_products ) ) {
|
||||
self::enable_conditional_recommendation( self::VIDEOPRESS_RECOMMENDATION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we provide a recommendation for videopress?
|
||||
* This method exists to facilitate unit testing
|
||||
*
|
||||
* @param array $site_plan A representation of the site's plan.
|
||||
* @param array $site_products An array of product slugs.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function should_recommend_videopress( $site_plan, $site_products ) {
|
||||
// Does the site have the VideoPress module enabled?
|
||||
if ( Jetpack::is_module_active( 'videopress' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does the site plan have upgraded videopress features?
|
||||
// For now, this just checks to see if the site has a free plan.
|
||||
// Jetpack_Plan::supports('videopress') returns true for all plans, since there is a free tier.
|
||||
$is_free_plan = 'free' === $site_plan['class'];
|
||||
if ( ! $is_free_plan ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does this site already have a VideoPress product?
|
||||
$has_videopress_product = count( array_intersect( array( 'jetpack_videopress', 'jetpack_videopress_monthly' ), $site_products ) ) > 0;
|
||||
if ( $has_videopress_product ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a recommendation.
|
||||
*
|
||||
* @param string $recommendation_name The name of the recommendation to enable.
|
||||
* @return false|void
|
||||
*/
|
||||
public static function enable_conditional_recommendation( $recommendation_name ) {
|
||||
if ( ! in_array( $recommendation_name, self::CONDITIONAL_RECOMMENDATIONS, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$conditional_recommendations = Jetpack_Options::get_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, array() );
|
||||
if ( ! in_array( $recommendation_name, $conditional_recommendations, true ) ) {
|
||||
$conditional_recommendations[] = $recommendation_name;
|
||||
Jetpack_Options::update_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, $conditional_recommendations );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a recommendation.
|
||||
*
|
||||
* @param string $recommendation_name The name of the recommendation to disable.
|
||||
* @return false|void
|
||||
*/
|
||||
public static function disable_conditional_recommendation( $recommendation_name ) {
|
||||
if ( ! in_array( $recommendation_name, self::CONDITIONAL_RECOMMENDATIONS, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$conditional_recommendations = Jetpack_Options::get_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, array() );
|
||||
$recommendation_index = array_search( $recommendation_name, $conditional_recommendations, true );
|
||||
|
||||
if ( false !== $recommendation_index ) {
|
||||
array_splice( $conditional_recommendations, $recommendation_index, 1 );
|
||||
Jetpack_Options::update_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, $conditional_recommendations );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if a recommendation is enabled or not.
|
||||
*
|
||||
* @param string $recommendation_name The name of the recommendation to check for.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_conditional_recommendation_enabled( $recommendation_name ) {
|
||||
$conditional_recommendations = Jetpack_Options::get_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, array() );
|
||||
return in_array( $recommendation_name, $conditional_recommendations, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets data for all conditional recommendations.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_conditional_recommendations() {
|
||||
return Jetpack_Options::get_option( self::CONDITIONAL_RECOMMENDATIONS_OPTION, array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of new conditional recommendations that have not been viewed.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_new_conditional_recommendations() {
|
||||
$conditional_recommendations = self::get_conditional_recommendations();
|
||||
$recommendations_data = Jetpack_Options::get_option( 'recommendations_data', array() );
|
||||
$viewed_recommendations = isset( $recommendations_data['viewedRecommendations'] ) ? $recommendations_data['viewedRecommendations'] : array();
|
||||
|
||||
// array_diff returns a keyed array - reduce to unique values.
|
||||
return array_unique( array_values( array_diff( $conditional_recommendations, $viewed_recommendations ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Recommendations step according to the Setup Wizard state.
|
||||
*/
|
||||
private static function initialize_jetpack_recommendations() {
|
||||
if ( Jetpack_Options::get_option( 'recommendations_step' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$setup_wizard_status = Jetpack_Options::get_option( 'setup_wizard_status' );
|
||||
if ( 'completed' === $setup_wizard_status ) {
|
||||
Jetpack_Options::update_option( 'recommendations_step', 'setup-wizard-completed' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data for the recommendations
|
||||
*
|
||||
* @return array Recommendations data
|
||||
*/
|
||||
public static function get_recommendations_data() {
|
||||
self::initialize_jetpack_recommendations();
|
||||
|
||||
return Jetpack_Options::get_option( 'recommendations_data', array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the data for the recommendations
|
||||
*
|
||||
* @param WP_REST_Request $data The data.
|
||||
*/
|
||||
public static function update_recommendations_data( $data ) {
|
||||
if ( ! empty( $data ) ) {
|
||||
Jetpack_Options::update_option( 'recommendations_data', $data );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data for the recommendations
|
||||
*
|
||||
* @return array Recommendations data
|
||||
*/
|
||||
public static function get_recommendations_step() {
|
||||
self::initialize_jetpack_recommendations();
|
||||
|
||||
return array(
|
||||
'step' => Jetpack_Options::get_option( 'recommendations_step', 'not-started' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the step for the recommendations
|
||||
*
|
||||
* @param WP_REST_Request $step The step.
|
||||
*/
|
||||
public static function update_recommendations_step( $step ) {
|
||||
if ( ! empty( $step ) ) {
|
||||
Jetpack_Options::update_option( 'recommendations_step', $step );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* Top Posts & Pages block helper.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Stats\WPCOM_Stats;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Top_Posts_Helper
|
||||
*/
|
||||
class Jetpack_Top_Posts_Helper {
|
||||
/**
|
||||
* Returns user's top posts.
|
||||
*
|
||||
* @param int $period Period of days to draw stats from.
|
||||
* @param int $items_count Optional. Number of items to display.
|
||||
* @param string $types Optional. Content types to include.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_top_posts( $period, $items_count = null, $types = null ) {
|
||||
$all_time_days = floor( ( time() - strtotime( get_option( 'site_created_date' ) ) ) / ( 60 * 60 * 24 * 365 ) );
|
||||
|
||||
// While we only display ten posts, users can filter out content types.
|
||||
// As such, we should obtain a few spare posts from the Stats endpoint.
|
||||
$posts_to_obtain_count = 30;
|
||||
|
||||
// We should not override cache when displaying the block on the frontend.
|
||||
// But we should allow instant preview of changes when editing the block.
|
||||
$is_rendering_block = ! empty( $types );
|
||||
$override_cache = ! $is_rendering_block;
|
||||
|
||||
$query_args = array(
|
||||
'max' => $posts_to_obtain_count,
|
||||
'summarize' => true,
|
||||
'num' => $period !== 'all-time' ? $period : $all_time_days,
|
||||
'period' => 'day',
|
||||
);
|
||||
|
||||
$data = ( new WPCOM_Stats() )->get_top_posts( $query_args, $override_cache );
|
||||
|
||||
if ( is_wp_error( $data ) ) {
|
||||
$data = array( 'summary' => array( 'postviews' => array() ) );
|
||||
}
|
||||
|
||||
// Remove posts that have subsequently been deleted.
|
||||
$data['summary']['postviews'] = array_filter(
|
||||
$data['summary']['postviews'],
|
||||
function ( $item ) {
|
||||
return get_post_status( $item['id'] ) === 'publish';
|
||||
}
|
||||
);
|
||||
|
||||
$posts_retrieved = is_countable( $data['summary']['postviews'] ) ? count( $data['summary']['postviews'] ) : 0;
|
||||
|
||||
// Fallback to random posts if user does not have enough top content.
|
||||
if ( $posts_retrieved < $posts_to_obtain_count ) {
|
||||
$args = array(
|
||||
'numberposts' => $posts_to_obtain_count - $posts_retrieved,
|
||||
'exclude' => array_column( $data['summary']['postviews'], 'id' ),
|
||||
'orderby' => 'rand',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$random_posts = get_posts( $args );
|
||||
|
||||
foreach ( $random_posts as $post ) {
|
||||
$random_posts_data = array(
|
||||
'id' => $post->ID,
|
||||
'href' => get_permalink( $post->ID ),
|
||||
'date' => $post->post_date,
|
||||
'title' => $post->post_title,
|
||||
'type' => 'post',
|
||||
'public' => true,
|
||||
);
|
||||
|
||||
$data['summary']['postviews'][] = $random_posts_data;
|
||||
}
|
||||
|
||||
$data['summary']['postviews'] = array_slice( $data['summary']['postviews'], 0, 10 );
|
||||
}
|
||||
|
||||
$top_posts = array();
|
||||
|
||||
foreach ( $data['summary']['postviews'] as $post ) {
|
||||
$post_id = $post['id'];
|
||||
$thumbnail = get_the_post_thumbnail_url( $post_id );
|
||||
|
||||
if ( ! $thumbnail ) {
|
||||
$post_images = get_attached_media( 'image', $post_id );
|
||||
$post_image = reset( $post_images );
|
||||
if ( $post_image ) {
|
||||
$thumbnail = wp_get_attachment_url( $post_image->ID );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $post['public'] ) {
|
||||
$top_posts[] = array(
|
||||
'id' => $post_id,
|
||||
'author' => get_the_author_meta( 'display_name', get_post_field( 'post_author', $post_id ) ),
|
||||
'context' => get_the_category( $post_id ) ? get_the_category( $post_id ) : get_the_tags( $post_id ),
|
||||
'href' => $post['href'],
|
||||
'date' => get_the_date( '', $post_id ),
|
||||
'title' => $post['title'],
|
||||
'type' => $post['type'],
|
||||
'public' => $post['public'],
|
||||
'views' => isset( $post['views'] ) ? $post['views'] : 0,
|
||||
'thumbnail' => $thumbnail,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This applies for rendering the block front-end, but not for editing it.
|
||||
if ( $is_rendering_block ) {
|
||||
$acceptable_types = explode( ',', $types );
|
||||
|
||||
$top_posts = array_filter(
|
||||
$top_posts,
|
||||
function ( $item ) use ( $acceptable_types ) {
|
||||
return in_array( $item['type'], $acceptable_types, true );
|
||||
}
|
||||
);
|
||||
|
||||
$top_posts = array_slice( $top_posts, 0, $items_count );
|
||||
}
|
||||
|
||||
return $top_posts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,904 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* 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 https://www.opensource.org/licenses/MIT
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
// phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
|
||||
|
||||
if ( ! class_exists( 'Jetpack_Color' ) ) {
|
||||
/**
|
||||
* Color utilities
|
||||
*/
|
||||
class Jetpack_Color {
|
||||
/**
|
||||
* Color code (later array or string, depending on type)
|
||||
*
|
||||
* @var int|array|string
|
||||
*/
|
||||
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' ) {
|
||||
_deprecated_function( 'Jetpack_Color::__construct', 'jetpack-13.8' );
|
||||
|
||||
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 $hex_value Color hex value.
|
||||
*
|
||||
* @return $this
|
||||
* @throws RangeException Invalid color code range error.
|
||||
*/
|
||||
public function fromHex( $hex_value ) {
|
||||
$hex_value = str_replace( '#', '', $hex_value );
|
||||
// handle short hex codes like #fff.
|
||||
if ( 3 === strlen( $hex_value ) ) {
|
||||
$hex_value = $hex_value[0] . $hex_value[0] . $hex_value[1] . $hex_value[1] . $hex_value[2] . $hex_value[2];
|
||||
}
|
||||
return $this->fromInt( hexdec( $hex_value ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Init color from integer RGB values
|
||||
*
|
||||
* @param int $red Red color code.
|
||||
* @param int $green Green color code.
|
||||
* @param int $blue Blue color code.
|
||||
*
|
||||
* @return $this
|
||||
* @throws RangeException Invalid color code range error.
|
||||
*/
|
||||
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 = ( intval( $red ) << 16 ) + ( intval( $green ) << 8 ) + intval( $blue );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init color from hex RGB values
|
||||
*
|
||||
* @param string $red Red color code.
|
||||
* @param string $green Green color code.
|
||||
* @param string $blue Blue color code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
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 https://en.wikipedia.org/wiki/HSL_color_space.
|
||||
*
|
||||
* @param int $h Hue. [0-360].
|
||||
* @param int $s Saturation [0, 100].
|
||||
* @param int $l Lightness [0, 100].
|
||||
*/
|
||||
public function fromHsl( $h, $s, $l ) {
|
||||
$h /= 360;
|
||||
$s /= 100;
|
||||
$l /= 100;
|
||||
|
||||
if ( 0 === $s ) {
|
||||
// achromatic.
|
||||
$r = $l;
|
||||
$g = $l;
|
||||
$b = $l;
|
||||
} 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()
|
||||
*
|
||||
* @param float $p Minimum of R/G/B [0, 1].
|
||||
* @param float $q Maximum of R/G/B [0, 1].
|
||||
* @param float $t Adjusted hue [0, 1].
|
||||
*/
|
||||
private function hue2rgb( $p, $q, $t ) {
|
||||
if ( $t < 0 ) {
|
||||
++$t;
|
||||
}
|
||||
if ( $t > 1 ) {
|
||||
--$t;
|
||||
}
|
||||
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 $int_value Color code.
|
||||
*
|
||||
* @return $this
|
||||
* @throws RangeException Invalid color code range error.
|
||||
*/
|
||||
public function fromInt( $int_value ) {
|
||||
if ( $int_value < 0 || $int_value > 16777215 ) {
|
||||
throw new RangeException( $int_value . ' out of valid color code range' );
|
||||
}
|
||||
|
||||
$this->color = $int_value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert color to hex
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toHex() {
|
||||
return sprintf( '%06x', $this->color );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
$rgb_min = min( $rgb );
|
||||
$rgb_max = max( $rgb );
|
||||
|
||||
$hsv = array(
|
||||
'hue' => 0,
|
||||
'sat' => 0,
|
||||
'val' => $rgb_max,
|
||||
);
|
||||
|
||||
// If v is 0, color is black.
|
||||
if ( 0 === $hsv['val'] ) {
|
||||
return $hsv;
|
||||
}
|
||||
|
||||
// Normalize RGB values to 1.
|
||||
$rgb['red'] /= $hsv['val'];
|
||||
$rgb['green'] /= $hsv['val'];
|
||||
$rgb['blue'] /= $hsv['val'];
|
||||
$rgb_min = min( $rgb );
|
||||
$rgb_max = max( $rgb );
|
||||
|
||||
// Calculate saturation.
|
||||
$hsv['sat'] = $rgb_max - $rgb_min;
|
||||
if ( 0 === $hsv['sat'] ) {
|
||||
$hsv['hue'] = 0;
|
||||
return $hsv;
|
||||
}
|
||||
|
||||
// Normalize saturation to 1.
|
||||
$rgb['red'] = ( $rgb['red'] - $rgb_min ) / ( $rgb_max - $rgb_min );
|
||||
$rgb['green'] = ( $rgb['green'] - $rgb_min ) / ( $rgb_max - $rgb_min );
|
||||
$rgb['blue'] = ( $rgb['blue'] - $rgb_min ) / ( $rgb_max - $rgb_min );
|
||||
$rgb_min = min( $rgb );
|
||||
$rgb_max = max( $rgb );
|
||||
|
||||
// Calculate hue.
|
||||
if ( $rgb_max === $rgb['red'] ) {
|
||||
$hsv['hue'] = 0.0 + 60 * ( $rgb['green'] - $rgb['blue'] );
|
||||
if ( $hsv['hue'] < 0 ) {
|
||||
$hsv['hue'] += 360;
|
||||
}
|
||||
} elseif ( $rgb_max === $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 array
|
||||
*/
|
||||
public function toHsvInt() {
|
||||
$rgb = $this->toRgbInt();
|
||||
|
||||
$rgb_min = min( $rgb );
|
||||
$rgb_max = max( $rgb );
|
||||
|
||||
$hsv = array(
|
||||
'hue' => 0,
|
||||
'sat' => 0,
|
||||
'val' => $rgb_max,
|
||||
);
|
||||
|
||||
// If value is 0, color is black.
|
||||
if ( 0 === $hsv['val'] ) {
|
||||
return $hsv;
|
||||
}
|
||||
|
||||
// Calculate saturation.
|
||||
$hsv['sat'] = round( 255 * ( $rgb_max - $rgb_min ) / $hsv['val'] );
|
||||
if ( 0 === $hsv['sat'] ) {
|
||||
$hsv['hue'] = 0;
|
||||
return $hsv;
|
||||
}
|
||||
|
||||
// Calculate hue.
|
||||
if ( $rgb_max === $rgb['red'] ) {
|
||||
$hsv['hue'] = round( 0 + 43 * ( $rgb['green'] - $rgb['blue'] ) / ( $rgb_max - $rgb_min ) );
|
||||
} elseif ( $rgb_max === $rgb['green'] ) {
|
||||
$hsv['hue'] = round( 85 + 43 * ( $rgb['blue'] - $rgb['red'] ) / ( $rgb_max - $rgb_min ) );
|
||||
} else {
|
||||
$hsv['hue'] = round( 171 + 43 * ( $rgb['red'] - $rgb['green'] ) / ( $rgb_max - $rgb_min ) );
|
||||
}
|
||||
if ( $hsv['hue'] < 0 ) {
|
||||
$hsv['hue'] += 255;
|
||||
}
|
||||
|
||||
return $hsv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGB color value to HSL. Conversion formula
|
||||
* adapted from https://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 );
|
||||
$l = ( $max + $min ) / 2;
|
||||
|
||||
if ( $max === $min ) {
|
||||
// achromatic.
|
||||
$s = 0;
|
||||
$h = 0;
|
||||
} 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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* From a color code to a string to be used in CSS declaration.
|
||||
*
|
||||
* @param string $type Color code type.
|
||||
* @param int $alpha Transparency.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toCSS( $type = 'hex', $alpha = 1 ) {
|
||||
switch ( $type ) {
|
||||
case 'hex':
|
||||
return $this->toString();
|
||||
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} )";
|
||||
}
|
||||
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} )";
|
||||
}
|
||||
default:
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Color code.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDistanceRgbFrom( Jetpack_Color $color ) {
|
||||
$rgb1 = $this->toRgbInt();
|
||||
$rgb2 = $color->toRgbInt();
|
||||
|
||||
$r_diff = abs( $rgb1['red'] - $rgb2['red'] );
|
||||
$g_diff = abs( $rgb1['green'] - $rgb2['green'] );
|
||||
$b_diff = abs( $rgb1['blue'] - $rgb2['blue'] );
|
||||
|
||||
// Sum of RGB differences.
|
||||
$diff = $r_diff + $g_diff + $b_diff;
|
||||
return $diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get distance from the given color using the Delta E method
|
||||
*
|
||||
* @param Jetpack_Color $color Color code.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getDistanceLabFrom( Jetpack_Color $color ) {
|
||||
$lab1 = $this->toLabCie();
|
||||
$lab2 = $color->toLabCie();
|
||||
|
||||
$l_diff = abs( $lab2['l'] - $lab1['l'] );
|
||||
$a_diff = abs( $lab2['a'] - $lab1['a'] );
|
||||
$b_diff = abs( $lab2['b'] - $lab1['b'] );
|
||||
|
||||
$delta = sqrt( $l_diff + $a_diff + $b_diff );
|
||||
|
||||
return $delta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate luminosity.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum contrast color.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function getMaxContrastColor() {
|
||||
$with_black = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#000' ) );
|
||||
$with_white = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#fff' ) );
|
||||
$color = new Jetpack_Color();
|
||||
$hex = ( $with_black >= $with_white ) ? '#000000' : '#ffffff';
|
||||
return $color->fromHex( $hex );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grayscale contrasting color.
|
||||
*
|
||||
* @param bool|int $contrast Contrast.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
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 ( 0 === $this->color || 16777215 === $this->color ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if color is grayscale
|
||||
*
|
||||
* @param int $threshold Max difference between colors.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isGrayscale( $threshold = 16 ) {
|
||||
$rgb = $this->toRgbInt();
|
||||
|
||||
// Get min and max rgb values, then difference between them.
|
||||
$rgb_min = min( $rgb );
|
||||
$rgb_max = max( $rgb );
|
||||
$diff = $rgb_max - $rgb_min;
|
||||
|
||||
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 ) {
|
||||
$match_dist = 10000;
|
||||
$match_key = null;
|
||||
foreach ( $colors as $key => $color ) {
|
||||
if ( false === ( $color instanceof Jetpack_Color ) ) {
|
||||
$c = new Jetpack_Color( $color );
|
||||
}
|
||||
$dist = $this->getDistanceLabFrom( $c );
|
||||
if ( $dist < $match_dist ) {
|
||||
$match_dist = $dist;
|
||||
$match_key = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $match_key;
|
||||
}
|
||||
|
||||
/* TRANSFORMS */
|
||||
|
||||
/**
|
||||
* Transform -- Darken color.
|
||||
*
|
||||
* @param int $amount Amount. Default to 5.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function darken( $amount = 5 ) {
|
||||
return $this->incrementLightness( - $amount );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- Lighten color.
|
||||
*
|
||||
* @param int $amount Amount. Default to 5.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function lighten( $amount = 5 ) {
|
||||
return $this->incrementLightness( $amount );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- Increment lightness.
|
||||
*
|
||||
* @param int $amount Amount.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function incrementLightness( $amount ) {
|
||||
$hsl = $this->toHsl();
|
||||
|
||||
$h = isset( $hsl['h'] ) ? $hsl['h'] : 0;
|
||||
$s = isset( $hsl['s'] ) ? $hsl['s'] : 0;
|
||||
$l = isset( $hsl['l'] ) ? $hsl['l'] : 0;
|
||||
|
||||
$l += $amount;
|
||||
if ( $l < 0 ) {
|
||||
$l = 0;
|
||||
}
|
||||
if ( $l > 100 ) {
|
||||
$l = 100;
|
||||
}
|
||||
return $this->fromHsl( $h, $s, $l );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- Saturate color.
|
||||
*
|
||||
* @param int $amount Amount. Default to 15.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function saturate( $amount = 15 ) {
|
||||
return $this->incrementSaturation( $amount );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- Desaturate color.
|
||||
*
|
||||
* @param int $amount Amount. Default to 15.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function desaturate( $amount = 15 ) {
|
||||
return $this->incrementSaturation( - $amount );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- Increment saturation.
|
||||
*
|
||||
* @param int $amount Amount.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function incrementSaturation( $amount ) {
|
||||
$hsl = $this->toHsl();
|
||||
|
||||
$h = isset( $hsl['h'] ) ? $hsl['h'] : 0;
|
||||
$s = isset( $hsl['s'] ) ? $hsl['s'] : 0;
|
||||
$l = isset( $hsl['l'] ) ? $hsl['l'] : 0;
|
||||
|
||||
$s += $amount;
|
||||
if ( $s < 0 ) {
|
||||
$s = 0;
|
||||
}
|
||||
if ( $s > 100 ) {
|
||||
$s = 100;
|
||||
}
|
||||
return $this->fromHsl( $h, $s, $l );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- To grayscale.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function toGrayscale() {
|
||||
$hsl = $this->toHsl();
|
||||
|
||||
$h = isset( $hsl['h'] ) ? $hsl['h'] : 0;
|
||||
$s = 0;
|
||||
$l = isset( $hsl['l'] ) ? $hsl['l'] : 0;
|
||||
|
||||
return $this->fromHsl( $h, $s, $l );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- To the complementary color.
|
||||
*
|
||||
* The complement is the color on the opposite side of the color wheel, 180° away.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function getComplement() {
|
||||
return $this->incrementHue( 180 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- To an analogous color of the complement.
|
||||
*
|
||||
* @param int $step Pass `1` or `-1` to choose which direction around the color wheel.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function getSplitComplement( $step = 1 ) {
|
||||
$incr = 180 + ( $step * 30 );
|
||||
return $this->incrementHue( $incr );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- To an analogous color.
|
||||
*
|
||||
* Analogous colors are those adjacent on the color wheel, separated by 30°.
|
||||
*
|
||||
* @param int $step Pass `1` or `-1` to choose which direction around the color wheel.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function getAnalog( $step = 1 ) {
|
||||
$incr = $step * 30;
|
||||
return $this->incrementHue( $incr );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- To a tetradic (rectangular) color.
|
||||
*
|
||||
* A rectangular color scheme uses a color, its complement, and the colors 60° from each.
|
||||
* This transforms the color to its 60° "tetrad".
|
||||
*
|
||||
* @param int $step Pass `1` or `-1` to choose which direction around the color wheel.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function getTetrad( $step = 1 ) {
|
||||
$incr = $step * 60;
|
||||
return $this->incrementHue( $incr );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- To a triadic color.
|
||||
*
|
||||
* A triadic color scheme uses three colors evenly spaced (120°) around the color wheel.
|
||||
* This transforms the color to one of its triadic colors.
|
||||
*
|
||||
* @param int $step Pass `1` or `-1` to choose which direction around the color wheel.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function getTriad( $step = 1 ) {
|
||||
$incr = $step * 120;
|
||||
return $this->incrementHue( $incr );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform -- Increment hue.
|
||||
*
|
||||
* @param int $amount Amount.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function incrementHue( $amount ) {
|
||||
$hsl = $this->toHsl();
|
||||
|
||||
$h = isset( $hsl['h'] ) ? $hsl['h'] : 0;
|
||||
$s = isset( $hsl['s'] ) ? $hsl['s'] : 0;
|
||||
$l = isset( $hsl['l'] ) ? $hsl['l'] : 0;
|
||||
|
||||
$h = ( $h + $amount ) % 360;
|
||||
if ( $h < 0 ) {
|
||||
$h += 360;
|
||||
}
|
||||
return $this->fromHsl( $h, $s, $l );
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase
|
||||
/**
|
||||
* This file has been moved to the jetpack-plugins-installer package
|
||||
*
|
||||
* @deprecated 10.7
|
||||
*
|
||||
* @package jetpack
|
||||
*/
|
||||
|
||||
class_alias( Automattic\Jetpack\Automatic_Install_Skin::class, 'Jetpack_Automatic_Install_Skin' );
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Tweak a preview when rendered in an iframe.
|
||||
* This is used when rendering iFrames in the Calypso app.
|
||||
*
|
||||
* This file is shared between WordPress.com and Jetpack.
|
||||
* The canonical source is Jetpack and no WordPress.com-specific code should exist in this file.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tweak a preview when rendered in an iframe.
|
||||
*/
|
||||
class Jetpack_Iframe_Embed {
|
||||
/**
|
||||
* Initialize class.
|
||||
*/
|
||||
public 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' ) );
|
||||
|
||||
$ver = sprintf( '%s-%s', gmdate( 'oW' ), defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : '' );
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
wp_enqueue_script(
|
||||
'jetpack-iframe-embed',
|
||||
'/wp-content/mu-plugins/jetpack-iframe-embed/jetpack-iframe-embed.js',
|
||||
array( 'jquery' ),
|
||||
$ver,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
wp_enqueue_script(
|
||||
'jetpack-iframe-embed',
|
||||
'//s0.wp.com/wp-content/mu-plugins/jetpack-iframe-embed/jetpack-iframe-embed.js',
|
||||
array( 'jquery' ),
|
||||
$ver,
|
||||
false
|
||||
);
|
||||
}
|
||||
wp_localize_script( 'jetpack-iframe-embed', '_previewSite', array( 'siteURL' => get_site_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that we are in an iFrame.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_embedding_in_iframe() {
|
||||
return (
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- No nonce needed, we're only checking for a specific screen view.
|
||||
isset( $_GET['iframe'] ) && 'true' === $_GET['iframe']
|
||||
&& (
|
||||
isset( $_GET['preview'] ) && 'true' === $_GET['preview']
|
||||
|| isset( $_GET['theme_preview'] ) && 'true' === $_GET['theme_preview']
|
||||
)
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public 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`
|
||||
*/
|
||||
public 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`
|
||||
*/
|
||||
public static function base_target_blank() {
|
||||
echo '<base target="_blank" />';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Utilities to interact with a Keyring instance.
|
||||
* Used for Publicize as well as the Site Verification tools.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Secrets;
|
||||
|
||||
/**
|
||||
* A series of utilities to interact with a Keyring instance.
|
||||
*/
|
||||
class Jetpack_Keyring_Service_Helper {
|
||||
/**
|
||||
* Class instance
|
||||
*
|
||||
* @var Jetpack_Keyring_Service_Helper
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Whether the `sharing` page is registered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $is_sharing_page_registered = false;
|
||||
|
||||
/**
|
||||
* Initialize instance.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new Jetpack_Keyring_Service_Helper();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
const SERVICES = array(
|
||||
'facebook' => array(
|
||||
'for' => 'publicize',
|
||||
),
|
||||
// @todo Remove when Twitter has been dropped from 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',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
add_action( 'admin_menu', array( __CLASS__, 'register_sharing_page' ) );
|
||||
|
||||
add_action( 'load-settings_page_sharing', array( __CLASS__, 'admin_page_load' ), 9 );
|
||||
}
|
||||
|
||||
/**
|
||||
* We need a `sharing` page to be able to connect and disconnect services.
|
||||
*/
|
||||
public static function register_sharing_page() {
|
||||
if ( self::$is_sharing_page_registered ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$is_sharing_page_registered = true;
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $_registered_pages;
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
|
||||
$hookname = get_plugin_page_hookname( 'sharing', 'options-general.php' );
|
||||
add_action( $hookname, array( __CLASS__, 'admin_page_load' ) );
|
||||
$_registered_pages[ $hookname ] = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of services.
|
||||
*
|
||||
* @param string $filter Choose 'all' to get all connected services, vs. just the connected ones.
|
||||
*/
|
||||
public 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.
|
||||
* On WordPress.com this is/calls Keyring::admin_url.
|
||||
*
|
||||
* @param string $service Shortname of a specific service.
|
||||
* @param array $params Parameters to append to an API connection URL.
|
||||
*
|
||||
* @return string URL to specific public-api process
|
||||
*/
|
||||
private 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a connection URL (sharing settings page with unique query args to create a connection).
|
||||
*
|
||||
* @param string $service_name Service name.
|
||||
* @param string $for Feature name.
|
||||
*/
|
||||
public 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,
|
||||
),
|
||||
admin_url( 'options-general.php?page=sharing' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a URL to refresh a connection (sharing settings page with unique query args to refresh a connection).
|
||||
* Similar to connect_url, but with a refresh parameter.
|
||||
*
|
||||
* @param string $service_name Service name.
|
||||
* @param string $for Feature name.
|
||||
*/
|
||||
public 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' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a URL to delete a connection (sharing settings page with unique query args to delete a connection).
|
||||
*
|
||||
* @param string $service_name Service name.
|
||||
* @param string $id Connection ID.
|
||||
*/
|
||||
public 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" ),
|
||||
),
|
||||
admin_url( 'options-general.php?page=sharing' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build contents handling Keyring connection management into Sharing settings screen.
|
||||
*/
|
||||
public static function admin_page_load() {
|
||||
if ( isset( $_GET['action'] ) ) {
|
||||
if ( isset( $_GET['service'] ) ) {
|
||||
$service_name = sanitize_text_field( wp_unslash( $_GET['service'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We verify below.
|
||||
}
|
||||
|
||||
switch ( $_GET['action'] ) {
|
||||
|
||||
case 'request':
|
||||
check_admin_referer( 'keyring-request', 'kr_nonce' );
|
||||
check_admin_referer( "keyring-request-$service_name", 'nonce' );
|
||||
|
||||
$verification = ( new Secrets() )->generate( 'publicize' );
|
||||
if ( ! $verification ) {
|
||||
$url = Jetpack::admin_url( 'jetpack#/settings' );
|
||||
wp_die(
|
||||
sprintf(
|
||||
wp_kses(
|
||||
/* Translators: placeholder is a URL to a Settings page. */
|
||||
__( "Jetpack is not connected. Please connect Jetpack by visiting <a href='%s'>Settings</a>.", 'jetpack' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
),
|
||||
esc_url( $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 = self::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 ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- The API URL is an external URL and is filterable.
|
||||
exit( 0 );
|
||||
|
||||
case 'completed':
|
||||
/*
|
||||
* We do not use a nonce here,
|
||||
* since we're populating a local cache of
|
||||
* the Publicize connections that were created and stored on WordPress.com.
|
||||
*/
|
||||
$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 = isset( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : null;
|
||||
|
||||
check_admin_referer( 'keyring-request', 'kr_nonce' );
|
||||
check_admin_referer( "keyring-request-$service_name", 'nonce' );
|
||||
|
||||
self::disconnect( $service_name, $id );
|
||||
|
||||
do_action( 'connection_disconnected', $service_name );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a Publicize connection
|
||||
*
|
||||
* @param string $service_name Service name.
|
||||
* @param string $connection_id Connection ID.
|
||||
* @param int|bool $_blog_id Blog ID.
|
||||
* @param int|bool $_user_id User ID.
|
||||
* @param bool $force_delete Force delete the connection.
|
||||
*/
|
||||
public static function disconnect( $service_name, $connection_id, $_blog_id = false, $_user_id = false, $force_delete = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$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,142 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
/**
|
||||
* Measure the performance of Jetpack Search queries.
|
||||
*/
|
||||
class Jetpack_Search_Performance_Logger {
|
||||
/**
|
||||
* Jetpack_Search_Performance_Logger instance.
|
||||
*
|
||||
* @var null|Jetpack_Search_Performance_Logger
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* WP_Query instance.
|
||||
*
|
||||
* @var null|WP_Query
|
||||
*/
|
||||
private $current_query = null;
|
||||
|
||||
/**
|
||||
* Time when the query was started.
|
||||
*
|
||||
* @var null|float
|
||||
*/
|
||||
private $query_started = null;
|
||||
|
||||
/**
|
||||
* Performance results.
|
||||
*
|
||||
* @var null|array
|
||||
*/
|
||||
private $stats = null;
|
||||
|
||||
/**
|
||||
* Initialize the class.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new Jetpack_Search_Performance_Logger();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
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' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the time when the query was started.
|
||||
*
|
||||
* @param WP_Query $query The query.
|
||||
*/
|
||||
public function begin_log_query( $query ) {
|
||||
if ( $this->should_log_query( $query ) ) {
|
||||
$this->query_started = microtime( true );
|
||||
$this->current_query = $query;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the time when an SQL query was completed.
|
||||
*
|
||||
* @param int $found_posts The number of posts found.
|
||||
* @param WP_Query $query The WP_Query instance (passed by reference).
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log Jetpack Search query.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset data after each log.
|
||||
*/
|
||||
private function reset_query_state() {
|
||||
$this->query_started = null;
|
||||
$this->current_query = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a query should be logged (a main query, or a jetpack search query).
|
||||
*
|
||||
* @param WP_Query $query The WP_Query instance.
|
||||
*/
|
||||
private function should_log_query( $query ) {
|
||||
return $query->is_main_query() && $query->is_search();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the time of a query.
|
||||
*
|
||||
* @param float $duration The duration of the query.
|
||||
* @param bool $was_jetpack_search Was this a Jetpack Search query.
|
||||
*/
|
||||
private function record_query_time( $duration, $was_jetpack_search ) {
|
||||
$this->stats[] = array( $was_jetpack_search, (int) ( $duration * 1000 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Print performance stats in the footer.
|
||||
*/
|
||||
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 = rawurlencode( site_url() );
|
||||
$url = "https://pixel.wp.com/boom.gif?v=0.9&u={$encoded_site_url}&json={$encoded_json}";
|
||||
echo '<img src="' . esc_url( $url ) . '" width="1" height="1" style="display:none;" alt=""/>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Class with methods to extract metadata from a post/page about videos, images, links, mentions embedded
|
||||
* in or attached to the post/page.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Shortcodes to keep.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private static $keeper_shortcodes = array(
|
||||
'audio',
|
||||
'youtube',
|
||||
'vimeo',
|
||||
'hulu',
|
||||
'ted',
|
||||
'video',
|
||||
'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 int $blog_id The ID of the blog.
|
||||
* @param int $post_id The ID of the post.
|
||||
* @param int $what_to_extract A mask of things to extract, e.g. Jetpack_Media_Meta_Extractor::IMAGES | Jetpack_Media_Meta_Extractor::MENTIONS.
|
||||
* @param boolean $extract_alt_text Should alt_text be extracted, defaults to false.
|
||||
*
|
||||
* @return array|WP_Error a structure containing metadata about the embedded things, or empty array if nothing found, or WP_Error on error.
|
||||
*/
|
||||
public static function extract( $blog_id, $post_id, $what_to_extract = self::ALL, $extract_alt_text = false ) {
|
||||
|
||||
// multisite?
|
||||
if ( function_exists( 'switch_to_blog' ) ) {
|
||||
switch_to_blog( $blog_id );
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post instanceof WP_Post ) {
|
||||
if ( function_exists( 'restore_current_blog' ) ) {
|
||||
restore_current_blog();
|
||||
}
|
||||
return array();
|
||||
}
|
||||
$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, array(), $extract_alt_text );
|
||||
|
||||
// Turn off images so we can safely call extract_from_content() below.
|
||||
$what_to_extract = $what_to_extract - self::IMAGES;
|
||||
}
|
||||
|
||||
if ( function_exists( 'restore_current_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 string $content The HTML post_content of a post.
|
||||
* @param int $what_to_extract A mask of things to extract, e.g. Jetpack_Media_Meta_Extractor::IMAGES | Jetpack_Media_Meta_Extractor::MENTIONS.
|
||||
* @param array $already_extracted Previously extracted things, e.g. images from extract(), which can be used for x-referencing here.
|
||||
*
|
||||
* @return array a structure containing metadata about the embedded things, or empty array if nothing found, or WP_Error on error.
|
||||
*/
|
||||
public static 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 = self::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, true ) ) {
|
||||
$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, true ) ) {
|
||||
// Clear shortcode ID data left from the last shortcode.
|
||||
$id = null;
|
||||
// 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 );
|
||||
} elseif ( method_exists( $shortcode_class_name, $shortcode_get_id_method ) ) {
|
||||
$id = call_user_func( array( $shortcode_class_name, $shortcode_get_id_method ), $attr );
|
||||
} elseif ( 'video' === $shortcode ) {
|
||||
$id = $attr['src'] ?? $attr['url'] ?? $attr['mp4'] ?? $attr['m4v'] ?? $attr['webm'] ?? $attr['ogv'] ?? $attr['wmv'] ?? $attr['flv'] ?? null;
|
||||
} elseif ( 'audio' === $shortcode ) {
|
||||
preg_match( '#(https?://(?:[^\s"|\']+)\.(?:mp3|ogg|flac|m4a|wav))([ "\'|]|$)#', implode( ' ', $attr ), $audio_matches );
|
||||
$id = $audio_matches[1] ?? null;
|
||||
}
|
||||
if ( ! empty( $id )
|
||||
&& ( ! isset( $shortcode_details[ $shortcode_name ] ) || ! in_array( $id, $shortcode_details[ $shortcode_name ], true ) ) ) {
|
||||
$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 = wp_parse_url( $link_raw );
|
||||
|
||||
// Data URI links.
|
||||
if ( ! isset( $url['scheme'] ) || 'data' === $url['scheme'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reject invalid URLs.
|
||||
if ( ! isset( $url['host'] ) ) {
|
||||
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'], true ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
list( $proto, $link_all_but_proto ) = explode( '://', $link_raw ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
// 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'];
|
||||
}
|
||||
}
|
||||
|
||||
$link = array(
|
||||
'url' => $link_all_but_proto,
|
||||
'host_reversed' => $host_reversed,
|
||||
'host' => $url['host'],
|
||||
);
|
||||
if ( ! in_array( $link, $links, true ) ) {
|
||||
$links[] = $link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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 = wp_parse_url( $link_raw );
|
||||
|
||||
list( $proto, $link_all_but_proto ) = explode( '://', $link_raw ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
// Check whether this "link" is really an embed.
|
||||
foreach ( $oembed->providers as $matchmask => $data ) {
|
||||
list( $providerurl, $regex ) = $data; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
// 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 ) ) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image fields for matching images.
|
||||
*
|
||||
* @uses Jetpack_PostImages
|
||||
*
|
||||
* @param WP_Post $post A post object.
|
||||
* @param array $args Optional args, see defaults list for details.
|
||||
* @param boolean $extract_alt_text Should alt_text be extracted, defaults to false.
|
||||
*
|
||||
* @return array Returns an array of all images meeting the specified criteria in $args.
|
||||
*/
|
||||
private static function get_image_fields( $post, $args = array(), $extract_alt_text = false ) {
|
||||
|
||||
if ( ! $post instanceof WP_Post ) {
|
||||
return 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 ) ) {
|
||||
if ( $extract_alt_text ) {
|
||||
$image_list = array_merge( $image_list, self::reduce_extracted_images( $from_featured_image ) );
|
||||
} else {
|
||||
$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 ) ) {
|
||||
if ( $extract_alt_text ) {
|
||||
$image_list = array_merge( $image_list, self::reduce_extracted_images( $from_slideshow ) );
|
||||
} else {
|
||||
$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 ) ) {
|
||||
if ( $extract_alt_text ) {
|
||||
$image_list = array_merge( $image_list, self::reduce_extracted_images( $from_gallery ) );
|
||||
} else {
|
||||
$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 = self::get_images_from_html( $post->post_content, $image_list, $extract_alt_text );
|
||||
|
||||
return self::build_image_struct( $image_list, $image_booleans );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an extracted image array reduce to src, alt_text, src_width, and src_height.
|
||||
*
|
||||
* @param array $images extracted image array.
|
||||
*
|
||||
* @return array reduced image array
|
||||
*/
|
||||
protected static function reduce_extracted_images( $images ) {
|
||||
$ret_images = array();
|
||||
foreach ( $images as $image ) {
|
||||
// skip if src isn't set.
|
||||
if ( empty( $image['src'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$ret_image = array(
|
||||
'url' => $image['src'],
|
||||
);
|
||||
if ( ! empty( $image['src_height'] ) || ! empty( $image['src_width'] ) ) {
|
||||
$ret_image['src_width'] = $image['src_width'] ?? '';
|
||||
$ret_image['src_height'] = $image['src_height'] ?? '';
|
||||
}
|
||||
if ( ! empty( $image['alt_text'] ) ) {
|
||||
$ret_image['alt_text'] = $image['alt_text'];
|
||||
} else {
|
||||
$ret_image = $image['src'];
|
||||
}
|
||||
$ret_images[] = $ret_image;
|
||||
}
|
||||
return $ret_images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get images from HTML and return it with the set sturcture.
|
||||
*
|
||||
* @param string $content HTML content.
|
||||
* @param array $image_list Array of already found images.
|
||||
* @param string $extract_alt_text Whether or not to extract the alt text.
|
||||
*
|
||||
* @return array|array[] Array of images.
|
||||
*/
|
||||
public static function extract_images_from_content( $content, $image_list, $extract_alt_text = false ) {
|
||||
$image_list = self::get_images_from_html( $content, $image_list, $extract_alt_text );
|
||||
return self::build_image_struct( $image_list, array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a set structure for extracted media items.
|
||||
*
|
||||
* @param array $image_list Array of images.
|
||||
* @param array $image_booleans Image booleans.
|
||||
*
|
||||
* @return array|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, SORT_REGULAR );
|
||||
foreach ( $image_list as $img ) {
|
||||
if ( is_string( $img ) ) {
|
||||
$retval['image'][] = array( 'url' => $img );
|
||||
} else {
|
||||
$retval['image'][] = $img;
|
||||
}
|
||||
}
|
||||
$image_booleans['image'] = count( $retval['image'] );
|
||||
if ( ! empty( $image_booleans ) ) {
|
||||
$retval['has'] = $image_booleans;
|
||||
}
|
||||
return $retval;
|
||||
} else {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts images from html.
|
||||
*
|
||||
* @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.
|
||||
* @param boolean $extract_alt_text Should alt_text be extracted, defaults to false.
|
||||
*
|
||||
* @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, $extract_alt_text = false ) {
|
||||
$image_list = $images_already_extracted;
|
||||
$from_html = Jetpack_PostImages::from_html( $html );
|
||||
// early return if no image in html.
|
||||
if ( empty( $from_html ) ) {
|
||||
return $image_list;
|
||||
}
|
||||
// process images.
|
||||
foreach ( $from_html as $extracted_image ) {
|
||||
$image_url = $extracted_image['src'];
|
||||
$length = strpos( $image_url, '?' );
|
||||
$src = wp_parse_url( $image_url );
|
||||
|
||||
if ( $src && isset( $src['scheme'] ) && isset( $src['host'] ) && isset( $src['path'] ) ) {
|
||||
// Rebuild the URL without the query string.
|
||||
$queryless = $src['scheme'] . '://' . $src['host'] . $src['path'];
|
||||
} elseif ( $length ) {
|
||||
// If wp_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, true ) ) {
|
||||
$image_to_add = array(
|
||||
'url' => $queryless,
|
||||
);
|
||||
if ( $extract_alt_text ) {
|
||||
if ( ! empty( $extracted_image['alt_text'] ) ) {
|
||||
$image_to_add['alt_text'] = $extracted_image['alt_text'];
|
||||
}
|
||||
if ( ! empty( $extracted_image['src_width'] ) || ! empty( $extracted_image['src_height'] ) ) {
|
||||
$image_to_add['src_width'] = $extracted_image['src_width'];
|
||||
$image_to_add['src_height'] = $extracted_image['src_height'];
|
||||
}
|
||||
} else {
|
||||
$image_to_add = $queryless;
|
||||
}
|
||||
$image_list[] = $image_to_add;
|
||||
}
|
||||
}
|
||||
return $image_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips concents of all tags, shortcodes, and decodes HTML entities.
|
||||
*
|
||||
* @param string $content Original content.
|
||||
*
|
||||
* @return string Cleaned content.
|
||||
*/
|
||||
private static function get_stripped_content( $content ) {
|
||||
$clean_content = wp_strip_all_tags( $content );
|
||||
$clean_content = html_entity_decode( $clean_content, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
|
||||
// completely strip shortcodes and any content they enclose.
|
||||
$clean_content = strip_shortcodes( $clean_content );
|
||||
return $clean_content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Provides media summary of a post.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Media_Summary
|
||||
*
|
||||
* Priority: embed [video] > gallery > image > text
|
||||
*/
|
||||
class Jetpack_Media_Summary {
|
||||
|
||||
/**
|
||||
* Media cache.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $cache = array();
|
||||
|
||||
/**
|
||||
* Get media summary for a post.
|
||||
*
|
||||
* @param ?int $post_id Post ID.
|
||||
* @param int $blog_id Blog ID, if applicable.
|
||||
* @param array $args {
|
||||
* Optional. An array of arguments.
|
||||
* @type int $max_words Maximum number of words.
|
||||
* @type int $max_chars Maximum number of characters.
|
||||
* }
|
||||
*
|
||||
* @return array|mixed|void
|
||||
*/
|
||||
public static function get( ?int $post_id, int $blog_id = 0, array $args = array() ) {
|
||||
$post_id = (int) $post_id;
|
||||
$blog_id = (int) $blog_id;
|
||||
|
||||
$defaults = array(
|
||||
'max_words' => 16,
|
||||
'max_chars' => 256,
|
||||
);
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$switched = false;
|
||||
if ( ! empty( $blog_id ) && get_current_blog_id() !== $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 ] ) ) {
|
||||
if ( $switched ) {
|
||||
restore_current_blog();
|
||||
}
|
||||
return self::$cache[ $cache_key ];
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'Jetpack_Media_Meta_Extractor' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media-extractor.php';
|
||||
}
|
||||
|
||||
$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 ( $post instanceof WP_Post && 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'] ) ) {
|
||||
if ( $switched ) {
|
||||
restore_current_blog();
|
||||
}
|
||||
self::$cache[ $cache_key ] = $return;
|
||||
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'] ) {
|
||||
if ( ! isset( $extract['shortcode']['youtube']['id'][0] ) ) {
|
||||
break;
|
||||
}
|
||||
$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 = wp_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 ( str_contains( $embed, 'youtube' ) ) {
|
||||
$return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
|
||||
$return['secure']['image'] = self::https( $return['image'] );
|
||||
} elseif ( str_contains( $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'] );
|
||||
} elseif ( str_contains( $embed, 'vimeo' ) ) {
|
||||
$poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
|
||||
if ( ! empty( $poster_image ) ) {
|
||||
$return['image'] = $poster_image;
|
||||
$poster_url_parts = wp_parse_url( $poster_image );
|
||||
$return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
|
||||
}
|
||||
} elseif ( str_contains( $embed, 'dailymotion' ) ) {
|
||||
$return['image'] = str_replace( 'dailymotion.com/video/', 'dailymotion.com/thumbnail/video/', $embed );
|
||||
$return['image'] = wp_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( wp_strip_all_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'];
|
||||
}
|
||||
} elseif ( ! empty( $extract['has']['image'] ) ) {
|
||||
// ... Or we try and select a single image that would make sense.
|
||||
$content = wpautop( wp_strip_all_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 ( str_contains( $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 && is_countable( $extract['image'] ) && 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts http to https://
|
||||
*
|
||||
* @param string $str URL.
|
||||
*
|
||||
* @return string URL.
|
||||
*/
|
||||
public static function https( $str ) {
|
||||
return str_replace( 'http://', 'https://', $str );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Photonized version of the URL.
|
||||
*
|
||||
* @param string $url URL.
|
||||
*
|
||||
* @return string URL.
|
||||
*/
|
||||
public static function ssl_img( $url ) {
|
||||
if ( str_contains( $url, 'files.wordpress.com' ) ) {
|
||||
return self::https( $url );
|
||||
} else {
|
||||
return self::https( Image_CDN_Core::cdn_url( $url ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the video poster.
|
||||
*
|
||||
* @param string $type Video service.
|
||||
* @param string $id Video ID for the service.
|
||||
*
|
||||
* @return string URL of image thumbnail for the video.
|
||||
*/
|
||||
public 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 );
|
||||
} elseif ( class_exists( 'VideoPress_Video' ) ) {
|
||||
$video = new VideoPress_Video( $id );
|
||||
return $video->poster_frame_uri;
|
||||
}
|
||||
} elseif ( 'youtube' === $type ) {
|
||||
return 'http://img.youtube.com/vi/' . $id . '/0.jpg';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean text of shortcodes and tags.
|
||||
*
|
||||
* @param string $text Dirty text.
|
||||
*
|
||||
* @return string Clean text.
|
||||
*/
|
||||
public static function clean_text( $text ) {
|
||||
return trim(
|
||||
preg_replace(
|
||||
'/[\s]+/',
|
||||
' ',
|
||||
preg_replace(
|
||||
'@https?://[\S]+@',
|
||||
'',
|
||||
strip_shortcodes(
|
||||
wp_strip_all_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.
|
||||
**/
|
||||
public 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 ( empty( $post_excerpt ) && 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 ) {
|
||||
// @todo Refactor to not need to override the global.
|
||||
// phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$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 );
|
||||
// phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$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.
|
||||
*
|
||||
* @return array Array of words.
|
||||
*/
|
||||
public static function split_content_in_words( $text ) {
|
||||
$words = preg_split( '/[\s!?;,.]+/', $text, -1, PREG_SPLIT_NO_EMPTY );
|
||||
|
||||
// Return an empty array if the split above fails.
|
||||
return $words ? $words : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the word count.
|
||||
*
|
||||
* @param string $post_content Post content.
|
||||
*
|
||||
* @return int Word count.
|
||||
*/
|
||||
public static function get_word_count( $post_content ) {
|
||||
return (int) count( self::split_content_in_words( self::clean_text( $post_content ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remainder word count (after the excerpt).
|
||||
*
|
||||
* @param string $post_content Post content.
|
||||
* @param string $excerpt_content Excerpt content.
|
||||
*
|
||||
* @return int Number of words after the excerpt.
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of links in a post.
|
||||
*
|
||||
* @param string $post_content Post content.
|
||||
*
|
||||
* @return false|int Number of links.
|
||||
*/
|
||||
public static function get_link_count( $post_content ) {
|
||||
return preg_match_all( '/\<a[\> ]/', $post_content, $matches );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
require_once JETPACK__PLUGIN_DIR . 'sal/class.json-api-date.php';
|
||||
|
||||
/**
|
||||
* Class to handle different actions related to media.
|
||||
*/
|
||||
class Jetpack_Media {
|
||||
/**
|
||||
* Original media meta data. Metadata key as stored by WP.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const WP_ORIGINAL_MEDIA = '_wp_original_post_media';
|
||||
/**
|
||||
* Revision history. Metadata key as stored by WP.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const WP_REVISION_HISTORY = '_wp_revision_history';
|
||||
/**
|
||||
* Maximum amount of revisions.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const REVISION_HISTORY_MAXIMUM_AMOUNT = 0;
|
||||
/**
|
||||
* Image Alt. Metadata key as stored by WP.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const 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 int $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'];
|
||||
|
||||
// Get the file parts from the current attachment.
|
||||
$current_file = get_attached_file( $media_id );
|
||||
$current_file_parts = pathinfo( $current_file );
|
||||
$current_file_ext = $current_file_parts['extension'];
|
||||
$current_file_dirname = $current_file_parts['dirname'];
|
||||
|
||||
// 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 {
|
||||
$filename_base = $current_file_parts['filename'];
|
||||
}
|
||||
|
||||
// Add unique seed based on the filename.
|
||||
$filename_base .= '-' . crc32( $filename_base ) . '-';
|
||||
|
||||
$number_suffix = time() . wp_rand( 100, 999 );
|
||||
|
||||
do {
|
||||
$filename = $filename_base;
|
||||
$filename .= "e{$number_suffix}";
|
||||
$file_ext = $new_file_ext ? $new_file_ext : $current_file_ext;
|
||||
|
||||
$new_filename = "{$filename}.{$file_ext}";
|
||||
$new_path = "{$current_file_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 int $media_id Attachment ID.
|
||||
* @return string
|
||||
*/
|
||||
private static function get_time_string_from_guid( $media_id ) {
|
||||
$time = gmdate( 'Y/m', strtotime( current_time( 'mysql' ) ) );
|
||||
|
||||
$media = get_post( $media_id );
|
||||
if ( $media ) {
|
||||
$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.
|
||||
*
|
||||
* @param array $default_mime_types Array of mime types.
|
||||
*
|
||||
* @return array mime_type array
|
||||
*/
|
||||
public 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 ) {
|
||||
return jetpack_is_file_supported_for_sideloading( $file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given uploaded temporary 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.
|
||||
*
|
||||
* Note this does not support sideloads, only uploads.
|
||||
*
|
||||
* @param array $file_array Data derived from `$_FILES` for an uploaded file.
|
||||
* @param int $media_id Attachment 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 ( ! is_uploaded_file( $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 )
|
||||
) {
|
||||
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_upload( $file_array, $overrides, $time );
|
||||
|
||||
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 array|WP_Error $file - File data, or WP_Error on error.
|
||||
* @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 int $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.
|
||||
*
|
||||
* @param int $media_id Attachment ID.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file.
|
||||
*
|
||||
* @param string $pathname Path name.
|
||||
*/
|
||||
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 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch
|
||||
}
|
||||
|
||||
return wp_delete_file( $pathname );
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to delete a file according to the dirname of
|
||||
* the media attached file and the filename.
|
||||
*
|
||||
* @param int $media_id - media post ID.
|
||||
* @param string $filename - basename of the file ( name-of-file.ext ).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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 $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 int $media_id - media post ID.
|
||||
* @param array $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, $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 int $media_id - media post ID.
|
||||
* @param null|int $limit - maximum amount of items. 20 as default.
|
||||
*
|
||||
* @return array items removed from `revision_history`
|
||||
*/
|
||||
public static function limit_revision_history( $media_id, $limit = null ) {
|
||||
if ( $limit === null ) {
|
||||
$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 int $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 int $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
|
||||
*
|
||||
* Note this does not support sideloads, only uploads.
|
||||
*
|
||||
* @param int $media_id - media post ID.
|
||||
* @param array $file_array - Data derived from `$_FILES` for an uploaded file.
|
||||
* @return WP_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 ) ) {
|
||||
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 new WP_Error( 'update_error', 'Media update error' );
|
||||
}
|
||||
|
||||
// Check maximum amount of revision_history before updating the attachment metadata.
|
||||
self::limit_revision_history( $media_id );
|
||||
|
||||
$new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
|
||||
wp_update_attachment_metadata( $media_id, $new_metadata );
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
|
||||
|
||||
/**
|
||||
* Clean revision history when the media item is deleted.
|
||||
*
|
||||
* @param int $media_id Attachment ID.
|
||||
*/
|
||||
function jetpack_clean_revision_history( $media_id ) {
|
||||
Jetpack_Media::clean_revision_history( $media_id );
|
||||
}
|
||||
add_action( 'delete_attachment', 'jetpack_clean_revision_history' );
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
use Automattic\Jetpack\Status;
|
||||
|
||||
/**
|
||||
* Components Library
|
||||
*
|
||||
* Load and display a pre-rendered component
|
||||
*/
|
||||
class Jetpack_Components {
|
||||
/**
|
||||
* Load and display a pre-rendered component
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @param string $name Component name.
|
||||
* @param array $props Component properties.
|
||||
*
|
||||
* @return string The component markup
|
||||
*/
|
||||
public static function render_component( $name, $props ) {
|
||||
|
||||
$rtl = is_rtl() ? '.rtl' : '';
|
||||
wp_enqueue_style( 'jetpack-components', plugins_url( "_inc/blocks/components{$rtl}.css", JETPACK__PLUGIN_FILE ), array( 'wp-components' ), JETPACK__VERSION );
|
||||
|
||||
ob_start();
|
||||
// `include` fails gracefully and throws a warning, but doesn't halt execution.
|
||||
include JETPACK__PLUGIN_DIR . "_inc/blocks/$name.html";
|
||||
$markup = ob_get_clean();
|
||||
|
||||
foreach ( $props as $key => $value ) {
|
||||
$markup = str_replace(
|
||||
"#$key#",
|
||||
$value,
|
||||
$markup
|
||||
);
|
||||
|
||||
// Workaround, required to replace strings in `sprintf`-expressions.
|
||||
// See extensions/i18n-to-php.js for more information.
|
||||
$markup = str_replace(
|
||||
"%($key)s",
|
||||
$value,
|
||||
$markup
|
||||
);
|
||||
}
|
||||
|
||||
return $markup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the frontend-nudge with the provided props.
|
||||
*
|
||||
* @param array $props Component properties.
|
||||
*
|
||||
* @return string The component markup.
|
||||
*/
|
||||
public static function render_frontend_nudge( $props ) {
|
||||
return self::render_component(
|
||||
'frontend-nudge',
|
||||
$props
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and display a pre-rendered component
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @param array $props Component properties.
|
||||
*
|
||||
* @return string The component markup
|
||||
*/
|
||||
public static function render_upgrade_nudge( $props ) {
|
||||
$plan_slug = $props['plan'];
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/plans.php';
|
||||
$plan = Jetpack_Plans::get_plan( $plan_slug );
|
||||
|
||||
if ( ! $plan ) {
|
||||
return self::render_component(
|
||||
'upgrade-nudge',
|
||||
array(
|
||||
'checkoutUrl' => '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// WP.com plan objects have a dedicated `path_slug` field, Jetpack plan objects don't.
|
||||
$plan_path_slug = wp_startswith( $plan_slug, 'jetpack_' )
|
||||
? $plan_slug
|
||||
: $plan->path_slug;
|
||||
|
||||
$post_id = get_the_ID();
|
||||
|
||||
$site_slug = ( new Status() )->get_site_suffix();
|
||||
|
||||
// Post-checkout: redirect back to the editor.
|
||||
$redirect_to = add_query_arg(
|
||||
array(
|
||||
'plan_upgraded' => 1,
|
||||
),
|
||||
get_edit_post_link( $post_id )
|
||||
);
|
||||
|
||||
$upgrade_url =
|
||||
$plan_path_slug
|
||||
? add_query_arg(
|
||||
'redirect_to',
|
||||
$redirect_to,
|
||||
"https://wordpress.com/checkout/{$site_slug}/{$plan_path_slug}"
|
||||
) : '';
|
||||
|
||||
return self::render_component(
|
||||
'upgrade-nudge',
|
||||
array(
|
||||
'checkoutUrl' => $upgrade_url,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* `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()`
|
||||
*
|
||||
* @todo - nicer API for array values?
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract WPCOM_REST_API_V2_Field_Controller class extended for different fields needed in the Jetpack plugin.
|
||||
*/
|
||||
abstract class WPCOM_REST_API_V2_Field_Controller {
|
||||
/**
|
||||
* The REST Object Type(s) to which the field should be added.
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
protected $object_type;
|
||||
|
||||
/**
|
||||
* The name of the REST API field to add.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $field_name;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( ! $this->object_type ) {
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::$object_type',
|
||||
sprintf(
|
||||
/* translators: %s: object_type */
|
||||
esc_html__( "Property '%s' must be overridden.", 'jetpack' ),
|
||||
'object_type'
|
||||
),
|
||||
'jetpack-6.8'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->field_name ) {
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::$field_name',
|
||||
sprintf(
|
||||
/* translators: %s: field_name */
|
||||
esc_html__( "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 ) {
|
||||
if ( $this->is_registered( $object_type ) ) {
|
||||
continue;
|
||||
}
|
||||
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(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field is already registered for the object_type
|
||||
*
|
||||
* @param string $object_type The name of the object type.
|
||||
* @return boolean Whether the field has been registered for the type.
|
||||
*/
|
||||
public function is_registered( $object_type ) {
|
||||
global $wp_rest_additional_fields;
|
||||
|
||||
return ! empty( $wp_rest_additional_fields[ $object_type ][ $this->field_name ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the response matches the schema and request context.
|
||||
*
|
||||
* @param mixed $value Value passed in request.
|
||||
* @param WP_REST_Request $request WP API 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 Schema to validate against.
|
||||
*
|
||||
* @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 WP API 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$permission_check = $this->get_permission_check( $object_data, $request );
|
||||
|
||||
if ( ! $permission_check ) {
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::get_permission_check',
|
||||
sprintf(
|
||||
/* translators: %s: get_permission_check() */
|
||||
esc_html__( "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 WP API 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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$permission_check = $this->update_permission_check( $value, $object_data, $request );
|
||||
|
||||
if ( ! $permission_check ) {
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::update_permission_check',
|
||||
sprintf(
|
||||
/* translators: %s: update_permission_check() */
|
||||
esc_html__( "Method '%s' must return either true or WP_Error.", 'jetpack' ),
|
||||
'update_permission_check'
|
||||
),
|
||||
'jetpack-6.8'
|
||||
);
|
||||
|
||||
return new WP_Error(
|
||||
'invalid_user_permission',
|
||||
sprintf(
|
||||
/* translators: %s: the name of an API response field */
|
||||
__( "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 WP API request.
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function get_permission_check( $object_data, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::get_permission_check',
|
||||
sprintf(
|
||||
/* translators: %s: method name. */
|
||||
esc_html__( "Method '%s' must be overridden.", 'jetpack' ),
|
||||
__METHOD__
|
||||
),
|
||||
'jetpack-6.8'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 WP API request.
|
||||
* @return mixed
|
||||
*/
|
||||
public function get( $object_data, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::get',
|
||||
sprintf(
|
||||
/* translators: %s: method name. */
|
||||
esc_html__( "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 WP API request.
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function update_permission_check( $value, $object_data, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::update_permission_check',
|
||||
sprintf(
|
||||
/* translators: %s: method name. */
|
||||
esc_html__( "Method '%s' must be overridden.", 'jetpack' ),
|
||||
__METHOD__
|
||||
),
|
||||
'jetpack-6.8'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 WP API request.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function update( $value, $object_data, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::update',
|
||||
sprintf(
|
||||
/* translators: %s: method name. */
|
||||
esc_html__( "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() {
|
||||
_doing_it_wrong(
|
||||
'WPCOM_REST_API_V2_Field_Controller::get_schema',
|
||||
sprintf(
|
||||
/* translators: %s: method name. */
|
||||
esc_html__( "Method '%s' must be overridden.", 'jetpack' ),
|
||||
__METHOD__
|
||||
),
|
||||
'jetpack-6.8'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that our request matches its expected context.
|
||||
*
|
||||
* @param array $schema Schema to validate against.
|
||||
* @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 Value passed to API request.
|
||||
* @param array $schema Schema to validate against.
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
+1727
File diff suppressed because it is too large
Load Diff
+303
@@ -0,0 +1,303 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* List of /site core REST API endpoints used in Jetpack's dashboard.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Publicize\Publicize;
|
||||
use Automattic\Jetpack\Stats\WPCOM_Stats;
|
||||
|
||||
/**
|
||||
* This is the endpoint class for `/site` endpoints.
|
||||
*/
|
||||
class Jetpack_Core_API_Site_Endpoint {
|
||||
/**
|
||||
* Returns commonly used WP_Error indicating failure to fetch data
|
||||
*
|
||||
* @return WP_Error that denotes our inability to fetch the requested data
|
||||
*/
|
||||
private static function get_failed_fetch_error() {
|
||||
return new WP_Error(
|
||||
'failed_to_fetch_data',
|
||||
esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 self::get_failed_fetch_error();
|
||||
}
|
||||
|
||||
// Decode the results.
|
||||
$results = json_decode( $response['body'], true );
|
||||
|
||||
// Bail if there were no results or plan details returned.
|
||||
if ( ! is_array( $results ) ) {
|
||||
return self::get_failed_fetch_error();
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
'message' => esc_html__( 'Site features correctly received.', 'jetpack' ),
|
||||
'data' => wp_remote_retrieve_body( $response ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of `/sites/%s/purchases` endpoint call.
|
||||
*
|
||||
* @return array of site purchases.
|
||||
*/
|
||||
public static function get_purchases() {
|
||||
// Make the API request.
|
||||
$request = sprintf( '/sites/%d/purchases', 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 self::get_failed_fetch_error();
|
||||
}
|
||||
|
||||
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
||||
return self::get_failed_fetch_error();
|
||||
}
|
||||
|
||||
// Decode the results.
|
||||
$results = json_decode( $response['body'], true );
|
||||
|
||||
// Bail if there were no results or purchase details returned.
|
||||
if ( ! is_array( $results ) ) {
|
||||
return self::get_failed_fetch_error();
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
'message' => esc_html__( 'Site purchases correctly received.', 'jetpack' ),
|
||||
'data' => wp_remote_retrieve_body( $response ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of `/sites/%d/products` endpoint call.
|
||||
*
|
||||
* @return array of site products.
|
||||
*/
|
||||
public static function get_products() {
|
||||
$url = sprintf( '/sites/%d/products?locale=%s&type=jetpack', Jetpack_Options::get_option( 'id' ), get_user_locale() );
|
||||
$response = Client::wpcom_json_api_request_as_blog( $url, '1.1' );
|
||||
|
||||
if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
|
||||
return self::get_failed_fetch_error();
|
||||
}
|
||||
|
||||
$results = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
if ( ! is_array( $results ) ) {
|
||||
return self::get_failed_fetch_error();
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
'message' => esc_html__( 'Site products correctly received.', 'jetpack' ),
|
||||
'data' => $results,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of data that show how Jetpack is currently being used to benefit the site.
|
||||
*
|
||||
* @since 7.7
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function get_benefits() {
|
||||
$benefits = array();
|
||||
|
||||
/*
|
||||
* We get different benefits from Stats:
|
||||
* - this year's visitors
|
||||
* - Followers (only if subs module is active)
|
||||
* - Sharing counts (not currently supported in Jetpack -- https://github.com/Automattic/jetpack/issues/844 )
|
||||
*/
|
||||
$wpcom_stats = new WPCOM_Stats();
|
||||
$stats = $wpcom_stats->convert_stats_array_to_object(
|
||||
$wpcom_stats->get_stats( array( 'fields' => 'stats' ) )
|
||||
);
|
||||
$has_stats = null !== $stats && ! is_wp_error( $stats );
|
||||
|
||||
// Yearly visitors.
|
||||
if ( $has_stats && $stats->stats->visitors > 0 ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'jetpack-stats',
|
||||
'title' => esc_html__( 'Jetpack Stats', 'jetpack' ),
|
||||
'description' => esc_html__( 'Visitors tracked by Jetpack', 'jetpack' ),
|
||||
'value' => absint( $stats->stats->visitors ),
|
||||
);
|
||||
}
|
||||
|
||||
// Protect blocked logins.
|
||||
if ( Jetpack::is_module_active( 'protect' ) ) {
|
||||
$protect = get_site_option( 'jetpack_protect_blocked_attempts' );
|
||||
if ( $protect > 0 ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'protect',
|
||||
'title' => esc_html__( 'Brute force protection', 'jetpack' ),
|
||||
'description' => esc_html__( 'The number of malicious login attempts blocked by Jetpack', 'jetpack' ),
|
||||
'value' => absint( $protect ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Number of followers.
|
||||
if ( $has_stats && $stats->stats->followers_blog > 0 && Jetpack::is_module_active( 'subscriptions' ) ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'subscribers',
|
||||
'title' => esc_html__( 'Subscribers', 'jetpack' ),
|
||||
'description' => esc_html__( 'People subscribed to your updates through Jetpack', 'jetpack' ),
|
||||
'value' => absint( $stats->stats->followers_blog ),
|
||||
);
|
||||
}
|
||||
|
||||
// VaultPress backups.
|
||||
if ( Jetpack::is_plugin_active( 'vaultpress/vaultpress.php' ) && class_exists( 'VaultPress' ) ) {
|
||||
$vaultpress = new VaultPress();
|
||||
if ( $vaultpress->is_registered() ) {
|
||||
$data = json_decode( base64_decode( $vaultpress->contact_service( 'plugin_data' ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
if ( $data && $data->features->backups && ! empty( $data->backups->stats ) && $data->backups->stats->revisions > 0 ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'jetpack-backup',
|
||||
'title' => esc_html__( 'Jetpack Backup', 'jetpack' ),
|
||||
'description' => esc_html__( 'The number of times Jetpack has backed up your site and kept it safe', 'jetpack' ),
|
||||
'value' => absint( $data->backups->stats->revisions ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Number of forms sent via a Jetpack contact form.
|
||||
if ( Jetpack::is_module_active( 'contact-form' ) ) {
|
||||
$contact_form_count = array_sum( get_object_vars( wp_count_posts( 'feedback' ) ) );
|
||||
if ( $contact_form_count > 0 ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'contact-form-feedback',
|
||||
'title' => esc_html__( 'Contact Form Feedback', 'jetpack' ),
|
||||
'description' => esc_html__( 'Form submissions stored by Jetpack', 'jetpack' ),
|
||||
'value' => absint( $contact_form_count ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Number of images in the library if Photon is active.
|
||||
if ( Jetpack::is_module_active( 'photon' ) ) {
|
||||
$photon_count = array_reduce(
|
||||
get_object_vars( wp_count_attachments( array( 'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp' ) ) ),
|
||||
function ( $i, $j ) {
|
||||
return $i + $j;
|
||||
}
|
||||
);
|
||||
if ( $photon_count > 0 ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'image-hosting',
|
||||
'title' => esc_html__( 'Image Hosting', 'jetpack' ),
|
||||
'description' => esc_html__( 'Super-fast, mobile-ready images served by Jetpack', 'jetpack' ),
|
||||
'value' => absint( $photon_count ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Number of VideoPress videos on the site.
|
||||
if ( Jetpack::is_module_active( 'videopress' ) ) {
|
||||
$videopress_attachments = wp_count_attachments( 'video/videopress' );
|
||||
if (
|
||||
isset( $videopress_attachments->{'video/videopress'} )
|
||||
&& $videopress_attachments->{'video/videopress'} > 0
|
||||
) {
|
||||
$benefits[] = array(
|
||||
'name' => 'video-hosting',
|
||||
'title' => esc_html__( 'Video Hosting', 'jetpack' ),
|
||||
'description' => esc_html__( 'Ad-free, lightning-fast videos delivered by Jetpack', 'jetpack' ),
|
||||
'value' => absint( $videopress_attachments->{'video/videopress'} ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Number of active Publicize connections.
|
||||
if ( Jetpack::is_module_active( 'publicize' ) && class_exists( Publicize::class ) ) {
|
||||
$publicize = new Publicize();
|
||||
$connections = $publicize->get_all_connections();
|
||||
|
||||
$number_of_connections = 0;
|
||||
if ( is_array( $connections ) && ! empty( $connections ) ) {
|
||||
$number_of_connections = count( $connections );
|
||||
}
|
||||
|
||||
if ( $number_of_connections > 0 ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'publicize',
|
||||
'title' => esc_html__( 'Jetpack Social', 'jetpack' ),
|
||||
'description' => esc_html__( 'Live social media site connections, powered by Jetpack', 'jetpack' ),
|
||||
'value' => absint( $number_of_connections ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Total number of shares.
|
||||
if ( $has_stats && $stats->stats->shares > 0 ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'sharing',
|
||||
'title' => esc_html__( 'Sharing', 'jetpack' ),
|
||||
'description' => esc_html__( 'The number of times visitors have shared your posts with the world using Jetpack', 'jetpack' ),
|
||||
'value' => absint( $stats->stats->shares ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( Jetpack::is_module_active( 'search' ) && ! class_exists( 'Automattic\\Jetpack\\Search_Plugin\\Jetpack_Search_Plugin' ) ) {
|
||||
$benefits[] = array(
|
||||
'name' => 'search',
|
||||
'title' => esc_html__( 'Search', 'jetpack' ),
|
||||
'description' => esc_html__( 'Help your visitors find exactly what they are looking for, fast', 'jetpack' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Finally, return the whole list of benefits.
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'code' => 'success',
|
||||
'message' => esc_html__( 'Site benefits correctly received.', 'jetpack' ),
|
||||
'data' => wp_json_encode( $benefits ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Interact with a specific widget via the REST API.
|
||||
* Currently only supports the Milestone widget.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Widget information getter endpoint.
|
||||
*/
|
||||
class Jetpack_Core_API_Widget_Endpoint {
|
||||
|
||||
/**
|
||||
* Get information about a widget that is supported by this 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;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* This is the base class for every Core API endpoint that needs an XMLRPC client.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 4.3.0
|
||||
*
|
||||
* @param Jetpack_IXR_Client $xmlrpc Jetpack_IXR_Client instance.
|
||||
*/
|
||||
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,62 @@
|
||||
<?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/
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Disable direct access.
|
||||
*/
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop through endpoint files and load them.
|
||||
*
|
||||
* @param string $file_pattern Path pattern to the endpoints (pattern must be supported by glob()).
|
||||
*/
|
||||
function wpcom_rest_api_v2_load_plugin_files( $file_pattern ) {
|
||||
$plugins = glob( __DIR__ . '/' . $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.
|
||||
*
|
||||
* @param string $class_name The name of the class to load.
|
||||
*/
|
||||
function wpcom_rest_api_v2_load_plugin( $class_name ) {
|
||||
global $wpcom_rest_api_v2_plugins;
|
||||
|
||||
if ( ! isset( $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 __DIR__ . '/class-wpcom-rest-field-controller.php';
|
||||
|
||||
/**
|
||||
* Load the REST API v2 plugin files during the plugins_loaded action.
|
||||
*/
|
||||
function load_wpcom_rest_api_v2_plugin_files() {
|
||||
wpcom_rest_api_v2_load_plugin_files( 'wpcom-endpoints/*.php' );
|
||||
wpcom_rest_api_v2_load_plugin_files( 'wpcom-fields/*.php' );
|
||||
}
|
||||
add_action( 'plugins_loaded', 'load_wpcom_rest_api_v2_plugin_files' );
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Build localized strings for use with the Business Hours Block.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Business Hours: Localized week
|
||||
*
|
||||
* @since 7.1
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Business_Hours extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public 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' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint route.
|
||||
*/
|
||||
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' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' );
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for admin color.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Admin_Color
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Admin_Color extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Namespace prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $namespace = 'wpcom/v2';
|
||||
|
||||
/**
|
||||
* Endpoint base route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $rest_base = 'admin-color';
|
||||
|
||||
/**
|
||||
* WPCOM_REST_API_V2_Endpoint_Admin_Color constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_item' ),
|
||||
'permission_callback' => array( $this, 'get_item_permissions_check' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to admin color.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
|
||||
*/
|
||||
public function get_item_permissions_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( ! current_user_can( 'read' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__( 'Sorry, you are not allowed to view admin color on this site.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the admin color.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$admin_color = get_user_option( 'admin_color' );
|
||||
return rest_ensure_response( array( 'admin_color' => $admin_color ) );
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Admin_Color' );
|
||||
+520
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for admin menus.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 9.1.0
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Admin_Menu
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Admin_Menu extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Namespace prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $namespace = 'wpcom/v2';
|
||||
|
||||
/**
|
||||
* Endpoint base route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $rest_base = 'admin-menu';
|
||||
|
||||
/**
|
||||
*
|
||||
* Set of core dashicons.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $dashicon_list;
|
||||
|
||||
/**
|
||||
* WPCOM_REST_API_V2_Endpoint_Admin_Menu constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_item' ),
|
||||
'permission_callback' => array( $this, 'get_item_permissions_check' ),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to admin menus.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
|
||||
*/
|
||||
public function get_item_permissions_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( ! current_user_can( 'read' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__( 'Sorry, you are not allowed to view menus on this site.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the admin menu.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( ! ( new Host() )->is_wpcom_platform() ) {
|
||||
require_once JETPACK__PLUGIN_DIR . 'jetpack_vendor/automattic/jetpack-masterbar/src/admin-menu/load.php';
|
||||
}
|
||||
|
||||
// All globals need to be declared for menu items to properly register.
|
||||
global $admin_page_hooks, $menu, $menu_order, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
$this->hide_customizer_menu_on_block_theme();
|
||||
require_once ABSPATH . 'wp-admin/includes/admin.php';
|
||||
require_once ABSPATH . 'wp-admin/menu.php';
|
||||
|
||||
return rest_ensure_response( $this->prepare_menu_for_response( $menu ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the Customizer menu items when the block theme is active by removing the dotcom-specific actions.
|
||||
* They are not needed for block themes.
|
||||
*
|
||||
* @see https://github.com/Automattic/jetpack/pull/36017
|
||||
*/
|
||||
private function hide_customizer_menu_on_block_theme() {
|
||||
if ( wp_is_block_theme() && ! is_customize_preview() ) {
|
||||
remove_action( 'customize_register', 'add_logotool_button', 20 );
|
||||
remove_action( 'customize_register', 'footercredits_register', 99 );
|
||||
remove_action( 'customize_register', 'wpcom_disable_customizer_site_icon', 20 );
|
||||
|
||||
if ( class_exists( '\Jetpack_Fonts' ) ) {
|
||||
$jetpack_fonts_instance = \Jetpack_Fonts::get_instance();
|
||||
remove_action( 'customize_register', array( $jetpack_fonts_instance, 'register_controls' ) );
|
||||
remove_action( 'customize_register', array( $jetpack_fonts_instance, 'maybe_prepopulate_option' ), 0 );
|
||||
}
|
||||
|
||||
remove_action( 'customize_register', array( 'Jetpack_Fonts_Typekit', 'maybe_override_for_advanced_mode' ), 20 );
|
||||
|
||||
if ( class_exists( 'Automattic\Jetpack\Masterbar' ) ) {
|
||||
remove_action( 'customize_register', 'Automattic\Jetpack\Masterbar\register_css_nudge_control' );
|
||||
}
|
||||
|
||||
// @phan-suppress-next-line PhanUndeclaredClassInCallable
|
||||
remove_action( 'customize_register', array( 'Jetpack_Custom_CSS_Enhancements', 'customize_register' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the admin menu for the REST response.
|
||||
*
|
||||
* @param array $menu Admin menu.
|
||||
* @return array Admin menu
|
||||
*/
|
||||
public function prepare_menu_for_response( array $menu ) {
|
||||
global $submenu;
|
||||
|
||||
$data = array();
|
||||
|
||||
/**
|
||||
* Note: if the shape of the API endpoint data changes it is important to also update
|
||||
* the corresponding schema.js file.
|
||||
* See: https://github.com/Automattic/wp-calypso/blob/ebde236ec9b21ea9621c0b0523bd5ea185523731/client/state/admin-menu/schema.js
|
||||
*/
|
||||
foreach ( $menu as $menu_item ) {
|
||||
$item = $this->prepare_menu_item( $menu_item );
|
||||
|
||||
// Are there submenu items to process?
|
||||
if ( ! empty( $submenu[ $menu_item[2] ] ) ) {
|
||||
$submenu_items = array_values( $submenu[ $menu_item[2] ] );
|
||||
|
||||
// Add submenu items.
|
||||
foreach ( $submenu_items as $submenu_item ) {
|
||||
$submenu_item = $this->prepare_submenu_item( $submenu_item, $menu_item );
|
||||
if ( ! empty( $submenu_item ) ) {
|
||||
$item['children'][] = $submenu_item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $item ) ) {
|
||||
$data[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return array_filter( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the admin menu's schema, conforming to JSON Schema.
|
||||
*
|
||||
* Note: if the shape of the API endpoint data changes it is important to also update
|
||||
* the corresponding schema.js file.
|
||||
*
|
||||
* @see https://github.com/Automattic/wp-calypso/blob/ebde236ec9b21ea9621c0b0523bd5ea185523731/client/state/admin-menu/schema.js
|
||||
*
|
||||
* @return array Item schema data.
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
return array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'Admin Menu',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'count' => array(
|
||||
'description' => 'Core/Plugin/Theme update count or unread comments count.',
|
||||
'type' => 'integer',
|
||||
),
|
||||
'icon' => array(
|
||||
'description' => 'Menu item icon. Dashicon slug or base64-encoded SVG.',
|
||||
'type' => 'string',
|
||||
),
|
||||
'inlineText' => array(
|
||||
'description' => 'Additional text to be added inline with the menu title.',
|
||||
'type' => 'string',
|
||||
),
|
||||
'badge' => array(
|
||||
'description' => 'Badge to be added inline with the menu title.',
|
||||
'type' => 'string',
|
||||
),
|
||||
'slug' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'children' => array(
|
||||
'items' => array(
|
||||
'count' => array(
|
||||
'description' => 'Core/Plugin/Theme update count or unread comments count.',
|
||||
'type' => 'integer',
|
||||
),
|
||||
'parent' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'slug' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'type' => array(
|
||||
'enum' => array( 'submenu-item' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'url' => array(
|
||||
'format' => 'uri',
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
'type' => 'array',
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'type' => array(
|
||||
'enum' => array( 'separator', 'menu-item' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'url' => array(
|
||||
'format' => 'uri',
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a menu item for consumption by Calypso.
|
||||
*
|
||||
* @param array $menu_item Menu item.
|
||||
* @return array Prepared menu item.
|
||||
*/
|
||||
private function prepare_menu_item( array $menu_item ) {
|
||||
global $submenu;
|
||||
|
||||
$current_user_can_access_menu = current_user_can( $menu_item[1] );
|
||||
$submenu_items = isset( $submenu[ $menu_item[2] ] ) ? array_values( $submenu[ $menu_item[2] ] ) : array();
|
||||
$has_first_menu_item = isset( $submenu_items[0] );
|
||||
|
||||
// Exclude unauthorized menu items when the user does not have access to the menu and the first submenu item.
|
||||
if ( ! $current_user_can_access_menu && $has_first_menu_item && ! current_user_can( $submenu_items[0][1] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Exclude unauthorized menu items that don't have submenus.
|
||||
if ( ! $current_user_can_access_menu && ! $has_first_menu_item ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Exclude hidden menu items.
|
||||
if ( str_contains( $menu_item[4], 'hide-if-js' ) ) {
|
||||
// Exclude submenu items as well.
|
||||
if ( ! empty( $submenu[ $menu_item[2] ] ) ) {
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$submenu[ $menu_item[2] ] = array();
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
// Handle menu separators.
|
||||
if ( str_contains( $menu_item[4], 'wp-menu-separator' ) ) {
|
||||
return array(
|
||||
'type' => 'separator',
|
||||
);
|
||||
}
|
||||
|
||||
$url = $menu_item[2];
|
||||
$parent_slug = '';
|
||||
|
||||
// If there are submenus, the parent menu should always link to the first submenu.
|
||||
// @see https://core.trac.wordpress.org/browser/trunk/src/wp-admin/menu-header.php?rev=49193#L152.
|
||||
if ( ! empty( $submenu[ $menu_item[2] ] ) ) {
|
||||
$parent_slug = $url;
|
||||
$first_submenu_item = reset( $submenu[ $menu_item[2] ] );
|
||||
$url = $first_submenu_item[2];
|
||||
}
|
||||
|
||||
$item = array(
|
||||
'icon' => $this->prepare_menu_item_icon( $menu_item[6] ),
|
||||
'slug' => sanitize_title_with_dashes( $menu_item[2] ),
|
||||
'title' => $menu_item[0],
|
||||
'type' => 'menu-item',
|
||||
'url' => $this->prepare_menu_item_url( $url, $parent_slug ),
|
||||
);
|
||||
|
||||
$parsed_item = $this->parse_menu_item( $item['title'] );
|
||||
if ( ! empty( $parsed_item ) ) {
|
||||
$item = array_merge( $item, $parsed_item );
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a submenu item for consumption by Calypso.
|
||||
*
|
||||
* @param array $submenu_item Submenu item.
|
||||
* @param array $menu_item Menu item.
|
||||
* @return array Prepared submenu item.
|
||||
*/
|
||||
private function prepare_submenu_item( array $submenu_item, array $menu_item ) {
|
||||
// Exclude unauthorized submenu items.
|
||||
if ( ! current_user_can( $submenu_item[1] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Exclude hidden submenu items.
|
||||
if ( isset( $submenu_item[4] ) && str_contains( $submenu_item[4], 'hide-if-js' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$item = array(
|
||||
'parent' => sanitize_title_with_dashes( $menu_item[2] ),
|
||||
'slug' => sanitize_title_with_dashes( $submenu_item[2] ),
|
||||
'title' => $submenu_item[0],
|
||||
'type' => 'submenu-item',
|
||||
'url' => $this->prepare_menu_item_url( $submenu_item[2], $menu_item[2] ),
|
||||
);
|
||||
|
||||
$parsed_item = $this->parse_menu_item( $item['title'] );
|
||||
if ( ! empty( $parsed_item ) ) {
|
||||
$item = array_merge( $item, $parsed_item );
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a menu icon for consumption by Calypso.
|
||||
*
|
||||
* @param string $icon Menu icon.
|
||||
* @return string
|
||||
*/
|
||||
private function prepare_menu_item_icon( $icon ) {
|
||||
$img = 'dashicons-admin-generic';
|
||||
|
||||
if ( ! empty( $icon ) && 'none' !== $icon && 'div' !== $icon ) {
|
||||
$img = esc_url( $icon );
|
||||
|
||||
if ( str_starts_with( $icon, 'data:image/svg+xml' ) ) {
|
||||
$img = $icon;
|
||||
} elseif ( str_starts_with( $icon, 'dashicons-' ) ) {
|
||||
$img = $this->prepare_dashicon( $icon );
|
||||
}
|
||||
}
|
||||
|
||||
return $img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the dashicon for consumption by Calypso. If the dashicon isn't found in a list of known icons
|
||||
* we will return the default dashicon.
|
||||
*
|
||||
* @param string $icon The dashicon string to check.
|
||||
*
|
||||
* @return string If the dashicon exists in core we return the dashicon, otherwise we return the default dashicon.
|
||||
*/
|
||||
private function prepare_dashicon( $icon ) {
|
||||
if ( empty( $this->dashicon_set ) ) {
|
||||
$this->dashicon_list = include JETPACK__PLUGIN_DIR . 'jetpack_vendor/automattic/jetpack-masterbar/src/admin-menu/dashicon-set.php';
|
||||
}
|
||||
|
||||
if ( isset( $this->dashicon_list[ $icon ] ) && $this->dashicon_list[ $icon ] ) {
|
||||
return $icon;
|
||||
}
|
||||
|
||||
return 'dashicons-admin-generic';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a menu item url for consumption by Calypso.
|
||||
*
|
||||
* @param string $url Menu slug.
|
||||
* @param string $parent_slug Optional. Parent menu item slug. Default empty string.
|
||||
* @return string
|
||||
*/
|
||||
private function prepare_menu_item_url( $url, $parent_slug = '' ) {
|
||||
// External URLS.
|
||||
if ( preg_match( '/^https?:\/\//', $url ) ) {
|
||||
// Allow URLs pointing to WordPress.com.
|
||||
if ( str_starts_with( $url, 'https://wordpress.com/' ) ) {
|
||||
// Calypso needs the domain removed so they're not interpreted as external links.
|
||||
$url = str_replace( 'https://wordpress.com', '', $url );
|
||||
// Replace special characters with their correct entities e.g. & to &.
|
||||
return wp_specialchars_decode( esc_url_raw( $url ) );
|
||||
}
|
||||
|
||||
// Allow URLs pointing to Jetpack.com.
|
||||
if ( str_starts_with( $url, 'https://jetpack.com/' ) ) {
|
||||
// Replace special characters with their correct entities e.g. & to &.
|
||||
return wp_specialchars_decode( esc_url_raw( $url ) );
|
||||
}
|
||||
|
||||
// Disallow other external URLs.
|
||||
if ( ! str_starts_with( $url, get_site_url() ) ) {
|
||||
return '';
|
||||
}
|
||||
// The URL matches that of the site, treat it as an internal URL.
|
||||
}
|
||||
|
||||
// Internal URLs.
|
||||
$menu_hook = get_plugin_page_hook( $url, $parent_slug );
|
||||
$menu_file = wp_parse_url( $url, PHP_URL_PATH ); // Removes query args to get a file name.
|
||||
$parent_file = wp_parse_url( $parent_slug, PHP_URL_PATH );
|
||||
|
||||
if (
|
||||
! empty( $menu_hook ) ||
|
||||
(
|
||||
'index.php' !== $url &&
|
||||
file_exists( WP_PLUGIN_DIR . "/$menu_file" ) &&
|
||||
! file_exists( ABSPATH . "/wp-admin/$menu_file" )
|
||||
)
|
||||
) {
|
||||
$admin_is_parent = false;
|
||||
if ( ! empty( $parent_slug ) ) {
|
||||
$menu_hook = get_plugin_page_hook( $parent_slug, 'admin.php' );
|
||||
$admin_is_parent = ! empty( $menu_hook ) || ( ( 'index.php' !== $parent_slug ) && file_exists( WP_PLUGIN_DIR . "/$parent_file" ) && ! file_exists( ABSPATH . "/wp-admin/$parent_file" ) );
|
||||
}
|
||||
|
||||
if (
|
||||
( false === $admin_is_parent && file_exists( WP_PLUGIN_DIR . "/$parent_file" ) && ! is_dir( WP_PLUGIN_DIR . "/$parent_file" ) ) ||
|
||||
( file_exists( ABSPATH . "/wp-admin/$parent_file" ) && ! is_dir( ABSPATH . "/wp-admin/$parent_file" ) )
|
||||
) {
|
||||
$url = add_query_arg( array( 'page' => $url ), admin_url( $parent_slug ) );
|
||||
} else {
|
||||
$url = add_query_arg( array( 'page' => $url ), admin_url( 'admin.php' ) );
|
||||
}
|
||||
} elseif ( file_exists( ABSPATH . "/wp-admin/$menu_file" ) ) {
|
||||
$url = admin_url( $url );
|
||||
}
|
||||
|
||||
return wp_specialchars_decode( esc_url_raw( $url ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* "Plugins", "Comments", "Updates" menu items have a count badge when there are updates available.
|
||||
* This method parses that information, removes the associated markup and adds it to the response.
|
||||
*
|
||||
* Also sanitizes the titles from remaining unexpected markup.
|
||||
*
|
||||
* @param string $title Title to parse.
|
||||
* @return array
|
||||
*/
|
||||
private function parse_menu_item( $title ) {
|
||||
$item = array();
|
||||
|
||||
if (
|
||||
str_contains( $title, 'count-' )
|
||||
&& preg_match( '/<span class=".+\s?count-(\d*).+\s?<\/span><\/span>/', $title, $matches )
|
||||
) {
|
||||
|
||||
$count = (int) ( $matches[1] );
|
||||
if ( $count > 0 ) {
|
||||
// Keep the counter in the item array.
|
||||
$item['count'] = $count;
|
||||
}
|
||||
|
||||
// Finally remove the markup.
|
||||
$title = trim( str_replace( $matches[0], '', $title ) );
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains( $title, 'inline-text' )
|
||||
&& preg_match( '/<span class="inline-text".+\s?>(.+)<\/span>/', $title, $matches )
|
||||
) {
|
||||
|
||||
$text = $matches[1];
|
||||
if ( $text ) {
|
||||
// Keep the text in the item array.
|
||||
$item['inlineText'] = $text;
|
||||
}
|
||||
|
||||
// Finally remove the markup.
|
||||
$title = trim( str_replace( $matches[0], '', $title ) );
|
||||
}
|
||||
|
||||
if (
|
||||
str_contains( $title, 'awaiting-mod' )
|
||||
&& preg_match( '/<span class="awaiting-mod">(.+)<\/span>/', $title, $matches )
|
||||
) {
|
||||
|
||||
$text = $matches[1];
|
||||
if ( $text ) {
|
||||
// Keep the text in the item array.
|
||||
$item['badge'] = $text;
|
||||
}
|
||||
|
||||
// Finally remove the markup.
|
||||
$title = trim( str_replace( $matches[0], '', $title ) );
|
||||
}
|
||||
|
||||
// It's important we sanitize the title after parsing data to remove any unexpected markup but keep the content.
|
||||
// We are also capitalizing the first letter in case there was a counter (now parsed) in front of the title.
|
||||
$item['title'] = ucfirst( wp_strip_all_tags( $title ) );
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Admin_Menu' );
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for the Jetpack AI blocks.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 11.8
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_AI
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_AI extends WP_REST_Controller {
|
||||
/**
|
||||
* Namespace prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $namespace = 'wpcom/v2';
|
||||
|
||||
/**
|
||||
* Endpoint base route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $rest_base = 'jetpack-ai';
|
||||
|
||||
/**
|
||||
* WPCOM_REST_API_V2_Endpoint_AI constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->is_wpcom = true;
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
|
||||
if ( ! class_exists( 'Jetpack_AI_Helper' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-ai-helper.php';
|
||||
}
|
||||
|
||||
// Register routes that don't require Jetpack AI to be enabled.
|
||||
add_action( 'rest_api_init', array( $this, 'register_basic_routes' ) );
|
||||
|
||||
if ( Jetpack_AI_Helper::is_ai_chat_enabled() ) {
|
||||
add_action( 'rest_api_init', array( $this, 'register_ai_chat_routes' ) );
|
||||
}
|
||||
|
||||
if ( ! \Jetpack_AI_Helper::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register routes that require Jetpack AI to be enabled.
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/completions',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'request_gpt_completion' ),
|
||||
'permission_callback' => array( 'Jetpack_AI_Helper', 'get_status_permission_check' ),
|
||||
),
|
||||
'args' => array(
|
||||
'content' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_textarea_field',
|
||||
),
|
||||
'post_id' => array(
|
||||
'required' => false,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'skip_cache' => array(
|
||||
'required' => false,
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to skip the cache and make a new request',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/images/generations',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'request_dalle_generation' ),
|
||||
'permission_callback' => array( 'Jetpack_AI_Helper', 'get_status_permission_check' ),
|
||||
),
|
||||
'args' => array(
|
||||
'prompt' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_textarea_field',
|
||||
),
|
||||
'post_id' => array(
|
||||
'required' => false,
|
||||
'type' => 'integer',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes for the AI Chat block.
|
||||
* Relies on a site connection and Jetpack Search.
|
||||
*/
|
||||
public function register_ai_chat_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/jetpack-search/ai/search',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'request_chat_with_site' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
'args' => array(
|
||||
'query' => array(
|
||||
'description' => 'Your question to the site',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'answer_prompt' => array(
|
||||
'description' => 'Answer prompt override',
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/jetpack-search/ai/rank',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'rank_response' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
'args' => array(
|
||||
'cache_key' => array(
|
||||
'description' => 'Cache key of your response',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'comment' => array(
|
||||
'description' => 'Optional feedback',
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'rank' => array(
|
||||
'description' => 'How do you rank this response',
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes that don't require Jetpack AI to be enabled.
|
||||
*/
|
||||
public function register_basic_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/ai-assistant-feature',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'request_get_ai_assistance_feature' ),
|
||||
'permission_callback' => array( 'Jetpack_AI_Helper', 'get_status_permission_check' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a response from chatting with the site.
|
||||
* Uses Jetpack Search.
|
||||
*
|
||||
* @param WP_REST_Request $request The request.
|
||||
* @return mixed
|
||||
*/
|
||||
public function request_chat_with_site( $request ) {
|
||||
$question = $request->get_param( 'query' );
|
||||
$blog_id = \Jetpack_Options::get_option( 'id' );
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/sites/%d/jetpack-search/ai/search', $blog_id ) . '?force=wpcom',
|
||||
2,
|
||||
array(
|
||||
'method' => 'GET',
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
'timeout' => MINUTE_IN_SECONDS,
|
||||
),
|
||||
array(
|
||||
'query' => $question,
|
||||
/**
|
||||
* Filter for an answer prompt override.
|
||||
* Example: "Talk like a cowboy."
|
||||
*
|
||||
* @param string $prompt_override The prompt override string.
|
||||
*
|
||||
* @since 12.6
|
||||
*/
|
||||
'answer_prompt' => apply_filters( 'jetpack_ai_chat_answer_prompt', false ),
|
||||
),
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
if ( empty( $data->cache_key ) ) {
|
||||
return new WP_Error( 'invalid_ask_response', __( 'Invalid response from the server.', 'jetpack' ), 400 );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rank a response from chatting with the site.
|
||||
*
|
||||
* @param WP_REST_Request $request The request.
|
||||
* @return mixed
|
||||
*/
|
||||
public function rank_response( $request ) {
|
||||
$rank = $request->get_param( 'rank' );
|
||||
$comment = $request->get_param( 'comment' );
|
||||
$cache_key = $request->get_param( 'cache_key' );
|
||||
|
||||
if ( strpos( $cache_key, 'jp-search-ai-' ) !== 0 ) {
|
||||
return new WP_Error( 'invalid_cache_key', __( 'Invalid cached context for the answer feedback.', 'jetpack' ), 400 );
|
||||
}
|
||||
|
||||
$blog_id = \Jetpack_Options::get_option( 'id' );
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/sites/%d/jetpack-search/ai/rank', $blog_id ) . '?force=wpcom',
|
||||
2,
|
||||
array(
|
||||
'method' => 'GET',
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
'timeout' => 30,
|
||||
),
|
||||
array(
|
||||
'rank' => $rank,
|
||||
'comment' => $comment,
|
||||
'cache_key' => $cache_key,
|
||||
),
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
if ( 'ok' !== $data ) {
|
||||
return new WP_Error( 'invalid_feedback_response', __( 'Invalid response from the server.', 'jetpack' ), 400 );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completions for a given text.
|
||||
*
|
||||
* @param WP_REST_Request $request The request.
|
||||
*/
|
||||
public function request_gpt_completion( $request ) {
|
||||
return Jetpack_AI_Helper::get_gpt_completion( $request['content'], $request['post_id'], $request['skip_cache'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image generations for a given prompt.
|
||||
*
|
||||
* @param WP_REST_Request $request The request.
|
||||
*/
|
||||
public function request_dalle_generation( $request ) {
|
||||
return Jetpack_AI_Helper::get_dalle_generation( $request['prompt'], $request['post_id'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect and provide relevat data about the AI feature,
|
||||
* such as the number of requests made.
|
||||
*/
|
||||
public function request_get_ai_assistance_feature() {
|
||||
return Jetpack_AI_Helper::get_ai_assistance_feature();
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_AI' );
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for the media uploaded by the Jetpack app.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 13.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Media uploaded by the Jetpack app helper API.
|
||||
*
|
||||
* @since 13.1
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_App_Media extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'app-media';
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the routes for external media.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base,
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_media' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
'args' => array(
|
||||
'number' => array(
|
||||
'description' => __( 'Number of media items in the request', 'jetpack' ),
|
||||
'type' => 'number',
|
||||
'default' => 20,
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'absint',
|
||||
|
||||
),
|
||||
'page_handle' => array(
|
||||
'type' => 'number',
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'absint',
|
||||
|
||||
),
|
||||
'after' => array(
|
||||
'description' => __( 'Timestamp since the media was uploaded', 'jetpack' ),
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to external media libraries.
|
||||
*/
|
||||
public function permission_callback() {
|
||||
return current_user_can( 'upload_files' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitization callback for media parameter.
|
||||
*
|
||||
* @param array $param Media parameter.
|
||||
* @return true|\WP_Error
|
||||
*/
|
||||
public function sanitize_media( $param ) {
|
||||
$param = $this->prepare_media_param( $param );
|
||||
|
||||
return rest_sanitize_value_from_schema( $param, $this->media_schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation callback for media parameter.
|
||||
*
|
||||
* @param array $param Media parameter.
|
||||
* @return true|\WP_Error
|
||||
*/
|
||||
public function validate_media( $param ) {
|
||||
$param = $this->prepare_media_param( $param );
|
||||
return rest_validate_value_from_schema( $param, $this->media_schema, 'media' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes guid json and sets parameter defaults.
|
||||
*
|
||||
* @param array $param Media parameter.
|
||||
* @return array
|
||||
*/
|
||||
private function prepare_media_param( $param ) {
|
||||
foreach ( $param as $key => $item ) {
|
||||
if ( ! empty( $item['guid'] ) ) {
|
||||
$param[ $key ]['guid'] = json_decode( $item['guid'], true );
|
||||
}
|
||||
|
||||
if ( empty( $param[ $key ]['caption'] ) ) {
|
||||
$param[ $key ]['caption'] = '';
|
||||
}
|
||||
if ( empty( $param[ $key ]['title'] ) ) {
|
||||
$param[ $key ]['title'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves media items from external libraries.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array|\WP_Error|mixed
|
||||
*/
|
||||
public function get_media( \WP_REST_Request $request ) {
|
||||
$params = $request->get_params();
|
||||
$number = $params['number'];
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'attachment',
|
||||
'post_status' => 'inherit',
|
||||
'number' => $number,
|
||||
'date_query' => array(
|
||||
'after' => gmdate( DATE_RSS, intval( $params['after'] ) ),
|
||||
),
|
||||
'paged' => $params['page_handle'],
|
||||
'author' => get_current_user_id(),
|
||||
'orderby' => 'date',
|
||||
);
|
||||
$media_query = new WP_Query( $query_args );
|
||||
$response = $this->format_response( $media_query );
|
||||
|
||||
wp_reset_postdata();
|
||||
return $response;
|
||||
}
|
||||
/**
|
||||
* Formats api the response.
|
||||
*
|
||||
* @param \WP_Query $media_query Media query.
|
||||
*/
|
||||
private function format_response( $media_query ) {
|
||||
$response = array();
|
||||
$response['media'] = array();
|
||||
while ( $media_query->have_posts() ) {
|
||||
$media_query->the_post();
|
||||
// only include images.
|
||||
if ( wp_attachment_is_image( $media_query->post->ID ) ) {
|
||||
$response['media'][] = $this->format_item( $media_query->post );
|
||||
}
|
||||
}
|
||||
$response['found'] = $media_query->found_posts;
|
||||
$response['meta'] = array( 'next_page' => $media_query->paged + 1 );
|
||||
return $response;
|
||||
}
|
||||
/**
|
||||
* Formats a single item.
|
||||
*
|
||||
* @param \WP_Post $item Media item.
|
||||
*/
|
||||
private function format_item( $item ) {
|
||||
return array(
|
||||
'ID' => $item->ID,
|
||||
'url' => wp_get_attachment_image_url( $item->ID, 'full', true ),
|
||||
'date' => get_date_from_gmt( $item->post_date_gmt ),
|
||||
'name' => get_the_title( $item ),
|
||||
'file' => basename( wp_get_attachment_url( $item->ID ) ),
|
||||
'title' => get_the_title( $item ),
|
||||
'guid' => get_the_guid( $item ),
|
||||
'type' => get_post_mime_type( $item ),
|
||||
'caption' => '',
|
||||
'thumbnails' => array(
|
||||
'thumbnail' => wp_get_attachment_image_url( $item->ID, 'thumbnail' ),
|
||||
'large' => wp_get_attachment_image_url( $item->ID, 'large' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_App_Media' );
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* Get blog stats.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Stats\WPCOM_Stats;
|
||||
|
||||
/**
|
||||
* Blog Stats block endpoint.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Blog_Stats extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
'wpcom/v2',
|
||||
'/blog-stats',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_blog_stats' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
'args' => array(
|
||||
'post_id' => array(
|
||||
'description' => __( 'Post ID to obtain stats for.', 'jetpack' ),
|
||||
'type' => array( 'string', 'integer' ),
|
||||
'required' => false,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_numeric( $param );
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the blog stats.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return array Blog stats.
|
||||
*/
|
||||
public function get_blog_stats( $request ) {
|
||||
$wpcom_stats = new WPCOM_Stats();
|
||||
$post_id = $request->get_param( 'post_id' );
|
||||
$post_data = $wpcom_stats->convert_stats_array_to_object(
|
||||
$wpcom_stats->get_post_views( $post_id, array( 'fields' => 'views' ) )
|
||||
);
|
||||
$blog_data = $wpcom_stats->convert_stats_array_to_object(
|
||||
$wpcom_stats->get_stats( array( 'fields' => 'stats' ) )
|
||||
);
|
||||
|
||||
if ( ! isset( $blog_data->stats->views ) || ! isset( $blog_data->stats->visitors ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $post_data->views ) ) {
|
||||
$post_data->views = 0;
|
||||
}
|
||||
|
||||
return array(
|
||||
'post-views' => $post_data->views,
|
||||
'blog-visitors' => $blog_data->stats->visitors,
|
||||
'blog-views' => $blog_data->stats->views,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Blog_Stats' );
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* Email Preview endpoint for the WordPress.com REST API.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
|
||||
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Email_Preview
|
||||
*
|
||||
* Returns an email preview given a post id.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Email_Preview extends WP_REST_Controller {
|
||||
|
||||
use WPCOM_REST_API_Proxy_Request;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->base_api_path = 'wpcom';
|
||||
$this->version = 'v2';
|
||||
$this->namespace = $this->base_api_path . '/' . $this->version;
|
||||
$this->rest_base = '/email-preview';
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
$this->wpcom_is_site_specific_endpoint = true;
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the routes for email preview.
|
||||
*
|
||||
* @see register_rest_route()
|
||||
*/
|
||||
public function register_routes() {
|
||||
$options = array(
|
||||
'show_in_index' => true,
|
||||
'methods' => 'GET',
|
||||
// if this is not a wpcom site, we need to proxy the request to wpcom
|
||||
'callback' => ( ( new Host() )->is_wpcom_simple() ) ? array(
|
||||
$this,
|
||||
'email_preview',
|
||||
) : array( $this, 'proxy_request_to_wpcom_as_user' ),
|
||||
'permission_callback' => array( $this, 'permissions_check' ),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'Unique identifier for the post.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'access' => array(
|
||||
'description' => __( 'Access level.', 'jetpack' ),
|
||||
'enum' => array( Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY, Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS, Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS ),
|
||||
'default' => Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return in_array(
|
||||
$param,
|
||||
array(
|
||||
Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY,
|
||||
Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS,
|
||||
Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS,
|
||||
),
|
||||
true
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base,
|
||||
$options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is connected and has access to edit the post
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has edit access, WP_Error object otherwise.
|
||||
*/
|
||||
public function permissions_check( $request ) {
|
||||
if ( ! ( new Host() )->is_wpcom_simple() ) {
|
||||
if ( ! ( new Manager() )->is_user_connected() ) {
|
||||
return new WP_Error(
|
||||
'rest_cannot_send_email_preview',
|
||||
__( 'Please connect your user account to WordPress.com', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$post = get_post( $request->get_param( 'post_id' ) );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new \WP_Error(
|
||||
'post_not_found',
|
||||
__( 'Post not found.', 'jetpack' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $post->ID ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden_context',
|
||||
__( 'Please connect your user account to WordPress.com', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an email preview of a post.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function email_preview( $request ) {
|
||||
$post_id = $request['post_id'];
|
||||
$access = $request['access'];
|
||||
$post = get_post( $post_id );
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
/**
|
||||
* Filters the generated email preview HTML.
|
||||
*
|
||||
* @since 13.8
|
||||
*
|
||||
* @param string $html The generated HTML for the email preview.
|
||||
* @param WP_Post $post The post object.
|
||||
* @param string $access The access level.
|
||||
*/
|
||||
'html' => apply_filters( 'jetpack_generate_email_preview_html', '', $post, $access ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Email_Preview' );
|
||||
+848
@@ -0,0 +1,848 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for the External Media.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 8.7.0
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
|
||||
/**
|
||||
* External Media helper API.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_External_Media extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Media argument schema for /copy endpoint.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $media_schema = array(
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'required' => true,
|
||||
'properties' => array(
|
||||
'caption' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'guid' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'caption' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'url' => array(
|
||||
'format' => 'uri',
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'meta' => array(
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => array(
|
||||
'vertical_id' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'text-field',
|
||||
),
|
||||
'pexels_object' => array(
|
||||
'type' => 'object',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Service regex.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $services_regex = '(?P<service>google_photos|openverse|pexels)';
|
||||
|
||||
/**
|
||||
* Temporary filename.
|
||||
*
|
||||
* Needed to cope with Google's very long file names.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $tmp_name;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'external-media';
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the routes for external media.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/list/' . self::$services_regex,
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_external_media' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
'args' => array(
|
||||
'search' => array(
|
||||
'description' => __( 'Media collection search term.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'number' => array(
|
||||
'description' => __( 'Number of media items in the request', 'jetpack' ),
|
||||
'type' => 'number',
|
||||
'default' => 20,
|
||||
),
|
||||
'path' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'page_handle' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'session_id' => array(
|
||||
'description' => __( 'Session id of a service, currently only Google Photos Picker', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/copy/' . self::$services_regex,
|
||||
array(
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'copy_external_media' ),
|
||||
'permission_callback' => array( $this, 'create_item_permissions_check' ),
|
||||
'args' => array(
|
||||
'media' => array(
|
||||
'description' => __( 'Media data to copy.', 'jetpack' ),
|
||||
'items' => $this->media_schema,
|
||||
'required' => true,
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => array( $this, 'sanitize_media' ),
|
||||
'validate_callback' => array( $this, 'validate_media' ),
|
||||
),
|
||||
'post_id' => array(
|
||||
'description' => __( 'The post ID to attach the upload to.', 'jetpack' ),
|
||||
'type' => 'number',
|
||||
'minimum' => 0,
|
||||
),
|
||||
'should_proxy' => array(
|
||||
'description' => __( 'Whether to proxy the media request.', 'jetpack' ),
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/connection/(?P<service>google_photos)',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_connection_details' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/connection/(?P<service>google_photos)',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::DELETABLE,
|
||||
'callback' => array( $this, 'delete_connection' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/connection/(?P<service>google_photos)/picker_status',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_picker_status' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Add new session route, currently for Google Photos Picker only
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/session/(?P<service>google_photos)',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_session' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Get new session route, currently for Google Photos Picker only
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/session/(?P<service>google_photos)/(?P<session_id>.*)',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_session' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Delete session route, currently for Google Photos Picker only
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/session/(?P<service>google_photos)/(?P<session_id>.*)',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::DELETABLE,
|
||||
'callback' => array( $this, 'delete_session' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Add new proxy route for media files
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/proxy/(?P<service>google_photos)',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'proxy_media_request' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
'args' => array(
|
||||
'url' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to external media libraries.
|
||||
*/
|
||||
public function permission_callback() {
|
||||
return current_user_can( 'upload_files' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to create an attachment.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
|
||||
*/
|
||||
public function create_item_permissions_check( $request ) {
|
||||
if ( ! empty( $request['id'] ) ) {
|
||||
return new WP_Error(
|
||||
'rest_post_exists',
|
||||
__( 'Cannot create existing post.', 'jetpack' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$post_type = get_post_type_object( 'attachment' );
|
||||
|
||||
if ( ! current_user_can( $post_type->cap->create_posts ) ) {
|
||||
return new WP_Error(
|
||||
'rest_cannot_create',
|
||||
__( 'Sorry, you are not allowed to create posts as this user.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'upload_files' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_cannot_create',
|
||||
__( 'Sorry, you are not allowed to upload media on this site.', 'jetpack' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitization callback for media parameter.
|
||||
*
|
||||
* @param array $param Media parameter.
|
||||
* @return true|\WP_Error
|
||||
*/
|
||||
public function sanitize_media( $param ) {
|
||||
$param = $this->prepare_media_param( $param );
|
||||
|
||||
return rest_sanitize_value_from_schema( $param, $this->media_schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation callback for media parameter.
|
||||
*
|
||||
* @param array $param Media parameter.
|
||||
* @return true|\WP_Error
|
||||
*/
|
||||
public function validate_media( $param ) {
|
||||
$param = $this->prepare_media_param( $param );
|
||||
|
||||
return rest_validate_value_from_schema( $param, $this->media_schema, 'media' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes guid json and sets parameter defaults.
|
||||
*
|
||||
* @param array $param Media parameter.
|
||||
* @return array
|
||||
*/
|
||||
private function prepare_media_param( $param ) {
|
||||
foreach ( $param as $key => $item ) {
|
||||
if ( ! empty( $item['guid'] ) ) {
|
||||
$param[ $key ]['guid'] = json_decode( $item['guid'], true );
|
||||
}
|
||||
|
||||
if ( empty( $param[ $key ]['caption'] ) ) {
|
||||
$param[ $key ]['caption'] = '';
|
||||
}
|
||||
if ( empty( $param[ $key ]['title'] ) ) {
|
||||
$param[ $key ]['title'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves media items from external libraries.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array|\WP_Error|mixed
|
||||
*/
|
||||
public function get_external_media( \WP_REST_Request $request ) {
|
||||
$params = $request->get_params();
|
||||
$wpcom_path = sprintf( '/meta/external-media/%s', rawurlencode( $params['service'] ) );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
|
||||
$request->set_query_params( $params );
|
||||
|
||||
return rest_do_request( $request );
|
||||
}
|
||||
|
||||
// Build query string to pass to wpcom endpoint.
|
||||
$service_args = array_filter(
|
||||
$params,
|
||||
function ( $key ) {
|
||||
return in_array( $key, array( 'search', 'number', 'path', 'page_handle', 'filter', 'session_id' ), true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
if ( ! empty( $service_args ) ) {
|
||||
$wpcom_path .= '?' . http_build_query( $service_args );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user( $wpcom_path );
|
||||
|
||||
switch ( wp_remote_retrieve_response_code( $response ) ) {
|
||||
case 200:
|
||||
$response = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
break;
|
||||
|
||||
case 401:
|
||||
$response = new WP_Error(
|
||||
'authorization_required',
|
||||
__( 'You are not connected to that service.', 'jetpack' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
break;
|
||||
|
||||
case 403:
|
||||
$error = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
$response = new WP_Error( $error->code, $error->message, $error->data );
|
||||
break;
|
||||
|
||||
default:
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$response->add_data( array( 'status' => 400 ) );
|
||||
break;
|
||||
}
|
||||
$response = new WP_Error(
|
||||
'rest_request_error',
|
||||
__( 'An unknown error has occurred. Please try again later.', 'jetpack' ),
|
||||
array( 'status' => wp_remote_retrieve_response_code( $response ) )
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an external media item to the media library.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array|\WP_Error|mixed
|
||||
**/
|
||||
public function copy_external_media( \WP_REST_Request $request ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
|
||||
$post_id = $request->get_param( 'post_id' );
|
||||
$should_proxy = $request->get_param( 'should_proxy' );
|
||||
$service = rawurlencode( $request->get_param( 'service' ) );
|
||||
|
||||
$responses = array();
|
||||
|
||||
foreach ( $request->get_param( 'media' ) as $item ) {
|
||||
// Download file to temp dir.
|
||||
if ( $should_proxy ) {
|
||||
$wpcom_path = sprintf( '/meta/external-media/proxy/%s', $service );
|
||||
$wpcom_path .= '?url=' . rawurlencode( $item['guid']['url'] );
|
||||
$download_url = wp_tempnam();
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
$wpcom_path,
|
||||
'2',
|
||||
array(
|
||||
'method' => 'POST',
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$responses[] = $response;
|
||||
continue;
|
||||
}
|
||||
$wp_filesystem = $this->get_wp_filesystem();
|
||||
$written = $wp_filesystem->put_contents( $download_url, wp_remote_retrieve_body( $response ) );
|
||||
|
||||
if ( false === $written ) {
|
||||
$responses[] = new WP_Error(
|
||||
'rest_upload_error',
|
||||
__( 'Could not download media file.', 'jetpack' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$download_url = $this->get_download_url( $item['guid'] );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $download_url ) ) {
|
||||
$responses[] = $download_url;
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = $this->sideload_media( $item['guid']['name'], $download_url, $post_id );
|
||||
if ( is_wp_error( $id ) ) {
|
||||
$responses[] = $id;
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->update_attachment_meta( $id, $item );
|
||||
|
||||
// Add attachment data or WP_Error.
|
||||
$responses[] = $this->get_attachment_data( $id, $item );
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets connection authorization details.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array|\WP_Error|mixed
|
||||
*/
|
||||
public function get_connection_details( \WP_REST_Request $request ) {
|
||||
$service = $request->get_param( 'service' );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$wpcom_path = sprintf( '/meta/external-media/connection/%s', rawurlencode( $service ) );
|
||||
$internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
|
||||
$internal_request->set_query_params( $request->get_params() );
|
||||
|
||||
return rest_do_request( $internal_request );
|
||||
}
|
||||
|
||||
$site_id = Manager::get_site_id();
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $site_id;
|
||||
}
|
||||
|
||||
$path = sprintf( '/sites/%d/external-services', $site_id );
|
||||
$response = Client::wpcom_json_api_request_as_user( $path );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
if ( ! property_exists( $body, 'services' ) || ! property_exists( $body->services, $service ) ) {
|
||||
return new WP_Error(
|
||||
'bad_request',
|
||||
__( 'An error occurred. Please try again later.', 'jetpack' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
return $body->services->{ $service };
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a Google Photos connection.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return array|WP_Error|WP_REST_Response
|
||||
*/
|
||||
public function delete_connection( WP_REST_Request $request ) {
|
||||
$service = rawurlencode( $request->get_param( 'service' ) );
|
||||
$wpcom_path = sprintf( '/meta/external-media/connection/%s', $service );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$internal_request = new WP_REST_Request( 'DELETE', '/' . $this->namespace . $wpcom_path );
|
||||
$internal_request->set_query_params( $request->get_params() );
|
||||
|
||||
return rest_do_request( $internal_request );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
$wpcom_path,
|
||||
'2',
|
||||
array(
|
||||
'method' => 'DELETE',
|
||||
)
|
||||
);
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Google Photos Picker enabled Status.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array|\WP_Error|mixed
|
||||
*/
|
||||
public function get_picker_status( \WP_REST_Request $request ) {
|
||||
$service = $request->get_param( 'service' );
|
||||
$wpcom_path = sprintf( '/meta/external-media/connection/%s/picker_status', rawurlencode( $service ) );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
|
||||
$internal_request->set_query_params( $request->get_params() );
|
||||
|
||||
return rest_do_request( $internal_request );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
$wpcom_path,
|
||||
'2',
|
||||
array(
|
||||
'method' => 'GET',
|
||||
)
|
||||
);
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new session for a service.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array|\WP_Error|mixed
|
||||
*/
|
||||
public function create_session( \WP_REST_Request $request ) {
|
||||
$service = $request->get_param( 'service' );
|
||||
$wpcom_path = sprintf( '/meta/external-media/session/%s', rawurlencode( $service ) );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$internal_request = new \WP_REST_Request( 'POST', '/' . $this->namespace . $wpcom_path );
|
||||
$internal_request->set_query_params( $request->get_params() );
|
||||
|
||||
return rest_do_request( $internal_request );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
$wpcom_path,
|
||||
'2',
|
||||
array(
|
||||
'method' => 'POST',
|
||||
)
|
||||
);
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a session for a service.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array|\WP_Error|mixed
|
||||
*/
|
||||
public function get_session( \WP_REST_Request $request ) {
|
||||
$service = $request->get_param( 'service' );
|
||||
$session_id = $request->get_param( 'session_id' );
|
||||
$wpcom_path = sprintf( '/meta/external-media/session/%s/%s', rawurlencode( $service ), rawurlencode( $session_id ) );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
|
||||
$internal_request->set_query_params( $request->get_params() );
|
||||
|
||||
return rest_do_request( $internal_request );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
$wpcom_path,
|
||||
'2',
|
||||
array(
|
||||
'method' => 'GET',
|
||||
)
|
||||
);
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a session for a service.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array|\WP_Error|mixed
|
||||
*/
|
||||
public function delete_session( \WP_REST_Request $request ) {
|
||||
$service = $request->get_param( 'service' );
|
||||
$session_id = $request->get_param( 'session_id' );
|
||||
$wpcom_path = sprintf( '/meta/external-media/session/%s/%s', rawurlencode( $service ), rawurlencode( $session_id ) );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$internal_request = new \WP_REST_Request( 'DELETE', '/' . $this->namespace . $wpcom_path );
|
||||
$internal_request->set_query_params( $request->get_params() );
|
||||
|
||||
return rest_do_request( $internal_request );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
$wpcom_path,
|
||||
'2',
|
||||
array(
|
||||
'method' => 'DELETE',
|
||||
)
|
||||
);
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies media requests with proper authorization headers
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error|array Response object or WP_Error.
|
||||
*/
|
||||
public function proxy_media_request( $request ) {
|
||||
$params = $request->get_params();
|
||||
$service = rawurlencode( $request->get_param( 'service' ) );
|
||||
$wpcom_path = sprintf( '/meta/external-media/proxy/%s', $service );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$request = new \WP_REST_Request( 'POST', '/' . $this->namespace . $wpcom_path );
|
||||
$request->set_query_params( $params );
|
||||
|
||||
return rest_do_request( $request );
|
||||
|
||||
} else {
|
||||
// Build query string to pass to wpcom endpoint.
|
||||
$service_args = array_filter(
|
||||
$params,
|
||||
function ( $key ) {
|
||||
return in_array( $key, array( 'url' ), true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
|
||||
if ( ! empty( $service_args ) ) {
|
||||
$wpcom_path .= '?' . http_build_query( $service_args );
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
$wpcom_path,
|
||||
'2',
|
||||
array(
|
||||
'method' => 'POST',
|
||||
)
|
||||
);
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
$headers = wp_remote_retrieve_headers( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
|
||||
// For non-200 responses, parse and return JSON error
|
||||
if ( $status_code !== 200 ) {
|
||||
$error_data = json_decode( $body, true );
|
||||
return new \WP_REST_Response( $error_data, $status_code );
|
||||
}
|
||||
}
|
||||
|
||||
// Return binary content directly
|
||||
$valid_headers = array(
|
||||
'content-type',
|
||||
'content-length',
|
||||
'content-disposition',
|
||||
);
|
||||
// Set content headers
|
||||
foreach ( $valid_headers as $header ) {
|
||||
if ( ! empty( $headers[ $header ] ) ) {
|
||||
header( ucwords( $header, '-' ) . ': ' . $headers[ $header ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Set cache headers
|
||||
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||
header( 'Pragma: no-cache' );
|
||||
header( 'Expires: 0' );
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Media binary data
|
||||
echo $body;
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter callback to provide a shorter file name for google images.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function tmp_name() {
|
||||
return $this->tmp_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a download URL, dealing with Google's long file names.
|
||||
*
|
||||
* @param array $guid Media information.
|
||||
* @return string|\WP_Error
|
||||
*/
|
||||
public function get_download_url( $guid ) {
|
||||
$this->tmp_name = $guid['name'];
|
||||
add_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) );
|
||||
$download_url = download_url( $guid['url'] );
|
||||
remove_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) );
|
||||
|
||||
if ( is_wp_error( $download_url ) ) {
|
||||
$download_url->add_data( array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
return $download_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads media file and creates attachment object.
|
||||
*
|
||||
* @param string $file_name Name of media file.
|
||||
* @param string $download_url Download URL.
|
||||
* @param int $post_id The ID of the post to attach the image to.
|
||||
*
|
||||
* @return int|\WP_Error
|
||||
*/
|
||||
public function sideload_media( $file_name, $download_url, $post_id = 0 ) {
|
||||
$file = array(
|
||||
'name' => wp_basename( $file_name ),
|
||||
'tmp_name' => $download_url,
|
||||
);
|
||||
|
||||
$id = media_handle_sideload( $file, $post_id, null );
|
||||
if ( is_wp_error( $id ) ) {
|
||||
wp_delete_file( $file['tmp_name'] );
|
||||
$id->add_data( array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates attachment meta data for media item.
|
||||
*
|
||||
* @param int $id Attachment ID.
|
||||
* @param array $item Media item.
|
||||
*/
|
||||
public function update_attachment_meta( $id, $item ) {
|
||||
$meta = wp_get_attachment_metadata( $id );
|
||||
$meta['image_meta']['title'] = $item['title'];
|
||||
$meta['image_meta']['caption'] = $item['caption'];
|
||||
|
||||
wp_update_attachment_metadata( $id, $meta );
|
||||
|
||||
update_post_meta( $id, '_wp_attachment_image_alt', $item['title'] );
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $id,
|
||||
'post_excerpt' => $item['caption'],
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $item['meta'] ) ) {
|
||||
foreach ( $item['meta'] as $meta_key => $meta_value ) {
|
||||
update_post_meta( $id, $meta_key, $meta_value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves attachment data for media item.
|
||||
*
|
||||
* @param int $id Attachment ID.
|
||||
* @param array $item Media item.
|
||||
*
|
||||
* @return array|\WP_REST_Response Attachment data on success, WP_Error on failure.
|
||||
*/
|
||||
public function get_attachment_data( $id, $item ) {
|
||||
$image_src = wp_get_attachment_image_src( $id, 'full' );
|
||||
|
||||
if ( empty( $image_src[0] ) ) {
|
||||
$response = new WP_Error(
|
||||
'rest_upload_error',
|
||||
__( 'Could not retrieve source URL.', 'jetpack' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
} else {
|
||||
$response = array(
|
||||
'id' => $id,
|
||||
'caption' => $item['caption'],
|
||||
'alt' => $item['title'],
|
||||
'type' => 'image',
|
||||
'url' => $image_src[0],
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the wp filesystem.
|
||||
*
|
||||
* @return \WP_Filesystem_Base|null
|
||||
*/
|
||||
private function get_wp_filesystem() {
|
||||
global $wp_filesystem;
|
||||
|
||||
if ( ! isset( $wp_filesystem ) ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/file.php';
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
return $wp_filesystem;
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_External_Media' );
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for the Jetpack Blogroll block.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 12.2
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Status\Visitor;
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Following
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Following extends WP_REST_Controller {
|
||||
/**
|
||||
* Namespace prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $namespace = 'wpcom/v2';
|
||||
|
||||
/**
|
||||
* Endpoint base route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $rest_base = 'following';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
$this->wpcom_is_site_specific_endpoint = false;
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/mine',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_following' ),
|
||||
'permission_callback' => 'is_user_logged_in',
|
||||
'args' => array(
|
||||
'ignore_user_blogs' => array(
|
||||
'type' => 'boolean',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/recommendations',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_recommendations' ),
|
||||
'permission_callback' => 'is_user_logged_in',
|
||||
'args' => array(
|
||||
'number' => array(
|
||||
'type' => 'number',
|
||||
'default' => 5,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_numeric( $param ) && $param <= 20;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sites the user is following
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return array|WP_Error list of followed sites, WP_Error otherwise
|
||||
*/
|
||||
public function get_following( $request ) {
|
||||
$ignore_user_blogs = $request->get_param( 'ignore_user_blogs' );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
require_lib( 'wpcom-get-user-followed-blogs' );
|
||||
return get_user_followed_blogs( get_current_user_id(), $ignore_user_blogs );
|
||||
}
|
||||
|
||||
$body = Client::wpcom_json_api_request_as_user(
|
||||
sprintf( '/me/following%s', $ignore_user_blogs ? '?ignore_user_blogs=true' : '' ),
|
||||
'2',
|
||||
array(
|
||||
'method' => 'GET',
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $body ) ) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $body ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets recommended sites for user
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return array|WP_Error list of following recommendations, WP_Error otherwise
|
||||
*/
|
||||
public function get_recommendations( $request ) {
|
||||
$number_of_recommendations = $request->get_param( 'number' );
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
require_lib( 'wpcom-get-user-followed-blogs' );
|
||||
return get_user_following_recommendations( get_current_user_id(), $number_of_recommendations );
|
||||
}
|
||||
|
||||
$body = Client::wpcom_json_api_request_as_user(
|
||||
sprintf( '/me/following/recommendations?number=%d', $number_of_recommendations ),
|
||||
'2',
|
||||
array(
|
||||
'method' => 'GET',
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $body ) ) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $body ) );
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Following' );
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* Get the User ID of a Goodreads account using its Author ID.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Goodreads block endpoint.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Goodreads extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint route.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
'wpcom/v2',
|
||||
'/goodreads/user-id',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_goodreads_user_id' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'Goodreads user ID', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'minimum' => 1,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_numeric( $param ) && (int) $param > 0;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user ID from the author ID.
|
||||
*
|
||||
* @param \WP_REST_Request $request request object.
|
||||
*
|
||||
* @return \WP_Error|int Goodreads user ID (or 404 error if not found).
|
||||
*/
|
||||
public function get_goodreads_user_id( $request ) {
|
||||
$profile_id = $request->get_param( 'id' );
|
||||
$url = 'https://www.goodreads.com/author/show/' . $profile_id;
|
||||
$response = wp_remote_get( esc_url_raw( $url ) );
|
||||
$not_found = new WP_Error( 'not_found', 'Goodreads user not found.', array( 'status' => 404 ) );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$pattern = '/goodreads\.com\/user\/updates_rss\/(\d+)/';
|
||||
|
||||
if ( preg_match( $pattern, $body, $matches ) ) {
|
||||
$user_id = intval( $matches[1] );
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
return $not_found;
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Goodreads' );
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* Validate whether a google doc is available for embedding.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Extensions\GoogleDocsEmbed;
|
||||
|
||||
/**
|
||||
* Google Docs block endpoint.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Google_Docs extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint route.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
'wpcom/v2',
|
||||
'/checkGoogleDocVisibility',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'check_document_visibility' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check URL
|
||||
*
|
||||
* @param \WP_REST_Request $request request object.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function check_document_visibility( $request ) {
|
||||
|
||||
$document_url = $request->get_param( 'url' );
|
||||
$document_url = GoogleDocsEmbed\map_gsuite_url( $document_url );
|
||||
$response_head = wp_safe_remote_head( $document_url );
|
||||
$is_public_document = ! is_wp_error( $response_head ) && ! empty( $response_head['response']['code'] ) && 200 === absint( $response_head['response']['code'] );
|
||||
|
||||
if ( ! $is_public_document ) {
|
||||
return new \WP_Error( 'Unauthorized', esc_html__( 'The document is not publicly accessible', 'jetpack' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
return new \WP_REST_Response( '', 200 );
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Google_Docs' );
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for the Instagram connections.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 8.5.0
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
|
||||
/**
|
||||
* Instagram connections helper API.
|
||||
*
|
||||
* @since 8.5
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Instagram_Gallery extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'instagram-gallery';
|
||||
$this->is_wpcom = false;
|
||||
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$this->is_wpcom = true;
|
||||
|
||||
if ( ! class_exists( 'WPCOM_Instagram_Gallery_Helper' ) ) {
|
||||
\require_lib( 'instagram-gallery-helper' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'Jetpack_Instagram_Gallery_Helper' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-instagram-gallery-helper.php';
|
||||
}
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the route.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/connect-url',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_instagram_connect_url' ),
|
||||
'permission_callback' => '__return_true',
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/connections',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_instagram_connections' ),
|
||||
'permission_callback' => 'is_user_logged_in',
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/gallery',
|
||||
array(
|
||||
'args' => array(
|
||||
'access_token' => array(
|
||||
'description' => __( 'An Instagram Keyring access token.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'minimum' => 1,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_numeric( $param ) && (int) $param > 0;
|
||||
},
|
||||
),
|
||||
'count' => array(
|
||||
'description' => __( 'How many Instagram posts?', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'minimum' => 1,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_numeric( $param ) && (int) $param > 0;
|
||||
},
|
||||
),
|
||||
),
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_instagram_gallery' ),
|
||||
'permission_callback' => '__return_true',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Instagram connect URL.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_instagram_connect_url() {
|
||||
if ( $this->is_wpcom ) {
|
||||
return WPCOM_Instagram_Gallery_Helper::get_connect_url();
|
||||
}
|
||||
|
||||
$site_id = Manager::get_site_id();
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $site_id;
|
||||
}
|
||||
|
||||
$path = sprintf( '/sites/%d/external-services', $site_id );
|
||||
$response = Client::wpcom_json_api_request_as_user( $path );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
if ( ! property_exists( $body, 'services' ) || ! property_exists( $body->services, 'instagram-basic-display' ) ) {
|
||||
return new WP_Error(
|
||||
'bad_request',
|
||||
__( 'An error occurred. Please try again later.', 'jetpack' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
return $body->services->{ 'instagram-basic-display' }->connect_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of stored Instagram connections for the current user.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_instagram_connections() {
|
||||
if ( $this->is_wpcom ) {
|
||||
return WPCOM_Instagram_Gallery_Helper::get_connections();
|
||||
}
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user( '/me/connections' );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
$connections = array();
|
||||
|
||||
if ( isset( $body->connections ) && is_array( $body->connections ) ) {
|
||||
foreach ( $body->connections as $connection ) {
|
||||
if ( 'instagram-basic-display' === $connection->service && 'ok' === $connection->status ) {
|
||||
$connections[] = array(
|
||||
'token' => (string) $connection->ID,
|
||||
'username' => $connection->external_name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Instagram Gallery.
|
||||
*
|
||||
* @param WP_REST_Request $request The request.
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_instagram_gallery( $request ) {
|
||||
return Jetpack_Instagram_Gallery_Helper::get_instagram_gallery( $request['access_token'], $request['count'] );
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Instagram_Gallery' );
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
/**
|
||||
* API endpoints to interact with WordPress.com
|
||||
* to get info from the Mailchimp API for use with the Mailchimp block.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Redirect;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
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' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/groups',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_mailchimp_groups' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] ) && isset( $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 = Redirect::get_url(
|
||||
'calypso-marketing-connections',
|
||||
array(
|
||||
'site' => rawurlencode( $site_id ),
|
||||
'query' => 'mailchimp',
|
||||
)
|
||||
);
|
||||
return array(
|
||||
'code' => $this->is_connected() ? 'connected' : 'not_connected',
|
||||
'connect_url' => $connect_url,
|
||||
'site_id' => $site_id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Mailchimp groups for the accounted connected to the current blog
|
||||
*
|
||||
* @return mixed
|
||||
* groups:array
|
||||
* site_id:int
|
||||
*/
|
||||
public function get_mailchimp_groups() {
|
||||
$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
|
||||
);
|
||||
}
|
||||
|
||||
// Do not attempt to fetch groups if Mailchimp is not connected.
|
||||
if ( ! $this->is_connected() ) {
|
||||
return new WP_Error(
|
||||
'mailchimp_not_connected',
|
||||
__( 'Your site is not connected to Mailchimp yet.', 'jetpack' ),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
$path = sprintf( '/sites/%d/mailchimp/groups', absint( $site_id ) );
|
||||
$request = Client::wpcom_json_api_request_as_blog( $path );
|
||||
$body = wp_remote_retrieve_body( $request );
|
||||
return json_decode( $body );
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Mailchimp' );
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for the Newsletter Categories
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 12.6
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Following
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Newsletter_Categories_List extends WP_REST_Controller {
|
||||
use WPCOM_REST_API_Proxy_Request;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
$this->wpcom_is_site_specific_endpoint = true;
|
||||
$this->base_api_path = 'wpcom';
|
||||
$this->version = 'v2';
|
||||
$this->namespace = $this->base_api_path . '/' . $this->version;
|
||||
$this->rest_base = '/newsletter-categories';
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
$this->wpcom_is_site_specific_endpoint = true;
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
$options = array(
|
||||
'show_in_index' => true,
|
||||
'methods' => 'GET',
|
||||
// if this is not a wpcom site, we need to proxy the request to wpcom
|
||||
'callback' => ( ( new Host() )->is_wpcom_simple() ) ? array(
|
||||
$this,
|
||||
'get_newsletter_categories',
|
||||
) : array( $this, 'proxy_request_to_wpcom_as_user' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base,
|
||||
$options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the site's newsletter categories
|
||||
*
|
||||
* @return array|WP_Error list of newsletter categories
|
||||
*/
|
||||
public function get_newsletter_categories() {
|
||||
require_lib( 'newsletter-categories' );
|
||||
|
||||
$newsletter_categories = \Newsletter_Categories\get_newsletter_categories();
|
||||
|
||||
// Include subscription counts for each category if the user can manage categories.
|
||||
if ( $this->can_manage_categories() === true ) {
|
||||
$subscription_counts_per_category = \Newsletter_Categories\get_blog_subscription_counts_per_category();
|
||||
array_walk(
|
||||
$newsletter_categories,
|
||||
function ( &$category ) use ( $subscription_counts_per_category ) {
|
||||
$category['subscription_count'] = $subscription_counts_per_category[ $category['id'] ] ? $subscription_counts_per_category[ $category['id'] ] : 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'enabled' => (bool) get_option( 'wpcom_newsletter_categories_enabled', false ),
|
||||
'newsletter_categories' => $newsletter_categories,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Newsletter_Categories_List' );
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for the Newsletter Categories
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 12.6
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Newsletter_Categories_Subscriptions_Count
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Newsletter_Categories_Subscriptions_Count extends WP_REST_Controller {
|
||||
use WPCOM_REST_API_Proxy_Request;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
$this->wpcom_is_site_specific_endpoint = true;
|
||||
$this->base_api_path = 'wpcom';
|
||||
$this->version = 'v2';
|
||||
$this->namespace = $this->base_api_path . '/' . $this->version;
|
||||
$this->rest_base = '/newsletter-categories/count';
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
$options = array(
|
||||
'show_in_index' => true,
|
||||
'methods' => 'GET',
|
||||
// if this is not a wpcom site, we need to proxy the request to wpcom
|
||||
'callback' => ( ( new Host() )->is_wpcom_simple() ) ? array(
|
||||
$this,
|
||||
'get_newsletter_categories_subscriptions_count',
|
||||
) : array( $this, 'proxy_request_to_wpcom_as_user' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
'args' => array(
|
||||
'term_ids' => array(
|
||||
'required' => false,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return empty( $param ) || ( is_string( $param ) && preg_match( '/^(\d+,)*\d+$/', $param ) );
|
||||
},
|
||||
'default' => '',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base,
|
||||
$options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscriptions count for the given categories.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_newsletter_categories_subscriptions_count( WP_REST_Request $request ) {
|
||||
require_lib( 'newsletter-categories' );
|
||||
|
||||
$blog_id = get_current_blog_id();
|
||||
$term_ids = explode( ',', $request->get_param( 'term_ids' ) );
|
||||
|
||||
$subscriptions_count = \Newsletter_Categories\get_blog_subscriptions_aggregate_count( $blog_id, $term_ids );
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'subscriptions_count' => $subscriptions_count,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Newsletter_Categories_Subscriptions_Count' );
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Podcast Player API
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 8.4.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetch podcast feeds and parse data for the Podcast Player block.
|
||||
*
|
||||
* @since 8.4.0
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Podcast_Player extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( ! class_exists( 'Jetpack_Podcast_Helper' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-podcast-helper.php';
|
||||
}
|
||||
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'podcast-player';
|
||||
// This endpoint *does not* need to connect directly to Jetpack sites.
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the route.
|
||||
*/
|
||||
public function register_routes() {
|
||||
// GET /sites/<blog_id>/podcast-player - Returns feed data.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_player_data' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
'args' => array(
|
||||
'url' => array(
|
||||
'description' => __( 'The Podcast RSS feed URL.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'required' => 'true',
|
||||
'validate_callback' => function ( $param ) {
|
||||
return wp_http_validate_url( $param );
|
||||
},
|
||||
),
|
||||
'guids' => array(
|
||||
'description' => __( 'A list of unique identifiers for fetching specific podcast episodes.', 'jetpack' ),
|
||||
'type' => 'array',
|
||||
'required' => 'false',
|
||||
'validate_callback' => function ( $guids ) {
|
||||
return is_array( $guids );
|
||||
},
|
||||
'sanitize_callback' => function ( $guids ) {
|
||||
return array_map( 'sanitize_text_field', $guids );
|
||||
},
|
||||
),
|
||||
'episode-options' => array(
|
||||
'description' => __( 'Whether we should return the episodes list for use in the selection UI', 'jetpack' ),
|
||||
'type' => 'boolean',
|
||||
'required' => 'false',
|
||||
),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// GET /sites/<blog_id>/podcast-player/track-quantity - Returns number of tracks.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/track-quantity',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_tracks_quantity' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tracks quantity
|
||||
*
|
||||
* @return Integer number of tracks.
|
||||
* */
|
||||
public function get_tracks_quantity() {
|
||||
return rest_ensure_response( Jetpack_Podcast_Helper::get_tracks_quantity() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves data needed to display a podcast player from RSS feed.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST API request data.
|
||||
* @return WP_REST_Response The REST API response.
|
||||
*/
|
||||
public function get_player_data( $request ) {
|
||||
$helper = new Jetpack_Podcast_Helper( $request['url'] );
|
||||
|
||||
$args = array();
|
||||
|
||||
if ( isset( $request['guids'] ) ) {
|
||||
$args['guids'] = $request['guids'];
|
||||
}
|
||||
|
||||
if ( isset( $request['episode-options'] ) && $request['episode-options'] ) {
|
||||
$args['episode-options'] = true;
|
||||
}
|
||||
|
||||
$player_data = $helper->get_player_data( $args );
|
||||
|
||||
if ( is_wp_error( $player_data ) ) {
|
||||
return rest_ensure_response( $player_data );
|
||||
}
|
||||
|
||||
$player_data = $this->prepare_for_response( $player_data, $request );
|
||||
return rest_ensure_response( $player_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out data based on ?_fields= request parameter
|
||||
*
|
||||
* @param array $player_data Data for the player.
|
||||
* @param WP_REST_Request $request The request.
|
||||
* @return array filtered $player_data
|
||||
*/
|
||||
public function prepare_for_response( $player_data, $request ) {
|
||||
if ( ! is_callable( array( $this, 'get_fields_for_response' ) ) ) {
|
||||
return $player_data;
|
||||
}
|
||||
|
||||
$fields = $this->get_fields_for_response( $request );
|
||||
|
||||
$response_data = array();
|
||||
foreach ( $player_data as $field => $value ) {
|
||||
if ( in_array( $field, $fields, true ) ) {
|
||||
$response_data[ $field ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $response_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the response schema, conforming to JSON Schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
return Jetpack_Podcast_Helper::get_player_data_schema();
|
||||
}
|
||||
}
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Podcast_Player' );
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for user profile.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Profile
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Profile extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'profile';
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_item' ),
|
||||
'permission_callback' => array( $this, 'get_item_permissions_check' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to user profile.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
|
||||
*/
|
||||
public function get_item_permissions_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( ! current_user_can( 'read' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__( 'Sorry, you are not allowed to view your user profile on this site.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the user profile.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'admin_color' => get_user_option( 'admin_color' ),
|
||||
'locale' => get_user_locale(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Profile' );
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* Publicize: Share post
|
||||
*
|
||||
* This file is synced from the Jetpack monorepo to WPCOM.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
|
||||
require_once __DIR__ . '/publicize-connections.php';
|
||||
|
||||
/**
|
||||
* Publicize: Share post class.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Publicize_Share_Post extends WP_REST_Controller {
|
||||
/**
|
||||
* The constructor sets the route namespace, rest_base, and registers our API route and endpoint.
|
||||
* Additionally, we check if we're executing this file on WPCOM or Jetpack.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
|
||||
// $wpcom_is_wpcom_only_endpoint = true keeps WPCOM from trying to loop back to the Jetpack endpoint.
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
|
||||
// Determine if this endpoint is running on WPCOM or not.
|
||||
$this->is_wpcom = false;
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$this->is_wpcom = true;
|
||||
}
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* This file is synced from Jetpack to WPCOM and this method creates a slightly different route for both sites.
|
||||
* Jetpack route: http://{$site}/wp-json/wpcom/v2/posts/{$postId}/publicize
|
||||
* WPCOM route: https://public-api.wordpress.com/wpcom/v2/sites/{$siteId}/posts/{$postId}/publicize
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/posts/(?P<postId>\d+)/publicize',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'share_post' ),
|
||||
'permission_callback' => array( $this, 'permissions_check' ),
|
||||
'args' => array(
|
||||
'message' => array(
|
||||
'description' => __( 'The message to share.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_string( $param );
|
||||
},
|
||||
'sanitize_callback' => 'sanitize_textarea_field',
|
||||
),
|
||||
'skipped_connections' => array(
|
||||
'description' => __( 'Array of external connection IDs to skip sharing.', 'jetpack' ),
|
||||
'type' => 'array',
|
||||
'required' => false,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_array( $param );
|
||||
},
|
||||
'sanitize_callback' => function ( $param ) {
|
||||
return array_map( 'absint', $param );
|
||||
},
|
||||
),
|
||||
'async' => array(
|
||||
'description' => __( 'Whether to share the post asynchronously.', 'jetpack' ),
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
),
|
||||
),
|
||||
),
|
||||
// override = true because this API route was commandeered from the file
|
||||
// wp-content/rest-api-plugins/endpoints/sites-publicize.php on WPCOM.
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the user has proper tokens and permissions to publish posts on this blog.
|
||||
*
|
||||
* @return WP_Error|boolean
|
||||
*/
|
||||
public function permissions_check() {
|
||||
if ( ! get_current_user_id() ) {
|
||||
return new WP_Error(
|
||||
'rest_cannot_view',
|
||||
__( 'Sorry, you cannot view this resource without a valid token for this blog.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
if ( ! current_user_can( 'publish_posts' ) ) {
|
||||
return new WP_Error( 'unauthorized', 'Your token must have permission to publish posts.', array( 'status' => 401 ) );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this method callback is executed on WPCOM, we share the post using republicize_post(). If this method callback
|
||||
* is executed on a Jetpack site, we make an API call to WPCOM using wpcom_json_api_request_as_user() and return
|
||||
* the results. In both cases, this file and method are executed, as this file is synced from Jetpack to WPCOM.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_Error|array The publicize results, including two arrays: `results` and `errors`
|
||||
*/
|
||||
public function share_post( $request ) {
|
||||
$post_id = $request->get_param( 'postId' );
|
||||
$message = trim( $request->get_param( 'message' ) );
|
||||
$skip_connection_ids = $request->get_param( 'skipped_connections' );
|
||||
$async = (bool) $request->get_param( 'async' );
|
||||
|
||||
if ( $this->is_wpcom ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( empty( $post ) ) {
|
||||
return new WP_Error( 'not_found', 'Cannot find that post', array( 'status' => 404 ) );
|
||||
}
|
||||
if ( 'publish' !== $post->post_status ) {
|
||||
return new WP_Error( 'not_published', 'Cannot share an unpublished post', array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$publicize = publicize_init();
|
||||
$result = $publicize->republicize_post( (int) $post_id, $message, $skip_connection_ids, true, ! $async, get_current_user_id() );
|
||||
if ( false === $result ) {
|
||||
return new WP_Error( 'not_found', 'Cannot find that post', array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return $result;
|
||||
} else {
|
||||
$response = $this->proxy_request( $post_id, $message, $skip_connection_ids, $async );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes the request on to the WPCOM endpoint, and returns the result.
|
||||
*
|
||||
* @param int $post_id The post ID being shared.
|
||||
* @param string $message The custom message to be used.
|
||||
* @param array $skip_connection_ids An array of connection IDs where the post shouldn't be shared.
|
||||
* @param bool $async Whether to share the post asynchronously.
|
||||
*
|
||||
* @return array|WP_Error $response Response data, else WP_Error on failure.
|
||||
*/
|
||||
public function proxy_request( $post_id, $message, $skip_connection_ids, $async = false ) {
|
||||
/*
|
||||
* Publicize endpoint on WPCOM:
|
||||
* [POST] wpcom/v2/sites/{$siteId}/posts/{$postId}/publicize
|
||||
* body:
|
||||
* - message: string
|
||||
* - skipped_connections: array of connection ids to skip
|
||||
*/
|
||||
$url = sprintf(
|
||||
'/sites/%d/posts/%d/publicize',
|
||||
Jetpack_Options::get_option( 'id' ),
|
||||
$post_id
|
||||
);
|
||||
|
||||
return Client::wpcom_json_api_request_as_user(
|
||||
$url,
|
||||
'v2',
|
||||
array(
|
||||
'method' => 'POST',
|
||||
),
|
||||
array(
|
||||
'message' => $message,
|
||||
'skipped_connections' => $skip_connection_ids,
|
||||
'async' => $async,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Publicize_Share_Post' );
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
/**
|
||||
* Publicize: Share status
|
||||
*
|
||||
* This file is synced from the Jetpack monorepo to WPCOM.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
|
||||
/**
|
||||
* Publicize: Share post class.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Publicize_Share_Status extends WP_REST_Controller {
|
||||
|
||||
const SOCIAL_SHARES_POST_META_KEY = '_publicize_shares';
|
||||
|
||||
/**
|
||||
* The constructor sets the route namespace, rest_base, and registers our API route and endpoint.
|
||||
* Additionally, we check if we're executing this file on WPCOM or Jetpack.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
|
||||
// $wpcom_is_wpcom_only_endpoint = true keeps WPCOM from trying to loop back to the Jetpack endpoint.
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
|
||||
// Determine if this endpoint is running on WPCOM or not.
|
||||
$this->is_wpcom = false;
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$this->is_wpcom = true;
|
||||
}
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* This file is synced from Jetpack to WPCOM and this method creates a slightly different route for both sites.
|
||||
* Jetpack route: http://{$site}/wp-json/wpcom/v2/publicize/share-status/{$postId}
|
||||
* WPCOM route: https://public-api.wordpress.com/wpcom/v2/sites/{$siteId}/publicize/share-status/{$postId}
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/publicize/share-status/(?P<post_id>\d+)',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_post_share_status' ),
|
||||
'permission_callback' => array( $this, 'permissions_check' ),
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the user has permissions to publish posts on this blog.
|
||||
*
|
||||
* @return WP_Error|boolean
|
||||
*/
|
||||
public function permissions_check() {
|
||||
return current_user_can( 'publish_posts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* If this method callback is executed on WPCOM, we share the post using republicize_post(). If this method callback
|
||||
* is executed on a Jetpack site, we make an API call to WPCOM using wpcom_json_api_request_as_user() and return
|
||||
* the results. In both cases, this file and method are executed, as this file is synced from Jetpack to WPCOM.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_Error|array The share status.
|
||||
*/
|
||||
public function get_post_share_status( $request ) {
|
||||
$post_id = $request->get_param( 'post_id' );
|
||||
|
||||
if ( $this->is_wpcom ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( empty( $post ) ) {
|
||||
return new WP_Error( 'not_found', 'Cannot find that post', array( 'status' => 404 ) );
|
||||
}
|
||||
if ( 'publish' !== $post->post_status ) {
|
||||
return new WP_Error( 'not_published', 'Cannot get share status for an unpublished post', array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
// Not passing the third argument as `true`.
|
||||
$shares = get_post_meta( $post_id, self::SOCIAL_SHARES_POST_META_KEY );
|
||||
|
||||
$done = metadata_exists( 'post', $post_id, self::SOCIAL_SHARES_POST_META_KEY );
|
||||
|
||||
return array(
|
||||
'shares' => $done ? $shares : array(),
|
||||
'done' => $done,
|
||||
);
|
||||
} else {
|
||||
$response = $this->proxy_request( $post_id );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
|
||||
return json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes the request on to the WPCOM endpoint, and returns the result.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return array|WP_Error $response Response data, else WP_Error on failure.
|
||||
*/
|
||||
public function proxy_request( $post_id ) {
|
||||
/*
|
||||
* Publicize endpoint on WPCOM:
|
||||
* [POST] wpcom/v2/sites/{$siteId}/publicize/share-status/{$postId}
|
||||
*/
|
||||
$url = sprintf(
|
||||
'/sites/%d/publicize/share-status/%d',
|
||||
Jetpack_Options::get_option( 'id' ),
|
||||
$post_id
|
||||
);
|
||||
|
||||
return Client::wpcom_json_api_request_as_user(
|
||||
$url,
|
||||
'v2',
|
||||
array(
|
||||
'method' => 'GET',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Publicize_Share_Status' );
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for Related Posts
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 12.6
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Related_Posts
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Related_Posts extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->base_api_path = 'wpcom';
|
||||
$this->version = 'v2';
|
||||
$this->namespace = $this->base_api_path . '/' . $this->version;
|
||||
$this->rest_base = '/related-posts';
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base,
|
||||
array(
|
||||
'show_in_index' => true,
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_options' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/enable',
|
||||
array(
|
||||
'show_in_index' => true,
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'enable_rp' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'manage_options' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/(?P<id>[\d]+)',
|
||||
array(
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'Unique identifier for the post.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'show_in_index' => true,
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_related_posts' ),
|
||||
'permission_callback' => array( $this, 'get_related_posts_permissions_check' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the site's Related Posts Options.
|
||||
*
|
||||
* @return WP_REST_Response Array of information about the site's Related Posts options.
|
||||
* - enabled: Whether Related Posts is enabled.
|
||||
* - options: Array of options for Related Posts.
|
||||
*/
|
||||
public function get_options() {
|
||||
$options = Jetpack_Options::get_option( 'relatedposts', array() );
|
||||
$enabled = isset( $options['enabled'] ) ? (bool) $options['enabled'] : false;
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'enabled' => $enabled,
|
||||
'options' => $options,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the site's Related Posts.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response Array confirming the new status.
|
||||
*/
|
||||
public function enable_rp( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$old_relatedposts_options = Jetpack_Options::get_option( 'relatedposts', array() );
|
||||
$relatedposts_options_to_save = $old_relatedposts_options;
|
||||
$relatedposts_options_to_save['enabled'] = true;
|
||||
|
||||
// Enable Related Posts.
|
||||
$enable = Jetpack_Options::update_option( 'relatedposts', $relatedposts_options_to_save );
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'enabled' => (bool) $enable,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to get the related posts.
|
||||
*
|
||||
* @since 4.7.0
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return bool|WP_Error True if the request has read access for the related posts, WP_Error object or false otherwise.
|
||||
*/
|
||||
public function get_related_posts_permissions_check( $request ) {
|
||||
$post = $this->get_post( $request['id'] );
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $post->ID ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden_context',
|
||||
__( 'Sorry, you are not allowed to get the related post.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related posts
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response Array The related posts
|
||||
*/
|
||||
public function get_related_posts( $request ) {
|
||||
$post = $this->get_post( $request['id'] );
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'Jetpack_RelatedPosts' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . 'modules/related-posts/jetpack-related-posts.php';
|
||||
}
|
||||
$related_posts = \Jetpack_RelatedPosts::init()->get_for_post_id( $post->ID, array( 'size' => 6 ) );
|
||||
return rest_ensure_response( $related_posts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the post, if the ID is valid.
|
||||
*
|
||||
* @param int $id Supplied ID.
|
||||
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
|
||||
*/
|
||||
public function get_post( $id ) {
|
||||
$error = new WP_Error(
|
||||
'rest_post_invalid_id',
|
||||
__( 'Invalid post ID.', 'jetpack' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
|
||||
if ( (int) $id <= 0 ) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$post = get_post( (int) $id );
|
||||
if ( empty( $post ) || empty( $post->ID ) ) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Related_Posts' );
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for resolving URL redirects.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 8.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolve URL redirects.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Resolve_Redirect extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'resolve-redirect';
|
||||
// This endpoint *does not* need to connect directly to Jetpack sites.
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the route.
|
||||
*/
|
||||
public function register_routes() {
|
||||
// GET /sites/<blog_id>/resolve-redirect/<url> - Follow 301/302 redirects on a URL, and return the final destination.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/?(?P<url>.+)?',
|
||||
array(
|
||||
'args' => array(
|
||||
'url' => array(
|
||||
'description' => __( 'The URL to check for redirects.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'required' => 'true',
|
||||
'validate_callback' => function ( $param ) {
|
||||
return wp_http_validate_url( $param );
|
||||
},
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'follow_redirect' ),
|
||||
'permission_callback' => 'is_user_logged_in',
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follows 301/302 redirect for the passed URL, and returns the final destination and status code.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST API request data.
|
||||
* @return WP_REST_Response The REST API response.
|
||||
*/
|
||||
public function follow_redirect( $request ) {
|
||||
// Add a User-Agent header since the request is sometimes blocked without it.
|
||||
$response = wp_safe_remote_get(
|
||||
$request['url'],
|
||||
array(
|
||||
'headers' => array(
|
||||
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:71.0) Gecko/20100101 Firefox/71.0',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'url' => '',
|
||||
'status' => $response->get_error_code(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'url' => $this->get_response_url( $response['http_response']->get_response_object() ),
|
||||
'status' => wp_remote_retrieve_response_code( $response ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the response schema, conforming to JSON Schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'resolve-redirect',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'url' => array(
|
||||
'description' => __( 'The final destination of the URL being checked for redirects.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'status' => array(
|
||||
'description' => __( 'The status code of the URL\'s response.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the destination url from an http response.
|
||||
*
|
||||
* @todo Add WpOrg\Requests\Response type hint to method when wpcom picks up the new Requests lib (it seems it was skipped during their update to 6.2).
|
||||
*
|
||||
* @param \WpOrg\Requests\Response $response Response object.
|
||||
* @return string Final url of the response.
|
||||
*/
|
||||
protected function get_response_url( $response ) {
|
||||
$history = $response->history;
|
||||
if ( ! $history ) {
|
||||
return $response->url;
|
||||
}
|
||||
|
||||
$location = $history[0]->headers->getValues( 'location' );
|
||||
if ( ! $location ) {
|
||||
return $response->url;
|
||||
}
|
||||
|
||||
return $location[0];
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Resolve_Redirect' );
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* Proxy endpoint for Jetpack Search
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Search\REST_Controller;
|
||||
|
||||
/**
|
||||
* Jetpack Search: Makes authenticated requests to the site search API using blog tokens.
|
||||
* This endpoint will only be used when trying to search private Jetpack and WordPress.com sites.
|
||||
*
|
||||
* @since 9.0.0
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Search extends WP_REST_Controller {
|
||||
/**
|
||||
* Forward request to controller in Search package.
|
||||
*
|
||||
* @var REST_Controller
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'search';
|
||||
$this->controller = new REST_Controller( defined( 'IS_WPCOM' ) && IS_WPCOM );
|
||||
|
||||
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(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this->controller, 'get_search_results' ),
|
||||
'permission_callback' => 'is_user_logged_in',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Search' );
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles the sending of email previews via the WordPress.com REST API.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Send_Email_Preview
|
||||
* Handles the sending of email previews via the WordPress.com REST API
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Send_Email_Preview extends WP_REST_Controller {
|
||||
|
||||
use WPCOM_REST_API_Proxy_Request;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->base_api_path = 'wpcom';
|
||||
$this->version = 'v2';
|
||||
$this->namespace = $this->base_api_path . '/' . $this->version;
|
||||
$this->rest_base = '/send-email-preview';
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
$this->wpcom_is_site_specific_endpoint = true;
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the routes for blogging prompts.
|
||||
*
|
||||
* @see register_rest_route()
|
||||
*/
|
||||
public function register_routes() {
|
||||
$options = array(
|
||||
'show_in_index' => true,
|
||||
'methods' => 'POST',
|
||||
// if this is not a wpcom site, we need to proxy the request to wpcom
|
||||
'callback' => ( ( new Host() )->is_wpcom_simple() ) ? array(
|
||||
$this,
|
||||
'send_email_preview',
|
||||
) : array( $this, 'proxy_request_to_wpcom_as_user' ),
|
||||
'permission_callback' => array( $this, 'permissions_check' ),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'Unique identifier for the post.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base,
|
||||
$options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is connected and has access to edit the post
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has edit access, WP_Error object otherwise.
|
||||
*/
|
||||
public function permissions_check( $request ) {
|
||||
if ( ! ( new Host() )->is_wpcom_simple() ) {
|
||||
if ( ! ( new Manager() )->is_user_connected() ) {
|
||||
return new WP_Error(
|
||||
'rest_cannot_send_email_preview',
|
||||
__( 'Please connect your user account to WordPress.com', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$post = get_post( $request->get_param( 'id' ) );
|
||||
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
if ( $post && ! current_user_can( 'edit_post', $post->ID ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden_context',
|
||||
__( 'Please connect your user account to WordPress.com', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email preview of a post to the current user.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function send_email_preview( $request ) {
|
||||
$post_id = $request['id'];
|
||||
$post = get_post( $post_id );
|
||||
|
||||
// Return error if the post cannot be retrieved
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
// Check if the user's email is verified
|
||||
if ( Email_Verification::is_email_unverified() ) {
|
||||
return new WP_Error( 'unverified', __( 'Your email address must be verified.', 'jetpack' ), array( 'status' => rest_authorization_required_code() ) );
|
||||
}
|
||||
|
||||
$current_user = wp_get_current_user();
|
||||
$email = $current_user->user_email;
|
||||
|
||||
// Try to create a new subscriber with the user's email
|
||||
$subscriber = Blog_Subscriber::create( $email );
|
||||
if ( ! $subscriber ) {
|
||||
return new WP_Error( 'unverified', __( 'Could not create subscriber.', 'jetpack' ), array( 'status' => rest_authorization_required_code() ) );
|
||||
}
|
||||
|
||||
// Send the post to the subscriber
|
||||
require_once ABSPATH . 'wp-content/mu-plugins/email-subscriptions/subscription-mailer.php';
|
||||
$mailer = new Subscription_Mailer( $subscriber );
|
||||
$subscription = $subscriber->get_subscription( get_current_blog_id() );
|
||||
$mailer->send_post( $post, $subscription );
|
||||
|
||||
// Return a response
|
||||
return new WP_REST_Response( 'Email preview sent successfully.', 200 );
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Send_Email_Preview' );
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for resolving template.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the correct template for the site's page based on the template type
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Template_Loader extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'template-loader';
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Called automatically on `rest_api_init()`.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
sprintf(
|
||||
'/%s/(?P<template_type>\w+)',
|
||||
$this->rest_base
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_item' ),
|
||||
'permission_callback' => array( $this, 'permissions_check' ),
|
||||
'args' => array(
|
||||
'template_type' => array(
|
||||
'description' => __( 'The type of the template.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_template_type' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has permissions to make the request.
|
||||
*
|
||||
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
|
||||
*/
|
||||
public function permissions_check() {
|
||||
// Verify if the current user has edit_theme_options capability.
|
||||
// This capability is required to edit/view/delete templates.
|
||||
if ( ! current_user_can( 'edit_theme_options' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_cannot_manage_templates',
|
||||
__( 'Sorry, you are not allowed to access the templates on this site.', 'jetpack' ),
|
||||
array(
|
||||
'status' => rest_authorization_required_code(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the template type.
|
||||
*
|
||||
* @param string $template_type The template type.
|
||||
* @return boolean True if the template type is valid.
|
||||
*/
|
||||
public function validate_template_type( $template_type ) {
|
||||
$template_types = array_keys( get_default_block_template_types() );
|
||||
if ( ! in_array( $template_type, $template_types, true ) ) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
sprintf(
|
||||
/* translators: %s: The template type. */
|
||||
__( 'The template type %s are not allowed.', 'jetpack' ),
|
||||
$template_type
|
||||
),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the template by specified type.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item( $request ) {
|
||||
$template_type = $request['template_type'];
|
||||
|
||||
// A list of template candidates, in descending order of priority.
|
||||
$templates = apply_filters( "{$template_type}_template_hierarchy", array( "{$template_type}.php" ) );
|
||||
$template = locate_template( $templates );
|
||||
$block_template = resolve_block_template( $template_type, $templates, $template );
|
||||
|
||||
if ( empty( $block_template ) ) {
|
||||
return new WP_Error( 'not_found', 'Template not found', array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'type' => $block_template->type,
|
||||
'id' => $block_template->id,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Template_Loader' );
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Get post types and top posts.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Top Posts & Pages block endpoint.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Top_Posts extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
|
||||
if ( ! class_exists( 'Jetpack_Top_Posts_Helper' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-top-posts-helper.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
'wpcom/v2',
|
||||
'/post-types',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_post_types' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Number of posts and selected post types are not needed in the Editor.
|
||||
// This is to minimise requests when it can already be handled by the block.
|
||||
register_rest_route(
|
||||
'wpcom/v2',
|
||||
'/top-posts',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_top_posts' ),
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
'args' => array(
|
||||
'period' => array(
|
||||
'description' => __( 'Timeframe for stats.', 'jetpack' ),
|
||||
'type' => array( 'string', 'integer' ),
|
||||
'required' => true,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_numeric( $param ) || is_string( $param );
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site's post types.
|
||||
*
|
||||
* @return array Site's post types.
|
||||
*/
|
||||
public function get_post_types() {
|
||||
$post_types = array_values( get_post_types( array( 'public' => true ) ) );
|
||||
$post_types_array = array();
|
||||
|
||||
foreach ( $post_types as $type ) {
|
||||
$post_types_array[] = array(
|
||||
'label' => get_post_type_object( $type )->labels->name,
|
||||
'id' => $type,
|
||||
);
|
||||
}
|
||||
|
||||
return $post_types_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site's top content.
|
||||
*
|
||||
* @param \WP_REST_Request $request request object.
|
||||
*
|
||||
* @return array Data on top posts.
|
||||
*/
|
||||
public function get_top_posts( $request ) {
|
||||
$period = $request->get_param( 'period' );
|
||||
return Jetpack_Top_Posts_Helper::get_top_posts( $period );
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Top_Posts' );
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoint for editing Jetpack Transients.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
* @since 9.7.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Jetpack transients API.
|
||||
*
|
||||
* @since 9.7.0
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Transient extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'transients';
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Called automatically on `rest_api_init()`.
|
||||
*/
|
||||
public function register_routes() {
|
||||
// DELETE /sites/<blog-id>/transients/$name route.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<name>\w{1,172})',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => array( $this, 'delete_transient' ),
|
||||
'permission_callback' => array( $this, 'delete_transient_permissions_check' ),
|
||||
'args' => array(
|
||||
'name' => array(
|
||||
'description' => __( 'The name of the transient to delete.', 'jetpack' ),
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete transient callback.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return array
|
||||
*/
|
||||
public function delete_transient( \WP_REST_Request $request ) {
|
||||
return array(
|
||||
'success' => delete_transient( $request->get_param( 'name' ) ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has read access, the transient name starts with
|
||||
* "jetpack_connected_user_data_", and that the user is editing
|
||||
* their own transient.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function delete_transient_permissions_check( \WP_REST_Request $request ) {
|
||||
$transient_name = $request->get_param( 'name' );
|
||||
$current_user_id = get_current_user_id();
|
||||
|
||||
if ( current_user_can( 'read' ) &&
|
||||
"jetpack_connected_user_data_{$current_user_id}" === $transient_name ) {
|
||||
return true;
|
||||
} else {
|
||||
return new WP_Error(
|
||||
'authorization_required',
|
||||
__( 'Sorry, you are not allowed to delete this transient.', 'jetpack' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Transient' );
|
||||
+565
@@ -0,0 +1,565 @@
|
||||
<?php
|
||||
/**
|
||||
* Blogging prompts endpoint for wpcom/v3.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
|
||||
|
||||
/**
|
||||
* REST API endpoint wpcom/v3/sites/%s/blogging-prompts.
|
||||
*/
|
||||
class WPCOM_REST_API_V3_Endpoint_Blogging_Prompts extends WP_REST_Posts_Controller {
|
||||
|
||||
use WPCOM_REST_API_Proxy_Request;
|
||||
|
||||
const TEMPLATE_BLOG_ID = 205876834;
|
||||
|
||||
/**
|
||||
* Whether the endpoint is running on wpcom, or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $is_wpcom;
|
||||
|
||||
/**
|
||||
* Day of the year, from 1 to 366, and 0 representing no query.
|
||||
*
|
||||
* Used with yearless dates like `--12-20`, to get prompts by month and day, regardless of year.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $day_of_year_query = 0;
|
||||
|
||||
/**
|
||||
* A year used to force one prompt per day for a specific year.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $force_year = 0;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->post_type = 'post';
|
||||
$this->base_api_path = 'wpcom';
|
||||
$this->version = 'v3';
|
||||
$this->namespace = $this->base_api_path . '/' . $this->version;
|
||||
$this->rest_base = 'blogging-prompts';
|
||||
$this->wpcom_is_wpcom_only_endpoint = true;
|
||||
$this->wpcom_is_site_specific_endpoint = true;
|
||||
$this->is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the routes for blogging prompts.
|
||||
*
|
||||
* @see register_rest_route()
|
||||
*/
|
||||
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, 'permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
'schema' => array( $this, 'get_item_schema' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||
array(
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'Unique identifier for the prompt.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_item' ),
|
||||
'permission_callback' => array( $this, 'permissions_check' ),
|
||||
),
|
||||
'schema' => array( $this, 'get_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a collection of blogging prompts.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
if ( ! $this->is_wpcom ) {
|
||||
return $this->proxy_request_to_wpcom( $request, '', 'user', true );
|
||||
}
|
||||
|
||||
if ( $request->get_param( 'force_year' ) ) {
|
||||
$this->force_year = $request->get_param( 'force_year' );
|
||||
}
|
||||
|
||||
switch_to_blog( self::TEMPLATE_BLOG_ID );
|
||||
add_action( 'pre_get_posts', array( $this, 'modify_query' ) );
|
||||
add_filter( 'posts_clauses', array( $this, 'filter_sql' ) );
|
||||
$items = parent::get_items( $request );
|
||||
remove_filter( 'posts_clauses', array( $this, 'filter_sql' ) );
|
||||
remove_action( 'pre_get_posts', array( $this, 'modify_query' ) );
|
||||
restore_current_blog();
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single blogging prompt.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item( $request ) {
|
||||
if ( ! $this->is_wpcom ) {
|
||||
return $this->proxy_request_to_wpcom( $request, $request->get_param( 'id' ), 'user', true );
|
||||
}
|
||||
|
||||
if ( $request->get_param( 'force_year' ) ) {
|
||||
$this->force_year = $request->get_param( 'force_year' );
|
||||
}
|
||||
|
||||
switch_to_blog( self::TEMPLATE_BLOG_ID );
|
||||
$item = parent::get_item( $request );
|
||||
restore_current_blog();
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the posts query using the {@see 'pre_get_posts'} hook.
|
||||
*
|
||||
* @param WP_Query $wp_query The WP_Query instance (passed by reference).
|
||||
*/
|
||||
public function modify_query( &$wp_query ) {
|
||||
if ( is_array( $wp_query->query_vars['date_query'] ) ) {
|
||||
$wp_query->query_vars['date_query'] = array_map(
|
||||
array( $this, 'map_date_query' ),
|
||||
$wp_query->query_vars['date_query']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify date_query items when querying prompts.
|
||||
*
|
||||
* @link https://developer.wordpress.org/reference/classes/WP_Query/#date-parameters
|
||||
*
|
||||
* @param array|string|null $date_query Date query.
|
||||
* @return array|string|null Modified date query.
|
||||
*/
|
||||
public function map_date_query( $date_query ) {
|
||||
if ( isset( $date_query['after'] ) ) {
|
||||
// `after` date queries should include posts on the specified date, so force `inclusive` queries.
|
||||
$date_query['inclusive'] = true;
|
||||
|
||||
// If using a "year-less" date, e.g. `--03-16`, override the date_query, and prepare to modify sql manually.
|
||||
// `after` should be a date string when making API requests, rather than an array.
|
||||
if ( is_string( $date_query['after'] ) && str_starts_with( $date_query['after'], '-' ) ) {
|
||||
$date = date_create_from_format( '--m-d', $date_query['after'] );
|
||||
|
||||
if ( false !== $date ) {
|
||||
// PHP day of the year starts with 0; normalize to match SQL DAYOFTHEYEAR which starts with 1.
|
||||
$this->day_of_year_query = $date->format( 'z' ) + 1;
|
||||
|
||||
// Unset the date query, since we'll by modifying the SQL manually.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $date_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify post sql for custom date ordering using the {@see 'posts_clauses'} hook.
|
||||
*
|
||||
* @param array $clauses SQL clauses for the current query.
|
||||
* @return array Modified SQL clauses.
|
||||
*/
|
||||
public function filter_sql( $clauses ) {
|
||||
global $wpdb;
|
||||
if ( $this->day_of_year_query > 0 ) {
|
||||
$day = $this->day_of_year_query;
|
||||
$year = $this->force_year ? $this->force_year : wp_date( 'Y' );
|
||||
|
||||
// Grab the current sort order, `ASC` or `DESC`, so we can reuse it.
|
||||
$exploded = explode( ' ', $clauses['orderby'] );
|
||||
$order = end( $exploded );
|
||||
|
||||
// Calculate the day of year for each prompt, from 1 to 366, but use the current year so that prompts published
|
||||
// during leap years have the correct day for non-leap years.
|
||||
$fields = $clauses['fields'] . $wpdb->prepare( ", DAYOFYEAR(CONCAT(%d, DATE_FORMAT({$wpdb->posts}.post_date, '-%%m-%%d'))) AS day_of_year", $year );
|
||||
|
||||
// When it's not a leap year, exclude posts used for Feb 29th. DAYOFYEAR will return null for Feb 29th on non-leap years.
|
||||
$where = $clauses['where'] . $wpdb->prepare( " AND DAYOFYEAR(CONCAT(%d, DATE_FORMAT({$wpdb->posts}.post_date, '-%%m-%%d'))) IS NOT NULL", $year );
|
||||
|
||||
// Order posts regardless of year: get a list of posts for each day,
|
||||
// starting with the query date through the end of the year, then from the start of the year through the day before.
|
||||
$orderby = $wpdb->prepare(
|
||||
'CASE ' .
|
||||
'WHEN day_of_year < %d ' .
|
||||
// Push posts from the beginning of the year until the day before to the end.
|
||||
'THEN day_of_year + 366 ' .
|
||||
// Otherwise order posts from the query date through the end of the year.
|
||||
'ELSE day_of_year ' .
|
||||
'END' .
|
||||
// Sort posts for the same day by year, in asc or desc order.
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- order string cannot be escaped.
|
||||
", YEAR({$wpdb->posts}.post_date) " . ( 'DESC' === $order ? 'DESC' : 'ASC' ),
|
||||
$day
|
||||
);
|
||||
|
||||
if ( $this->force_year ) {
|
||||
// If we're forcing the year, group by day of year, so that we only get one prompt per day.
|
||||
$clauses['groupby'] = 'day_of_year';
|
||||
|
||||
// Ensure we get either to newest or oldest prompt for each day of the year, depending on the sort order.
|
||||
// GROUP BY runs and collects the prompts for each day of the year before ORDER BY is run, so we first need to use MAX/MIN on post_date
|
||||
// to find the most recent/oldest prompt for each day and join the results to the main query.
|
||||
$clauses['join'] = $wpdb->prepare(
|
||||
'INNER JOIN (' .
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL function cannot be escaped.
|
||||
'SELECT ' . ( 'DESC' === $order ? 'MAX' : 'MIN' ) . "({$wpdb->posts}.post_date) AS post_date, DAYOFYEAR(CONCAT(%d, DATE_FORMAT(post_date, '-%%m-%%d'))) AS day_of_year " .
|
||||
"FROM {$wpdb->posts} " .
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- reuses unmodified existing clause.
|
||||
"WHERE 1=1 {$clauses['where']} " .
|
||||
'GROUP BY day_of_year' .
|
||||
") AS newest_prompts ON {$wpdb->posts}.post_date = newest_prompts.post_date",
|
||||
$year
|
||||
);
|
||||
}
|
||||
|
||||
$clauses['fields'] = $fields;
|
||||
$clauses['where'] = $where;
|
||||
$clauses['orderby'] = $orderby;
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a single blogging prompt output for response.
|
||||
*
|
||||
* @param WP_Post $prompt Post object.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response Response object.
|
||||
*/
|
||||
public function prepare_item_for_response( $prompt, $request ) {
|
||||
require_once WP_CONTENT_DIR . '/lib/blogging-prompts/answers.php';
|
||||
require_once WP_CONTENT_DIR . '/lib/blogging-prompts/utils.php';
|
||||
|
||||
$fields = $this->get_fields_for_response( $request );
|
||||
|
||||
// Base fields for every post.
|
||||
$data = array();
|
||||
|
||||
if ( rest_is_field_included( 'id', $fields ) ) {
|
||||
$data['id'] = $prompt->ID;
|
||||
}
|
||||
|
||||
if ( rest_is_field_included( 'date', $fields ) ) {
|
||||
$data['date'] = $this->prepare_date_response( $prompt->post_date_gmt );
|
||||
}
|
||||
|
||||
if ( rest_is_field_included( 'label', $fields ) ) {
|
||||
if ( $this->is_in_bloganuary( $prompt->post_date_gmt ) ) {
|
||||
$data['label'] = __( 'Bloganuary writing prompt', 'jetpack' );
|
||||
} else {
|
||||
$data['label'] = __( 'Daily writing prompt', 'jetpack' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( rest_is_field_included( 'text', $fields ) ) {
|
||||
$text = \BloggingPrompts\prompt_without_blocks( $prompt->post_content );
|
||||
// Allow translating a variable, since this text is imported from bloggingpromptstemplates.wordpress.com for translation.
|
||||
$translated_text = __( $text, 'jetpack' ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
|
||||
$data['text'] = wp_kses( $translated_text, wp_kses_allowed_html( 'post' ) );
|
||||
}
|
||||
|
||||
if ( rest_is_field_included( 'attribution', $fields ) ) {
|
||||
$data['attribution'] = esc_html( get_post_meta( $prompt->ID, 'blogging_prompts_attribution', true ) );
|
||||
}
|
||||
|
||||
// Will always be false when requesting as blog.
|
||||
if ( rest_is_field_included( 'answered', $fields ) ) {
|
||||
$data['answered'] = (bool) \A8C\BloggingPrompts\Answers::is_answered_by_user( $prompt->ID, get_current_user_id() );
|
||||
}
|
||||
|
||||
if ( rest_is_field_included( 'answered_users_count', $fields ) ) {
|
||||
$data['answered_users_count'] = (int) \A8C\BloggingPrompts\Answers::get_count( $prompt->ID );
|
||||
}
|
||||
|
||||
if ( rest_is_field_included( 'answered_users_sample', $fields ) ) {
|
||||
$data['answered_users_sample'] = $this->build_answering_users_sample( $prompt->ID );
|
||||
}
|
||||
|
||||
if ( rest_is_field_included( 'answered_link', $fields ) ) {
|
||||
if ( $this->is_in_bloganuary( $prompt->post_date_gmt ) ) {
|
||||
$bloganuary_id = $this->get_bloganuary_id( $prompt->post_date_gmt );
|
||||
$data['answered_link'] = esc_url( "https://wordpress.com/tag/{$bloganuary_id}" );
|
||||
} else {
|
||||
$data['answered_link'] = esc_url( "https://wordpress.com/tag/dailyprompt-{$prompt->ID}" );
|
||||
}
|
||||
}
|
||||
|
||||
if ( rest_is_field_included( 'answered_link_text', $fields ) ) {
|
||||
$data['answered_link_text'] = __( 'View all responses', 'jetpack' );
|
||||
}
|
||||
|
||||
if ( $this->is_in_bloganuary( $prompt->post_date_gmt ) && rest_is_field_included( 'bloganuary_id', $fields ) ) {
|
||||
$data['bloganuary_id'] = $this->get_bloganuary_id( $prompt->post_date_gmt );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the post is in "Bloganuary"
|
||||
*
|
||||
* @param string $post_date_gmt Unused - Post date in GMT.
|
||||
* @return bool Always returns false as Bloganuary is disabled.
|
||||
*/
|
||||
protected function is_in_bloganuary( $post_date_gmt ) { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
/*
|
||||
Disable for January 2025 and beyond (see https://wp.me/p5uIfZ-gxX).
|
||||
Previously, this method would check if the post was published in January:
|
||||
- Extract month from post_date_gmt -- $post_month = gmdate( 'm', strtotime( $post_date_gmt ) );
|
||||
- Return true if month was '01' -- return $post_month === '01';
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bloganuary id of the form `bloganuary-yyyy-dd`
|
||||
*
|
||||
* @param string $post_date_gmt Post date in GMT.
|
||||
* @return string Bloganuary id.
|
||||
*/
|
||||
protected function get_bloganuary_id( $post_date_gmt ) {
|
||||
$post_year_day = gmdate( 'Y-d', strtotime( $post_date_gmt ) );
|
||||
if ( $this->force_year ) {
|
||||
$post_year_day = $this->force_year . '-' . gmdate( 'd', strtotime( $post_date_gmt ) );
|
||||
}
|
||||
return 'bloganuary-' . $post_year_day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for a blogging prompt, omiting the time.
|
||||
*
|
||||
* @param string $date_gmt Publish datetime of the prompt in GMT, i.e. 0000-00-00 00:00:00.
|
||||
* @param string $date Publish datetime of the prompt, i.e. 0000-00-00 00:00:00.
|
||||
* @return string Publish date of the prompt in YYYY-MM-DD format.
|
||||
*/
|
||||
public function prepare_date_response( $date_gmt, $date = null ) {
|
||||
$post_date = $date ? $date : $date_gmt;
|
||||
$date_obj = date_create( $post_date );
|
||||
|
||||
if ( $this->force_year ) {
|
||||
$date_obj->setDate( $this->force_year, $date_obj->format( 'm' ), $date_obj->format( 'd' ) );
|
||||
|
||||
// If ascending by day of year, go to the next year when we pass the last day of the year.
|
||||
if ( $date_obj->format( 'm-d' ) === '12-31' ) {
|
||||
$this->force_year += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return false !== $date_obj ? $date_obj->format( 'Y-m-d' ) : substr( $post_date, 0, 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the query params for blogging prompt collections.
|
||||
*
|
||||
* @return array Query parameters for the collection.
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$parent_args = parent::get_collection_params();
|
||||
|
||||
$args = array(
|
||||
// Modify date args so that will except a YYYY-MM-DD without a time.
|
||||
'after' => array(
|
||||
'description' => __( 'Show prompts following a given date.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'validate_callback' => function ( $param ) {
|
||||
// Allow month and day without year, e.g. `--02-28`
|
||||
if ( str_starts_with( $param, '-' ) ) {
|
||||
return false !== date_create_from_format( '--m-d', $param );
|
||||
}
|
||||
|
||||
return false !== date_create( $param );
|
||||
},
|
||||
),
|
||||
'before' => array(
|
||||
'description' => __( 'Show prompts before a given date.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'validate_callback' => function ( $param ) {
|
||||
return false !== date_create( $param );
|
||||
},
|
||||
),
|
||||
'force_year' => array(
|
||||
'description' => __( 'Force the returned prompts to be for a specific year. Returns only one prompt for each day.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
'validate_callback' => function ( $param ) {
|
||||
return is_numeric( $param ) && intval( $param ) > 0 && intval( $param ) < 9999;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
$args['exclude'] = $parent_args['exclude'];
|
||||
$args['include'] = $parent_args['include'];
|
||||
$args['page'] = $parent_args['page'];
|
||||
$args['per_page'] = $parent_args['per_page'];
|
||||
$args['order'] = $parent_args['order'];
|
||||
$args['order']['default'] = 'asc';
|
||||
$args['orderby'] = $parent_args['orderby'];
|
||||
$args['search'] = $parent_args['search'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the blogging prompt's schema, conforming to JSON Schema.
|
||||
*
|
||||
* @return array Item schema data.
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
return array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'blogging-prompt',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'Unique identifier for the post.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'date' => array(
|
||||
'description' => __( "The date the post was published, in the site's timezone.", 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'label' => array(
|
||||
'description' => __( 'Label for the prompt.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'text' => array(
|
||||
'description' => __( 'The text of the prompt. May include html tags like <em>.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'attribution' => array(
|
||||
'description' => __( 'Source of the prompt, if known.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'answered' => array(
|
||||
'description' => __( 'Whether the user has answered the prompt.', 'jetpack' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'answered_users_count' => array(
|
||||
'description' => __( 'Number of users who have answered the prompt.', 'jetpack' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'answered_users_sample' => array(
|
||||
'description' => __( 'Sample of users who have answered the prompt.', 'jetpack' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'avatar' => array(
|
||||
'description' => __( "Gravatar URL for the user's avatar image.", 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'answered_link' => array(
|
||||
'description' => __( 'Link to answers for the prompt.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'answered_link_text' => array(
|
||||
'description' => __( 'Text for the link to answers for the prompt.', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'bloganuary_id' => array(
|
||||
'description' => __( 'Id used by the bloganuary promotion', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given request has access to read blogging prompts for a site.
|
||||
*
|
||||
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
|
||||
*/
|
||||
public function permissions_check() {
|
||||
if ( current_user_can( 'edit_posts' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow "as blog" requests to wpcom so users without accounts can insert the Writing prompt block in the editor.
|
||||
if ( $this->is_wpcom && is_jetpack_site( get_current_blog_id() ) ) {
|
||||
if ( ! class_exists( 'WPCOM_REST_API_V2_Endpoint_Jetpack_Auth' ) ) {
|
||||
require_once dirname( __DIR__ ) . '/rest-api-plugins/endpoints/jetpack-auth.php';
|
||||
}
|
||||
|
||||
$jp_auth_endpoint = new WPCOM_REST_API_V2_Endpoint_Jetpack_Auth();
|
||||
if ( true === $jp_auth_endpoint->is_jetpack_authorized_for_site() ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'rest_cannot_read_prompts',
|
||||
__( 'Sorry, you are not allowed to access blogging prompts on this site.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sample of users who have answered a blogging prompt.
|
||||
*
|
||||
* @param int $prompt_id Prompt ID.
|
||||
* @return array List of users, including a gravatar url for each user.
|
||||
*/
|
||||
protected function build_answering_users_sample( $prompt_id ) {
|
||||
$results = \A8C\BloggingPrompts\Answers::get_sample_users_by( $prompt_id );
|
||||
|
||||
if ( ! $results ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$users = array();
|
||||
|
||||
foreach ( $results as $user ) {
|
||||
$url = wpcom_get_avatar_url( $user->user_id, 96, 'identicon', false );
|
||||
if ( has_gravatar( $user->user_id ) && ! empty( $url[0] ) && ! is_wp_error( $url[0] ) ) {
|
||||
$users[] = array(
|
||||
'avatar' => (string) esc_url_raw( htmlspecialchars_decode( $url[0], ENT_COMPAT ) ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array_slice( $users, 0, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V3_Endpoint_Blogging_Prompts' );
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Interact with the list of available block editor extensions (blocks, plugins)
|
||||
* made available by the Jetpack plugin.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gutenberg: List Available Gutenberg Extensions (Blocks and Plugins)
|
||||
*
|
||||
* [
|
||||
* { # Availability 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 {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public 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' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the endpoint route.
|
||||
*/
|
||||
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,45 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Example of a WP.com endpoint.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example endpoint.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Hello {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint route.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
'wpcom/v2',
|
||||
'/hello',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_data' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data in response to the endpoint request.
|
||||
*
|
||||
* @param WP_REST_Request $request API request.
|
||||
*/
|
||||
public function get_data( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return array( 'hello' => 'world' );
|
||||
}
|
||||
}
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Hello' );
|
||||
@@ -0,0 +1,546 @@
|
||||
<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Memberships: API to communicate with "product" database.
|
||||
*
|
||||
* @package Jetpack
|
||||
* @since 7.3.0
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
|
||||
|
||||
/**
|
||||
* Class WPCOM_REST_API_V2_Endpoint_Memberships
|
||||
* This introduces V2 endpoints.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Memberships extends WP_REST_Controller {
|
||||
|
||||
use WPCOM_REST_API_Proxy_Request;
|
||||
|
||||
/**
|
||||
* WPCOM_REST_API_V2_Endpoint_Memberships constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->base_api_path = 'wpcom';
|
||||
$this->version = 'v2';
|
||||
$this->namespace = $this->base_api_path . '/' . $this->version;
|
||||
$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' ),
|
||||
'args' => array(
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return in_array( $param, array( 'donation', 'all' ), true );
|
||||
},
|
||||
),
|
||||
'source' => array(
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
'validate_callback' => function ( $param ) {
|
||||
return in_array(
|
||||
$param,
|
||||
array(
|
||||
'calypso',
|
||||
'earn',
|
||||
'earn-newsletter',
|
||||
'gutenberg',
|
||||
'gutenberg-wpcom',
|
||||
'launchpad',
|
||||
'import-paid-subscribers',
|
||||
),
|
||||
true
|
||||
);
|
||||
},
|
||||
),
|
||||
'is_editable' => array(
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
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' => 'number',
|
||||
'required' => true,
|
||||
),
|
||||
'currency' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'interval' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'is_editable' => array(
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
),
|
||||
'buyer_can_change_amount' => array(
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'tier' => array(
|
||||
'type' => 'integer',
|
||||
'required' => false,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/products/?',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_products' ),
|
||||
'permission_callback' => array( $this, 'can_modify_products_permission_check' ),
|
||||
'args' => array(
|
||||
'currency' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'is_editable' => array(
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'list_products' ),
|
||||
'permission_callback' => array( $this, 'get_status_permission_check' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base . '/product/(?P<product_id>[0-9]+)/?',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_product' ),
|
||||
'permission_callback' => array( $this, 'can_modify_products_permission_check' ),
|
||||
'args' => array(
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'price' => array(
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
),
|
||||
'currency' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'interval' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'is_editable' => array(
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
),
|
||||
'buyer_can_change_amount' => array(
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'tier' => array(
|
||||
'type' => 'integer',
|
||||
'required' => false,
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => array( $this, 'delete_product' ),
|
||||
'permission_callback' => array( $this, 'can_modify_products_permission_check' ),
|
||||
'args' => array(
|
||||
'cancel_subscriptions' => array(
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the user has proper permissions for getting status and listing products
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function get_status_permission_check() {
|
||||
return current_user_can( 'edit_posts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the user has proper permissions to modify products
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function can_modify_products_permission_check() {
|
||||
return current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically generate products according to type.
|
||||
*
|
||||
* @param object $request - request passed from WP.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_products( $request ) {
|
||||
$is_editable = isset( $request['is_editable'] ) ? (bool) $request['is_editable'] : null;
|
||||
|
||||
if ( $this->is_wpcom() ) {
|
||||
require_lib( 'memberships' );
|
||||
Memberships_Store_Sandbox::get_instance()->init( true );
|
||||
|
||||
$result = Memberships_Product::generate_default_products( get_current_blog_id(), $request['type'], $request['currency'], $is_editable );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$status = 'invalid_param' === $result->get_error_code() ? 400 : 500;
|
||||
return new WP_Error( $result->get_error_code(), $result->get_error_message(), array( 'status' => $status ) );
|
||||
}
|
||||
return $result;
|
||||
} else {
|
||||
return $this->proxy_request_to_wpcom_as_user( $request, 'products' );
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* List already-created products.
|
||||
*
|
||||
* @param \WP_REST_Request $request - request passed from WP.
|
||||
*
|
||||
* @return WP_Error|array ['products']
|
||||
*/
|
||||
public function list_products( WP_REST_Request $request ) {
|
||||
$is_editable = isset( $request['is_editable'] ) ? (bool) $request['is_editable'] : null;
|
||||
$type = isset( $request['type'] ) ? $request['type'] : null;
|
||||
|
||||
if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
|
||||
require_lib( 'memberships' );
|
||||
require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
|
||||
try {
|
||||
return array( 'products' => $this->list_products_from_wpcom( $request, $type, $is_editable ) );
|
||||
} catch ( \Exception $e ) {
|
||||
return array( 'error' => $e->getMessage() );
|
||||
}
|
||||
} else {
|
||||
|
||||
return $this->proxy_request_to_wpcom_as_user( $request, 'products' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do create a product based on data, or pass request to wpcom.
|
||||
*
|
||||
* @param WP_REST_Request $request - request passed from WP.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_product( WP_REST_Request $request ) {
|
||||
$payload = $this->get_payload_for_product( $request );
|
||||
|
||||
if ( $this->is_wpcom() ) {
|
||||
require_lib( 'memberships' );
|
||||
try {
|
||||
return $this->create_product_from_wpcom( $payload );
|
||||
} catch ( \Exception $e ) {
|
||||
return array( 'error' => $e->getMessage() );
|
||||
}
|
||||
} else {
|
||||
return $this->proxy_request_to_wpcom_as_user( $request, 'product' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing memberships product
|
||||
*
|
||||
* @param \WP_REST_Request $request The request passed from WP.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function update_product( \WP_REST_Request $request ) {
|
||||
$product_id = $request->get_param( 'product_id' );
|
||||
$payload = $this->get_payload_for_product( $request );
|
||||
|
||||
if ( $this->is_wpcom() ) {
|
||||
require_lib( 'memberships' );
|
||||
try {
|
||||
return array( 'product' => $this->update_product_from_wpcom( $product_id, $payload ) );
|
||||
} catch ( \Exception $e ) {
|
||||
return array( 'error' => $e->getMessage() );
|
||||
}
|
||||
} else {
|
||||
return $this->proxy_request_to_wpcom_as_user( $request, "product/$product_id" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing memberships product
|
||||
*
|
||||
* @param \WP_REST_Request $request The request passed from WP.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function delete_product( \WP_REST_Request $request ) {
|
||||
$product_id = $request->get_param( 'product_id' );
|
||||
$cancel_subscriptions = $request->get_param( 'cancel_subscriptions' );
|
||||
if ( $this->is_wpcom() ) {
|
||||
require_lib( 'memberships' );
|
||||
try {
|
||||
$this->delete_product_from_wpcom( $product_id, $cancel_subscriptions );
|
||||
return array( 'deleted' => true );
|
||||
} catch ( \Exception $e ) {
|
||||
return array( 'error' => $e->getMessage() );
|
||||
}
|
||||
} else {
|
||||
return $this->proxy_request_to_wpcom_as_user( $request, "product/$product_id" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a status of connection for the site. If this is Jetpack, pass the request to wpcom.
|
||||
*
|
||||
* @param \WP_REST_Request $request - request passed from WP.
|
||||
*
|
||||
* @return WP_Error|array ['products','connected_account_id','connect_url']
|
||||
*/
|
||||
public function get_status( \WP_REST_Request $request ) {
|
||||
$product_type = $request['type'];
|
||||
|
||||
if ( ! empty( $request['source'] ) ) {
|
||||
$source = sanitize_text_field( wp_unslash( $request['source'] ) );
|
||||
} else {
|
||||
$source = 'gutenberg';
|
||||
}
|
||||
|
||||
$is_editable = ! isset( $request['is_editable'] ) ? null : (bool) $request['is_editable'];
|
||||
|
||||
if ( $this->is_wpcom() ) {
|
||||
require_lib( 'memberships' );
|
||||
Memberships_Store_Sandbox::get_instance()->init( true );
|
||||
$blog_id = get_current_blog_id();
|
||||
$membership_settings = get_memberships_settings_for_site( $blog_id, $product_type, $is_editable, $source );
|
||||
|
||||
if ( is_wp_error( $membership_settings ) ) {
|
||||
// Get error messages from the $membership_settings.
|
||||
$error_codes = $membership_settings->get_error_codes();
|
||||
$error_messages = array();
|
||||
|
||||
foreach ( $error_codes as $code ) {
|
||||
$messages = $membership_settings->get_error_messages( $code );
|
||||
foreach ( $messages as $message ) {
|
||||
// Sanitize error message
|
||||
$error_messages[] = esc_html( $message );
|
||||
}
|
||||
}
|
||||
|
||||
$error_messages_string = implode( ' ', $error_messages );
|
||||
// translators: %s is a list of error messages.
|
||||
$base_message = __( 'Could not get the membership settings due to the following error(s): %s', 'jetpack' );
|
||||
$full_message = sprintf( $base_message, $error_messages_string );
|
||||
|
||||
return new WP_Error( 'membership_settings_error', $full_message, array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return (array) $membership_settings;
|
||||
} else {
|
||||
return $this->proxy_request_to_wpcom_as_user( $request, 'status' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function throws an exception if it is run outside of wpcom.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception If the function is run outside of WPCOM.
|
||||
*/
|
||||
private function prevent_running_outside_of_wpcom() {
|
||||
if ( ! $this->is_wpcom() || ! class_exists( 'Memberships_Product' ) ) {
|
||||
throw new \Exception( 'This function is intended to be run from WPCOM' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List products via the WPCOM-specific Memberships_Product class.
|
||||
*
|
||||
* @param WP_REST_Request $request The request for this endpoint.
|
||||
* @param ?string $type The type of the products to list.
|
||||
* @param ?bool $is_editable If we are looking for editable or non-editable products.
|
||||
* @throws \Exception If blog is not known or if there is an error getting products.
|
||||
* @return array List of products.
|
||||
*/
|
||||
private function list_products_from_wpcom( WP_REST_Request $request, $type, $is_editable ) {
|
||||
$this->prevent_running_outside_of_wpcom();
|
||||
Memberships_Store_Sandbox::get_instance()->init( true );
|
||||
$blog_id = $request->get_param( 'blog_id' );
|
||||
if ( is_wp_error( $blog_id ) ) {
|
||||
throw new \Exception( 'Unknown blog' );
|
||||
}
|
||||
$list = Memberships_Product::get_product_list( get_current_blog_id(), $type, $is_editable );
|
||||
if ( is_wp_error( $list ) ) {
|
||||
throw new \Exception( $list->get_error_message() );
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a product by product id via the WPCOM-specific Memberships_Product class.
|
||||
*
|
||||
* @param string|int $product_id The ID of the product to be found.
|
||||
* @throws \Exception If there is an error getting the product or if the product was not found.
|
||||
* @return object The found product.
|
||||
*/
|
||||
private function find_product_from_wpcom( $product_id ) {
|
||||
$this->prevent_running_outside_of_wpcom();
|
||||
Memberships_Store_Sandbox::get_instance()->init( true );
|
||||
$product = Memberships_Product::get_from_post( get_current_blog_id(), $product_id );
|
||||
if ( is_wp_error( $product ) ) {
|
||||
throw new \Exception( $product->get_error_message() );
|
||||
}
|
||||
if ( ! $product || ! $product instanceof Memberships_Product ) {
|
||||
throw new \Exception( __( 'Product not found.', 'jetpack' ) );
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a product via the WPCOM-specific Memberships_Product class.
|
||||
*
|
||||
* @param array $payload The request payload which contains details about the product.
|
||||
* @throws \Exception When the product failed to be created.
|
||||
* @return array The newly created product.
|
||||
*/
|
||||
private function create_product_from_wpcom( $payload ) {
|
||||
$this->prevent_running_outside_of_wpcom();
|
||||
Memberships_Store_Sandbox::get_instance()->init( true );
|
||||
$product = Memberships_Product::create( get_current_blog_id(), $payload );
|
||||
if ( is_wp_error( $product ) ) {
|
||||
throw new \Exception( __( 'Creating product has failed.', 'jetpack' ) );
|
||||
}
|
||||
return $product->to_array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a product via the WPCOM-specific Memberships_Product class.
|
||||
*
|
||||
* @param string|int $product_id The ID of the product being updated.
|
||||
* @param array $payload The request payload which contains details about the product.
|
||||
* @throws \Exception When there is a problem updating the product.
|
||||
* @return object The newly updated product.
|
||||
*/
|
||||
private function update_product_from_wpcom( $product_id, $payload ) {
|
||||
Memberships_Store_Sandbox::get_instance()->init( true );
|
||||
$product = $this->find_product_from_wpcom( $product_id ); // prevents running outside of wpcom
|
||||
$updated_product = $product->update( $payload );
|
||||
if ( is_wp_error( $updated_product ) ) {
|
||||
throw new \Exception( $updated_product->get_error_message() );
|
||||
}
|
||||
return $updated_product->to_array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a product via the WPCOM-specific Memberships_Product class.
|
||||
*
|
||||
* @param string|int $product_id The ID of the product being deleted.
|
||||
* @param bool $cancel_subscriptions Whether to cancel subscriptions to the product as well.
|
||||
* @throws \Exception When there is a problem deleting the product.
|
||||
* @return void
|
||||
*/
|
||||
private function delete_product_from_wpcom( $product_id, $cancel_subscriptions = false ) {
|
||||
Memberships_Store_Sandbox::get_instance()->init( true );
|
||||
$product = $this->find_product_from_wpcom( $product_id ); // prevents running outside of wpcom
|
||||
$result = $product->delete( $cancel_subscriptions ? Memberships_Product::CANCEL_SUBSCRIPTIONS : Memberships_Product::KEEP_SUBSCRIPTIONS );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
throw new \Exception( $result->get_error_message() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a payload for creating or updating products by parsing the request.
|
||||
*
|
||||
* @param WP_REST_Request $request The request for this endpoint, containing the details needed to build the payload.
|
||||
* @return array The built payload.
|
||||
*/
|
||||
private function get_payload_for_product( WP_REST_Request $request ) {
|
||||
$is_editable = isset( $request['is_editable'] ) ? (bool) $request['is_editable'] : null;
|
||||
$type = isset( $request['type'] ) ? $request['type'] : null;
|
||||
$tier = isset( $request['tier'] ) ? $request['tier'] : null;
|
||||
$buyer_can_change_amount = isset( $request['buyer_can_change_amount'] ) && (bool) $request['buyer_can_change_amount'];
|
||||
|
||||
$payload = array(
|
||||
'title' => $request['title'],
|
||||
'price' => $request['price'],
|
||||
'currency' => $request['currency'],
|
||||
'buyer_can_change_amount' => $buyer_can_change_amount,
|
||||
'interval' => $request['interval'],
|
||||
'type' => $type,
|
||||
'welcome_email_content' => $request['welcome_email_content'],
|
||||
'subscribe_as_site_subscriber' => $request['subscribe_as_site_subscriber'],
|
||||
'multiple_per_user' => $request['multiple_per_user'],
|
||||
);
|
||||
|
||||
if ( null !== $tier ) {
|
||||
$payload['tier'] = $tier;
|
||||
}
|
||||
|
||||
// If we pass directly the value "null", it will break the argument validation.
|
||||
if ( null !== $is_editable ) {
|
||||
$payload['is_editable'] = $is_editable;
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if run from WPCOM.
|
||||
*
|
||||
* @return boolean true if run from wpcom, otherwise false.
|
||||
*/
|
||||
private function is_wpcom() {
|
||||
return defined( 'IS_WPCOM' ) && IS_WPCOM;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() ) {
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Memberships' );
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Fetch information about Publicize connections on a site, including tests and connection status.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/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 {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
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 Jetpack Social connection test pass?', 'jetpack' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'error_code' => array(
|
||||
'description' => __( 'Jetpack Social connection error code', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'test_message' => array(
|
||||
'description' => __( 'Jetpack Social connection success or error message', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'can_refresh' => array(
|
||||
'description' => __( 'Can the current user refresh the Jetpack Social connection?', 'jetpack' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'refresh_text' => array(
|
||||
'description' => __( 'Message instructing the user to refresh their Connection to the Jetpack Social service', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'refresh_url' => array(
|
||||
'description' => __( 'URL for refreshing the Connection to the Jetpack Social service', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of Publicize Connections.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
*
|
||||
* @see Publicize::get_publicize_conns_test_results()
|
||||
* @return WP_REST_Response suitable for 1-page collection
|
||||
*/
|
||||
public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
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['connectionID'] ] = $test_result;
|
||||
}
|
||||
|
||||
$mapping = array(
|
||||
'test_success' => 'connectionTestPassed',
|
||||
'test_message' => 'connectionTestMessage',
|
||||
'error_code' => 'connectionTestErrorCode',
|
||||
'can_refresh' => 'userCanRefresh',
|
||||
'refresh_text' => 'refreshText',
|
||||
'refresh_url' => 'refreshURL',
|
||||
'connection_id' => 'connectionID',
|
||||
);
|
||||
|
||||
foreach ( $items as &$item ) {
|
||||
$test_result = $test_results_by_unique_id[ $item['connection_id'] ];
|
||||
|
||||
foreach ( $mapping as $field => $test_result_field ) {
|
||||
$item[ $field ] = $test_result[ $test_result_field ];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $item['id'] )
|
||||
&& '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' );
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Fetch information about Publicize connections on a site.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Publicize: List Connections
|
||||
*
|
||||
* [
|
||||
* { # Connection 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;
|
||||
|
||||
/**
|
||||
* Called automatically on `rest_api_init()`.
|
||||
*/
|
||||
public function register_routes() {
|
||||
// Endpoint moved to publicize package.
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Jetpack Social connection', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'service_name' => array(
|
||||
'description' => __( 'Alphanumeric identifier for the Jetpack Social service', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'display_name' => array(
|
||||
'description' => __( 'Display name of the connected account', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'username' => array(
|
||||
'description' => __( 'Username of the connected account', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'profile_display_name' => array(
|
||||
'description' => __( 'The name to display in the profile of the connected account', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'profile_picture' => array(
|
||||
'description' => __( 'Profile picture of the connected account', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'global' => array(
|
||||
'description' => __( 'Is this connection available to all users?', 'jetpack' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'external_id' => array(
|
||||
'description' => __( 'The external ID of the connected account', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for the endpoint.
|
||||
*
|
||||
* @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 ),
|
||||
'connection_id' => (string) $publicize->get_connection_id( $connection ),
|
||||
'service_name' => $service_name,
|
||||
'display_name' => $publicize->get_display_name( $service_name, $connection ),
|
||||
'username' => $publicize->get_username( $service_name, $connection ),
|
||||
'profile_display_name' => ! empty( $connection_meta['profile_display_name'] ) ? $connection_meta['profile_display_name'] : '',
|
||||
'profile_picture' => ! empty( $connection_meta['profile_picture'] ) ? $connection_meta['profile_picture'] : '',
|
||||
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- We expect an integer, but do loose comparison below in case some other type is stored.
|
||||
'global' => 0 == $connection_data['user_id'],
|
||||
'external_id' => $connection_meta['external_id'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of connected Publicize connections.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the 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 Array of info about a specific Publicize connection.
|
||||
* @param WP_REST_Request $request Full details about the 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, Jetpack Social 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 Jetpack Social 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,176 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Endpoint used to fetch information to connect to a Publicize service.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
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' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for the publicize services endpoint.
|
||||
*
|
||||
* @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 Jetpack Social service', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'label' => array(
|
||||
'description' => __( 'Human readable label for the Jetpack Social service', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'url' => array(
|
||||
'description' => __( 'The URL used to connect to the Jetpack Social 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 Full details about the request.
|
||||
*
|
||||
* @return WP_REST_Response suitable for 1-page collection
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
global $publicize;
|
||||
|
||||
if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
|
||||
/**
|
||||
* We need this because Publicize::get_available_service_data() uses `Jetpack_Keyring_Service_Helper`
|
||||
* and `Jetpack_Keyring_Service_Helper` needs a `sharing` page to be registered.
|
||||
*/
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.jetpack-keyring-service-helper.php';
|
||||
Jetpack_Keyring_Service_Helper::register_sharing_page();
|
||||
}
|
||||
|
||||
$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 UI service connection data for a specific Publicize service.
|
||||
* @param WP_REST_Request $request Full details about the 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, Jetpack Social 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 Jetpack Social 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,335 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Get and save API keys for a site.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service API Keys: Exposes 3rd party api keys that are used on a site.
|
||||
*
|
||||
* [
|
||||
* { # Availability 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
|
||||
* service_api_key_source: (string) The source of the API key, defaults to "site"
|
||||
* message: (string) User friendly message
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
*
|
||||
* @since 6.9
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Service_API_Keys extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'wpcom/v2';
|
||||
$this->rest_base = 'service-api-keys';
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint 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' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
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' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => array( __CLASS__, 'delete_service_api_key' ),
|
||||
'permission_callback' => array( __CLASS__, 'edit_others_posts_check' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission 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',
|
||||
),
|
||||
'service_api_key_source' => array(
|
||||
'description' => __( 'The source of the API key. Defaults to "site"', '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();
|
||||
}
|
||||
|
||||
switch ( $service ) {
|
||||
case 'mapbox':
|
||||
if ( ! class_exists( 'Jetpack_Mapbox_Helper' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-mapbox-helper.php';
|
||||
}
|
||||
$mapbox = Jetpack_Mapbox_Helper::get_access_token();
|
||||
$service_api_key = $mapbox['key'];
|
||||
$service_api_key_source = $mapbox['source'];
|
||||
break;
|
||||
default:
|
||||
$option = self::key_for_api_service( $service );
|
||||
$service_api_key = Jetpack_Options::get_option( $option, '' );
|
||||
$service_api_key_source = 'site';
|
||||
}
|
||||
|
||||
$message = esc_html__( 'API key retrieved successfully.', 'jetpack' );
|
||||
|
||||
return array(
|
||||
'code' => 'success',
|
||||
'service' => $service,
|
||||
'service_api_key' => $service_api_key,
|
||||
'service_api_key_source' => $service_api_key_source,
|
||||
'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 );
|
||||
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, '' ),
|
||||
'service_api_key_source' => 'site',
|
||||
'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' );
|
||||
|
||||
switch ( $service ) {
|
||||
case 'mapbox':
|
||||
// After deleting a custom Mapbox key, try to revert to the WordPress.com one if available.
|
||||
if ( ! class_exists( 'Jetpack_Mapbox_Helper' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-mapbox-helper.php';
|
||||
}
|
||||
$mapbox = Jetpack_Mapbox_Helper::get_access_token();
|
||||
$service_api_key = $mapbox['key'];
|
||||
$service_api_key_source = $mapbox['source'];
|
||||
break;
|
||||
default:
|
||||
$service_api_key = Jetpack_Options::get_option( $option, '' );
|
||||
$service_api_key_source = 'site';
|
||||
}
|
||||
|
||||
return array(
|
||||
'code' => 'success',
|
||||
'service' => $service,
|
||||
'service_api_key' => $service_api_key,
|
||||
'service_api_key_source' => $service_api_key_source,
|
||||
'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 = isset( $mapbox_geocode_json->message ) ? $mapbox_geocode_json->message : 'Unknown error';
|
||||
}
|
||||
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' );
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* WPCOM Add Featured Media URL
|
||||
* Adds `jetpack_featured_media_url` to post responses
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add featured media url to API post responses.
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Sites_Posts_Add_Featured_Media_URL {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'add_featured_media_url' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add featured media url to post responses.
|
||||
*/
|
||||
public 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,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get featured media url.
|
||||
*
|
||||
* @param mixed $object What the endpoint returns.
|
||||
* @param string $field_name Should always match `->field_name`.
|
||||
* @param WP_REST_Request $request WP API request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_featured_media_url( $object, $field_name, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( ! isset( $object['id'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$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,109 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Get subscriber count from Jetpack's Subscriptions module.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
/**
|
||||
* Subscribers: Get subscriber count
|
||||
*
|
||||
* @since 6.9
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Endpoint_Subscribers extends WP_REST_Controller {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public 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' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register API 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' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
// GET /sites/<blog_id>/subscriber/counts - Return splitted number of subscribers for this site
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/counts',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_subscriber_counts' ),
|
||||
'permission_callback' => array( $this, 'readable_permission_check' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission check. Only authors can access this endpoint.
|
||||
*/
|
||||
public function readable_permission_check() {
|
||||
// @phan-suppress-next-line PhanDeprecatedFunction -- @todo Switch to current_user_can_for_site when we drop support for WP 6.6.
|
||||
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 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// 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' );
|
||||
delete_transient( 'wpcom_subscribers_total_no_publicize' );
|
||||
}
|
||||
$subscriber_count = Jetpack_Subscriptions_Widget::fetch_subscriber_count();
|
||||
|
||||
return array(
|
||||
'count' => $subscriber_count,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves splitted subscriber counts
|
||||
*
|
||||
* @return array data object containing subscriber counts ['email_subscribers' => 0, 'social_followers' => 0]
|
||||
*/
|
||||
public function get_subscriber_counts() {
|
||||
if ( ! Constants::is_defined( 'TESTING_IN_JETPACK' ) ) {
|
||||
delete_transient( 'wpcom_subscribers_totals' );
|
||||
}
|
||||
|
||||
$subscriber_info = Automattic\Jetpack\Extensions\Subscriptions\fetch_subscriber_counts();
|
||||
$subscriber_counts = $subscriber_info['value'];
|
||||
|
||||
return array( 'counts' => $subscriber_counts );
|
||||
}
|
||||
}
|
||||
|
||||
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' );
|
||||
}
|
||||
+444
@@ -0,0 +1,444 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Handle Publicize connection information for each post.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
* profile_picture: (string) Profile picture 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_feature_enabled: (boolean) Is this publicize feature enabled?
|
||||
* jetpack_publicize_message: (string) The message to use instead of the post's title when sharing.
|
||||
* jetpack_social_options: {
|
||||
* attached_media: (array) List of media that will be attached to the social media post.
|
||||
* image_generator_settings: (array) List of settings related to the generated image.
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* @since 6.8.0
|
||||
*/
|
||||
class WPCOM_REST_API_V2_Post_Publicize_Connections_Field extends WPCOM_REST_API_V2_Field_Controller {
|
||||
/**
|
||||
* Array of post types that can handle Publicize.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $object_type = array( 'post' );
|
||||
|
||||
/**
|
||||
* Field name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $field_name = 'jetpack_publicize_connections';
|
||||
|
||||
/**
|
||||
* Array of post IDs that have been updated.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $meta_saved = array();
|
||||
|
||||
/**
|
||||
* Used to memoize the updates for a given post.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
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 ) {
|
||||
if ( $this->is_registered( $post_type ) ) {
|
||||
continue;
|
||||
}
|
||||
// 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(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for the endpoint.
|
||||
*/
|
||||
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 Jetpack Social connection', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'service_name' => array(
|
||||
'description' => __( 'Alphanumeric identifier for the Jetpack Social service', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'display_name' => array(
|
||||
'description' => __( 'Display name of the connected account', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'username' => array(
|
||||
'description' => __( 'Username of the connected account', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'profile_picture' => array(
|
||||
'description' => __( 'Profile picture 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 Jetpack Social 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,
|
||||
),
|
||||
'external_id' => array(
|
||||
'description' => __( 'The external ID of the connected account', 'jetpack' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission check, based on module availability and user capabilities.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function permission_check( $post_id ) {
|
||||
global $publicize;
|
||||
|
||||
if ( ! $publicize ) {
|
||||
return new WP_Error(
|
||||
'publicize_not_available',
|
||||
__( 'Sorry, Jetpack Social 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 Jetpack Social data for this post.', 'jetpack' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter permission check
|
||||
*
|
||||
* @param mixed $post_array Response from the post endpoint.
|
||||
* @param WP_REST_Request $request API request.
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function get_permission_check( $post_array, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return $this->permission_check( isset( $post_array['id'] ) ? $post_array['id'] : 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter permission check.
|
||||
*
|
||||
* @param mixed $value The new value for the field.
|
||||
* @param WP_Post $post The post object.
|
||||
* @param WP_REST_Request $request API request.
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function update_permission_check( $value, $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
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 $request API request.
|
||||
*
|
||||
* @return array List of connections
|
||||
*/
|
||||
public function get( $post_array, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
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_connection['connection_id'] = (string) $connection['id'];
|
||||
|
||||
$output_connection['can_disconnect'] = current_user_can( 'edit_others_posts' ) || get_current_user_id() === (int) $connection['user_id'];
|
||||
$output_connection['shared'] = $connection['global'];
|
||||
|
||||
$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 API request.
|
||||
*
|
||||
* @return object|WP_Error 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 );
|
||||
|
||||
if ( isset( $post->ID ) ) {
|
||||
// Set the meta before we mark the post as published so that publicize works as expected.
|
||||
// If this is not the case post end up on social media when they are marked as skipped.
|
||||
$this->update( $request['jetpack_publicize_connections'], $post, $request );
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* After creating a new post, update our cached data to reflect
|
||||
* the new post ID.
|
||||
*
|
||||
* @param WP_Post $post Post data to update.
|
||||
* @param WP_REST_Request $request API request.
|
||||
* @param bool $is_new Is this a new post.
|
||||
*/
|
||||
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] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of meta data to update per post ID.
|
||||
*
|
||||
* @param array $requested_connections Publicize connections to update.
|
||||
* Items are either `{ id: (string) }` or `{ service_name: (string) }`.
|
||||
* @param int $post_id Post ID.
|
||||
*/
|
||||
protected function get_meta_to_update( $requested_connections, $post_id = 0 ) {
|
||||
global $publicize;
|
||||
|
||||
if ( ! $publicize || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( isset( $post->post_status ) && 'publish' === $post->post_status ) {
|
||||
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_connection_id = array();
|
||||
$available_connections_by_service_name = array();
|
||||
foreach ( $available_connections as $available_connection ) {
|
||||
$available_connections_by_connection_id[ $available_connection['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) }.
|
||||
// If the service is not available, it will be skipped.
|
||||
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 ) {
|
||||
if ( $requested_connection['connection_id'] === $available_connection['id'] ) {
|
||||
$changed_connections[ $available_connection['id'] ] = $requested_connection['enabled'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle { id: $id, enabled: (bool) }
|
||||
// These override the service_name settings.
|
||||
foreach ( $requested_connections as $requested_connection ) {
|
||||
if ( ! isset( $requested_connection['connection_id'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset( $available_connections_by_connection_id[ $requested_connection['connection_id'] ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$changed_connections[ $requested_connection['connection_id'] ] = $requested_connection['enabled'];
|
||||
}
|
||||
|
||||
// Set all changed connections to their new value.
|
||||
foreach ( $changed_connections as $id => $enabled ) {
|
||||
$connection = $available_connections_by_connection_id[ $id ];
|
||||
|
||||
if ( $connection['done'] || ! $connection['toggleable'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$available_connections_by_connection_id[ $id ]['enabled'] = $enabled;
|
||||
}
|
||||
|
||||
$meta_to_update = array();
|
||||
// For all connections, ensure correct post_meta.
|
||||
foreach ( $available_connections_by_connection_id as $connection_id => $available_connection ) {
|
||||
if ( $available_connection['enabled'] ) {
|
||||
$meta_to_update[ $publicize->POST_SKIP_PUBLICIZE . $connection_id ] = null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
} else {
|
||||
$meta_to_update[ $publicize->POST_SKIP_PUBLICIZE . $connection_id ] = 1; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
}
|
||||
}
|
||||
|
||||
$this->memoized_updates[ $post_id ] = $meta_to_update;
|
||||
|
||||
return $meta_to_update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the connections slated to be shared to.
|
||||
*
|
||||
* @param array $requested_connections Publicize conenctions to update.
|
||||
* Items are either `{ id: (string) }` or `{ service_name: (string) }`.
|
||||
* @param WP_Post $post Post data.
|
||||
* @param WP_REST_Request $request API request.
|
||||
*/
|
||||
public function update( $requested_connections, $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( isset( $this->meta_saved[ $post->ID ] ) ) { // Make sure we only save it once - per request.
|
||||
return;
|
||||
}
|
||||
foreach ( $this->get_meta_to_update( $requested_connections, $post->ID ) as $meta_key => $meta_value ) {
|
||||
if ( $meta_value === null ) {
|
||||
delete_post_meta( $post->ID, $meta_key );
|
||||
} else {
|
||||
update_post_meta( $post->ID, $meta_key, $meta_value );
|
||||
}
|
||||
}
|
||||
$this->meta_saved[ $post->ID ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( Jetpack::is_module_active( 'publicize' ) ) {
|
||||
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Post_Publicize_Connections_Field' );
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Loading the various functions used for Jetpack Debugging.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/* Jetpack Connection Testing Framework */
|
||||
require_once __DIR__ . '/debugger/class-jetpack-cxn-test-base.php';
|
||||
/* Jetpack Connection Tests */
|
||||
require_once __DIR__ . '/debugger/class-jetpack-cxn-tests.php';
|
||||
/* Jetpack Debug Data */
|
||||
require_once __DIR__ . '/debugger/class-jetpack-debug-data.php';
|
||||
/* The "In-Plugin Debugger" admin page. */
|
||||
require_once __DIR__ . '/debugger/class-jetpack-debugger.php';
|
||||
/* General Debugging Functions */
|
||||
require_once __DIR__ . '/debugger/debug-functions.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,549 @@
|
||||
<?php
|
||||
/**
|
||||
* Base class for Jetpack's debugging tests.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Status;
|
||||
|
||||
/**
|
||||
* 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 automattic/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['short_description'] ) ) {
|
||||
unset( $results[ $test ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for a passing test.
|
||||
* Possible Args:
|
||||
* - name: string The raw method name that runs the test. Default 'unnamed_test'.
|
||||
* - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
|
||||
* - short_description: bool|string A brief, non-html description that will appear in CLI results. Default 'Test passed!'.
|
||||
* - long_description: bool|string An html description that will appear in the site health page. Default false.
|
||||
* - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
|
||||
* - action: bool|string A URL for the recommended action. Default: false
|
||||
* - action_label: bool|string The label for the recommended action. Default: false
|
||||
* - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
|
||||
*
|
||||
* @param array $args Arguments to override defaults.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function passing_test( $args ) {
|
||||
$defaults = self::test_result_defaults();
|
||||
$defaults['short_description'] = __( 'Test passed!', 'jetpack' );
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$args['pass'] = true;
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for a skipped test.
|
||||
* Possible Args:
|
||||
* - name: string The raw method name that runs the test. Default unnamed_test.
|
||||
* - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
|
||||
* - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default false.
|
||||
* - long_description: bool|string An html description that will appear in the site health page. Default false.
|
||||
* - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
|
||||
* - action: bool|string A URL for the recommended action. Default: false
|
||||
* - action_label: bool|string The label for the recommended action. Default: false
|
||||
* - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
|
||||
*
|
||||
* @param array $args Arguments to override defaults.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function skipped_test( $args = array() ) {
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
self::test_result_defaults()
|
||||
);
|
||||
|
||||
$args['pass'] = 'skipped';
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for an informational test.
|
||||
* Possible Args:
|
||||
* - name: string The raw method name that runs the test. Default unnamed_test.
|
||||
* - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
|
||||
* - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default false.
|
||||
* - long_description: bool|string An html description that will appear in the site health page. Default false.
|
||||
* - severity: bool|string 'critical', 'recommended', or 'good'. Default: false.
|
||||
* - action: bool|string A URL for the recommended action. Default: false
|
||||
* - action_label: bool|string The label for the recommended action. Default: false
|
||||
* - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
|
||||
*
|
||||
* @param array $args Arguments to override defaults.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function informational_test( $args = array() ) {
|
||||
$args = wp_parse_args(
|
||||
$args,
|
||||
self::test_result_defaults()
|
||||
);
|
||||
|
||||
$args['pass'] = 'informational';
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for a failing test.
|
||||
* Possible Args:
|
||||
* - name: string The raw method name that runs the test. Default unnamed_test.
|
||||
* - label: bool|string If false, tests will be labeled with their `name`. You can pass a string to override this behavior. Default false.
|
||||
* - short_description: bool|string A brief, non-html description that will appear in CLI results, and as headings in admin UIs. Default 'Test failed!'.
|
||||
* - long_description: bool|string An html description that will appear in the site health page. Default false.
|
||||
* - severity: bool|string 'critical', 'recommended', or 'good'. Default: 'critical'.
|
||||
* - action: bool|string A URL for the recommended action. Default: false.
|
||||
* - action_label: bool|string The label for the recommended action. Default: false.
|
||||
* - show_in_site_health: bool True if the test should be shown on the Site Health page. Default: true
|
||||
*
|
||||
* @since 7.1.0
|
||||
*
|
||||
* @param array $args Arguments to override defaults.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function failing_test( $args ) {
|
||||
$defaults = self::test_result_defaults();
|
||||
$defaults['short_description'] = __( 'Test failed!', 'jetpack' );
|
||||
$defaults['severity'] = 'critical';
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$args['pass'] = false;
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides defaults for test arguments.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @return array Result defaults.
|
||||
*/
|
||||
private static function test_result_defaults() {
|
||||
return array(
|
||||
'name' => 'unnamed_test',
|
||||
'label' => false,
|
||||
'short_description' => false,
|
||||
'long_description' => false,
|
||||
'severity' => false,
|
||||
'action' => false,
|
||||
'action_label' => false,
|
||||
'show_in_site_health' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ( ( new Status() )->is_offline_mode() ) {
|
||||
WP_CLI::line( __( 'Jetpack is in Offline 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['short_description'] ) {
|
||||
WP_CLI::log( ' ' . $test['short_description'] ); // Number of spaces to "tab indent" the reason.
|
||||
}
|
||||
} elseif ( 'informational' === $test['pass'] ) {
|
||||
WP_CLI::log( WP_CLI::colorize( '%yInfo:%n ' . $test['name'] ) );
|
||||
if ( $test['short_description'] ) {
|
||||
WP_CLI::log( ' ' . $test['short_description'] ); // Number of spaces to "tab indent" the reason.
|
||||
}
|
||||
} else { // Failed.
|
||||
WP_CLI::log( WP_CLI::colorize( '%rFailed:%n ' . $test['name'] ) );
|
||||
WP_CLI::log( ' ' . $test['short_description'] ); // 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['short_description'];
|
||||
$data = array(
|
||||
'resolution' => $result['action'] ?
|
||||
$result['action_label'] . ' :' . $result['action'] :
|
||||
'',
|
||||
);
|
||||
if ( ! $error ) {
|
||||
$error = new WP_Error( $code, $message, $data );
|
||||
} else {
|
||||
$error->add( $code, $message, $data );
|
||||
}
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt data for sending to WordPress.com.
|
||||
*
|
||||
* @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 );
|
||||
|
||||
// Select the first allowed cipher method.
|
||||
$allowed_methods = array( 'aes-256-ctr', 'aes-256-cbc' );
|
||||
$methods = array_intersect( $allowed_methods, openssl_get_cipher_methods() );
|
||||
$method = array_shift( $methods );
|
||||
|
||||
$iv = '';
|
||||
if ( $public_key && $method && openssl_seal( $data, $encrypted_data, $env_key, array( $public_key ), $method, $iv ) ) {
|
||||
// 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
|
||||
'iv' => base64_encode( $iv ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
'cipher' => strtoupper( $method ),
|
||||
);
|
||||
}
|
||||
|
||||
// openssl_free_key was deprecated as no longer needed in PHP 8.0+. Can remove when PHP 8.0 is our minimum. (lol).
|
||||
if ( PHP_VERSION_ID < 80000 ) {
|
||||
openssl_free_key( $public_key ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.openssl_free_keyDeprecated, Generic.PHP.DeprecatedFunctions.Deprecated
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,815 @@
|
||||
<?php
|
||||
/**
|
||||
* Collection of tests to run on the Jetpack connection locally.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Connection\Tokens;
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Automattic\Jetpack\Sync\Health as Sync_Health;
|
||||
use Automattic\Jetpack\Sync\Settings as Sync_Settings;
|
||||
|
||||
/**
|
||||
* 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 ( ! str_contains( $method, 'test__' ) ) {
|
||||
continue;
|
||||
}
|
||||
$this->add_test( array( $this, $method ), $method, 'direct' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after loading default Jetpack Connection tests.
|
||||
*
|
||||
* @since 7.1.0
|
||||
* @since 8.3.0 Passes the Jetpack_Cxn_Tests instance.
|
||||
*/
|
||||
do_action( 'jetpack_connection_tests_loaded', $this );
|
||||
|
||||
/**
|
||||
* Determines if the WP.com testing suite should be included.
|
||||
*
|
||||
* @since 7.1.0
|
||||
* @since 8.1.0 Default false.
|
||||
*
|
||||
* @param bool $run_test To run the WP.com testing suite. Default false.
|
||||
*/
|
||||
if ( apply_filters( 'jetpack_debugger_run_self_test', false ) ) {
|
||||
/**
|
||||
* 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_connection_ready() && ! ( new Status() )->is_offline_mode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the `blog_token` if it exists.
|
||||
*
|
||||
* @return object|false
|
||||
*/
|
||||
protected function helper_get_blog_token() {
|
||||
return ( new Tokens() )->get_access_token();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a support url based on using a development version.
|
||||
*/
|
||||
protected function helper_get_support_url() {
|
||||
return Jetpack::is_development_version()
|
||||
? Redirect::get_url( 'jetpack-contact-support-beta-group' )
|
||||
: Redirect::get_url( 'jetpack-contact-support' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to reconnect Jetpack.
|
||||
*
|
||||
* @return string The reconnect url.
|
||||
*/
|
||||
protected static function helper_get_reconnect_url() {
|
||||
return admin_url( 'admin.php?page=jetpack#/reconnect' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets translated support text.
|
||||
*/
|
||||
protected function helper_get_support_text() {
|
||||
return __( 'Please contact Jetpack support.', 'jetpack' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translated text to reconnect Jetpack.
|
||||
*
|
||||
* @return string The translated reconnect text.
|
||||
*/
|
||||
protected static function helper_get_reconnect_text() {
|
||||
return __( 'Reconnect Jetpack now', 'jetpack' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translated text for failing tests due to timeouts.
|
||||
*
|
||||
* @return string The translated timeout text.
|
||||
*/
|
||||
protected static function helper_get_timeout_text() {
|
||||
return __( 'The test timed out which may sometimes indicate a failure or may be a false failure. Please relaunch tests.', 'jetpack' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets translated reconnect long description.
|
||||
*
|
||||
* @param string $connection_error The connection specific error.
|
||||
* @param string $recommendation The recommendation for resolving the connection error.
|
||||
*
|
||||
* @return string The translated long description for reconnection recommendations.
|
||||
*/
|
||||
protected static function helper_get_reconnect_long_description( $connection_error, $recommendation ) {
|
||||
|
||||
return sprintf(
|
||||
'<p>%1$s</p>' .
|
||||
'<p><span class="dashicons fail"><span class="screen-reader-text">%2$s</span></span> %3$s</p><p><strong>%4$s</strong></p>',
|
||||
__( 'A healthy connection ensures Jetpack essential services are provided to your WordPress site, such as Stats and Site Security.', 'jetpack' ),
|
||||
/* translators: screen reader text indicating a test failed */
|
||||
__( 'Error', 'jetpack' ),
|
||||
$connection_error,
|
||||
$recommendation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return consistent responses for a connection failing test.
|
||||
*
|
||||
* @param string $name The raw method name that runs the test. Default unnamed_test.
|
||||
* @param string $connection_error The connection specific error. Default 'Your site is not connected to Jetpack.'.
|
||||
* @param string $recommendation The recommendation for resolving the connection error. Default 'We recommend reconnecting Jetpack.'.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
public static function connection_failing_test( $name, $connection_error = '', $recommendation = '' ) {
|
||||
$connection_error = empty( $connection_error ) ? __( 'Your site is not connected to Jetpack.', 'jetpack' ) : $connection_error;
|
||||
$recommendation = empty( $recommendation ) ? __( 'We recommend reconnecting Jetpack.', 'jetpack' ) : $recommendation;
|
||||
|
||||
$args = array(
|
||||
'name' => $name,
|
||||
'short_description' => $connection_error,
|
||||
'action' => self::helper_get_reconnect_url(),
|
||||
'action_label' => self::helper_get_reconnect_text(),
|
||||
'long_description' => self::helper_get_reconnect_long_description( $connection_error, $recommendation ),
|
||||
);
|
||||
|
||||
return self::failing_test( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets translated text to enable outbound requests.
|
||||
*
|
||||
* @param string $protocol Either 'HTTP' or 'HTTPS'.
|
||||
*
|
||||
* @return string The translated text.
|
||||
*/
|
||||
protected function helper_enable_outbound_requests( $protocol ) {
|
||||
return sprintf(
|
||||
/* translators: %1$s - request protocol, either http or https */
|
||||
__(
|
||||
'Your server did not successfully connect to the Jetpack server using %1$s
|
||||
Please ask your hosting provider to confirm your server can make outbound requests to jetpack.com.',
|
||||
'jetpack'
|
||||
),
|
||||
$protocol
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 increase_timeout() {
|
||||
return 30; // seconds.
|
||||
}
|
||||
|
||||
/**
|
||||
* The test verifies the blog token exists.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function test__blog_token_if_exists() {
|
||||
$name = __FUNCTION__;
|
||||
|
||||
if ( ! $this->helper_is_jetpack_connected() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Jetpack is not connected. No blog token to check.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$blog_token = $this->helper_get_blog_token();
|
||||
|
||||
if ( $blog_token ) {
|
||||
$result = self::passing_test( array( 'name' => $name ) );
|
||||
} else {
|
||||
$connection_error = __( 'Blog token is missing.', 'jetpack' );
|
||||
|
||||
$result = self::connection_failing_test( $name, $connection_error );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if Jetpack is connected.
|
||||
*/
|
||||
protected function test__check_if_connected() {
|
||||
$name = __FUNCTION__;
|
||||
|
||||
if ( ! $this->helper_get_blog_token() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Blog token is missing.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->helper_is_jetpack_connected() ) {
|
||||
$result = self::passing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'label' => __( 'Your site is connected to Jetpack', 'jetpack' ),
|
||||
'long_description' => sprintf(
|
||||
'<p>%1$s</p>' .
|
||||
'<p><span class="dashicons pass"><span class="screen-reader-text">%2$s</span></span> %3$s</p>',
|
||||
__( 'A healthy connection ensures Jetpack essential services are provided to your WordPress site, such as Stats and Site Security.', 'jetpack' ),
|
||||
/* translators: Screen reader text indicating a test has passed */
|
||||
__( 'Passed', 'jetpack' ),
|
||||
__( 'Your site is connected to Jetpack.', 'jetpack' )
|
||||
),
|
||||
)
|
||||
);
|
||||
} elseif ( ( new Status() )->is_offline_mode() ) {
|
||||
$result = self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Jetpack is in Offline Mode:', 'jetpack' ) . ' ' . Jetpack::development_mode_trigger_text(),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$connection_error = __( 'Your site is not connected to Jetpack', 'jetpack' );
|
||||
|
||||
$result = self::connection_failing_test( $name, $connection_error );
|
||||
}
|
||||
|
||||
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(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Jetpack is not connected. No master user to check.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( ! ( new Connection_Manager() )->get_connection_owner_id() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Jetpack is running without a connected user. No master user to check.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$local_user = $this->helper_retrieve_local_master_user();
|
||||
|
||||
if ( $local_user->exists() ) {
|
||||
$result = self::passing_test( array( 'name' => $name ) );
|
||||
} else {
|
||||
$connection_error = __( 'The user who setup the Jetpack connection no longer exists on this site.', 'jetpack' );
|
||||
|
||||
$result = self::connection_failing_test( $name, $connection_error );
|
||||
}
|
||||
|
||||
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(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Jetpack is not connected.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( ! ( new Connection_Manager() )->get_connection_owner_id() ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Jetpack is running without a connected user. No master user to check.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$master_user = $this->helper_retrieve_local_master_user();
|
||||
|
||||
if ( user_can( $master_user, 'manage_options' ) ) {
|
||||
$result = self::passing_test( array( 'name' => $name ) );
|
||||
} else {
|
||||
/* translators: a WordPress username */
|
||||
$connection_error = sprintf( __( 'The user (%s) who setup the Jetpack connection is not an administrator.', 'jetpack' ), $master_user->user_login );
|
||||
/* translators: a WordPress username */
|
||||
$recommendation = sprintf( __( 'We recommend either upgrading the user (%s) or reconnecting Jetpack.', 'jetpack' ), $master_user->user_login );
|
||||
|
||||
$result = self::connection_failing_test( $name, $connection_error, $recommendation );
|
||||
}
|
||||
|
||||
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( array( 'name' => $name ) );
|
||||
} else {
|
||||
$result = self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'label' => __( 'PHP XML manipulation libraries are not available.', 'jetpack' ),
|
||||
'short_description' => __( 'Please ask your hosting provider to refer to our server requirements and enable PHP\'s XML module.', 'jetpack' ),
|
||||
'action_label' => __( 'View our server requirements', 'jetpack' ),
|
||||
'action' => Redirect::get_url( 'jetpack-support-server-requirements' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
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 === (int) $code ) {
|
||||
$result = self::passing_test( array( 'name' => $name ) );
|
||||
} else {
|
||||
$result = self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => $this->helper_enable_outbound_requests( 'HTTP' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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 === (int) $code ) {
|
||||
$result = self::passing_test( array( 'name' => $name ) );
|
||||
} else {
|
||||
$result = self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => $this->helper_enable_outbound_requests( 'HTTPS' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Jetpack is not connected.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$identity_crisis = Jetpack::check_identity_crisis();
|
||||
|
||||
if ( ! $identity_crisis ) {
|
||||
$result = self::passing_test( array( 'name' => $name ) );
|
||||
} else {
|
||||
$result = self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => 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']
|
||||
),
|
||||
'action_label' => $this->helper_get_support_text(),
|
||||
'action' => $this->helper_get_support_url(),
|
||||
)
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the health of the Connection tokens.
|
||||
*
|
||||
* This will always check the blog token health. It will also check the user token health if
|
||||
* a user is logged in and connected, or if there's a connected owner.
|
||||
*
|
||||
* @since 9.0.0
|
||||
* @since 9.6.0 Checks only blog token if current user not connected or site does not have a connected owner.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
protected function test__connection_token_health() {
|
||||
$name = __FUNCTION__;
|
||||
$m = new Connection_Manager();
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Check if there's a connected logged in user.
|
||||
if ( $user_id && ! $m->is_user_connected( $user_id ) ) {
|
||||
$user_id = false;
|
||||
}
|
||||
|
||||
// If no logged in user to check, let's see if there's a master_user set.
|
||||
if ( ! $user_id ) {
|
||||
$user_id = Jetpack_Options::get_option( 'master_user' );
|
||||
if ( $user_id && ! $m->is_user_connected( $user_id ) ) {
|
||||
return self::connection_failing_test( $name, __( 'Missing token for the connection owner.', 'jetpack' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $user_id ) {
|
||||
return $this->check_tokens_health( $user_id );
|
||||
} else {
|
||||
return $this->check_blog_token_health();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests blog and user's token against wp.com's check-token-health endpoint.
|
||||
*
|
||||
* @since 9.6.0
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
protected function check_blog_token_health() {
|
||||
$name = 'test__connection_token_health';
|
||||
$valid = ( new Tokens() )->validate_blog_token();
|
||||
|
||||
if ( ! $valid ) {
|
||||
return self::connection_failing_test( $name, __( 'Blog token validation failed.', 'jetpack' ) );
|
||||
} else {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests blog token against wp.com's check-token-health endpoint.
|
||||
*
|
||||
* @since 9.6.0
|
||||
*
|
||||
* @param int $user_id The user ID to check the tokens for.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
protected function check_tokens_health( $user_id ) {
|
||||
$name = 'test__connection_token_health';
|
||||
$validated_tokens = ( new Tokens() )->validate( $user_id );
|
||||
|
||||
if ( ! is_array( $validated_tokens ) || count( array_diff_key( array_flip( array( 'blog_token', 'user_token' ) ), $validated_tokens ) ) ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Token health check failed to validate tokens.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$invalid_tokens_exist = false;
|
||||
foreach ( $validated_tokens as $validated_token ) {
|
||||
if ( ! $validated_token['is_healthy'] ) {
|
||||
$invalid_tokens_exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( false === $invalid_tokens_exist ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
$connection_error = __( 'Invalid Jetpack connection tokens.', 'jetpack' );
|
||||
|
||||
return self::connection_failing_test( $name, $connection_error );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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__;
|
||||
|
||||
$status = new Status();
|
||||
if ( ! Jetpack::is_connection_ready() || $status->is_offline_mode() || $status->in_safe_mode() || ! $this->pass ) {
|
||||
return self::skipped_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
add_filter( 'http_request_timeout', array( 'Jetpack_Cxn_Tests', 'increase_timeout' ) );
|
||||
$response = Client::wpcom_json_api_request_as_blog(
|
||||
sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
|
||||
Client::WPCOM_JSON_API_VERSION
|
||||
);
|
||||
remove_filter( 'http_request_timeout', array( 'Jetpack_Cxn_Tests', 'increase_timeout' ) );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
if ( str_contains( $response->get_error_message(), 'cURL error 28' ) ) { // Timeout.
|
||||
$result = self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => self::helper_get_timeout_text(),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
/* 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() );
|
||||
|
||||
$result = self::connection_failing_test( $name, $message );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
if ( ! $body ) {
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'Connection test failed (empty response body)', 'jetpack' ) . wp_remote_retrieve_response_code( $response ),
|
||||
'action_label' => $this->helper_get_support_text(),
|
||||
'action' => $this->helper_get_support_url(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( 404 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'The WordPress.com API returned a 404 error.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$result = json_decode( $body );
|
||||
$is_connected = ! empty( $result->connected );
|
||||
$message = $result->message . ': ' . wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $is_connected ) {
|
||||
$res = self::passing_test( array( 'name' => $name ) );
|
||||
} else {
|
||||
$res = self::connection_failing_test( $name, $message );
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] ) ) {
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => __( 'The server port values are not defined. This is most common when running PHP via a CLI.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
$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( array( 'name' => $name ) ); // Not currently testing for this situation.
|
||||
}
|
||||
|
||||
if ( is_ssl() && in_array( $server_port, $https_ports, true ) ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
} elseif ( in_array( $server_port, $http_ports, true ) ) {
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
} else {
|
||||
if ( is_ssl() ) {
|
||||
$needed_constant = 'JETPACK_SIGNATURE__HTTPS_PORT';
|
||||
} else {
|
||||
$needed_constant = 'JETPACK_SIGNATURE__HTTP_PORT';
|
||||
}
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => sprintf(
|
||||
/* translators: %1$s - a PHP code snippet */
|
||||
__(
|
||||
'The server port value is unexpected.
|
||||
Try adding the following to your wp-config.php file: %1$s',
|
||||
'jetpack'
|
||||
),
|
||||
"define( '$needed_constant', $server_port )"
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Health Tests.
|
||||
*
|
||||
* Disabled: Results in a failing test (recommended)
|
||||
* Delayed: Results in failing test (recommended)
|
||||
* Error: Results in failing test (critical)
|
||||
*/
|
||||
protected function test__sync_health() {
|
||||
|
||||
$name = __FUNCTION__;
|
||||
|
||||
if ( ! $this->helper_is_jetpack_connected() ) {
|
||||
// If the site is not connected, there is no point in testing Sync health.
|
||||
return self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'show_in_site_health' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Sync is disabled.
|
||||
if ( ! Sync_Settings::is_sync_enabled() ) {
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'label' => __( 'Jetpack Sync has been disabled on your site.', 'jetpack' ),
|
||||
'severity' => 'recommended',
|
||||
'action' => 'https://github.com/Automattic/jetpack/blob/trunk/projects/packages/sync/src/class-settings.php',
|
||||
'action_label' => __( 'See Github for more on Sync Settings', 'jetpack' ),
|
||||
'short_description' => __( 'Jetpack Sync has been disabled on your site. This could be impacting some of your site’s Jetpack-powered features. Developers may enable / disable syncing using the Sync Settings API.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
// Sync has experienced Data Loss.
|
||||
if ( Sync_Health::get_status() === Sync_Health::STATUS_OUT_OF_SYNC ) {
|
||||
return self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'label' => __( 'Jetpack has detected a problem with the communication between your site and WordPress.com', 'jetpack' ),
|
||||
'severity' => 'critical',
|
||||
'action' => Redirect::get_url( 'jetpack-contact-support' ),
|
||||
'action_label' => __( 'Contact Jetpack Support', 'jetpack' ),
|
||||
'short_description' => __( 'There is a problem with the communication between your site and WordPress.com. This could be impacting some of your site’s Jetpack-powered features. If you continue to see this error, please contact support for assistance.', 'jetpack' ),
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return self::passing_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @since 7.1.0
|
||||
* @since 7.9.0 Timeout waiting for a WP.com response no longer fails the test. Test is marked skipped instead.
|
||||
*
|
||||
* @return array Test results.
|
||||
*/
|
||||
protected function last__wpcom_self_test() {
|
||||
$name = 'test__wpcom_self_test';
|
||||
|
||||
$status = new Status();
|
||||
if ( ! Jetpack::is_connection_ready() || $status->is_offline_mode() || $status->in_safe_mode() || ! $this->pass ) {
|
||||
return self::skipped_test( array( 'name' => $name ) );
|
||||
}
|
||||
|
||||
$self_xml_rpc_url = site_url( 'xmlrpc.php' );
|
||||
|
||||
$testsite_url = JETPACK__API_BASE . 'testsite/1/?url=';
|
||||
|
||||
// Using PHP_INT_MAX - 1 so that there is still a way to override this if needed and since it only impacts this one call.
|
||||
add_filter( 'http_request_timeout', array( 'Jetpack_Cxn_Tests', 'increase_timeout' ), PHP_INT_MAX - 1 );
|
||||
|
||||
$response = wp_remote_get( $testsite_url . $self_xml_rpc_url );
|
||||
|
||||
remove_filter( 'http_request_timeout', array( 'Jetpack_Cxn_Tests', 'increase_timeout' ), PHP_INT_MAX - 1 );
|
||||
|
||||
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
$result = self::passing_test( array( 'name' => $name ) );
|
||||
} elseif ( is_wp_error( $response ) && str_contains( $response->get_error_message(), 'cURL error 28' ) ) { // Timeout.
|
||||
$result = self::skipped_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => self::helper_get_timeout_text(),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$result = self::failing_test(
|
||||
array(
|
||||
'name' => $name,
|
||||
'short_description' => sprintf(
|
||||
/* translators: %1$s - A debugging url */
|
||||
__( 'Jetpack.com detected an error on the WP.com Self Test. Visit the Jetpack Debug page for more info: %1$s, or contact support.', 'jetpack' ),
|
||||
Redirect::get_url( 'jetpack-support-debug', array( 'query' => 'url=' . rawurlencode( site_url() ) ) )
|
||||
),
|
||||
'action_label' => $this->helper_get_support_text(),
|
||||
'action' => $this->helper_get_support_url(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack Debug Data for the Site Health sections.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Tokens;
|
||||
use Automattic\Jetpack\Connection\Urls;
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
|
||||
use Automattic\Jetpack\Identity_Crisis;
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Status;
|
||||
use Automattic\Jetpack\Sync\Modules;
|
||||
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 = (int) $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 = (int) ( $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()
|
||||
? Redirect::get_url( 'jetpack-contact-support-beta-group' )
|
||||
: Redirect::get_url( 'jetpack-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(), // Only ID number and user name.
|
||||
'private' => false,
|
||||
);
|
||||
$debug_info['is_offline_mode'] = array(
|
||||
'label' => 'Jetpack Offline Mode',
|
||||
'value' => ( new Status() )->is_offline_mode() ? 'on' : 'off',
|
||||
'private' => false,
|
||||
);
|
||||
$debug_info['is_offline_mode_constant'] = array(
|
||||
'label' => 'JETPACK_DEV_DEBUG Constant',
|
||||
'value' => ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) ? 'on' : 'off',
|
||||
'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 = ( new Tokens() )->get_access_token();
|
||||
$user_token = ( new Tokens() )->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' => empty( $_SERVER[ $header ] ) ? 'false' : filter_var( wp_unslash( $_SERVER[ $header ] ) ),
|
||||
'private' => true, // This isn't really 'private' information, but we don't want folks to easily paste these into public forums.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$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' );
|
||||
'@phan-var \Automattic\Jetpack\Sync\Modules\Full_Sync_Immediately|\Automattic\Jetpack\Sync\Modules\Full_Sync $sync_module';
|
||||
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 )
|
||||
? gmdate( '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' => Urls::home_url(),
|
||||
'siteurl' => Urls::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' => Identity_Crisis::should_handle_idc(),
|
||||
'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', $user->ID, $user->user_login ); // Format: "#1 username".
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack Debugger functionality allowing for self-service diagnostic information via the legacy jetpack debugger.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Redirect;
|
||||
use Automattic\Jetpack\Status;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Debugger
|
||||
*
|
||||
* A namespacing class for functionality related to the legacy in-plugin diagnostic tooling.
|
||||
*/
|
||||
class Jetpack_Debugger {
|
||||
/**
|
||||
* Disconnect Jetpack and redirect user to connection flow.
|
||||
*
|
||||
* Used in class.jetpack-admin.php.
|
||||
*/
|
||||
public static function disconnect_and_redirect() {
|
||||
if ( ! ( isset( $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'], 'jp_disconnect' ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! empty( $_GET['disconnect'] ) ) {
|
||||
if ( Jetpack::is_connection_ready() ) {
|
||||
Jetpack::disconnect();
|
||||
wp_safe_redirect( Jetpack::admin_url() );
|
||||
exit( 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles output to the browser for the in-plugin debugger.
|
||||
*/
|
||||
public static function jetpack_debug_display_handler() {
|
||||
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()
|
||||
? Redirect::get_url( 'jetpack-contact-support-beta-group' )
|
||||
: Redirect::get_url( 'jetpack-contact-support' );
|
||||
|
||||
$cxntests = new Jetpack_Cxn_Tests();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<div class="jp-static-block">
|
||||
<h2><?php esc_html_e( 'Debugging Center', 'jetpack' ); ?></h2>
|
||||
|
||||
<div class="jp-static-block-body">
|
||||
<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>';
|
||||
} else {
|
||||
$failures = $cxntests->list_fails();
|
||||
foreach ( $failures as $fail ) {
|
||||
?>
|
||||
<div class="notice notice-error inline">
|
||||
<div class="notice-icon-wrapper">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" width="24" height="24" class="y_IPyP1wIAOhyNaqvXJq" aria-hidden="true" focusable="false"><path d="M10 2c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8zm1.13 9.38l.35-6.46H8.52l.35 6.46h2.26zm-.09 3.36c.24-.23.37-.55.37-.96 0-.42-.12-.74-.36-.97s-.59-.35-1.06-.35-.82.12-1.07.35-.37.55-.37.97c0 .41.13.73.38.96.26.23.61.34 1.06.34s.8-.11 1.05-.34z"></path></svg>
|
||||
</div>
|
||||
|
||||
<div class="notice-main-content">
|
||||
<div class="notice-title"><?php echo esc_html( $fail['short_description'] ); ?></div>
|
||||
|
||||
<div class="notice-action-bar">
|
||||
<div>
|
||||
<a href="<?php echo esc_attr( $fail['action'] ); ?>" aria-disabled="false" class="components-button is-primary"><span><?php echo esc_html( $fail['action_label'] ); ?></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="entry-content">
|
||||
<h4><?php esc_html_e( 'Trouble with Jetpack?', 'jetpack' ); ?></h4>
|
||||
<p><?php esc_html_e( 'It may be caused by one of these issues, which you can diagnose yourself:', 'jetpack' ); ?></p>
|
||||
<ol>
|
||||
<li><?php esc_html_e( 'A known issue.', 'jetpack' ); ?>
|
||||
<?php
|
||||
printf(
|
||||
wp_kses(
|
||||
/* translators: URLs to Jetpack support pages. */
|
||||
__( 'Some themes and plugins have <a href="%1$s" target="_blank" rel="noopener noreferrer">known conflicts</a> with Jetpack – check the list. (You can also browse the <a href="%2$s" target="_blank" rel="noopener noreferrer">Jetpack support pages</a> or <a href="%3$s" target="_blank" rel="noopener noreferrer">Jetpack support forum</a> to see if others have experienced and solved the problem.)', 'jetpack' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
'target' => array(),
|
||||
'rel' => array(),
|
||||
),
|
||||
)
|
||||
),
|
||||
esc_url( Redirect::get_url( 'jetpack-contact-support-known-issues' ) ),
|
||||
esc_url( Redirect::get_url( 'jetpack-support' ) ),
|
||||
esc_url( Redirect::get_url( 'wporg-support-plugin-jetpack' ) )
|
||||
);
|
||||
?>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'An incompatible plugin.', 'jetpack' ); ?>
|
||||
<?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>
|
||||
<?php esc_html_e( 'A theme conflict.', 'jetpack' ); ?>
|
||||
<?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>
|
||||
<?php esc_html_e( 'A problem with your XML-RPC file.', 'jetpack' ); ?>
|
||||
<?php
|
||||
printf(
|
||||
wp_kses(
|
||||
/* translators: The URL to the site's xmlrpc.php file. */
|
||||
__( 'Load your <a href="%s">XML-RPC 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 the XML-RPC file.', 'jetpack' ); ?></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<?php if ( current_user_can( 'jetpack_disconnect' ) && Jetpack::is_connection_ready() ) : ?>
|
||||
<li>
|
||||
<?php esc_html_e( 'A connection problem with WordPress.com.', 'jetpack' ); ?>
|
||||
<?php
|
||||
printf(
|
||||
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>
|
||||
<?php esc_html_e( 'Ask us for help!', 'jetpack' ); ?>
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Offload to new WordPress debug data.
|
||||
*/
|
||||
printf(
|
||||
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' )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="jp-static-block">
|
||||
<h2><?php esc_html_e( 'More details about your Jetpack settings', 'jetpack' ); ?></h2>
|
||||
|
||||
<div class="jp-static-block-body">
|
||||
<?php if ( Jetpack::is_connection_ready() ) : ?>
|
||||
<div id="connected-user-details">
|
||||
<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 offline mode</a>.', 'jetpack' ),
|
||||
array( 'a' => array( 'href' => array() ) )
|
||||
),
|
||||
esc_url( Redirect::get_url( 'jetpack-support-development-mode' ) )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
if (
|
||||
current_user_can( 'jetpack_manage_modules' )
|
||||
&& ( ( new Status() )->is_offline_mode() || Jetpack::is_connection_ready() )
|
||||
) {
|
||||
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>
|
||||
</div>
|
||||
</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: 8px 0;
|
||||
}
|
||||
|
||||
#connected-user-details p strong {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.jetpack-tests-succeed {
|
||||
font-size: large;
|
||||
color: #069E08;
|
||||
}
|
||||
|
||||
.jetpack-test-details {
|
||||
margin: 4px 6px;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.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 #dcdcde;
|
||||
border-radius: 11px;
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.1);
|
||||
color: #646970;
|
||||
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: #dcdcde;
|
||||
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,118 @@
|
||||
<?php
|
||||
/**
|
||||
* WP Site Health debugging functions.
|
||||
*
|
||||
* @package automattic/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'],
|
||||
/**
|
||||
* Callable for Core's Site Health system to execute.
|
||||
*
|
||||
* @var array $test A Jetpack Testing Suite test array.
|
||||
* @var Jetpack_Cxn_Tests $cxn_tests An instance of the Jetpack Test Suite.
|
||||
*
|
||||
* @return array {
|
||||
* A results array to match the format expected by WordPress Core.
|
||||
*
|
||||
* @type string $label Name for the test.
|
||||
* @type string $status 'critical', 'recommended', or 'good'.
|
||||
* @type array $badge Array for Site Health status. Keys label and color.
|
||||
* @type string $description Description of the test result.
|
||||
* @type string $action HTML to a link to resolve issue.
|
||||
* @type string $test Unique test identifier.
|
||||
* }
|
||||
*/
|
||||
'test' => function () use ( $test, $cxn_tests ) {
|
||||
$results = $cxn_tests->run_test( $test['name'] );
|
||||
if ( is_wp_error( $results ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$label = $results['label'] ?
|
||||
$results['label'] :
|
||||
ucwords(
|
||||
str_replace(
|
||||
'_',
|
||||
' ',
|
||||
str_replace( 'test__', '', $test['name'] )
|
||||
)
|
||||
);
|
||||
if ( $results['long_description'] ) {
|
||||
$description = $results['long_description'];
|
||||
} elseif ( $results['short_description'] ) {
|
||||
$description = sprintf(
|
||||
'<p>%s</p>',
|
||||
$results['short_description']
|
||||
);
|
||||
} else {
|
||||
$description = sprintf(
|
||||
'<p>%s</p>',
|
||||
__( 'This test successfully passed!', 'jetpack' )
|
||||
);
|
||||
}
|
||||
|
||||
$return = array(
|
||||
'label' => $label,
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => __( 'Jetpack', 'jetpack' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => $description,
|
||||
'actions' => '',
|
||||
'test' => 'jetpack_' . $test['name'],
|
||||
);
|
||||
|
||||
if ( false === $results['pass'] ) {
|
||||
$return['status'] = $results['severity'];
|
||||
if ( ! empty( $results['action'] ) ) {
|
||||
$return['actions'] = sprintf(
|
||||
'<a 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'] ),
|
||||
$results['action_label'],
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
/** phpcs:disable Squiz.Commenting.FileComment.MissingPackageTag,Generic.Commenting.DocComment.MissingShort
|
||||
*
|
||||
* Declare two functions to handle notification emails to authors and moderators.
|
||||
*
|
||||
* These functions are hooked into filters to short circuit the regular flow and send the emails.
|
||||
* Code was copied from the original pluggable functions and slightly modified (modifications are commented).
|
||||
*
|
||||
* In the past, we used to overwrite the whole pluggable function, but we started using filters to avoid having
|
||||
* to check for Jetpack::is_active() too early in the load flow.
|
||||
*
|
||||
* @deprecated 13.9 File became unused.
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Redirect;
|
||||
|
||||
_deprecated_file( __FILE__, 'jetpack-13.9' );
|
||||
|
||||
// phpcs:disable WordPress.WP.I18n.MissingArgDomain --reason: Code copied from Core, so using Core strings.
|
||||
// phpcs:disable WordPress.Utils.I18nTextDomainFixer.MissingArgDomain --reason: Code copied from Core, so using Core strings.
|
||||
|
||||
/**
|
||||
* Short circuits the {@see `wp_notify_postauthor`} function via the `comment_notification_recipients` filter.
|
||||
*
|
||||
* Notify an author (and/or others) of a comment/trackback/pingback on a post.
|
||||
*
|
||||
* @since 5.8.0
|
||||
* @since 9.3.0 Switched from pluggable function to filter callback
|
||||
*
|
||||
* @param array $emails List of recipients.
|
||||
* @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
|
||||
* @return array Empty array to shortcircuit wp_notify_postauthor execution. $emails if we want to disable the filter.
|
||||
*/
|
||||
function jetpack_notify_postauthor( $emails, $comment_id ) {
|
||||
// Don't do anything if Jetpack isn't connected.
|
||||
if ( ! Jetpack::is_connection_ready() || empty( $emails ) ) {
|
||||
return $emails;
|
||||
}
|
||||
|
||||
// Original function modified: Code before the comment_notification_recipients filter removed.
|
||||
|
||||
$comment = get_comment( $comment_id );
|
||||
if ( ! $comment ) {
|
||||
return $emails;
|
||||
}
|
||||
|
||||
$post = get_post( $comment->comment_post_ID );
|
||||
$author = get_userdata( $post->post_author );
|
||||
|
||||
// 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 && get_current_user_id() === $post->post_author ) {
|
||||
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 ( array() === $emails ) {
|
||||
return array(); // Original function modified. Return empty array instead of false.
|
||||
} else {
|
||||
$emails = array_flip( $emails );
|
||||
}
|
||||
|
||||
$switched_locale = switch_to_locale( get_locale() );
|
||||
|
||||
$comment_author_domain = '';
|
||||
if ( WP_Http::is_ip_address( $comment->comment_author_IP ) ) {
|
||||
$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 );
|
||||
|
||||
// Original function modified.
|
||||
$moderate_on_wpcom = ! in_array( // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||
false,
|
||||
array_map( 'jetpack_notify_is_user_connected_by_email', $emails )
|
||||
);
|
||||
|
||||
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";
|
||||
/* translators: %s: Site URL */
|
||||
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
|
||||
/* translators: %s: Comment Content */
|
||||
$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";
|
||||
/* translators: %s: Site URL */
|
||||
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
|
||||
/* translators: %s: Comment Content */
|
||||
$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.
|
||||
/* translators: 1: Post title */
|
||||
$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";
|
||||
/* translators: %s: Email address */
|
||||
$notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n";
|
||||
/* translators: %s: Site URL */
|
||||
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
|
||||
/* translators: %s: Comment Content */
|
||||
$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;
|
||||
}
|
||||
|
||||
// Original function modified: Consider $moderate_on_wpcom when building $notify_message.
|
||||
$notify_message .= $moderate_on_wpcom
|
||||
? Redirect::get_url(
|
||||
'calypso-comments-all',
|
||||
array(
|
||||
'path' => $comment->comment_post_ID,
|
||||
)
|
||||
) . "/\r\n\r\n"
|
||||
: get_permalink( $comment->comment_post_ID ) . "#comments\r\n\r\n";
|
||||
|
||||
/* translators: %s: URL */
|
||||
$notify_message .= sprintf( __( 'Permalink: %s' ), get_comment_link( $comment ) ) . "\r\n";
|
||||
|
||||
$base_wpcom_edit_comment_url = Redirect::get_url(
|
||||
'calypso-edit-comment',
|
||||
array(
|
||||
'path' => $comment_id,
|
||||
'query' => 'action=__action__', // __action__ will be replaced by the actual action.
|
||||
)
|
||||
);
|
||||
|
||||
// Original function modified: Consider $moderate_on_wpcom when building $notify_message.
|
||||
if ( user_can( $post->post_author, 'edit_comment', $comment->comment_ID ) ) {
|
||||
if ( EMPTY_TRASH_DAYS ) {
|
||||
$notify_message .= sprintf(
|
||||
/* translators: Placeholder is the edit URL */
|
||||
__( 'Trash it: %s' ),
|
||||
$moderate_on_wpcom
|
||||
? str_replace( '__action__', 'trash', $base_wpcom_edit_comment_url )
|
||||
: admin_url( "comment.php?action=trash&c={$comment->comment_ID}#wpbody-content" )
|
||||
) . "\r\n";
|
||||
} else {
|
||||
$notify_message .= sprintf(
|
||||
/* translators: Placeholder is the edit URL */
|
||||
__( 'Delete it: %s' ),
|
||||
$moderate_on_wpcom
|
||||
? str_replace( '__action__', 'delete', $base_wpcom_edit_comment_url )
|
||||
: admin_url( "comment.php?action=delete&c={$comment->comment_ID}#wpbody-content" )
|
||||
) . "\r\n";
|
||||
}
|
||||
$notify_message .= sprintf(
|
||||
/* translators: Placeholder is the edit URL */
|
||||
__( 'Spam it: %s' ),
|
||||
$moderate_on_wpcom
|
||||
? str_replace( '__action__', 'spam', $base_wpcom_edit_comment_url )
|
||||
: admin_url( "comment.php?action=spam&c={$comment->comment_ID}#wpbody-content" )
|
||||
) . "\r\n";
|
||||
}
|
||||
|
||||
$wp_email = 'wordpress@' . preg_replace( '#^www\.#', '', strtolower( isset( $_SERVER['SERVER_NAME'] ) ? filter_var( wp_unslash( $_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 array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Short circuits the {@see `wp_notify_moderator`} function via the `notify_moderator` filter.
|
||||
*
|
||||
* Notifies the moderator of the site about a new comment that is awaiting approval.
|
||||
*
|
||||
* @since 5.8.0
|
||||
* @since 9.2.0 Switched from pluggable function to filter callback
|
||||
* @since 9.5.0 Updated the passing condition to call get_option( 'moderation_notify' ); directly.
|
||||
*
|
||||
* @global wpdb $wpdb WordPress database abstraction object.
|
||||
*
|
||||
* @param string $notify_moderator The value of the moderation_notify option OR if the comment is awaiting moderation.
|
||||
* @param int $comment_id Comment ID.
|
||||
* @return boolean Returns false to shortcircuit the execution of wp_notify_moderator
|
||||
*/
|
||||
function jetpack_notify_moderator( $notify_moderator, $comment_id ) {
|
||||
/*
|
||||
* $notify_moderator is a tricky one. This filter is called in two places in Core. One is just to pass if a comment
|
||||
* is being held for moderation. See https://core.trac.wordpress.org/browser/tags/5.6/src/wp-includes/comment.php#L2296
|
||||
*
|
||||
* So we can't just assume that a true value here is what we need. The second time the filter is called, it checks
|
||||
* the option -- which is what we expected here. See https://core.trac.wordpress.org/browser/tags/5.6/src/wp-includes/pluggable.php#L1737
|
||||
*
|
||||
* It's possible another plugin would be filtering this value to true despite the option setting; however, since we're running at priority 1,
|
||||
* they can still do that. They'll just get the Core flow instead of this one.
|
||||
*/
|
||||
|
||||
// If Jetpack is not active, or if Notify moderators options is not set, let the default flow go on.
|
||||
if ( ! $notify_moderator || ! get_option( 'moderation_notify' ) || ! Jetpack::is_connection_ready() ) {
|
||||
return $notify_moderator;
|
||||
}
|
||||
|
||||
// Original function modified: Removed code before the notify_moderator filter.
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$comment = get_comment( $comment_id );
|
||||
if ( ! $comment ) {
|
||||
return $notify_moderator;
|
||||
}
|
||||
|
||||
$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 = '';
|
||||
if ( WP_Http::is_ip_address( $comment->comment_author_IP ) ) {
|
||||
$comment_author_domain = gethostbyaddr( $comment->comment_author_IP );
|
||||
}
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$comments_waiting = (int) $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 );
|
||||
|
||||
// Original function modified.
|
||||
$moderate_on_wpcom = ! in_array( // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||
false,
|
||||
array_map( 'jetpack_notify_is_user_connected_by_email', $emails )
|
||||
);
|
||||
|
||||
$base_wpcom_edit_comment_url = Redirect::get_url(
|
||||
'calypso-edit-comment',
|
||||
array(
|
||||
'path' => $comment_id,
|
||||
'query' => 'action=__action__', // __action__ will be replaced by the actual action.
|
||||
)
|
||||
);
|
||||
|
||||
// Original function modified: Consider $moderate_on_wpcom when building $notify_message.
|
||||
$notify_message .= sprintf(
|
||||
/* translators: Comment moderation. 1: Comment action URL */
|
||||
__( 'Approve it: %s' ),
|
||||
$moderate_on_wpcom
|
||||
? str_replace( '__action__', 'approve', $base_wpcom_edit_comment_url )
|
||||
: admin_url( "comment.php?action=approve&c={$comment_id}#wpbody-content" )
|
||||
) . "\r\n";
|
||||
|
||||
if ( EMPTY_TRASH_DAYS ) {
|
||||
$notify_message .= sprintf(
|
||||
/* translators: Comment moderation. 1: Comment action URL */
|
||||
__( 'Trash it: %s' ),
|
||||
$moderate_on_wpcom
|
||||
? str_replace( '__action__', 'trash', $base_wpcom_edit_comment_url )
|
||||
: admin_url( "comment.php?action=trash&c={$comment_id}#wpbody-content" )
|
||||
) . "\r\n";
|
||||
} else {
|
||||
$notify_message .= sprintf(
|
||||
/* translators: Comment moderation. 1: Comment action URL */
|
||||
__( 'Delete it: %s' ),
|
||||
$moderate_on_wpcom
|
||||
? str_replace( '__action__', 'delete', $base_wpcom_edit_comment_url )
|
||||
: admin_url( "comment.php?action=delete&c={$comment_id}#wpbody-content" )
|
||||
) . "\r\n";
|
||||
}
|
||||
|
||||
$notify_message .= sprintf(
|
||||
/* translators: Comment moderation. 1: Comment action URL */
|
||||
__( 'Spam it: %s' ),
|
||||
$moderate_on_wpcom
|
||||
? str_replace( '__action__', 'spam', $base_wpcom_edit_comment_url )
|
||||
: admin_url( "comment.php?action=spam&c={$comment_id}#wpbody-content" )
|
||||
) . "\r\n";
|
||||
|
||||
$notify_message .= sprintf(
|
||||
/* translators: Comment moderation. 1: Number of comments awaiting approval */
|
||||
_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
|
||||
? Redirect::get_url( 'calypso-comments-pending' )
|
||||
: 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 false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an user by email and verify if it's connected
|
||||
*
|
||||
* @param string $email The user email.
|
||||
* @return boolean
|
||||
*/
|
||||
function jetpack_notify_is_user_connected_by_email( $email ) {
|
||||
$user = get_user_by( 'email', $email );
|
||||
return ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user->ID );
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Class aliases for SimplePie.
|
||||
*
|
||||
* Core renamed the classes in 6.7, and for type declarations and such to work right we need to use the correct names.
|
||||
* This provides aliases for use with WordPress 6.7.
|
||||
*
|
||||
* @todo Remove this once we drop support for WordPress 6.6
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
class_alias( SimplePie\SimplePie::class, Jetpack\SimplePie\SimplePie::class );
|
||||
class_alias( SimplePie\File::class, Jetpack\SimplePie\File::class );
|
||||
class_alias( SimplePie\Item::class, Jetpack\SimplePie\Item::class );
|
||||
class_alias( SimplePie\Locator::class, Jetpack\SimplePie\Locator::class );
|
||||
class_alias( SimplePie\Enclosure::class, Jetpack\SimplePie\Enclosure::class );
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Class aliases for SimplePie.
|
||||
*
|
||||
* Core renamed the classes in 6.7, and for type declarations and such to work right we need to use the correct names.
|
||||
* This provides aliases for use with WordPress 6.6.
|
||||
*
|
||||
* @todo Remove this once we drop support for WordPress 6.6
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
class_alias( SimplePie::class, Jetpack\SimplePie\SimplePie::class );
|
||||
class_alias( SimplePie_File::class, Jetpack\SimplePie\File::class );
|
||||
class_alias( SimplePie_Item::class, Jetpack\SimplePie\Item::class );
|
||||
class_alias( SimplePie_Locator::class, Jetpack\SimplePie\Locator::class );
|
||||
class_alias( SimplePie_Enclosure::class, Jetpack\SimplePie\Enclosure::class );
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Class aliases for SimplePie.
|
||||
*
|
||||
* Core renamed the classes in 6.7, and for type declarations and such to work right we need to use the correct names.
|
||||
* This selects the correct alias file for the current version of WordPress.
|
||||
*
|
||||
* @todo Remove this once we drop support for WordPress 6.6
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once ABSPATH . '/wp-includes/class-simplepie.php';
|
||||
// @phan-suppress-next-line PhanUndeclaredClassReference -- Being tested for. @phan-suppress-current-line UnusedPluginSuppression
|
||||
if ( class_exists( SimplePie\SimplePie::class ) ) {
|
||||
require_once __DIR__ . '/jp-simplepie-alias-new.php';
|
||||
} else {
|
||||
require_once __DIR__ . '/jp-simplepie-alias-old.php';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* Loader for the Markdown library.
|
||||
*
|
||||
* This file loads in a couple specific things from the markdown dir.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
if ( ! class_exists( 'MarkdownExtra_Parser' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/markdown/extra.php';
|
||||
}
|
||||
|
||||
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/markdown/gfm.php';
|
||||
@@ -0,0 +1,19 @@
|
||||
# Markdown parsing library
|
||||
|
||||
Contains two libraries:
|
||||
|
||||
* `/extra`
|
||||
- Gives you `MardownExtra_Parser` and `Markdown_Parser`
|
||||
- Docs at http://michelf.ca/projects/php-markdown/extra/
|
||||
|
||||
* `/gfm` -- Github Flavored Markdown
|
||||
- Gives you `WPCom_GHF_Markdown_Parser`
|
||||
- It has the same interface as `MarkdownExtra_Parser`
|
||||
- Adds support for fenced code blocks: https://help.github.com/articles/creating-and-highlighting-code-blocks/#fenced-code-blocks
|
||||
- By default it replaces them with a code shortcode
|
||||
- You can change this using the `$use_code_shortcode` member variable
|
||||
- You can change the code shortcode wrapping with `$shortcode_start` and `$shortcode_end` member variables
|
||||
- The `$preserve_shortcodes` member variable will preserve all registered shortcodes untouched. Requires WordPress to be loaded for `get_shortcode_regex()`
|
||||
- The `$preserve_latex` member variable will preserve oldskool $latex yer-latex$ codes untouched.
|
||||
- The `$strip_paras` member variable will strip <p> tags because that's what WordPress likes.
|
||||
- See `WPCom_GHF_Markdown_Parser::__construct()` for how the above member variable defaults are set.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,445 @@
|
||||
<?php
|
||||
/**
|
||||
* GitHub-Flavoured Markdown. Inspired by Evan's plugin, but modified.
|
||||
*
|
||||
* @author Evan Solomon
|
||||
* @author Matt Wiebe <wiebe@automattic.com>
|
||||
* @author Brandon Kraft <kraft@automattic.com> -- Strikedown support converted from GPL code at https://github.com/annaesvensson/yellow-markdown/blob/main/markdown.php
|
||||
* @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.1';
|
||||
|
||||
/**
|
||||
* 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' );
|
||||
|
||||
$this->span_gamut['do_strikethrough'] = 55;
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
// Do not process characters inside URLs.
|
||||
$text = $this->urls_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid characters inside URLs from being formatted by Markdown in any way.
|
||||
*
|
||||
* @param string $text Text in which to preserve URLs.
|
||||
*
|
||||
* @return string Text with URLs replaced by a hash that will be restored later.
|
||||
*/
|
||||
protected function urls_preserve( $text ) {
|
||||
$text = preg_replace_callback(
|
||||
'#(?<!<)(?:https?|ftp)://([^\s<>"\'\[\]()]+|\[(?1)*+\]|\((?1)*+\))+(?<![_*.?])#i',
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add strikethrough support.
|
||||
*
|
||||
* GPL code modified from https://github.com/annaesvensson/yellow-markdown/blob/main/markdown.php
|
||||
*/
|
||||
public function do_strikethrough($text) {
|
||||
$parts = preg_split("/(?<![~])(~~)(?![~])/", $text, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
if (count($parts)>3) {
|
||||
$text = "";
|
||||
$open = false;
|
||||
foreach ($parts as $part) {
|
||||
if ($part=="~~") {
|
||||
$text .= $open ? "</del>" : "<del>";
|
||||
$open = !$open;
|
||||
} else {
|
||||
$text .= $part;
|
||||
}
|
||||
}
|
||||
if ($open) $text .= "</del>";
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Plans Library
|
||||
*
|
||||
* Fetch plans data from WordPress.com.
|
||||
*
|
||||
* Not to be confused with the `Jetpack_Plan` (singular)
|
||||
* class, which stores and syncs data about the site's _current_ plan.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
class Jetpack_Plans {
|
||||
/**
|
||||
* Get a list of all available plans from WordPress.com
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @return array The plans list
|
||||
*/
|
||||
public static function get_plans() {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
if ( ! class_exists( 'Store_Product_List' ) ) {
|
||||
require WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/store-product-list.php';
|
||||
}
|
||||
|
||||
return Store_Product_List::api_only_get_active_plans_v1_4();
|
||||
}
|
||||
|
||||
// We're on Jetpack, so it's safe to use this namespace.
|
||||
$request = Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user(
|
||||
'/plans?_locale=' . get_user_locale(),
|
||||
// We're using version 1.5 of the endpoint rather than the default version 2
|
||||
// since the latter only returns Jetpack Plans, but we're also interested in
|
||||
// WordPress.com plans, for consumers of this method that run on WP.com.
|
||||
'1.5',
|
||||
array(
|
||||
'method' => 'GET',
|
||||
'headers' => array(
|
||||
'X-Forwarded-For' => ( new Automattic\Jetpack\Status\Visitor() )->get_ip( true ),
|
||||
),
|
||||
),
|
||||
null,
|
||||
'rest'
|
||||
);
|
||||
|
||||
$body = wp_remote_retrieve_body( $request );
|
||||
if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
|
||||
return json_decode( $body );
|
||||
} else {
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plan information for a plan given its slug
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @param string $plan_slug Plan slug.
|
||||
*
|
||||
* @return object The plan object
|
||||
*/
|
||||
public static function get_plan( $plan_slug ) {
|
||||
$plans = self::get_plans();
|
||||
if ( ! is_array( $plans ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $plans as $plan ) {
|
||||
if ( $plan_slug === $plan->product_slug ) {
|
||||
return $plan;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
/**
|
||||
* This file has been moved to the jetpack-plugins-installer package
|
||||
*
|
||||
* @deprecated 10.7
|
||||
*
|
||||
* @package jetpack
|
||||
*/
|
||||
|
||||
class_alias( Automattic\Jetpack\Plugins_Installer::class, 'Jetpack_Plugins' );
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* Tonesque
|
||||
* Grab an average color representation from an image.
|
||||
*
|
||||
* @author Automattic
|
||||
* @author Matias Ventura
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
if ( ! class_exists( 'Tonesque' ) ) {
|
||||
/**
|
||||
* Color representation class.
|
||||
*/
|
||||
class Tonesque {
|
||||
/**
|
||||
* Image URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $image_url = '';
|
||||
/**
|
||||
* Image identifier representing the image.
|
||||
*
|
||||
* @var null|object
|
||||
*/
|
||||
private $image_obj = null;
|
||||
/**
|
||||
* Color code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $color = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $image_url Image URL.
|
||||
*/
|
||||
public function __construct( $image_url ) {
|
||||
_deprecated_function( 'Tonesque::__construct', 'jetpack-13.8' );
|
||||
|
||||
if ( ! class_exists( 'Jetpack_Color' ) ) {
|
||||
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class.color.php';
|
||||
}
|
||||
|
||||
$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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an image object from a URL.
|
||||
*
|
||||
* @param string $image_url Image URL.
|
||||
*
|
||||
* @return object|bool Image object or false if the image could not be loaded.
|
||||
*/
|
||||
public static function imagecreatefromurl( $image_url ) {
|
||||
_deprecated_function( 'Tonesque::imagecreatefromurl', 'jetpack-13.8' );
|
||||
|
||||
$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 ( str_starts_with( $_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 );
|
||||
$type = $filetype['type'];
|
||||
|
||||
if ( str_starts_with( $type, 'image/' ) ) {
|
||||
$data = file_get_contents( $_image_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $data ) ) {
|
||||
$response = wp_safe_remote_get( $image_url );
|
||||
$response_code = wp_remote_retrieve_response_code( $response );
|
||||
if (
|
||||
is_wp_error( $response )
|
||||
|| ! $response_code
|
||||
|| $response_code < 200
|
||||
|| $response_code >= 300
|
||||
) {
|
||||
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 );
|
||||
$type = $filetype['type'];
|
||||
|
||||
if ( str_starts_with( $type, 'image/' ) ) {
|
||||
$data = file_get_contents( $image_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
}
|
||||
}
|
||||
|
||||
if ( null === $data ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now turn it into an image and return it.
|
||||
return imagecreatefromstring( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct object from image.
|
||||
*
|
||||
* @param string $type Type (hex, rgb, hsv) (optional).
|
||||
*
|
||||
* @return string|bool color as a string formatted as $type or false if the image could not be loaded.
|
||||
*/
|
||||
public 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.
|
||||
return self::get_color( $color, $type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the color index for each of five sample points of the image
|
||||
*
|
||||
* @param string $type can be 'index' or 'hex'.
|
||||
*
|
||||
* @return array|false color indices or false if the image could not be loaded.
|
||||
*/
|
||||
public 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
|
||||
*
|
||||
* @return array|bool array with rgb color or false if the image could not be loaded.
|
||||
*/
|
||||
public function grab_color() {
|
||||
$img = $this->image_obj;
|
||||
if ( ! $img ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$rgb = self::grab_points();
|
||||
|
||||
$r = array();
|
||||
$g = array();
|
||||
$b = array();
|
||||
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* @param string $color Color code.
|
||||
* @param string $type Color type (rgb, hex, hsv).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public 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 $c->toHex();
|
||||
}
|
||||
|
||||
return $color;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Checks contrast against main color
|
||||
* Gives either black or white for using with opacity
|
||||
*
|
||||
* @return string|bool Returns black or white or false if the image could not be loaded.
|
||||
*/
|
||||
public function contrast() {
|
||||
if ( ! $this->color ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$c = $this->color->getMaxContrastColor();
|
||||
return implode( ',', $c->toRgbInt() );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,787 @@
|
||||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
/**
|
||||
* Widgets and Sidebars Library
|
||||
*/
|
||||
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|null $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 (int) $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 ( (int) $widget['position'] > (int) $last_position ) {
|
||||
$last_position = (int) $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 ];
|
||||
$settings = self::sanitize_widget_settings( $widget_id, $settings, $old_settings );
|
||||
|
||||
if ( ! $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 ) {
|
||||
$widget = self::get_registered_widget_object( self::get_widget_id_base( $widget_id ) );
|
||||
|
||||
if ( ! $widget ) {
|
||||
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 $position (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->tracks_record_event(
|
||||
wp_get_current_user(),
|
||||
'wpcom_widgets_activate_widget',
|
||||
array(
|
||||
'widget' => $id_base,
|
||||
'settings' => wp_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 = (int) self::get_widget_instance_key( $last_widget['id'] );
|
||||
|
||||
return $last_val;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used to sort widgets
|
||||
*
|
||||
* @since 5.4
|
||||
*
|
||||
* @param array $a A normalized array representing a widget.
|
||||
* @param array $b A normalized array representing a widget.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function sort_widgets( $a, $b ) {
|
||||
$a_val = (int) self::get_widget_instance_key( $a['id'] );
|
||||
$b_val = (int) self::get_widget_instance_key( $b['id'] );
|
||||
return $a_val <=> $b_val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public 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.
|
||||
*/
|
||||
public 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 ( str_contains( $widget, $widget_id ) ) {
|
||||
$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.
|
||||
*/
|
||||
public 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