Added login request

This commit is contained in:
Nedim Uka
2018-06-20 18:03:43 +02:00
parent 4e52521fae
commit 593b445a21
4716 changed files with 1218265 additions and 57 deletions

View File

@@ -0,0 +1,222 @@
<?php
// Shared logic between Jetpack admin pages
abstract class Jetpack_Admin_Page {
// Add page specific actions given the page hook
abstract function add_page_actions( $hook );
// Create a menu item for the page and returns the hook
abstract function get_page_hook();
// Enqueue and localize page specific scripts
abstract function page_admin_scripts();
// Render page specific HTML
abstract function page_render();
/**
* Should we block the page rendering because the site is in IDC?
* @var bool
*/
static $block_page_rendering_for_idc;
/**
* Function called after admin_styles to load any additional needed styles.
*
* @since 4.3.0
*/
function additional_styles() {}
function __construct() {
$this->jetpack = Jetpack::init();
self::$block_page_rendering_for_idc = (
Jetpack::validate_sync_error_idc_option() && ! Jetpack_Options::get_option( 'safe_mode_confirmed' )
);
}
function add_actions() {
// If user is not an admin and site is in Dev Mode, don't do anything
if ( ! current_user_can( 'manage_options' ) && Jetpack::is_development_mode() ) {
return;
}
// Don't add in the modules page unless modules are available!
if ( $this->dont_show_if_not_active && ! Jetpack::is_active() && ! Jetpack::is_development_mode() ) {
return;
}
// Initialize menu item for the page in the admin
$hook = $this->get_page_hook();
// Attach hooks common to all Jetpack admin pages based on the created
// hook
add_action( "load-$hook", array( $this, 'admin_help' ) );
add_action( "load-$hook", array( $this, 'admin_page_load' ) );
add_action( "admin_head-$hook", array( $this, 'admin_head' ) );
add_action( "admin_print_styles-$hook", array( $this, 'admin_styles' ) );
add_action( "admin_print_scripts-$hook", array( $this, 'admin_scripts' ) );
if ( ! self::$block_page_rendering_for_idc ) {
add_action( "admin_print_styles-$hook", array( $this, 'additional_styles' ) );
}
// 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 );
}
function admin_head() {
if ( isset( $_GET['configure'] ) && Jetpack::is_module( $_GET['configure'] ) && current_user_can( 'manage_options' ) ) {
/**
* Fires in the <head> of a particular Jetpack configuation page.
*
* The dynamic portion of the hook name, `$_GET['configure']`,
* refers to the slug of module, such as 'stats', 'sso', etc.
* A complete hook for the latter would be
* 'jetpack_module_configuation_head_sso'.
*
* @since 3.0.0
*/
do_action( 'jetpack_module_configuration_head_' . $_GET['configure'] );
}
}
// Render the page with a common top and bottom part, and page specific content
function render() {
// We're in an IDC: we need a decision made before we show the UI again.
if ( self::$block_page_rendering_for_idc ) {
return;
}
$this->page_render();
}
function admin_help() {
$this->jetpack->admin_help();
}
function admin_page_load() {
// This is big. For the moment, just call the existing one.
$this->jetpack->admin_page_load();
}
function admin_page_top() {
include_once( JETPACK__PLUGIN_DIR . '_inc/header.php' );
}
function admin_page_bottom() {
include_once( JETPACK__PLUGIN_DIR . '_inc/footer.php' );
}
// Add page specific scripts and jetpack stats for all menu pages
function admin_scripts() {
$this->page_admin_scripts(); // Delegate to inheriting class
add_action( 'admin_footer', array( $this->jetpack, 'do_stats' ) );
}
// Enqueue the Jetpack admin stylesheet
function admin_styles() {
$min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
wp_enqueue_style( 'jetpack-admin', plugins_url( "css/jetpack-admin{$min}.css", JETPACK__PLUGIN_FILE ), array( 'genericons' ), JETPACK__VERSION . '-20121016' );
wp_style_add_data( 'jetpack-admin', 'rtl', 'replace' );
wp_style_add_data( 'jetpack-admin', 'suffix', $min );
}
/**
* Checks if WordPress version is too old to have REST API.
*
* @since 4.3
*
* @return bool
*/
function is_wp_version_too_old() {
global $wp_version;
return ( ! function_exists( 'rest_api_init' ) || version_compare( $wp_version, '4.4-z', '<=' ) );
}
/**
* Checks if REST API is enabled.
*
* @since 4.4.2
*
* @return bool
*/
function is_rest_api_enabled() {
return
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
apply_filters( 'rest_enabled', true ) &&
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
apply_filters( 'rest_jsonp_enabled', true ) &&
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
apply_filters( 'rest_authentication_errors', true );
}
/**
* Checks the site plan and deactivates modules that were active but are no longer included in the plan.
*
* @since 4.4.0
*
* @param $page
*
* @return array
*/
function check_plan_deactivate_modules( $page ) {
if (
Jetpack::is_development_mode()
|| ! in_array(
$page->base,
array(
'toplevel_page_jetpack',
'admin_page_jetpack_modules',
'jetpack_page_vaultpress',
'jetpack_page_stats',
'jetpack_page_akismet-key-config'
)
)
) {
return false;
}
$current = Jetpack::get_active_plan();
$to_deactivate = array();
if ( isset( $current['product_slug'] ) ) {
$active = Jetpack::get_active_modules();
switch ( $current['product_slug'] ) {
case 'jetpack_free':
$to_deactivate = array( 'seo-tools', 'videopress', 'google-analytics', 'wordads', 'search' );
break;
case 'jetpack_personal':
case 'jetpack_personal_monthly':
$to_deactivate = array( 'seo-tools', 'videopress', 'google-analytics', 'wordads', 'search' );
break;
case 'jetpack_premium':
case 'jetpack_premium_monthly':
$to_deactivate = array( 'seo-tools', 'google-analytics', 'search' );
break;
}
$to_deactivate = array_intersect( $active, $to_deactivate );
$to_leave_enabled = array();
foreach ( $to_deactivate as $feature ) {
if ( Jetpack::active_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
);
}
}

View File

@@ -0,0 +1,3 @@
<?php
// This is intentionally left empty as a stub because some sites were caching the require()
// @see https://github.com/Automattic/jetpack/issues/5091

View File

@@ -0,0 +1,400 @@
<?php
include_once( 'class.jetpack-admin-page.php' );
// Builds the landing page and its menu
class Jetpack_React_Page extends Jetpack_Admin_Page {
protected $dont_show_if_not_active = false;
protected $is_redirecting = false;
function get_page_hook() {
$title = _x( 'Jetpack', 'The menu item label', 'jetpack' );
// Add the main admin Jetpack menu
return add_menu_page( 'Jetpack', $title, 'jetpack_admin_page', 'jetpack', array( $this, 'render' ), 'div' );
}
function add_page_actions( $hook ) {
/** This action is documented in class.jetpack.php */
do_action( 'jetpack_admin_menu', $hook );
// Place the Jetpack menu item on top and others in the order they appear
add_filter( 'custom_menu_order', '__return_true' );
add_filter( 'menu_order', array( $this, 'jetpack_menu_order' ) );
if ( ! isset( $_GET['page'] ) || 'jetpack' !== $_GET['page'] || ! empty( $_GET['configure'] ) ) {
return; // No need to handle the fallback redirection if we are not on the Jetpack page
}
// Adding a redirect meta tag for older WordPress versions or if the REST API is disabled
if ( $this->is_wp_version_too_old() || ! $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' ) );
// Adding a redirect tag wrapped in browser conditional comments
add_action( 'admin_head', array( $this, 'add_legacy_browsers_head_script' ) );
}
/**
* Add Jetpack Dashboard sub-link and point it to AAG if the user can view stats, manage modules or if Protect is active.
*
* Works in Dev Mode or when user is connected.
*
* @since 4.3.0
*/
function jetpack_add_dashboard_sub_nav_item() {
if ( Jetpack::is_development_mode() || Jetpack::is_active() ) {
global $submenu;
if ( current_user_can( 'jetpack_admin_page' ) ) {
$submenu['jetpack'][] = array( __( 'Dashboard', 'jetpack' ), 'jetpack_admin_page', 'admin.php?page=jetpack#/dashboard' );
}
}
}
/**
* If user is allowed to see the Jetpack Admin, add Settings sub-link.
*
* @since 4.3.0
*/
function jetpack_add_settings_sub_nav_item() {
if ( ( Jetpack::is_development_mode() || Jetpack::is_active() ) && current_user_can( 'jetpack_admin_page' ) && current_user_can( 'edit_posts' ) ) {
global $submenu;
$submenu['jetpack'][] = array( __( 'Settings', 'jetpack' ), 'jetpack_admin_page', 'admin.php?page=jetpack#/settings' );
}
}
function add_fallback_head_meta() {
echo '<meta http-equiv="refresh" content="0; url=?page=jetpack_modules">';
}
function add_noscript_head_meta() {
echo '<noscript>';
$this->add_fallback_head_meta();
echo '</noscript>';
}
function add_legacy_browsers_head_script() {
echo
"<script type=\"text/javascript\">\n"
. "/*@cc_on\n"
. "if ( @_jscript_version <= 10) {\n"
. "window.location.href = '?page=jetpack_modules';\n"
. "}\n"
. "@*/\n"
. "</script>";
}
function jetpack_menu_order( $menu_order ) {
$jp_menu_order = array();
foreach ( $menu_order as $index => $item ) {
if ( $item != 'jetpack' )
$jp_menu_order[] = $item;
if ( $index == 0 )
$jp_menu_order[] = 'jetpack';
}
return $jp_menu_order;
}
// Render the configuration page for the module if it exists and an error
// screen if the module is not configurable
// @todo remove when real settings are in place
function render_nojs_configurable( $module_name ) {
$module_name = preg_replace( '/[^\da-z\-]+/', '', $_GET['configure'] );
include_once( JETPACK__PLUGIN_DIR . '_inc/header.php' );
echo '<div class="wrap configure-module">';
if ( Jetpack::is_module( $module_name ) && current_user_can( 'jetpack_configure_modules' ) ) {
Jetpack::admin_screen_configure_module( $module_name );
} else {
echo '<h2>' . esc_html__( 'Error, bad module.', 'jetpack' ) . '</h2>';
}
echo '</div><!-- /wrap -->';
}
function page_render() {
// Handle redirects to configuration pages
if ( ! empty( $_GET['configure'] ) ) {
return $this->render_nojs_configurable( $_GET['configure'] );
}
/** This action is already documented in views/admin/admin-page.php */
do_action( 'jetpack_notices' );
// Try fetching by patch
$static_html = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static.html' );
if ( false === $static_html ) {
// If we still have nothing, display an error
echo '<p>';
esc_html_e( 'Error fetching static.html. Try running: ', 'jetpack' );
echo '<code>yarn distclean && yarn build</code>';
echo '</p>';
} else {
// We got the static.html so let's display it
echo $static_html;
}
}
/**
* Gets array of any Jetpack notices that have been dismissed.
*
* @since 4.0.1
* @return mixed|void
*/
function get_dismissed_jetpack_notices() {
$jetpack_dismissed_notices = get_option( 'jetpack_dismissed_notices', array() );
/**
* Array of notices that have been dismissed.
*
* @since 4.0.1
*
* @param array $jetpack_dismissed_notices If empty, will not show any Jetpack notices.
*/
$dismissed_notices = apply_filters( 'jetpack_dismissed_notices', $jetpack_dismissed_notices );
return $dismissed_notices;
}
function additional_styles() {
$rtl = is_rtl() ? '.rtl' : '';
wp_enqueue_style( 'dops-css', plugins_url( "_inc/build/admin.dops-style$rtl.css", JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
wp_enqueue_style( 'components-css', plugins_url( "_inc/build/style.min$rtl.css", JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
}
function page_admin_scripts() {
if ( $this->is_redirecting ) {
return; // No need for scripts on a fallback page
}
$is_dev_mode = Jetpack::is_development_mode();
// Enqueue jp.js and localize it
wp_enqueue_script( 'react-plugin', plugins_url( '_inc/build/admin.js', JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION, true );
if ( ! $is_dev_mode && Jetpack::is_active() ) {
// Required for Analytics
wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
}
// Collecting roles that can view site stats.
$stats_roles = array();
$enabled_roles = function_exists( 'stats_get_option' ) ? stats_get_option( 'roles' ) : array( 'administrator' );
if ( ! function_exists( 'get_editable_roles' ) ) {
require_once ABSPATH . 'wp-admin/includes/user.php';
}
foreach ( get_editable_roles() as $slug => $role ) {
$stats_roles[ $slug ] = array(
'name' => translate_user_role( $role['name'] ),
'canView' => is_array( $enabled_roles ) ? in_array( $slug, $enabled_roles, true ) : false,
);
}
// Load API endpoint base classes and endpoints for getting the module list fed into the JS Admin Page
require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php';
require_once JETPACK__PLUGIN_DIR . '_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php';
$moduleListEndpoint = new Jetpack_Core_API_Module_List_Endpoint();
$modules = $moduleListEndpoint->get_modules();
// Preparing translated fields for JSON encoding by transforming all HTML entities to
// respective characters.
foreach( $modules as $slug => $data ) {
$modules[ $slug ]['name'] = html_entity_decode( $data['name'] );
$modules[ $slug ]['description'] = html_entity_decode( $data['description'] );
$modules[ $slug ]['short_description'] = html_entity_decode( $data['short_description'] );
$modules[ $slug ]['long_description'] = html_entity_decode( $data['long_description'] );
}
// 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();
// 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' );
}
}
// Add objects to be passed to the initial state of the app
wp_localize_script( 'react-plugin', 'Initial_State', array(
'WP_API_root' => esc_url_raw( rest_url() ),
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
'pluginBaseUrl' => plugins_url( '', JETPACK__PLUGIN_FILE ),
'connectionStatus' => array(
'isActive' => Jetpack::is_active(),
'isStaging' => Jetpack::is_staging_site(),
'devMode' => array(
'isActive' => $is_dev_mode,
'constant' => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
'url' => site_url() && false === strpos( site_url(), '.' ),
'filter' => apply_filters( 'jetpack_development_mode', false ),
),
'isPublic' => '1' == get_option( 'blog_public' ),
'isInIdentityCrisis' => Jetpack::validate_sync_error_idc_option(),
),
'connectUrl' => Jetpack::init()->build_connect_url( true, false, false ),
'dismissedNotices' => $this->get_dismissed_jetpack_notices(),
'isDevVersion' => Jetpack::is_development_version(),
'currentVersion' => JETPACK__VERSION,
'getModules' => $modules,
'showJumpstart' => jetpack_show_jumpstart(),
'rawUrl' => Jetpack::build_raw_urls( get_home_url() ),
'adminUrl' => esc_url( admin_url() ),
'stats' => array(
// data is populated asynchronously on page load
'data' => array(
'general' => false,
'day' => false,
'week' => false,
'month' => false,
),
'roles' => $stats_roles,
),
'settings' => $this->get_flattened_settings( $modules ),
'userData' => array(
// 'othersLinked' => Jetpack::get_other_linked_admins(),
'currentUser' => jetpack_current_user_data(),
),
'siteData' => array(
'icon' => has_site_icon()
? apply_filters( 'jetpack_photon_url', get_site_icon_url(), array( 'w' => 64 ) )
: '',
'siteVisibleToSearchEngines' => '1' == get_option( 'blog_public' ),
/**
* Whether promotions are visible or not.
*
* @since 4.8.0
*
* @param bool $are_promotions_active Status of promotions visibility. True by default.
*/
'showPromotions' => apply_filters( 'jetpack_show_promotions', true ),
'isAtomicSite' => jetpack_is_atomic_site(),
'plan' => Jetpack::get_active_plan(),
),
'themeData' => array(
'name' => $current_theme->get( 'Name' ),
'hasUpdate' => (bool) get_theme_update_available( $current_theme ),
'support' => array(
'infinite-scroll' => current_theme_supports( 'infinite-scroll' ) || in_array( $current_theme->get_stylesheet(), $inf_scr_support_themes ),
),
),
'locale' => Jetpack::get_i18n_data_json(),
'localeSlug' => join( '-', explode( '_', jetpack_get_user_locale() ) ),
'jetpackStateNotices' => array(
'messageCode' => Jetpack::state( 'message' ),
'errorCode' => Jetpack::state( 'error' ),
'errorDescription' => Jetpack::state( 'error_description' ),
),
'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(),
'currentIp' => function_exists( 'jetpack_protect_get_ip' ) ? jetpack_protect_get_ip() : false,
'lastPostUrl' => esc_url( $last_post ),
) );
}
/**
* Returns an array of modules and settings both as first class members of the object.
*
* @param array $modules the result of an API request to get all modules.
*
* @return array flattened settings with modules.
*/
function get_flattened_settings( $modules ) {
$core_api_endpoint = new Jetpack_Core_API_Data();
$settings = $core_api_endpoint->get_all_options();
return $settings->data;
}
}
/*
* Only show Jump Start on first activation.
* Any option 'jumpstart' other than 'new connection' will hide it.
*
* The option can be of 4 things, and will be stored as such:
* new_connection : Brand new connection - Show
* jumpstart_activated : Jump Start has been activated - dismiss
* jetpack_action_taken: Manual activation of a module already happened - dismiss
* jumpstart_dismissed : Manual dismissal of Jump Start - dismiss
*
* @todo move to functions.global.php when available
* @since 3.6
* @return bool | show or hide
*/
function jetpack_show_jumpstart() {
if ( ! Jetpack::is_active() ) {
return false;
}
$jumpstart_option = Jetpack_Options::get_option( 'jumpstart' );
$hide_options = array(
'jumpstart_activated',
'jetpack_action_taken',
'jumpstart_dismissed'
);
if ( ! $jumpstart_option || in_array( $jumpstart_option, $hide_options ) ) {
return false;
}
return true;
}
/**
* Gather data about the current user.
*
* @since 4.1.0
*
* @return array
*/
function jetpack_current_user_data() {
$current_user = wp_get_current_user();
$is_master_user = $current_user->ID == Jetpack_Options::get_option( 'master_user' );
$dotcom_data = Jetpack::get_connected_user_data();
// Add connected user gravatar to the returned dotcom_data.
$dotcom_data['avatar'] = get_avatar_url( $dotcom_data['email'], array( 'size' => 64, 'default' => 'mysteryman' ) );
$current_user_data = array(
'isConnected' => Jetpack::is_user_connected( $current_user->ID ),
'isMaster' => $is_master_user,
'username' => $current_user->user_login,
'id' => $current_user->ID,
'wpcomUser' => $dotcom_data,
'gravatar' => get_avatar( $current_user->ID, 40, 'mm', '', array( 'force_display' => true ) ),
'permissions' => array(
'admin_page' => current_user_can( 'jetpack_admin_page' ),
'connect' => current_user_can( 'jetpack_connect' ),
'disconnect' => current_user_can( 'jetpack_disconnect' ),
'manage_modules' => current_user_can( 'jetpack_manage_modules' ),
'network_admin' => current_user_can( 'jetpack_network_admin_page' ),
'network_sites_page' => current_user_can( 'jetpack_network_sites_page' ),
'edit_posts' => current_user_can( 'edit_posts' ),
'publish_posts' => current_user_can( 'publish_posts' ),
'manage_options' => current_user_can( 'manage_options' ),
'view_stats' => current_user_can( 'view_stats' ),
'manage_plugins' => current_user_can( 'install_plugins' )
&& current_user_can( 'activate_plugins' )
&& current_user_can( 'update_plugins' )
&& current_user_can( 'delete_plugins' ),
),
);
return $current_user_data;
}

View File

@@ -0,0 +1,189 @@
<?php
include_once( 'class.jetpack-admin-page.php' );
include_once( JETPACK__PLUGIN_DIR . 'class.jetpack-modules-list-table.php' );
// Builds the settings page and its menu
class Jetpack_Settings_Page extends Jetpack_Admin_Page {
// Show the settings page only when Jetpack is connected or in dev mode
protected $dont_show_if_not_active = true;
function add_page_actions( $hook ) {}
// Adds the Settings sub menu
function get_page_hook() {
return add_submenu_page( null, __( 'Jetpack Settings', 'jetpack' ), __( 'Settings', 'jetpack' ), 'jetpack_manage_modules', 'jetpack_modules', array( $this, 'render' ) );
}
// Renders the module list table where you can use bulk action or row
// actions to activate/deactivate and configure modules
function page_render() {
$list_table = new Jetpack_Modules_List_Table;
$static_html = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static.html' );
// If static.html isn't there, there's nothing else we can do.
if ( false === $static_html ) {
echo '<p>';
esc_html_e( 'Error fetching static.html. Try running: ', 'jetpack' );
echo '<code>yarn distclean && yarn build</code>';
echo '</p>';
return;
}
// 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' );
$version_notice = $rest_api_notice = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static-version-notice.html' );
$ie_notice = @file_get_contents( JETPACK__PLUGIN_DIR . '_inc/build/static-ie-notice.html' );
$noscript_notice = str_replace(
'#HEADER_TEXT#',
esc_html__( 'You have JavaScript disabled', 'jetpack' ),
$noscript_notice
);
$noscript_notice = str_replace(
'#TEXT#',
esc_html__( "Turn on JavaScript to unlock Jetpack's full potential!", 'jetpack' ),
$noscript_notice
);
$version_notice = str_replace(
'#HEADER_TEXT#',
esc_html__( 'You are using an outdated version of WordPress', 'jetpack' ),
$version_notice
);
$version_notice = str_replace(
'#TEXT#',
esc_html__( "Update WordPress to unlock Jetpack's full potential!", 'jetpack' ),
$version_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
);
$ie_notice = str_replace(
'#HEADER_TEXT#',
esc_html__( 'You are using an unsupported browser version.', 'jetpack' ),
$ie_notice
);
$ie_notice = str_replace(
'#TEXT#',
esc_html__( "Update your browser to unlock Jetpack's full potential!", 'jetpack' ),
$ie_notice
);
ob_start();
$this->admin_page_top();
if ( $this->is_wp_version_too_old() ) {
echo $version_notice;
}
if ( ! $this->is_rest_api_enabled() ) {
echo $rest_api_notice;
}
echo $noscript_notice;
echo $ie_notice;
?>
<div class="page-content configure">
<div class="frame top hide-if-no-js">
<div class="wrap">
<div class="manage-left">
<table class="table table-bordered fixed-top">
<thead>
<tr>
<th class="check-column"><input type="checkbox" class="checkall"></th>
<th colspan="2">
<?php $list_table->unprotected_display_tablenav( 'top' ); ?>
<span class="filter-search">
<button type="button" class="button">Filter</button>
</span>
</th>
</tr>
</thead>
</table>
</div>
</div><!-- /.wrap -->
</div><!-- /.frame -->
<div class="frame bottom">
<div class="wrap">
<div class="manage-right" style="display: none;">
<div class="bumper">
<form class="navbar-form" role="search">
<input type="hidden" name="page" value="jetpack_modules" />
<?php $list_table->search_box( __( 'Search', 'jetpack' ), 'srch-term' ); ?>
<p><?php esc_html_e( 'View:', 'jetpack' ); ?></p>
<div class="button-group filter-active">
<button type="button" class="button <?php if ( empty( $_GET['activated'] ) ) echo 'active'; ?>"><?php esc_html_e( 'All', 'jetpack' ); ?></button>
<button type="button" class="button <?php if ( ! empty( $_GET['activated'] ) && 'true' == $_GET['activated'] ) echo 'active'; ?>" data-filter-by="activated" data-filter-value="true"><?php esc_html_e( 'Active', 'jetpack' ); ?></button>
<button type="button" class="button <?php if ( ! empty( $_GET['activated'] ) && 'false' == $_GET['activated'] ) echo 'active'; ?>" data-filter-by="activated" data-filter-value="false"><?php esc_html_e( 'Inactive', 'jetpack' ); ?></button>
</div>
<p><?php esc_html_e( 'Sort by:', 'jetpack' ); ?></p>
<div class="button-group sort">
<button type="button" class="button <?php if ( empty( $_GET['sort_by'] ) ) echo 'active'; ?>" data-sort-by="name"><?php esc_html_e( 'Alphabetical', 'jetpack' ); ?></button>
<button type="button" class="button <?php if ( ! empty( $_GET['sort_by'] ) && 'introduced' == $_GET['sort_by'] ) echo 'active'; ?>" data-sort-by="introduced" data-sort-order="reverse"><?php esc_html_e( 'Newest', 'jetpack' ); ?></button>
<button type="button" class="button <?php if ( ! empty( $_GET['sort_by'] ) && 'sort' == $_GET['sort_by'] ) echo 'active'; ?>" data-sort-by="sort"><?php esc_html_e( 'Popular', 'jetpack' ); ?></button>
</div>
<p><?php esc_html_e( 'Show:', 'jetpack' ); ?></p>
<?php $list_table->views(); ?>
</form>
</div>
</div>
<div class="manage-left" style="width: 100%;">
<form class="jetpack-modules-list-table-form" onsubmit="return false;">
<table class="<?php echo implode( ' ', $list_table->get_table_classes() ); ?>">
<tbody id="the-list">
<?php $list_table->display_rows_or_placeholder(); ?>
</tbody>
</table>
</form>
</div>
</div><!-- /.wrap -->
</div><!-- /.frame -->
</div><!-- /.content -->
<?php
$this->admin_page_bottom();
$page_content = ob_get_contents();
ob_end_clean();
echo str_replace(
'<div class="jp-loading-placeholder"><span class="dashicons dashicons-wordpress-alt"></span></div>',
$page_content,
$static_html
);
JetpackTracking::record_user_event( 'wpa_page_view', array( 'path' => 'old_settings' ) );
}
/**
* Load styles for static page.
*
* @since 4.3.0
*/
function additional_styles() {
$rtl = is_rtl() ? '.rtl' : '';
wp_enqueue_style( 'dops-css', plugins_url( "_inc/build/admin.dops-style$rtl.css", JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
wp_enqueue_style( 'components-css', plugins_url( "_inc/build/style.min$rtl.css", JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
}
// Javascript logic specific to the list table
function page_admin_scripts() {
wp_enqueue_script(
'jetpack-admin-js',
Jetpack::get_file_url_for_environment( '_inc/build/jetpack-admin.min.js', '_inc/jetpack-admin.js' ),
array( 'jquery' ),
JETPACK__VERSION
);
}
}

View File

@@ -0,0 +1,755 @@
<?php
/**
* Color utility and conversion
*
* Represents a color value, and converts between RGB/HSV/XYZ/Lab/HSL
*
* Example:
* $color = new Jetpack_Color(0xFFFFFF);
*
* @author Harold Asbridge <hasbridge@gmail.com>
* @author Matt Wiebe <wiebe@automattic.com>
* @license http://www.opensource.org/licenses/MIT
*/
class Jetpack_Color {
/**
* @var int
*/
protected $color = 0;
/**
* Initialize object
*
* @param string|array $color A color of the type $type
* @param string $type The type of color we will construct from.
* One of hex (default), rgb, hsl, int
*/
public function __construct( $color = null, $type = 'hex' ) {
if ( $color ) {
switch ( $type ) {
case 'hex':
$this->fromHex( $color );
break;
case 'rgb':
if ( is_array( $color ) && count( $color ) == 3 ) {
list( $r, $g, $b ) = array_values( $color );
$this->fromRgbInt( $r, $g, $b );
}
break;
case 'hsl':
if ( is_array( $color ) && count( $color ) == 3 ) {
list( $h, $s, $l ) = array_values( $color );
$this->fromHsl( $h, $s, $l );
}
break;
case 'int':
$this->fromInt( $color );
break;
default:
// there is no default.
break;
}
}
}
/**
* Init color from hex value
*
* @param string $hexValue
*
* @return Jetpack_Color
*/
public function fromHex($hexValue) {
$hexValue = str_replace( '#', '', $hexValue );
// handle short hex codes like #fff
if ( 3 === strlen( $hexValue ) ) {
$short = $hexValue;
$i = 0;
$hexValue = '';
while ( $i < 3 ) {
$chunk = substr($short, $i, 1 );
$hexValue .= $chunk . $chunk;
$i++;
}
}
$intValue = hexdec( $hexValue );
if ( $intValue < 0 || $intValue > 16777215 ) {
throw new RangeException( $hexValue . " out of valid color code range" );
}
$this->color = $intValue;
return $this;
}
/**
* Init color from integer RGB values
*
* @param int $red
* @param int $green
* @param int $blue
*
* @return Jetpack_Color
*/
public function fromRgbInt($red, $green, $blue)
{
if ( $red < 0 || $red > 255 )
throw new RangeException( "Red value " . $red . " out of valid color code range" );
if ( $green < 0 || $green > 255 )
throw new RangeException( "Green value " . $green . " out of valid color code range" );
if ( $blue < 0 || $blue > 255 )
throw new RangeException( "Blue value " . $blue . " out of valid color code range" );
$this->color = (int)(($red << 16) + ($green << 8) + $blue);
return $this;
}
/**
* Init color from hex RGB values
*
* @param string $red
* @param string $green
* @param string $blue
*
* @return Jetpack_Color
*/
public function fromRgbHex($red, $green, $blue)
{
return $this->fromRgbInt(hexdec($red), hexdec($green), hexdec($blue));
}
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* @param int $h Hue. [0-360]
* @param in $s Saturation [0, 100]
* @param int $l Lightness [0, 100]
*/
public function fromHsl( $h, $s, $l ) {
$h /= 360; $s /= 100; $l /= 100;
if ( $s == 0 ) {
$r = $g = $b = $l; // achromatic
}
else {
$q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s;
$p = 2 * $l - $q;
$r = $this->hue2rgb( $p, $q, $h + 1/3 );
$g = $this->hue2rgb( $p, $q, $h );
$b = $this->hue2rgb( $p, $q, $h - 1/3 );
}
return $this->fromRgbInt( $r * 255, $g * 255, $b * 255 );
}
/**
* Helper function for Jetpack_Color::fromHsl()
*/
private function hue2rgb( $p, $q, $t ) {
if ( $t < 0 ) $t += 1;
if ( $t > 1 ) $t -= 1;
if ( $t < 1/6 ) return $p + ( $q - $p ) * 6 * $t;
if ( $t < 1/2 ) return $q;
if ( $t < 2/3 ) return $p + ( $q - $p ) * ( 2/3 - $t ) * 6;
return $p;
}
/**
* Init color from integer value
*
* @param int $intValue
*
* @return Jetpack_Color
*/
public function fromInt($intValue)
{
if ( $intValue < 0 || $intValue > 16777215 )
throw new RangeException( $intValue . " out of valid color code range" );
$this->color = $intValue;
return $this;
}
/**
* Convert color to hex
*
* @return string
*/
public function toHex()
{
return str_pad(dechex($this->color), 6, '0', STR_PAD_LEFT);
}
/**
* Convert color to RGB array (integer values)
*
* @return array
*/
public function toRgbInt()
{
return array(
'red' => (int)(255 & ($this->color >> 16)),
'green' => (int)(255 & ($this->color >> 8)),
'blue' => (int)(255 & ($this->color))
);
}
/**
* Convert color to RGB array (hex values)
*
* @return array
*/
public function toRgbHex()
{
$r = array();
foreach ($this->toRgbInt() as $item) {
$r[] = dechex($item);
}
return $r;
}
/**
* Get Hue/Saturation/Value for the current color
* (float values, slow but accurate)
*
* @return array
*/
public function toHsvFloat()
{
$rgb = $this->toRgbInt();
$rgbMin = min($rgb);
$rgbMax = max($rgb);
$hsv = array(
'hue' => 0,
'sat' => 0,
'val' => $rgbMax
);
// If v is 0, color is black
if ($hsv['val'] == 0) {
return $hsv;
}
// Normalize RGB values to 1
$rgb['red'] /= $hsv['val'];
$rgb['green'] /= $hsv['val'];
$rgb['blue'] /= $hsv['val'];
$rgbMin = min($rgb);
$rgbMax = max($rgb);
// Calculate saturation
$hsv['sat'] = $rgbMax - $rgbMin;
if ($hsv['sat'] == 0) {
$hsv['hue'] = 0;
return $hsv;
}
// Normalize saturation to 1
$rgb['red'] = ($rgb['red'] - $rgbMin) / ($rgbMax - $rgbMin);
$rgb['green'] = ($rgb['green'] - $rgbMin) / ($rgbMax - $rgbMin);
$rgb['blue'] = ($rgb['blue'] - $rgbMin) / ($rgbMax - $rgbMin);
$rgbMin = min($rgb);
$rgbMax = max($rgb);
// Calculate hue
if ($rgbMax == $rgb['red']) {
$hsv['hue'] = 0.0 + 60 * ($rgb['green'] - $rgb['blue']);
if ($hsv['hue'] < 0) {
$hsv['hue'] += 360;
}
} else if ($rgbMax == $rgb['green']) {
$hsv['hue'] = 120 + (60 * ($rgb['blue'] - $rgb['red']));
} else {
$hsv['hue'] = 240 + (60 * ($rgb['red'] - $rgb['green']));
}
return $hsv;
}
/**
* Get HSV values for color
* (integer values from 0-255, fast but less accurate)
*
* @return int
*/
public function toHsvInt()
{
$rgb = $this->toRgbInt();
$rgbMin = min($rgb);
$rgbMax = max($rgb);
$hsv = array(
'hue' => 0,
'sat' => 0,
'val' => $rgbMax
);
// If value is 0, color is black
if ($hsv['val'] == 0) {
return $hsv;
}
// Calculate saturation
$hsv['sat'] = round(255 * ($rgbMax - $rgbMin) / $hsv['val']);
if ($hsv['sat'] == 0) {
$hsv['hue'] = 0;
return $hsv;
}
// Calculate hue
if ($rgbMax == $rgb['red']) {
$hsv['hue'] = round(0 + 43 * ($rgb['green'] - $rgb['blue']) / ($rgbMax - $rgbMin));
} else if ($rgbMax == $rgb['green']) {
$hsv['hue'] = round(85 + 43 * ($rgb['blue'] - $rgb['red']) / ($rgbMax - $rgbMin));
} else {
$hsv['hue'] = round(171 + 43 * ($rgb['red'] - $rgb['green']) / ($rgbMax - $rgbMin));
}
if ($hsv['hue'] < 0) {
$hsv['hue'] += 255;
}
return $hsv;
}
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h in [0, 360], s in [0, 100], l in [0, 100]
*
* @return Array The HSL representation
*/
public function toHsl() {
list( $r, $g, $b ) = array_values( $this->toRgbInt() );
$r /= 255; $g /= 255; $b /= 255;
$max = max( $r, $g, $b );
$min = min( $r, $g, $b );
$h = $s = $l = ( $max + $min ) / 2;
#var_dump( array( compact('max', 'min', 'r', 'g', 'b')) );
if ( $max == $min ) {
$h = $s = 0; // achromatic
}
else {
$d = $max - $min;
$s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min );
switch ( $max ) {
case $r:
$h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 );
break;
case $g:
$h = ( $b - $r ) / $d + 2;
break;
case $b:
$h = ( $r - $g ) / $d + 4;
break;
}
$h /= 6;
}
$h = (int) round( $h * 360 );
$s = (int) round( $s * 100 );
$l = (int) round( $l * 100 );
return compact( 'h', 's', 'l' );
}
public function toCSS( $type = 'hex', $alpha = 1 ) {
switch ( $type ) {
case 'hex':
return $this->toString();
break;
case 'rgb':
case 'rgba':
list( $r, $g, $b ) = array_values( $this->toRgbInt() );
if ( is_numeric( $alpha ) && $alpha < 1 ) {
return "rgba( {$r}, {$g}, {$b}, $alpha )";
}
else {
return "rgb( {$r}, {$g}, {$b} )";
}
break;
case 'hsl':
case 'hsla':
list( $h, $s, $l ) = array_values( $this->toHsl() );
if ( is_numeric( $alpha ) && $alpha < 1 ) {
return "hsla( {$h}, {$s}, {$l}, $alpha )";
}
else {
return "hsl( {$h}, {$s}, {$l} )";
}
break;
default:
return $this->toString();
break;
}
}
/**
* Get current color in XYZ format
*
* @return array
*/
public function toXyz()
{
$rgb = $this->toRgbInt();
// Normalize RGB values to 1
$rgb_new = array();
foreach ($rgb as $item) {
$rgb_new[] = $item / 255;
}
$rgb = $rgb_new;
$rgb_new = array();
foreach ($rgb as $item) {
if ($item > 0.04045) {
$item = pow((($item + 0.055) / 1.055), 2.4);
} else {
$item = $item / 12.92;
}
$rgb_new[] = $item * 100;
}
$rgb = $rgb_new;
// Observer. = 2°, Illuminant = D65
$xyz = array(
'x' => ($rgb['red'] * 0.4124) + ($rgb['green'] * 0.3576) + ($rgb['blue'] * 0.1805),
'y' => ($rgb['red'] * 0.2126) + ($rgb['green'] * 0.7152) + ($rgb['blue'] * 0.0722),
'z' => ($rgb['red'] * 0.0193) + ($rgb['green'] * 0.1192) + ($rgb['blue'] * 0.9505)
);
return $xyz;
}
/**
* Get color CIE-Lab values
*
* @return array
*/
public function toLabCie()
{
$xyz = $this->toXyz();
//Ovserver = 2*, Iluminant=D65
$xyz['x'] /= 95.047;
$xyz['y'] /= 100;
$xyz['z'] /= 108.883;
$xyz_new = array();
foreach ($xyz as $item) {
if ($item > 0.008856) {
$xyz_new[] = pow($item, 1/3);
} else {
$xyz_new[] = (7.787 * $item) + (16 / 116);
}
}
$xyz = $xyz_new;
$lab = array(
'l' => (116 * $xyz['y']) - 16,
'a' => 500 * ($xyz['x'] - $xyz['y']),
'b' => 200 * ($xyz['y'] - $xyz['z'])
);
return $lab;
}
/**
* Convert color to integer
*
* @return int
*/
public function toInt()
{
return $this->color;
}
/**
* Alias of toString()
*
* @return string
*/
public function __toString()
{
return $this->toString();
}
/**
* Get color as string
*
* @return string
*/
public function toString()
{
$str = $this->toHex();
return strtoupper("#{$str}");
}
/**
* Get the distance between this color and the given color
*
* @param Jetpack_Color $color
*
* @return int
*/
public function getDistanceRgbFrom(Jetpack_Color $color)
{
$rgb1 = $this->toRgbInt();
$rgb2 = $color->toRgbInt();
$rDiff = abs($rgb1['red'] - $rgb2['red']);
$gDiff = abs($rgb1['green'] - $rgb2['green']);
$bDiff = abs($rgb1['blue'] - $rgb2['blue']);
// Sum of RGB differences
$diff = $rDiff + $gDiff + $bDiff;
return $diff;
}
/**
* Get distance from the given color using the Delta E method
*
* @param Jetpack_Color $color
*
* @return float
*/
public function getDistanceLabFrom(Jetpack_Color $color)
{
$lab1 = $this->toLabCie();
$lab2 = $color->toLabCie();
$lDiff = abs($lab2['l'] - $lab1['l']);
$aDiff = abs($lab2['a'] - $lab1['a']);
$bDiff = abs($lab2['b'] - $lab1['b']);
$delta = sqrt($lDiff + $aDiff + $bDiff);
return $delta;
}
public function toLuminosity() {
$lum = array();
foreach( $this->toRgbInt() as $slot => $value ) {
$chan = $value / 255;
$lum[ $slot ] = ( $chan <= 0.03928 ) ? $chan / 12.92 : pow( ( ( $chan + 0.055 ) / 1.055 ), 2.4 );
}
return 0.2126 * $lum['red'] + 0.7152 * $lum['green'] + 0.0722 * $lum['blue'];
}
/**
* Get distance between colors using luminance.
* Should be more than 5 for readable contrast
*
* @param Jetpack_Color $color Another color
* @return float
*/
public function getDistanceLuminosityFrom( Jetpack_Color $color ) {
$L1 = $this->toLuminosity();
$L2 = $color->toLuminosity();
if ( $L1 > $L2 ) {
return ( $L1 + 0.05 ) / ( $L2 + 0.05 );
}
else{
return ( $L2 + 0.05 ) / ( $L1 + 0.05 );
}
}
public function getMaxContrastColor() {
$withBlack = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#000') );
$withWhite = $this->getDistanceLuminosityFrom( new Jetpack_Color( '#fff') );
$color = new Jetpack_Color;
$hex = ( $withBlack >= $withWhite ) ? '#000000' : '#ffffff';
return $color->fromHex( $hex );
}
public function getGrayscaleContrastingColor( $contrast = false ) {
if ( ! $contrast ) {
return $this->getMaxContrastColor();
}
// don't allow less than 5
$target_contrast = ( $contrast < 5 ) ? 5 : $contrast;
$color = $this->getMaxContrastColor();
$contrast = $color->getDistanceLuminosityFrom( $this );
// if current max contrast is less than the target contrast, we had wishful thinking.
if ( $contrast <= $target_contrast ) {
return $color;
}
$incr = ( '#000000' === $color->toString() ) ? 1 : -1;
while ( $contrast > $target_contrast ) {
$color = $color->incrementLightness( $incr );
$contrast = $color->getDistanceLuminosityFrom( $this );
}
return $color;
}
/**
* Gets a readable contrasting color. $this is assumed to be the text and $color the background color.
* @param object $bg_color A Color object that will be compared against $this
* @param integer $min_contrast The minimum contrast to achieve, if possible.
* @return object A Color object, an increased contrast $this compared against $bg_color
*/
public function getReadableContrastingColor( $bg_color = false, $min_contrast = 5 ) {
if ( ! $bg_color || ! is_a( $bg_color, 'Jetpack_Color' ) ) {
return $this;
}
// you shouldn't use less than 5, but you might want to.
$target_contrast = $min_contrast;
// working things
$contrast = $bg_color->getDistanceLuminosityFrom( $this );
$max_contrast_color = $bg_color->getMaxContrastColor();
$max_contrast = $max_contrast_color->getDistanceLuminosityFrom( $bg_color );
// if current max contrast is less than the target contrast, we had wishful thinking.
// still, go max
if ( $max_contrast <= $target_contrast ) {
return $max_contrast_color;
}
// or, we might already have sufficient contrast
if ( $contrast >= $target_contrast ) {
return $this;
}
$incr = ( 0 === $max_contrast_color->toInt() ) ? -1 : 1;
while ( $contrast < $target_contrast ) {
$this->incrementLightness( $incr );
$contrast = $bg_color->getDistanceLuminosityFrom( $this );
// infininite loop prevention: you never know.
if ( $this->color === 0 || $this->color === 16777215 ) {
break;
}
}
return $this;
}
/**
* Detect if color is grayscale
*
* @param int @threshold
*
* @return bool
*/
public function isGrayscale($threshold = 16)
{
$rgb = $this->toRgbInt();
// Get min and max rgb values, then difference between them
$rgbMin = min($rgb);
$rgbMax = max($rgb);
$diff = $rgbMax - $rgbMin;
return $diff < $threshold;
}
/**
* Get the closest matching color from the given array of colors
*
* @param array $colors array of integers or Jetpack_Color objects
*
* @return mixed the array key of the matched color
*/
public function getClosestMatch(array $colors)
{
$matchDist = 10000;
$matchKey = null;
foreach($colors as $key => $color) {
if (false === ($color instanceof Jetpack_Color)) {
$c = new Jetpack_Color($color);
}
$dist = $this->getDistanceLabFrom($c);
if ($dist < $matchDist) {
$matchDist = $dist;
$matchKey = $key;
}
}
return $matchKey;
}
/* TRANSFORMS */
public function darken( $amount = 5 ) {
return $this->incrementLightness( - $amount );
}
public function lighten( $amount = 5 ) {
return $this->incrementLightness( $amount );
}
public function incrementLightness( $amount ) {
$hsl = $this->toHsl();
extract( $hsl );
$l += $amount;
if ( $l < 0 ) $l = 0;
if ( $l > 100 ) $l = 100;
return $this->fromHsl( $h, $s, $l );
}
public function saturate( $amount = 15 ) {
return $this->incrementSaturation( $amount );
}
public function desaturate( $amount = 15 ) {
return $this->incrementSaturation( - $amount );
}
public function incrementSaturation( $amount ) {
$hsl = $this->toHsl();
extract( $hsl );
$s += $amount;
if ( $s < 0 ) $s = 0;
if ( $s > 100 ) $s = 100;
return $this->fromHsl( $h, $s, $l );
}
public function toGrayscale() {
$hsl = $this->toHsl();
extract( $hsl );
$s = 0;
return $this->fromHsl( $h, $s, $l );
}
public function getComplement() {
return $this->incrementHue( 180 );
}
public function getSplitComplement( $step = 1 ) {
$incr = 180 + ( $step * 30 );
return $this->incrementHue( $incr );
}
public function getAnalog( $step = 1 ) {
$incr = $step * 30;
return $this->incrementHue( $incr );
}
public function getTetrad( $step = 1 ) {
$incr = $step * 60;
return $this->incrementHue( $incr );
}
public function getTriad( $step = 1 ) {
$incr = $step * 120;
return $this->incrementHue( $incr );
}
public function incrementHue( $amount ) {
$hsl = $this->toHsl();
extract( $hsl );
$h = ( $h + $amount ) % 360;
if ( $h < 0 ) $h = 360 - $h;
return $this->fromHsl( $h, $s, $l );
}
} // class Jetpack_Color

View File

@@ -0,0 +1,111 @@
<?php
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . 'wp-admin/includes/file.php';
/**
* Allows us to capture that the site doesn't have proper file system access.
* In order to update the plugin.
*/
class Jetpack_Automatic_Install_Skin extends Automatic_Upgrader_Skin {
/**
* Stores the last error key;
**/
protected $main_error_code = 'install_error';
/**
* Stores the last error message.
**/
protected $main_error_message = 'An unknown error occurred during installation';
/**
* Overwrites the set_upgrader to be able to tell if we e ven have the ability to write to the files.
*
* @param WP_Upgrader $upgrader
*
*/
public function set_upgrader( &$upgrader ) {
parent::set_upgrader( $upgrader );
// Check if we even have permission to.
$result = $upgrader->fs_connect( array( WP_CONTENT_DIR, WP_PLUGIN_DIR ) );
if ( ! $result ) {
// set the string here since they are not available just yet
$upgrader->generic_strings();
$this->feedback( 'fs_unavailable' );
}
}
/**
* Overwrites the error function
*/
public function error( $error ) {
if ( is_wp_error( $error ) ) {
$this->feedback( $error );
}
}
private function set_main_error_code( $code ) {
// Don't set the process_failed as code since it is not that helpful unless we don't have one already set.
$this->main_error_code = ( $code === 'process_failed' && $this->main_error_code ? $this->main_error_code : $code );
}
private function set_main_error_message( $message, $code ) {
// Don't set the process_failed as message since it is not that helpful unless we don't have one already set.
$this->main_error_message = ( $code === 'process_failed' && $this->main_error_code ? $this->main_error_code : $message );
}
public function get_main_error_code() {
return $this->main_error_code;
}
public function get_main_error_message() {
return $this->main_error_message;
}
/**
* Overwrites the feedback function
*/
public function feedback( $data ) {
$current_error = null;
if ( is_wp_error( $data ) ) {
$this->set_main_error_code( $data->get_error_code() );
$string = $data->get_error_message();
} elseif ( is_array( $data ) ) {
return;
} else {
$string = $data;
}
if ( ! empty( $this->upgrader->strings[$string] ) ) {
$this->set_main_error_code( $string );
$current_error = $string;
$string = $this->upgrader->strings[$string];
}
if ( strpos( $string, '%' ) !== false ) {
$args = func_get_args();
$args = array_splice( $args, 1 );
if ( ! empty( $args ) ) {
$string = vsprintf( $string, $args );
}
}
$string = trim( $string );
$string = wp_kses(
$string, array(
'a' => array(
'href' => true
),
'br' => true,
'em' => true,
'strong' => true,
)
);
$this->set_main_error_message( $string, $current_error );
$this->messages[] = $string;
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* Tweak the preview when rendered in an iframe
*/
class Jetpack_Iframe_Embed {
static function init() {
if ( ! self::is_embedding_in_iframe() ) {
return;
}
// Disable the admin bar
if ( ! defined( 'IFRAME_REQUEST' ) ) {
define( 'IFRAME_REQUEST', true );
}
// Prevent canonical redirects
remove_filter( 'template_redirect', 'redirect_canonical' );
add_action( 'wp_head', array( 'Jetpack_Iframe_Embed', 'noindex' ), 1 );
add_action( 'wp_head', array( 'Jetpack_Iframe_Embed', 'base_target_blank' ), 1 );
add_filter( 'shortcode_atts_video', array( 'Jetpack_Iframe_Embed', 'disable_autoplay' ) );
add_filter( 'shortcode_atts_audio', array( 'Jetpack_Iframe_Embed', 'disable_autoplay' ) );
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
wp_enqueue_script( 'jetpack-iframe-embed', WPMU_PLUGIN_URL . '/jetpack-iframe-embed/jetpack-iframe-embed.js', array( 'jquery' ) );
} else {
$ver = sprintf( '%s-%s', gmdate( 'oW' ), defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : '' );
wp_enqueue_script( 'jetpack-iframe-embed', '//s0.wp.com/wp-content/mu-plugins/jetpack-iframe-embed/jetpack-iframe-embed.js', array( 'jquery' ), $ver );
}
wp_localize_script( 'jetpack-iframe-embed', '_previewSite', array( 'siteURL' => get_site_url() ) );
}
static function is_embedding_in_iframe() {
return (
self::has_iframe_get_param() && (
self::has_preview_get_param() ||
self::has_preview_theme_preview_param()
)
);
}
private static function has_iframe_get_param() {
return isset( $_GET['iframe'] ) && $_GET['iframe'] === 'true';
}
private static function has_preview_get_param() {
return isset( $_GET['preview'] ) && $_GET['preview'] === 'true';
}
private static function has_preview_theme_preview_param() {
return isset( $_GET['theme_preview'] ) && $_GET['theme_preview'] === 'true';
}
/**
* Disable `autoplay` shortcode attribute in context of an iframe
* Added via `shortcode_atts_video` & `shortcode_atts_audio` in `init`
*
* @param array $atts The output array of shortcode attributes.
*
* @return array The output array of shortcode attributes.
*/
static function disable_autoplay( $atts ) {
return array_merge( $atts, array( 'autoplay' => false ) );
}
/**
* We don't want search engines to index iframe previews
* Added via `wp_head` action in `init`
*/
static function noindex() {
echo '<meta name="robots" content="noindex,nofollow" />';
}
/**
* Make sure all links and forms open in a new window by default
* (unless overridden on client-side by JS)
* Added via `wp_head` action in `init`
*/
static function base_target_blank() {
echo '<base target="_blank" />';
}
}

View File

@@ -0,0 +1,83 @@
<?php
class Jetpack_Search_Performance_Logger {
/**
* @var Jetpack_Search_Performance_Logger
**/
private static $instance = null;
private $current_query = null;
private $query_started = null;
private $stats = null;
static function init() {
if ( is_null( self::$instance ) ) {
self::$instance = new Jetpack_Search_Performance_Logger;
}
return self::$instance;
}
private function __construct() {
$this->stats = array();
add_action( 'pre_get_posts', array( $this, 'begin_log_query' ), 10, 1 );
add_action( 'did_jetpack_search_query', array( $this, 'log_jetpack_search_query' ) );
add_filter( 'found_posts', array( $this, 'log_mysql_query' ), 10, 2 );
add_action( 'wp_footer', array( $this, 'print_stats' ) );
}
public function begin_log_query( $query ) {
if ( $this->should_log_query( $query ) ) {
$this->query_started = microtime( true );
$this->current_query = $query;
}
}
public function log_mysql_query( $found_posts, $query ) {
if ( $this->current_query === $query ) {
$duration = microtime( true ) - $this->query_started;
if ( $duration < 60 ) { // eliminate outliers, likely tracking errors
$this->record_query_time( $duration, false );
}
$this->reset_query_state();
}
return $found_posts;
}
public function log_jetpack_search_query() {
$duration = microtime( true ) - $this->query_started;
if ( $duration < 60 ) { // eliminate outliers, likely tracking errors
$this->record_query_time( $duration, true );
}
$this->reset_query_state();
}
private function reset_query_state() {
$this->query_started = null;
$this->current_query = null;
}
private function should_log_query( $query ) {
return $query->is_main_query() && $query->is_search();
}
private function record_query_time( $duration, $was_jetpack_search ) {
$this->stats[] = array( $was_jetpack_search, intval( $duration * 1000 ) );
}
public function print_stats() {
$beacons = array();
if ( ! empty( $this->stats ) ) {
foreach( $this->stats as $stat ) {
$search_type = $stat[0] ? 'es' : 'mysql';
$beacons[] = "%22jetpack.search.{$search_type}.duration:{$stat[1]}|ms%22";
}
$encoded_json = '{%22beacons%22:[' . implode(',', $beacons ) . ']}';
$encoded_site_url = urlencode( site_url() );
$url = "https://pixel.wp.com/boom.gif?v=0.9&u={$encoded_site_url}&json={$encoded_json}";
echo '<img src="' . $url . '" width="1" height="1" style="display:none;" alt=":)"/>';
}
}
}

View File

@@ -0,0 +1,436 @@
<?php
/**
* Class with methods to extract metadata from a post/page about videos, images, links, mentions embedded
* in or attached to the post/page.
*
* @todo Additionally, have some filters on number of items in each field
*/
class Jetpack_Media_Meta_Extractor {
// Some consts for what to extract
const ALL = 255;
const LINKS = 1;
const MENTIONS = 2;
const IMAGES = 4;
const SHORTCODES = 8; // Only the keeper shortcodes below
const EMBEDS = 16;
const HASHTAGS = 32;
// For these, we try to extract some data from the shortcode, rather than just recording its presence (which we do for all)
// There should be a function get_{shortcode}_id( $atts ) or static method SomethingShortcode::get_{shortcode}_id( $atts ) for these.
private static $KEEPER_SHORTCODES = array(
'youtube',
'vimeo',
'hulu',
'ted',
'wpvideo',
'videopress',
);
/**
* Gets the specified media and meta info from the given post.
* NOTE: If you have the post's HTML content already and don't need image data, use extract_from_content() instead.
*
* @param $blog_id The ID of the blog
* @param $post_id The ID of the post
* @param $what_to_extract (int) A mask of things to extract, e.g. Jetpack_Media_Meta_Extractor::IMAGES | Jetpack_Media_Meta_Extractor::MENTIONS
* @returns a structure containing metadata about the embedded things, or empty array if nothing found, or WP_Error on error
*/
static public function extract( $blog_id, $post_id, $what_to_extract = self::ALL ) {
// multisite?
if ( function_exists( 'switch_to_blog') )
switch_to_blog( $blog_id );
$post = get_post( $post_id );
$content = $post->post_title . "\n\n" . $post->post_content;
$char_cnt = strlen( $content );
//prevent running extraction on really huge amounts of content
if ( $char_cnt > 100000 ) //about 20k English words
$content = substr( $content, 0, 100000 );
$extracted = array();
// Get images first, we need the full post for that
if ( self::IMAGES & $what_to_extract ) {
$extracted = self::get_image_fields( $post );
// Turn off images so we can safely call extract_from_content() below
$what_to_extract = $what_to_extract - self::IMAGES;
}
if ( function_exists( 'switch_to_blog') )
restore_current_blog();
// All of the other things besides images can be extracted from just the content
$extracted = self::extract_from_content( $content, $what_to_extract, $extracted );
return $extracted;
}
/**
* Gets the specified meta info from the given post content.
* NOTE: If you want IMAGES, call extract( $blog_id, $post_id, ...) which will give you more/better image extraction
* This method will give you an error if you ask for IMAGES.
*
* @param $content The HTML post_content of a post
* @param $what_to_extract (int) A mask of things to extract, e.g. Jetpack_Media_Meta_Extractor::IMAGES | Jetpack_Media_Meta_Extractor::MENTIONS
* @param $already_extracted (array) Previously extracted things, e.g. images from extract(), which can be used for x-referencing here
* @returns a structure containing metadata about the embedded things, or empty array if nothing found, or WP_Error on error
*/
static public function extract_from_content( $content, $what_to_extract = self::ALL, $already_extracted = array() ) {
$stripped_content = self::get_stripped_content( $content );
// Maybe start with some previously extracted things (e.g. images from extract()
$extracted = $already_extracted;
// Embedded media objects will have already been converted to shortcodes by pre_kses hooks on save.
if ( self::IMAGES & $what_to_extract ) {
$images = Jetpack_Media_Meta_Extractor::extract_images_from_content( $stripped_content, array() );
$extracted = array_merge( $extracted, $images );
}
// ----------------------------------- MENTIONS ------------------------------
if ( self::MENTIONS & $what_to_extract ) {
if ( preg_match_all( '/(^|\s)@(\w+)/u', $stripped_content, $matches ) ) {
$mentions = array_values( array_unique( $matches[2] ) ); //array_unique() retains the keys!
$mentions = array_map( 'strtolower', $mentions );
$extracted['mention'] = array( 'name' => $mentions );
if ( !isset( $extracted['has'] ) )
$extracted['has'] = array();
$extracted['has']['mention'] = count( $mentions );
}
}
// ----------------------------------- HASHTAGS ------------------------------
/** Some hosts may not compile with --enable-unicode-properties and kick a warning:
* Warning: preg_match_all() [function.preg-match-all]: Compilation failed: support for \P, \p, and \X has not been compiled
* Therefore, we only run this code block on wpcom, not in Jetpack.
*/
if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) && ( self::HASHTAGS & $what_to_extract ) ) {
//This regex does not exactly match Twitter's
// if there are problems/complaints we should implement this:
// https://github.com/twitter/twitter-text/blob/master/java/src/com/twitter/Regex.java
if ( preg_match_all( '/(?:^|\s)#(\w*\p{L}+\w*)/u', $stripped_content, $matches ) ) {
$hashtags = array_values( array_unique( $matches[1] ) ); //array_unique() retains the keys!
$hashtags = array_map( 'strtolower', $hashtags );
$extracted['hashtag'] = array( 'name' => $hashtags );
if ( !isset( $extracted['has'] ) )
$extracted['has'] = array();
$extracted['has']['hashtag'] = count( $hashtags );
}
}
// ----------------------------------- SHORTCODES ------------------------------
// Always look for shortcodes.
// If we don't want them, we'll just remove them, so we don't grab them as links below
$shortcode_pattern = '/' . get_shortcode_regex() . '/s';
if ( preg_match_all( $shortcode_pattern, $content, $matches ) ) {
$shortcode_total_count = 0;
$shortcode_type_counts = array();
$shortcode_types = array();
$shortcode_details = array();
if ( self::SHORTCODES & $what_to_extract ) {
foreach( $matches[2] as $key => $shortcode ) {
//Elasticsearch (and probably other things) doesn't deal well with some chars as key names
$shortcode_name = preg_replace( '/[.,*"\'\/\\\\#+ ]/', '_', $shortcode );
$attr = shortcode_parse_atts( $matches[3][ $key ] );
$shortcode_total_count++;
if ( ! isset( $shortcode_type_counts[$shortcode_name] ) )
$shortcode_type_counts[$shortcode_name] = 0;
$shortcode_type_counts[$shortcode_name]++;
// Store (uniquely) presence of all shortcode regardless of whether it's a keeper (for those, get ID below)
// @todo Store number of occurrences?
if ( ! in_array( $shortcode_name, $shortcode_types ) )
$shortcode_types[] = $shortcode_name;
// For keeper shortcodes, also store the id/url of the object (e.g. youtube video, TED talk, etc.)
if ( in_array( $shortcode, self::$KEEPER_SHORTCODES ) ) {
unset( $id ); // Clear shortcode ID data left from the last shortcode
// We'll try to get the salient ID from the function jetpack_shortcode_get_xyz_id()
// If the shortcode is a class, we'll call XyzShortcode::get_xyz_id()
$shortcode_get_id_func = "jetpack_shortcode_get_{$shortcode}_id";
$shortcode_class_name = ucfirst( $shortcode ) . 'Shortcode';
$shortcode_get_id_method = "get_{$shortcode}_id";
if ( function_exists( $shortcode_get_id_func ) ) {
$id = call_user_func( $shortcode_get_id_func, $attr );
} else if ( method_exists( $shortcode_class_name, $shortcode_get_id_method ) ) {
$id = call_user_func( array( $shortcode_class_name, $shortcode_get_id_method ), $attr );
}
if ( ! empty( $id )
&& ( ! isset( $shortcode_details[$shortcode_name] ) || ! in_array( $id, $shortcode_details[$shortcode_name] ) ) )
$shortcode_details[$shortcode_name][] = $id;
}
}
if ( $shortcode_total_count > 0 ) {
// Add the shortcode info to the $extracted array
if ( !isset( $extracted['has'] ) )
$extracted['has'] = array();
$extracted['has']['shortcode'] = $shortcode_total_count;
$extracted['shortcode'] = array();
foreach ( $shortcode_type_counts as $type => $count )
$extracted['shortcode'][$type] = array( 'count' => $count );
if ( ! empty( $shortcode_types ) )
$extracted['shortcode_types'] = $shortcode_types;
foreach ( $shortcode_details as $type => $id )
$extracted['shortcode'][$type]['id'] = $id;
}
}
// Remove the shortcodes form our copy of $content, so we don't count links in them as links below.
$content = preg_replace( $shortcode_pattern, ' ', $content );
}
// ----------------------------------- LINKS ------------------------------
if ( self::LINKS & $what_to_extract ) {
// To hold the extracted stuff we find
$links = array();
// @todo Get the text inside the links?
// Grab any links, whether in <a href="..." or not, but subtract those from shortcodes and images
// (we treat embed links as just another link)
if ( preg_match_all( '#(?:^|\s|"|\')(https?://([^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))))#', $content, $matches ) ) {
foreach ( $matches[1] as $link_raw ) {
$url = parse_url( $link_raw );
// Data URI links
if ( isset( $url['scheme'] ) && 'data' === $url['scheme'] )
continue;
// Remove large (and likely invalid) links
if ( 4096 < strlen( $link_raw ) )
continue;
// Build a simple form of the URL so we can compare it to ones we found in IMAGES or SHORTCODES and exclude those
$simple_url = $url['scheme'] . '://' . $url['host'] . ( ! empty( $url['path'] ) ? $url['path'] : '' );
if ( isset( $extracted['image']['url'] ) ) {
if ( in_array( $simple_url, (array) $extracted['image']['url'] ) )
continue;
}
list( $proto, $link_all_but_proto ) = explode( '://', $link_raw );
// Build a reversed hostname
$host_parts = array_reverse( explode( '.', $url['host'] ) );
$host_reversed = '';
foreach ( $host_parts as $part ) {
$host_reversed .= ( ! empty( $host_reversed ) ? '.' : '' ) . $part;
}
$link_analyzed = '';
if ( !empty( $url['path'] ) ) {
// The whole path (no query args or fragments)
$path = substr( $url['path'], 1 ); // strip the leading '/'
$link_analyzed .= ( ! empty( $link_analyzed ) ? ' ' : '' ) . $path;
// The path split by /
$path_split = explode( '/', $path );
if ( count( $path_split ) > 1 ) {
$link_analyzed .= ' ' . implode( ' ', $path_split );
}
// The fragment
if ( ! empty( $url['fragment'] ) )
$link_analyzed .= ( ! empty( $link_analyzed ) ? ' ' : '' ) . $url['fragment'];
}
// @todo Check unique before adding
$links[] = array(
'url' => $link_all_but_proto,
'host_reversed' => $host_reversed,
'host' => $url['host'],
);
}
}
$link_count = count( $links );
if ( $link_count ) {
$extracted[ 'link' ] = $links;
if ( !isset( $extracted['has'] ) )
$extracted['has'] = array();
$extracted['has']['link'] = $link_count;
}
}
// ----------------------------------- EMBEDS ------------------------------
//Embeds are just individual links on their own line
if ( self::EMBEDS & $what_to_extract ) {
if ( !function_exists( '_wp_oembed_get_object' ) )
include( ABSPATH . WPINC . '/class-oembed.php' );
// get an oembed object
$oembed = _wp_oembed_get_object();
// Grab any links on their own lines that may be embeds
if ( preg_match_all( '|^\s*(https?://[^\s"]+)\s*$|im', $content, $matches ) ) {
// To hold the extracted stuff we find
$embeds = array();
foreach ( $matches[1] as $link_raw ) {
$url = parse_url( $link_raw );
list( $proto, $link_all_but_proto ) = explode( '://', $link_raw );
// Check whether this "link" is really an embed.
foreach ( $oembed->providers as $matchmask => $data ) {
list( $providerurl, $regex ) = $data;
// Turn the asterisk-type provider URLs into regex
if ( !$regex ) {
$matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i';
$matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask );
}
if ( preg_match( $matchmask, $link_raw ) ) {
$provider = str_replace( '{format}', 'json', $providerurl ); // JSON is easier to deal with than XML
$embeds[] = $link_all_but_proto; // @todo Check unique before adding
// @todo Try to get ID's for the ones we care about (shortcode_keepers)
break;
}
}
}
if ( ! empty( $embeds ) ) {
if ( !isset( $extracted['has'] ) )
$extracted['has'] = array();
$extracted['has']['embed'] = count( $embeds );
$extracted['embed'] = array( 'url' => array() );
foreach ( $embeds as $e )
$extracted['embed']['url'][] = $e;
}
}
}
return $extracted;
}
/**
* @param $post A post object
* @param $args (array) Optional args, see defaults list for details
* @returns array Returns an array of all images meeting the specified criteria in $args
*
* Uses Jetpack Post Images
*/
private static function get_image_fields( $post, $args = array() ) {
$defaults = array(
'width' => 200, // Required minimum width (if possible to determine)
'height' => 200, // Required minimum height (if possible to determine)
);
$args = wp_parse_args( $args, $defaults );
$image_list = array();
$image_booleans = array();
$image_booleans['gallery'] = 0;
$from_featured_image = Jetpack_PostImages::from_thumbnail( $post->ID, $args['width'], $args['height'] );
if ( !empty( $from_featured_image ) ) {
$srcs = wp_list_pluck( $from_featured_image, 'src' );
$image_list = array_merge( $image_list, $srcs );
}
$from_slideshow = Jetpack_PostImages::from_slideshow( $post->ID, $args['width'], $args['height'] );
if ( !empty( $from_slideshow ) ) {
$srcs = wp_list_pluck( $from_slideshow, 'src' );
$image_list = array_merge( $image_list, $srcs );
}
$from_gallery = Jetpack_PostImages::from_gallery( $post->ID );
if ( !empty( $from_gallery ) ) {
$srcs = wp_list_pluck( $from_gallery, 'src' );
$image_list = array_merge( $image_list, $srcs );
$image_booleans['gallery']++; // @todo This count isn't correct, will only every count 1
}
// @todo Can we check width/height of these efficiently? Could maybe use query args at least, before we strip them out
$image_list = Jetpack_Media_Meta_Extractor::get_images_from_html( $post->post_content, $image_list );
return Jetpack_Media_Meta_Extractor::build_image_struct( $image_list, $image_booleans );
}
public static function extract_images_from_content( $content, $image_list ) {
$image_list = Jetpack_Media_Meta_Extractor::get_images_from_html( $content, $image_list );
return Jetpack_Media_Meta_Extractor::build_image_struct( $image_list, array() );
}
public static function build_image_struct( $image_list, $image_booleans ) {
if ( ! empty( $image_list ) ) {
$retval = array( 'image' => array() );
$image_list = array_unique( $image_list );
foreach ( $image_list as $img ) {
$retval['image'][] = array( 'url' => $img );
}
$image_booleans['image'] = count( $retval['image'] );
if ( ! empty( $image_booleans ) )
$retval['has'] = $image_booleans;
return $retval;
} else {
return array();
}
}
/**
*
* @param string $html Some markup, possibly containing image tags
* @param array $images_already_extracted (just an array of image URLs without query strings, no special structure), used for de-duplication
* @return array Image URLs extracted from the HTML, stripped of query params and de-duped
*/
public static function get_images_from_html( $html, $images_already_extracted ) {
$image_list = $images_already_extracted;
$from_html = Jetpack_PostImages::from_html( $html );
if ( !empty( $from_html ) ) {
$srcs = wp_list_pluck( $from_html, 'src' );
foreach( $srcs as $image_url ) {
if ( ( $src = parse_url( $image_url ) ) && isset( $src['scheme'], $src['host'], $src['path'] ) ) {
// Rebuild the URL without the query string
$queryless = $src['scheme'] . '://' . $src['host'] . $src['path'];
} elseif ( $length = strpos( $image_url, '?' ) ) {
// If parse_url() didn't work, strip off the query string the old fashioned way
$queryless = substr( $image_url, 0, $length );
} else {
// Failing that, there was no spoon! Err ... query string!
$queryless = $image_url;
}
// Discard URLs that are longer then 4KB, these are likely data URIs or malformed HTML.
if ( 4096 < strlen( $queryless ) ) {
continue;
}
if ( ! in_array( $queryless, $image_list ) ) {
$image_list[] = $queryless;
}
}
}
return $image_list;
}
private static function get_stripped_content( $content ) {
$clean_content = strip_tags( $content );
$clean_content = html_entity_decode( $clean_content );
//completely strip shortcodes and any content they enclose
$clean_content = strip_shortcodes( $clean_content );
return $clean_content;
}
}

View File

@@ -0,0 +1,350 @@
<?php
/**
* Class Jetpack_Media_Summary
*
* embed [video] > gallery > image > text
*/
class Jetpack_Media_Summary {
private static $cache = array();
static function get( $post_id, $blog_id = 0, $args = array() ) {
$defaults = array(
'max_words' => 16,
'max_chars' => 256,
);
$args = wp_parse_args( $args, $defaults );
$switched = false;
if ( !empty( $blog_id ) && $blog_id != get_current_blog_id() && function_exists( 'switch_to_blog' ) ) {
switch_to_blog( $blog_id );
$switched = true;
} else {
$blog_id = get_current_blog_id();
}
$cache_key = "{$blog_id}_{$post_id}_{$args['max_words']}_{$args['max_chars']}";
if ( isset( self::$cache[ $cache_key ] ) ) {
return self::$cache[ $cache_key ];
}
if ( ! class_exists( 'Jetpack_Media_Meta_Extractor' ) ) {
jetpack_require_lib( 'class.media-extractor' );
}
$post = get_post( $post_id );
$permalink = get_permalink( $post_id );
$return = array(
'type' => 'standard',
'permalink' => $permalink,
'image' => '',
'excerpt' => '',
'word_count' => 0,
'secure' => array(
'image' => '',
),
'count' => array(
'image' => 0,
'video' => 0,
'word' => 0,
'link' => 0,
),
);
if ( empty( $post->post_password ) ) {
$return['excerpt'] = self::get_excerpt( $post->post_content, $post->post_excerpt, $args['max_words'], $args['max_chars'] , $post);
$return['count']['word'] = self::get_word_count( $post->post_content );
$return['count']['word_remaining'] = self::get_word_remaining_count( $post->post_content, $return['excerpt'] );
$return['count']['link'] = self::get_link_count( $post->post_content );
}
$extract = Jetpack_Media_Meta_Extractor::extract( $blog_id, $post_id, Jetpack_Media_Meta_Extractor::ALL );
if ( empty( $extract['has'] ) )
return $return;
// Prioritize [some] video embeds
if ( !empty( $extract['has']['shortcode'] ) ) {
foreach ( $extract['shortcode'] as $type => $data ) {
switch ( $type ) {
case 'videopress':
case 'wpvideo':
if ( 0 == $return['count']['video'] ) {
// If there is no id on the video, then let's just skip this
if ( ! isset ( $data['id'][0] ) ) {
continue;
}
$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 ) ) {
continue;
}
$url = sprintf(
'https://videos.files.wordpress.com/%1$s/%2$s',
$guid,
$video_info->files->std->mp4
);
$thumbnail = $video_info->poster;
if ( ! empty( $thumbnail ) ) {
$return['image'] = $thumbnail;
$return['secure']['image'] = $thumbnail;
}
$return['type'] = 'video';
$return['video'] = esc_url_raw( $url );
$return['video_type'] = 'video/mp4';
$return['secure']['video'] = $return['video'];
}
}
$return['count']['video']++;
break;
case 'youtube':
if ( 0 == $return['count']['video'] ) {
$return['type'] = 'video';
$return['video'] = esc_url_raw( 'http://www.youtube.com/watch?feature=player_embedded&v=' . $extract['shortcode']['youtube']['id'][0] );
$return['image'] = self::get_video_poster( 'youtube', $extract['shortcode']['youtube']['id'][0] );
$return['secure']['video'] = self::https( $return['video'] );
$return['secure']['image'] = self::https( $return['image'] );
}
$return['count']['video']++;
break;
case 'vimeo':
if ( 0 == $return['count']['video'] ) {
$return['type'] = 'video';
$return['video'] = esc_url_raw( 'http://vimeo.com/' . $extract['shortcode']['vimeo']['id'][0] );
$return['secure']['video'] = self::https( $return['video'] );
$poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
if ( !empty( $poster_image ) ) {
$return['image'] = $poster_image;
$poster_url_parts = parse_url( $poster_image );
$return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
}
}
$return['count']['video']++;
break;
}
}
}
if ( !empty( $extract['has']['embed'] ) ) {
foreach( $extract['embed']['url'] as $embed ) {
if ( preg_match( '/((youtube|vimeo|dailymotion)\.com|youtu.be)/', $embed ) ) {
if ( 0 == $return['count']['video'] ) {
$return['type'] = 'video';
$return['video'] = 'http://' . $embed;
$return['secure']['video'] = self::https( $return['video'] );
if ( false !== strpos( $embed, 'youtube' ) ) {
$return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
$return['secure']['image'] = self::https( $return['image'] );
} else if ( false !== strpos( $embed, 'youtu.be' ) ) {
$youtube_id = jetpack_get_youtube_id( $return['video'] );
$return['video'] = 'http://youtube.com/watch?v=' . $youtube_id . '&feature=youtu.be';
$return['secure']['video'] = self::https( $return['video'] );
$return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
$return['secure']['image'] = self::https( $return['image'] );
} else if ( false !== strpos( $embed, 'vimeo' ) ) {
$poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
if ( !empty( $poster_image ) ) {
$return['image'] = $poster_image;
$poster_url_parts = parse_url( $poster_image );
$return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
}
} else if ( false !== strpos( $embed, 'dailymotion' ) ) {
$return['image'] = str_replace( 'dailymotion.com/video/','dailymotion.com/thumbnail/video/', $embed );
$return['image'] = parse_url( $return['image'], PHP_URL_SCHEME ) === null ? 'http://' . $return['image'] : $return['image'];
$return['secure']['image'] = self::https( $return['image'] );
}
}
$return['count']['video']++;
}
}
}
// Do we really want to make the video the primary focus of the post?
if ( 'video' == $return['type'] ) {
$content = wpautop( strip_tags( $post->post_content ) );
$paragraphs = explode( '</p>', $content );
$number_of_paragraphs = 0;
foreach ( $paragraphs as $i => $paragraph ) {
// Don't include blank lines as a paragraph
if ( '' == trim( $paragraph ) ) {
unset( $paragraphs[$i] );
continue;
}
$number_of_paragraphs++;
}
$number_of_paragraphs = $number_of_paragraphs - $return['count']['video']; // subtract amount for videos..
// More than 2 paragraph? The video is not the primary focus so we can do some more analysis
if ( $number_of_paragraphs > 2 )
$return['type'] = 'standard';
}
// If we don't have any prioritized embed...
if ( 'standard' == $return['type'] ) {
if ( ( ! empty( $extract['has']['gallery'] ) || ! empty( $extract['shortcode']['gallery']['count'] ) ) && ! empty( $extract['image'] ) ) {
//... Then we prioritize galleries first (multiple images returned)
$return['type'] = 'gallery';
$return['images'] = $extract['image'];
foreach ( $return['images'] as $image ) {
$return['secure']['images'][] = array( 'url' => self::ssl_img( $image['url'] ) );
$return['count']['image']++;
}
} else if ( ! empty( $extract['has']['image'] ) ) {
// ... Or we try and select a single image that would make sense
$content = wpautop( strip_tags( $post->post_content ) );
$paragraphs = explode( '</p>', $content );
$number_of_paragraphs = 0;
foreach ( $paragraphs as $i => $paragraph ) {
// Don't include 'actual' captions as a paragraph
if ( false !== strpos( $paragraph, '[caption' ) ) {
unset( $paragraphs[$i] );
continue;
}
// Don't include blank lines as a paragraph
if ( '' == trim( $paragraph ) ) {
unset( $paragraphs[$i] );
continue;
}
$number_of_paragraphs++;
}
$return['image'] = $extract['image'][0]['url'];
$return['secure']['image'] = self::ssl_img( $return['image'] );
$return['count']['image']++;
if ( $number_of_paragraphs <= 2 && 1 == count( $extract['image'] ) ) {
// If we have lots of text or images, let's not treat it as an image post, but return its first image
$return['type'] = 'image';
}
}
}
if ( $switched ) {
restore_current_blog();
}
/**
* Allow a theme or plugin to inspect and ultimately change the media summary.
*
* @since 4.4.0
*
* @param array $data The calculated media summary data.
* @param int $post_id The id of the post this data applies to.
*/
$return = apply_filters( 'jetpack_media_summary_output', $return, $post_id );
self::$cache[ $cache_key ] = $return;
return $return;
}
static function https( $str ) {
return str_replace( 'http://', 'https://', $str );
}
static function ssl_img( $url ) {
if ( false !== strpos( $url, 'files.wordpress.com' ) ) {
return self::https( $url );
} else {
return self::https( jetpack_photon_url( $url ) );
}
}
static function get_video_poster( $type, $id ) {
if ( 'videopress' == $type ) {
if ( function_exists( 'video_get_highest_resolution_image_url' ) ) {
return video_get_highest_resolution_image_url( $id );
} else if ( class_exists( 'VideoPress_Video' ) ) {
$video = new VideoPress_Video( $id );
return $video->poster_frame_uri;
}
} else if ( 'youtube' == $type ) {
return 'http://img.youtube.com/vi/'.$id.'/0.jpg';
}
}
static function clean_text( $text ) {
return trim(
preg_replace(
'/[\s]+/',
' ',
preg_replace(
'@https?://[\S]+@',
'',
strip_shortcodes(
strip_tags(
$text
)
)
)
)
);
}
/**
* Retrieve an excerpt for the post summary.
*
* This function works around a suspected problem with Core. If resolved, this function should be simplified.
* @link https://github.com/Automattic/jetpack/pull/8510
* @link https://core.trac.wordpress.org/ticket/42814
*
* @param string $post_content The post's content.
* @param string $post_excerpt The post's excerpt. Empty if none was explicitly set.
* @param int $max_words Maximum number of words for the excerpt. Used on wp.com. Default 16.
* @param int $max_chars Maximum characters in the excerpt. Used on wp.com. Default 256.
* @param WP_Post $requested_post The post object.
* @return string Post excerpt.
**/
static function get_excerpt( $post_content, $post_excerpt, $max_words = 16, $max_chars = 256, $requested_post = null ) {
global $post;
$original_post = $post; // Saving the global for later use.
if ( function_exists( 'wpcom_enhanced_excerpt_extract_excerpt' ) ) {
return self::clean_text( wpcom_enhanced_excerpt_extract_excerpt( array(
'text' => $post_content,
'excerpt_only' => true,
'show_read_more' => false,
'max_words' => $max_words,
'max_chars' => $max_chars,
'read_more_threshold' => 25,
) ) );
} elseif ( $requested_post instanceof WP_Post ) {
$post = $requested_post; // setup_postdata does not set the global.
setup_postdata( $post );
/** This filter is documented in core/src/wp-includes/post-template.php */
$post_excerpt = apply_filters( 'get_the_excerpt', $post_excerpt, $post );
$post = $original_post; // wp_reset_postdata uses the $post global.
wp_reset_postdata();
return self::clean_text( $post_excerpt );
}
return '';
}
static function get_word_count( $post_content ) {
return str_word_count( self::clean_text( $post_content ) );
}
static function get_word_remaining_count( $post_content, $excerpt_content ) {
return str_word_count( self::clean_text( $post_content ) ) - str_word_count( self::clean_text( $excerpt_content ) );
}
static function get_link_count( $post_content ) {
return preg_match_all( '/\<a[\> ]/', $post_content, $matches );
}
}

View File

@@ -0,0 +1,504 @@
<?php
require_once( JETPACK__PLUGIN_DIR . 'sal/class.json-api-date.php' );
/**
* Class to handle different actions related to media.
*/
class Jetpack_Media {
public static $WP_ORIGINAL_MEDIA = '_wp_original_post_media';
public static $WP_REVISION_HISTORY = '_wp_revision_history';
public static $REVISION_HISTORY_MAXIMUM_AMOUNT = 0;
public static $WP_ATTACHMENT_IMAGE_ALT = '_wp_attachment_image_alt';
/**
* Generate a filename in function of the original filename of the media.
* The returned name has the `{basename}-{hash}-{random-number}.{ext}` shape.
* The hash is built according to the filename trying to avoid name collisions
* with other media files.
*
* @param number $media_id - media post ID
* @param string $new_filename - the new filename
* @return string A random filename.
*/
public static function generate_new_filename( $media_id, $new_filename ) {
// get the right filename extension
$new_filename_paths = pathinfo( $new_filename );
$new_file_ext = $new_filename_paths['extension'];
// take out filename from the original file or from the current attachment
$original_media = (array) self::get_original_media( $media_id );
if ( ! empty( $original_media ) ) {
$original_file_parts = pathinfo( $original_media['file'] );
$filename_base = $original_file_parts['filename'];
} else {
$current_file = get_attached_file( $media_id );
$current_file_parts = pathinfo( $current_file );
$current_file_ext = $current_file_parts['filename'];
$filename_base = $current_file_parts['filename'];
}
// add unique seed based on the filename
$filename_base .= '-' . crc32( $filename_base ) . '-';
$number_suffix = time() . rand( 100, 999 );
do {
$filename = $filename_base;
$filename .= $number_suffix;
$file_ext = $new_file_ext ? $new_file_ext : $current_file_ext;
$new_filename = "{$filename}.{$file_ext}";
$new_path = "{$current_file_parts['dirname']}/$new_filename";
$number_suffix++;
} while( file_exists( $new_path ) );
return $new_filename;
}
/**
* File urls use the post (image item) date to generate a folder path.
* Post dates can change, so we use the original date used in the `guid`
* url so edits can remain in the same folder. In the following function
* we capture a string in the format of `YYYY/MM` from the guid.
*
* For example with a guid of
* "http://test.files.wordpress.com/2016/10/test.png" the resulting string
* would be: "2016/10"
*
* @param number $media_id
* @return string
*/
private function get_time_string_from_guid( $media_id ) {
$time = date( "Y/m", strtotime( current_time( 'mysql' ) ) );
if ( $media = get_post( $media_id ) ) {
$pattern = '/\/(\d{4}\/\d{2})\//';
preg_match( $pattern, $media->guid, $matches );
if ( count( $matches ) > 1 ) {
$time = $matches[1];
}
}
return $time;
}
/**
* Return an array of allowed mime_type items used to upload a media file.
*
* @return array mime_type array
*/
static function get_allowed_mime_types( $default_mime_types ) {
return array_unique( array_merge( $default_mime_types, array(
'application/msword', // .doc
'application/vnd.ms-powerpoint', // .ppt, .pps
'application/vnd.ms-excel', // .xls
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
'application/vnd.openxmlformats-officedocument.presentationml.slideshow', // .ppsx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/vnd.oasis.opendocument.text', // .odt
'application/pdf', // .pdf
) ) );
}
/**
* Checks that the mime type of the file
* is among those in a filterable list of mime types.
*
* @param string $file Path to file to get its mime type.
* @return bool
*/
protected static function is_file_supported_for_sideloading( $file ) {
if ( class_exists( 'finfo' ) ) { // php 5.3+
$finfo = new finfo( FILEINFO_MIME );
$mime = explode( '; ', $finfo->file( $file ) );
$type = $mime[0];
} elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
$type = mime_content_type( $file );
} else {
return false;
}
/**
* Filter the list of supported mime types for media sideloading.
*
* @since 4.0
*
* @module json-api
*
* @param array $supported_mime_types Array of the supported mime types for media sideloading.
*/
$supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
'image/png',
'image/jpeg',
'image/gif',
'image/bmp',
'video/quicktime',
'video/mp4',
'video/mpeg',
'video/ogg',
'video/3gpp',
'video/3gpp2',
'video/h261',
'video/h262',
'video/h264',
'video/x-msvideo',
'video/x-ms-wmv',
'video/x-ms-asf',
) );
// If the type returned was not an array as expected, then we know we don't have a match.
if ( ! is_array( $supported_mime_types ) ) {
return false;
}
return in_array( $type, $supported_mime_types );
}
/**
* Try to remove the temporal file from the given file array.
*
* @param array $file_array Array with data about the temporal file
* @return bool `true` if the file has been removed. `false` either the file doesn't exist or it couldn't be removed.
*/
private static function remove_tmp_file( $file_array ) {
if ( ! file_exists ( $file_array['tmp_name'] ) ) {
return false;
}
return @unlink( $file_array['tmp_name'] );
}
/**
* Save the given temporal file considering file type,
* correct location according to the original file path, etc.
* The file type control is done through of `jetpack_supported_media_sideload_types` filter,
* which allows define to the users their own file types list.
*
* @param array $file_array file to save
* @param number $media_id
* @return array|WP_Error an array with information about the new file saved or a WP_Error is something went wrong.
*/
public static function save_temporary_file( $file_array, $media_id ) {
$tmp_filename = $file_array['tmp_name'];
if ( ! file_exists( $tmp_filename ) ) {
return new WP_Error( 'invalid_input', 'No media provided in input.' );
}
// add additional mime_types through of the `jetpack_supported_media_sideload_types` filter
$mime_type_static_filter = array(
'Jetpack_Media',
'get_allowed_mime_types'
);
add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
if (
! self::is_file_supported_for_sideloading( $tmp_filename ) &&
! file_is_displayable_image( $tmp_filename )
) {
@unlink( $tmp_filename );
return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
}
remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
// generate a new file name
$tmp_new_filename = self::generate_new_filename( $media_id, $file_array[ 'name' ] );
// start to create the parameters to move the temporal file
$overrides = array( 'test_form' => false );
// get time according to the original filaname
$time = self::get_time_string_from_guid( $media_id );
$file_array['name'] = $tmp_new_filename;
$file = wp_handle_sideload( $file_array, $overrides, $time );
self::remove_tmp_file( $file_array );
if ( isset( $file['error'] ) ) {
return new WP_Error( 'upload_error', $file['error'] );
}
return $file;
}
/**
* Return an object with an snapshot of a revision item.
*
* @param object $media_item - media post object
* @return object a revision item
*/
public static function get_snapshot( $media_item ) {
$current_file = get_attached_file( $media_item->ID );
$file_paths = pathinfo( $current_file );
$snapshot = array(
'date' => (string) WPCOM_JSON_API_Date::format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
'URL' => (string) wp_get_attachment_url( $media_item->ID ),
'file' => (string) $file_paths['basename'],
'extension' => (string) $file_paths['extension'],
'mime_type' => (string) $media_item->post_mime_type,
'size' => (int) filesize( $current_file )
);
return (object) $snapshot;
}
/**
* Add a new item into revision_history array.
*
* @param object $media_item - media post object
* @param file $file - file recently added
* @param bool $has_original_media - condition is the original media has been already added
* @return bool `true` if the item has been added. Otherwise `false`.
*/
public static function register_revision( $media_item, $file, $has_original_media ) {
if ( is_wp_error( $file ) || ! $has_original_media ) {
return false;
}
add_post_meta( $media_item->ID, self::$WP_REVISION_HISTORY, self::get_snapshot( $media_item ) );
}
/**
* Return the `revision_history` of the given media.
*
* @param number $media_id - media post ID
* @return array `revision_history` array
*/
public static function get_revision_history( $media_id ) {
return array_reverse( get_post_meta( $media_id, self::$WP_REVISION_HISTORY ) );
}
/**
* Return the original media data
*/
public static function get_original_media( $media_id ) {
$original = get_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, true );
$original = $original ? $original : array();
return $original;
}
public static function delete_file( $pathname ) {
if ( ! file_exists( $pathname ) || ! is_file( $pathname ) ) {
// let's touch a fake file to try to `really` remove the media file
touch( $pathname );
}
return wp_delete_file( $pathname );
}
/**
* Try to delete a file according to the dirname of
* the media attached file and the filename.
*
* @param number $media_id - media post ID
* @param string $filename - basename of the file ( name-of-file.ext )
* @return bool `true` is the file has been removed, `false` if not.
*/
private static function delete_media_history_file( $media_id, $filename ) {
$attached_path = get_attached_file( $media_id );
$attached_parts = pathinfo( $attached_path );
$dirname = $attached_parts['dirname'];
$pathname = $dirname . '/' . $filename;
// remove thumbnails
$metadata = wp_generate_attachment_metadata( $media_id, $pathname );
if ( isset( $metadata ) && isset( $metadata['sizes'] ) ) {
foreach ( $metadata['sizes'] as $size => $properties ) {
self::delete_file( $dirname . '/' . $properties['file'] );
}
}
// remove primary file
self::delete_file( $pathname );
}
/**
* Remove specific items from the `revision history` array
* depending on the given criteria: array(
* 'from' => (int) <from>,
* 'to' => (int) <to>,
* )
*
* Also, it removes the file defined in each item.
*
* @param number $media_id - media post ID
* @param object $criteria - criteria to remove the items
* @param array [$revision_history] - revision history array
* @return array `revision_history` array updated.
*/
public static function remove_items_from_revision_history( $media_id, $criteria = array(), $revision_history ) {
if ( ! isset ( $revision_history ) ) {
$revision_history = self::get_revision_history( $media_id );
}
$from = $criteria['from'];
$to = $criteria['to'] ? $criteria['to'] : ( $from + 1 );
for ( $i = $from; $i < $to; $i++ ) {
$removed_item = array_slice( $revision_history, $from, 1 );
if ( ! $removed_item ) {
break;
}
array_splice( $revision_history, $from, 1 );
self::delete_media_history_file( $media_id, $removed_item[0]->file );
}
// override all history items
delete_post_meta( $media_id, self::$WP_REVISION_HISTORY );
$revision_history = array_reverse( $revision_history );
foreach ( $revision_history as &$item ) {
add_post_meta( $media_id, self::$WP_REVISION_HISTORY, $item );
}
return $revision_history;
}
/**
* Limit the number of items of the `revision_history` array.
* When the stack is overflowing the oldest item is remove from there (FIFO).
*
* @param number $media_id - media post ID
* @param number [$limit] - maximun amount of items. 20 as default.
* @return array items removed from `revision_history`
*/
public static function limit_revision_history( $media_id, $limit = null) {
if ( is_null( $limit ) ) {
$limit = self::$REVISION_HISTORY_MAXIMUM_AMOUNT;
}
$revision_history = self::get_revision_history( $media_id );
$total = count( $revision_history );
if ( $total < $limit ) {
return array();
}
self::remove_items_from_revision_history(
$media_id,
array( 'from' => $limit, 'to' => $total ),
$revision_history
);
return self::get_revision_history( $media_id );
}
/**
* Remove the original file and clean the post metadata.
*
* @param number $media_id - media post ID
*/
public static function clean_original_media( $media_id ) {
$original_file = self::get_original_media( $media_id );
if ( ! $original_file ) {
return null;
}
self::delete_media_history_file( $media_id, $original_file->file );
return delete_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA );
}
/**
* Clean `revision_history` of the given $media_id. it means:
* - remove all media files tied to the `revision_history` items.
* - clean `revision_history` meta data.
* - remove and clean the `original_media`
*
* @param number $media_id - media post ID
* @return array results of removing these files
*/
public static function clean_revision_history( $media_id ) {
self::clean_original_media( $media_id );
$revision_history = self::get_revision_history( $media_id );
$total = count( $revision_history );
$updated_history = array();
if ( $total < 1 ) {
return $updated_history;
}
$updated_history = self::remove_items_from_revision_history(
$media_id,
array( 'from' => 0, 'to' => $total ),
$revision_history
);
return $updated_history;
}
/**
* Edit media item process:
*
* - update attachment file
* - preserve original media file
* - trace revision history
*
* @param number $media_id - media post ID
* @param array $file_array - temporal file
* @return {Post|WP_Error} Updated media item or a WP_Error is something went wrong.
*/
public static function edit_media_file( $media_id, $file_array ) {
$media_item = get_post( $media_id );
$has_original_media = self::get_original_media( $media_id );
if ( ! $has_original_media ) {
// The first time that the media is updated
// the original media is stored into the revision_history
$snapshot = self::get_snapshot( $media_item );
add_post_meta( $media_id, self::$WP_ORIGINAL_MEDIA, $snapshot, true );
}
// save temporary file in the correct location
$uploaded_file = self::save_temporary_file( $file_array, $media_id );
if ( is_wp_error( $uploaded_file ) ) {
self::remove_tmp_file( $file_array );
return $uploaded_file;
}
// revision_history control
self::register_revision( $media_item, $uploaded_file, $has_original_media );
$uploaded_path = $uploaded_file['file'];
$udpated_mime_type = $uploaded_file['type'];
$was_updated = update_attached_file( $media_id, $uploaded_path );
if ( ! $was_updated ) {
return WP_Error( 'update_error', 'Media update error' );
}
$new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
wp_update_attachment_metadata( $media_id, $new_metadata );
// check maximum amount of revision_history
self::limit_revision_history( $media_id );
$edited_action = wp_update_post( (object) array(
'ID' => $media_id,
'post_mime_type' => $udpated_mime_type
), true );
if ( is_wp_error( $edited_action ) ) {
return $edited_action;
}
return $media_item;
}
}
// hook: clean revision history when the media item is deleted
function clean_revision_history( $media_id ) {
Jetpack_Media::clean_revision_history( $media_id );
};
add_action( 'delete_attachment', 'clean_revision_history' );

View File

@@ -0,0 +1,60 @@
<?php
/**
* This is the endpoint class for `/site` endpoints.
*
*/
class Jetpack_Core_API_Site_Endpoint {
/**
* Returns the result of `/sites/%s/features` endpoint call.
* @return object $features has 'active' and 'available' properties each of which contain feature slugs.
* 'active' is a simple array of slugs that are active on the current plan.
* 'available' is an object with keys that represent feature slugs and values are arrays
* of plan slugs that enable these features
*/
public static function get_features() {
// Make the API request
$request = sprintf( '/sites/%d/features', Jetpack_Options::get_option( 'id' ) );
$response = Jetpack_Client::wpcom_json_api_request_as_blog( $request, '1.1' );
// Bail if there was an error or malformed response
if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
return new WP_Error(
'failed_to_fetch_data',
esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
array( 'status' => 500 )
);
}
// Decode the results
$results = json_decode( $response['body'], true );
// Bail if there were no results or plan details returned
if ( ! is_array( $results ) ) {
return new WP_Error(
'failed_to_fetch_data',
esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
array( 'status' => 500 )
);
}
return rest_ensure_response( array(
'code' => 'success',
'message' => esc_html__( 'Site features correctly received.', 'jetpack' ),
'data' => wp_remote_retrieve_body( $response ),
)
);
}
/**
* Check that the current user has permissions to request information about this site.
*
* @since 5.1.0
*
* @return bool
*/
public static function can_request() {
return current_user_can( 'jetpack_manage_modules' );
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* Widget information getter endpoint.
*
*/
class Jetpack_Core_API_Widget_Endpoint {
/**
* @since 5.5.0
*
* @param WP_REST_Request $request {
* Array of parameters received by request.
*
* @type string $id Widget id.
* }
*
* @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
*/
public function process( $request ) {
$widget_base = _get_widget_id_base( $request['id'] );
$widget_id = (int) substr( $request['id'], strlen( $widget_base ) + 1 );
switch( $widget_base ) {
case 'milestone_widget':
$instances = get_option( 'widget_milestone_widget', array() );
if (
class_exists( 'Milestone_Widget' )
&& is_active_widget( false, $widget_base . '-' . $widget_id, $widget_base )
&& isset( $instances[ $widget_id ] )
) {
$instance = $instances[ $widget_id ];
$widget = new Milestone_Widget();
return $widget->get_widget_data( $instance );
}
}
return new WP_Error(
'not_found',
esc_html__( 'The requested widget was not found.', 'jetpack' ),
array( 'status' => 404 )
);
}
/**
* Check that the current user has permissions to view widget information.
* For the currently supported widget there are no permissions required.
*
* @since 5.5.0
*
* @return bool
*/
public function can_request() {
return true;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* This is the base class for every Core API endpoint that needs an XMLRPC client.
*
*/
abstract class Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
/**
* An instance of the Jetpack XMLRPC client to make WordPress.com requests
*
* @private
* @var Jetpack_IXR_Client
*/
protected $xmlrpc;
/**
*
* @since 4.3.0
*
* @param Jetpack_IXR_Client $xmlrpc
*/
public function __construct( $xmlrpc = null ) {
$this->xmlrpc = $xmlrpc;
}
/**
* Checks if the site is public and returns the result.
*
* @since 4.3.0
*
* @return Boolean $is_public
*/
protected function is_site_public() {
if ( $this->xmlrpc->query( 'jetpack.isSitePubliclyAccessible', home_url() ) ) {
return $this->xmlrpc->getResponse();
}
return false;
}
}

View File

@@ -0,0 +1,353 @@
<?php
if ( ! function_exists( 'wp_notify_postauthor' ) && Jetpack::is_active() ) :
/**
* Notify an author (and/or others) of a comment/trackback/pingback on a post.
*
* @since 1.0.0
*
* @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
* @param string $deprecated Not used
* @return bool True on completion. False if no email addresses were specified.
*/
function wp_notify_postauthor( $comment_id, $deprecated = null ) {
if ( null !== $deprecated ) {
_deprecated_argument( __FUNCTION__, '3.8.0' );
}
$comment = get_comment( $comment_id );
if ( empty( $comment ) || empty( $comment->comment_post_ID ) ) {
return false;
}
$post = get_post( $comment->comment_post_ID );
$author = get_userdata( $post->post_author );
// Who to notify? By default, just the post author, but others can be added.
$emails = array();
if ( $author ) {
$emails[] = $author->user_email;
}
/** This filter is documented in core/src/wp-includes/pluggable.php */
$emails = apply_filters( 'comment_notification_recipients', $emails, $comment->comment_ID );
$emails = array_filter( $emails );
// If there are no addresses to send the comment to, bail.
if ( ! count( $emails ) ) {
return false;
}
// Facilitate unsetting below without knowing the keys.
$emails = array_flip( $emails );
/** This filter is documented in core/src/wp-includes/pluggable.php */
$notify_author = apply_filters( 'comment_notification_notify_author', false, $comment->comment_ID );
// The comment was left by the author
if ( $author && ! $notify_author && $comment->user_id == $post->post_author ) {
unset( $emails[ $author->user_email ] );
}
// The author moderated a comment on their own post
if ( $author && ! $notify_author && $post->post_author == get_current_user_id() ) {
unset( $emails[ $author->user_email ] );
}
// The post author is no longer a member of the blog
if ( $author && ! $notify_author && ! user_can( $post->post_author, 'read_post', $post->ID ) ) {
unset( $emails[ $author->user_email ] );
}
// If there's no email to send the comment to, bail, otherwise flip array back around for use below
if ( ! count( $emails ) ) {
return false;
} else {
$emails = array_flip( $emails );
}
$switched_locale = switch_to_locale( get_locale() );
$comment_author_domain = @gethostbyaddr( $comment->comment_author_IP );
// The blogname option is escaped with esc_html on the way into the database in sanitize_option
// we want to reverse this for the plain text arena of emails.
$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
$comment_content = wp_specialchars_decode( $comment->comment_content );
function is_user_connected( $email ) {
$user = get_user_by( 'email', $email );
return Jetpack::is_user_connected( $user->ID );
}
$moderate_on_wpcom = ! in_array( false, array_map( 'is_user_connected', $emails ) );
$primary_site_slug = Jetpack::build_raw_urls( get_home_url() );
switch ( $comment->comment_type ) {
case 'trackback':
/* translators: 1: Post title */
$notify_message = sprintf( __( 'New trackback on your post "%s"' ), $post->post_title ) . "\r\n";
/* translators: 1: Trackback/pingback website name, 2: website IP address, 3: website hostname */
$notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
$notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
$notify_message .= __( 'You can see all trackbacks on this post here:' ) . "\r\n";
/* translators: 1: blog name, 2: post title */
$subject = sprintf( __( '[%1$s] Trackback: "%2$s"' ), $blogname, $post->post_title );
break;
case 'pingback':
/* translators: 1: Post title */
$notify_message = sprintf( __( 'New pingback on your post "%s"' ), $post->post_title ) . "\r\n";
/* translators: 1: Trackback/pingback website name, 2: website IP address, 3: website hostname */
$notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
$notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
$notify_message .= __( 'You can see all pingbacks on this post here:' ) . "\r\n";
/* translators: 1: blog name, 2: post title */
$subject = sprintf( __( '[%1$s] Pingback: "%2$s"' ), $blogname, $post->post_title );
break;
default: // Comments
$notify_message = sprintf( __( 'New comment on your post "%s"' ), $post->post_title ) . "\r\n";
/* translators: 1: comment author, 2: comment author's IP address, 3: comment author's hostname */
$notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
$notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n";
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
$notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
$notify_message .= __( 'You can see all comments on this post here:' ) . "\r\n";
/* translators: 1: blog name, 2: post title */
$subject = sprintf( __( '[%1$s] Comment: "%2$s"' ), $blogname, $post->post_title );
break;
}
$notify_message .= $moderate_on_wpcom
? "https://wordpress.com/comments/all/{$primary_site_slug}/{$comment->comment_post_ID}/\r\n\r\n"
: get_permalink( $comment->comment_post_ID ) . "#comments\r\n\r\n";
$notify_message .= sprintf( __( 'Permalink: %s' ), get_comment_link( $comment ) ) . "\r\n";
if ( user_can( $post->post_author, 'edit_comment', $comment->comment_ID ) ) {
if ( EMPTY_TRASH_DAYS ) {
$notify_message .= sprintf(
__( 'Trash it: %s' ), $moderate_on_wpcom
? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=trash"
: admin_url( "comment.php?action=trash&c={$comment->comment_ID}#wpbody-content" )
) . "\r\n";
} else {
$notify_message .= sprintf(
__( 'Delete it: %s' ), $moderate_on_wpcom
? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=delete"
: admin_url( "comment.php?action=delete&c={$comment->comment_ID}#wpbody-content" )
) . "\r\n";
}
$notify_message .= sprintf(
__( 'Spam it: %s' ), $moderate_on_wpcom ?
"https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=spam"
: admin_url( "comment.php?action=spam&c={$comment->comment_ID}#wpbody-content" )
) . "\r\n";
}
$wp_email = 'wordpress@' . preg_replace( '#^www\.#', '', strtolower( $_SERVER['SERVER_NAME'] ) );
if ( '' == $comment->comment_author ) {
$from = "From: \"$blogname\" <$wp_email>";
if ( '' != $comment->comment_author_email ) {
$reply_to = "Reply-To: $comment->comment_author_email";
}
} else {
$from = "From: \"$comment->comment_author\" <$wp_email>";
if ( '' != $comment->comment_author_email ) {
$reply_to = "Reply-To: \"$comment->comment_author_email\" <$comment->comment_author_email>";
}
}
$message_headers = "$from\n"
. 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . "\"\n";
if ( isset( $reply_to ) ) {
$message_headers .= $reply_to . "\n";
}
/** This filter is documented in core/src/wp-includes/pluggable.php */
$notify_message = apply_filters( 'comment_notification_text', $notify_message, $comment->comment_ID );
/** This filter is documented in core/src/wp-includes/pluggable.php */
$subject = apply_filters( 'comment_notification_subject', $subject, $comment->comment_ID );
/** This filter is documented in core/src/wp-includes/pluggable.php */
$message_headers = apply_filters( 'comment_notification_headers', $message_headers, $comment->comment_ID );
foreach ( $emails as $email ) {
@wp_mail( $email, wp_specialchars_decode( $subject ), $notify_message, $message_headers );
}
if ( $switched_locale ) {
restore_previous_locale();
}
return true;
}
endif;
if ( ! function_exists( 'wp_notify_moderator' ) && Jetpack::is_active() ) :
/**
* Notifies the moderator of the site about a new comment that is awaiting approval.
*
* @since 1.0.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* Uses the {@see 'notify_moderator'} filter to determine whether the site moderator
* should be notified, overriding the site setting.
*
* @param int $comment_id Comment ID.
* @return true Always returns true.
*/
function wp_notify_moderator( $comment_id ) {
global $wpdb;
$maybe_notify = get_option( 'moderation_notify' );
/** This filter is documented in core/src/wp-includes/pluggable.php */
$maybe_notify = apply_filters( 'notify_moderator', $maybe_notify, $comment_id );
if ( ! $maybe_notify ) {
return true;
}
$comment = get_comment( $comment_id );
$post = get_post( $comment->comment_post_ID );
$user = get_userdata( $post->post_author );
// Send to the administration and to the post author if the author can modify the comment.
$emails = array( get_option( 'admin_email' ) );
if ( $user && user_can( $user->ID, 'edit_comment', $comment_id ) && ! empty( $user->user_email ) ) {
if ( 0 !== strcasecmp( $user->user_email, get_option( 'admin_email' ) ) ) {
$emails[] = $user->user_email;
}
}
$switched_locale = switch_to_locale( get_locale() );
$comment_author_domain = @gethostbyaddr( $comment->comment_author_IP );
$comments_waiting = $wpdb->get_var( "SELECT count(comment_ID) FROM $wpdb->comments WHERE comment_approved = '0'" );
// The blogname option is escaped with esc_html on the way into the database in sanitize_option
// we want to reverse this for the plain text arena of emails.
$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
$comment_content = wp_specialchars_decode( $comment->comment_content );
switch ( $comment->comment_type ) {
case 'trackback':
/* translators: 1: Post title */
$notify_message = sprintf( __( 'A new trackback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
$notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
/* translators: 1: Trackback/pingback website name, 2: website IP address, 3: website hostname */
$notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
/* translators: 1: Trackback/pingback/comment author URL */
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
$notify_message .= __( 'Trackback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n";
break;
case 'pingback':
/* translators: 1: Post title */
$notify_message = sprintf( __( 'A new pingback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
$notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
/* translators: 1: Trackback/pingback website name, 2: website IP address, 3: website hostname */
$notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
/* translators: 1: Trackback/pingback/comment author URL */
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
$notify_message .= __( 'Pingback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n";
break;
default: // Comments
/* translators: 1: Post title */
$notify_message = sprintf( __( 'A new comment on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
$notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
/* translators: 1: Comment author name, 2: comment author's IP address, 3: comment author's hostname */
$notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
/* translators: 1: Comment author URL */
$notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n";
/* translators: 1: Trackback/pingback/comment author URL */
$notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
/* translators: 1: Comment text */
$notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
break;
}
/** This filter is documented in core/src/wp-includes/pluggable.php */
$emails = apply_filters( 'comment_moderation_recipients', $emails, $comment_id );
function is_user_connected( $email ) {
$user = get_user_by( 'email', $email );
return Jetpack::is_user_connected( $user->ID );
}
$moderate_on_wpcom = ! in_array( false, array_map( 'is_user_connected', $emails ) );
$primary_site_slug = Jetpack::build_raw_urls( get_home_url() );
/* translators: Comment moderation. 1: Comment action URL */
$notify_message .= sprintf(
__( 'Approve it: %s' ), $moderate_on_wpcom
? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=approve"
: admin_url( "comment.php?action=approve&c={$comment_id}#wpbody-content" )
) . "\r\n";
if ( EMPTY_TRASH_DAYS ) {
/* translators: Comment moderation. 1: Comment action URL */
$notify_message .= sprintf(
__( 'Trash it: %s' ), $moderate_on_wpcom
? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=trash"
: admin_url( "comment.php?action=trash&c={$comment_id}#wpbody-content" )
) . "\r\n";
} else {
/* translators: Comment moderation. 1: Comment action URL */
$notify_message .= sprintf(
__( 'Delete it: %s' ), $moderate_on_wpcom
? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=delete"
: admin_url( "comment.php?action=delete&c={$comment_id}#wpbody-content" )
) . "\r\n";
}
/* translators: Comment moderation. 1: Comment action URL */
$notify_message .= sprintf(
__( 'Spam it: %s' ), $moderate_on_wpcom
? "https://wordpress.com/comment/{$primary_site_slug}/{$comment_id}?action=spam"
: admin_url( "comment.php?action=spam&c={$comment_id}#wpbody-content" )
) . "\r\n";
/* translators: Comment moderation. 1: Number of comments awaiting approval */
$notify_message .= sprintf(
_n(
'Currently %s comment is waiting for approval. Please visit the moderation panel:',
'Currently %s comments are waiting for approval. Please visit the moderation panel:', $comments_waiting
), number_format_i18n( $comments_waiting )
) . "\r\n";
$notify_message .= $moderate_on_wpcom
? "https://wordpress.com/comments/pending/{$primary_site_slug}/"
: admin_url( 'edit-comments.php?comment_status=moderated#wpbody-content' ) . "\r\n";
/* translators: Comment moderation notification email subject. 1: Site name, 2: Post title */
$subject = sprintf( __( '[%1$s] Please moderate: "%2$s"' ), $blogname, $post->post_title );
$message_headers = '';
/** This filter is documented in core/src/wp-includes/pluggable.php */
$notify_message = apply_filters( 'comment_moderation_text', $notify_message, $comment_id );
/** This filter is documented in core/src/wp-includes/pluggable.php */
$subject = apply_filters( 'comment_moderation_subject', $subject, $comment_id );
/** This filter is documented in core/src/wp-includes/pluggable.php */
$message_headers = apply_filters( 'comment_moderation_headers', $message_headers, $comment_id );
foreach ( $emails as $email ) {
@wp_mail( $email, wp_specialchars_decode( $subject ), $notify_message, $message_headers );
}
if ( $switched_locale ) {
restore_previous_locale();
}
return true;
}
endif;

View File

@@ -0,0 +1,913 @@
<?php
/**
* Gets and renders iCal feeds for the Upcoming Events widget and shortcode
*/
class iCalendarReader {
public $todo_count = 0;
public $event_count = 0;
public $cal = array();
public $_lastKeyWord = '';
public $timezone = null;
/**
* Class constructor
*
* @return void
*/
public function __construct() {}
/**
* Return an array of events
*
* @param string $url (default: '')
* @return array | false on failure
*/
public function get_events( $url = '', $count = 5 ) {
$count = (int) $count;
$transient_id = 'icalendar_vcal_' . md5( $url ) . '_' . $count;
$vcal = get_transient( $transient_id );
if ( ! empty( $vcal ) ) {
if ( isset( $vcal['TIMEZONE'] ) )
$this->timezone = $this->timezone_from_string( $vcal['TIMEZONE'] );
if ( isset( $vcal['VEVENT'] ) ) {
$vevent = $vcal['VEVENT'];
if ( $count > 0 )
$vevent = array_slice( $vevent, 0, $count );
$this->cal['VEVENT'] = $vevent;
return $this->cal['VEVENT'];
}
}
if ( ! $this->parse( $url ) )
return false;
$vcal = array();
if ( $this->timezone ) {
$vcal['TIMEZONE'] = $this->timezone->getName();
} else {
$this->timezone = $this->timezone_from_string( '' );
}
if ( ! empty( $this->cal['VEVENT'] ) ) {
$vevent = $this->cal['VEVENT'];
// check for recurring events
// $vevent = $this->add_recurring_events( $vevent );
// remove before caching - no sense in hanging onto the past
$vevent = $this->filter_past_and_recurring_events( $vevent );
// order by soonest start date
$vevent = $this->sort_by_recent( $vevent );
$vcal['VEVENT'] = $vevent;
}
set_transient( $transient_id, $vcal, HOUR_IN_SECONDS );
if ( !isset( $vcal['VEVENT'] ) )
return false;
if ( $count > 0 )
return array_slice( $vcal['VEVENT'], 0, $count );
return $vcal['VEVENT'];
}
function apply_timezone_offset( $events ) {
if ( ! $events ) {
return $events;
}
// get timezone offset from the timezone name.
$timezone_name = get_option( 'timezone_string' );
if ( $timezone_name ) {
$timezone = new DateTimeZone( $timezone_name );
$timezone_offset_interval = false;
} else {
// If the timezone isn't set then the GMT offset must be set.
// generate a DateInterval object from the timezone offset
$gmt_offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
$timezone_offset_interval = date_interval_create_from_date_string( "{$gmt_offset} seconds" );
$timezone = new DateTimeZone( 'UTC' );
}
$offsetted_events = array();
foreach ( $events as $event ) {
// Don't handle all-day events
if ( 8 < strlen( $event['DTSTART'] ) ) {
$start_time = preg_replace( '/Z$/', '', $event['DTSTART'] );
$start_time = new DateTime( $start_time, $this->timezone );
$start_time->setTimeZone( $timezone );
$end_time = preg_replace( '/Z$/', '', $event['DTEND'] );
$end_time = new DateTime( $end_time, $this->timezone );
$end_time->setTimeZone( $timezone );
if ( $timezone_offset_interval ) {
$start_time->add( $timezone_offset_interval );
$end_time->add( $timezone_offset_interval );
}
$event['DTSTART'] = $start_time->format( 'YmdHis\Z' );
$event['DTEND'] = $end_time->format( 'YmdHis\Z' );
}
$offsetted_events[] = $event;
}
return $offsetted_events;
}
protected function filter_past_and_recurring_events( $events ) {
$upcoming = array();
$set_recurring_events = array();
$recurrences = array();
/**
* This filter allows any time to be passed in for testing or changing timezones, etc...
*
* @module widgets
*
* @since 3.4.0
*
* @param object time() A time object.
*/
$current = apply_filters( 'ical_get_current_time', time() );
foreach ( $events as $event ) {
$date_from_ics = strtotime( $event['DTSTART'] );
if ( isset( $event['DTEND'] ) ) {
$duration = strtotime( $event['DTEND'] ) - strtotime( $event['DTSTART'] );
} else {
$duration = 0;
}
if ( isset( $event['RRULE'] ) && $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
try {
$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone('UTC') );
$adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
$date_from_ics = strtotime( $event['DTSTART'] );
$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
} catch ( Exception $e ) {
// Invalid argument to DateTime
}
if ( isset( $event['EXDATE'] ) ) {
$exdates = array();
foreach ( (array) $event['EXDATE'] as $exdate ) {
try {
$adjusted_time = new DateTime( $exdate, new DateTimeZone('UTC') );
$adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
if ( 8 == strlen( $event['DTSTART'] ) ) {
$exdates[] = $adjusted_time->format( 'Ymd' );
} else {
$exdates[] = $adjusted_time->format( 'Ymd\THis' );
}
} catch ( Exception $e ) {
// Invalid argument to DateTime
}
}
$event['EXDATE'] = $exdates;
} else {
$event['EXDATE'] = array();
}
}
if ( ! isset( $event['DTSTART'] ) ) {
continue;
}
// Process events with RRULE before other events
$rrule = isset( $event['RRULE'] ) ? $event['RRULE'] : false ;
$uid = $event['UID'];
if ( $rrule && ! in_array( $uid, $set_recurring_events ) ) {
// Break down the RRULE into digestible chunks
$rrule_array = array();
foreach ( explode( ";", $event['RRULE'] ) as $rline ) {
list( $rkey, $rvalue ) = explode( "=", $rline, 2 );
$rrule_array[$rkey] = $rvalue;
}
$interval = ( isset( $rrule_array['INTERVAL'] ) ) ? $rrule_array['INTERVAL'] : 1;
$rrule_count = ( isset( $rrule_array['COUNT'] ) ) ? $rrule_array['COUNT'] : 0;
$until = ( isset( $rrule_array['UNTIL'] ) ) ? strtotime( $rrule_array['UNTIL'] ) : strtotime( '+1 year', $current );
// Used to bound event checks
$echo_limit = 10;
$noop = false;
// Set bydays for the event
$weekdays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );
$bydays = $weekdays;
// Calculate a recent start date for incrementing depending on the frequency and interval
switch ( $rrule_array['FREQ'] ) {
case 'DAILY':
$frequency = 'day';
$echo_limit = 10;
if ( $date_from_ics >= $current ) {
$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
} else {
// Interval and count
$catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * DAY_IN_SECONDS ) );
if ( $rrule_count && $catchup > 0 ) {
if ( $catchup < $rrule_count ) {
$rrule_count = $rrule_count - $catchup;
$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
} else {
$noop = true;
}
} else {
$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
}
}
break;
case 'WEEKLY':
$frequency = 'week';
$echo_limit = 4;
// BYDAY exception to current date
$day = false;
if ( ! isset( $rrule_array['BYDAY'] ) ) {
$day = $rrule_array['BYDAY'] = strtoupper( substr( date( 'D', strtotime( $event['DTSTART'] ) ), 0, 2 ) );
}
$bydays = explode( ',', $rrule_array['BYDAY'] );
if ( $date_from_ics >= $current ) {
$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
} else {
// Interval and count
$catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * WEEK_IN_SECONDS ) );
if ( $rrule_count && $catchup > 0 ) {
if ( ( $catchup * count( $bydays ) ) < $rrule_count ) {
$rrule_count = $rrule_count - ( $catchup * count( $bydays ) ); // Estimate current event count
$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
} else {
$noop = true;
}
} else {
$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
}
}
// Set to Sunday start
if ( ! $noop && 'SU' !== strtoupper( substr( date( 'D', strtotime( $recurring_event_date_start ) ), 0, 2 ) ) ) {
$recurring_event_date_start = date( 'Ymd', strtotime( "last Sunday", strtotime( $recurring_event_date_start ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
}
break;
case 'MONTHLY':
$frequency = 'month';
$echo_limit = 1;
if ( $date_from_ics >= $current ) {
$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
} else {
// Describe the date in the month
if ( isset( $rrule_array['BYDAY'] ) ) {
$day_number = substr( $rrule_array['BYDAY'], 0, 1 );
$week_day = substr( $rrule_array['BYDAY'], 1 );
$day_cardinals = array( 1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth' );
$weekdays = array( 'SU' => 'Sunday', 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday' );
$event_date_desc = "{$day_cardinals[$day_number]} {$weekdays[$week_day]} of ";
} else {
$event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) );
}
// Interval only
if ( $interval > 1 ) {
$catchup = 0;
$maybe = strtotime( $event['DTSTART'] );
while ( $maybe < $current ) {
$maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) );
$catchup++;
}
$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * ( $catchup - 1 ) ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
} else {
$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
}
// Add one interval if necessary
if ( strtotime( $recurring_event_date_start ) < $current ) {
if ( $interval > 1 ) {
$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
} else {
try {
$adjustment = new DateTime( date( 'Y-m-d', $current ) );
$adjustment->modify( 'first day of next month' );
$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
} catch ( Exception $e ) {
// Invalid argument to DateTime
}
}
}
}
break;
case 'YEARLY':
$frequency = 'year';
$echo_limit = 1;
if ( $date_from_ics >= $current ) {
$recurring_event_date_start = date( "Ymd\THis", strtotime( $event['DTSTART'] ) );
} else {
$recurring_event_date_start = date( 'Y', $current ) . date( "md\THis", strtotime( $event['DTSTART'] ) );
if ( strtotime( $recurring_event_date_start ) < $current ) {
try {
$next = new DateTime( date( 'Y-m-d', $current ) );
$next->modify( 'first day of next year' );
$recurring_event_date_start = $next->format( 'Y' ) . date ( 'md\THis', strtotime( $event['DTSTART'] ) );
} catch ( Exception $e ) {
// Invalid argument to DateTime
}
}
}
break;
default:
$frequency = false;
}
if ( $frequency !== false && ! $noop ) {
$count_counter = 1;
// If no COUNT limit, go to 10
if ( empty( $rrule_count ) ) {
$rrule_count = 10;
}
// Set up EXDATE handling for the event
$exdates = ( isset( $event['EXDATE'] ) ) ? $event['EXDATE'] : array();
for ( $i = 1; $i <= $echo_limit; $i++ ) {
// Weeks need a daily loop and must check for inclusion in BYDAYS
if ( 'week' == $frequency ) {
$byday_event_date_start = strtotime( $recurring_event_date_start );
foreach ( $weekdays as $day ) {
$event_start_timestamp = $byday_event_date_start;
$start_time = date( 'His', $event_start_timestamp );
$event_end_timestamp = $event_start_timestamp + $duration;
$end_time = date( 'His', $event_end_timestamp );
if ( 8 == strlen( $event['DTSTART'] ) ) {
$exdate_compare = date( 'Ymd', $event_start_timestamp );
} else {
$exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
}
if ( in_array( $day, $bydays ) && $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) {
if ( 8 == strlen( $event['DTSTART'] ) ) {
$event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
$event['DTEND'] = date( 'Ymd', $event_end_timestamp );
} else {
$event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp );
$event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp );
}
if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
try {
$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
} catch ( Exception $e ) {
// Invalid argument to DateTime
}
}
$upcoming[] = $event;
$count_counter++;
}
// Move forward one day
$byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time );
}
// Restore first event timestamp
$event_start_timestamp = strtotime( $recurring_event_date_start );
} else {
$event_start_timestamp = strtotime( $recurring_event_date_start );
$start_time = date( 'His', $event_start_timestamp );
$event_end_timestamp = $event_start_timestamp + $duration;
$end_time = date( 'His', $event_end_timestamp );
if ( 8 == strlen( $event['DTSTART'] ) ) {
$exdate_compare = date( 'Ymd', $event_start_timestamp );
} else {
$exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
}
if ( $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) {
if ( 8 == strlen( $event['DTSTART'] ) ) {
$event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
$event['DTEND'] = date( 'Ymd', $event_end_timestamp );
} else {
$event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time;
$event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time;
}
if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
try {
$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
} catch ( Exception $e ) {
// Invalid argument to DateTime
}
}
$upcoming[] = $event;
$count_counter++;
}
}
// Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact
$next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp );
if ( 8 == strlen( $event['DTSTART'] ) ) {
$event['DTSTART'] = date( 'Ymd', $next_start_timestamp );
$event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration );
} else {
$event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp );
$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
}
// Move recurring event date forward
$recurring_event_date_start = $event['DTSTART'];
}
$set_recurring_events[] = $uid;
}
} else {
// Process normal events
if ( strtotime( isset( $event['DTEND'] ) ? $event['DTEND'] : $event['DTSTART'] ) >= $current ) {
$upcoming[] = $event;
}
}
}
return $upcoming;
}
/**
* Parse events from an iCalendar feed
*
* @param string $url (default: '')
* @return array | false on failure
*/
public function parse( $url = '' ) {
$cache_group = 'icalendar_reader_parse';
$disable_get_key = 'disable:' . md5( $url );
// Check to see if previous attempts have failed
if ( false !== wp_cache_get( $disable_get_key, $cache_group ) )
return false;
// rewrite webcal: URI schem to HTTP
$url = preg_replace('/^webcal/', 'http', $url );
// try to fetch
$r = wp_remote_get( $url, array( 'timeout' => 3, 'sslverify' => false ) );
if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
// We were unable to fetch any content, so don't try again for another 60 seconds
wp_cache_set( $disable_get_key, 1, $cache_group, 60 );
return false;
}
$body = wp_remote_retrieve_body( $r );
if ( empty( $body ) )
return false;
$body = str_replace( "\r\n", "\n", $body );
$lines = preg_split( "/\n(?=[A-Z])/", $body );
if ( empty( $lines ) )
return false;
if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) )
return false;
foreach ( $lines as $line ) {
$add = $this->key_value_from_string( $line );
if ( ! $add ) {
$this->add_component( $type, false, $line );
continue;
}
list( $keyword, $value ) = $add;
switch ( $keyword ) {
case 'BEGIN':
case 'END':
switch ( $line ) {
case 'BEGIN:VTODO':
$this->todo_count++;
$type = 'VTODO';
break;
case 'BEGIN:VEVENT':
$this->event_count++;
$type = 'VEVENT';
break;
case 'BEGIN:VCALENDAR':
case 'BEGIN:DAYLIGHT':
case 'BEGIN:VTIMEZONE':
case 'BEGIN:STANDARD':
$type = $value;
break;
case 'END:VTODO':
case 'END:VEVENT':
case 'END:VCALENDAR':
case 'END:DAYLIGHT':
case 'END:VTIMEZONE':
case 'END:STANDARD':
$type = 'VCALENDAR';
break;
}
break;
case 'TZID':
if ( 'VTIMEZONE' == $type && ! $this->timezone )
$this->timezone = $this->timezone_from_string( $value );
break;
case 'X-WR-TIMEZONE':
if ( ! $this->timezone )
$this->timezone = $this->timezone_from_string( $value );
break;
default:
$this->add_component( $type, $keyword, $value );
break;
}
}
// Filter for RECURRENCE-IDs
$recurrences = array();
if ( array_key_exists( 'VEVENT', $this->cal ) ) {
foreach ( $this->cal['VEVENT'] as $event ) {
if ( isset( $event['RECURRENCE-ID'] ) ) {
$recurrences[] = $event;
}
}
foreach ( $recurrences as $recurrence ) {
for ( $i = 0; $i < count( $this->cal['VEVENT'] ); $i++ ) {
if ( $this->cal['VEVENT'][ $i ]['UID'] == $recurrence['UID'] && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] ) ) {
$this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID'];
break;
}
}
}
}
return $this->cal;
}
/**
* Parse key:value from a string
*
* @param string $text (default: '')
* @return array
*/
public function key_value_from_string( $text = '' ) {
preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches );
if ( 0 == count( $matches ) )
return false;
return array( $matches[1], $matches[3] );
}
/**
* Convert a timezone name into a timezone object.
*
* @param string $text Timezone name. Example: America/Chicago
* @return object|null A DateTimeZone object if the conversion was successful.
*/
private function timezone_from_string( $text ) {
try {
$timezone = new DateTimeZone( $text );
} catch ( Exception $e ) {
$blog_timezone = get_option( 'timezone_string' );
if ( ! $blog_timezone ) {
$blog_timezone = 'Etc/UTC';
}
$timezone = new DateTimeZone( $blog_timezone );
}
return $timezone;
}
/**
* Add a component to the calendar array
*
* @param string $component (default: '')
* @param string $keyword (default: '')
* @param string $value (default: '')
* @return void
*/
public function add_component( $component = '', $keyword = '', $value = '' ) {
if ( false == $keyword ) {
$keyword = $this->last_keyword;
switch ( $component ) {
case 'VEVENT':
$value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value;
break;
case 'VTODO' :
$value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value;
break;
}
}
/*
* Some events have a specific timezone set in their start/end date,
* and it may or may not be different than the calendar timzeone.
* Valid formats include:
* DTSTART;TZID=Pacific Standard Time:20141219T180000
* DTEND;TZID=Pacific Standard Time:20141219T200000
* EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
* EXDATE;VALUE=DATE:2015050
* EXDATE;TZID=America/New_York:20150424T170000
* EXDATE;TZID=Pacific Standard Time:20120615T140000,20120629T140000,20120706T140000
*/
// Always store EXDATE as an array
if ( stristr( $keyword, 'EXDATE' ) ) {
$value = explode( ',', $value );
}
// Adjust DTSTART, DTEND, and EXDATE according to their TZID if set
if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) {
$keyword = explode( ';', $keyword );
$tzid = false;
if ( 2 == count( $keyword ) ) {
$tparam = $keyword[1];
if ( strpos( $tparam, "TZID" ) !== false ) {
$tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) );
}
}
// Normalize all times to default UTC
if ( $tzid ) {
$adjusted_times = array();
foreach ( (array) $value as $v ) {
try {
$adjusted_time = new DateTime( $v, $tzid );
$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
$adjusted_times[] = $adjusted_time->format('Ymd\THis');
} catch ( Exception $e ) {
// Invalid argument to DateTime
return;
}
}
$value = $adjusted_times;
}
// Format for adding to event
$keyword = $keyword[0];
if ( 'EXDATE' != $keyword ) {
$value = implode( (array) $value );
}
}
foreach ( (array) $value as $v ) {
switch ($component) {
case 'VTODO':
if ( 'EXDATE' == $keyword ) {
$this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v;
} else {
$this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v;
}
break;
case 'VEVENT':
if ( 'EXDATE' == $keyword ) {
$this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v;
} else {
$this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v;
}
break;
default:
$this->cal[ $component ][ $keyword ] = $v;
break;
}
}
$this->last_keyword = $keyword;
}
/**
* Escape strings with wp_kses, allow links
*
* @param string $string (default: '')
* @return string
*/
public function escape( $string = '' ) {
// Unfold content lines per RFC 5545
$string = str_replace( "\n\t", '', $string );
$string = str_replace( "\n ", '', $string );
$allowed_html = array(
'a' => array(
'href' => array(),
'title' => array()
)
);
$allowed_tags = '';
foreach ( array_keys( $allowed_html ) as $tag ) {
$allowed_tags .= "<{$tag}>";
}
// Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc
// because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra
// safety and good measure.
return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html );
}
/**
* Render the events
*
* @param string $url (default: '')
* @param string $context (default: 'widget') or 'shortcode'
* @return mixed bool|string false on failure, rendered HTML string on success.
*/
public function render( $url = '', $args = array() ) {
$args = wp_parse_args( $args, array(
'context' => 'widget',
'number' => 5
) );
$events = $this->get_events( $url, $args['number'] );
$events = $this->apply_timezone_offset( $events );
if ( empty( $events ) )
return false;
ob_start();
if ( 'widget' == $args['context'] ) : ?>
<ul class="upcoming-events">
<?php foreach ( $events as $event ) : ?>
<li>
<strong class="event-summary"><?php echo $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></strong>
<span class="event-when"><?php echo $this->formatted_date( $event ); ?></span>
<?php if ( ! empty( $event['LOCATION'] ) ) : ?>
<span class="event-location"><?php echo $this->escape( stripslashes( $event['LOCATION'] ) ); ?></span>
<?php endif; ?>
<?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
<span class="event-description"><?php echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif;
if ( 'shortcode' == $args['context'] ) : ?>
<table class="upcoming-events">
<thead>
<tr>
<th><?php esc_html_e( 'Location', 'jetpack' ); ?></th>
<th><?php esc_html_e( 'When', 'jetpack' ); ?></th>
<th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th>
<th><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $events as $event ) : ?>
<tr>
<td><?php echo empty( $event['LOCATION'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['LOCATION'] ) ); ?></td>
<td><?php echo $this->formatted_date( $event ); ?></td>
<td><?php echo empty( $event['SUMMARY'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></td>
<td><?php echo empty( $event['DESCRIPTION'] ) ? '&nbsp;' : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif;
$rendered = ob_get_clean();
if ( empty( $rendered ) )
return false;
return $rendered;
}
public function formatted_date( $event ) {
$date_format = get_option( 'date_format' );
$time_format = get_option( 'time_format' );
$start = strtotime( $event['DTSTART'] );
$end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false;
$all_day = ( 8 == strlen( $event['DTSTART'] ) );
if ( !$all_day && $this->timezone ) {
try {
$start_time = new DateTime( $event['DTSTART'] );
$timezone_offset = $this->timezone->getOffset( $start_time );
$start += $timezone_offset;
if ( $end ) {
$end += $timezone_offset;
}
} catch ( Exception $e ) {
// Invalid argument to DateTime
}
}
$single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true;
/* translators: Date and time */
$date_with_time = __( '%1$s at %2$s' , 'jetpack' );
/* translators: Two dates with a separator */
$two_dates = __( '%1$s &ndash; %2$s' , 'jetpack' );
// we'll always have the start date. Maybe with time
if ( $all_day )
$date = date_i18n( $date_format, $start );
else
$date = sprintf( $date_with_time, date_i18n( $date_format, $start ), date_i18n( $time_format, $start ) );
// single day, timed
if ( $single_day && ! $all_day && false !== $end )
$date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) );
// multi-day
if ( ! $single_day ) {
if ( $all_day ) {
// DTEND for multi-day events represents "until", not "including", so subtract one minute
$end_date = date_i18n( $date_format, $end - 60 );
} else {
$end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) );
}
$date = sprintf( $two_dates, $date, $end_date );
}
return $date;
}
protected function sort_by_recent( $list ) {
$dates = $sorted_list = array();
foreach ( $list as $key => $row ) {
$date = $row['DTSTART'];
// pad some time onto an all day date
if ( 8 === strlen( $date ) )
$date .= 'T000000Z';
$dates[$key] = $date;
}
asort( $dates );
foreach( $dates as $key => $value ) {
$sorted_list[$key] = $list[$key];
}
unset($list);
return $sorted_list;
}
}
/**
* Wrapper function for iCalendarReader->get_events()
*
* @param string $url (default: '')
* @return array
*/
function icalendar_get_events( $url = '', $count = 5 ) {
// Find your calendar's address http://support.google.com/calendar/bin/answer.py?hl=en&answer=37103
$ical = new iCalendarReader();
return $ical->get_events( $url, $count );
}
/**
* Wrapper function for iCalendarReader->render()
*
* @param string $url (default: '')
* @param string $context (default: 'widget') or 'shortcode'
* @return mixed bool|string false on failure, rendered HTML string on success.
*/
function icalendar_render_events( $url = '', $args = array() ) {
$ical = new iCalendarReader();
return $ical->render( $url, $args );
}

View File

@@ -0,0 +1,341 @@
<?php
/**
* Provides an interface for easily building a complex search query that
* combines multiple ranking signals.
*
*
* $bldr = new Jetpack_WPES_Query_Builder();
* $bldr->add_filter( ... );
* $bldr->add_filter( ... );
* $bldr->add_query( ... );
* $es_query = $bldr->build_query();
*
*
* All ES queries take a standard form with main query (with some filters),
* wrapped in a function_score
*
* Bucketed queries use an aggregation to diversify results. eg a bunch
* of separate filters where to get different sets of results.
*
*/
class Jetpack_WPES_Query_Builder {
protected $es_filters = array();
// Custom boosting with function_score
protected $functions = array();
protected $decays = array();
protected $scripts = array();
protected $functions_max_boost = 2.0;
protected $functions_score_mode = 'multiply';
protected $query_bool_boost = null;
// General aggregations for buckets and metrics
protected $aggs_query = false;
protected $aggs = array();
// The set of top level text queries to combine
protected $must_queries = array();
protected $should_queries = array();
protected $dis_max_queries = array();
protected $diverse_buckets_query = false;
protected $bucket_filters = array();
protected $bucket_sub_aggs = array();
////////////////////////////////////
// Methods for building a query
public function add_filter( $filter ) {
$this->es_filters[] = $filter;
}
public function add_query( $query, $type = 'must' ) {
switch ( $type ) {
case 'dis_max':
$this->dis_max_queries[] = $query;
break;
case 'should':
$this->should_queries[] = $query;
break;
case 'must':
default:
$this->must_queries[] = $query;
break;
}
}
/**
* Add a scoring function to the query
*
* NOTE: For decays (linear, exp, or gauss), use Jetpack_WPES_Query_Builder::add_decay() instead
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
*
* @param $function string name of the function
* @param $params array functions parameters
*
* @return void
*/
public function add_function( $function, $params ) {
$this->functions[ $function ][] = $params;
}
/**
* Add a decay function to score results
*
* This method should be used instead of Jetpack_WPES_Query_Builder::add_function() for decays, as the internal ES structure
* is slightly different for them.
*
* @see https://www.elastic.co/guide/en/elasticsearch/guide/current/decay-functions.html
*
* @param $function string name of the decay function - linear, exp, or gauss
* @param $params array The decay functions parameters, passed to ES directly
*
* @return void
*/
public function add_decay( $function, $params ) {
$this->decays[ $function ][] = $params;
}
/**
* Add a scoring mode to the query
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
*
* @param $mode string name of how to score
*
* @return void
*/
public function add_score_mode_to_functions( $mode='multiply' ) {
$this->functions_score_mode = $mode;
}
public function add_max_boost_to_functions( $boost ) {
$this->functions_max_boost = $boost;
}
public function add_boost_to_query_bool( $boost ) {
$this->query_bool_boost = $boost;
}
public function add_aggs( $aggs_name, $aggs ) {
$this->aggs_query = true;
$this->aggs[$aggs_name] = $aggs;
}
public function add_aggs_sub_aggs( $aggs_name, $sub_aggs ) {
if ( ! array_key_exists( 'aggs', $this->aggs[$aggs_name] ) ) {
$this->aggs[$aggs_name]['aggs'] = array();
}
$this->aggs[$aggs_name]['aggs'] = $sub_aggs;
}
public function add_bucketed_query( $name, $query ) {
$this->_add_bucket_filter( $name, $query );
$this->add_query( $query, 'dis_max' );
}
public function add_bucketed_terms( $name, $field, $terms, $boost = 1 ) {
if ( ! is_array( $terms ) ) {
$terms = array( $terms );
}
$this->_add_bucket_filter( $name, array(
'terms' => array(
$field => $terms,
),
));
$this->add_query( array(
'constant_score' => array(
'filter' => array(
'terms' => array(
$field => $terms,
),
),
'boost' => $boost,
),
), 'dis_max' );
}
public function add_bucket_sub_aggs( $agg ) {
$this->bucket_sub_aggs = array_merge( $this->bucket_sub_aggs, $agg );
}
protected function _add_bucket_filter( $name, $filter ) {
$this->diverse_buckets_query = true;
$this->bucket_filters[ $name ] = $filter;
}
////////////////////////////////////
// Building Final Query
/**
* Combine all the queries, functions, decays, scripts, and max_boost into an ES query
*
* @return array Array representation of the built ES query
*/
public function build_query() {
$query = array();
//dis_max queries just become a single must query
if ( ! empty( $this->dis_max_queries ) ) {
$this->must_queries[] = array(
'dis_max' => array(
'queries' => $this->dis_max_queries,
),
);
}
if ( empty( $this->must_queries ) ) {
$this->must_queries = array(
array(
'match_all' => array(),
),
);
}
if ( empty( $this->should_queries ) ) {
if ( 1 == count( $this->must_queries ) ) {
$query = $this->must_queries[0];
} else {
$query = array(
'bool' => array(
'must' => $this->must_queries,
),
);
}
} else {
$query = array(
'bool' => array(
'must' => $this->must_queries,
'should' => $this->should_queries,
),
);
}
if ( ! is_null( $this->query_bool_boost ) && isset( $query['bool'] ) ) {
$query['bool']['boost'] = $this->query_bool_boost;
}
// If there are any function score adjustments, then combine those
if ( $this->functions || $this->decays || $this->scripts ) {
$weighting_functions = array();
if ( $this->functions ) {
foreach ( $this->functions as $function_type => $configs ) {
foreach ( $configs as $config ) {
foreach ( $config as $field => $params ) {
$func_arr = $params;
$func_arr['field'] = $field;
$weighting_functions[] = array(
$function_type => $func_arr,
);
}
}
}
}
if ( $this->decays ) {
foreach ( $this->decays as $decay_type => $configs ) {
foreach ( $configs as $config ) {
foreach ( $config as $field => $params ) {
$weighting_functions[] = array(
$decay_type => array(
$field => $params,
),
);
}
}
}
}
if ( $this->scripts ) {
foreach ( $this->scripts as $script ) {
$weighting_functions[] = array(
'script_score' => array(
'script' => $script,
),
);
}
}
$query = array(
'function_score' => array(
'query' => $query,
'functions' => $weighting_functions,
'max_boost' => $this->functions_max_boost,
'score_mode' => $this->functions_score_mode,
),
);
} // End if().
return $query;
}
/**
* Assemble the 'filter' portion of an ES query, from all registered filters
*
* @return array|null Combined ES filters, or null if none have been defined
*/
public function build_filter() {
if ( empty( $this->es_filters ) ) {
$filter = null;
} elseif ( 1 == count( $this->es_filters ) ) {
$filter = $this->es_filters[0];
} else {
$filter = array(
'and' => $this->es_filters,
);
}
return $filter;
}
/**
* Assemble the 'aggregation' portion of an ES query, from all general aggregations.
*
* @return array An aggregation query as an array of topics, filters, and bucket names
*/
public function build_aggregation() {
if ( empty( $this->bucket_sub_aggs ) && empty( $this->aggs_query ) ) {
return array();
}
if ( ! $this->diverse_buckets_query && empty( $this->aggs_query ) ) {
return $this->bucket_sub_aggs;
}
$aggregations = array(
'topics' => array(
'filters' => array(
'filters' => array(),
),
),
);
if ( ! empty( $this->bucket_sub_aggs ) ) {
$aggregations['topics']['aggs'] = $this->bucket_sub_aggs;
}
foreach ( $this->bucket_filters as $bucket_name => $filter ) {
$aggregations['topics']['filters']['filters'][ $bucket_name ] = $filter;
}
if ( ! empty( $this->aggs_query ) ) {
$aggregations = $this->aggs;
}
return $aggregations;
}
}

View File

@@ -0,0 +1,683 @@
<?php
/**
* Parse a pure text query into WordPress Elasticsearch query. This builds on
* the Jetpack_WPES_Query_Builder() to provide search query parsing.
*
* The key part of this parser is taking a user's query string typed into a box
* and converting it into an ES search query.
*
* This varies by application, but roughly it means extracting some parts of the query
* (authors, tags, and phrases) that are treated as a filter. Then taking the
* remaining words and building the correct query (possibly with prefix searching
* if we are doing search as you type)
*
* This class only supports ES 2.x+
*
* This parser builds queries of the form:
* bool:
* must:
* AND match of a single field (ideally an edgengram field)
* filter:
* filter clauses from context (eg @gibrown, #news, etc)
* should:
* boosting of results by various fields
*
* Features supported:
* - search as you type
* - phrases
* - supports querying across multiple languages at once
*
* Example usage (from Search on Reader Manage):
*
* require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-search-query-parser' );
* $parser = new WPES_Search_Query_Parser( $args['q'], array( $lang ) );
*
* //author
* $parser->author_field_filter( array(
* 'prefixes' => array( '@' ),
* 'wpcom_id_field' => 'author_id',
* 'must_query_fields' => array( 'author.engram', 'author_login.engram' ),
* 'boost_query_fields' => array( 'author^2', 'author_login^2', 'title.default.engram' ),
* ) );
*
* //remainder of query
* $match_content_fields = $parser->merge_ml_fields(
* array(
* 'all_content' => 0.1,
* ),
* array(
* 'all_content.default.engram^0.1',
* )
* );
* $boost_content_fields = $parser->merge_ml_fields(
* array(
* 'title' => 2,
* 'description' => 1,
* 'tags' => 1,
* ),
* array(
* 'author_login^2',
* 'author^2',
* )
* );
*
* $parser->phrase_filter( array(
* 'must_query_fields' => $match_content_fields,
* 'boost_query_fields' => $boost_content_fields,
* ) );
* $parser->remaining_query( array(
* 'must_query_fields' => $match_content_fields,
* 'boost_query_fields' => $boost_content_fields,
* ) );
*
* //Boost on phrases
* $parser->remaining_query( array(
* 'boost_query_fields' => $boost_content_fields,
* 'boost_query_type' => 'phrase',
* ) );
*
* //boosting
* $parser->add_max_boost_to_functions( 20 );
* $parser->add_function( 'field_value_factor', array(
* 'follower_count' => array(
* 'modifier' => 'sqrt',
* 'factor' => 1,
* 'missing' => 0,
* ) ) );
*
* //Filtering
* $parser->add_filter( array(
* 'exists' => array( 'field' => 'langs.' . $lang )
* ) );
*
* //run the query
* $es_query_args = array(
* 'name' => 'feeds',
* 'blog_id' => false,
* 'security_strategy' => 'a8c',
* 'type' => 'feed,blog',
* 'fields' => array( 'blog_id', 'feed_id' ),
* 'query' => $parser->build_query(),
* 'filter' => $parser->build_filter(),
* 'size' => $size,
* 'from' => $from
* );
* $es_results = es_api_search_index( $es_query_args, 'api-feed-find' );
*
*/
jetpack_require_lib( 'jetpack-wpes-query-builder' );
class Jetpack_WPES_Search_Query_Parser extends Jetpack_WPES_Query_Builder {
protected $orig_query = '';
protected $current_query = '';
protected $langs;
protected $avail_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' );
public function __construct( $user_query, $langs ) {
$this->orig_query = $user_query;
$this->current_query = $this->orig_query;
$this->langs = $this->norm_langs( $langs );
}
protected $extracted_phrases = array();
///////////////////////////////////////////////////////
// Methods for Building arrays of multilingual fields
/*
* Normalize language codes
*/
public function norm_langs( $langs ) {
$lst = array();
foreach( $langs as $l ) {
$l = strtok( $l, '-_' );
if ( in_array( $l, $this->avail_langs ) ) {
$lst[$l] = true;
} else {
$lst['default'] = true;
}
}
return array_keys( $lst );
}
/*
* Take a list of field prefixes and expand them for multi-lingual
* with the provided boostings.
*/
public function merge_ml_fields( $fields2boosts, $additional_fields ) {
$flds = array();
foreach( $fields2boosts as $f => $b ) {
foreach( $this->langs as $l ) {
$flds[] = $f . '.' . $l . '^' . $b;
}
}
foreach( $additional_fields as $f ) {
$flds[] = $f;
}
return $flds;
}
////////////////////////////////////
// Extract Fields for Filtering on
/*
* Extract any @mentions from the user query
* use them as a filter if we can find a wp.com id
* otherwise use them as a
*
* args:
* wpcom_id_field: wp.com id field
* must_query_fields: array of fields to search for matching results (optional)
* boost_query_fields: array of fields to search in for boosting results (optional)
* prefixes: array of prefixes that the user can use to indicate an author
*
* returns true/false of whether any were found
*
* See also: https://github.com/twitter/twitter-text/blob/master/java/src/com/twitter/Regex.java
*/
public function author_field_filter( $args ) {
$defaults = array(
'wpcom_id_field' => 'author_id',
'must_query_fields' => null,
'boost_query_fields' => null,
'prefixes' => array( '@' ),
);
$args = wp_parse_args( $args, $defaults );
$names = array();
foreach( $args['prefixes'] as $p ) {
$found = $this->get_fields( $p );
if ( $found ) {
foreach( $found as $f ) {
$names[] = $f;
}
}
}
if ( empty( $names ) ) {
return false;
}
foreach( $args['prefixes'] as $p ) {
$this->remove_fields( $p );
}
$user_ids = array();
$query_names = array();
//loop through the matches and separate into filters and queries
foreach( $names as $n ) {
//check for exact match on login
$userdata = get_user_by( 'login', strtolower( $n ) );
$filtering = false;
if ( $userdata ) {
$user_ids[ $userdata->ID ] = true;
$filtering = true;
}
$is_phrase = false;
if ( preg_match( '/"/', $n ) ) {
$is_phrase = true;
$n = preg_replace( '/"/', '', $n );
}
if ( !empty( $args['must_query_fields'] ) && !$filtering ) {
if ( $is_phrase ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['must_query_fields'],
'query' => $n,
'type' => 'phrase',
) ) );
} else {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['must_query_fields'],
'query' => $n,
) ) );
}
}
if ( !empty( $args['boost_query_fields'] ) ) {
if ( $is_phrase ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $n,
'type' => 'phrase',
) ), 'should' );
} else {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $n,
) ), 'should' );
}
}
}
if ( ! empty( $user_ids ) ) {
$user_ids = array_keys( $user_ids );
$this->add_filter( array( 'terms' => array( $args['wpcom_id_field'] => $user_ids ) ) );
}
return true;
}
/*
* Extract any prefix followed by text use them as a must clause,
* and optionally as a boost to the should query
* This can be used for hashtags. eg #News, or #"current events",
* but also works for any arbitrary field. eg from:Greg
*
* args:
* must_query_fields: array of fields that must match the tag (optional)
* boost_query_fields: array of fields to boost search on (optional)
* prefixes: array of prefixes that the user can use to indicate a tag
*
* returns true/false of whether any were found
*
*/
public function text_field_filter( $args ) {
$defaults = array(
'must_query_fields' => array( 'tag.name' ),
'boost_query_fields' => array( 'tag.name' ),
'prefixes' => array( '#' ),
);
$args = wp_parse_args( $args, $defaults );
$tags = array();
foreach( $args['prefixes'] as $p ) {
$found = $this->get_fields( $p );
if ( $found ) {
foreach( $found as $f ) {
$tags[] = $f;
}
}
}
if ( empty( $tags ) ) {
return false;
}
foreach( $args['prefixes'] as $p ) {
$this->remove_fields( $p );
}
foreach( $tags as $t ) {
$is_phrase = false;
if ( preg_match( '/"/', $t ) ) {
$is_phrase = true;
$t = preg_replace( '/"/', '', $t );
}
if ( ! empty( $args['must_query_fields'] ) ) {
if ( $is_phrase ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['must_query_fields'],
'query' => $t,
'type' => 'phrase',
) ) );
} else {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['must_query_fields'],
'query' => $t,
) ) );
}
}
if ( ! empty( $args['boost_query_fields'] ) ) {
if ( $is_phrase ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $t,
'type' => 'phrase',
) ), 'should' );
} else {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $t,
) ), 'should' );
}
}
}
return true;
}
/*
* Extract anything surrounded by quotes or if there is an opening quote
* that is not complete, and add them to the query as a phrase query.
* Quotes can be either '' or ""
*
* args:
* must_query_fields: array of fields that must match the phrases
* boost_query_fields: array of fields to boost the phrases on (optional)
*
* returns true/false of whether any were found
*
*/
public function phrase_filter( $args ) {
$defaults = array(
'must_query_fields' => array( 'all_content' ),
'boost_query_fields' => array( 'title' ),
);
$args = wp_parse_args( $args, $defaults );
$phrases = array();
if ( preg_match_all( '/"([^"]+)"/', $this->current_query, $matches ) ) {
foreach ( $matches[1] as $match ) {
$phrases[] = $match;
}
$this->current_query = preg_replace( '/"([^"]+)"/', '', $this->current_query );
}
if ( preg_match_all( "/'([^']+)'/", $this->current_query, $matches ) ) {
foreach ( $matches[1] as $match ) {
$phrases[] = $match;
}
$this->current_query = preg_replace( "/'([^']+)'/", '', $this->current_query );
}
//look for a final, uncompleted phrase
$phrase_prefix = false;
if ( preg_match_all( '/"([^"]+)$/', $this->current_query, $matches ) ) {
$phrase_prefix = $matches[1][0];
$this->current_query = preg_replace( '/"([^"]+)$/', '', $this->current_query );
}
if ( preg_match_all( "/(?:'\B|\B')([^']+)$/", $this->current_query, $matches ) ) {
$phrase_prefix = $matches[1][0];
$this->current_query = preg_replace( "/(?:'\B|\B')([^']+)$/", '', $this->current_query );
}
if ( $phrase_prefix ) {
$phrases[] = $phrase_prefix;
}
if ( empty( $phrases ) ) {
return false;
}
foreach ( $phrases as $p ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['must_query_fields'],
'query' => $p,
'type' => 'phrase',
) ) );
if ( ! empty( $args['boost_query_fields'] ) ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $p,
'operator' => 'and',
) ), 'should' );
}
}
return true;
}
/*
* Query fields based on the remaining parts of the query
* This could be the final AND part of the query terms to match, or it
* could be boosting certain elements of the query
*
* args:
* must_query_fields: array of fields that must match the remaining terms (optional)
* boost_query_fields: array of fields to boost the remaining terms on (optional)
*
*/
public function remaining_query( $args ) {
$defaults = array(
'must_query_fields' => null,
'boost_query_fields' => null,
'boost_operator' => 'and',
'boost_query_type' => 'best_fields',
);
$args = wp_parse_args( $args, $defaults );
if ( empty( $this->current_query ) || ctype_space( $this->current_query ) ) {
return;
}
if ( ! empty( $args['must_query_fields'] ) ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['must_query_fields'],
'query' => $this->current_query,
'operator' => 'and',
) ) );
}
if ( ! empty( $args['boost_query_fields'] ) ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $this->current_query,
'operator' => $args['boost_operator'],
'type' => $args['boost_query_type'],
) ), 'should' );
}
}
/*
* Query fields using a prefix query (alphabetical expansions on the index).
* This is not recommended. Slower performance and worse relevancy.
*
* (UNTESTED! Copied from old prefix expansion code)
*
* args:
* must_query_fields: array of fields that must match the remaining terms (optional)
* boost_query_fields: array of fields to boost the remaining terms on (optional)
*
*/
public function remaining_prefix_query( $args ) {
$defaults = array(
'must_query_fields' => array( 'all_content' ),
'boost_query_fields' => array( 'title' ),
'boost_operator' => 'and',
'boost_query_type' => 'best_fields',
);
$args = wp_parse_args( $args, $defaults );
if ( empty( $this->current_query ) || ctype_space( $this->current_query ) ) {
return;
}
//////////////////////////////////
// Example cases to think about:
// "elasticse"
// "elasticsearch"
// "elasticsearch "
// "elasticsearch lucen"
// "elasticsearch lucene"
// "the future" - note the stopword which will match nothing!
// "F1" - an exact match that also has tons of expansions
// "こんにちは" ja "hello"
// "こんにちは友人" ja "hello friend" - we just rely on the prefix phrase and ES to split words
// - this could still be better I bet. Maybe we need to analyze with ES first?
//
/////////////////////////////
//extract pieces of query
// eg: "PREFIXREMAINDER PREFIXWORD"
// "elasticsearch lucen"
$prefix_word = false;
$prefix_remainder = false;
if ( preg_match_all( '/([^ ]+)$/', $this->current_query, $matches ) ) {
$prefix_word = $matches[1][0];
}
$prefix_remainder = preg_replace( '/([^ ]+)$/', '', $this->current_query );
if ( ctype_space( $prefix_remainder ) ) {
$prefix_remainder = false;
}
if ( ! $prefix_word ) {
//Space at the end of the query, so skip using a prefix query
if ( ! empty( $args['must_query_fields'] ) ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['must_query_fields'],
'query' => $this->current_query,
'operator' => 'and',
) ) );
}
if ( ! empty( $args['boost_query_fields'] ) ) {
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $this->current_query,
'operator' => $args['boost_operator'],
'type' => $args['boost_query_type'],
) ), 'should' );
}
} else {
//must match the prefix word and the prefix remainder
if ( ! empty( $args['must_query_fields'] ) ) {
//need to do an OR across a few fields to handle all cases
$must_q = array( 'bool' => array( 'should' => array( ), 'minimum_should_match' => 1 ) );
//treat all words as an exact search (boosts complete word like "news"
//from prefixes of "newspaper")
$must_q['bool']['should'][] = array( 'multi_match' => array(
'fields' => $this->all_fields,
'query' => $full_text,
'operator' => 'and',
'type' => 'cross_fields',
) );
//always optimistically try and match the full text as a phrase
//prefix "the futu" should try to match "the future"
//otherwise the first stopword kinda breaks
//This also works as the prefix match for a single word "elasticsea"
$must_q['bool']['should'][] = array( 'multi_match' => array(
'fields' => $this->phrase_fields,
'query' => $full_text,
'operator' => 'and',
'type' => 'phrase_prefix',
'max_expansions' => 100,
) );
if ( $prefix_remainder ) {
//Multiple words found, so treat each word on its own and not just as
//a part of a phrase
//"elasticsearch lucen" => "elasticsearch" exact AND "lucen" prefix
$q['bool']['should'][] = array( 'bool' => array(
'must' => array(
array( 'multi_match' => array(
'fields' => $this->phrase_fields,
'query' => $prefix_word,
'operator' => 'and',
'type' => 'phrase_prefix',
'max_expansions' => 100,
) ),
array( 'multi_match' => array(
'fields' => $this->all_fields,
'query' => $prefix_remainder,
'operator' => 'and',
'type' => 'cross_fields',
) ),
)
) );
}
$this->add_query( $must_q );
}
//Now add any boosting of the query
if ( ! empty( $args['boost_query_fields'] ) ) {
//treat all words as an exact search (boosts complete word like "news"
//from prefixes of "newspaper")
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $this->current_query,
'operator' => $args['boost_query_operator'],
'type' => $args['boost_query_type'],
) ), 'should' );
//optimistically boost the full phrase prefix match
$this->add_query( array(
'multi_match' => array(
'fields' => $args['boost_query_fields'],
'query' => $this->current_query,
'operator' => 'and',
'type' => 'phrase_prefix',
'max_expansions' => 100,
) ) );
}
}
}
/*
* Boost results based on the lang probability overlaps
*
* args:
* langs2prob: list of languages to search in with associated boosts
*/
public function boost_lang_probs( $langs2prob ) {
foreach( $langs2prob as $l => $p ) {
$this->add_function( 'field_value_factor', array(
'modifier' => 'none',
'factor' => $p,
'missing' => 0.01, //1% chance doc did not have right lang detected
) );
}
}
////////////////////////////////////
// Helper Methods
//Get the text after some prefix. eg @gibrown, or @"Greg Brown"
protected function get_fields( $field_prefix ) {
$regex = '/' . $field_prefix . '(("[^"]+")|([^\\p{Z}]+))/';
if ( preg_match_all( $regex, $this->current_query, $match ) ) {
return $match[1];
}
return false;
}
//Remove the prefix and text from the query
protected function remove_fields( $field_name ) {
$regex = '/' . $field_name . '(("[^"]+")|([^\\p{Z}]+))/';
$this->current_query = preg_replace( $regex, '', $this->current_query );
}
//Best effort string truncation that splits on word breaks
protected function truncate_string( $string, $limit, $break=" " ) {
if ( mb_strwidth( $string ) <= $limit ) {
return $string;
}
// walk backwards from $limit to find first break
$breakpoint = $limit;
$broken = false;
while ( $breakpoint > 0 ) {
if ( $break === mb_strimwidth( $string, $breakpoint, 1 ) ) {
$string = mb_strimwidth( $string, 0, $breakpoint );
$broken = true;
break;
}
$breakpoint--;
}
// if we weren't able to find a break, need to chop mid-word
if ( !$broken ) {
$string = mb_strimwidth( $string, 0, $limit );
}
return $string;
}
}

View File

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

View File

@@ -0,0 +1,19 @@
# Markdown parsing library
Contains two libraries:
* `/extra`
- Gives you `MardownExtra_Parser` and `Markdown_Parser`
- Docs at http://michelf.ca/projects/php-markdown/extra/
* `/gfm` -- Github Flavored Markdown
- Gives you `WPCom_GHF_Markdown_Parser`
- It has the same interface as `MarkdownExtra_Parser`
- Adds support for fenced code blocks: https://help.github.com/articles/creating-and-highlighting-code-blocks/#fenced-code-blocks
- By default it replaces them with a code shortcode
- You can change this using the `$use_code_shortcode` member variable
- You can change the code shortcode wrapping with `$shortcode_start` and `$shortcode_end` member variables
- The `$preserve_shortcodes` member variable will preserve all registered shortcodes untouched. Requires WordPress to be loaded for `get_shortcode_regex()`
- The `$preserve_latex` member variable will preserve oldskool $latex yer-latex$ codes untouched.
- The `$strip_paras` member variable will strip <p> tags because that's what WordPress likes.
- See `WPCom_GHF_Markdown_Parser::__construct()` for how the above member variable defaults are set.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,400 @@
<?php
/**
* GitHub-Flavoured Markdown. Inspired by Evan's plugin, but modified.
*
* @author Evan Solomon
* @author Matt Wiebe <wiebe@automattic.com>
* @link https://github.com/evansolomon/wp-github-flavored-markdown-comments
*
* Add a few extras from GitHub's Markdown implementation. Must be used in a WordPress environment.
*/
class WPCom_GHF_Markdown_Parser extends MarkdownExtra_Parser {
/**
* Hooray somewhat arbitrary numbers that are fearful of 1.0.x.
*/
const WPCOM_GHF_MARDOWN_VERSION = '0.9.0';
/**
* Use a [code] shortcode when encountering a fenced code block
* @var boolean
*/
public $use_code_shortcode = true;
/**
* Preserve shortcodes, untouched by Markdown.
* This requires use within a WordPress installation.
* @var boolean
*/
public $preserve_shortcodes = true;
/**
* Preserve the legacy $latex your-latex-code-here$ style
* LaTeX markup
*/
public $preserve_latex = true;
/**
* Preserve single-line <code> blocks.
* @var boolean
*/
public $preserve_inline_code_blocks = true;
/**
* Strip paragraphs from the output. This is the right default for WordPress,
* which generally wants to create its own paragraphs with `wpautop`
* @var boolean
*/
public $strip_paras = true;
// Will run through sprintf - you can supply your own syntax if you want
public $shortcode_start = '[code lang=%s]';
public $shortcode_end = '[/code]';
// Stores shortcodes we remove and then replace
protected $preserve_text_hash = array();
/**
* Set environment defaults based on presence of key functions/classes.
*/
public function __construct() {
$this->use_code_shortcode = class_exists( 'SyntaxHighlighter' );
/**
* Allow processing shortcode contents.
*
* @module markdown
*
* @since 4.4.0
*
* @param boolean $preserve_shortcodes Defaults to $this->preserve_shortcodes.
*/
$this->preserve_shortcodes = apply_filters( 'jetpack_markdown_preserve_shortcodes', $this->preserve_shortcodes ) && function_exists( 'get_shortcode_regex' );
$this->preserve_latex = function_exists( 'latex_markup' );
$this->strip_paras = function_exists( 'wpautop' );
parent::__construct();
}
/**
* Overload to specify heading styles only if the hash has space(s) after it. This is actually in keeping with
* the documentation and eases the semantic overload of the hash character.
* #Will Not Produce a Heading 1
* # This Will Produce a Heading 1
*
* @param string $text Markdown text
* @return string HTML-transformed text
*/
public function transform( $text ) {
// Preserve anything inside a single-line <code> element
if ( $this->preserve_inline_code_blocks ) {
$text = $this->single_line_code_preserve( $text );
}
// Remove all shortcodes so their interiors are left intact
if ( $this->preserve_shortcodes ) {
$text = $this->shortcode_preserve( $text );
}
// Remove legacy LaTeX so it's left intact
if ( $this->preserve_latex ) {
$text = $this->latex_preserve( $text );
}
// escape line-beginning # chars that do not have a space after them.
$text = preg_replace_callback( '|^#{1,6}( )?|um', array( $this, '_doEscapeForHashWithoutSpacing' ), $text );
/**
* Allow third-party plugins to define custom patterns that won't be processed by Markdown.
*
* @module markdown
*
* @since 3.9.2
*
* @param array $custom_patterns Array of custom patterns to be ignored by Markdown.
*/
$custom_patterns = apply_filters( 'jetpack_markdown_preserve_pattern', array() );
if ( is_array( $custom_patterns ) && ! empty( $custom_patterns ) ) {
foreach ( $custom_patterns as $pattern ) {
$text = preg_replace_callback( $pattern, array( $this, '_doRemoveText'), $text );
}
}
// run through core Markdown
$text = parent::transform( $text );
// Occasionally Markdown Extra chokes on a para structure, producing odd paragraphs.
$text = str_replace( "<p>&lt;</p>\n\n<p>p>", '<p>', $text );
// put start-of-line # chars back in place
$text = $this->restore_leading_hash( $text );
// Strip paras if set
if ( $this->strip_paras ) {
$text = $this->unp( $text );
}
// Restore preserved things like shortcodes/LaTeX
$text = $this->do_restore( $text );
return $text;
}
/**
* Prevents blocks like <code>__this__</code> from turning into <code><strong>this</strong></code>
* @param string $text Text that may need preserving
* @return string Text that was preserved if needed
*/
public function single_line_code_preserve( $text ) {
return preg_replace_callback( '|<code\b[^>]*>(.*?)</code>|', array( $this, 'do_single_line_code_preserve' ), $text );
}
/**
* Regex callback for inline code presevation
* @param array $matches Regex matches
* @return string Hashed content for later restoration
*/
public function do_single_line_code_preserve( $matches ) {
return '<code>' . $this->hash_block( $matches[1] ) . '</code>';
}
/**
* Preserve code block contents by HTML encoding them. Useful before getting to KSES stripping.
* @param string $text Markdown/HTML content
* @return string Markdown/HTML content with escaped code blocks
*/
public function codeblock_preserve( $text ) {
return preg_replace_callback( "/^([`~]{3})([^`\n]+)?\n([^`~]+)(\\1)/m", array( $this, 'do_codeblock_preserve' ), $text );
}
/**
* Regex callback for code block preservation.
* @param array $matches Regex matches
* @return string Codeblock with escaped interior
*/
public function do_codeblock_preserve( $matches ) {
$block = stripslashes( $matches[3] );
$block = esc_html( $block );
$block = str_replace( '\\', '\\\\', $block );
$open = $matches[1] . $matches[2] . "\n";
return $open . $block . $matches[4];
}
/**
* Restore previously preserved (i.e. escaped) code block contents.
* @param string $text Markdown/HTML content with escaped code blocks
* @return string Markdown/HTML content
*/
public function codeblock_restore( $text ) {
return preg_replace_callback( "/^([`~]{3})([^`\n]+)?\n([^`~]+)(\\1)/m", array( $this, 'do_codeblock_restore' ), $text );
}
/**
* Regex callback for code block restoration (unescaping).
* @param array $matches Regex matches
* @return string Codeblock with unescaped interior
*/
public function do_codeblock_restore( $matches ) {
$block = html_entity_decode( $matches[3], ENT_QUOTES );
$open = $matches[1] . $matches[2] . "\n";
return $open . $block . $matches[4];
}
/**
* Called to preserve legacy LaTeX like $latex some-latex-text $
* @param string $text Text in which to preserve LaTeX
* @return string Text with LaTeX replaced by a hash that will be restored later
*/
protected function latex_preserve( $text ) {
// regex from latex_remove()
$regex = '%
\$latex(?:=\s*|\s+)
((?:
[^$]+ # Not a dollar
|
(?<=(?<!\\\\)\\\\)\$ # Dollar preceded by exactly one slash
)+)
(?<!\\\\)\$ # Dollar preceded by zero slashes
%ix';
$text = preg_replace_callback( $regex, array( $this, '_doRemoveText'), $text );
return $text;
}
/**
* Called to preserve WP shortcodes from being formatted by Markdown in any way.
* @param string $text Text in which to preserve shortcodes
* @return string Text with shortcodes replaced by a hash that will be restored later
*/
protected function shortcode_preserve( $text ) {
$text = preg_replace_callback( $this->get_shortcode_regex(), array( $this, '_doRemoveText' ), $text );
return $text;
}
/**
* Restores any text preserved by $this->hash_block()
* @param string $text Text that may have hashed preservation placeholders
* @return string Text with hashed preseravtion placeholders replaced by original text
*/
protected function do_restore( $text ) {
// Reverse hashes to ensure nested blocks are restored.
$hashes = array_reverse( $this->preserve_text_hash, true );
foreach( $hashes as $hash => $value ) {
$placeholder = $this->hash_maker( $hash );
$text = str_replace( $placeholder, $value, $text );
}
// reset the hash
$this->preserve_text_hash = array();
return $text;
}
/**
* Regex callback for text preservation
* @param array $m Regex $matches array
* @return string A placeholder that will later be replaced by the original text
*/
protected function _doRemoveText( $m ) {
return $this->hash_block( $m[0] );
}
/**
* Call this to store a text block for later restoration.
* @param string $text Text to preserve for later
* @return string Placeholder that will be swapped out later for the original text
*/
protected function hash_block( $text ) {
$hash = md5( $text );
$this->preserve_text_hash[ $hash ] = $text;
$placeholder = $this->hash_maker( $hash );
return $placeholder;
}
/**
* Less glamorous than the Keymaker
* @param string $hash An md5 hash
* @return string A placeholder hash
*/
protected function hash_maker( $hash ) {
return 'MARKDOWN_HASH' . $hash . 'MARKDOWN_HASH';
}
/**
* Remove bare <p> elements. <p>s with attributes will be preserved.
* @param string $text HTML content
* @return string <p>-less content
*/
public function unp( $text ) {
return preg_replace( "#<p>(.*?)</p>(\n|$)#ums", '$1$2', $text );
}
/**
* A regex of all shortcodes currently registered by the current
* WordPress installation
* @uses get_shortcode_regex()
* @return string A regex for grabbing shortcodes.
*/
protected function get_shortcode_regex() {
$pattern = get_shortcode_regex();
// don't match markdown link anchors that could be mistaken for shortcodes.
$pattern .= '(?!\()';
return "/$pattern/s";
}
/**
* Since we escape unspaced #Headings, put things back later.
* @param string $text text with a leading escaped hash
* @return string text with leading hashes unescaped
*/
protected function restore_leading_hash( $text ) {
return preg_replace( "/^(<p>)?(&#35;|\\\\#)/um", "$1#", $text );
}
/**
* Overload to support ```-fenced code blocks for pre-Markdown Extra 1.2.8
* https://help.github.com/articles/github-flavored-markdown#fenced-code-blocks
*/
public function doFencedCodeBlocks( $text ) {
// If we're at least at 1.2.8, native fenced code blocks are in.
// Below is just copied from it in case we somehow got loaded on
// top of someone else's Markdown Extra
if ( version_compare( MARKDOWNEXTRA_VERSION, '1.2.8', '>=' ) )
return parent::doFencedCodeBlocks( $text );
#
# Adding the fenced code block syntax to regular Markdown:
#
# ~~~
# Code block
# ~~~
#
$less_than_tab = $this->tab_width;
$text = preg_replace_callback('{
(?:\n|\A)
# 1: Opening marker
(
(?:~{3,}|`{3,}) # 3 or more tildes/backticks.
)
[ ]*
(?:
\.?([-_:a-zA-Z0-9]+) # 2: standalone class name
|
'.$this->id_class_attr_catch_re.' # 3: Extra attributes
)?
[ ]* \n # Whitespace and newline following marker.
# 4: Content
(
(?>
(?!\1 [ ]* \n) # Not a closing marker.
.*\n+
)+
)
# Closing marker.
\1 [ ]* (?= \n )
}xm',
array($this, '_doFencedCodeBlocks_callback'), $text);
return $text;
}
/**
* Callback for pre-processing start of line hashes to slyly escape headings that don't
* have a leading space
* @param array $m preg_match matches
* @return string possibly escaped start of line hash
*/
public function _doEscapeForHashWithoutSpacing( $m ) {
if ( ! isset( $m[1] ) )
$m[0] = '\\' . $m[0];
return $m[0];
}
/**
* Overload to support Viper's [code] shortcode. Because awesome.
*/
public function _doFencedCodeBlocks_callback( $matches ) {
// in case we have some escaped leading hashes right at the start of the block
$matches[4] = $this->restore_leading_hash( $matches[4] );
// just MarkdownExtra_Parser if we're not going ultra-deluxe
if ( ! $this->use_code_shortcode ) {
return parent::_doFencedCodeBlocks_callback( $matches );
}
// default to a "text" class if one wasn't passed. Helps with encoding issues later.
if ( empty( $matches[2] ) ) {
$matches[2] = 'text';
}
$classname =& $matches[2];
$codeblock = preg_replace_callback('/^\n+/', array( $this, '_doFencedCodeBlocks_newlines' ), $matches[4] );
if ( $classname{0} == '.' )
$classname = substr( $classname, 1 );
$codeblock = esc_html( $codeblock );
$codeblock = sprintf( $this->shortcode_start, $classname ) . "\n{$codeblock}" . $this->shortcode_end;
return "\n\n" . $this->hashBlock( $codeblock ). "\n\n";
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* Plugins Library
*
* Helper functions for installing and activating plugins.
*
* Used by the REST API
*
* @autounit api plugins
*/
include_once( 'class.jetpack-automatic-install-skin.php' );
class Jetpack_Plugins {
/**
* Install and activate a plugin.
*
* @since 5.8.0
*
* @param string $slug Plugin slug.
*
* @return bool|WP_Error True if installation succeeded, error object otherwise.
*/
public static function install_and_activate_plugin( $slug ) {
$plugin_id = self::get_plugin_id_by_slug( $slug );
if ( ! $plugin_id ) {
$installed = self::install_plugin( $slug );
if ( is_wp_error( $installed ) ) {
return $installed;
}
$plugin_id = self::get_plugin_id_by_slug( $slug );
} else if ( is_plugin_active( $plugin_id ) ) {
return true; // Already installed and active
}
if ( ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error( 'not_allowed', __( 'You are not allowed to activate plugins on this site.', 'jetpack' ) );
}
$activated = activate_plugin( $plugin_id );
if ( is_wp_error( $activated ) ) {
return $activated;
}
return true;
}
/**
* Install a plugin.
*
* @since 5.8.0
*
* @param string $slug Plugin slug.
*
* @return bool|WP_Error True if installation succeeded, error object otherwise.
*/
public static function install_plugin( $slug ) {
if ( is_multisite() && ! current_user_can( 'manage_network' ) ) {
return new WP_Error( 'not_allowed', __( 'You are not allowed to install plugins on this site.', 'jetpack' ) );
}
$skin = new Jetpack_Automatic_Install_Skin();
$upgrader = new Plugin_Upgrader( $skin );
$zip_url = self::generate_wordpress_org_plugin_download_link( $slug );
$result = $upgrader->install( $zip_url );
if ( is_wp_error( $result ) ) {
return $result;
}
$plugin = Jetpack_Plugins::get_plugin_id_by_slug( $slug );
$error_code = 'install_error';
if ( ! $plugin ) {
$error = __( 'There was an error installing your plugin', 'jetpack' );
}
if ( ! $result ) {
$error_code = $upgrader->skin->get_main_error_code();
$message = $upgrader->skin->get_main_error_message();
$error = $message ? $message : __( 'An unknown error occurred during installation', 'jetpack' );
}
if ( ! empty( $error ) ) {
if ( 'download_failed' === $error_code ) {
// For backwards compatibility: versions prior to 3.9 would return no_package instead of download_failed.
$error_code = 'no_package';
}
return new WP_Error( $error_code, $error, 400 );
}
return (array) $upgrader->skin->get_upgrade_messages();
}
protected static function generate_wordpress_org_plugin_download_link( $plugin_slug ) {
return "https://downloads.wordpress.org/plugin/$plugin_slug.latest-stable.zip";
}
public static function get_plugin_id_by_slug( $slug ) {
// Check if get_plugins() function exists. This is required on the front end of the
// site, since it is in a file that is normally only loaded in the admin.
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
$plugins = apply_filters( 'all_plugins', get_plugins() );
if ( ! is_array( $plugins ) ) {
return false;
}
foreach ( $plugins as $plugin_file => $plugin_data ) {
if ( self::get_slug_from_file_path( $plugin_file ) === $slug ) {
return $plugin_file;
}
}
return false;
}
protected static function get_slug_from_file_path( $plugin_file ) {
// Similar to get_plugin_slug() method.
$slug = dirname( $plugin_file );
if ( '.' === $slug ) {
$slug = preg_replace( "/(.+)\.php$/", "$1", $plugin_file );
}
return $slug;
}
}

View File

@@ -0,0 +1,237 @@
<?php
/*
Plugin Name: Tonesque
Plugin URI: http://automattic.com/
Description: Grab an average color representation from an image.
Version: 1.0
Author: Automattic, Matias Ventura
Author URI: http://automattic.com/
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
*/
class Tonesque {
private $image_url = '';
private $image_obj = NULL;
private $color = '';
function __construct( $image_url ) {
if ( ! class_exists( 'Jetpack_Color' ) ) {
jetpack_require_lib( 'class.color' );
}
$this->image_url = esc_url_raw( $image_url );
$this->image_url = trim( $this->image_url );
/**
* Allows any image URL to be passed in for $this->image_url.
*
* @module theme-tools
*
* @since 2.5.0
*
* @param string $image_url The URL to any image
*/
$this->image_url = apply_filters( 'tonesque_image_url', $this->image_url );
$this->image_obj = self::imagecreatefromurl( $this->image_url );
}
public static function imagecreatefromurl( $image_url ) {
$data = null;
// If it's a URL:
if ( preg_match( '#^https?://#i', $image_url ) ) {
// If it's a url pointing to a local media library url:
$content_url = content_url();
$_image_url = set_url_scheme( $image_url );
if ( wp_startswith( $_image_url, $content_url ) ) {
$_image_path = str_replace( $content_url, WP_CONTENT_DIR, $_image_url );
if ( file_exists( $_image_path ) ) {
$filetype = wp_check_filetype( $_image_path );
$ext = $filetype['ext'];
$type = $filetype['type'];
if ( wp_startswith( $type, 'image/' ) ) {
$data = file_get_contents( $_image_path );
}
}
}
if ( empty( $data ) ) {
$response = wp_remote_get( $image_url );
if ( is_wp_error( $response ) ) {
return false;
}
$data = wp_remote_retrieve_body( $response );
}
}
// If it's a local path in our WordPress install:
if ( file_exists( $image_url ) ) {
$filetype = wp_check_filetype( $image_url );
$ext = $filetype['ext'];
$type = $filetype['type'];
if ( wp_startswith( $type, 'image/' ) ) {
$data = file_get_contents( $image_url );
}
}
// Now turn it into an image and return it.
return imagecreatefromstring( $data );
}
/**
*
* Construct object from image.
*
* @param optional $type (hex, rgb, hsl)
* @return color as a string formatted as $type
*
*/
function color( $type = 'hex' ) {
// Bail if there is no image to work with
if ( ! $this->image_obj )
return false;
// Finds dominant color
$color = self::grab_color();
// Passes value to Color class
$color = self::get_color( $color, $type );
return $color;
}
/**
*
* Grabs the color index for each of five sample points of the image
*
* @param $image
* @param $type can be 'index' or 'hex'
* @return array() with color indices
*
*/
function grab_points( $type = 'index' ) {
$img = $this->image_obj;
if ( ! $img )
return false;
$height = imagesy( $img );
$width = imagesx( $img );
// Sample five points in the image
// Based on rule of thirds and center
$topy = round( $height / 3 );
$bottomy = round( ( $height / 3 ) * 2 );
$leftx = round( $width / 3 );
$rightx = round( ( $width / 3 ) * 2 );
$centery = round( $height / 2 );
$centerx = round( $width / 2 );
// Cast those colors into an array
$points = array(
imagecolorat( $img, $leftx, $topy ),
imagecolorat( $img, $rightx, $topy ),
imagecolorat( $img, $leftx, $bottomy ),
imagecolorat( $img, $rightx, $bottomy ),
imagecolorat( $img, $centerx, $centery ),
);
if ( 'hex' == $type ) {
foreach ( $points as $i => $p ) {
$c = imagecolorsforindex( $img, $p );
$points[ $i ] = self::get_color( array(
'r' => $c['red'],
'g' => $c['green'],
'b' => $c['blue'],
), 'hex' );
}
}
return $points;
}
/**
*
* Finds the average color of the image based on five sample points
*
* @param $image
* @return array() with rgb color
*
*/
function grab_color() {
$img = $this->image_obj;
if ( ! $img )
return false;
$rgb = self::grab_points();
// Process the color points
// Find the average representation
foreach ( $rgb as $color ) {
$index = imagecolorsforindex( $img, $color );
$r[] = $index['red'];
$g[] = $index['green'];
$b[] = $index['blue'];
$red = round( array_sum( $r ) / 5 );
$green = round( array_sum( $g ) / 5 );
$blue = round( array_sum( $b ) / 5 );
}
// The average color of the image as rgb array
$color = array(
'r' => $red,
'g' => $green,
'b' => $blue,
);
return $color;
}
/**
*
* Get a Color object using /lib class.color
* Convert to appropriate type
*
* @return string
*
*/
function get_color( $color, $type ) {
$c = new Jetpack_Color( $color, 'rgb' );
$this->color = $c;
switch ( $type ) {
case 'rgb' :
$color = implode( $c->toRgbInt(), ',' );
break;
case 'hex' :
$color = $c->toHex();
break;
case 'hsv' :
$color = implode( $c->toHsvInt(), ',' );
break;
default:
return $color = $c->toHex();
}
return $color;
}
/**
*
* Checks contrast against main color
* Gives either black or white for using with opacity
*
* @return string
*
*/
function contrast() {
if ( ! $this->color )
return false;
$c = $this->color->getMaxContrastColor();
return implode( $c->toRgbInt(), ',' );
}
};

View File

@@ -0,0 +1,191 @@
<?php
/**
* Jetpack_Tracks_Client
* @autounit nosara tracks-client
*
* Send Tracks events on behalf of a user
*
* Example Usage:
```php
require( dirname(__FILE__).'path/to/tracks/class.tracks-client' );
$result = Jetpack_Tracks_Client::record_event( array(
'_en' => $event_name, // required
'_ui' => $user_id, // required unless _ul is provided
'_ul' => $user_login, // required unless _ui is provided
// Optional, but recommended
'_ts' => $ts_in_ms, // Default: now
'_via_ip' => $client_ip, // we use it for geo, etc.
// Possibly useful to set some context for the event
'_via_ua' => $client_user_agent,
'_via_url' => $client_url,
'_via_ref' => $client_referrer,
// For user-targeted tests
'abtest_name' => $abtest_name,
'abtest_variation' => $abtest_variation,
// Your application-specific properties
'custom_property' => $some_value,
) );
if ( is_wp_error( $result ) ) {
// Handle the error in your app
}
```
*/
require_once( dirname(__FILE__).'/class.tracks-client.php' );
class Jetpack_Tracks_Client {
const PIXEL = 'https://pixel.wp.com/t.gif';
const BROWSER_TYPE = 'php-agent';
const USER_AGENT_SLUG = 'tracks-client';
const VERSION = '0.3';
/**
* record_event
* @param mixed $event Event object to send to Tracks. An array will be cast to object. Required.
* Properties are included directly in the pixel query string after light validation.
* @return mixed True on success, WP_Error on failure
*/
static function record_event( $event ) {
if ( ! Jetpack::jetpack_tos_agreed() || ! empty( $_COOKIE['tk_opt-out'] ) ) {
return false;
}
if ( ! $event instanceof Jetpack_Tracks_Event ) {
$event = new Jetpack_Tracks_Event( $event );
}
if ( is_wp_error( $event ) ) {
return $event;
}
$pixel = $event->build_pixel_url( $event );
if ( ! $pixel ) {
return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
}
return self::record_pixel( $pixel );
}
/**
* Synchronously request the pixel
*/
static function record_pixel( $pixel ) {
// Add the Request Timestamp and URL terminator just before the HTTP request.
$pixel .= '&_rt=' . self::build_timestamp() . '&_=_';
$response = wp_remote_get( $pixel, array(
'blocking' => true, // The default, but being explicit here :)
'timeout' => 1,
'redirection' => 2,
'httpversion' => '1.1',
'user-agent' => self::get_user_agent(),
) );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = isset( $response['response']['code'] ) ? $response['response']['code'] : 0;
if ( $code !== 200 ) {
return new WP_Error( 'request_failed', 'Tracks pixel request failed', $code );
}
return true;
}
static function get_user_agent() {
return Jetpack_Tracks_Client::USER_AGENT_SLUG . '-v' . Jetpack_Tracks_Client::VERSION;
}
/**
* Build an event and return its tracking URL
* @deprecated Call the `build_pixel_url` method on a Jetpack_Tracks_Event object instead.
* @param array $event Event keys and values
* @return string URL of a tracking pixel
*/
static function build_pixel_url( $event ) {
$_event = new Jetpack_Tracks_Event( $event );
return $_event->build_pixel_url();
}
/**
* Validate input for a tracks event.
* @deprecated Instantiate a Jetpack_Tracks_Event object instead
* @param array $event Event keys and values
* @return mixed Validated keys and values or WP_Error on failure
*/
private static function validate_and_sanitize( $event ) {
$_event = new Jetpack_Tracks_Event( $event );
if ( is_wp_error( $_event ) ) {
return $_event;
}
return get_object_vars( $_event );
}
// Milliseconds since 1970-01-01
static function build_timestamp() {
$ts = round( microtime( true ) * 1000 );
return number_format( $ts, 0, '', '' );
}
/**
* Grabs the user's anon id from cookies, or generates and sets a new one
*
* @return string An anon id for the user
*/
static function get_anon_id() {
static $anon_id = null;
if ( ! isset( $anon_id ) ) {
// Did the browser send us a cookie?
if ( isset( $_COOKIE[ 'tk_ai' ] ) && preg_match( '#^[A-Za-z0-9+/=]{24}$#', $_COOKIE[ 'tk_ai' ] ) ) {
$anon_id = $_COOKIE[ 'tk_ai' ];
} else {
$binary = '';
// Generate a new anonId and try to save it in the browser's cookies
// Note that base64-encoding an 18 character string generates a 24-character anon id
for ( $i = 0; $i < 18; ++$i ) {
$binary .= chr( mt_rand( 0, 255 ) );
}
$anon_id = 'jetpack:' . base64_encode( $binary );
if ( ! headers_sent()
&& ! ( defined( 'REST_REQUEST' ) && REST_REQUEST )
&& ! ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
) {
setcookie( 'tk_ai', $anon_id );
}
}
}
return $anon_id;
}
/**
* Gets the WordPress.com user's Tracks identity, if connected.
*
* @return array|bool
*/
static function get_connected_user_tracks_identity() {
if ( ! $user_data = Jetpack::get_connected_user_data() ) {
return false;
}
return array(
'userid' => $user_data['ID'],
'username' => $user_data['login'],
);
}
}

View File

@@ -0,0 +1,149 @@
<?php
/**
* @autounit nosara tracks-client
*
* Example Usage:
```php
require_once( dirname(__FILE__) . 'path/to/tracks/class.tracks-event' );
$event = new Jetpack_Tracks_Event( array(
'_en' => $event_name, // required
'_ui' => $user_id, // required unless _ul is provided
'_ul' => $user_login, // required unless _ui is provided
// Optional, but recommended
'_via_ip' => $client_ip, // for geo, etc.
// Possibly useful to set some context for the event
'_via_ua' => $client_user_agent,
'_via_url' => $client_url,
'_via_ref' => $client_referrer,
// For user-targeted tests
'abtest_name' => $abtest_name,
'abtest_variation' => $abtest_variation,
// Your application-specific properties
'custom_property' => $some_value,
) );
if ( is_wp_error( $event->error ) ) {
// Handle the error in your app
}
$bump_and_redirect_pixel = $event->build_signed_pixel_url();
```
*/
require_once( dirname(__FILE__) . '/class.tracks-client.php' );
class Jetpack_Tracks_Event {
const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/';
const PROP_NAME_REGEX = '/^[a-z_][a-z0-9_]*$/';
public $error;
function __construct( $event ) {
$_event = self::validate_and_sanitize( $event );
if ( is_wp_error( $_event ) ) {
$this->error = $_event;
return;
}
foreach( $_event as $key => $value ) {
$this->{$key} = $value;
}
}
function record() {
return Jetpack_Tracks_Client::record_event( $this );
}
/**
* Annotate the event with all relevant info.
* @param mixed $event Object or (flat) array
* @return mixed The transformed event array or WP_Error on failure.
*/
static function validate_and_sanitize( $event ) {
$event = (object) $event;
// Required
if ( ! $event->_en ) {
return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 );
}
// delete non-routable addresses otherwise geoip will discard the record entirely
if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) {
unset($event->_via_ip);
}
$validated = array(
'browser_type' => Jetpack_Tracks_Client::BROWSER_TYPE,
'_aua' => Jetpack_Tracks_Client::get_user_agent(),
);
$_event = (object) array_merge( (array) $event, $validated );
// If you want to blacklist property names, do it here.
// Make sure we have an event timestamp.
if ( ! isset( $_event->_ts ) ) {
$_event->_ts = Jetpack_Tracks_Client::build_timestamp();
}
return $_event;
}
/**
* Build a pixel URL that will send a Tracks event when fired.
* On error, returns an empty string ('').
*
* @return string A pixel URL or empty string ('') if there were invalid args.
*/
function build_pixel_url() {
if ( $this->error ) {
return '';
}
$args = get_object_vars( $this );
// Request Timestamp and URL Terminator must be added just before the HTTP request or not at all.
unset( $args['_rt'] );
unset( $args['_'] );
$validated = self::validate_and_sanitize( $args );
if ( is_wp_error( $validated ) )
return '';
return Jetpack_Tracks_Client::PIXEL . '?' . http_build_query( $validated );
}
static function event_name_is_valid( $name ) {
return preg_match( Jetpack_Tracks_Event::EVENT_NAME_REGEX, $name );
}
static function prop_name_is_valid( $name ) {
return preg_match( Jetpack_Tracks_Event::PROP_NAME_REGEX, $name );
}
static function scrutinize_event_names( $event ) {
if ( ! Jetpack_Tracks_Event::event_name_is_valid( $event->_en ) ) {
return;
}
$whitelisted_key_names = array(
'anonId',
'Browser_Type',
);
foreach ( array_keys( (array) $event ) as $key ) {
if ( in_array( $key, $whitelisted_key_names ) ) {
continue;
}
if ( ! Jetpack_Tracks_Event::prop_name_is_valid( $key ) ) {
return;
}
}
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* PHP Tracks Client
* @autounit nosara tracks-client
* Example Usage:
*
```php
include( plugin_dir_path( __FILE__ ) . 'lib/tracks/client.php');
$result = jetpack_tracks_record_event( $user, $event_name, $properties );
if ( is_wp_error( $result ) ) {
// Handle the error in your app
}
```
*/
// Load the client classes
require_once( dirname(__FILE__) . '/class.tracks-event.php' );
require_once( dirname(__FILE__) . '/class.tracks-client.php' );
// Now, let's export a sprinkling of syntactic sugar!
/**
* Procedurally (vs. Object-oriented), track an event object (or flat array)
* NOTE: Use this only when the simpler jetpack_tracks_record_event() function won't work for you.
* @param \Jetpack_Tracks_Event $event The event object.
* @return \Jetpack_Tracks_Event|\WP_Error
*/
function jetpack_tracks_record_event_raw( $event ) {
return Jetpack_Tracks_Client::record_event( $event );
}
/**
* Procedurally build a Tracks Event Object.
* NOTE: Use this only when the simpler jetpack_tracks_record_event() function won't work for you.
* @param $identity WP_user object
* @param string $event_name The name of the event
* @param array $properties Custom properties to send with the event
* @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred
* @return \Jetpack_Tracks_Event|\WP_Error
*/
function jetpack_tracks_build_event_obj( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
$identity = jetpack_tracks_get_identity( $user->ID );
$properties['user_lang'] = $user->get( 'WPLANG' );
$blog_details = array(
'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' )
);
$timestamp = ( $event_timestamp_millis !== false ) ? $event_timestamp_millis : round( microtime( true ) * 1000 );
$timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' );
return new Jetpack_Tracks_Event( array_merge( $blog_details, (array) $properties, $identity, array(
'_en' => $event_name,
'_ts' => $timestamp_string
) ) );
}
/*
* Get the identity to send to tracks.
*
* @param int $user_id The user id of the local user
* @return array $identity
*/
function jetpack_tracks_get_identity( $user_id ) {
// Meta is set, and user is still connected. Use WPCOM ID
$wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true );
if ( $wpcom_id && Jetpack::is_user_connected( $user_id ) ) {
return array(
'_ut' => 'wpcom:user_id',
'_ui' => $wpcom_id
);
}
// User is connected, but no meta is set yet. Use WPCOM ID and set meta.
if ( Jetpack::is_user_connected( $user_id ) ) {
$wpcom_user_data = Jetpack::get_connected_user_data( $user_id );
add_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'], true );
return array(
'_ut' => 'wpcom:user_id',
'_ui' => $wpcom_user_data['ID']
);
}
// User isn't linked at all. Fall back to anonymous ID.
$anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true );
if ( ! $anon_id ) {
$anon_id = Jetpack_Tracks_Client::get_anon_id();
add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false );
}
if ( ! isset( $_COOKIE[ 'tk_ai' ] ) && ! headers_sent() ) {
setcookie( 'tk_ai', $anon_id );
}
return array(
'_ut' => 'anon',
'_ui' => $anon_id
);
}
/**
* Record an event in Tracks - this is the preferred way to record events from PHP.
*
* @param mixed $identity username, user_id, or WP_user object
* @param string $event_name The name of the event
* @param array $properties Custom properties to send with the event
* @param int $event_timestamp_millis The time in millis since 1970-01-01 00:00:00 when the event occurred
* @return bool true for success | \WP_Error if the event pixel could not be fired
*/
function jetpack_tracks_record_event( $user, $event_name, $properties = array(), $event_timestamp_millis = false ) {
// We don't want to track user events during unit tests/CI runs.
if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) {
return false;
}
$event_obj = jetpack_tracks_build_event_obj( $user, $event_name, $properties, $event_timestamp_millis );
if ( is_wp_error( $event_obj->error ) ) {
return $event_obj->error;
}
return $event_obj->record();
}

View File

@@ -0,0 +1,57 @@
/* global jpTracksAJAX, jQuery */
(function( $, jpTracksAJAX ) {
window.jpTracksAJAX = window.jpTracksAJAX || {};
window.jpTracksAJAX.record_ajax_event = function ( eventName, eventType, eventProp ) {
var data = {
tracksNonce: jpTracksAJAX.jpTracksAJAX_nonce,
action: 'jetpack_tracks',
tracksEventType: eventType,
tracksEventName: eventName,
tracksEventProp: eventProp || false
};
return $.ajax( {
type: 'POST',
url: jpTracksAJAX.ajaxurl,
data: data
} );
};
$( document ).ready( function () {
$( 'body' ).on( 'click', '.jptracks a, a.jptracks', function( event ) {
// We know that the jptracks element is either this, or its ancestor
var $jptracks = $( this ).closest( '.jptracks' );
// We need an event name at least
var eventName = $jptracks.attr( 'data-jptracks-name' );
if ( undefined === eventName ) {
return;
}
var eventProp = $jptracks.attr( 'data-jptracks-prop' ) || false;
var url = $( this ).attr( 'href' );
var target = $( this ).get( 0 ).target;
if ( url && target && '_self' !== target ) {
var newTabWindow = window.open( '', target );
newTabWindow.opener = null;
}
event.preventDefault();
window.jpTracksAJAX.record_ajax_event( eventName, 'click', eventProp ).always( function() {
// Continue on to whatever url they were trying to get to.
if ( url ) {
if ( newTabWindow ) {
newTabWindow.location = url;
return;
}
window.location = url;
}
} );
} );
} );
} )( jQuery, jpTracksAJAX );

View File

@@ -0,0 +1,72 @@
/**
* This was abstracted from wp-calypso's analytics lib: https://github.com/Automattic/wp-calypso/blob/master/client/lib/analytics/README.md
* Some stuff was removed like GA tracking and other things not necessary for Jetpack tracking.
*
* This library should only be used and loaded if the Jetpack site is connected.
*/
// Load tracking scripts
window._tkq = window._tkq || [];
function buildQuerystring( group, name ) {
var uriComponent = '';
if ( 'object' === typeof group ) {
for ( var key in group ) {
uriComponent += '&x_' + encodeURIComponent( key ) + '=' + encodeURIComponent( group[ key ] );
}
} else {
uriComponent = '&x_' + encodeURIComponent( group ) + '=' + encodeURIComponent( name );
}
return uriComponent;
}
var analytics = {
initialize: function( userId, username ) {
analytics.setUser( userId, username );
analytics.identifyUser();
},
mc: {
bumpStat: function( group, name ) {
var uriComponent = buildQuerystring( group, name ); // prints debug info
new Image().src = document.location.protocol + '//pixel.wp.com/g.gif?v=wpcom-no-pv' + uriComponent + '&t=' + Math.random();
}
},
tracks: {
recordEvent: function( eventName, eventProperties ) {
eventProperties = eventProperties || {};
if ( eventName.indexOf( 'jetpack_' ) !== 0 ) {
debug( '- Event name must be prefixed by "jetpack_"' );
return;
}
window._tkq.push( [ 'recordEvent', eventName, eventProperties ] );
},
recordPageView: function( urlPath ) {
analytics.tracks.recordEvent( 'jetpack_page_view', {
'path': urlPath
} );
}
},
setUser: function( userId, username ) {
_user = { ID: userId, username: username };
},
identifyUser: function() {
// Don't identify the user if we don't have one
if ( _user ) {
window._tkq.push( [ 'identifyUser', _user.ID, _user.username ] );
}
},
clearedIdentity: function() {
window._tkq.push( [ 'clearIdentity' ] );
}
};

View File

@@ -0,0 +1,776 @@
<?php
/**
* Widgets and Sidebars Library
*
* Helper functions for manipulating widgets on a per-blog basis.
* Only helpful on `wp_loaded` or later (currently requires widgets to be registered and the theme context to already be loaded).
*
* Used by the REST API
*
* @autounit api widgets
*/
class Jetpack_Widgets {
/**
* Returns the `sidebars_widgets` option with the `array_version` element removed.
*
* @return array The current value of sidebars_widgets
*/
public static function get_sidebars_widgets() {
$sidebars = get_option( 'sidebars_widgets', array() );
if ( isset( $sidebars['array_version'] ) ) {
unset( $sidebars['array_version'] );
}
return $sidebars;
}
/**
* Format widget data for output and for use by other widget functions.
*
* The output looks like:
*
* array(
* 'id' => 'text-3',
* 'sidebar' => 'sidebar-1',
* 'position' => '0',
* 'settings' => array(
* 'title' => 'hello world'
* )
* )
*
*
* @param string|integer $position The position of the widget in its sidebar.
* @param string $widget_id The widget's id (eg: 'text-3').
* @param string $sidebar The widget's sidebar id (eg: 'sidebar-1').
* @param array (Optional) $settings The settings for the widget.
*
* @return array A normalized array representing this widget.
*/
public static function format_widget( $position, $widget_id, $sidebar, $settings = null ) {
if ( ! $settings ) {
$all_settings = get_option( self::get_widget_option_name( $widget_id ) );
$instance = self::get_widget_instance_key( $widget_id );
$settings = $all_settings[$instance];
}
$widget = array();
$widget['id'] = $widget_id;
$widget['id_base'] = self::get_widget_id_base( $widget_id );
$widget['settings'] = $settings;
$widget['sidebar'] = $sidebar;
$widget['position'] = $position;
return $widget;
}
/**
* Return a widget's id_base from its id.
*
* @param string $widget_id The id of a widget. (eg: 'text-3')
*
* @return string The id_base of a widget (eg: 'text').
*/
public static function get_widget_id_base( $widget_id ) {
// Grab what's before the hyphen.
return substr( $widget_id, 0, strrpos( $widget_id, '-' ) );
}
/**
* Determine a widget's option name (the WP option where the widget's settings
* are stored - generally `widget_` + the widget's id_base).
*
* @param string $widget_id The id of a widget. (eg: 'text-3')
*
* @return string The option name of the widget's settings. (eg: 'widget_text')
*/
public static function get_widget_option_name( $widget_id ) {
return 'widget_' . self::get_widget_id_base( $widget_id );
}
/**
* Determine a widget instance key from its ID. (eg: 'text-3' becomes '3').
* Used to access the widget's settings.
*
* @param string $widget_id The id of a widget.
*
* @return integer The instance key of that widget.
*/
public static function get_widget_instance_key( $widget_id ) {
// Grab all numbers from the end of the id.
preg_match('/(\d+)$/', $widget_id, $matches );
return intval( $matches[0] );
}
/**
* Return a widget by ID (formatted for output) or null if nothing is found.
*
* @param string $widget_id The id of a widget to look for.
*
* @return array|null The matching formatted widget (see format_widget).
*/
public static function get_widget_by_id( $widget_id ) {
$found = null;
foreach ( self::get_all_widgets() as $widget ) {
if ( $widget['id'] === $widget_id ) {
$found = $widget;
}
}
return $found;
}
/**
* Return an array of all widgets (active and inactive) formatted for output.
*
* @return array An array of all widgets (see format_widget).
*/
public static function get_all_widgets() {
$all_widgets = array();
$sidebars_widgets = self::get_all_sidebars();
foreach ( $sidebars_widgets as $sidebar => $widgets ) {
if ( ! is_array( $widgets ) ) {
continue;
}
foreach ( $widgets as $key => $widget_id ) {
array_push( $all_widgets, self::format_widget( $key, $widget_id, $sidebar ) );
}
}
return $all_widgets;
}
/**
* Return an array of all active widgets formatted for output.
*
* @return array An array of all active widgets (see format_widget).
*/
public static function get_active_widgets() {
$active_widgets = array();
$all_widgets = self::get_all_widgets();
foreach( $all_widgets as $widget ) {
if ( 'wp_inactive_widgets' === $widget['sidebar'] ) {
continue;
}
array_push( $active_widgets, $widget );
}
return $active_widgets;
}
/**
* Return an array of all widget IDs (active and inactive)
*
* @return array An array of all widget IDs.
*/
public static function get_all_widget_ids() {
$all_widgets = array();
$sidebars_widgets = self::get_all_sidebars();
foreach ( array_values( $sidebars_widgets ) as $widgets ) {
if ( ! is_array( $widgets ) ) {
continue;
}
foreach ( array_values( $widgets ) as $widget_id ) {
array_push( $all_widgets, $widget_id );
}
}
return $all_widgets;
}
/**
* Return an array of widgets with a specific id_base (eg: `text`).
*
* @param string $id_base The id_base of a widget type.
*
* @return array All the formatted widgets matching that widget type (see format_widget).
*/
public static function get_widgets_with_id_base( $id_base ) {
$matching_widgets = array();
foreach ( self::get_all_widgets() as $widget ) {
if ( self::get_widget_id_base( $widget['id'] ) === $id_base ) {
array_push( $matching_widgets, $widget );
}
}
return $matching_widgets;
}
/**
* Return the array of widget IDs in a sidebar or null if that sidebar does
* not exist. Will return an empty array for an existing empty sidebar.
*
* @param string $sidebar The id of a sidebar.
*
* @return array|null The array of widget IDs in the sidebar.
*/
public static function get_widgets_in_sidebar( $sidebar ) {
$sidebars = self::get_all_sidebars();
if ( ! $sidebars || ! is_array( $sidebars ) ) {
return null;
}
if ( ! $sidebars[ $sidebar ] && array_key_exists( $sidebar, $sidebars ) ) {
return array();
}
return $sidebars[ $sidebar ];
}
/**
* Return an associative array of all registered sidebars for this theme,
* active and inactive, including the hidden disabled widgets sidebar (keyed
* by `wp_inactive_widgets`). Each sidebar is keyed by the ID of the sidebar
* and its value is an array of widget IDs for that sidebar.
*
* @return array An associative array of all sidebars and their widget IDs.
*/
public static function get_all_sidebars() {
$sidebars_widgets = self::get_sidebars_widgets();
if ( ! is_array( $sidebars_widgets ) ) {
return array();
}
return $sidebars_widgets;
}
/**
* Return an associative array of all active sidebars for this theme, Each
* sidebar is keyed by the ID of the sidebar and its value is an array of
* widget IDs for that sidebar.
*
* @return array An associative array of all active sidebars and their widget IDs.
*/
public static function get_active_sidebars() {
$sidebars = array();
foreach ( self::get_all_sidebars() as $sidebar => $widgets ) {
if ( 'wp_inactive_widgets' === $sidebar || ! isset( $widgets ) || ! is_array( $widgets ) ) {
continue;
}
$sidebars[ $sidebar ] = $widgets;
}
return $sidebars;
}
/**
* Activates a widget in a sidebar. Does not validate that the sidebar exists,
* so please do that first. Also does not save the widget's settings. Please
* do that with `set_widget_settings`.
*
* If position is not set, it will be set to the next available position.
*
* @param string $widget_id The newly-formed id of the widget to be added.
* @param string $sidebar The id of the sidebar where the widget will be added.
* @param string|integer $position (Optional) The position within the sidebar where the widget will be added.
*
* @return bool
*/
public static function add_widget_to_sidebar( $widget_id, $sidebar, $position ) {
return self::move_widget_to_sidebar( array( 'id' => $widget_id ), $sidebar, $position );
}
/**
* Removes a widget from a sidebar. Does not validate that the sidebar exists
* or remove any settings from the widget, so please do that separately.
*
* @param array $widget The widget to be removed.
*/
public static function remove_widget_from_sidebar( $widget ) {
$sidebars_widgets = self::get_sidebars_widgets();
// Remove the widget from its old location and reflow the positions of the remaining widgets.
array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
update_option( 'sidebars_widgets', $sidebars_widgets );
}
/**
* Moves a widget to a sidebar. Does not validate that the sidebar exists,
* so please do that first. Also does not save the widget's settings. Please
* do that with `set_widget_settings`. The first argument should be a
* widget as returned by `format_widget` including `id`, `sidebar`, and
* `position`.
*
* If $position is not set, it will be set to the next available position.
*
* Can be used to add a new widget to a sidebar if
* $widget['sidebar'] === NULL
*
* Can be used to move a widget within a sidebar as well if
* $widget['sidebar'] === $sidebar.
*
* @param array $widget The widget to be moved (see format_widget).
* @param string $sidebar The sidebar where this widget will be moved.
* @param string|integer $position (Optional) The position where this widget will be moved in the sidebar.
*
* @return bool
*/
public static function move_widget_to_sidebar( $widget, $sidebar, $position ) {
$sidebars_widgets = self::get_sidebars_widgets();
// If a position is passed and the sidebar isn't empty,
// splice the widget into the sidebar, update the sidebar option, and return the result
if ( isset( $widget['sidebar'] ) && isset( $widget['position'] ) ) {
array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
}
// Sometimes an existing empty sidebar is NULL, so initialize it.
if ( array_key_exists( $sidebar, $sidebars_widgets ) && ! is_array( $sidebars_widgets[ $sidebar ] ) ) {
$sidebars_widgets[ $sidebar ] = array();
}
// If no position is passed, set one from items in sidebar
if ( ! isset( $position ) ) {
$position = 0;
$last_position = self::get_last_position_in_sidebar( $sidebar );
if ( isset( $last_position ) && is_numeric( $last_position ) ) {
$position = $last_position + 1;
}
}
// Add the widget to the sidebar and reflow the positions of the other widgets.
if ( empty( $sidebars_widgets[ $sidebar ] ) ) {
$sidebars_widgets[ $sidebar ][] = $widget['id'];
} else {
array_splice( $sidebars_widgets[ $sidebar ], (int)$position, 0, $widget['id'] );
}
set_theme_mod( 'sidebars_widgets', array( 'time' => time(), 'data' => $sidebars_widgets ) );
return update_option( 'sidebars_widgets', $sidebars_widgets );
}
/**
* Return an integer containing the largest position number in a sidebar or
* null if there are no widgets in that sidebar.
*
* @param string $sidebar The id of a sidebar.
*
* @return integer|null The last index position of a widget in that sidebar.
*/
public static function get_last_position_in_sidebar( $sidebar ) {
$widgets = self::get_widgets_in_sidebar( $sidebar );
if ( ! $widgets ) {
return null;
}
$last_position = 0;
foreach ( $widgets as $widget_id ) {
$widget = self::get_widget_by_id( $widget_id );
if ( intval( $widget['position'] ) > intval( $last_position ) ) {
$last_position = intval( $widget['position'] );
}
}
return $last_position;
}
/**
* Saves settings for a widget. Does not add that widget to a sidebar. Please
* do that with `move_widget_to_sidebar` first. Will merge the settings of
* any existing widget with the same `$widget_id`.
*
* @param string $widget_id The id of a widget.
* @param array $settings An associative array of settings to merge with any existing settings on this widget.
*
* @return boolean|WP_Error True if update was successful.
*/
public static function set_widget_settings( $widget_id, $settings ) {
$widget_option_name = self::get_widget_option_name( $widget_id );
$widget_settings = get_option( $widget_option_name );
$instance_key = self::get_widget_instance_key( $widget_id );
$old_settings = $widget_settings[ $instance_key ];
if ( ! $settings = self::sanitize_widget_settings( $widget_id, $settings, $old_settings ) ) {
return new WP_Error( 'invalid_data', 'Update failed.', 500 );
}
if ( is_array( $old_settings ) ) {
// array_filter prevents empty arguments from replacing existing ones
$settings = wp_parse_args( array_filter( $settings ), $old_settings );
}
$widget_settings[ $instance_key ] = $settings;
return update_option( $widget_option_name, $widget_settings );
}
/**
* Sanitize an associative array for saving.
*
* @param string $widget_id The id of a widget.
* @param array $settings A widget settings array.
* @param array $old_settings The existing widget settings array.
*
* @return array|false The settings array sanitized by `WP_Widget::update` or false if sanitization failed.
*/
private static function sanitize_widget_settings( $widget_id, $settings, $old_settings ) {
if ( ! $widget = self::get_registered_widget_object( self::get_widget_id_base( $widget_id ) ) ) {
return false;
}
$new_settings = $widget->update( $settings, $old_settings );
if ( ! is_array( $new_settings ) ) {
return false;
}
return $new_settings;
}
/**
* Deletes settings for a widget. Does not remove that widget to a sidebar. Please
* do that with `remove_widget_from_sidebar` first.
*
* @param array $widget The widget which will have its settings removed (see format_widget).
*/
public static function remove_widget_settings( $widget ) {
$widget_option_name = self::get_widget_option_name( $widget['id'] );
$widget_settings = get_option( $widget_option_name );
unset( $widget_settings[ self::get_widget_instance_key( $widget['id'] ) ] );
update_option( $widget_option_name, $widget_settings );
}
/**
* Update a widget's settings, sidebar, and position. Returns the (updated)
* formatted widget if successful or a WP_Error if it fails.
*
* @param string $widget_id The id of a widget to update.
* @param string $sidebar (Optional) A sidebar to which this widget will be moved.
* @param string|integer (Optional) A new position to which this widget will be moved within its new or existing sidebar.
* @param array|object|string $settings Settings to merge with the existing settings of the widget (will be passed through `decode_settings`).
*
* @return array|WP_Error The newly added widget as an associative array with all the above properties.
*/
public static function update_widget( $widget_id, $sidebar, $position, $settings ) {
$settings = self::decode_settings( $settings );
if ( isset( $settings ) && ! is_array( $settings ) ) {
return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
}
// Default to an empty array if nothing is specified.
if ( ! is_array( $settings ) ) {
$settings = array();
}
$widget = self::get_widget_by_id( $widget_id );
if ( ! $widget ) {
return new WP_Error( 'not_found', 'No widget found.', 400 );
}
if ( ! $sidebar ) {
$sidebar = $widget['sidebar'];
}
if ( ! isset( $position ) ) {
$position = $widget['position'];
}
if ( ! is_numeric( $position ) ) {
return new WP_Error( 'invalid_data', 'Invalid position', 400 );
}
$widgets_in_sidebar = self::get_widgets_in_sidebar( $sidebar );
if ( ! isset( $widgets_in_sidebar ) ) {
return new WP_Error( 'invalid_data', 'No such sidebar exists', 400 );
}
self::move_widget_to_sidebar( $widget, $sidebar, $position );
$widget_save_status = self::set_widget_settings( $widget_id, $settings );
if ( is_wp_error( $widget_save_status ) ) {
return $widget_save_status;
}
return self::get_widget_by_id( $widget_id );
}
/**
* Deletes a widget entirely including all its settings. Returns a WP_Error if
* the widget could not be found. Otherwise returns an empty array.
*
* @param string $widget_id The id of a widget to delete. (eg: 'text-2')
*
* @return array|WP_Error An empty array if successful.
*/
public static function delete_widget( $widget_id ) {
$widget = self::get_widget_by_id( $widget_id );
if ( ! $widget ) {
return new WP_Error( 'not_found', 'No widget found.', 400 );
}
self::remove_widget_from_sidebar( $widget );
self::remove_widget_settings( $widget );
return array();
}
/**
* Return an array of settings. The input can be either an object, a JSON
* string, or an array.
*
* @param array|string|object $settings The settings of a widget as passed into the API.
*
* @return array Decoded associative array of settings.
*/
public static function decode_settings( $settings ) {
// Treat as string in case JSON was passed
if ( is_object( $settings ) && property_exists( $settings, 'scalar' ) ) {
$settings = $settings->scalar;
}
if ( is_object( $settings ) ) {
$settings = (array) $settings;
}
// Attempt to decode JSON string
if ( is_string( $settings ) ) {
$settings = (array) json_decode( $settings );
}
return $settings;
}
/**
* Activate a new widget.
*
* @param string $id_base The id_base of the new widget (eg: 'text')
* @param string $sidebar The id of the sidebar where this widget will go. Dependent on theme. (eg: 'sidebar-1')
* @param string|integer $position (Optional) The position of the widget in the sidebar. Defaults to the last position.
* @param array|object|string $settings (Optional) An associative array of settings for this widget (will be passed through `decode_settings`). Varies by widget.
*
* @return array|WP_Error The newly added widget as an associative array with all the above properties except 'id_base' replaced with the generated 'id'.
*/
public static function activate_widget( $id_base, $sidebar, $position, $settings ) {
if ( ! isset( $id_base ) || ! self::validate_id_base( $id_base ) ) {
return new WP_Error( 'invalid_data', 'Invalid ID base', 400 );
}
if ( ! isset( $sidebar ) ) {
return new WP_Error( 'invalid_data', 'No sidebar provided', 400 );
}
if ( isset( $position ) && ! is_numeric( $position ) ) {
return new WP_Error( 'invalid_data', 'Invalid position', 400 );
}
$settings = self::decode_settings( $settings );
if ( isset( $settings ) && ! is_array( $settings ) ) {
return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
}
// Default to an empty array if nothing is specified.
if ( ! is_array( $settings ) ) {
$settings = array();
}
$widget_counter = 1 + self::get_last_widget_instance_key_with_id_base( $id_base );
$widget_id = $id_base . '-' . $widget_counter;
if ( 0 >= $widget_counter ) {
return new WP_Error( 'invalid_data', 'Error creating widget ID' . $widget_id, 500 );
}
if ( self::get_widget_by_id( $widget_id ) ) {
return new WP_Error( 'invalid_data', 'Widget ID already exists', 500 );
}
self::add_widget_to_sidebar( $widget_id, $sidebar, $position );
$widget_save_status = self::set_widget_settings( $widget_id, $settings );
if ( is_wp_error( $widget_save_status ) ) {
return $widget_save_status;
}
// Add a Tracks event for non-Headstart activity.
if ( ! defined( 'HEADSTART' ) ) {
jetpack_require_lib( 'tracks/client' );
jetpack_tracks_record_event( wp_get_current_user(), 'wpcom_widgets_activate_widget', array(
'widget' => $id_base,
'settings' => json_encode( $settings ),
) );
}
return self::get_widget_by_id( $widget_id );
}
/**
* Activate an array of new widgets. Like calling `activate_widget` multiple times.
*
* @param array $widgets An array of widget arrays. Each sub-array must be of the format required by `activate_widget`.
*
* @return array|WP_Error The newly added widgets in the form returned by `get_all_widgets`.
*/
public static function activate_widgets( $widgets ) {
if ( ! is_array( $widgets ) ) {
return new WP_Error( 'invalid_data', 'Invalid widgets', 400 );
}
$added_widgets = array();
foreach( $widgets as $widget ) {
$added_widgets[] = self::activate_widget( $widget['id_base'], $widget['sidebar'], $widget['position'], $widget['settings'] );
}
return $added_widgets;
}
/**
* Return the last instance key (integer) of an existing widget matching
* `$id_base`. So if you pass in `text`, and there is a widget with the id
* `text-2`, this function will return `2`.
*
* @param string $id_base The id_base of a type of widget. (eg: 'rss')
*
* @return integer The last instance key of that type of widget.
*/
public static function get_last_widget_instance_key_with_id_base( $id_base ) {
$similar_widgets = self::get_widgets_with_id_base( $id_base );
if ( ! empty( $similar_widgets ) ) {
// If the last widget with the same name is `text-3`, we want `text-4`
usort( $similar_widgets, __CLASS__ . '::sort_widgets' );
$last_widget = array_pop( $similar_widgets );
$last_val = intval( self::get_widget_instance_key( $last_widget['id'] ) );
return $last_val;
}
return 0;
}
/**
* Method used to sort widgets
*
* @since 5.4
*
* @param array $a
* @param array $b
*
* @return int
*/
public static function sort_widgets( $a, $b ) {
$a_val = intval( self::get_widget_instance_key( $a['id'] ) );
$b_val = intval( self::get_widget_instance_key( $b['id'] ) );
if ( $a_val > $b_val ) {
return 1;
}
if ( $a_val < $b_val ) {
return -1;
}
return 0;
}
/**
* Retrieve a given widget object instance by ID base (eg. 'text' or 'archives').
*
* @param string $id_base The id_base of a type of widget.
*
* @return WP_Widget|false The found widget object or false if the id_base was not found.
*/
public static function get_registered_widget_object( $id_base ) {
if ( ! $id_base ) {
return false;
}
// Get all of the registered widgets.
global $wp_widget_factory;
if ( ! isset( $wp_widget_factory ) ) {
return false;
}
$registered_widgets = $wp_widget_factory->widgets;
if ( empty( $registered_widgets ) ) {
return false;
}
foreach ( array_values( $registered_widgets ) as $registered_widget_object ) {
if ( $registered_widget_object->id_base === $id_base ) {
return $registered_widget_object;
}
}
return false;
}
/**
* Validate a given widget ID base (eg. 'text' or 'archives').
*
* @param string $id_base The id_base of a type of widget.
*
* @return boolean True if the widget is of a known type.
*/
public static function validate_id_base( $id_base ) {
return ( false !== self::get_registered_widget_object( $id_base ) );
}
/**
* Insert a new widget in a given sidebar.
*
* @param string $widget_id ID of the widget.
* @param array $widget_options Content of the widget.
* @param string $sidebar ID of the sidebar to which the widget will be added.
*
* @return WP_Error|true True when data has been saved correctly, error otherwise.
*/
static function insert_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
// Retrieve sidebars, widgets and their instances
$sidebars_widgets = get_option( 'sidebars_widgets', array() );
$widget_instances = get_option( 'widget_' . $widget_id, array() );
// Retrieve the key of the next widget instance
$numeric_keys = array_filter( array_keys( $widget_instances ), 'is_int' );
$next_key = $numeric_keys ? max( $numeric_keys ) + 1 : 2;
// Add this widget to the sidebar
if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
$sidebars_widgets[ $sidebar ] = array();
}
$sidebars_widgets[ $sidebar ][] = $widget_id . '-' . $next_key;
// Add the new widget instance
$widget_instances[ $next_key ] = $widget_options;
// Store updated sidebars, widgets and their instances
if (
! ( update_option( 'sidebars_widgets', $sidebars_widgets ) )
|| ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) )
) {
return new WP_Error( 'widget_update_failed', 'Failed to update widget or sidebar.', 400 );
};
return true;
}
/**
* Update the content of an existing widget in a given sidebar.
*
* @param string $widget_id ID of the widget.
* @param array $widget_options New content for the update.
* @param string $sidebar ID of the sidebar to which the widget will be added.
*
* @return WP_Error|true True when data has been updated correctly, error otherwise.
*/
static function update_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
// Retrieve sidebars, widgets and their instances
$sidebars_widgets = get_option( 'sidebars_widgets', array() );
$widget_instances = get_option( 'widget_' . $widget_id, array() );
// Retrieve index of first widget instance in that sidebar
$widget_key = false;
foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
if ( strpos( $widget, $widget_id ) !== false ) {
$widget_key = absint( str_replace( $widget_id . '-', '', $widget ) );
break;
}
}
// There is no widget instance
if ( ! $widget_key ) {
return new WP_Error( 'invalid_data', 'No such widget.', 400 );
}
// Update the widget instance and option if the data has changed
if ( $widget_instances[ $widget_key ]['title'] !== $widget_options['title']
|| $widget_instances[ $widget_key ]['address'] !== $widget_options['address']
) {
$widget_instances[ $widget_key ] = array_merge( $widget_instances[ $widget_key ], $widget_options );
// Store updated widget instances and return Error when not successful
if ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) ) {
return new WP_Error( 'widget_update_failed', 'Failed to update widget.', 400 );
};
};
return true;
}
/**
* Retrieve the first active sidebar.
*
* @return string|WP_Error First active sidebar, error if none exists.
*/
static function get_first_sidebar() {
$active_sidebars = get_option( 'sidebars_widgets', array() );
unset( $active_sidebars[ 'wp_inactive_widgets' ], $active_sidebars[ 'array_version' ] );
if ( empty( $active_sidebars ) ) {
return false;
}
$active_sidebars_keys = array_keys( $active_sidebars );
return array_shift( $active_sidebars_keys );
}
}