Changeset 4280
- Timestamp:
- 10/21/2016 04:10:02 PM (8 years ago)
- Location:
- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner
- Files:
-
- 2 added
- 2 deleted
- 6 edited
Legend:
- Unmodified
- Added
- Removed
-
sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/site-control.php
r1715 r4280 1 1 <?php 2 2 3 /** 4 * Custom Customizer Control for Search WordCamp sites to clone 5 */ 6 3 7 namespace WordCamp\Site_Cloner; 4 5 8 defined( 'WPINC' ) or die(); 6 9 7 /**8 * Custom Customizer Control for a WordCamp site9 */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 public function __construct( $manager, $id, $args = array() ) { 12 parent::__construct( $manager, $id, $args ); 13 14 $this->capability = 'edit_theme_options'; 15 $this->section = 'wcsc_sites'; 16 } 14 17 15 18 /** … … 17 20 */ 18 21 public function enqueue() { 22 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_view_templates' ) ); 23 19 24 wp_enqueue_style( 'wordcamp-site-cloner' ); 20 25 wp_enqueue_script( 'wordcamp-site-cloner' ); … … 25 30 */ 26 31 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 ); 32 require_once( dirname( __DIR__ ) . '/templates/site-control.php' ); 33 } 34 34 35 require( dirname( __DIR__ ) . '/templates/site-control.php' ); 35 /** 36 * Render the control's Underscores templates 37 */ 38 public function print_view_templates() { 39 require_once( dirname( __DIR__ ) . '/templates/site-option.php' ); 40 require_once( dirname( __DIR__ ) . '/templates/site-filters.php' ); 36 41 } 37 42 } -
sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/includes/source-site-id-setting.php
r1715 r4280 2 2 3 3 namespace WordCamp\Site_Cloner; 4 5 4 defined( 'WPINC' ) or die(); 6 5 … … 16 15 public $default = 0; 17 16 public $sanitize_callback = 'absint'; 17 18 18 protected $preview_source_site_id; 19 19 … … 29 29 add_filter( 'get_post_metadata', array( $this, 'preview_jetpack_postmeta' ), 10, 4 ); 30 30 add_filter( 'safecss_skip_stylesheet', array( $this, 'preview_skip_stylesheet' ) ); 31 32 // Disable the current site's Custom CSS from being output 33 remove_action( 'wp_head', array( 'Jetpack_Custom_CSS', 'link_tag' ), 101 ); 31 34 } 32 35 … … 110 113 * 111 114 * @param int $source_site_id 115 * 116 * @return null 112 117 */ 113 118 protected function update( $source_site_id ) { -
sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/templates/site-control.php
r1715 r4280 1 <?php defined( 'WPINC' ) or die(); ?>1 <?php 2 2 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> 3 /** 4 * Top level template for the output of the Site Cloner Customizer Control 5 */ 7 6 8 <h3 class="wcsc-site-name"> 9 <?php echo esc_html( $this->site_name ); ?> 7 namespace WordCamp\Site_Cloner; 8 defined( 'WPINC' ) or die(); 9 10 ?> 11 12 <div id="wcsc-cloner"> 13 <h3> 14 <?php esc_html_e( 'WordCamp Sites', 'wordcamporg' ); ?> 15 <span id="wcsc-sites-count" class="title-count wcsc-sites-count"></span> 10 16 </h3> 11 17 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> 18 <div class="filters"></div> 19 20 <div class="wcsc-search"> 21 <ul id="wcsc-results"></ul> 22 </div> 15 23 </div> -
sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.css
r1715 r4280 1 .control-section-wcsc-sites { 2 padding: 0 8px; 1 2 .wcsc-filter { 3 margin-bottom: 10px; 3 4 } 4 5 5 #wcsc-sites { 6 overflow: auto; 7 } 8 9 .wcscSite { 6 .wcsc-site { 10 7 position: relative; 11 8 cursor: pointer; 12 9 border: 1px solid #DEDEDE; 13 10 box-shadow: 0 1px 1px -1px rgba( 0, 0, 0, 0.1 ); 11 margin-top: 5px; 14 12 } 15 13 … … 18 16 } 19 17 20 .wcsc Site:hover .wcsc-site-screenshot {18 .wcsc-site:hover .wcsc-site-screenshot { 21 19 opacity: 0.4; 22 20 } 23 21 24 .wcsc-live-preview-label { 22 .wcsc-live-preview-label, 23 .wcsc-previewing-label { 25 24 opacity: 0; 26 25 position: absolute; … … 39 38 } 40 39 41 .wcscSite:hover .wcsc-live-preview-label { 40 .wcsc-site:hover .wcsc-live-preview-label, 41 .wcsc-site .wcsc-previewing-label { 42 42 opacity: 1; 43 43 } -
sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.js
r2933 r4280 1 ( function( wp, $ ) {1 ( function( wp, $, Backbone, win, settings ) { 2 2 'use strict'; 3 3 … … 6 6 } 7 7 8 var api = wp.customize; 8 wp.customize.WordCamp = wp.customize.WordCamp || {}; 9 10 var api = wp.customize, 11 wcsc = api.WordCamp.SiteCloner = { 12 models : {}, 13 views : {}, 14 collections : {}, 15 routers : {}, 16 settings : {} 17 }; 18 19 wcsc.settings = settings || {}; 20 21 // Model for a single site 22 wcsc.models.Site = Backbone.Model.extend( { 23 idAttribute : 'site_id', 24 25 defaults : { 26 'active' : false 27 } 28 } ); 29 30 // Model representing the filter state for searching/filtering sites 31 wcsc.models.SearchFilter = Backbone.Model.extend( { 32 's' : '', 33 'theme_slug' : '', 34 'year' : '', 35 'css_preprocessor' : '' 36 } ); 37 38 // Top level view for the Site Cloner Control 39 wcsc.views.SiteSearch = Backbone.View.extend( { 40 el : '#wcsc-cloner .wcsc-search', 41 42 // Index of the currently viewed page of results 43 page : 0, 44 45 initialize : function( options ) { 46 // Update scroller position 47 _.bindAll( this, 'scroller' ); 48 49 // Container that will be scrolled within 50 this.$container = $( '#wcsc-cloner' ).parents( 'ul.accordion-section-content' ); 51 // Bind scrolling within the container to check for infinite scroll 52 this.$container.bind( 'scroll', _.throttle( this.scroller, 300 ) ); 53 54 // The model and view for filtering the site results 55 this.filterView = new wcsc.views.SearchFilters( { 56 model : this.collection.searchFilter, 57 parent : this 58 } ); 59 60 // View for listing the matching sites 61 this.resultsView = new wcsc.views.SearchResults( { 62 collection : this.collection, 63 parent : this 64 } ); 65 }, 66 67 render : function() { 68 this.filterView.render(); 69 this.resultsView.render(); 70 71 this.$el.empty().append( this.resultsView.el ); 72 }, 73 74 /** 75 * Checks if a user has reached the bottom of the list and triggers a scroll event to show more sites if 76 * needed. 77 */ 78 scroller : function() { 79 var visibleBottom, threshold, elementHeight, containerHeight, scrollTop; 80 81 scrollTop = this.$container.scrollTop(); 82 containerHeight = this.$container.innerHeight(); 83 elementHeight = this.$container.get( 0 ).scrollHeight; 84 85 visibleBottom = scrollTop + containerHeight; 86 threshold = Math.round( elementHeight * 0.9 ); 87 88 if ( visibleBottom > threshold ) { 89 this.trigger( 'wcsc:scroll' ); 90 } 91 } 92 } ); 93 94 // Collection representing the list of cloneable sites 95 wcsc.collections.Sites = Backbone.Collection.extend( { 96 model : wcsc.models.Site, 97 url : wcsc.settings.apiUrl, 98 99 initialize : function( options ) { 100 this.searchFilter = options.searchFilter || {}; 101 102 this.listenTo( this.searchFilter, 'change', this.applyFilter ); 103 }, 104 105 // Filter this collection by the updated searchFilter attributes 106 applyFilter : function() { 107 var filters = this.searchFilter.toJSON(), 108 activeFilters = _.pick( filters, _.identity ), 109 term = '', 110 sites; 111 112 // Nothing actually changed, so don't update the collection 113 if ( _.isEmpty( this.searchFilter.changedAttributes() ) ) { 114 return; 115 } 116 117 // No active filters. Reset to the full list and bail 118 if ( _.isEmpty( activeFilters ) ) { 119 this.resetCanonical(); 120 return; 121 } 122 123 this.resetCanonical( { silent: true } ); 124 125 // Remove the search query restriction since we already filtered by word matches above 126 if ( activeFilters.s ) { 127 term = activeFilters.s; 128 129 delete activeFilters.s; 130 } 131 132 sites = this.where( activeFilters ); 133 134 if ( term ) { 135 sites = this.filterBySearch( sites, term ); 136 } 137 138 this.reset( sites ); 139 }, 140 141 // Internal method for filtering sites by search terms 142 filterBySearch : function( sites, term ) { 143 var match, name; 144 145 // Escape the term string for RegExp meta characters 146 term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); 147 148 // Consider spaces as word delimiters and match the whole string 149 // so matching terms can be combined 150 term = term.replace( / /g, ')(?=.*' ); 151 match = new RegExp( '^(?=.*' + term + ').+', 'i' ); 152 153 return _.filter( sites, function( site ) { 154 name = site.get( 'name' ).replace( /(<([^>]+)>)/ig, '' ); 155 156 return match.test( name ); 157 } ); 158 }, 159 160 paginate : function( pageIndex ) { 161 var collection = this; 162 163 pageIndex = pageIndex || 0; 164 165 collection = _( collection.rest( 20 * pageIndex ) ); 166 collection = _( collection.first( 20 ) ); 167 168 return collection; 169 }, 170 171 // Resets the site collection dataset to the canonical list originally pulled from the api 172 resetCanonical : function( options ) { 173 var activeSite, 174 activeSiteId = api( 'wcsc_source_site_id' ).get(); 175 176 options = options || {}; 177 178 this.reset( wcsc.settings.siteData, options ); 179 180 // Restore the currently active site 181 if ( activeSiteId ) { 182 activeSite = this.find( { site_id : activeSiteId } ); 183 184 if ( 'undefined' !== typeof activeSite ) { 185 activeSite.set( { active: true } ); 186 } 187 } 188 } 189 } ); 190 191 // View for a single site 192 wcsc.views.Site = Backbone.View.extend( { 193 className : 'wcsc-site', 194 html : wp.template( 'wcsc-site-option' ), 195 touchDrag : false, 196 197 attributes : function() { 198 return { 199 'id' : 'wcsc-site-' + this.model.get( 'site_id' ), 200 'data-site-id' : this.model.get( 'site_id' ) 201 } 202 }, 203 204 events : { 205 'click' : 'preview', 206 'keydown' : 'preview', 207 'touchend' : 'preview', 208 'touchmove' : 'preventPreview' 209 }, 210 211 initialize : function( options ) { 212 this.parent = options.parent; 213 214 this.listenTo( this.model, 'change', this.render ); 215 this.render(); 216 }, 217 218 render : function() { 219 this.$el.html( this.html( this.model.toJSON() ) ); 220 }, 221 222 preventPreview : function() { 223 this.touchDrag = true; 224 }, 225 226 preview : function( event ) { 227 event = event || window.event; 228 229 // Ignore touches caused by scrolling 230 if ( this.touchDrag === true ) { 231 this.touchDrag = false; 232 } 233 234 event.preventDefault(); 235 236 this.$el.trigger( 'wcsc:previewSite', this.model ); 237 } 238 } ); 239 240 // View for the site results list 241 wcsc.views.SearchResults = Backbone.View.extend( { 242 className: 'wcsc-results', 243 244 initialize : function( options ) { 245 var self = this; 246 247 this.parent = options.parent; 248 this.$siteCount = $( '#wcsc-sites-count' ); 249 250 // Re-render the view whenever a collection change is complete 251 this.listenTo( this.collection, 'reset', function() { 252 self.parent.page = 0; 253 self.render( this ); 254 } ); 255 256 this.listenTo( this.parent, 'wcsc:scroll', function() { 257 self.renderSites( self.parent.page ); 258 } ); 259 }, 260 261 render : function() { 262 this.$el.empty(); 263 this.renderSites( this.parent.page ); 264 this.$siteCount.text( this.collection.length ); 265 }, 266 267 renderSites : function( page ) { 268 var self = this; 269 270 // Get a collection of just the requested page 271 this.instance = this.collection.paginate( page ); 272 273 if ( this.instance.size() === 0 ) { 274 this.parent.trigger( 'wcsc:end' ); 275 return; 276 } 277 278 this.instance.each( function( site ) { 279 var siteView = new wcsc.views.Site( { 280 model : site, 281 parent : self 282 } ); 283 284 siteView.render(); 285 286 self.$el.append( siteView.el ); 287 } ); 288 289 this.parent.page++; 290 } 291 292 } ); 293 294 // View for the search and dropdown filters 295 wcsc.views.SearchFilters = Backbone.View.extend( { 296 el : '#wcsc-cloner .filters', 297 className : 'wscs-filters', 298 html : wp.template( 'wcsc-site-filters' ), 299 300 events : { 301 "input #wcsc-filter-search-input" : "search", 302 "keyup #wcsc-filter-search-input" : "search", 303 "change select" : "applyFilter" 304 }, 305 306 initialize : function( options ) { 307 this.parent = options.parent; 308 }, 309 310 render : function() { 311 var data = {}; 312 313 data.themeOptions = wcsc.settings.themes; 314 data.yearOptions = _.uniq( this.parent.collection.pluck( 'year' ) ).sort(); 315 data.preprocessorOptions = _.uniq( this.parent.collection.pluck( 'css_preprocessor' ) ).sort(); 316 317 this.$el.html( this.html( data ) ); 318 319 this.$searchInput = $( '#wcsc-filter-search-input' ); 320 this.$themeFilter = $( '#wcsc-filter-theme_slug' ); 321 this.$yearFilter = $( '#wcsc-filter-year' ); 322 this.$preprocessorFilter = $( '#wcsc-filter-css_preprocessor' ); 323 }, 324 325 search : function( event ) { 326 // Clear on escape. 327 if ( event.type === 'keyup' && event.which === 27 ) { 328 event.target.value = ''; 329 } 330 331 /** 332 * Since doSearch is debounced, it will only run when user input comes to a rest 333 */ 334 this.doSearch( event ); 335 }, 336 337 doSearch : _.debounce( function( event ) { 338 this.model.set( 's', event.target.value ); 339 }, 500 ), 340 341 applyFilter : function( event ) { 342 var $target = $( event.target ), 343 value = $target.val(), 344 filter = $target.data( 'filter' ); 345 346 this.model.set( filter, value ); 347 }, 348 349 // Set the inputs to the set of filters as triggered by the router on initial load 350 setInputs : function( filters ) { 351 this.model.set( filters, { silent : true } ); 352 353 this.$searchInput.val( this.model.get( 's' ) ); 354 this.$themeFilter.val( this.model.get( 'theme_slug' ) ); 355 this.$yearFilter.val( this.model.get( 'year' ) ); 356 this.$preprocessorFilter.val( this.model.get( 'css_preprocessor' ) ); 357 358 this.model.trigger( 'change', this.model ); 359 } 360 } ); 9 361 10 362 /** 11 * The Clone Another WordCamp panel 363 * Sets up a listener to store the user's selected filters and search, so that a user's position can be 364 * restored as well as possible after a theme changes causes a full refresh. 12 365 */ 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 */ 366 wcsc.routers.FilterState = Backbone.Router.extend( { 367 routes : { 368 'wcsc?*filters' : 'applyFilters' 369 }, 370 371 initialize : function( options ) { 372 this.parent = options.parent; 373 374 // Any time the collection is reset, we need to update the displayed route 375 this.listenTo( this.parent.view.collection, 'reset', this.updateLocation ); 376 }, 377 378 // Applies the filters set in the query string to the view 379 applyFilters : function( queryString ) { 380 var filters = deserializeQueryString( queryString ); 381 382 this.parent.view.filterView.setInputs( filters ); 383 }, 384 385 updateLocation : function() { 386 var filters = this.parent.view.collection.searchFilter.toJSON(), 387 activeFilters = _.pick( filters, _.identity ), 388 queryString = $.param( activeFilters ); 389 390 this.navigate( 'wcsc?' + queryString ); 391 } 392 } ); 393 394 // Customizer Control wrapping the site search applet 395 api.controlConstructor.wcscSearch = api.Control.extend( { 21 396 ready : function() { 22 var urlParams = getUrlParams( window.location.href ); 23 397 var filter = new wcsc.models.SearchFilter(); // Top level model representing the current filter applied to the collection 398 399 this.siteCollection = new wcsc.collections.Sites( { searchFilter : filter } ); 400 401 // Fill the site collection and setup search when complete 402 this.siteCollection.fetch( { 403 success : this.setupSearch.bind( this ) 404 } ); 405 }, 406 407 // Initialize the site search instance for cloning other sites 408 setupSearch : function() { 409 var currentSite, 410 control = this, 411 urlParams = getUrlParams( win.location.href ); 412 413 // Set a canonical array of all sites prior to filtering 414 wcsc.settings.siteData = this.siteCollection.toJSON(); 415 416 // If the wcsc_source_site_id is set, it;s most likely from a user previewing a site, so bring them back 24 417 if ( urlParams.hasOwnProperty( 'wcsc_source_site_id' ) ) { 25 this.expand();26 api( 'wcsc_source_site_id' ).set( urlParams.wcsc_source_site_id ); 27 }28 } 29 } );30 31 /**32 * Custom control representing a site that can be previewed/imported33 */ 34 api.controlConstructor.wcscSite = api.Control.extend({35 /**36 * Initialize the control after it's loaded37 */ 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 ID47 * to refresh the Previewer with the current theme and the new site's CSS, etc.48 *49 * @param {object} event50 */ 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 ) {418 api.section( this.section() ).expand(); 419 420 currentSite = this.siteCollection.find( { site_id : urlParams.wcsc_source_site_id } ); 421 422 if ( currentSite ) { 423 this.setActiveSite( currentSite ); 424 } 425 } 426 427 $( '#wcsc-cloner' ).on( 'wcsc:previewSite', '.wcsc-site', function( event, site ) { 428 control.previewSite( site ); 429 } ); 430 431 // Setup the top level Site Search View 432 this.view = new wcsc.views.SiteSearch( { 433 parent : this, 434 collection : this.siteCollection 435 } ); 436 437 this.view.render(); 438 439 // Initialize the router to allow state to be restored after a full refresh 440 wcsc.router = new wcsc.routers.FilterState( { parent : this } ); 441 Backbone.history.start(); 442 }, 443 444 previewSite : function( site ) { 445 var queryString, routerFragment; 446 447 if ( api( 'wcsc_source_site_id' ).get() == site.get( 'site_id' ) ) { 448 // We're already previewing this site 56 449 return; 57 450 } 58 451 59 if ( api.settings.theme.stylesheet === previewUrlParams.theme) {60 api( 'wcsc_source_site_id' ).set( previewUrlParams.wcsc_source_site_id);452 if ( api.settings.theme.stylesheet === site.get( 'theme_slug' ) ) { 453 this.setActiveSite( site ); 61 454 } else { 62 window.parent.location = previewUrl; 63 } 455 // We have to do a full refresh when changing themes or other controls won't correlate to the current theme. 456 queryString = $.param( { 457 'theme' : site.get( 'theme_slug' ), 458 'wcsc_source_site_id' : site.get( 'site_id' ) 459 } ); 460 461 routerFragment = Backbone.history.getFragment(); 462 win.parent.location = wcsc.settings.customizerUrl + '?' + queryString + '#' + routerFragment; 463 } 464 }, 465 466 // Set the active site and update the model to reflect the change 467 setActiveSite : function( site ) { 468 var site_id = site.get( 'site_id' ); 469 470 this.siteCollection.each( function( _site ) { 471 _site.set( { active : false } ); 472 } ); 473 474 site.set( { active : true } ); 475 api( 'wcsc_source_site_id' ).set( site.get( 'site_id' ) ); 64 476 } 65 477 } ); … … 75 487 */ 76 488 function getUrlParams( url ) { 77 var match, questionMarkIndex, query, 78 urlParams = {}, 79 pl = /\+/g, // Regex for replacing addition symbol with a space 80 search = /([^&=]+)=?([^&]*)/g, 81 decode = function ( s ) { 82 return decodeURIComponent( s.replace( pl, " " ) ); 83 }; 489 var questionMarkIndex, query, hashIndex; 490 491 // Strip hash first 492 hashIndex = url.indexOf( '#' ); 493 494 if ( hashIndex > -1 ) { 495 url = url.substring( 0, hashIndex ); 496 } 84 497 85 498 questionMarkIndex = url.indexOf( '?' ); 86 499 87 500 if ( -1 === questionMarkIndex ) { 88 return urlParams;501 return {}; 89 502 } else { 90 503 query = url.substring( questionMarkIndex + 1 ); 91 504 } 92 505 93 while ( match = search.exec( query ) ) { 506 return deserializeQueryString( query ); 507 } 508 509 /** 510 * Deserialize a query string into an object 511 * 512 * @param queryString 513 * @returns {object} 514 */ 515 function deserializeQueryString( queryString ) { 516 var match, 517 urlParams = {}, 518 pl = /\+/g, // Regex for replacing addition symbol with a space 519 search = /([^&=]+)=?([^&]*)/g, 520 decode = function( s ) { 521 return decodeURIComponent( s.replace( pl, " " ) ); 522 }; 523 524 while ( match = search.exec( queryString ) ) { 94 525 urlParams[ decode( match[ 1 ] ) ] = decode( match[ 2 ] ); 95 526 } … … 97 528 return urlParams; 98 529 } 99 } )( window.wp, jQuery);530 })( wp, jQuery, Backbone, window, _wcscSettings ); -
sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-site-cloner/wordcamp-site-cloner.php
r2933 r4280 1 1 <?php 2 3 namespace WordCamp\Site_Cloner;4 5 defined( 'WPINC' ) or die();6 2 7 3 /* 8 4 Plugin Name: WordCamp Site Cloner 9 5 Description: Allows organizers to clone another WordCamp's theme and custom CSS as a starting point for their site. 10 Version: 0. 16 Version: 0.2 11 7 Author: WordCamp.org 12 8 Author URI: http://wordcamp.org … … 14 10 */ 15 11 16 // todo if Jetpack_Custom_CSS:get_css is callable, register these, otherwise fatal errors 17 18 add_action( 'plugins_loaded', __NAMESPACE__ . '\get_wordcamp_sites' ); 19 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts' ); 20 add_action( 'admin_menu', __NAMESPACE__ . '\add_submenu_page' ); 21 add_action( 'customize_register', __NAMESPACE__ . '\register_customizer_components' ); 12 namespace WordCamp\Site_Cloner; 13 defined( 'WPINC' ) or die(); 14 15 const PRIME_SITES_CRON_ACTION = 'wcsc_prime_sites'; 16 const WORDCAMP_SITES_TRANSIENT_KEY = 'wcsc_sites'; 17 18 /** 19 * Initialization 20 */ 21 function initialize() { 22 // We rely on the Custom CSS module being available 23 if ( ! class_exists( '\Jetpack' ) ) { 24 return; 25 } 26 27 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\register_scripts' ); 28 add_action( 'admin_menu', __NAMESPACE__ . '\add_submenu_page' ); 29 add_action( 'customize_register', __NAMESPACE__ . '\register_customizer_components' ); 30 add_action( 'rest_api_init', __NAMESPACE__ . '\register_api_endpoints' ); 31 add_action( PRIME_SITES_CRON_ACTION, __NAMESPACE__ . '\prime_wordcamp_sites' ); 32 33 if ( ! wp_next_scheduled( PRIME_SITES_CRON_ACTION ) ) { 34 wp_schedule_event( time(), 'daily', PRIME_SITES_CRON_ACTION ); 35 } 36 } 37 add_action( 'plugins_loaded', __NAMESPACE__ . '\initialize' ); // After Jetpack has loaded 22 38 23 39 /** … … 29 45 plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.css', 30 46 array(), 31 147 2 32 48 ); 33 49 … … 35 51 'wordcamp-site-cloner', 36 52 plugin_dir_url( __FILE__ ) . 'wordcamp-site-cloner.js', 37 array( 'jquery', 'customize-controls' ),38 1,53 array( 'jquery', 'customize-controls', 'wp-backbone' ), 54 2, 39 55 true 40 56 ); 57 58 wp_localize_script( 59 'wordcamp-site-cloner', 60 '_wcscSettings', 61 array( 62 'apiUrl' => get_rest_url( null, '/wordcamp-site-cloner/v1/sites/' ), 63 'customizerUrl' => admin_url( 'customize.php' ), 64 'themes' => get_available_themes(), 65 ) 66 ); 67 } 68 69 /** 70 * Get all of the available themes 71 * 72 * @return array 73 */ 74 function get_available_themes() { 75 /** @var \WP_Theme $theme */ 76 $available_themes = array(); 77 $raw_themes = wp_get_themes( array( 'allowed' => true ) ); 78 79 foreach ( $raw_themes as $theme ) { 80 $theme_name = $theme->display( 'Name' ); 81 $available_themes[] = array( 82 'slug' => $theme->get_stylesheet(), 83 'name' => $theme_name ?: $theme->get_stylesheet() 84 ); 85 } 86 87 return $available_themes; 41 88 } 42 89 … … 53 100 __( 'Clone Another WordCamp', 'wordcamporg' ), 54 101 'switch_themes', 55 'customize.php?autofocus[ panel]=wordcamp_site_cloner'102 'customize.php?autofocus[section]=wcsc_sites' 56 103 ); 57 104 } … … 64 111 function register_customizer_components( $wp_customize ) { 65 112 require_once( __DIR__ . '/includes/source-site-id-setting.php' ); 66 require_once( __DIR__ . '/includes/sites-section.php' );67 113 require_once( __DIR__ . '/includes/site-control.php' ); 68 69 $wp_customize->register_control_type( __NAMESPACE__ . '\Site_Control' );70 114 71 115 $wp_customize->add_setting( new Source_Site_ID_Setting( 72 116 $wp_customize, 73 117 'wcsc_source_site_id', 74 array( )118 array( 'capability' => 'switch_themes' ) 75 119 ) ); 76 120 77 $wp_customize->add_panel( 78 'wordcamp_site_cloner', 79 array( 80 'type' => 'wcscPanel', 81 'title' => __( 'Clone Another WordCamp', 'wordcamporg' ), 82 'description' => __( "Clone another WordCamp's theme and custom CSS as a starting point for your site.", 'wordcamporg' ), 83 ) 84 ); 85 86 $wp_customize->add_section( new Sites_Section( 87 $wp_customize, 121 $wp_customize->add_section( 88 122 'wcsc_sites', 89 123 array( 90 'panel' => 'wordcamp_site_cloner', 91 'title' => __( 'WordCamp Sites', 'wordcamporg' ), 124 'title' => __( 'Clone Another WordCamp', 'wordcamporg' ), 125 'capability' => 'switch_themes' 126 ) 127 ); 128 129 $wp_customize->add_control( new Site_Control( 130 $wp_customize, 131 'wcsc_site_search', 132 array( 133 'type' => 'wcscSearch', 134 'label' => __( 'Search', 'wordcamporg' ), 135 'settings' => 'wcsc_source_site_id', 136 'section' => 'wcsc_sites' 92 137 ) 93 138 ) ); 94 95 foreach( get_wordcamp_sites() as $wordcamp ) { 96 if ( get_current_blog_id() == $wordcamp['site_id'] ) { 97 continue; 98 } 99 100 $wp_customize->add_control( new Site_Control( 101 $wp_customize, 102 'wcsc_site_id_' . $wordcamp['site_id'], 103 array( 104 'type' => 'wcscSite', // todo should be able to set this in control instead of here, but if do that then control contents aren't rendered 105 'site_id' => $wordcamp['site_id'], 106 'site_name' => $wordcamp['name'], 107 'theme_slug' => $wordcamp['theme_slug'], 108 'screenshot_url' => $wordcamp['screenshot_url'], 109 ) 110 ) ); 111 } 112 } 113 114 /** 115 * Get required data for relevant WordCamp sites 116 * 117 * This isn't actually used until register_customizer_components(), but it's called during `plugins_loaded` in 118 * order to prime the cache. That has to be done before `setup_theme`, because the Theme Switcher will override 119 * the current theme when `?theme=` is present in the URL parameters, and it's safer to just avoid that than to 120 * muck with the internals and try to reverse it on the fly. 139 } 140 141 /** 142 * Register the REST API endpoint for the Customizer to use to retriever the site list 143 */ 144 function register_api_endpoints() { 145 if ( ! current_user_can( 'switch_themes' ) ) { 146 return; 147 148 // todo - use `permission_callback` instead 149 } 150 151 register_rest_route( 152 'wordcamp-site-cloner/v1', 153 '/sites', 154 array( 155 'methods' => 'GET', 156 'callback' => __NAMESPACE__ . '\sites_endpoint', 157 ) 158 ); 159 } 160 161 /** 162 * Handle the response for the Sites endpoint 163 * 164 * This always pulls cached data, because Central needs to be the site generating it. See get_wordcamp_sites(). 121 165 * 122 166 * @return array 123 167 */ 168 function sites_endpoint() { 169 $sites = array(); 170 $cached_sites = get_site_transient( WORDCAMP_SITES_TRANSIENT_KEY ); 171 172 if ( $cached_sites ) { 173 unset( $cached_sites[ get_current_blog_id() ] ); 174 175 $sites = array_values( $cached_sites ); 176 } 177 178 return $sites; 179 } 180 181 /** 182 * Prime the cache of cloneable WordCamp sites 183 * 184 * This is called via WP Cron. 185 * 186 * @todo - Reintroduce batching from `1112.3.diff` to get more than 500 sites 187 */ 188 function prime_wordcamp_sites() { 189 // This only needs to run on a single site, then the whole network can use the cached result 190 if ( ! is_main_site() ) { 191 return; 192 } 193 194 // Keep the cache longer than needed, just to be sure that it doesn't expire before the cron job runs again 195 set_site_transient( WORDCAMP_SITES_TRANSIENT_KEY, get_wordcamp_sites(), DAY_IN_SECONDS * 2 ); 196 } 197 198 /** 199 * Get WordCamp sites that are suitable for cloning 200 * 201 * @return array 202 */ 124 203 function get_wordcamp_sites() { 125 require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-loader.php' ); 126 127 // plugins_loaded is runs on every screen, but we only need this when loading the Customizer and Previewer 128 if ( 'customize.php' != basename( $_SERVER['SCRIPT_NAME'] ) && empty( $_REQUEST['wp_customize'] ) ) { 204 /* 205 * The post statuses that \WordCamp_Loader::get_public_post_statuses() returns are only created on Central, 206 * because the plugin isn't active on any other sites. 207 */ 208 if ( ! is_main_site() ) { 129 209 return array(); 130 210 } 131 211 132 $transient_key = 'wcsc_sites'; 133 134 if ( $sites = get_site_transient( $transient_key ) ) { 135 return $sites; 212 if ( ! \Jetpack::is_module_active( 'custom-css' ) ) { 213 \Jetpack::activate_module( 'custom-css', false, false ); 136 214 } 137 215 138 216 switch_to_blog( BLOG_ID_CURRENT_SITE ); // central.wordcamp.org 139 217 140 $sites = array(); 141 $wordcamps = get_posts( array( 142 'post_type' => 'wordcamp', 218 $wordcamp_query = new \WP_Query( array( 219 'post_type' => WCPT_POST_TYPE_ID, 143 220 '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 manageable221 'posts_per_page' => 500, 145 222 'meta_key' => 'Start Date (YYYY-mm-dd)', 146 223 'orderby' => 'meta_value_num', 224 'order' => 'DESC', 147 225 148 226 'meta_query' => array( 149 227 array( 228 // New sites won't have finished designs, so ignore them 150 229 'key' => 'Start Date (YYYY-mm-dd)', 151 230 'value' => strtotime( 'now - 1 month' ), 152 231 'compare' => '<' 153 ) ,232 ) 154 233 ), 155 234 ) ); 156 235 157 foreach( $wordcamps as $wordcamp ) { 158 $site_id = get_wordcamp_site_id( $wordcamp ); 159 $site_url = get_post_meta( $wordcamp->ID, 'URL', true ); 160 161 if ( ! $site_id || ! $site_url ) { 236 $sites = get_filtered_wordcamp_sites( $wordcamp_query->get_posts() ); 237 238 uasort( $sites, __NAMESPACE__ . '\sort_sites_by_year' ); 239 240 restore_current_blog(); 241 242 return $sites; 243 } 244 245 /** 246 * Filter out sites that aren't relevant to the Cloner 247 * 248 * @param array $wordcamps 249 * 250 * @return array 251 */ 252 function get_filtered_wordcamp_sites( $wordcamps ) { 253 $sites = array(); 254 255 foreach ( $wordcamps as $wordcamp ) { 256 $site_id = get_wordcamp_site_id( $wordcamp ); 257 $site_url = get_post_meta( $wordcamp->ID, 'URL', true ); 258 $start_date = get_post_meta( $wordcamp->ID, 'Start Date (YYYY-mm-dd)', true ); 259 260 if ( ! $site_id || ! $site_url || ! $start_date ) { 162 261 continue; 163 262 } … … 165 264 switch_to_blog( $site_id ); 166 265 167 $sites[] = array( 168 'site_id' => $site_id, 169 'name' => get_wordcamp_name(), 170 'theme_slug' => get_stylesheet(), 171 'screenshot_url' => get_screenshot_url( $site_url ), 172 ); 266 /* 267 * Sites with Coming Soon enabled probably don't have a finished design yet, so there's no point in 268 * cloning it. 269 */ 270 if ( ! coming_soon_plugin_enabled() ) { 271 $preprocessor = \Jetpack_Custom_CSS::get_preprocessor(); 272 $preprocessor = isset( $preprocessor[ 'name' ] ) ? $preprocessor[ 'name' ] : 'none'; 273 274 $sites[ $site_id ] = array( 275 'site_id' => $site_id, 276 'name' => get_wordcamp_name(), 277 'theme_slug' => get_stylesheet(), 278 'screenshot_url' => get_screenshot_url( $site_url ), 279 'year' => date( 'Y', $start_date ), 280 'css_preprocessor' => $preprocessor, 281 ); 282 } 173 283 174 284 restore_current_blog(); 175 285 } 176 286 177 restore_current_blog();178 179 set_site_transient( $transient_key, $sites, DAY_IN_SECONDS );180 181 287 return $sites; 288 } 289 290 /** 291 * Determine if the Coming Soon plugin is enabled for the current site 292 * 293 * @return bool 294 */ 295 function coming_soon_plugin_enabled() { 296 global $WCCSP_Settings; 297 $enabled = false; 298 299 if ( ! is_callable( 'WCCSP_Settings::get_settings' ) ) { 300 return $enabled; 301 } 302 303 // We may need to instantiate the class if this is the first time calling this function 304 if ( ! is_a( $WCCSP_Settings, 'WCCSP_Settings' ) ) { 305 $WCCSP_Settings = new \WCCSP_Settings(); 306 } 307 308 $settings = $WCCSP_Settings->get_settings(); 309 310 if ( isset( $settings[ 'enabled' ] ) && 'on' === $settings[ 'enabled' ] ) { 311 $enabled = true; 312 } 313 314 return $enabled; 182 315 } 183 316 … … 196 329 return apply_filters( 'wcsc_site_screenshot_url', $screenshot_url ); 197 330 } 331 332 /** 333 * Sort arrays by the year 334 * 335 * @param array $site_a 336 * @param array $site_b 337 * 338 * @return int 339 */ 340 function sort_sites_by_year( $site_a, $site_b ) { 341 if ( $site_a[ 'year' ] === $site_b[ 'year' ] ) { 342 return 0; 343 } 344 345 return ( $site_a[ 'year' ] < $site_b[ 'year' ] ? 1 : -1 ); 346 }
Note: See TracChangeset
for help on using the changeset viewer.