<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[4280] sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner: WordCamp Site Cloner: Convert to REST/Backbone to improve scalability</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/4280">4280</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/4280","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>iandunn</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2016-10-21 16:10:02 +0000 (Fri, 21 Oct 2016)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>WordCamp Site Cloner: Convert to REST/Backbone to improve scalability

See <a href="http://meta.trac.wordpress.org/ticket/1112">#1112</a>
Props prettyboymp</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerincludessitecontrolphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerincludessourcesiteidsettingphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonertemplatessitecontrolphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerwordcampsiteclonercss">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerwordcampsiteclonerjs">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerwordcampsiteclonerphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonertemplatessitefiltersphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonertemplatessiteoptionphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php</a></li>
</ul>

<h3>Removed Paths</h3>
<ul>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerincludessitessectionphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonertemplatessitessectionphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerincludessitecontrolphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php    2016-10-21 16:09:56 UTC (rev 4279)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php      2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,21 +1,26 @@
</span><span class="cx" style="display: block; padding: 0 10px"> <?php
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * Custom Customizer Control for Search WordCamp sites to clone
+ */
+
</ins><span class="cx" style="display: block; padding: 0 10px"> namespace WordCamp\Site_Cloner;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px"> defined( 'WPINC' ) or die();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/**
- * Custom Customizer Control for a WordCamp site
- */
</del><span class="cx" style="display: block; padding: 0 10px"> class Site_Control extends \WP_Customize_Control {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public $site_id, $site_name, $screenshot_url, $theme_slug;
-       public $settings = 'wcsc_source_site_id';
-       public $section  = 'wcsc_sites';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function __construct( $manager, $id, $args = array() ) {
+               parent::__construct( $manager, $id, $args );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->capability = 'edit_theme_options';
+               $this->section    = 'wcsc_sites';
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Enqueue scripts and styles
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function enqueue() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_view_templates' ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 wp_enqueue_style(  'wordcamp-site-cloner' );
</span><span class="cx" style="display: block; padding: 0 10px">                wp_enqueue_script( 'wordcamp-site-cloner' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -24,14 +29,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Render the control's content
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function render_content() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $preview_url = add_query_arg(
-                       array(
-                               'theme'               => rawurlencode( $this->theme_slug ),
-                               'wcsc_source_site_id' => rawurlencode( $this->site_id ),
-                       ),
-                       admin_url( 'customize.php' )
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         require_once( dirname( __DIR__ ) . '/templates/site-control.php' );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                require( dirname( __DIR__ ) . '/templates/site-control.php' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+        * Render the control's Underscores templates
+        */
+       public function print_view_templates() {
+               require_once( dirname( __DIR__ ) . '/templates/site-option.php'  );
+               require_once( dirname( __DIR__ ) . '/templates/site-filters.php' );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerincludessitessectionphp"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php   2016-10-21 16:09:56 UTC (rev 4279)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php     2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,19 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-<?php
-
-namespace WordCamp\Site_Cloner;
-
-defined( 'WPINC' ) or die();
-
-/**
- * Custom Customizer Section for WordCamp sites
- */
-class Sites_Section extends \WP_Customize_Section {
-       public $type = 'wcsc-sites';
-
-       /**
-        * Render the section's content
-        */
-       protected function render() {
-               require_once( dirname( __DIR__ ) . '/templates/sites-section.php' );
-       }
-}
</del></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerincludessourcesiteidsettingphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php  2016-10-21 16:09:56 UTC (rev 4279)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php    2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,7 +1,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> <?php
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> namespace WordCamp\Site_Cloner;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px"> defined( 'WPINC' ) or die();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -15,6 +14,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public $type              = 'wcsc-source-site-id';
</span><span class="cx" style="display: block; padding: 0 10px">        public $default           = 0;
</span><span class="cx" style="display: block; padding: 0 10px">        public $sanitize_callback = 'absint';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">         protected $preview_source_site_id;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -28,6 +28,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                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
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'get_post_metadata',       array( $this, 'preview_jetpack_postmeta' ), 10, 4 );
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'safecss_skip_stylesheet', array( $this, 'preview_skip_stylesheet'  ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               // Disable the current site's Custom CSS from being output
+               remove_action( 'wp_head', array( 'Jetpack_Custom_CSS', 'link_tag' ), 101 );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -109,6 +112,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * to the URL.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param int $source_site_id
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *
+        * @return null
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        protected function update( $source_site_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                switch_to_blog( $source_site_id );
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonertemplatessitecontrolphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php   2016-10-21 16:09:56 UTC (rev 4279)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php     2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,15 +1,23 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-<?php defined( 'WPINC' ) or die(); ?>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-<div id="wcsc-site-<?php echo esc_attr( $this->site_id ); ?>" class="wcscSite" data-preview-url="<?php echo esc_url( $preview_url ); ?>">
-       <div class="wcsc-site-screenshot">
-               <img src="<?php echo esc_url( $this->screenshot_url ); ?>" alt="<?php echo esc_attr( $this->site_name ); ?>" />
-       </div>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * Top level template for the output of the Site Cloner Customizer Control
+ */
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        <h3 class="wcsc-site-name">
-               <?php echo esc_html( $this->site_name ); ?>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+namespace WordCamp\Site_Cloner;
+defined( 'WPINC' ) or die();
+
+?>
+
+<div id="wcsc-cloner">
+       <h3>
+               <?php esc_html_e( 'WordCamp Sites', 'wordcamporg' ); ?>
+               <span id="wcsc-sites-count" class="title-count wcsc-sites-count"></span>
</ins><span class="cx" style="display: block; padding: 0 10px">         </h3>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        <span id="live-preview-label-<?php echo esc_attr( $this->site_id ); ?>" class="wcsc-live-preview-label">
-               <?php _e( 'Live Preview', 'wordcamporg' ); ?>
-       </span>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ <div class="filters"></div>
+
+       <div class="wcsc-search">
+               <ul id="wcsc-results"></ul>
+       </div>
</ins><span class="cx" style="display: block; padding: 0 10px"> </div>
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonertemplatessitefiltersphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php                           (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php     2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,78 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Template for the site filters
+ */
+
+namespace WordCamp\Site_Cloner;
+defined( 'WPINC' ) or die();
+
+?>
+
+<script id="tmpl-wcsc-site-filters" type="text/html">
+       <div class="wcsc-filter">
+               <label for="wcsc-filter-search-input">
+                       <span class="customize-control-title">
+                               <?php esc_html_e( 'Search', 'wordcamporg' ); ?>
+                       </span>
+
+                       <div class="customize-control-content">
+                               <input type="search" id="wcsc-filter-search-input" class="wcsc-filter-search" />
+                       </div>
+               </label>
+       </div>
+
+       <div class="wcsc-filter">
+               <label for="wcsc-filter-theme_slug">
+                       <span class="customize-control-title">
+                               <?php esc_html_e( 'Theme', 'wordcamporg' ); ?>
+                       </span>
+
+                       <div class="customize-control-content">
+                               <select id="wcsc-filter-theme_slug" data-filter="theme_slug">
+                                       <option value="">Any</option>
+
+                                       <# _.each( data.themeOptions, function( themeOption ) { #>
+                                               <option value="{{themeOption.slug}}">{{themeOption.name}}</option>
+                                       <# }); #>
+                               </select>
+                       </div>
+               </label>
+       </div>
+
+       <div class="wcsc-filter">
+               <label for="wcsc-filter-year">
+                       <span class="customize-control-title">
+                               <?php esc_html_e( 'WordCamp Year', 'wordcamporg' ); ?>
+                       </span>
+
+                       <div class="customize-control-content">
+                               <select id="wcsc-filter-year" data-filter="year">
+                                       <option value="">Any</option>
+
+                                       <# _.each( data.yearOptions, function( yearOption ) { #>
+                                               <option value="{{yearOption}}">{{yearOption}}</option>
+                                       <# }); #>
+                               </select>
+                       </div>
+               </label>
+       </div>
+
+       <div class="wcsc-filter">
+               <label for="wcsc-filter-css_preprocessor">
+                       <span class="customize-control-title">
+                               <?php esc_html_e( 'CSS Preprocessor', 'wordcamporg' ); ?>
+                       </span>
+
+                       <div class="customize-control-content">
+                               <select id="wcsc-filter-css_preprocessor" data-filter="css_preprocessor">
+                                       <option value="">Any</option>
+
+                                       <# _.each( data.preprocessorOptions, function( preprocessorOption ) { #>
+                                               <option value="{{preprocessorOption}}">{{preprocessorOption}}</option>
+                                       <# }); #>
+                               </select>
+                       </div>
+               </label>
+       </div>
+</script>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonertemplatessiteoptionphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php                            (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php      2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,34 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Template to display a single Site within the Site Cloner Control
+ */
+
+namespace WordCamp\Site_Cloner;
+defined( 'WPINC' ) or die();
+
+?>
+
+<script id="tmpl-wcsc-site-option" type="text/html">
+       <div class="wcsc-site-screenshot">
+               <img src="{{ data.screenshot_url }}" alt="{{ data.name }}"/>
+       </div>
+
+       <h3 class="wcsc-site-name">
+               {{ data.name }}
+       </h3>
+
+       <# if ( data.active ) { #>
+
+               <span id="live-previewing-{{ data.site_id }}" class="wcsc-previewing-label">
+                       <?php _e( 'Viewing', 'wordcamporg' ); ?>
+               </span>
+
+       <# } else { #>
+
+               <span id="live-preview-label-{{ data.site_id }}" class="wcsc-live-preview-label">
+                       <?php _e( 'Live Preview', 'wordcamporg' ); ?>
+               </span>
+
+       <# } #>
+</script>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonertemplatessitessectionphp"></a>
<div class="delfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Deleted: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php  2016-10-21 16:09:56 UTC (rev 4279)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php    2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,15 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-<?php defined( 'WPINC' ) or die(); ?>
-
-<li id="section-<?php echo esc_attr( $this->id ); ?>" class="accordion-section control-section control-section-<?php echo esc_attr( $this->type ); ?>">
-       <h3>
-               <?php esc_html_e( 'WordCamp Sites' ); ?>
-
-               <span class="title-count wcsc-sites-count">
-                       <?php echo count( $this->controls ); ?>
-               </span>
-       </h3>
-
-       <div class="wcsc-sites-section-content">
-               <ul id="wcsc-sites"></ul>
-       </div>
-</li>
</del></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerwordcampsiteclonercss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css     2016-10-21 16:09:56 UTC (rev 4279)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css       2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,27 +1,26 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-.control-section-wcsc-sites {
-       padding: 0 8px;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+.wcsc-filter {
+       margin-bottom: 10px;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        #wcsc-sites {
-               overflow: auto;
-       }
-
-               .wcscSite {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         .wcsc-site {
</ins><span class="cx" style="display: block; padding: 0 10px">                         position: relative;
</span><span class="cx" style="display: block; padding: 0 10px">                        cursor: pointer;
</span><span class="cx" style="display: block; padding: 0 10px">                        border: 1px solid #DEDEDE;
</span><span class="cx" style="display: block; padding: 0 10px">                        box-shadow: 0 1px 1px -1px rgba( 0, 0, 0, 0.1 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        margin-top: 5px;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        .wcsc-site-screenshot {
</span><span class="cx" style="display: block; padding: 0 10px">                                transition: opacity 0.2s ease-in-out 0s;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                .wcscSite:hover .wcsc-site-screenshot {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         .wcsc-site:hover .wcsc-site-screenshot {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         opacity: 0.4;
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        .wcsc-live-preview-label {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 .wcsc-live-preview-label,
+                       .wcsc-previewing-label {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 opacity: 0;
</span><span class="cx" style="display: block; padding: 0 10px">                                position: absolute;
</span><span class="cx" style="display: block; padding: 0 10px">                                top: 35%;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -38,6 +37,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                transition: opacity 0.1s ease-in-out 0s;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                .wcscSite:hover .wcsc-live-preview-label {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         .wcsc-site:hover .wcsc-live-preview-label,
+                               .wcsc-site .wcsc-previewing-label {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         opacity: 1;
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerwordcampsiteclonerjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js      2016-10-21 16:09:56 UTC (rev 4279)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js        2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,66 +1,478 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-( function( wp, $ ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+( function( wp, $, Backbone, win, settings ) {
</ins><span class="cx" style="display: block; padding: 0 10px">         'use strict';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( ! wp || ! wp.customize ) {
</span><span class="cx" style="display: block; padding: 0 10px">                return;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        var api = wp.customize;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ wp.customize.WordCamp = wp.customize.WordCamp || {};
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        /**
-        * The Clone Another WordCamp panel
-        */
-       api.panelConstructor.wcscPanel = api.Panel.extend( {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ var api  = wp.customize,
+           wcsc = api.WordCamp.SiteCloner = {
+                       models      : {},
+                       views       : {},
+                       collections : {},
+                       routers     : {},
+                       settings    : {}
+               };
+
+       wcsc.settings = settings || {};
+
+       // Model for a single site
+       wcsc.models.Site = Backbone.Model.extend( {
+               idAttribute : 'site_id',
+
+               defaults : {
+                       'active' : false
+               }
+       } );
+
+       // Model representing the filter state for searching/filtering sites
+       wcsc.models.SearchFilter = Backbone.Model.extend( {
+               's'                : '',
+               'theme_slug'       : '',
+               'year'             : '',
+               'css_preprocessor' : ''
+       } );
+
+       // Top level view for the Site Cloner Control
+       wcsc.views.SiteSearch = Backbone.View.extend( {
+               el : '#wcsc-cloner .wcsc-search',
+
+               // Index of the currently viewed page of results
+               page : 0,
+
+               initialize : function( options ) {
+                       // Update scroller position
+                       _.bindAll( this, 'scroller' );
+
+                       // Container that will be scrolled within
+                       this.$container = $( '#wcsc-cloner' ).parents( 'ul.accordion-section-content' );
+                       // Bind scrolling within the container to check for infinite scroll
+                       this.$container.bind( 'scroll', _.throttle( this.scroller, 300 ) );
+
+                       // The model and view for filtering the site results
+                       this.filterView = new wcsc.views.SearchFilters( {
+                               model  : this.collection.searchFilter,
+                               parent : this
+                       } );
+
+                       // View for listing the matching sites
+                       this.resultsView = new wcsc.views.SearchResults( {
+                               collection : this.collection,
+                               parent     : this
+                       } );
+               },
+
+               render : function() {
+                       this.filterView.render();
+                       this.resultsView.render();
+
+                       this.$el.empty().append( this.resultsView.el );
+               },
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                 * Initialize the panel after it's loaded
-                *
-                * Ideally, the Previewer would be set to the requested site ID during the initial PHP request, rather than
-                * loading the host site in the Previewer, and then refreshing it to use the requested site. That became a
-                * rabbit hole, though, so it's done this way instead.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * Checks if a user has reached the bottom of the list and triggers a scroll event to show more sites if
+                * needed.
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                ready : function() {
-                       var urlParams = getUrlParams( window.location.href );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         scroller : function() {
+                       var visibleBottom, threshold, elementHeight, containerHeight, scrollTop;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) {
-                               this.expand();
-                               api( 'wcsc_source_site_id' ).set( urlParams.wcsc_source_site_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 scrollTop       = this.$container.scrollTop();
+                       containerHeight = this.$container.innerHeight();
+                       elementHeight   = this.$container.get( 0 ).scrollHeight;
+
+                       visibleBottom = scrollTop + containerHeight;
+                       threshold     = Math.round( elementHeight * 0.9 );
+
+                       if ( visibleBottom > threshold ) {
+                               this.trigger( 'wcsc:scroll' );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Collection representing the list of cloneable sites
+       wcsc.collections.Sites = Backbone.Collection.extend( {
+               model : wcsc.models.Site,
+               url   : wcsc.settings.apiUrl,
+
+               initialize : function( options ) {
+                       this.searchFilter = options.searchFilter || {};
+
+                       this.listenTo( this.searchFilter, 'change', this.applyFilter );
+               },
+
+               // Filter this collection by the updated searchFilter attributes
+               applyFilter : function() {
+                       var filters       = this.searchFilter.toJSON(),
+                           activeFilters = _.pick( filters, _.identity ),
+                           term          = '',
+                           sites;
+
+                       // Nothing actually changed, so don't update the collection
+                       if ( _.isEmpty( this.searchFilter.changedAttributes() ) ) {
+                               return;
+                       }
+
+                       // No active filters. Reset to the full list and bail
+                       if ( _.isEmpty( activeFilters ) ) {
+                               this.resetCanonical();
+                               return;
+                       }
+
+                       this.resetCanonical( { silent: true } );
+
+                       // Remove the search query restriction since we already filtered by word matches above
+                       if ( activeFilters.s ) {
+                               term = activeFilters.s;
+
+                               delete activeFilters.s;
+                       }
+
+                       sites = this.where( activeFilters );
+
+                       if ( term ) {
+                               sites = this.filterBySearch( sites, term );
+                       }
+
+                       this.reset( sites );
+               },
+
+               // Internal method for filtering sites by search terms
+               filterBySearch : function( sites, term ) {
+                       var match, name;
+
+                       // Escape the term string for RegExp meta characters
+                       term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
+
+                       // Consider spaces as word delimiters and match the whole string
+                       // so matching terms can be combined
+                       term  = term.replace( / /g, ')(?=.*' );
+                       match = new RegExp( '^(?=.*' + term + ').+', 'i' );
+
+                       return _.filter( sites, function( site ) {
+                               name = site.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
+
+                               return match.test( name );
+                       } );
+               },
+
+               paginate : function( pageIndex ) {
+                       var collection = this;
+
+                       pageIndex  = pageIndex || 0;
+
+                       collection = _( collection.rest( 20 * pageIndex ) );
+                       collection = _( collection.first( 20 ) );
+
+                       return collection;
+               },
+
+               // Resets the site collection dataset to the canonical list originally pulled from the api
+               resetCanonical : function( options ) {
+                       var activeSite,
+                           activeSiteId = api( 'wcsc_source_site_id' ).get();
+
+                       options = options || {};
+
+                       this.reset( wcsc.settings.siteData, options );
+
+                       // Restore the currently active site
+                       if ( activeSiteId ) {
+                               activeSite = this.find( { site_id : activeSiteId } );
+
+                               if ( 'undefined' !== typeof activeSite ) {
+                                       activeSite.set( { active: true } );
+                               }
+                       }
+               }
+       } );
+
+       // View for a single site
+       wcsc.views.Site = Backbone.View.extend( {
+               className : 'wcsc-site',
+               html      : wp.template( 'wcsc-site-option' ),
+               touchDrag : false,
+
+               attributes : function() {
+                       return {
+                               'id'           : 'wcsc-site-' + this.model.get( 'site_id' ),
+                               'data-site-id' : this.model.get( 'site_id' )
+                       }
+               },
+
+               events : {
+                       'click'     : 'preview',
+                       'keydown'   : 'preview',
+                       'touchend'  : 'preview',
+                       'touchmove' : 'preventPreview'
+               },
+
+               initialize : function( options ) {
+                       this.parent = options.parent;
+
+                       this.listenTo( this.model, 'change', this.render );
+                       this.render();
+               },
+
+               render : function() {
+                       this.$el.html( this.html( this.model.toJSON() ) );
+               },
+
+               preventPreview : function() {
+                       this.touchDrag = true;
+               },
+
+               preview : function( event ) {
+                       event = event || window.event;
+
+                       // Ignore touches caused by scrolling
+                       if ( this.touchDrag === true ) {
+                               this.touchDrag = false;
+                       }
+
+                       event.preventDefault();
+
+                       this.$el.trigger( 'wcsc:previewSite', this.model );
+               }
+       } );
+
+       // View for the site results list
+       wcsc.views.SearchResults = Backbone.View.extend( {
+               className: 'wcsc-results',
+
+               initialize : function( options ) {
+                       var self = this;
+
+                       this.parent     = options.parent;
+                       this.$siteCount = $( '#wcsc-sites-count' );
+
+                       // Re-render the view whenever a collection change is complete
+                       this.listenTo( this.collection, 'reset', function() {
+                               self.parent.page = 0;
+                               self.render( this );
+                       } );
+
+                       this.listenTo( this.parent, 'wcsc:scroll', function() {
+                               self.renderSites( self.parent.page );
+                       } );
+               },
+
+               render : function() {
+                       this.$el.empty();
+                       this.renderSites( this.parent.page );
+                       this.$siteCount.text( this.collection.length );
+               },
+
+               renderSites : function( page ) {
+                       var self = this;
+
+                       // Get a collection of just the requested page
+                       this.instance = this.collection.paginate( page );
+
+                       if ( this.instance.size() === 0 ) {
+                               this.parent.trigger( 'wcsc:end' );
+                               return;
+                       }
+
+                       this.instance.each( function( site ) {
+                               var siteView = new wcsc.views.Site( {
+                                       model  : site,
+                                       parent : self
+                               } );
+
+                               siteView.render();
+
+                               self.$el.append( siteView.el );
+                       } );
+
+                       this.parent.page++;
+               }
+
+       } );
+
+       // View for the search and dropdown filters
+       wcsc.views.SearchFilters = Backbone.View.extend( {
+               el        : '#wcsc-cloner .filters',
+               className : 'wscs-filters',
+               html      : wp.template( 'wcsc-site-filters' ),
+
+               events : {
+                       "input #wcsc-filter-search-input" : "search",
+                       "keyup #wcsc-filter-search-input" : "search",
+                       "change select"                   : "applyFilter"
+               },
+
+               initialize : function( options ) {
+                       this.parent = options.parent;
+               },
+
+               render : function() {
+                       var data = {};
+
+                       data.themeOptions        = wcsc.settings.themes;
+                       data.yearOptions         = _.uniq( this.parent.collection.pluck( 'year' ) ).sort();
+                       data.preprocessorOptions = _.uniq( this.parent.collection.pluck( 'css_preprocessor' ) ).sort();
+
+                       this.$el.html( this.html( data ) );
+
+                       this.$searchInput        = $( '#wcsc-filter-search-input' );
+                       this.$themeFilter        = $( '#wcsc-filter-theme_slug' );
+                       this.$yearFilter         = $( '#wcsc-filter-year' );
+                       this.$preprocessorFilter = $( '#wcsc-filter-css_preprocessor' );
+               },
+
+               search : function( event ) {
+                       // Clear on escape.
+                       if ( event.type === 'keyup' && event.which === 27 ) {
+                               event.target.value = '';
+                       }
+
+                       /**
+                        * Since doSearch is debounced, it will only run when user input comes to a rest
+                        */
+                       this.doSearch( event );
+               },
+
+               doSearch : _.debounce( function( event ) {
+                       this.model.set( 's', event.target.value );
+               }, 500 ),
+
+               applyFilter : function( event ) {
+                       var $target = $( event.target ),
+                           value   = $target.val(),
+                           filter  = $target.data( 'filter' );
+
+                       this.model.set( filter, value );
+               },
+
+               // Set the inputs to the set of filters as triggered by the router on initial load
+               setInputs : function( filters ) {
+                       this.model.set( filters, { silent : true } );
+
+                       this.$searchInput.val(        this.model.get( 's'                ) );
+                       this.$themeFilter.val(        this.model.get( 'theme_slug'       ) );
+                       this.$yearFilter.val(         this.model.get( 'year'             ) );
+                       this.$preprocessorFilter.val( this.model.get( 'css_preprocessor' ) );
+
+                       this.model.trigger( 'change', this.model );
+               }
+       } );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Custom control representing a site that can be previewed/imported
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Sets up a listener to store the user's selected filters and search, so that a user's position can be
+        * restored as well as possible after a theme changes causes a full refresh.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        api.controlConstructor.wcscSite = api.Control.extend( {
-               /**
-                * Initialize the control after it's loaded
-                */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ wcsc.routers.FilterState = Backbone.Router.extend( {
+               routes : {
+                       'wcsc?*filters' : 'applyFilters'
+               },
+
+               initialize : function( options ) {
+                       this.parent = options.parent;
+
+                       // Any time the collection is reset, we need to update the displayed route
+                       this.listenTo( this.parent.view.collection, 'reset', this.updateLocation );
+               },
+
+               // Applies the filters set in the query string to the view
+               applyFilters : function( queryString ) {
+                       var filters = deserializeQueryString( queryString );
+
+                       this.parent.view.filterView.setInputs( filters );
+               },
+
+               updateLocation : function() {
+                       var filters       = this.parent.view.collection.searchFilter.toJSON(),
+                           activeFilters = _.pick( filters, _.identity ),
+                           queryString   = $.param( activeFilters );
+
+                       this.navigate( 'wcsc?' + queryString );
+               }
+       } );
+
+       // Customizer Control wrapping the site search applet
+       api.controlConstructor.wcscSearch = api.Control.extend( {
</ins><span class="cx" style="display: block; padding: 0 10px">                 ready : function() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        this.container.on( 'click', '.wcscSite', this.previewSite );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var filter = new wcsc.models.SearchFilter(); // Top level model representing the current filter applied to the collection
+
+                       this.siteCollection = new wcsc.collections.Sites( { searchFilter : filter } );
+
+                       // Fill the site collection and setup search when complete
+                       this.siteCollection.fetch( {
+                               success : this.setupSearch.bind( this )
+                       } );
</ins><span class="cx" style="display: block; padding: 0 10px">                 },
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                /**
-                * Preview the selected site
-                *
-                * If the site is using a different theme, then reload the entire Customizer with the theme URL parameter
-                * set, so that the Theme Switcher will handle previewing the new theme for us. Otherwise just set the ID
-                * to refresh the Previewer with the current theme and the new site's CSS, etc.
-                *
-                * @param {object} event
-                */
-               previewSite : function( event ) {
-                       var previewUrl       = $( this ).data( 'preview-url' ),
-                               previewUrlParams = getUrlParams( previewUrl );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Initialize the site search instance for cloning other sites
+               setupSearch : function() {
+                       var currentSite,
+                           control   = this,
+                           urlParams = getUrlParams( win.location.href );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( api( 'wcsc_source_site_id' ).get() == previewUrlParams.wcsc_source_site_id ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Set a canonical array of all sites prior to filtering
+                       wcsc.settings.siteData = this.siteCollection.toJSON();
+
+                       // If the wcsc_source_site_id is set, it;s most likely from a user previewing a site, so bring them back
+                       if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) {
+                               api.section( this.section() ).expand();
+
+                               currentSite = this.siteCollection.find( { site_id : urlParams.wcsc_source_site_id } );
+
+                               if ( currentSite ) {
+                                       this.setActiveSite( currentSite );
+                               }
+                       }
+
+                       $( '#wcsc-cloner' ).on( 'wcsc:previewSite', '.wcsc-site', function( event, site ) {
+                               control.previewSite( site );
+                       } );
+
+                       // Setup the top level Site Search View
+                       this.view = new wcsc.views.SiteSearch( {
+                               parent     : this,
+                               collection : this.siteCollection
+                       } );
+
+                       this.view.render();
+
+                       // Initialize the router to allow state to be restored after a full refresh
+                       wcsc.router = new wcsc.routers.FilterState( { parent : this } );
+                       Backbone.history.start();
+               },
+
+               previewSite : function( site ) {
+                       var queryString, routerFragment;
+
+                       if ( api( 'wcsc_source_site_id' ).get() == site.get( 'site_id' ) ) {
+                               // We're already previewing this site
</ins><span class="cx" style="display: block; padding: 0 10px">                                 return;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( api.settings.theme.stylesheet === previewUrlParams.theme ) {
-                               api( 'wcsc_source_site_id' ).set( previewUrlParams.wcsc_source_site_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( api.settings.theme.stylesheet === site.get( 'theme_slug' ) ) {
+                               this.setActiveSite( site );
</ins><span class="cx" style="display: block; padding: 0 10px">                         } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                window.parent.location = previewUrl;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         // We have to do a full refresh when changing themes or other controls won't correlate to the current theme.
+                               queryString = $.param( {
+                                       'theme'               : site.get( 'theme_slug' ),
+                                       'wcsc_source_site_id' : site.get( 'site_id' )
+                               } );
+
+                               routerFragment      = Backbone.history.getFragment();
+                               win.parent.location = wcsc.settings.customizerUrl + '?' + queryString + '#' + routerFragment;
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                },
+
+               // Set the active site and update the model to reflect the change
+               setActiveSite : function( site ) {
+                       var site_id = site.get( 'site_id' );
+
+                       this.siteCollection.each( function( _site ) {
+                               _site.set( { active : false } );
+                       } );
+
+                       site.set( { active : true } );
+                       api( 'wcsc_source_site_id' ).set( site.get( 'site_id' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">        } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -74,26 +486,45 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @returns {object}
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function getUrlParams( url ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                var match, questionMarkIndex, query,
-                       urlParams = {},
-                       pl        = /\+/g,  // Regex for replacing addition symbol with a space
-                       search    = /([^&=]+)=?([^&]*)/g,
-                       decode    = function ( s ) {
-                               return decodeURIComponent( s.replace( pl, " " ) );
-                       };
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         var questionMarkIndex, query, hashIndex;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Strip hash first
+               hashIndex = url.indexOf( '#' );
+
+               if ( hashIndex > -1 ) {
+                       url = url.substring( 0, hashIndex );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 questionMarkIndex = url.indexOf( '?' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( -1 === questionMarkIndex ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        return urlParams;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 return {};
</ins><span class="cx" style="display: block; padding: 0 10px">                 } else {
</span><span class="cx" style="display: block; padding: 0 10px">                        query = url.substring( questionMarkIndex + 1 );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                while ( match = search.exec( query ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return deserializeQueryString( query );
+       }
+
+       /**
+        * Deserialize a query string into an object
+        *
+        * @param queryString
+        * @returns {object}
+        */
+       function deserializeQueryString( queryString ) {
+               var match,
+                   urlParams = {},
+                   pl        = /\+/g,  // Regex for replacing addition symbol with a space
+                   search    = /([^&=]+)=?([^&]*)/g,
+                   decode    = function( s ) {
+                           return decodeURIComponent( s.replace( pl, " " ) );
+                   };
+
+               while ( match = search.exec( queryString ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         urlParams[ decode( match[ 1 ] ) ] = decode( match[ 2 ] );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return urlParams;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-} )( window.wp, jQuery );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+})( wp, jQuery, Backbone, window, _wcscSettings );
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampsiteclonerwordcampsiteclonerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php     2016-10-21 16:09:56 UTC (rev 4279)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php       2016-10-21 16:10:02 UTC (rev 4280)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,26 +1,42 @@
</span><span class="cx" style="display: block; padding: 0 10px"> <?php
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-namespace WordCamp\Site_Cloner;
-
-defined( 'WPINC' ) or die();
-
</del><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px"> Plugin Name: WordCamp Site Cloner
</span><span class="cx" style="display: block; padding: 0 10px"> Description: Allows organizers to clone another WordCamp's theme and custom CSS as a starting point for their site.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-Version:     0.1
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+Version:     0.2
</ins><span class="cx" style="display: block; padding: 0 10px"> Author:      WordCamp.org
</span><span class="cx" style="display: block; padding: 0 10px"> Author URI:  http://wordcamp.org
</span><span class="cx" style="display: block; padding: 0 10px"> License:     GPLv2 or later
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-// todo if Jetpack_Custom_CSS:get_css is callable, register these, otherwise fatal errors
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+namespace WordCamp\Site_Cloner;
+defined( 'WPINC' ) or die();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_action( 'plugins_loaded',        __NAMESPACE__ . '\get_wordcamp_sites' );
-add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts' );
-add_action( 'admin_menu',            __NAMESPACE__ . '\add_submenu_page' );
-add_action( 'customize_register',    __NAMESPACE__ . '\register_customizer_components' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+const PRIME_SITES_CRON_ACTION      = 'wcsc_prime_sites';
+const WORDCAMP_SITES_TRANSIENT_KEY = 'wcsc_sites';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Initialization
+ */
+function initialize() {
+       // We rely on the Custom CSS module being available
+       if ( ! class_exists( '\Jetpack' ) ) {
+               return;
+       }
+
+       add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts'               );
+       add_action( 'admin_menu',            __NAMESPACE__ . '\add_submenu_page'               );
+       add_action( 'customize_register',    __NAMESPACE__ . '\register_customizer_components' );
+       add_action( 'rest_api_init',         __NAMESPACE__ . '\register_api_endpoints'         );
+       add_action( PRIME_SITES_CRON_ACTION, __NAMESPACE__ . '\prime_wordcamp_sites'           );
+
+       if ( ! wp_next_scheduled( PRIME_SITES_CRON_ACTION ) ) {
+               wp_schedule_event( time(), 'daily', PRIME_SITES_CRON_ACTION );
+       }
+}
+add_action( 'plugins_loaded', __NAMESPACE__ . '\initialize' ); // After Jetpack has loaded
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Register scripts and styles
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function register_scripts() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -28,19 +44,50 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'wordcamp-site-cloner',
</span><span class="cx" style="display: block; padding: 0 10px">                plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.css',
</span><span class="cx" style="display: block; padding: 0 10px">                array(),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                1
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         2
</ins><span class="cx" style="display: block; padding: 0 10px">         );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        wp_register_script(
</span><span class="cx" style="display: block; padding: 0 10px">                'wordcamp-site-cloner',
</span><span class="cx" style="display: block; padding: 0 10px">                plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.js',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                array( 'jquery', 'customize-controls' ),
-               1,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         array( 'jquery', 'customize-controls', 'wp-backbone' ),
+               2,
</ins><span class="cx" style="display: block; padding: 0 10px">                 true
</span><span class="cx" style="display: block; padding: 0 10px">        );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       wp_localize_script(
+               'wordcamp-site-cloner',
+               '_wcscSettings',
+               array(
+                       'apiUrl'        => get_rest_url( null, '/wordcamp-site-cloner/v1/sites/' ),
+                       'customizerUrl' => admin_url( 'customize.php' ),
+                       'themes'        => get_available_themes(),
+               )
+       );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Get all of the available themes
+ *
+ * @return array
+ */
+function get_available_themes() {
+       /** @var \WP_Theme $theme */
+       $available_themes = array();
+       $raw_themes       = wp_get_themes( array( 'allowed' => true ) );
+
+       foreach ( $raw_themes as $theme ) {
+               $theme_name         = $theme->display( 'Name' );
+               $available_themes[] = array(
+                       'slug' => $theme->get_stylesheet(),
+                       'name' => $theme_name ?: $theme->get_stylesheet()
+               );
+       }
+
+       return $available_themes;
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Add a submenu page
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * This helps organizers to realize that this tool exists, because they otherwise wouldn't see it unless
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -52,7 +99,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                __( 'Clone Another WordCamp', 'wordcamporg' ),
</span><span class="cx" style="display: block; padding: 0 10px">                __( 'Clone Another WordCamp', 'wordcamporg' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'switch_themes',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                'customize.php?autofocus[panel]=wordcamp_site_cloner'
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         'customize.php?autofocus[section]=wcsc_sites'
</ins><span class="cx" style="display: block; padding: 0 10px">         );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -63,122 +110,208 @@
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function register_customizer_components( $wp_customize ) {
</span><span class="cx" style="display: block; padding: 0 10px">        require_once( __DIR__ . '/includes/source-site-id-setting.php' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        require_once( __DIR__ . '/includes/sites-section.php' );
</del><span class="cx" style="display: block; padding: 0 10px">         require_once( __DIR__ . '/includes/site-control.php' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $wp_customize->register_control_type( __NAMESPACE__ . '\Site_Control' );
-
</del><span class="cx" style="display: block; padding: 0 10px">         $wp_customize->add_setting( new Source_Site_ID_Setting(
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_customize,
</span><span class="cx" style="display: block; padding: 0 10px">                'wcsc_source_site_id',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                array()
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         array( 'capability' => 'switch_themes' )
</ins><span class="cx" style="display: block; padding: 0 10px">         ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $wp_customize->add_panel(
-               'wordcamp_site_cloner',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $wp_customize->add_section(
+               'wcsc_sites',
</ins><span class="cx" style="display: block; padding: 0 10px">                 array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'type'        => 'wcscPanel',
-                       'title'       => __( 'Clone Another WordCamp', 'wordcamporg' ),
-                       'description' => __( "Clone another WordCamp's theme and custom CSS as a starting point for your site.", 'wordcamporg' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'title'      => __( 'Clone Another WordCamp', 'wordcamporg' ),
+                       'capability' => 'switch_themes'
</ins><span class="cx" style="display: block; padding: 0 10px">                 )
</span><span class="cx" style="display: block; padding: 0 10px">        );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $wp_customize->add_section( new Sites_Section(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $wp_customize->add_control( new Site_Control(
</ins><span class="cx" style="display: block; padding: 0 10px">                 $wp_customize,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                'wcsc_sites',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         'wcsc_site_search',
</ins><span class="cx" style="display: block; padding: 0 10px">                 array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'panel' => 'wordcamp_site_cloner',
-                       'title' => __( 'WordCamp Sites', 'wordcamporg' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'type'     => 'wcscSearch',
+                       'label'    => __( 'Search', 'wordcamporg' ),
+                       'settings' => 'wcsc_source_site_id',
+                       'section'  => 'wcsc_sites'
</ins><span class="cx" style="display: block; padding: 0 10px">                 )
</span><span class="cx" style="display: block; padding: 0 10px">        ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+}
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        foreach( get_wordcamp_sites() as $wordcamp ) {
-               if ( get_current_blog_id() == $wordcamp['site_id'] ) {
-                       continue;
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * Register the REST API endpoint for the Customizer to use to retriever the site list
+ */
+function register_api_endpoints() {
+       if ( ! current_user_can( 'switch_themes' ) ) {
+               return;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $wp_customize->add_control( new Site_Control(
-                       $wp_customize,
-                       'wcsc_site_id_' . $wordcamp['site_id'],
-                       array(
-                               'type'           => 'wcscSite',                      // todo should be able to set this in control instead of here, but if do that then control contents aren't rendered
-                               'site_id'        => $wordcamp['site_id'],
-                               'site_name'      => $wordcamp['name'],
-                               'theme_slug'     => $wordcamp['theme_slug'],
-                               'screenshot_url' => $wordcamp['screenshot_url'],
-                       )
-               ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // todo - use `permission_callback` instead
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       register_rest_route(
+               'wordcamp-site-cloner/v1',
+               '/sites',
+               array(
+                       'methods'  => 'GET',
+                       'callback' => __NAMESPACE__ . '\sites_endpoint',
+               )
+       );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Get required data for relevant WordCamp sites
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Handle the response for the Sites endpoint
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * This isn't actually used until register_customizer_components(), but it's called during `plugins_loaded` in
- * order to prime the cache. That has to be done before `setup_theme`, because the Theme Switcher will override
- * the current theme when `?theme=` is present in the URL parameters, and it's safer to just avoid that than to
- * muck with the internals and try to reverse it on the fly.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * This always pulls cached data, because Central needs to be the site generating it. See get_wordcamp_sites().
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @return array
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-function get_wordcamp_sites() {
-       require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function sites_endpoint() {
+       $sites        = array();
+       $cached_sites = get_site_transient( WORDCAMP_SITES_TRANSIENT_KEY );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        // plugins_loaded is runs on every screen, but we only need this when loading the Customizer and Previewer
-       if ( 'customize.php' != basename( $_SERVER['SCRIPT_NAME'] ) && empty( $_REQUEST['wp_customize'] ) ) {
-               return array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $cached_sites ) {
+               unset( $cached_sites[ get_current_blog_id() ] );
+
+           $sites = array_values( $cached_sites );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $transient_key = 'wcsc_sites';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ return $sites;
+}
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( $sites = get_site_transient( $transient_key ) ) {
-               return $sites;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * Prime the cache of cloneable WordCamp sites
+ *
+ * This is called via WP Cron.
+ *
+ * @todo - Reintroduce batching from `1112.3.diff` to get more than 500 sites
+ */
+function prime_wordcamp_sites() {
+       // This only needs to run on a single site, then the whole network can use the cached result
+       if ( ! is_main_site() ) {
+               return;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Keep the cache longer than needed, just to be sure that it doesn't expire before the cron job runs again
+       set_site_transient( WORDCAMP_SITES_TRANSIENT_KEY, get_wordcamp_sites(), DAY_IN_SECONDS * 2 );
+}
+
+/**
+ * Get WordCamp sites that are suitable for cloning
+ *
+ * @return array
+ */
+function get_wordcamp_sites() {
+       /*
+        * The post statuses that \WordCamp_Loader::get_public_post_statuses() returns are only created on Central,
+        * because the plugin isn't active on any other sites.
+        */
+       if ( ! is_main_site() ) {
+               return array();
+       }
+
+       if ( ! \Jetpack::is_module_active( 'custom-css' ) ) {
+               \Jetpack::activate_module( 'custom-css', false, false );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         switch_to_blog( BLOG_ID_CURRENT_SITE ); // central.wordcamp.org
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $sites = array();
-       $wordcamps = get_posts( array(
-               'post_type'      => 'wordcamp',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $wordcamp_query = new \WP_Query( array(
+               'post_type'      => WCPT_POST_TYPE_ID,
</ins><span class="cx" style="display: block; padding: 0 10px">                 'post_status'    => \WordCamp_Loader::get_public_post_statuses(),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                'posts_per_page' => 125, // todo temporary workaround until able to add filters to make hundreds of sites manageable
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         'posts_per_page' => 500,
</ins><span class="cx" style="display: block; padding: 0 10px">                 'meta_key'       => 'Start Date (YYYY-mm-dd)',
</span><span class="cx" style="display: block; padding: 0 10px">                'orderby'        => 'meta_value_num',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'order'          => 'DESC',
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                'meta_query' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                // New sites won't have finished designs, so ignore them
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'key'     => 'Start Date (YYYY-mm-dd)',
</span><span class="cx" style="display: block; padding: 0 10px">                                'value'   => strtotime( 'now - 1 month' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'compare' => '<'
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 )
</ins><span class="cx" style="display: block; padding: 0 10px">                 ),
</span><span class="cx" style="display: block; padding: 0 10px">        ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        foreach( $wordcamps as $wordcamp ) {
-               $site_id  = get_wordcamp_site_id( $wordcamp );
-               $site_url = get_post_meta( $wordcamp->ID, 'URL', true );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $sites = get_filtered_wordcamp_sites( $wordcamp_query->get_posts() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! $site_id || ! $site_url ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ uasort( $sites, __NAMESPACE__ . '\sort_sites_by_year' );
+
+       restore_current_blog();
+
+       return $sites;
+}
+
+/**
+ * Filter out sites that aren't relevant to the Cloner
+ *
+ * @param array $wordcamps
+ *
+ * @return array
+ */
+function get_filtered_wordcamp_sites( $wordcamps ) {
+       $sites = array();
+
+       foreach ( $wordcamps as $wordcamp ) {
+               $site_id    = get_wordcamp_site_id( $wordcamp );
+               $site_url   = get_post_meta( $wordcamp->ID, 'URL',                     true );
+               $start_date = get_post_meta( $wordcamp->ID, 'Start Date (YYYY-mm-dd)', true );
+
+               if ( ! $site_id || ! $site_url || ! $start_date ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         continue;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                switch_to_blog( $site_id );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $sites[] = array(
-                       'site_id'        => $site_id,
-                       'name'           => get_wordcamp_name(),
-                       'theme_slug'     => get_stylesheet(),
-                       'screenshot_url' => get_screenshot_url( $site_url ),
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * Sites with Coming Soon enabled probably don't have a finished design yet, so there's no point in
+                * cloning it.
+                */
+               if ( ! coming_soon_plugin_enabled() ) {
+                       $preprocessor = \Jetpack_Custom_CSS::get_preprocessor();
+                       $preprocessor = isset( $preprocessor[ 'name' ] ) ? $preprocessor[ 'name' ] : 'none';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        $sites[ $site_id ] = array(
+                               'site_id'          => $site_id,
+                               'name'             => get_wordcamp_name(),
+                               'theme_slug'       => get_stylesheet(),
+                               'screenshot_url'   => get_screenshot_url( $site_url ),
+                               'year'             => date( 'Y', $start_date ),
+                               'css_preprocessor' => $preprocessor,
+                       );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 restore_current_blog();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        restore_current_blog();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ return $sites;
+}
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        set_site_transient( $transient_key, $sites, DAY_IN_SECONDS );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * Determine if the Coming Soon plugin is enabled for the current site
+ *
+ * @return bool
+ */
+function coming_soon_plugin_enabled() {
+       global $WCCSP_Settings;
+       $enabled = false;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        return $sites;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( ! is_callable( 'WCCSP_Settings::get_settings' ) ) {
+               return $enabled;
+       }
+
+       // We may need to instantiate the class if this is the first time calling this function
+       if ( ! is_a( $WCCSP_Settings, 'WCCSP_Settings' ) ) {
+               $WCCSP_Settings = new \WCCSP_Settings();
+       }
+
+       $settings = $WCCSP_Settings->get_settings();
+
+       if ( isset( $settings[ 'enabled' ] ) && 'on' === $settings[ 'enabled' ] ) {
+               $enabled = true;
+       }
+
+       return $enabled;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -195,3 +328,19 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        return apply_filters( 'wcsc_site_screenshot_url', $screenshot_url );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Sort arrays by the year
+ *
+ * @param array $site_a
+ * @param array $site_b
+ *
+ * @return int
+ */
+function sort_sites_by_year( $site_a, $site_b ) {
+       if ( $site_a[ 'year' ] === $site_b[ 'year' ] ) {
+               return 0;
+       }
+
+       return ( $site_a[ 'year' ] < $site_b[ 'year' ] ? 1 : -1 );
+}
</ins></span></pre>
</div>
</div>

</body>
</html>