Making WordPress.org


Ignore:
Timestamp:
10/21/2016 04:10:02 PM (9 years ago)
Author:
iandunn
Message:

WordCamp Site Cloner: Convert to REST/Backbone to improve scalability

See #1112
Props prettyboymp

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php

    r2933 r4280  
    11<?php
    2 
    3 namespace WordCamp\Site_Cloner;
    4 
    5 defined( 'WPINC' ) or die();
    62
    73/*
    84Plugin Name: WordCamp Site Cloner
    95Description: Allows organizers to clone another WordCamp's theme and custom CSS as a starting point for their site.
    10 Version:     0.1
     6Version:     0.2
    117Author:      WordCamp.org
    128Author URI:  http://wordcamp.org
     
    1410*/
    1511
    16 // todo if Jetpack_Custom_CSS:get_css is callable, register these, otherwise fatal errors
    17 
    18 add_action( 'plugins_loaded',        __NAMESPACE__ . '\get_wordcamp_sites' );
    19 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts' );
    20 add_action( 'admin_menu',            __NAMESPACE__ . '\add_submenu_page' );
    21 add_action( 'customize_register',    __NAMESPACE__ . '\register_customizer_components' );
     12namespace WordCamp\Site_Cloner;
     13defined( 'WPINC' ) or die();
     14
     15const PRIME_SITES_CRON_ACTION      = 'wcsc_prime_sites';
     16const WORDCAMP_SITES_TRANSIENT_KEY = 'wcsc_sites';
     17
     18/**
     19 * Initialization
     20 */
     21function initialize() {
     22    // We rely on the Custom CSS module being available
     23    if ( ! class_exists( '\Jetpack' ) ) {
     24        return;
     25    }
     26
     27    add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts'               );
     28    add_action( 'admin_menu',            __NAMESPACE__ . '\add_submenu_page'               );
     29    add_action( 'customize_register',    __NAMESPACE__ . '\register_customizer_components' );
     30    add_action( 'rest_api_init',         __NAMESPACE__ . '\register_api_endpoints'         );
     31    add_action( PRIME_SITES_CRON_ACTION, __NAMESPACE__ . '\prime_wordcamp_sites'           );
     32
     33    if ( ! wp_next_scheduled( PRIME_SITES_CRON_ACTION ) ) {
     34        wp_schedule_event( time(), 'daily', PRIME_SITES_CRON_ACTION );
     35    }
     36}
     37add_action( 'plugins_loaded', __NAMESPACE__ . '\initialize' ); // After Jetpack has loaded
    2238
    2339/**
     
    2945        plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.css',
    3046        array(),
    31         1
     47        2
    3248    );
    3349
     
    3551        'wordcamp-site-cloner',
    3652        plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.js',
    37         array( 'jquery', 'customize-controls' ),
    38         1,
     53        array( 'jquery', 'customize-controls', 'wp-backbone' ),
     54        2,
    3955        true
    4056    );
     57
     58    wp_localize_script(
     59        'wordcamp-site-cloner',
     60        '_wcscSettings',
     61        array(
     62            'apiUrl'        => get_rest_url( null, '/wordcamp-site-cloner/v1/sites/' ),
     63            'customizerUrl' => admin_url( 'customize.php' ),
     64            'themes'        => get_available_themes(),
     65        )
     66    );
     67}
     68
     69/**
     70 * Get all of the available themes
     71 *
     72 * @return array
     73 */
     74function get_available_themes() {
     75    /** @var \WP_Theme $theme */
     76    $available_themes = array();
     77    $raw_themes       = wp_get_themes( array( 'allowed' => true ) );
     78
     79    foreach ( $raw_themes as $theme ) {
     80        $theme_name         = $theme->display( 'Name' );
     81        $available_themes[] = array(
     82            'slug' => $theme->get_stylesheet(),
     83            'name' => $theme_name ?: $theme->get_stylesheet()
     84        );
     85    }
     86
     87    return $available_themes;
    4188}
    4289
     
    53100        __( 'Clone Another WordCamp', 'wordcamporg' ),
    54101        'switch_themes',
    55         'customize.php?autofocus[panel]=wordcamp_site_cloner'
     102        'customize.php?autofocus[section]=wcsc_sites'
    56103    );
    57104}
     
    64111function register_customizer_components( $wp_customize ) {
    65112    require_once( __DIR__ . '/includes/source-site-id-setting.php' );
    66     require_once( __DIR__ . '/includes/sites-section.php' );
    67113    require_once( __DIR__ . '/includes/site-control.php' );
    68 
    69     $wp_customize->register_control_type( __NAMESPACE__ . '\Site_Control' );
    70114
    71115    $wp_customize->add_setting( new Source_Site_ID_Setting(
    72116        $wp_customize,
    73117        'wcsc_source_site_id',
    74         array()
     118        array( 'capability' => 'switch_themes' )
    75119    ) );
    76120
    77     $wp_customize->add_panel(
    78         'wordcamp_site_cloner',
    79         array(
    80             'type'        => 'wcscPanel',
    81             'title'       => __( 'Clone Another WordCamp', 'wordcamporg' ),
    82             'description' => __( "Clone another WordCamp's theme and custom CSS as a starting point for your site.", 'wordcamporg' ),
    83         )
    84     );
    85 
    86     $wp_customize->add_section( new Sites_Section(
    87         $wp_customize,
     121    $wp_customize->add_section(
    88122        'wcsc_sites',
    89123        array(
    90             'panel' => 'wordcamp_site_cloner',
    91             'title' => __( 'WordCamp Sites', 'wordcamporg' ),
     124            'title'      => __( 'Clone Another WordCamp', 'wordcamporg' ),
     125            'capability' => 'switch_themes'
     126        )
     127    );
     128
     129    $wp_customize->add_control( new Site_Control(
     130        $wp_customize,
     131        'wcsc_site_search',
     132        array(
     133            'type'     => 'wcscSearch',
     134            'label'    => __( 'Search', 'wordcamporg' ),
     135            'settings' => 'wcsc_source_site_id',
     136            'section'  => 'wcsc_sites'
    92137        )
    93138    ) );
    94 
    95     foreach( get_wordcamp_sites() as $wordcamp ) {
    96         if ( get_current_blog_id() == $wordcamp['site_id'] ) {
    97             continue;
    98         }
    99 
    100         $wp_customize->add_control( new Site_Control(
    101             $wp_customize,
    102             'wcsc_site_id_' . $wordcamp['site_id'],
    103             array(
    104                 'type'           => 'wcscSite',                      // todo should be able to set this in control instead of here, but if do that then control contents aren't rendered
    105                 'site_id'        => $wordcamp['site_id'],
    106                 'site_name'      => $wordcamp['name'],
    107                 'theme_slug'     => $wordcamp['theme_slug'],
    108                 'screenshot_url' => $wordcamp['screenshot_url'],
    109             )
    110         ) );
    111     }
    112 }
    113 
    114 /**
    115  * Get required data for relevant WordCamp sites
    116  *
    117  * This isn't actually used until register_customizer_components(), but it's called during `plugins_loaded` in
    118  * order to prime the cache. That has to be done before `setup_theme`, because the Theme Switcher will override
    119  * the current theme when `?theme=` is present in the URL parameters, and it's safer to just avoid that than to
    120  * muck with the internals and try to reverse it on the fly.
     139}
     140
     141/**
     142 * Register the REST API endpoint for the Customizer to use to retriever the site list
     143 */
     144function register_api_endpoints() {
     145    if ( ! current_user_can( 'switch_themes' ) ) {
     146        return;
     147
     148        // todo - use `permission_callback` instead
     149    }
     150
     151    register_rest_route(
     152        'wordcamp-site-cloner/v1',
     153        '/sites',
     154        array(
     155            'methods'  => 'GET',
     156            'callback' => __NAMESPACE__ . '\sites_endpoint',
     157        )
     158    );
     159}
     160
     161/**
     162 * Handle the response for the Sites endpoint
     163 *
     164 * This always pulls cached data, because Central needs to be the site generating it. See get_wordcamp_sites().
    121165 *
    122166 * @return array
    123167 */
     168function sites_endpoint() {
     169    $sites        = array();
     170    $cached_sites = get_site_transient( WORDCAMP_SITES_TRANSIENT_KEY );
     171
     172    if ( $cached_sites ) {
     173        unset( $cached_sites[ get_current_blog_id() ] );
     174
     175        $sites = array_values( $cached_sites );
     176    }
     177
     178    return $sites;
     179}
     180
     181/**
     182 * Prime the cache of cloneable WordCamp sites
     183 *
     184 * This is called via WP Cron.
     185 *
     186 * @todo - Reintroduce batching from `1112.3.diff` to get more than 500 sites
     187 */
     188function prime_wordcamp_sites() {
     189    // This only needs to run on a single site, then the whole network can use the cached result
     190    if ( ! is_main_site() ) {
     191        return;
     192    }
     193
     194    // Keep the cache longer than needed, just to be sure that it doesn't expire before the cron job runs again
     195    set_site_transient( WORDCAMP_SITES_TRANSIENT_KEY, get_wordcamp_sites(), DAY_IN_SECONDS * 2 );
     196}
     197
     198/**
     199 * Get WordCamp sites that are suitable for cloning
     200 *
     201 * @return array
     202 */
    124203function get_wordcamp_sites() {
    125     require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' );
    126 
    127     // plugins_loaded is runs on every screen, but we only need this when loading the Customizer and Previewer
    128     if ( 'customize.php' != basename( $_SERVER['SCRIPT_NAME'] ) && empty( $_REQUEST['wp_customize'] ) ) {
     204    /*
     205     * The post statuses that \WordCamp_Loader::get_public_post_statuses() returns are only created on Central,
     206     * because the plugin isn't active on any other sites.
     207     */
     208    if ( ! is_main_site() ) {
    129209        return array();
    130210    }
    131211
    132     $transient_key = 'wcsc_sites';
    133 
    134     if ( $sites = get_site_transient( $transient_key ) ) {
    135         return $sites;
     212    if ( ! \Jetpack::is_module_active( 'custom-css' ) ) {
     213        \Jetpack::activate_module( 'custom-css', false, false );
    136214    }
    137215
    138216    switch_to_blog( BLOG_ID_CURRENT_SITE ); // central.wordcamp.org
    139217
    140     $sites = array();
    141     $wordcamps = get_posts( array(
    142         'post_type'      => 'wordcamp',
     218    $wordcamp_query = new \WP_Query( array(
     219        'post_type'      => WCPT_POST_TYPE_ID,
    143220        'post_status'    => \WordCamp_Loader::get_public_post_statuses(),
    144         'posts_per_page' => 125, // todo temporary workaround until able to add filters to make hundreds of sites manageable
     221        'posts_per_page' => 500,
    145222        'meta_key'       => 'Start Date (YYYY-mm-dd)',
    146223        'orderby'        => 'meta_value_num',
     224        'order'          => 'DESC',
    147225
    148226        'meta_query' => array(
    149227            array(
     228                // New sites won't have finished designs, so ignore them
    150229                'key'     => 'Start Date (YYYY-mm-dd)',
    151230                'value'   => strtotime( 'now - 1 month' ),
    152231                'compare' => '<'
    153             ),
     232            )
    154233        ),
    155234    ) );
    156235
    157     foreach( $wordcamps as $wordcamp ) {
    158         $site_id  = get_wordcamp_site_id( $wordcamp );
    159         $site_url = get_post_meta( $wordcamp->ID, 'URL', true );
    160 
    161         if ( ! $site_id || ! $site_url ) {
     236    $sites = get_filtered_wordcamp_sites( $wordcamp_query->get_posts() );
     237
     238    uasort( $sites, __NAMESPACE__ . '\sort_sites_by_year' );
     239
     240    restore_current_blog();
     241
     242    return $sites;
     243}
     244
     245/**
     246 * Filter out sites that aren't relevant to the Cloner
     247 *
     248 * @param array $wordcamps
     249 *
     250 * @return array
     251 */
     252function get_filtered_wordcamp_sites( $wordcamps ) {
     253    $sites = array();
     254
     255    foreach ( $wordcamps as $wordcamp ) {
     256        $site_id    = get_wordcamp_site_id( $wordcamp );
     257        $site_url   = get_post_meta( $wordcamp->ID, 'URL',                     true );
     258        $start_date = get_post_meta( $wordcamp->ID, 'Start Date (YYYY-mm-dd)', true );
     259
     260        if ( ! $site_id || ! $site_url || ! $start_date ) {
    162261            continue;
    163262        }
     
    165264        switch_to_blog( $site_id );
    166265
    167         $sites[] = array(
    168             'site_id'        => $site_id,
    169             'name'           => get_wordcamp_name(),
    170             'theme_slug'     => get_stylesheet(),
    171             'screenshot_url' => get_screenshot_url( $site_url ),
    172         );
     266        /*
     267         * Sites with Coming Soon enabled probably don't have a finished design yet, so there's no point in
     268         * cloning it.
     269         */
     270        if ( ! coming_soon_plugin_enabled() ) {
     271            $preprocessor = \Jetpack_Custom_CSS::get_preprocessor();
     272            $preprocessor = isset( $preprocessor[ 'name' ] ) ? $preprocessor[ 'name' ] : 'none';
     273
     274            $sites[ $site_id ] = array(
     275                'site_id'          => $site_id,
     276                'name'             => get_wordcamp_name(),
     277                'theme_slug'       => get_stylesheet(),
     278                'screenshot_url'   => get_screenshot_url( $site_url ),
     279                'year'             => date( 'Y', $start_date ),
     280                'css_preprocessor' => $preprocessor,
     281            );
     282        }
    173283
    174284        restore_current_blog();
    175285    }
    176286
    177     restore_current_blog();
    178 
    179     set_site_transient( $transient_key, $sites, DAY_IN_SECONDS );
    180 
    181287    return $sites;
     288}
     289
     290/**
     291 * Determine if the Coming Soon plugin is enabled for the current site
     292 *
     293 * @return bool
     294 */
     295function coming_soon_plugin_enabled() {
     296    global $WCCSP_Settings;
     297    $enabled = false;
     298
     299    if ( ! is_callable( 'WCCSP_Settings::get_settings' ) ) {
     300        return $enabled;
     301    }
     302
     303    // We may need to instantiate the class if this is the first time calling this function
     304    if ( ! is_a( $WCCSP_Settings, 'WCCSP_Settings' ) ) {
     305        $WCCSP_Settings = new \WCCSP_Settings();
     306    }
     307
     308    $settings = $WCCSP_Settings->get_settings();
     309
     310    if ( isset( $settings[ 'enabled' ] ) && 'on' === $settings[ 'enabled' ] ) {
     311        $enabled = true;
     312    }
     313
     314    return $enabled;
    182315}
    183316
     
    196329    return apply_filters( 'wcsc_site_screenshot_url', $screenshot_url );
    197330}
     331
     332/**
     333 * Sort arrays by the year
     334 *
     335 * @param array $site_a
     336 * @param array $site_b
     337 *
     338 * @return int
     339 */
     340function sort_sites_by_year( $site_a, $site_b ) {
     341    if ( $site_a[ 'year' ] === $site_b[ 'year' ] ) {
     342        return 0;
     343    }
     344
     345    return ( $site_a[ 'year' ] < $site_b[ 'year' ] ? 1 : -1 );
     346}
Note: See TracChangeset for help on using the changeset viewer.