Ticket #1112: 1112.3.diff
File 1112.3.diff, 40.9 KB (added by , 9 years ago) |
---|
-
wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php index 8628d2c..88bc3d1 100644
a b namespace WordCamp\Site_Cloner; 5 5 defined( 'WPINC' ) or die(); 6 6 7 7 /** 8 * Custom Customizer Control for a WordCamp site8 * Custom Customizer Control for Search WordCamp sites to clone 9 9 */ 10 10 class Site_Control extends \WP_Customize_Control { 11 public $site_id, $site_name, $screenshot_url, $theme_slug; 12 public $settings = 'wcsc_source_site_id'; 13 public $section = 'wcsc_sites'; 11 12 public function __construct( $manager, $id, $args = [ ] ) { 13 parent::__construct( $manager, $id, $args ); 14 15 $this->capability = 'edit_theme_options'; 16 $this->section = 'wcsc_sites'; 17 } 14 18 15 19 /** 16 20 * Enqueue scripts and styles … … class Site_Control extends \WP_Customize_Control { 18 22 public function enqueue() { 19 23 wp_enqueue_style( 'wordcamp-site-cloner' ); 20 24 wp_enqueue_script( 'wordcamp-site-cloner' ); 25 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_view_templates' ) ); 21 26 } 22 27 23 /**24 * Render the control's content25 */26 28 public function render_content() { 27 $preview_url = add_query_arg(28 array(29 'theme' => rawurlencode( $this->theme_slug ),30 'wcsc_source_site_id' => rawurlencode( $this->site_id ),31 ),32 admin_url( 'customize.php' )33 );34 35 29 require( dirname( __DIR__ ) . '/templates/site-control.php' ); 36 30 } 31 32 public function print_view_templates() { 33 ?> 34 <script id="tmpl-wcsc-site-option" type="text/html"> 35 <?php include dirname( __DIR__ ) . '/templates/site-option.php'; ?> 36 </script> 37 <script id="tmpl-wcsc-site-filters" type="text/html"> 38 <?php include dirname( __DIR__ ) . '/templates/site-filters.php'; ?> 39 </script> 40 <?php 41 } 37 42 } -
deleted file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/sites-section.php deleted file mode 100644 index 69b85e4..0000000
+ - 1 <?php2 3 namespace WordCamp\Site_Cloner;4 5 defined( 'WPINC' ) or die();6 7 /**8 * Custom Customizer Section for WordCamp sites9 */10 class Sites_Section extends \WP_Customize_Section {11 public $type = 'wcsc-sites';12 13 /**14 * Render the section's content15 */16 protected function render() {17 require_once( dirname( __DIR__ ) . '/templates/sites-section.php' );18 }19 } -
wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php index ae450ea..5e84f42 100644
a b class Source_Site_ID_Setting extends \WP_Customize_Setting { 25 25 return; 26 26 } 27 27 28 // disable the current site's custom css from being output 29 remove_action( 'wp_head', array( 'Jetpack_Custom_CSS', 'link_tag' ), 101 ); 28 30 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 29 31 add_filter( 'get_post_metadata', array( $this, 'preview_jetpack_postmeta' ), 10, 4 ); 30 32 add_filter( 'safecss_skip_stylesheet', array( $this, 'preview_skip_stylesheet' ) ); … … class Source_Site_ID_Setting extends \WP_Customize_Setting { 109 111 * to the URL. 110 112 * 111 113 * @param int $source_site_id 114 * 115 * @return null 112 116 */ 113 117 protected function update( $source_site_id ) { 114 118 switch_to_blog( $source_site_id ); -
wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php index 952a134..9b60dba 100644
a b 1 <?php defined( 'WPINC' ) or die(); ?> 1 <?php 2 defined( 'WPINC' ) or die(); 2 3 3 <div id="wcsc-site-<?php echo esc_attr( $this->site_id ); ?>" class="wcscSite" data-preview-url="<?php echo esc_url( $preview_url ); ?>"> 4 <div class="wcsc-site-screenshot"> 5 <img src="<?php echo esc_url( $this->screenshot_url ); ?>" alt="<?php echo esc_attr( $this->site_name ); ?>" /> 6 </div> 7 8 <h3 class="wcsc-site-name"> 9 <?php echo esc_html( $this->site_name ); ?> 4 /** 5 * Top level template for the output of the Site Cloner Customizer Control 6 */ 7 ?> 8 <div id="wcsc-cloner"> 9 <h3> 10 <?php esc_html_e( 'WordCamp Sites', 'wordcamporg' ); ?> 11 <span id="wcsc-sites-count" class="title-count wcsc-sites-count"></span> 10 12 </h3> 13 <div class="filters"> 14 </div> 11 15 12 <span id="live-preview-label-<?php echo esc_attr( $this->site_id ); ?>" class="wcsc-live-preview-label"> 13 <?php _e( 'Live Preview', 'wordcamporg' ); ?> 14 </span> 15 </div> 16 <div class="wcsc-search"> 17 <ul id="wcsc-results"> 18 </ul> 19 </div> 20 </div> 21 No newline at end of file -
new file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-filters.php new file mode 100644 index 0000000..ecf9a9e
- + 1 <?php 2 /** 3 * Template for the search and drop down filters 4 */ 5 6 defined( 'WPINC' ) or die(); 7 8 ?> 9 <div class="wcsc-filter"> 10 <label for="wcsc-filter-search-input"> 11 <span class="customize-control-title"><?php _e( 'Search', 'wordcamporg' ); ?></span> 12 <div class="customize-control-content"> 13 <input type="search" id="wcsc-filter-search-input" class="wcsc-filter-search"/> 14 </div> 15 </label> 16 </div> 17 18 <div class="wcsc-filter"> 19 <label for="wcsc-filter-theme_slug"> 20 <span class="customize-control-title"><?php _e( 'Theme', 'wordcamporg' ); ?></span> 21 <div class="customize-control-content"> 22 <select id="wcsc-filter-theme_slug" data-filter="theme_slug"> 23 <option value="">Any</option> 24 <# _.each(data.themeOptions, function(themeOption) { #> 25 <option value="{{themeOption.slug}}">{{{themeOption.name}}}</option> 26 <# }); #> 27 </select> 28 </div> 29 </label> 30 </div> 31 32 <div class="wcsc-filter"> 33 <label for="wcsc-filter-year"> 34 <span class="customize-control-title"><?php _e( 'WordCamp Year', 'wordcamporg' ); ?></span> 35 <div class="customize-control-content"> 36 <select id="wcsc-filter-year" data-filter="year"> 37 <option value="">Any</option> 38 <# _.each(data.yearOptions, function(yearOption) { #> 39 <option value="{{yearOption}}">{{yearOption}}</option> 40 <# }); #> 41 </select> 42 </div> 43 </label> 44 </div> 45 46 <div class="wcsc-filter"> 47 <label for="wcsc-filter-css_preprocessor"> 48 <span class="customize-control-title"><?php _e( 'CSS Preprocessor', 'wordcamporg' ); ?></span> 49 <div class="customize-control-content"> 50 <select id="wcsc-filter-css_preprocessor" data-filter="css_preprocessor"> 51 <option value="">Any</option> 52 <# _.each(data.preprocessorOptions, function(preprocessorOption) { #> 53 <option value="{{preprocessorOption}}">{{preprocessorOption}}</option> 54 <# }); #> 55 </select> 56 </div> 57 </label> 58 </div> 59 <?php 60 No newline at end of file -
new file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-option.php new file mode 100644 index 0000000..a8c7213
- + 1 <?php 2 /** 3 * Template for a single Site representation within the Site Cloner Control 4 */ 5 6 defined( 'WPINC' ) or die(); 7 8 ?> 9 <div class="wcsc-site-screenshot"> 10 <img src="{{ data.screenshot_url }}" alt="{{ data.name }}"/> 11 </div> 12 13 <h3 class="wcsc-site-name"> 14 {{ data.name }} 15 </h3> 16 17 <# if ( data.active ) { #> 18 <span id="live-previewing-{{ data.site_id }}" class="wcsc-previewing-label"> 19 <?php _e( 'Viewing', 'wordcamporg' ); ?> 20 </span> 21 <# } else { #> 22 <span id="live-preview-label-{{ data.site_id }}" class="wcsc-live-preview-label"> 23 <?php _e( 'Live Preview', 'wordcamporg' ); ?> 24 </span> 25 <# } #> 26 <?php 27 No newline at end of file -
deleted file wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/sites-section.php deleted file mode 100644 index a05272c..0000000
+ - 1 <?php defined( 'WPINC' ) or die(); ?>2 3 <li id="section-<?php echo esc_attr( $this->id ); ?>" class="accordion-section control-section control-section-<?php echo esc_attr( $this->type ); ?>">4 <h3>5 <?php esc_html_e( 'WordCamp Sites' ); ?>6 7 <span class="title-count wcsc-sites-count">8 <?php echo count( $this->controls ); ?>9 </span>10 </h3>11 12 <div class="wcsc-sites-section-content">13 <ul id="wcsc-sites"></ul>14 </div>15 </li> -
wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css index 0a33876..98eaf07 100644
a b 6 6 overflow: auto; 7 7 } 8 8 9 .wcsc Site {9 .wcsc-site { 10 10 position: relative; 11 11 cursor: pointer; 12 12 border: 1px solid #DEDEDE; 13 13 box-shadow: 0 1px 1px -1px rgba( 0, 0, 0, 0.1 ); 14 margin-top: 5px; 14 15 } 15 16 16 17 .wcsc-site-screenshot { 17 18 transition: opacity 0.2s ease-in-out 0s; 18 19 } 19 20 20 .wcsc Site:hover .wcsc-site-screenshot {21 .wcsc-site:hover .wcsc-site-screenshot { 21 22 opacity: 0.4; 22 23 } 23 24 24 .wcsc-live-preview-label {25 .wcsc-live-preview-label, .wcsc-previewing-label { 25 26 opacity: 0; 26 27 position: absolute; 27 28 top: 35%; … … 38 39 transition: opacity 0.1s ease-in-out 0s; 39 40 } 40 41 41 .wcsc Site:hover .wcsc-live-preview-label {42 .wcsc-site:hover .wcsc-live-preview-label, .wcsc-site .wcsc-previewing-label { 42 43 opacity: 1; 43 44 } 45 46 div.wcsc-filter { 47 margin-bottom: 10px; 48 } 49 No newline at end of file -
wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js index 5c05f82..5bf3f64 100644
a b 1 ( function( wp, $) {1 (function ( wp, $, Backbone, win, settings ) { 2 2 'use strict'; 3 3 4 if ( ! wp || !wp.customize ) {4 if ( !wp || !wp.customize ) { 5 5 return; 6 6 } 7 7 8 var api = wp.customize; 8 var api = wp.customize, 9 wcsc = wp.wcSiteCloner = { models: {}, views: {}, collections: {}, routers: {}, settings: {} }; 9 10 10 /** 11 * The Clone Another WordCamp panel 12 */ 13 api.panelConstructor.wcscPanel = api.Panel.extend( { 14 /** 15 * Initialize the panel after it's loaded 16 * 17 * Ideally, the Previewer would be set to the requested site ID during the initial PHP request, rather than 18 * loading the host site in the Previewer, and then refreshing it to use the requested site. That became a 19 * rabbit hole, though, so it's done this way instead. 20 */ 21 ready : function() { 22 var urlParams = getUrlParams( window.location.href ); 11 wcsc.settings = settings || {}; 23 12 24 if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) { 25 this.expand(); 26 api( 'wcsc_source_site_id' ).set( urlParams.wcsc_source_site_id ); 13 // Model for a single site 14 wcsc.models.Site = Backbone.Model.extend( { 15 defaults: { 16 'active': false 17 }, 18 idAttribute: 'site_id' 19 } ); 20 21 // Model representing the filter state for searching/filtering sites 22 wcsc.models.SearchFilter = Backbone.Model.extend( { 23 's': '', 24 'theme_slug': '', 25 'year': '', 26 'css_preprocessor': '' 27 } ); 28 29 // Top level view for the Site Cloner Control 30 wcsc.views.SiteSearch = Backbone.View.extend( { 31 el: '#wcsc-cloner .wcsc-search', 32 33 // index of the currently viewed page of results 34 page: 0, 35 36 initialize: function ( options ) { 37 38 // update scroller position 39 _.bindAll( this, 'scroller' ); 40 41 // container that will be scrolled within 42 this.$container = $( '#wcsc-cloner' ).parents( 'ul.accordion-section-content' ); 43 // bind scrolling within the container to check for infinite scroll 44 this.$container.bind( 'scroll', _.throttle( this.scroller, 300 ) ); 45 46 // the model and view for filtering the site results 47 this.filterView = new wcsc.views.SearchFilters( { 48 model: this.collection.searchFilter, 49 parent: this 50 } ); 51 52 // View for listing the matching sites 53 this.resultsView = new wcsc.views.SearchResults( { 54 collection: this.collection, 55 parent: this 56 } ); 57 58 }, 59 60 render: function () { 61 this.filterView.render(); 62 63 this.resultsView.render(); 64 65 this.$el.empty().append( this.resultsView.el ); 66 }, 67 68 // Checks if a user has reached the bottom of the list 69 // and triggers a scroll event to show more sites if needed 70 scroller: function () { 71 var visibleBottom, threshold, elementHeight, containerHeight, scrollTop; 72 73 scrollTop = this.$container.scrollTop(); 74 containerHeight = this.$container.innerHeight(); 75 elementHeight = this.$container.get( 0 ).scrollHeight; 76 77 visibleBottom = scrollTop + containerHeight; 78 threshold = Math.round( elementHeight * 0.9 ); 79 80 if ( visibleBottom > threshold ) { 81 this.trigger( 'wcsc:scroll' ); 27 82 } 28 83 } 84 29 85 } ); 30 86 31 /** 32 * Custom control representing a site that can be previewed/imported 33 */ 34 api.controlConstructor.wcscSite = api.Control.extend( { 35 /** 36 * Initialize the control after it's loaded 37 */ 38 ready : function() { 39 this.container.on( 'click', '.wcscSite', this.previewSite ); 40 }, 41 42 /** 43 * Preview the selected site 44 * 45 * If the site is using a different theme, then reload the entire Customizer with the theme URL parameter 46 * set, so that the Theme Switcher will handle previewing the new theme for us. Otherwise just set the ID 47 * to refresh the Previewer with the current theme and the new site's CSS, etc. 48 * 49 * @param {object} event 50 */ 51 previewSite : function( event ) { 52 var previewUrl = $( this ).data( 'preview-url' ), 53 previewUrlParams = getUrlParams( previewUrl ); 54 55 if ( api( 'wcsc_source_site_id' ).get() == previewUrlParams.wcsc_source_site_id ) { 87 // Collection representing the list of cloneable sites 88 wcsc.collections.Sites = Backbone.Collection.extend( { 89 model: wcsc.models.Site, 90 url: wcsc.settings.apiUrl, 91 92 initialize: function ( options ) { 93 this.searchFilter = options.searchFilter || {}; 94 95 this.listenTo( this.searchFilter, 'change', this.applyFilter ); 96 }, 97 98 // Filter this collection by the updated searchFilter attributes 99 applyFilter: function () { 100 var filters = this.searchFilter.toJSON(), 101 activeFilters = _.pick( filters, _.identity ), 102 term = '', 103 sites; 104 105 // nothing actually changed, so don't update the collection 106 if ( _.isEmpty( this.searchFilter.changedAttributes() ) ) { 107 return; 108 } 109 110 //no active filters. Reset to the full list and bail 111 if ( _.isEmpty( activeFilters ) ) { 112 this.resetCanonical(); 113 return; 114 } 115 116 this.resetCanonical( { silent: true } ); 117 118 // remove the search query restriction since we already filtered by word matches above 119 if ( activeFilters.s ) { 120 term = activeFilters.s; 121 delete activeFilters.s; 122 } 123 124 sites = this.where( activeFilters ); 125 126 if ( term ) { 127 sites = this.filterBySearch( sites, term ); 128 } 129 130 this.reset( sites ); 131 132 }, 133 134 // internal method for filtering sites by search terms 135 filterBySearch: function ( sites, term ) { 136 var match, name; 137 138 // Escape the term string for RegExp meta characters 139 term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); 140 141 // Consider spaces as word delimiters and match the whole string 142 // so matching terms can be combined 143 term = term.replace( / /g, ')(?=.*' ); 144 match = new RegExp( '^(?=.*' + term + ').+', 'i' ); 145 146 return _.filter( sites, function ( site ) { 147 name = site.get( 'name' ).replace( /(<([^>]+)>)/ig, '' ); 148 149 return match.test( name ); 150 } ); 151 }, 152 153 paginate: function ( pageIndex ) { 154 var collection = this; 155 pageIndex = pageIndex || 0; 156 157 collection = _( collection.rest( 20 * pageIndex ) ); 158 collection = _( collection.first( 20 ) ); 159 return collection; 160 }, 161 162 // Resets the site collection dataset to the canonical list originally pulled from the api 163 resetCanonical: function ( options ) { 164 var activeSiteId = api( 'wcsc_source_site_id' ).get(), 165 activeSite; 166 options = options || {}; 167 168 this.reset( wcsc.settings.siteData, options ); 169 170 //restore the currently active site 171 if ( activeSiteId ) { 172 activeSite = this.find( { site_id: activeSiteId } ); 173 if ( typeof activeSite !== 'undefined' ) { 174 activeSite.set( { active: true } ); 175 } 176 } 177 } 178 } ); 179 180 // View for a single site 181 wcsc.views.Site = Backbone.View.extend( { 182 className: 'wcsc-site', 183 184 attributes: function () { 185 return { 186 'id': 'wcsc-site-' + this.model.get( 'site_id' ), 187 'data-site-id': this.model.get( 'site_id' ) 188 } 189 }, 190 191 html: wp.template( 'wcsc-site-option' ), 192 193 touchDrag: false, 194 195 events: { 196 'click': 'preview', 197 'keydown': 'preview', 198 'touchend': 'preview', 199 'touchmove': 'preventPreview' 200 }, 201 202 initialize: function ( options ) { 203 this.parent = options.parent; 204 205 this.listenTo( this.model, 'change', this.render ); 206 207 this.render(); 208 }, 209 210 render: function () { 211 var data = this.model.toJSON(); 212 this.$el.html( this.html( data ) ); 213 }, 214 215 preventPreview: function () { 216 this.touchDrag = true; 217 }, 218 219 preview: function ( event ) { 220 event = event || window.event; 221 222 //ignore touches caused by scrolling 223 if ( this.touchDrag === true ) { 224 this.touchDrag = false; 225 } 226 227 event.preventDefault(); 228 229 this.$el.trigger( 'wcsc:previewSite', this.model ); 230 } 231 232 } ); 233 234 // View for the site results list 235 wcsc.views.SearchResults = Backbone.View.extend( { 236 className: 'wcsc-results', 237 238 initialize: function ( options ) { 239 var self = this; 240 241 this.parent = options.parent; 242 243 this.$siteCount = $( '#wcsc-sites-count' ); 244 245 // Rerender the view whenever a collection change is complete 246 this.listenTo( this.collection, 'reset', function () { 247 //reset pagination 248 self.parent.page = 0; 249 self.render( this ); 250 } ); 251 252 this.listenTo( this.parent, 'wcsc:scroll', function () { 253 self.renderSites( self.parent.page ); 254 } ); 255 }, 256 257 render: function () { 258 this.$el.empty(); 259 260 this.renderSites( this.parent.page ); 261 262 this.$siteCount.text( this.collection.length ); 263 }, 264 265 renderSites: function ( page ) { 266 var self = this; 267 268 // get a collection of just the requested page 269 this.instance = this.collection.paginate( page ); 270 271 if ( this.instance.size() === 0 ) { 272 this.parent.trigger( 'wcsc:end' ); 273 return; 274 } 275 276 this.instance.each( function ( site ) { 277 var siteView = new wcsc.views.Site( { 278 model: site, 279 parent: self 280 } ); 281 282 siteView.render(); 283 284 self.$el.append( siteView.el ); 285 } ); 286 287 this.parent.page++; 288 } 289 290 } ); 291 292 // View for the search and dropdown filters 293 wcsc.views.SearchFilters = Backbone.View.extend( { 294 el: '#wcsc-cloner .filters', 295 className: 'wscs-filters', 296 html: wp.template( 'wcsc-site-filters' ), 297 events: { 298 "input #wcsc-filter-search-input": "search", 299 "keyup #wcsc-filter-search-input": "search", 300 "change select": "applyFilter" 301 }, 302 initialize: function ( options ) { 303 this.parent = options.parent; 304 }, 305 306 render: function () { 307 var data = {}; 308 309 data.themeOptions = wcsc.settings.themes; 310 data.yearOptions = _.uniq( this.parent.collection.pluck( 'year' ) ).sort(); 311 data.preprocessorOptions = _.uniq( this.parent.collection.pluck( 'css_preprocessor' ) ).sort(); 312 313 this.$el.html( this.html( data ) ); 314 315 this.$searchInput = $( '#wcsc-filter-search-input' ); 316 this.$themeFilter = $( '#wcsc-filter-theme_slug' ); 317 this.$yearFilter = $( '#wcsc-filter-year' ); 318 this.$preprocessorFilter = $( '#wcsc-filter-css_preprocessor' ); 319 }, 320 321 search: function ( event ) { 322 // Clear on escape. 323 if ( event.type === 'keyup' && event.which === 27 ) { 324 event.target.value = ''; 325 } 326 327 /** 328 * Since doSearch is debounced, it will only run when user input comes to a rest 329 */ 330 this.doSearch( event ); 331 }, 332 333 doSearch: _.debounce( function ( event ) { 334 var options = {}; 335 336 this.model.set( 's', event.target.value ); 337 338 }, 500 ), 339 340 applyFilter: function ( event ) { 341 var $target = $( event.target ), 342 value = $target.val(), 343 filter = $target.data( 'filter' ); 344 this.model.set( filter, value ); 345 }, 346 347 // Set the inputs to the set of filters as triggered by the router on initial load 348 setInputs: function ( filters ) { 349 this.model.set( filters, { silent: true } ); 350 this.$searchInput.val( this.model.get( 's' ) ); 351 this.$themeFilter.val( this.model.get( 'theme_slug' ) ); 352 this.$yearFilter.val( this.model.get( 'year' ) ); 353 this.$preprocessorFilter.val( this.model.get( 'css_preprocessor' ) ); 354 this.model.trigger( 'change', this.model ); 355 } 356 357 } ); 358 359 // Sets up listener to store the user's selected filters and search so a user's position can be 360 // restored as well as possible after a theme changes causes a full refresh 361 wcsc.routers.FilterState = Backbone.Router.extend( { 362 routes: { 363 'wcsc?*filters': 'applyFilters' 364 }, 365 initialize: function ( options ) { 366 this.parent = options.parent; 367 368 //any time the collection is reset, we need to update the displayed route 369 this.listenTo(this.parent.view.collection, 'reset', this.updateLocation); 370 }, 371 372 // Applies the filters set in the query string to the view 373 applyFilters: function ( queryString ) { 374 var filters = deserializeQueryString( queryString ); 375 376 this.parent.view.filterView.setInputs(filters); 377 }, 378 updateLocation: function () { 379 var filters = this.parent.view.collection.searchFilter.toJSON(), 380 activeFilters = _.pick( filters, _.identity ), 381 queryString = $.param( activeFilters ); 382 this.navigate( 'wcsc?' + queryString ); 383 } 384 } ); 385 386 // Customizer Control wrapping the site search applet 387 api.controlConstructor.wcscSearch = api.Control.extend( { 388 ready: function () { 389 var filter = new wcsc.models.SearchFilter(); //top level model representing the current filter applied to the collection 390 391 this.siteCollection = new wcsc.collections.Sites( { searchFilter: filter } ); 392 393 // fill the site collection and setup search when complete 394 this.siteCollection.fetch( { 395 success: this.setupSearch.bind(this) 396 } ); 397 }, 398 399 // Initialize the site search instance for cloning other sites 400 setupSearch: function () { 401 var control = this, 402 urlParams = getUrlParams( win.location.href ), 403 currentSite; 404 405 //Set a canonical array of all sites prior to filtering 406 wcsc.settings.siteData = this.siteCollection.toJSON(); 407 408 // if the wcsc_source_site_id is set, its most likely from a user previewing a site, so bring them back 409 if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) { 410 api.section( this.section() ).expand(); 411 currentSite = this.siteCollection.find({site_id: urlParams.wcsc_source_site_id}); 412 if(currentSite) { 413 this.setActiveSite(currentSite); 414 } 415 } 416 417 $( '#wcsc-cloner' ).on( 'wcsc:previewSite', '.wcsc-site', function ( event, site ) { 418 control.previewSite( site ); 419 } ); 420 421 // setup the top level Site Search View 422 this.view = new wcsc.views.SiteSearch( { 423 parent: this, 424 collection: this.siteCollection 425 } ); 426 427 this.view.render(); 428 429 // Initialize the router to allow state to be restored after a full refresh 430 wcsc.router = new wcsc.routers.FilterState( { parent: this } ); 431 Backbone.history.start(); 432 433 }, 434 435 previewSite: function ( site ) { 436 var queryString, routerFragment; 437 if ( api( 'wcsc_source_site_id' ).get() == site.get( 'site_id' ) ) { 438 //we're already previewing this site 56 439 return; 57 440 } 58 441 59 if ( api.settings.theme.stylesheet === previewUrlParams.theme) {60 api( 'wcsc_source_site_id' ).set( previewUrlParams.wcsc_source_site_id);442 if ( api.settings.theme.stylesheet === site.get( 'theme_slug' ) ) { 443 this.setActiveSite( site ); 61 444 } else { 62 window.parent.location = previewUrl; 445 446 //we have to do a full refresh when changing themes or other controls won't correlate to the current theme. 447 queryString = $.param( { 448 'theme': site.get( 'theme_slug' ), 449 'wcsc_source_site_id': site.get( 'site_id' ) 450 } ); 451 452 routerFragment = Backbone.history.getFragment(); 453 win.parent.location = wcsc.settings.customizerUrl + '?' + queryString + 454 '#' + routerFragment; 63 455 } 456 }, 457 458 // Set the active site and update the model to reflect the change 459 setActiveSite: function(site) { 460 var site_id = site.get('site_id'); 461 this.siteCollection.each( function ( _site ) { 462 _site.set( { active: false } ); 463 } ); 464 site.set( { active: true } ); 465 api( 'wcsc_source_site_id' ).set( site.get('site_id') ); 64 466 } 467 65 468 } ); 66 469 470 67 471 /** 68 472 * Parse the URL parameters 69 473 * … … 74 478 * @returns {object} 75 479 */ 76 480 function getUrlParams( url ) { 77 var match, questionMarkIndex, query,78 urlParams = {}, 79 pl = /\+/g, // Regex for replacing addition symbol with a space80 search = /([^&=]+)=?([^&]*)/g,81 decode = function ( s) {82 return decodeURIComponent( s.replace( pl, " " ));83 };481 var questionMarkIndex, query, hashIndex; 482 483 //strip hash first 484 hashIndex = url.indexOf('#'); 485 if(hashIndex > -1) { 486 url = url.substring(0, hashIndex); 487 } 84 488 85 489 questionMarkIndex = url.indexOf( '?' ); 86 490 87 491 if ( -1 === questionMarkIndex ) { 88 return urlParams;492 return {}; 89 493 } else { 90 494 query = url.substring( questionMarkIndex + 1 ); 91 495 } 92 496 93 while ( match = search.exec( query ) ) { 497 return deserializeQueryString( query ); 498 } 499 500 /** 501 * Deserialize a query string into an object 502 * 503 * @param queryString 504 * @returns {{}} 505 */ 506 function deserializeQueryString( queryString ) { 507 var match, 508 urlParams = {}, 509 pl = /\+/g, // Regex for replacing addition symbol with a space 510 search = /([^&=]+)=?([^&]*)/g, 511 decode = function ( s ) { 512 return decodeURIComponent( s.replace( pl, " " ) ); 513 }; 514 515 while ( match = search.exec( queryString ) ) { 94 516 urlParams[ decode( match[ 1 ] ) ] = decode( match[ 2 ] ); 95 517 } 96 518 97 519 return urlParams; 98 520 } 99 } )( window.wp, jQuery ); 521 })( wp, jQuery, Backbone, window, _wcscSettings ); 522 No newline at end of file -
wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php
diff --git a/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php b/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php index c398534..2776352 100755
a b defined( 'WPINC' ) or die(); 7 7 /* 8 8 Plugin Name: WordCamp Site Cloner 9 9 Description: Allows organizers to clone another WordCamp's theme and custom CSS as a starting point for their site. 10 Version: 0. 110 Version: 0.2.0-alpha 11 11 Author: WordCamp.org 12 12 Author URI: http://wordcamp.org 13 13 License: GPLv2 or later 14 14 */ 15 15 16 // todo if Jetpack_Custom_CSS:get_css is callable, register these, otherwise fatal errors 16 /** 17 * Register the plugin's hooks after `plugins_loaded` so we can first verify that JetPack is available. If not, 18 * this plugin can't be used. 19 */ 20 function register_hooks() { 21 if ( ! class_exists( '\JetPack' ) ) { 22 return; 23 } 24 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts' ); 25 add_action( 'admin_menu', __NAMESPACE__ . '\add_submenu_page' ); 26 add_action( 'customize_register', __NAMESPACE__ . '\register_customizer_components' ); 27 add_action( 'rest_api_init', __NAMESPACE__ . '\initialize_site_list_API' ); 28 29 // Setup a daily cron to run on the main central.wordcamp.org site to keep the site list up to date. 30 if ( is_main_site() ) { 31 $hook = 'wcsc_prime_sites'; 32 if ( ! wp_next_scheduled( $hook ) ) { 33 wp_schedule_event( time(), 'daily', $hook ); 34 } 35 36 add_action( $hook, __NAMESPACE__ . '\prime_wordcamp_sites' ); 37 } 38 } 39 40 add_action( 'plugins_loaded', __NAMESPACE__ . '\register_hooks' ); 41 17 42 18 add_action( 'plugins_loaded', __NAMESPACE__ . '\get_wordcamp_sites' );19 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts' );20 add_action( 'admin_menu', __NAMESPACE__ . '\add_submenu_page' );21 add_action( 'customize_register', __NAMESPACE__ . '\register_customizer_components' );22 43 23 44 /** 24 45 * Register scripts and styles … … function register_scripts() { 28 49 'wordcamp-site-cloner', 29 50 plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.css', 30 51 array(), 31 152 '0.2.0-alpha' 32 53 ); 33 54 34 55 wp_register_script( 35 56 'wordcamp-site-cloner', 36 57 plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.js', 37 array( 'jquery', 'customize-controls' ),38 1,58 array( 'jquery', 'customize-controls', 'wp-backbone' ), 59 '0.2.0-alpha', 39 60 true 40 61 ); 62 63 $availableThemes = [ ]; 64 foreach ( wp_get_themes( array( 'allowed' => true ) ) as $theme ) { 65 $themeName = $theme->display( 'Name' ); 66 $availableThemes[] = array( 67 'slug' => $theme->get_stylesheet(), 68 'name' => $themeName ?: $theme->get_stylesheet() 69 ); 70 } 71 72 wp_localize_script( 'wordcamp-site-cloner', '_wcscSettings', array( 73 'apiUrl' => get_rest_url( null, '/wordcamp-site-cloner/v1/sites/' ), 74 'customizerUrl' => admin_url( 'customize.php' ), 75 'themes' => $availableThemes 76 ) ); 41 77 } 42 78 43 79 /** … … function add_submenu_page() { 52 88 __( 'Clone Another WordCamp', 'wordcamporg' ), 53 89 __( 'Clone Another WordCamp', 'wordcamporg' ), 54 90 'switch_themes', 55 'customize.php?autofocus[ panel]=wordcamp_site_cloner'91 'customize.php?autofocus[section]=wcsc_sites' 56 92 ); 57 93 } 58 94 … … function add_submenu_page() { 63 99 */ 64 100 function register_customizer_components( $wp_customize ) { 65 101 require_once( __DIR__ . '/includes/source-site-id-setting.php' ); 66 require_once( __DIR__ . '/includes/sites-section.php' );67 102 require_once( __DIR__ . '/includes/site-control.php' ); 68 103 69 $wp_customize->register_control_type( __NAMESPACE__ . '\Site_Control' );70 71 104 $wp_customize->add_setting( new Source_Site_ID_Setting( 72 105 $wp_customize, 73 106 'wcsc_source_site_id', 74 array() 107 array( 108 'capability' => 'switch_themes' 109 ) 75 110 ) ); 76 111 77 $wp_customize->add_panel( 78 'wordcamp_site_cloner', 79 array( 80 'type' => 'wcscPanel', 81 'title' => __( 'Clone Another WordCamp', 'wordcamporg' ), 82 'description' => __( "Clone another WordCamp's theme and custom CSS as a starting point for your site.", 'wordcamporg' ), 112 $wp_customize->add_section( 'wcsc_sites', array( 113 'title' => __( 'Clone Another WordCamp', 'wordcamporg' ), 114 'capability' => 'switch_themes' 83 115 ) 84 116 ); 85 117 86 $wp_customize->add_ section( new Sites_Section(118 $wp_customize->add_control( new Site_Control( 87 119 $wp_customize, 88 'wcsc_site s',120 'wcsc_site_search', 89 121 array( 90 'panel' => 'wordcamp_site_cloner', 91 'title' => __( 'WordCamp Sites', 'wordcamporg' ), 122 'type' => 'wcscSearch', 123 'label' => __( 'Search', 'wordcamporg' ), 124 'settings' => 'wcsc_source_site_id', 125 'section' => 'wcsc_sites' 92 126 ) 93 127 ) ); 128 } 94 129 95 foreach( get_wordcamp_sites() as $wordcamp ) { 96 if ( get_current_blog_id() == $wordcamp['site_id'] ) { 97 continue; 98 } 99 100 $wp_customize->add_control( new Site_Control( 101 $wp_customize, 102 'wcsc_site_id_' . $wordcamp['site_id'], 103 array( 104 'type' => 'wcscSite', // todo should be able to set this in control instead of here, but if do that then control contents aren't rendered 105 'site_id' => $wordcamp['site_id'], 106 'site_name' => $wordcamp['name'], 107 'theme_slug' => $wordcamp['theme_slug'], 108 'screenshot_url' => $wordcamp['screenshot_url'], 109 ) 130 /** 131 * Register the api route for the customizer to use to retriever the site list 132 */ 133 function initialize_site_list_API() { 134 if ( current_user_can( 'switch_themes' ) ) { 135 register_rest_route( 'wordcamp-site-cloner/v1', '/sites', array( 136 'methods' => 'GET', 137 'callback' => __NAMESPACE__ . '\get_wordcamp_sites' 110 138 ) ); 111 139 } 112 140 } … … function register_customizer_components( $wp_customize ) { 114 142 /** 115 143 * Get required data for relevant WordCamp sites 116 144 * 117 * This isn't actually used until register_customizer_components(), but it's called during `plugins_loaded` in118 * order to prime the cache. That has to be done before `setup_theme`, because the Theme Switcher will override119 * the current theme when `?theme=` is present in the URL parameters, and it's safer to just avoid that than to120 * muck with the internals and try to reverse it on the fly.121 *122 145 * @return array 123 146 */ 124 147 function get_wordcamp_sites() { 125 require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' );148 $transient_key = 'wcsc_sites'; 126 149 127 // plugins_loaded is runs on every screen, but we only need this when loading the Customizer and Previewer 128 if ( 'customize.php' != basename( $_SERVER['SCRIPT_NAME'] ) && empty( $_REQUEST['wp_customize'] ) ) { 129 return array(); 150 if ( false == ( $sites = get_site_transient( $transient_key ) ) ) { 151 $sites = _get_wordcamp_sites(); 152 153 //set the transient longer than needed to be sure it doesn't expire before the cron runs again. 154 set_site_transient( $transient_key, $sites, DAY_IN_SECONDS * 2 ); 130 155 } 131 156 132 $transient_key = 'wcsc_sites'; 157 // remove the current site from the result set 158 unset( $sites[ get_current_blog_id() ] ); 159 160 return array_values( $sites ); 161 } 162 163 /** 164 * Queries and filters the available sites to clone by a single page. If more than one page of WordCamp posts exists, 165 * a cron will be scheduled in 10 minutes to update the next page's worth of sites in the cache. 166 * 167 * @internal 168 * 169 * @param array $args { 170 * An array of elements that make up a post to update or insert. 171 * 172 * @type int $page The page of results to select. Default 1. 173 * @type int $posts_per_page The number of results to return in the initial query. Default 500 174 * @type bool $schedule_cron Whether to schedule a cron job to prime the next page of results. Default true 175 * } 176 * 177 * @return array WordCamp Sites array. 178 */ 179 function _get_wordcamp_sites($args = array()) { 133 180 134 if ( $sites = get_site_transient( $transient_key ) ) { 135 return $sites; 181 $options = wp_parse_args( $args, array( 182 'page' => 1, 183 'posts_per_page' => 500, 184 'schedule_cron' => true 185 ) ); 186 187 $page = absint($options['page']); 188 189 //verify that the required plugins and JetPack modules are available 190 require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' ); 191 if ( ! \Jetpack::is_module_active( 'custom-css' ) ) { 192 \Jetpack::activate_module( 'custom-css', false, false ); 136 193 } 137 194 138 switch_to_blog( BLOG_ID_CURRENT_SITE ); // central.wordcamp.org195 switch_to_blog( get_current_site()->blog_id ); 139 196 140 197 $sites = array(); 141 $wordcamp s = get_posts( array(198 $wordcamp_query = new \WP_Query( array( 142 199 'post_type' => 'wordcamp', 143 200 'post_status' => \WordCamp_Loader::get_public_post_statuses(), 144 'posts_per_page' => 125, // todo temporary workaround until able to add filters to make hundreds of sites manageable201 'posts_per_page' => $options[ 'posts_per_page' ], 145 202 'meta_key' => 'Start Date (YYYY-mm-dd)', 146 203 'orderby' => 'meta_value_num', 147 148 'meta_query' => array( 204 'order' => 'DESC', 205 'paged' => $page, 206 'meta_query' => array( 149 207 array( 150 208 'key' => 'Start Date (YYYY-mm-dd)', 151 209 'value' => strtotime( 'now - 1 month' ), 152 210 'compare' => '<' 153 ) ,211 ) 154 212 ), 155 213 ) ); 156 214 157 foreach( $wordcamps as $wordcamp ) { 158 $site_id = get_wordcamp_site_id( $wordcamp ); 215 //If there are more pages to process, reschedule the cron to run again with the next page 216 if ( $options[ 'schedule_cron' ] && $wordcamp_query->max_num_pages > $page && ! wp_next_scheduled( 'wcsc_prime_sites', [ $page + 1 ] ) ) { 217 wp_schedule_single_event( time() + 10 * MINUTE_IN_SECONDS, 'wcsc_prime_sites', [ $page + 1 ] ); 218 } 219 220 $wordcamps = $wordcamp_query->get_posts(); 221 222 $coming_soon_settings_handler = false; 223 if(class_exists('\WCCSP_Settings')) { 224 $coming_soon_settings_handler = isset( $GLOBALS[ 'WCCSP_Settings' ] ) ? $GLOBALS[ 'WCCSP_Settings' ] : new \WCCSP_Settings(); 225 } 226 227 foreach ( $wordcamps as $wordcamp ) { 228 $site_id = get_wordcamp_site_id( $wordcamp ); 159 229 $site_url = get_post_meta( $wordcamp->ID, 'URL', true ); 230 $start_date = get_post_meta( $wordcamp->ID, 'Start Date (YYYY-mm-dd)', true ); 160 231 161 if ( ! $site_id || ! $site_url ) {232 if ( ! $site_id || ! $site_url || ! $start_date ) { 162 233 continue; 163 234 } 164 235 165 236 switch_to_blog( $site_id ); 166 237 167 $sites[] = array( 168 'site_id' => $site_id, 169 'name' => get_wordcamp_name(), 170 'theme_slug' => get_stylesheet(), 171 'screenshot_url' => get_screenshot_url( $site_url ), 238 //skip any sites with the coming soon enabled. 239 if ( $coming_soon_settings_handler ) { 240 $coming_soon_settings = $coming_soon_settings_handler->get_settings(); 241 if ( isset( $coming_soon_settings[ 'enabled' ] ) && $coming_soon_settings[ 'enabled' ] === 'on' ) { 242 restore_current_blog(); 243 continue; 244 } 245 } 246 247 $preprocessor = \Jetpack_Custom_CSS::get_preprocessor(); 248 249 $sites[ $site_id ] = array( 250 'site_id' => $site_id, 251 'name' => get_wordcamp_name(), 252 'theme_slug' => get_stylesheet(), 253 'screenshot_url' => get_screenshot_url( $site_url ), 254 'year' => date( 'Y', $start_date ), 255 'css_preprocessor' => is_array( $preprocessor ) && isset( $preprocessor[ 'name' ] ) ? $preprocessor[ 'name' ] : 'none' 172 256 ); 173 257 174 258 restore_current_blog(); … … function get_wordcamp_sites() { 176 260 177 261 restore_current_blog(); 178 262 179 set_site_transient( $transient_key, $sites, DAY_IN_SECONDS );180 181 263 return $sites; 182 264 } 183 265 184 266 /** 267 * Daily cron job callback to keep the site list transient valid. This is only run on the main site. 268 * 269 * Since it can be expensive to loop through 100's of sites with switch_to_blog, we're breaking the cron up 270 * into smaller groups and updating a set at a time. This means that users getting the initial load may not 271 * see every site, but since they are ordered by date descending, most applicable sites should be available 272 * immediately if the transient gets flushed. 273 * 274 * The current logic assumes that no sites that have previously made the list of sites will end up getting 275 * removed. If this changes, a separate cron will likely need to be added to prune sites from the list. 276 * 277 */ 278 function prime_wordcamp_sites($page = 1) { 279 $transient_key = 'wcsc_sites'; 280 281 $sites = _get_wordcamp_sites( compact( 'page' ) ); 282 283 $stale_sites = get_site_transient( $transient_key ); 284 285 if ( ! empty( $stale_sites ) ) { 286 //merge the fresh sites with the previously stored sites, overriding old copies with the new 287 $sites = array_merge( $stale_sites, $sites ); 288 289 //sort the sites in descending order by year. More exact dates can be added if the rough order causes confusion. 290 uasort( $sites, function( $site_a, $site_b ) { 291 if ( $site_a[ 'year' ] === $site_b[ 'year' ] ) { 292 return 0; 293 } 294 return ( $site_a[ 'year' ] < $site_b[ 'year' ] ? 1 : -1 ); 295 } ); 296 } 297 298 //set the transient longer than needed to be sure it doesn't expire before the cron runs again. 299 set_site_transient( 'wcsc_sites', $sites, DAY_IN_SECONDS * 2 ); 300 } 301 302 /** 185 303 * Get the mShot URL for the given site URL 186 304 * 187 305 * Allow it to be filtered so that production URLs can be changed to match development URLs in local environments. … … function get_screenshot_url( $site_url ) { 194 312 $screenshot_url = add_query_arg( 'w', 275, 'https://www.wordpress.com/mshots/v1/' . rawurlencode( $site_url ) ); 195 313 196 314 return apply_filters( 'wcsc_site_screenshot_url', $screenshot_url ); 197 } 315 } 316 No newline at end of file