WordPress.org

Making WordPress.org

Ticket #1112: 1112.3.diff

File 1112.3.diff, 40.9 KB (added by prettyboymp, 4 years ago)
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php
    index 8628d2c..88bc3d1 100644
    a b namespace WordCamp\Site_Cloner; 
    55defined( 'WPINC' ) or die();
    66
    77/**
    8  * Custom Customizer Control for a WordCamp site
     8 * Custom Customizer Control for Search WordCamp sites to clone
    99 */
    1010class Site_Control extends \WP_Customize_Control {
    11         public $site_id, $site_name, $screenshot_url, $theme_slug;
    12         public $settings = 'wcsc_source_site_id';
    13         public $section  = 'wcsc_sites';
     11
     12        public function __construct( $manager, $id, $args = [ ] ) {
     13                parent::__construct( $manager, $id, $args );
     14
     15                $this->capability = 'edit_theme_options';
     16                $this->section = 'wcsc_sites';
     17        }
    1418
    1519        /**
    1620         * Enqueue scripts and styles
    class Site_Control extends \WP_Customize_Control { 
    1822        public function enqueue() {
    1923                wp_enqueue_style(  'wordcamp-site-cloner' );
    2024                wp_enqueue_script( 'wordcamp-site-cloner' );
     25                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_view_templates' ) );
    2126        }
    2227
    23         /**
    24          * Render the control's content
    25          */
    2628        public function render_content() {
    27                 $preview_url = add_query_arg(
    28                         array(
    29                                 'theme'               => rawurlencode( $this->theme_slug ),
    30                                 'wcsc_source_site_id' => rawurlencode( $this->site_id ),
    31                         ),
    32                         admin_url( 'customize.php' )
    33                 );
    34 
    3529                require( dirname( __DIR__ ) . '/templates/site-control.php' );
    3630        }
     31
     32        public function print_view_templates() {
     33                ?>
     34                <script id="tmpl-wcsc-site-option" type="text/html">
     35                        <?php include dirname( __DIR__ ) . '/templates/site-option.php'; ?>
     36                </script>
     37                <script id="tmpl-wcsc-site-filters" type="text/html">
     38                        <?php include dirname( __DIR__ ) . '/templates/site-filters.php'; ?>
     39                </script>
     40                <?php
     41        }
    3742}
  • deleted file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php
    deleted file mode 100644
    index 69b85e4..0000000
    + -  
    1 <?php
    2 
    3 namespace WordCamp\Site_Cloner;
    4 
    5 defined( 'WPINC' ) or die();
    6 
    7 /**
    8  * Custom Customizer Section for WordCamp sites
    9  */
    10 class Sites_Section extends \WP_Customize_Section {
    11         public $type = 'wcsc-sites';
    12 
    13         /**
    14          * Render the section's content
    15          */
    16         protected function render() {
    17                 require_once( dirname( __DIR__ ) . '/templates/sites-section.php' );
    18         }
    19 }
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php
    index ae450ea..5e84f42 100644
    a b class Source_Site_ID_Setting extends \WP_Customize_Setting { 
    2525                        return;
    2626                }
    2727
     28                // disable the current site's custom css from being output
     29                remove_action( 'wp_head', array( 'Jetpack_Custom_CSS', 'link_tag' ), 101 );
    2830                add_action( 'wp_head',                 array( $this, 'preview_source_site_css'  ), 99 );   // wp_print_styles is too early; the theme's stylesheet would get enqueued later and take precedence
    2931                add_filter( 'get_post_metadata',       array( $this, 'preview_jetpack_postmeta' ), 10, 4 );
    3032                add_filter( 'safecss_skip_stylesheet', array( $this, 'preview_skip_stylesheet'  ) );
    class Source_Site_ID_Setting extends \WP_Customize_Setting { 
    109111         * to the URL.
    110112         *
    111113         * @param int $source_site_id
     114         *
     115         * @return null
    112116         */
    113117        protected function update( $source_site_id ) {
    114118                switch_to_blog( $source_site_id );
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php
    index 952a134..9b60dba 100644
    a b  
    1 <?php defined( 'WPINC' ) or die(); ?>
     1<?php
     2defined( 'WPINC' ) or die();
    23
    3 <div id="wcsc-site-<?php echo esc_attr( $this->site_id ); ?>" class="wcscSite" data-preview-url="<?php echo esc_url( $preview_url ); ?>">
    4         <div class="wcsc-site-screenshot">
    5                 <img src="<?php echo esc_url( $this->screenshot_url ); ?>" alt="<?php echo esc_attr( $this->site_name ); ?>" />
    6         </div>
    7 
    8         <h3 class="wcsc-site-name">
    9                 <?php echo esc_html( $this->site_name ); ?>
     4/**
     5 *  Top level template for the output of the Site Cloner Customizer Control
     6 */
     7?>
     8<div id="wcsc-cloner">
     9        <h3>
     10                <?php esc_html_e( 'WordCamp Sites', 'wordcamporg' ); ?>
     11                <span id="wcsc-sites-count" class="title-count wcsc-sites-count"></span>
    1012        </h3>
     13        <div class="filters">
     14        </div>
    1115
    12         <span id="live-preview-label-<?php echo esc_attr( $this->site_id ); ?>" class="wcsc-live-preview-label">
    13                 <?php _e( 'Live Preview', 'wordcamporg' ); ?>
    14         </span>
    15 </div>
     16        <div class="wcsc-search">
     17                <ul id="wcsc-results">
     18                </ul>
     19        </div>
     20</div>
     21 No newline at end of file
  • new file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php
    new file mode 100644
    index 0000000..ecf9a9e
    - +  
     1<?php
     2/**
     3 *  Template for the search and drop down filters
     4 */
     5
     6defined( 'WPINC' ) or die();
     7
     8?>
     9        <div class="wcsc-filter">
     10                <label for="wcsc-filter-search-input">
     11                        <span class="customize-control-title"><?php _e( 'Search', 'wordcamporg' ); ?></span>
     12                        <div class="customize-control-content">
     13                                <input type="search" id="wcsc-filter-search-input" class="wcsc-filter-search"/>
     14                        </div>
     15                </label>
     16        </div>
     17
     18        <div class="wcsc-filter">
     19                <label for="wcsc-filter-theme_slug">
     20                        <span class="customize-control-title"><?php _e( 'Theme', 'wordcamporg' ); ?></span>
     21                        <div class="customize-control-content">
     22                                <select id="wcsc-filter-theme_slug" data-filter="theme_slug">
     23                                        <option value="">Any</option>
     24                                        <# _.each(data.themeOptions, function(themeOption) { #>
     25                                                <option value="{{themeOption.slug}}">{{{themeOption.name}}}</option>
     26                                                <# }); #>
     27                                </select>
     28                        </div>
     29                </label>
     30        </div>
     31
     32        <div class="wcsc-filter">
     33                <label for="wcsc-filter-year">
     34                        <span class="customize-control-title"><?php _e( 'WordCamp Year', 'wordcamporg' ); ?></span>
     35                        <div class="customize-control-content">
     36                                <select id="wcsc-filter-year" data-filter="year">
     37                                        <option value="">Any</option>
     38                                        <# _.each(data.yearOptions, function(yearOption) { #>
     39                                                <option value="{{yearOption}}">{{yearOption}}</option>
     40                                                <# }); #>
     41                                </select>
     42                        </div>
     43                </label>
     44        </div>
     45
     46        <div class="wcsc-filter">
     47                <label for="wcsc-filter-css_preprocessor">
     48                        <span class="customize-control-title"><?php _e( 'CSS Preprocessor', 'wordcamporg' ); ?></span>
     49                        <div class="customize-control-content">
     50                                <select id="wcsc-filter-css_preprocessor" data-filter="css_preprocessor">
     51                                        <option value="">Any</option>
     52                                        <# _.each(data.preprocessorOptions, function(preprocessorOption) { #>
     53                                                <option value="{{preprocessorOption}}">{{preprocessorOption}}</option>
     54                                                <# }); #>
     55                                </select>
     56                        </div>
     57                </label>
     58        </div>
     59<?php
     60 No newline at end of file
  • new file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php
    new file mode 100644
    index 0000000..a8c7213
    - +  
     1<?php
     2/**
     3 *  Template for a single Site representation within the Site Cloner Control
     4 */
     5
     6defined( 'WPINC' ) or die();
     7
     8?>
     9        <div class="wcsc-site-screenshot">
     10                <img src="{{ data.screenshot_url }}" alt="{{ data.name }}"/>
     11        </div>
     12
     13        <h3 class="wcsc-site-name">
     14                {{ data.name }}
     15        </h3>
     16
     17        <# if ( data.active ) { #>
     18                <span id="live-previewing-{{ data.site_id }}" class="wcsc-previewing-label">
     19                        <?php _e( 'Viewing', 'wordcamporg' ); ?>
     20                </span>
     21        <# } else { #>
     22                <span id="live-preview-label-{{ data.site_id }}" class="wcsc-live-preview-label">
     23                        <?php _e( 'Live Preview', 'wordcamporg' ); ?>
     24                </span>
     25        <# } #>
     26<?php
     27 No newline at end of file
  • deleted file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php
    deleted file mode 100644
    index a05272c..0000000
    + -  
    1 <?php defined( 'WPINC' ) or die(); ?>
    2 
    3 <li id="section-<?php echo esc_attr( $this->id ); ?>" class="accordion-section control-section control-section-<?php echo esc_attr( $this->type ); ?>">
    4         <h3>
    5                 <?php esc_html_e( 'WordCamp Sites' ); ?>
    6 
    7                 <span class="title-count wcsc-sites-count">
    8                         <?php echo count( $this->controls ); ?>
    9                 </span>
    10         </h3>
    11 
    12         <div class="wcsc-sites-section-content">
    13                 <ul id="wcsc-sites"></ul>
    14         </div>
    15 </li>
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css
    index 0a33876..98eaf07 100644
    a b  
    66                overflow: auto;
    77        }
    88
    9                 .wcscSite {
     9                .wcsc-site {
    1010                        position: relative;
    1111                        cursor: pointer;
    1212                        border: 1px solid #DEDEDE;
    1313                        box-shadow: 0 1px 1px -1px rgba( 0, 0, 0, 0.1 );
     14                        margin-top: 5px;
    1415                }
    1516
    1617                        .wcsc-site-screenshot {
    1718                                transition: opacity 0.2s ease-in-out 0s;
    1819                        }
    1920
    20                                 .wcscSite:hover .wcsc-site-screenshot {
     21                                .wcsc-site:hover .wcsc-site-screenshot {
    2122                                        opacity: 0.4;
    2223                                }
    2324
    24                         .wcsc-live-preview-label {
     25                        .wcsc-live-preview-label, .wcsc-previewing-label {
    2526                                opacity: 0;
    2627                                position: absolute;
    2728                                top: 35%;
     
    3839                                transition: opacity 0.1s ease-in-out 0s;
    3940                        }
    4041
    41                                 .wcscSite:hover .wcsc-live-preview-label {
     42                                .wcsc-site:hover .wcsc-live-preview-label, .wcsc-site .wcsc-previewing-label {
    4243                                        opacity: 1;
    4344                                }
     45
     46div.wcsc-filter {
     47        margin-bottom: 10px;
     48}
     49 No newline at end of file
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js
    index 5c05f82..5bf3f64 100644
    a b  
    1 ( function( wp, $ ) {
     1(function ( wp, $, Backbone, win, settings ) {
    22        'use strict';
    33
    4         if ( ! wp || ! wp.customize ) {
     4        if ( !wp || !wp.customize ) {
    55                return;
    66        }
    77
    8         var api = wp.customize;
     8        var api  = wp.customize,
     9                        wcsc = wp.wcSiteCloner = { models: {}, views: {}, collections: {}, routers: {}, settings: {} };
    910
    10         /**
    11          * The Clone Another WordCamp panel
    12          */
    13         api.panelConstructor.wcscPanel = api.Panel.extend( {
    14                 /**
    15                  * Initialize the panel after it's loaded
    16                  *
    17                  * Ideally, the Previewer would be set to the requested site ID during the initial PHP request, rather than
    18                  * loading the host site in the Previewer, and then refreshing it to use the requested site. That became a
    19                  * rabbit hole, though, so it's done this way instead.
    20                  */
    21                 ready : function() {
    22                         var urlParams = getUrlParams( window.location.href );
     11        wcsc.settings = settings || {};
    2312
    24                         if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) {
    25                                 this.expand();
    26                                 api( 'wcsc_source_site_id' ).set( urlParams.wcsc_source_site_id );
     13        // Model for a single site
     14        wcsc.models.Site = Backbone.Model.extend( {
     15                defaults: {
     16                        'active': false
     17                },
     18                idAttribute: 'site_id'
     19        } );
     20
     21        // Model representing the filter state for searching/filtering sites
     22        wcsc.models.SearchFilter = Backbone.Model.extend( {
     23                's': '',
     24                'theme_slug': '',
     25                'year': '',
     26                'css_preprocessor': ''
     27        } );
     28
     29        // Top level view for the Site Cloner Control
     30        wcsc.views.SiteSearch = Backbone.View.extend( {
     31                el: '#wcsc-cloner .wcsc-search',
     32
     33                // index of the currently viewed page of results
     34                page: 0,
     35
     36                initialize: function ( options ) {
     37
     38                        // update scroller position
     39                        _.bindAll( this, 'scroller' );
     40
     41                        // container that will be scrolled within
     42                        this.$container = $( '#wcsc-cloner' ).parents( 'ul.accordion-section-content' );
     43                        // bind scrolling within the container to check for infinite scroll
     44                        this.$container.bind( 'scroll', _.throttle( this.scroller, 300 ) );
     45
     46                        // the model and view for filtering the site results
     47                        this.filterView = new wcsc.views.SearchFilters( {
     48                                model: this.collection.searchFilter,
     49                                parent: this
     50                        } );
     51
     52                        // View for listing the matching sites
     53                        this.resultsView = new wcsc.views.SearchResults( {
     54                                collection: this.collection,
     55                                parent: this
     56                        } );
     57
     58                },
     59
     60                render: function () {
     61                        this.filterView.render();
     62
     63                        this.resultsView.render();
     64
     65                        this.$el.empty().append( this.resultsView.el );
     66                },
     67
     68                // Checks if a user has reached the bottom of the list
     69                // and triggers a scroll event to show more sites if needed
     70                scroller: function () {
     71                        var visibleBottom, threshold, elementHeight, containerHeight, scrollTop;
     72
     73                        scrollTop = this.$container.scrollTop();
     74                        containerHeight = this.$container.innerHeight();
     75                        elementHeight = this.$container.get( 0 ).scrollHeight;
     76
     77                        visibleBottom = scrollTop + containerHeight;
     78                        threshold = Math.round( elementHeight * 0.9 );
     79
     80                        if ( visibleBottom > threshold ) {
     81                                this.trigger( 'wcsc:scroll' );
    2782                        }
    2883                }
     84
    2985        } );
    3086
    31         /**
    32          * Custom control representing a site that can be previewed/imported
    33          */
    34         api.controlConstructor.wcscSite = api.Control.extend( {
    35                 /**
    36                  * Initialize the control after it's loaded
    37                  */
    38                 ready : function() {
    39                         this.container.on( 'click', '.wcscSite', this.previewSite );
    40                 },
    41 
    42                 /**
    43                  * Preview the selected site
    44                  *
    45                  * If the site is using a different theme, then reload the entire Customizer with the theme URL parameter
    46                  * set, so that the Theme Switcher will handle previewing the new theme for us. Otherwise just set the ID
    47                  * to refresh the Previewer with the current theme and the new site's CSS, etc.
    48                  *
    49                  * @param {object} event
    50                  */
    51                 previewSite : function( event ) {
    52                         var previewUrl       = $( this ).data( 'preview-url' ),
    53                                 previewUrlParams = getUrlParams( previewUrl );
    54 
    55                         if ( api( 'wcsc_source_site_id' ).get() == previewUrlParams.wcsc_source_site_id ) {
     87        // Collection representing the list of cloneable sites
     88        wcsc.collections.Sites = Backbone.Collection.extend( {
     89                model: wcsc.models.Site,
     90                url: wcsc.settings.apiUrl,
     91
     92                initialize: function ( options ) {
     93                        this.searchFilter = options.searchFilter || {};
     94
     95                        this.listenTo( this.searchFilter, 'change', this.applyFilter );
     96                },
     97
     98                // Filter this collection by the updated searchFilter attributes
     99                applyFilter: function () {
     100                        var filters       = this.searchFilter.toJSON(),
     101                                        activeFilters = _.pick( filters, _.identity ),
     102                                        term          = '',
     103                                        sites;
     104
     105                        // nothing actually changed, so don't update the collection
     106                        if ( _.isEmpty( this.searchFilter.changedAttributes() ) ) {
     107                                return;
     108                        }
     109
     110                        //no active filters.  Reset to the full list and bail
     111                        if ( _.isEmpty( activeFilters ) ) {
     112                                this.resetCanonical();
     113                                return;
     114                        }
     115
     116                        this.resetCanonical( { silent: true } );
     117
     118                        // remove the search query restriction since we already filtered by word matches above
     119                        if ( activeFilters.s ) {
     120                                term = activeFilters.s;
     121                                delete activeFilters.s;
     122                        }
     123
     124                        sites = this.where( activeFilters );
     125
     126                        if ( term ) {
     127                                sites = this.filterBySearch( sites, term );
     128                        }
     129
     130                        this.reset( sites );
     131
     132                },
     133
     134                // internal method for filtering sites by search terms
     135                filterBySearch: function ( sites, term ) {
     136                        var match, name;
     137
     138                        // Escape the term string for RegExp meta characters
     139                        term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
     140
     141                        // Consider spaces as word delimiters and match the whole string
     142                        // so matching terms can be combined
     143                        term = term.replace( / /g, ')(?=.*' );
     144                        match = new RegExp( '^(?=.*' + term + ').+', 'i' );
     145
     146                        return _.filter( sites, function ( site ) {
     147                                name = site.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
     148
     149                                return match.test( name );
     150                        } );
     151                },
     152
     153                paginate: function ( pageIndex ) {
     154                        var collection = this;
     155                        pageIndex = pageIndex || 0;
     156
     157                        collection = _( collection.rest( 20 * pageIndex ) );
     158                        collection = _( collection.first( 20 ) );
     159                        return collection;
     160                },
     161
     162                // Resets the site collection dataset to the canonical list originally pulled from the api
     163                resetCanonical: function ( options ) {
     164                        var activeSiteId = api( 'wcsc_source_site_id' ).get(),
     165                                        activeSite;
     166                        options = options || {};
     167
     168                        this.reset( wcsc.settings.siteData, options );
     169
     170                        //restore the currently active site
     171                        if ( activeSiteId ) {
     172                                activeSite = this.find( { site_id: activeSiteId } );
     173                                if ( typeof activeSite !== 'undefined' ) {
     174                                        activeSite.set( { active: true } );
     175                                }
     176                        }
     177                }
     178        } );
     179
     180        // View for a single site
     181        wcsc.views.Site = Backbone.View.extend( {
     182                className: 'wcsc-site',
     183
     184                attributes: function () {
     185                        return {
     186                                'id': 'wcsc-site-' + this.model.get( 'site_id' ),
     187                                'data-site-id': this.model.get( 'site_id' )
     188                        }
     189                },
     190
     191                html: wp.template( 'wcsc-site-option' ),
     192
     193                touchDrag: false,
     194
     195                events: {
     196                        'click': 'preview',
     197                        'keydown': 'preview',
     198                        'touchend': 'preview',
     199                        'touchmove': 'preventPreview'
     200                },
     201
     202                initialize: function ( options ) {
     203                        this.parent = options.parent;
     204
     205                        this.listenTo( this.model, 'change', this.render );
     206
     207                        this.render();
     208                },
     209
     210                render: function () {
     211                        var data = this.model.toJSON();
     212                        this.$el.html( this.html( data ) );
     213                },
     214
     215                preventPreview: function () {
     216                        this.touchDrag = true;
     217                },
     218
     219                preview: function ( event ) {
     220                        event = event || window.event;
     221
     222                        //ignore touches caused by scrolling
     223                        if ( this.touchDrag === true ) {
     224                                this.touchDrag = false;
     225                        }
     226
     227                        event.preventDefault();
     228
     229                        this.$el.trigger( 'wcsc:previewSite', this.model );
     230                }
     231
     232        } );
     233
     234        // View for the site results list
     235        wcsc.views.SearchResults = Backbone.View.extend( {
     236                className: 'wcsc-results',
     237
     238                initialize: function ( options ) {
     239                        var self = this;
     240
     241                        this.parent = options.parent;
     242
     243                        this.$siteCount = $( '#wcsc-sites-count' );
     244
     245                        // Rerender the view whenever a collection change is complete
     246                        this.listenTo( this.collection, 'reset', function () {
     247                                //reset pagination
     248                                self.parent.page = 0;
     249                                self.render( this );
     250                        } );
     251
     252                        this.listenTo( this.parent, 'wcsc:scroll', function () {
     253                                self.renderSites( self.parent.page );
     254                        } );
     255                },
     256
     257                render: function () {
     258                        this.$el.empty();
     259
     260                        this.renderSites( this.parent.page );
     261
     262                        this.$siteCount.text( this.collection.length );
     263                },
     264
     265                renderSites: function ( page ) {
     266                        var self = this;
     267
     268                        // get a collection of just the requested page
     269                        this.instance = this.collection.paginate( page );
     270
     271                        if ( this.instance.size() === 0 ) {
     272                                this.parent.trigger( 'wcsc:end' );
     273                                return;
     274                        }
     275
     276                        this.instance.each( function ( site ) {
     277                                var siteView = new wcsc.views.Site( {
     278                                        model: site,
     279                                        parent: self
     280                                } );
     281
     282                                siteView.render();
     283
     284                                self.$el.append( siteView.el );
     285                        } );
     286
     287                        this.parent.page++;
     288                }
     289
     290        } );
     291
     292        // View for the search and dropdown filters
     293        wcsc.views.SearchFilters = Backbone.View.extend( {
     294                el: '#wcsc-cloner .filters',
     295                className: 'wscs-filters',
     296                html: wp.template( 'wcsc-site-filters' ),
     297                events: {
     298                        "input #wcsc-filter-search-input": "search",
     299                        "keyup #wcsc-filter-search-input": "search",
     300                        "change select": "applyFilter"
     301                },
     302                initialize: function ( options ) {
     303                        this.parent = options.parent;
     304                },
     305
     306                render: function () {
     307                        var data = {};
     308
     309                        data.themeOptions = wcsc.settings.themes;
     310                        data.yearOptions = _.uniq( this.parent.collection.pluck( 'year' ) ).sort();
     311                        data.preprocessorOptions = _.uniq( this.parent.collection.pluck( 'css_preprocessor' ) ).sort();
     312
     313                        this.$el.html( this.html( data ) );
     314
     315                        this.$searchInput = $( '#wcsc-filter-search-input' );
     316                        this.$themeFilter = $( '#wcsc-filter-theme_slug' );
     317                        this.$yearFilter = $( '#wcsc-filter-year' );
     318                        this.$preprocessorFilter = $( '#wcsc-filter-css_preprocessor' );
     319                },
     320
     321                search: function ( event ) {
     322                        // Clear on escape.
     323                        if ( event.type === 'keyup' && event.which === 27 ) {
     324                                event.target.value = '';
     325                        }
     326
     327                        /**
     328                         * Since doSearch is debounced, it will only run when user input comes to a rest
     329                         */
     330                        this.doSearch( event );
     331                },
     332
     333                doSearch: _.debounce( function ( event ) {
     334                        var options = {};
     335
     336                        this.model.set( 's', event.target.value );
     337
     338                }, 500 ),
     339
     340                applyFilter: function ( event ) {
     341                        var $target = $( event.target ),
     342                                        value   = $target.val(),
     343                                        filter  = $target.data( 'filter' );
     344                        this.model.set( filter, value );
     345                },
     346
     347                // Set the inputs to the set of filters as triggered by the router on initial load
     348                setInputs: function ( filters ) {
     349                        this.model.set( filters, { silent: true } );
     350                        this.$searchInput.val( this.model.get( 's' ) );
     351                        this.$themeFilter.val( this.model.get( 'theme_slug' ) );
     352                        this.$yearFilter.val( this.model.get( 'year' ) );
     353                        this.$preprocessorFilter.val( this.model.get( 'css_preprocessor' ) );
     354                        this.model.trigger( 'change', this.model );
     355                }
     356
     357        } );
     358
     359        // Sets up listener to store the user's selected filters and search so a user's position can be
     360        // restored as well as possible after a theme changes causes a full refresh
     361        wcsc.routers.FilterState = Backbone.Router.extend( {
     362                routes: {
     363                        'wcsc?*filters': 'applyFilters'
     364                },
     365                initialize: function ( options ) {
     366                        this.parent = options.parent;
     367
     368                        //any time the collection is reset, we need to update the displayed route
     369                        this.listenTo(this.parent.view.collection, 'reset', this.updateLocation);
     370                },
     371
     372                // Applies the filters set in the query string to the view
     373                applyFilters: function ( queryString ) {
     374                        var filters = deserializeQueryString( queryString );
     375
     376                        this.parent.view.filterView.setInputs(filters);
     377                },
     378                updateLocation: function () {
     379                        var filters = this.parent.view.collection.searchFilter.toJSON(),
     380                                        activeFilters = _.pick( filters, _.identity ),
     381                                        queryString = $.param( activeFilters );
     382                        this.navigate( 'wcsc?' + queryString );
     383                }
     384        } );
     385
     386        // Customizer Control wrapping the site search applet
     387        api.controlConstructor.wcscSearch = api.Control.extend( {
     388                ready: function () {
     389                        var filter = new wcsc.models.SearchFilter(); //top level model representing the current filter applied to the collection
     390
     391                        this.siteCollection = new wcsc.collections.Sites( { searchFilter: filter } );
     392
     393                        // fill the site collection and setup search when complete
     394                        this.siteCollection.fetch( {
     395                                success: this.setupSearch.bind(this)
     396                        } );
     397                },
     398
     399                // Initialize the site search instance for cloning other sites
     400                setupSearch: function () {
     401                        var control   = this,
     402                                        urlParams = getUrlParams( win.location.href ),
     403                                        currentSite;
     404
     405                        //Set a canonical array of all sites prior to filtering
     406                        wcsc.settings.siteData = this.siteCollection.toJSON();
     407
     408                        // if the wcsc_source_site_id is set, its most likely from a user previewing a site, so bring them back
     409                        if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) {
     410                                api.section( this.section() ).expand();
     411                                currentSite = this.siteCollection.find({site_id: urlParams.wcsc_source_site_id});
     412                                if(currentSite) {
     413                                        this.setActiveSite(currentSite);
     414                                }
     415                        }
     416
     417                        $( '#wcsc-cloner' ).on( 'wcsc:previewSite', '.wcsc-site', function ( event, site ) {
     418                                control.previewSite( site );
     419                        } );
     420
     421                        // setup the top level Site Search View
     422                        this.view = new wcsc.views.SiteSearch( {
     423                                parent: this,
     424                                collection: this.siteCollection
     425                        } );
     426
     427                        this.view.render();
     428
     429                        // Initialize the router to allow state to be restored after a full refresh
     430                        wcsc.router = new wcsc.routers.FilterState( { parent: this } );
     431                        Backbone.history.start();
     432
     433                },
     434
     435                previewSite: function ( site ) {
     436                        var queryString, routerFragment;
     437                        if ( api( 'wcsc_source_site_id' ).get() == site.get( 'site_id' ) ) {
     438                                //we're already previewing this site
    56439                                return;
    57440                        }
    58441
    59                         if ( api.settings.theme.stylesheet === previewUrlParams.theme ) {
    60                                 api( 'wcsc_source_site_id' ).set( previewUrlParams.wcsc_source_site_id );
     442                        if ( api.settings.theme.stylesheet === site.get( 'theme_slug' ) ) {
     443                                this.setActiveSite( site );
    61444                        } else {
    62                                 window.parent.location = previewUrl;
     445
     446                                //we have to do a full refresh when changing themes or other controls won't correlate to the current theme.
     447                                queryString = $.param( {
     448                                        'theme': site.get( 'theme_slug' ),
     449                                        'wcsc_source_site_id': site.get( 'site_id' )
     450                                } );
     451
     452                                routerFragment = Backbone.history.getFragment();
     453                                win.parent.location = wcsc.settings.customizerUrl + '?' + queryString +
     454                                        '#' + routerFragment;
    63455                        }
     456                },
     457
     458                // Set the active site and update the model to reflect the change
     459                setActiveSite: function(site) {
     460                        var site_id = site.get('site_id');
     461                        this.siteCollection.each( function ( _site ) {
     462                                _site.set( { active: false } );
     463                        } );
     464                        site.set( { active: true } );
     465                        api( 'wcsc_source_site_id' ).set( site.get('site_id') );
    64466                }
     467
    65468        } );
    66469
     470
    67471        /**
    68472         * Parse the URL parameters
    69473         *
     
    74478         * @returns {object}
    75479         */
    76480        function getUrlParams( url ) {
    77                 var match, questionMarkIndex, query,
    78                         urlParams = {},
    79                         pl        = /\+/g,  // Regex for replacing addition symbol with a space
    80                         search    = /([^&=]+)=?([^&]*)/g,
    81                         decode    = function ( s ) {
    82                                 return decodeURIComponent( s.replace( pl, " " ) );
    83                         };
     481                var questionMarkIndex, query, hashIndex;
     482
     483                //strip hash first
     484                hashIndex = url.indexOf('#');
     485                if(hashIndex > -1) {
     486                        url = url.substring(0, hashIndex);
     487                }
    84488
    85489                questionMarkIndex = url.indexOf( '?' );
    86490
    87491                if ( -1 === questionMarkIndex ) {
    88                         return urlParams;
     492                        return {};
    89493                } else {
    90494                        query = url.substring( questionMarkIndex + 1 );
    91495                }
    92496
    93                 while ( match = search.exec( query ) ) {
     497                return deserializeQueryString( query );
     498        }
     499
     500        /**
     501         * Deserialize a query string into an object
     502         *
     503         * @param queryString
     504         * @returns {{}}
     505         */
     506        function deserializeQueryString( queryString ) {
     507                var match,
     508                                urlParams = {},
     509                                pl        = /\+/g,  // Regex for replacing addition symbol with a space
     510                                search    = /([^&=]+)=?([^&]*)/g,
     511                                decode    = function ( s ) {
     512                                        return decodeURIComponent( s.replace( pl, " " ) );
     513                                };
     514
     515                while ( match = search.exec( queryString ) ) {
    94516                        urlParams[ decode( match[ 1 ] ) ] = decode( match[ 2 ] );
    95517                }
    96518
    97519                return urlParams;
    98520        }
    99 } )( window.wp, jQuery );
     521})( wp, jQuery, Backbone, window, _wcscSettings );
     522 No newline at end of file
  • wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php

    diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php
    index c398534..2776352 100755
    a b defined( 'WPINC' ) or die(); 
    77/*
    88Plugin Name: WordCamp Site Cloner
    99Description: Allows organizers to clone another WordCamp's theme and custom CSS as a starting point for their site.
    10 Version:     0.1
     10Version:     0.2.0-alpha
    1111Author:      WordCamp.org
    1212Author URI:  http://wordcamp.org
    1313License:     GPLv2 or later
    1414*/
    1515
    16 // todo if Jetpack_Custom_CSS:get_css is callable, register these, otherwise fatal errors
     16/**
     17 * Register the plugin's hooks after `plugins_loaded` so we can first verify that JetPack is available.  If not,
     18 * this plugin can't be used.
     19 */
     20function register_hooks() {
     21        if ( ! class_exists( '\JetPack' ) ) {
     22                return;
     23        }
     24        add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts' );
     25        add_action( 'admin_menu',            __NAMESPACE__ . '\add_submenu_page' );
     26        add_action( 'customize_register',    __NAMESPACE__ . '\register_customizer_components' );
     27        add_action( 'rest_api_init',         __NAMESPACE__ . '\initialize_site_list_API' );
     28
     29  // Setup a daily cron to run on the main central.wordcamp.org site to keep the site list up to date.
     30        if ( is_main_site() ) {
     31                $hook = 'wcsc_prime_sites';
     32                if ( ! wp_next_scheduled( $hook ) ) {
     33                        wp_schedule_event( time(), 'daily', $hook );
     34                }
     35
     36                add_action( $hook, __NAMESPACE__ . '\prime_wordcamp_sites' );
     37        }
     38}
     39
     40add_action( 'plugins_loaded', __NAMESPACE__ . '\register_hooks' );
     41
    1742
    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' );
    2243
    2344/**
    2445 * Register scripts and styles
    function register_scripts() { 
    2849                'wordcamp-site-cloner',
    2950                plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.css',
    3051                array(),
    31                 1
     52                '0.2.0-alpha'
    3253        );
    3354
    3455        wp_register_script(
    3556                'wordcamp-site-cloner',
    3657                plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.js',
    37                 array( 'jquery', 'customize-controls' ),
    38                 1,
     58                array( 'jquery', 'customize-controls', 'wp-backbone' ),
     59                '0.2.0-alpha',
    3960                true
    4061        );
     62
     63        $availableThemes = [ ];
     64        foreach ( wp_get_themes( array( 'allowed' => true ) ) as $theme ) {
     65                $themeName = $theme->display( 'Name' );
     66                $availableThemes[] = array(
     67                        'slug' => $theme->get_stylesheet(),
     68                        'name' => $themeName ?: $theme->get_stylesheet()
     69                );
     70        }
     71
     72        wp_localize_script( 'wordcamp-site-cloner', '_wcscSettings', array(
     73                'apiUrl'        => get_rest_url( null, '/wordcamp-site-cloner/v1/sites/' ),
     74                'customizerUrl' => admin_url( 'customize.php' ),
     75                'themes' => $availableThemes
     76        ) );
    4177}
    4278
    4379/**
    function add_submenu_page() { 
    5288                __( 'Clone Another WordCamp', 'wordcamporg' ),
    5389                __( 'Clone Another WordCamp', 'wordcamporg' ),
    5490                'switch_themes',
    55                 'customize.php?autofocus[panel]=wordcamp_site_cloner'
     91                'customize.php?autofocus[section]=wcsc_sites'
    5692        );
    5793}
    5894
    function add_submenu_page() { 
    6399 */
    64100function register_customizer_components( $wp_customize ) {
    65101        require_once( __DIR__ . '/includes/source-site-id-setting.php' );
    66         require_once( __DIR__ . '/includes/sites-section.php' );
    67102        require_once( __DIR__ . '/includes/site-control.php' );
    68103
    69         $wp_customize->register_control_type( __NAMESPACE__ . '\Site_Control' );
    70 
    71104        $wp_customize->add_setting( new Source_Site_ID_Setting(
    72105                $wp_customize,
    73106                'wcsc_source_site_id',
    74                 array()
     107                array(
     108                        'capability' => 'switch_themes'
     109                )
    75110        ) );
    76111
    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' ),
     112        $wp_customize->add_section( 'wcsc_sites', array(
     113                        'title'      => __( 'Clone Another WordCamp', 'wordcamporg' ),
     114                        'capability' => 'switch_themes'
    83115                )
    84116        );
    85117
    86         $wp_customize->add_section( new Sites_Section(
     118        $wp_customize->add_control( new Site_Control(
    87119                $wp_customize,
    88                 'wcsc_sites',
     120                'wcsc_site_search',
    89121                array(
    90                         'panel' => 'wordcamp_site_cloner',
    91                         'title' => __( 'WordCamp Sites', 'wordcamporg' ),
     122                        'type'     => 'wcscSearch',
     123                        'label'    => __( 'Search', 'wordcamporg' ),
     124                        'settings' => 'wcsc_source_site_id',
     125                        'section'  => 'wcsc_sites'
    92126                )
    93127        ) );
     128}
    94129
    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                         )
     130/**
     131 * Register the api route for the customizer to use to retriever the site list
     132 */
     133function initialize_site_list_API() {
     134        if ( current_user_can( 'switch_themes' ) ) {
     135                register_rest_route( 'wordcamp-site-cloner/v1', '/sites', array(
     136                        'methods'  => 'GET',
     137                        'callback' => __NAMESPACE__ . '\get_wordcamp_sites'
    110138                ) );
    111139        }
    112140}
    function register_customizer_components( $wp_customize ) { 
    114142/**
    115143 * Get required data for relevant WordCamp sites
    116144 *
    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.
    121  *
    122145 * @return array
    123146 */
    124147function get_wordcamp_sites() {
    125         require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' );
     148        $transient_key = 'wcsc_sites';
    126149
    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'] ) ) {
    129                 return array();
     150        if ( false == ( $sites = get_site_transient( $transient_key ) ) ) {
     151                $sites = _get_wordcamp_sites();
     152
     153                //set the transient longer than needed to be sure it doesn't expire before the cron runs again.
     154                set_site_transient( $transient_key, $sites, DAY_IN_SECONDS * 2 );
    130155        }
    131156
    132         $transient_key = 'wcsc_sites';
     157        // remove the current site from the result set
     158        unset( $sites[ get_current_blog_id() ] );
     159
     160        return array_values( $sites );
     161}
     162
     163/**
     164 * Queries and filters the available sites to clone by a single page.  If more than one page of WordCamp posts exists,
     165 * a cron will be scheduled in 10 minutes to update the next page's worth of sites in the cache.
     166 *
     167 * @internal
     168 *
     169 * @param array $args {
     170 *     An array of elements that make up a post to update or insert.
     171 *
     172 *     @type int    $page                  The page of results to select. Default 1.
     173 *     @type int    $posts_per_page        The number of results to return in the initial query. Default 500
     174 *     @type bool   $schedule_cron         Whether to schedule a cron job to prime the next page of results.  Default true
     175 * }
     176 *
     177 * @return array WordCamp Sites array.
     178 */
     179function _get_wordcamp_sites($args = array()) {
    133180
    134         if ( $sites = get_site_transient( $transient_key ) ) {
    135                 return $sites;
     181        $options = wp_parse_args( $args, array(
     182                'page'           => 1,
     183                'posts_per_page' => 500,
     184                'schedule_cron'  => true
     185        ) );
     186
     187        $page = absint($options['page']);
     188
     189        //verify that the required plugins and JetPack modules are available
     190        require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' );
     191        if ( ! \Jetpack::is_module_active( 'custom-css' ) ) {
     192                \Jetpack::activate_module( 'custom-css', false, false );
    136193        }
    137194
    138         switch_to_blog( BLOG_ID_CURRENT_SITE ); // central.wordcamp.org
     195        switch_to_blog( get_current_site()->blog_id );
    139196
    140197        $sites = array();
    141         $wordcamps = get_posts( array(
     198        $wordcamp_query = new \WP_Query( array(
    142199                'post_type'      => 'wordcamp',
    143200                '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
     201                'posts_per_page' => $options[ 'posts_per_page' ],
    145202                'meta_key'       => 'Start Date (YYYY-mm-dd)',
    146203                'orderby'        => 'meta_value_num',
    147 
    148                 'meta_query' => array(
     204                'order'          => 'DESC',
     205                'paged'          => $page,
     206                'meta_query'     => array(
    149207                        array(
    150208                                'key'     => 'Start Date (YYYY-mm-dd)',
    151209                                'value'   => strtotime( 'now - 1 month' ),
    152210                                'compare' => '<'
    153                         ),
     211                        )
    154212                ),
    155213        ) );
    156214
    157         foreach( $wordcamps as $wordcamp ) {
    158                 $site_id  = get_wordcamp_site_id( $wordcamp );
     215        //If there are more pages to process, reschedule the cron to run again with the next page
     216        if ( $options[ 'schedule_cron' ] && $wordcamp_query->max_num_pages > $page && ! wp_next_scheduled( 'wcsc_prime_sites', [ $page + 1 ] ) ) {
     217                wp_schedule_single_event( time() + 10 * MINUTE_IN_SECONDS, 'wcsc_prime_sites', [ $page + 1 ] );
     218        }
     219
     220        $wordcamps = $wordcamp_query->get_posts();
     221
     222        $coming_soon_settings_handler = false;
     223        if(class_exists('\WCCSP_Settings')) {
     224                $coming_soon_settings_handler = isset( $GLOBALS[ 'WCCSP_Settings' ] ) ? $GLOBALS[ 'WCCSP_Settings' ] : new \WCCSP_Settings();
     225        }
     226
     227        foreach ( $wordcamps as $wordcamp ) {
     228                $site_id = get_wordcamp_site_id( $wordcamp );
    159229                $site_url = get_post_meta( $wordcamp->ID, 'URL', true );
     230                $start_date = get_post_meta( $wordcamp->ID, 'Start Date (YYYY-mm-dd)', true );
    160231
    161                 if ( ! $site_id || ! $site_url ) {
     232                if ( ! $site_id || ! $site_url || ! $start_date ) {
    162233                        continue;
    163234                }
    164235
    165236                switch_to_blog( $site_id );
    166237
    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 ),
     238                //skip any sites with the coming soon enabled.
     239                if ( $coming_soon_settings_handler ) {
     240                        $coming_soon_settings = $coming_soon_settings_handler->get_settings();
     241                        if ( isset( $coming_soon_settings[ 'enabled' ] ) && $coming_soon_settings[ 'enabled' ] === 'on' ) {
     242                                restore_current_blog();
     243                                continue;
     244                        }
     245                }
     246
     247                $preprocessor = \Jetpack_Custom_CSS::get_preprocessor();
     248
     249                $sites[ $site_id ] = array(
     250                        'site_id'          => $site_id,
     251                        'name'             => get_wordcamp_name(),
     252                        'theme_slug'       => get_stylesheet(),
     253                        'screenshot_url'   => get_screenshot_url( $site_url ),
     254                        'year'             => date( 'Y', $start_date ),
     255                        'css_preprocessor' => is_array( $preprocessor ) && isset( $preprocessor[ 'name' ] ) ? $preprocessor[ 'name' ] : 'none'
    172256                );
    173257
    174258                restore_current_blog();
    function get_wordcamp_sites() { 
    176260
    177261        restore_current_blog();
    178262
    179         set_site_transient( $transient_key, $sites, DAY_IN_SECONDS );
    180 
    181263        return $sites;
    182264}
    183265
    184266/**
     267 * Daily cron job callback to keep the site list transient valid.  This is only run on the main site.
     268 *
     269 * Since it can be expensive to loop through 100's of sites with switch_to_blog, we're breaking the cron up
     270 * into smaller groups and updating a set at a time.  This means that users getting the initial load may not
     271 * see every site, but since they are ordered by date descending, most applicable sites should be available
     272 * immediately if the transient gets flushed.
     273 *
     274 * The current logic assumes that no sites that have previously made the list of sites will end up getting
     275 * removed.  If this changes, a separate cron will likely need to be added to prune sites from the list.
     276 *
     277 */
     278function prime_wordcamp_sites($page = 1) {
     279        $transient_key = 'wcsc_sites';
     280
     281        $sites = _get_wordcamp_sites( compact( 'page' ) );
     282
     283        $stale_sites = get_site_transient( $transient_key );
     284
     285        if ( ! empty( $stale_sites ) ) {
     286                //merge the fresh sites with the previously stored sites, overriding old copies with the new
     287                $sites = array_merge( $stale_sites, $sites );
     288
     289                //sort the sites in descending order by year.  More exact dates can be added if the rough order causes confusion.
     290                uasort( $sites, function( $site_a, $site_b ) {
     291                        if ( $site_a[ 'year' ] === $site_b[ 'year' ] ) {
     292                                return 0;
     293                        }
     294                        return ( $site_a[ 'year' ] < $site_b[ 'year' ] ? 1 : -1 );
     295                } );
     296        }
     297
     298        //set the transient longer than needed to be sure it doesn't expire before the cron runs again.
     299        set_site_transient( 'wcsc_sites', $sites, DAY_IN_SECONDS * 2 );
     300}
     301
     302/**
    185303 * Get the mShot URL for the given site URL
    186304 *
    187305 * Allow it to be filtered so that production URLs can be changed to match development URLs in local environments.
    function get_screenshot_url( $site_url ) { 
    194312        $screenshot_url = add_query_arg( 'w', 275, 'https://www.wordpress.com/mshots/v1/' . rawurlencode( $site_url ) );
    195313
    196314        return apply_filters( 'wcsc_site_screenshot_url', $screenshot_url );
    197 }
     315}
     316 No newline at end of file