├── .gitmodules ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── core-patched └── wp-admin │ └── js │ └── widgets.js ├── css ├── customize-widgets.css └── post-edit.css ├── customize-widgets-plus.php ├── instance.php ├── js ├── base.js ├── deferred-customize-widgets.js ├── https-resource-proxy.js ├── widget-number-incrementing-customizer.js └── widget-number-incrementing.js ├── php ├── class-base-test-case.php ├── class-deferred-customize-widgets.php ├── class-exception.php ├── class-https-resource-proxy.php ├── class-non-autoloaded-widget-options.php ├── class-optimized-widget-registration.php ├── class-plugin-base.php ├── class-plugin.php ├── class-widget-number-incrementing.php ├── class-widget-posts-cli-command.php ├── class-widget-posts.php ├── class-widget-settings.php └── class-wp-customize-widget-setting.php ├── phpunit.xml ├── readme.md ├── readme.txt ├── svn-url └── tests ├── data └── class-acme-widget.php ├── test-class-https-resource-proxy.php ├── test-class-non-autoloaded-widget-options.php ├── test-class-optimized-widget-registration.php ├── test-class-plugin.php ├── test-class-widget-number-incrementing.php ├── test-class-widget-posts-with-customizer.php ├── test-class-widget-posts.php ├── test-class-widget-settings.php ├── test-class-wp-customize-widget-setting.php ├── test-core-customize-widgets-with-widget-posts.php └── test-core-widgets-with-widget-posts.php /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dev-lib"] 2 | path = dev-lib 3 | url = https://github.com/xwp/wp-dev-lib.git 4 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "wordpress", 3 | "requireCamelCaseOrUpperCaseIdentifiers": false, 4 | "excludeFiles": [ 5 | "**/vendor/**", 6 | "**.min.js", 7 | "**/node_modules/**", 8 | "core-patched/wp-admin/js/widgets.js" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | core-patched/wp-admin/js/widgets.js 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | dev-lib/.jshintrc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: 4 | - php 5 | - node_js 6 | 7 | php: 8 | - 5.4 9 | - 7.0 10 | 11 | node_js: 12 | - 0.10 13 | 14 | env: 15 | - WP_VERSION=trunk WP_MULTISITE=1 16 | - WP_VERSION=trunk WP_MULTISITE=0 17 | 18 | branches: 19 | only: 20 | - master 21 | 22 | install: 23 | - export DEV_LIB_PATH=dev-lib 24 | - if [ ! -e "$DEV_LIB_PATH" ] && [ -L .travis.yml ]; then export DEV_LIB_PATH=$( dirname $( readlink .travis.yml ) ); fi 25 | - if [ ! -e "$DEV_LIB_PATH" ]; then git clone https://github.com/xwp/wp-dev-lib.git $DEV_LIB_PATH; fi 26 | - source $DEV_LIB_PATH/travis.install.sh 27 | 28 | script: 29 | - source $DEV_LIB_PATH/travis.script.sh 30 | 31 | after_script: 32 | - source $DEV_LIB_PATH/travis.after_script.sh 33 | -------------------------------------------------------------------------------- /core-patched/wp-admin/js/widgets.js: -------------------------------------------------------------------------------- 1 | /*global ajaxurl, isRtl, wpCustomizeWidgetsPlus, console */ 2 | var wpWidgets; 3 | (function($) { 4 | var $document = $( document ); 5 | 6 | wpWidgets = { 7 | /** 8 | * A closed Sidebar that gets a Widget dragged over it. 9 | * 10 | * @var element|null 11 | */ 12 | hoveredSidebar: null, 13 | 14 | init : function() { 15 | var rem, the_id, 16 | self = this, 17 | chooser = $('.widgets-chooser'), 18 | selectSidebar = chooser.find('.widgets-chooser-sidebars'), 19 | sidebars = $('div.widgets-sortables'), 20 | isRTL = !! ( 'undefined' !== typeof isRtl && isRtl ); 21 | 22 | $('#widgets-right .sidebar-name').click( function() { 23 | var $this = $(this), 24 | $wrap = $this.closest('.widgets-holder-wrap'); 25 | 26 | if ( $wrap.hasClass('closed') ) { 27 | $wrap.removeClass('closed'); 28 | $this.parent().sortable('refresh'); 29 | } else { 30 | $wrap.addClass('closed'); 31 | } 32 | 33 | $document.triggerHandler( 'wp-pin-menu' ); 34 | }); 35 | 36 | $('#widgets-left .sidebar-name').click( function() { 37 | $(this).closest('.widgets-holder-wrap').toggleClass('closed'); 38 | $document.triggerHandler( 'wp-pin-menu' ); 39 | }); 40 | 41 | $(document.body).bind('click.widgets-toggle', function(e) { 42 | var target = $(e.target), 43 | css = { 'z-index': 100 }, 44 | widget, inside, targetWidth, widgetWidth, margin; 45 | 46 | if ( target.parents('.widget-top').length && ! target.parents('#available-widgets').length ) { 47 | widget = target.closest('div.widget'); 48 | inside = widget.children('.widget-inside'); 49 | targetWidth = parseInt( widget.find('input.widget-width').val(), 10 ), 50 | widgetWidth = widget.parent().width(); 51 | 52 | if ( inside.is(':hidden') ) { 53 | if ( targetWidth > 250 && ( targetWidth + 30 > widgetWidth ) && widget.closest('div.widgets-sortables').length ) { 54 | if ( widget.closest('div.widget-liquid-right').length ) { 55 | margin = isRTL ? 'margin-right' : 'margin-left'; 56 | } else { 57 | margin = isRTL ? 'margin-left' : 'margin-right'; 58 | } 59 | 60 | css[ margin ] = widgetWidth - ( targetWidth + 30 ) + 'px'; 61 | widget.css( css ); 62 | } 63 | widget.addClass( 'open' ); 64 | inside.slideDown('fast'); 65 | } else { 66 | inside.slideUp('fast', function() { 67 | widget.attr( 'style', '' ); 68 | widget.removeClass( 'open' ); 69 | }); 70 | } 71 | e.preventDefault(); 72 | } else if ( target.hasClass('widget-control-save') ) { 73 | wpWidgets.save( target.closest('div.widget'), 0, 1, 0 ); 74 | e.preventDefault(); 75 | } else if ( target.hasClass('widget-control-remove') ) { 76 | wpWidgets.save( target.closest('div.widget'), 1, 1, 0 ); 77 | e.preventDefault(); 78 | } else if ( target.hasClass('widget-control-close') ) { 79 | widget = target.closest('div.widget'); 80 | widget.removeClass( 'open' ); 81 | wpWidgets.close( widget ); 82 | e.preventDefault(); 83 | } else if ( target.attr( 'id' ) === 'inactive-widgets-control-remove' ) { 84 | wpWidgets.removeInactiveWidgets(); 85 | e.preventDefault(); 86 | } 87 | }); 88 | 89 | sidebars.children('.widget').each( function() { 90 | var $this = $(this); 91 | 92 | wpWidgets.appendTitle( this ); 93 | 94 | if ( $this.find( 'p.widget-error' ).length ) { 95 | $this.find( 'a.widget-action' ).trigger('click'); 96 | } 97 | }); 98 | 99 | $('#widget-list').children('.widget').draggable({ 100 | connectToSortable: 'div.widgets-sortables', 101 | handle: '> .widget-top > .widget-title', 102 | distance: 2, 103 | helper: 'clone', 104 | zIndex: 100, 105 | containment: '#wpwrap', 106 | refreshPositions: true, 107 | start: function( event, ui ) { 108 | var chooser = $(this).find('.widgets-chooser'); 109 | 110 | ui.helper.find('div.widget-description').hide(); 111 | the_id = this.id; 112 | 113 | if ( chooser.length ) { 114 | // Hide the chooser and move it out of the widget 115 | $( '#wpbody-content' ).append( chooser.hide() ); 116 | // Delete the cloned chooser from the drag helper 117 | ui.helper.find('.widgets-chooser').remove(); 118 | self.clearWidgetSelection(); 119 | } 120 | }, 121 | stop: function() { 122 | if ( rem ) { 123 | $(rem).hide(); 124 | } 125 | 126 | rem = ''; 127 | } 128 | }); 129 | 130 | /** 131 | * Opens and closes previously closed Sidebars when Widgets are dragged over/out of them. 132 | */ 133 | sidebars.droppable( { 134 | tolerance: 'intersect', 135 | 136 | /** 137 | * Open Sidebar when a Widget gets dragged over it. 138 | * 139 | * @param event 140 | */ 141 | over: function( event ) { 142 | var $wrap = $( event.target ).parent(); 143 | 144 | if ( wpWidgets.hoveredSidebar && ! $wrap.is( wpWidgets.hoveredSidebar ) ) { 145 | // Close the previous Sidebar as the Widget has been dragged onto another Sidebar. 146 | wpWidgets.closeSidebar( event ); 147 | } 148 | 149 | if ( $wrap.hasClass( 'closed' ) ) { 150 | wpWidgets.hoveredSidebar = $wrap; 151 | $wrap.removeClass( 'closed' ); 152 | } 153 | 154 | $( this ).sortable( 'refresh' ); 155 | }, 156 | 157 | /** 158 | * Close Sidebar when the Widget gets dragged out of it. 159 | * 160 | * @param event 161 | */ 162 | out: function( event ) { 163 | if ( wpWidgets.hoveredSidebar ) { 164 | wpWidgets.closeSidebar( event ); 165 | } 166 | } 167 | } ); 168 | 169 | sidebars.sortable({ 170 | placeholder: 'widget-placeholder', 171 | items: '> .widget', 172 | handle: '> .widget-top > .widget-title', 173 | cursor: 'move', 174 | distance: 2, 175 | containment: '#wpwrap', 176 | tolerance: 'pointer', 177 | refreshPositions: true, 178 | start: function( event, ui ) { 179 | var height, $this = $(this), 180 | $wrap = $this.parent(), 181 | inside = ui.item.children('.widget-inside'); 182 | 183 | if ( inside.css('display') === 'block' ) { 184 | ui.item.removeClass('open'); 185 | inside.hide(); 186 | $(this).sortable('refreshPositions'); 187 | } 188 | 189 | if ( ! $wrap.hasClass('closed') ) { 190 | // Lock all open sidebars min-height when starting to drag. 191 | // Prevents jumping when dragging a widget from an open sidebar to a closed sidebar below. 192 | height = ui.item.hasClass('ui-draggable') ? $this.height() : 1 + $this.height(); 193 | $this.css( 'min-height', height + 'px' ); 194 | } 195 | }, 196 | 197 | stop: function( event, ui ) { 198 | var addNew, $sidebar, $children, child, item, 199 | $widget = ui.item, 200 | id = the_id, 201 | continuation, 202 | incrWidgetNumberRequest; 203 | 204 | // Reset the var to hold a previously closed sidebar. 205 | wpWidgets.hoveredSidebar = null; 206 | 207 | if ( $widget.hasClass('deleting') ) { 208 | wpWidgets.save( $widget, 1, 0, 1 ); // delete widget 209 | $widget.remove(); 210 | return; 211 | } 212 | 213 | addNew = $widget.find('input.add_new').val(); 214 | 215 | $widget.attr( 'style', '' ).removeClass('ui-draggable'); 216 | the_id = ''; 217 | 218 | continuation = function ( widgetNumber ) { 219 | if ( addNew ) { 220 | if ( 'multi' === addNew ) { 221 | $widget.html( 222 | $widget.html().replace( /<[^<>]+>/g, function ( tag ) { 223 | return tag.replace( /__i__|%i%/g, widgetNumber ); 224 | } ) 225 | ); 226 | 227 | $widget.attr( 'id', id.replace( '__i__', widgetNumber ) ); 228 | $widget.find( 'input[name=widget_number]:first' ).val( widgetNumber ); 229 | $widget.find( 'input[name=multi_number]:first' ).val( widgetNumber ); 230 | } else if ( 'single' === addNew ) { 231 | $widget.attr( 'id', 'new-' + id ); 232 | rem = 'div#' + id; 233 | } 234 | 235 | wpWidgets.save( $widget, 0, 0, 1 ); 236 | $widget.find('input.add_new').val(''); 237 | $document.trigger( 'widget-added', [ $widget ] ); 238 | } 239 | 240 | $sidebar = $widget.parent(); 241 | 242 | if ( $sidebar.parent().hasClass( 'closed' ) ) { 243 | $sidebar.parent().removeClass( 'closed' ); 244 | $children = $sidebar.children( '.widget' ); 245 | 246 | // Make sure the dropped widget is at the top 247 | if ( $children.length > 1 ) { 248 | child = $children.get( 0 ); 249 | item = $widget.get( 0 ); 250 | 251 | if ( child.id && item.id && child.id !== item.id ) { 252 | $( child ).before( $widget ); 253 | } 254 | } 255 | } 256 | 257 | if ( addNew ) { 258 | $widget.find( 'a.widget-action' ).trigger( 'click' ); 259 | } else { 260 | wpWidgets.saveOrder( $sidebar.attr( 'id' ) ); 261 | } 262 | }; 263 | 264 | if ( 'multi' !== addNew || 'undefined' === typeof wpCustomizeWidgetsPlus ) { 265 | continuation(); 266 | return; 267 | } 268 | 269 | incrWidgetNumberRequest = wp.ajax.post( wpCustomizeWidgetsPlus.widgetNumberIncrementing.action, { 270 | nonce: wpCustomizeWidgetsPlus.widgetNumberIncrementing.nonce, 271 | idBase: $widget.find( 'input.id_base' ).val() 272 | } ); 273 | 274 | incrWidgetNumberRequest.done( function( res ) { 275 | continuation( res.number ); 276 | } ); 277 | 278 | incrWidgetNumberRequest.fail( function( res ) { 279 | var errorCode; 280 | if ( '0' === res ) { 281 | errorCode = 'not_logged_in'; 282 | } else if ( '-1' === res ) { 283 | errorCode = 'invalid_nonce'; 284 | } else if ( res && res.message ) { 285 | errorCode = res.message; 286 | } else { 287 | errorCode = 'unknown'; 288 | } 289 | console.error( 'Add widget failure: ' + errorCode ); 290 | } ); 291 | }, 292 | 293 | activate: function() { 294 | $(this).parent().addClass( 'widget-hover' ); 295 | }, 296 | 297 | deactivate: function() { 298 | // Remove all min-height added on "start" 299 | $(this).css( 'min-height', '' ).parent().removeClass( 'widget-hover' ); 300 | }, 301 | 302 | receive: function( event, ui ) { 303 | var $sender = $( ui.sender ); 304 | 305 | // Don't add more widgets to orphaned sidebars 306 | if ( this.id.indexOf('orphaned_widgets') > -1 ) { 307 | $sender.sortable('cancel'); 308 | return; 309 | } 310 | 311 | // If the last widget was moved out of an orphaned sidebar, close and remove it. 312 | if ( $sender.attr('id').indexOf('orphaned_widgets') > -1 && ! $sender.children('.widget').length ) { 313 | $sender.parents('.orphan-sidebar').slideUp( 400, function(){ $(this).remove(); } ); 314 | } 315 | } 316 | }).sortable( 'option', 'connectWith', 'div.widgets-sortables' ); 317 | 318 | $('#available-widgets').droppable({ 319 | tolerance: 'pointer', 320 | accept: function(o){ 321 | return $(o).parent().attr('id') !== 'widget-list'; 322 | }, 323 | drop: function(e,ui) { 324 | ui.draggable.addClass('deleting'); 325 | $('#removing-widget').hide().children('span').empty(); 326 | }, 327 | over: function(e,ui) { 328 | ui.draggable.addClass('deleting'); 329 | $('div.widget-placeholder').hide(); 330 | 331 | if ( ui.draggable.hasClass('ui-sortable-helper') ) { 332 | $('#removing-widget').show().children('span') 333 | .html( ui.draggable.find( 'div.widget-title' ).children( 'h3' ).html() ); 334 | } 335 | }, 336 | out: function(e,ui) { 337 | ui.draggable.removeClass('deleting'); 338 | $('div.widget-placeholder').show(); 339 | $('#removing-widget').hide().children('span').empty(); 340 | } 341 | }); 342 | 343 | // Area Chooser 344 | $( '#widgets-right .widgets-holder-wrap' ).each( function( index, element ) { 345 | var $element = $( element ), 346 | name = $element.find( '.sidebar-name h2' ).text(), 347 | id = $element.find( '.widgets-sortables' ).attr( 'id' ), 348 | li = $('
  • ').text( $.trim( name ) ); 349 | 350 | if ( index === 0 ) { 351 | li.addClass( 'widgets-chooser-selected' ); 352 | } 353 | 354 | selectSidebar.append( li ); 355 | li.data( 'sidebarId', id ); 356 | }); 357 | 358 | $( '#available-widgets .widget .widget-title' ).on( 'click.widgets-chooser', function() { 359 | var $widget = $(this).closest( '.widget' ); 360 | 361 | if ( $widget.hasClass( 'widget-in-question' ) || $( '#widgets-left' ).hasClass( 'chooser' ) ) { 362 | self.closeChooser(); 363 | } else { 364 | // Open the chooser 365 | self.clearWidgetSelection(); 366 | $( '#widgets-left' ).addClass( 'chooser' ); 367 | $widget.addClass( 'widget-in-question' ).children( '.widget-description' ).after( chooser ); 368 | 369 | chooser.slideDown( 300, function() { 370 | selectSidebar.find('.widgets-chooser-selected').focus(); 371 | }); 372 | 373 | selectSidebar.find( 'li' ).on( 'focusin.widgets-chooser', function() { 374 | selectSidebar.find('.widgets-chooser-selected').removeClass( 'widgets-chooser-selected' ); 375 | $(this).addClass( 'widgets-chooser-selected' ); 376 | } ); 377 | } 378 | }); 379 | 380 | // Add event handlers 381 | chooser.on( 'click.widgets-chooser', function( event ) { 382 | var $target = $( event.target ); 383 | 384 | if ( $target.hasClass('button-primary') ) { 385 | self.addWidget( chooser ); 386 | self.closeChooser(); 387 | } else if ( $target.hasClass('button-secondary') ) { 388 | self.closeChooser(); 389 | } 390 | }).on( 'keyup.widgets-chooser', function( event ) { 391 | if ( event.which === $.ui.keyCode.ENTER ) { 392 | if ( $( event.target ).hasClass('button-secondary') ) { 393 | // Close instead of adding when pressing Enter on the Cancel button 394 | self.closeChooser(); 395 | } else { 396 | self.addWidget( chooser ); 397 | self.closeChooser(); 398 | } 399 | } else if ( event.which === $.ui.keyCode.ESCAPE ) { 400 | self.closeChooser(); 401 | } 402 | }); 403 | }, 404 | 405 | saveOrder : function( sidebarId ) { 406 | var data = { 407 | action: 'widgets-order', 408 | savewidgets: $('#_wpnonce_widgets').val(), 409 | sidebars: [] 410 | }; 411 | 412 | if ( sidebarId ) { 413 | $( '#' + sidebarId ).find( '.spinner:first' ).addClass( 'is-active' ); 414 | } 415 | 416 | $('div.widgets-sortables').each( function() { 417 | if ( $(this).sortable ) { 418 | data['sidebars[' + $(this).attr('id') + ']'] = $(this).sortable('toArray').join(','); 419 | } 420 | }); 421 | 422 | $.post( ajaxurl, data, function() { 423 | $( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length ); 424 | $( '.spinner' ).removeClass( 'is-active' ); 425 | }); 426 | }, 427 | 428 | save : function( widget, del, animate, order ) { 429 | var sidebarId = widget.closest('div.widgets-sortables').attr('id'), 430 | data = widget.find('form').serialize(), a; 431 | 432 | widget = $(widget); 433 | $( '.spinner', widget ).addClass( 'is-active' ); 434 | 435 | a = { 436 | action: 'save-widget', 437 | savewidgets: $('#_wpnonce_widgets').val(), 438 | sidebar: sidebarId 439 | }; 440 | 441 | if ( del ) { 442 | a.delete_widget = 1; 443 | } 444 | 445 | data += '&' + $.param(a); 446 | 447 | $.post( ajaxurl, data, function(r) { 448 | var id; 449 | 450 | if ( del ) { 451 | if ( ! $('input.widget_number', widget).val() ) { 452 | id = $('input.widget-id', widget).val(); 453 | $('#available-widgets').find('input.widget-id').each(function(){ 454 | if ( $(this).val() === id ) { 455 | $(this).closest('div.widget').show(); 456 | } 457 | }); 458 | } 459 | 460 | if ( animate ) { 461 | order = 0; 462 | widget.slideUp('fast', function(){ 463 | $(this).remove(); 464 | wpWidgets.saveOrder(); 465 | }); 466 | } else { 467 | widget.remove(); 468 | 469 | if ( sidebarId === 'wp_inactive_widgets' ) { 470 | $( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length ); 471 | } 472 | } 473 | } else { 474 | $( '.spinner' ).removeClass( 'is-active' ); 475 | if ( r && r.length > 2 ) { 476 | $( 'div.widget-content', widget ).html( r ); 477 | wpWidgets.appendTitle( widget ); 478 | $document.trigger( 'widget-updated', [ widget ] ); 479 | 480 | if ( sidebarId === 'wp_inactive_widgets' ) { 481 | $( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length ); 482 | } 483 | } 484 | } 485 | 486 | if ( order ) { 487 | wpWidgets.saveOrder(); 488 | } 489 | }); 490 | }, 491 | 492 | removeInactiveWidgets : function() { 493 | var $element = $( '.remove-inactive-widgets' ), a, data; 494 | 495 | $( '.spinner', $element ).addClass( 'is-active' ); 496 | 497 | a = { 498 | action : 'delete-inactive-widgets', 499 | removeinactivewidgets : $( '#_wpnonce_remove_inactive_widgets' ).val() 500 | }; 501 | 502 | data = $.param( a ); 503 | 504 | $.post( ajaxurl, data, function() { 505 | $( '#wp_inactive_widgets .widget' ).remove(); 506 | $( '#inactive-widgets-control-remove' ).prop( 'disabled' , true ); 507 | $( '.spinner', $element ).removeClass( 'is-active' ); 508 | } ); 509 | }, 510 | 511 | appendTitle : function(widget) { 512 | var title = $('input[id*="-title"]', widget).val() || ''; 513 | 514 | if ( title ) { 515 | title = ': ' + title.replace(/<[^<>]+>/g, '').replace(//g, '>'); 516 | } 517 | 518 | $(widget).children('.widget-top').children('.widget-title').children() 519 | .children('.in-widget-title').html(title); 520 | 521 | }, 522 | 523 | close : function(widget) { 524 | widget.children('.widget-inside').slideUp('fast', function() { 525 | widget.attr( 'style', '' ); 526 | }); 527 | }, 528 | 529 | addWidget: function( chooser ) { 530 | var widget, widgetId, add, n, viewportTop, viewportBottom, sidebarBounds, 531 | sidebarId = chooser.find( '.widgets-chooser-selected' ).data('sidebarId'), 532 | sidebar = $( '#' + sidebarId ), 533 | continuation, 534 | incrWidgetNumberRequest, 535 | idBase; 536 | 537 | widget = $('#available-widgets').find('.widget-in-question').clone(); 538 | widgetId = widget.attr('id'); 539 | idBase = widget.find( 'input.id_base' ).val(); 540 | add = widget.find( 'input.add_new' ).val(); 541 | n = widget.find( 'input.multi_number' ).val(); // @todo Multi number obtained here 542 | 543 | // Remove the cloned chooser from the widget 544 | widget.find('.widgets-chooser').remove(); 545 | 546 | continuation = function ( n ) { 547 | if ( 'multi' === add ) { 548 | widget.html( 549 | widget.html().replace( /<[^<>]+>/g, function(m) { 550 | return m.replace( /__i__|%i%/g, n ); 551 | }) 552 | ); 553 | 554 | widget.attr( 'id', widgetId.replace( '__i__', n ) ); 555 | widget.find( 'input[name=widget_number]:first' ).val( n ); 556 | widget.find( 'input[name=multi_number]:first' ).val( n ); 557 | } else if ( 'single' === add ) { 558 | widget.attr( 'id', 'new-' + widgetId ); 559 | $( '#' + widgetId ).hide(); 560 | } 561 | 562 | // Open the widgets container 563 | sidebar.closest( '.widgets-holder-wrap' ).removeClass('closed'); 564 | 565 | sidebar.append( widget ); 566 | sidebar.sortable('refresh'); 567 | 568 | wpWidgets.save( widget, 0, 0, 1 ); 569 | // No longer "new" widget 570 | widget.find( 'input.add_new' ).val(''); 571 | 572 | $document.trigger( 'widget-added', [ widget ] ); 573 | 574 | /* 575 | * Check if any part of the sidebar is visible in the viewport. If it is, don't scroll. 576 | * Otherwise, scroll up to so the sidebar is in view. 577 | * 578 | * We do this by comparing the top and bottom, of the sidebar so see if they are within 579 | * the bounds of the viewport. 580 | */ 581 | viewportTop = $(window).scrollTop(); 582 | viewportBottom = viewportTop + $(window).height(); 583 | sidebarBounds = sidebar.offset(); 584 | 585 | sidebarBounds.bottom = sidebarBounds.top + sidebar.outerHeight(); 586 | 587 | if ( viewportTop > sidebarBounds.bottom || viewportBottom < sidebarBounds.top ) { 588 | $( 'html, body' ).animate({ 589 | scrollTop: sidebarBounds.top - 130 590 | }, 200 ); 591 | } 592 | 593 | window.setTimeout( function() { 594 | // Cannot use a callback in the animation above as it fires twice, 595 | // have to queue this "by hand". 596 | widget.find( '.widget-title' ).trigger('click'); 597 | }, 250 ); 598 | }; 599 | 600 | if ( 'multi' !== add || 'undefined' === typeof wpCustomizeWidgetsPlus ) { 601 | continuation( n ); 602 | return; 603 | } 604 | 605 | incrWidgetNumberRequest = wp.ajax.post( wpCustomizeWidgetsPlus.widgetNumberIncrementing.action, { 606 | nonce: wpCustomizeWidgetsPlus.widgetNumberIncrementing.nonce, 607 | idBase: idBase 608 | } ); 609 | 610 | incrWidgetNumberRequest.done( function( res ) { 611 | continuation( res.number ); 612 | } ); 613 | 614 | incrWidgetNumberRequest.fail( function( res ) { 615 | var errorCode, errorMessage; 616 | if ( '0' === res ) { 617 | errorCode = 'not_logged_in'; 618 | } else if ( '-1' === res ) { 619 | errorCode = 'invalid_nonce'; 620 | } else if ( res && res.message ) { 621 | errorCode = res.message; 622 | } else { 623 | errorCode = 'unknown'; 624 | } 625 | errorMessage = 'Failed request count: ' + wpCustomizeWidgetsPlus.widgetNumberIncrementing.retryCount + '.'; 626 | errorMessage += ' Last error code: ' + errorCode; 627 | console.error( errorMessage ); 628 | } ); 629 | }, 630 | 631 | closeChooser: function() { 632 | var self = this; 633 | 634 | $( '.widgets-chooser' ).slideUp( 200, function() { 635 | $( '#wpbody-content' ).append( this ); 636 | self.clearWidgetSelection(); 637 | }); 638 | }, 639 | 640 | clearWidgetSelection: function() { 641 | $( '#widgets-left' ).removeClass( 'chooser' ); 642 | $( '.widget-in-question' ).removeClass( 'widget-in-question' ); 643 | }, 644 | 645 | /** 646 | * Closes a Sidebar that was previously closed, but opened by dragging a Widget over it. 647 | * 648 | * Used when a Widget gets dragged in/out of the Sidebar and never dropped. 649 | * 650 | * @param sidebar 651 | */ 652 | closeSidebar: function( sidebar ) { 653 | this.hoveredSidebar.addClass( 'closed' ); 654 | $( sidebar.target ).css( 'min-height', '' ); 655 | this.hoveredSidebar = null; 656 | } 657 | }; 658 | 659 | $document.ready( function(){ wpWidgets.init(); } ); 660 | 661 | })(jQuery); 662 | -------------------------------------------------------------------------------- /css/customize-widgets.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Add progress indicator to widget form controls while widget is being updated 4 | * and the preview is being refreshed since the spinner at the bottom may be 5 | * out of view. 6 | */ 7 | .customize-control-widget_form.widget-form-loading, 8 | .customize-control-widget_form.previewer-loading { 9 | cursor: progress; 10 | } 11 | -------------------------------------------------------------------------------- /css/post-edit.css: -------------------------------------------------------------------------------- 1 | body.post-type-widget_instance .add-new-h2, 2 | body.post-type-widget_instance #minor-publishing-actions, 3 | body.post-type-widget_instance #publishing-action, 4 | body.post-type-widget_instance .misc-pub-section.misc-pub-post-status, 5 | body.post-type-widget_instance .misc-pub-section.misc-pub-visibility, 6 | body.post-type-widget_instance .misc-pub-section.curtime.misc-pub-curtime, 7 | body.post-type-widget_instance #delete-action { 8 | display: none; 9 | } 10 | -------------------------------------------------------------------------------- /customize-widgets-plus.php: -------------------------------------------------------------------------------- 1 | =' ) ) { 32 | require_once __DIR__ . '/instance.php'; 33 | } else { 34 | function customize_widgets_plus_php_version_error() { 35 | printf( '

    %s

    ', esc_html__( 'Customize Widgets Plus plugin error: Your version of PHP is too old to run this plugin. You must be running PHP 5.3 or higher.', 'customize-widgets-plus' ) ); 36 | } 37 | if ( defined( 'WP_CLI' ) ) { 38 | WP_CLI::warning( __( 'Customize Widgets Plus plugin error: Your PHP version is too old. You must have 5.3 or higher.', 'customize-widgets-plus' ) ); 39 | } else { 40 | add_action( 'admin_notices', 'customize_widgets_plus_php_version_error' ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /instance.php: -------------------------------------------------------------------------------- 1 | = remainingRetryCount ) { 84 | errorMessage = 'Failed request count: ' + wpCustomizeWidgetsPlus.widgetNumberIncrementing.retryCount + '.'; 85 | errorMessage += ' Last error code: ' + errorCode; 86 | widgetAdded.reject( errorMessage ); 87 | } else if ( 'invalid_nonce' === errorCode || 'not_logged_in' === errorCode || 'unauthorized' === errorCode ) { 88 | if ( 'invalid_nonce' === errorCode ) { 89 | deferred = previewer.refreshNonces(); 90 | deferred.done( function() { 91 | incrWidgetNumber(); 92 | } ); 93 | } else { 94 | previewer.preview.iframe.hide(); 95 | deferred = previewer.login(); 96 | deferred.done( function() { 97 | previewer.preview.iframe.show(); 98 | incrWidgetNumber(); 99 | } ); 100 | } 101 | deferred.fail( function() { 102 | previewer.cheatin(); 103 | widgetAdded.reject(); 104 | }); 105 | } else { 106 | incrWidgetNumber(); /* Network error or temporary server problem */ 107 | } 108 | } ); 109 | }; 110 | incrWidgetNumber(); 111 | 112 | widgetAdded.always( function() { 113 | if ( ! wasSpinnerActive ) { 114 | spinner.removeClass( 'is-active' ); 115 | } 116 | } ); 117 | 118 | widgetAdded.fail( function( result ) { 119 | if ( 'undefined' !== typeof console ) { 120 | console.error( 'addWidget failure: ' + result ); 121 | } 122 | } ); 123 | 124 | deferredWidgetFormControl = { 125 | focus: function() { 126 | widgetAdded.done( function( result ) { 127 | if ( result ) { 128 | result.focus(); 129 | } 130 | } ); 131 | } 132 | }; 133 | return deferredWidgetFormControl; 134 | }; 135 | 136 | self.init(); 137 | return self; 138 | }( jQuery ) ); 139 | -------------------------------------------------------------------------------- /js/widget-number-incrementing.js: -------------------------------------------------------------------------------- 1 | /*global wpCustomizeWidgetsPlus, _customizeWidgetsPlusWidgetNumberIncrementingExports */ 2 | 3 | wpCustomizeWidgetsPlus.widgetNumberIncrementing = ( function( $ ) { 4 | var self = { 5 | nonce: '', 6 | action: '', 7 | retryCount: 0 8 | }; 9 | $.extend( self, _customizeWidgetsPlusWidgetNumberIncrementingExports ); 10 | 11 | return self; 12 | }( jQuery ) ); 13 | -------------------------------------------------------------------------------- /php/class-base-test-case.php: -------------------------------------------------------------------------------- 1 | plugin = get_plugin_instance(); 24 | remove_action( 'widgets_init', 'twentyfourteen_widgets_init' ); 25 | remove_action( 'customize_register', 'twentyfourteen_customize_register' ); 26 | remove_all_actions( 'send_headers' ); // prevent X-hacker header in VIP Quickstart 27 | 28 | // For why these hooks have to be removed, see https://github.com/Automattic/nginx-http-concat/issues/5 29 | $this->css_concat_init_priority = has_action( 'init', 'css_concat_init' ); 30 | if ( $this->css_concat_init_priority ) { 31 | remove_action( 'init', 'css_concat_init', $this->css_concat_init_priority ); 32 | } 33 | $this->js_concat_init_priority = has_action( 'init', 'js_concat_init' ); 34 | if ( $this->js_concat_init_priority ) { 35 | remove_action( 'init', 'js_concat_init', $this->js_concat_init_priority ); 36 | } 37 | 38 | parent::setUp(); 39 | } 40 | 41 | function clean_up_global_scope() { 42 | global $wp_registered_sidebars, $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_widget_updates, $wp_post_types, $wp_customize; 43 | // @codingStandardsIgnoreStart 44 | $wp_registered_sidebars = $wp_registered_widgets = $wp_registered_widget_controls = $wp_registered_widget_updates = array(); 45 | // @codingStandardsIgnoreEnd 46 | $this->plugin->widget_factory->widgets = array(); 47 | unset( $wp_post_types[ Widget_Posts::INSTANCE_POST_TYPE ] ); 48 | $wp_customize = null; 49 | parent::clean_up_global_scope(); 50 | } 51 | 52 | function tearDown() { 53 | parent::tearDown(); 54 | if ( $this->css_concat_init_priority ) { 55 | add_action( 'init', 'css_concat_init', $this->css_concat_init_priority ); 56 | } 57 | if ( $this->js_concat_init_priority ) { 58 | add_action( 'init', 'js_concat_init', $this->js_concat_init_priority ); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /php/class-deferred-customize-widgets.php: -------------------------------------------------------------------------------- 1 | plugin->config[ static::MODULE_SLUG ]; 52 | } elseif ( isset( $this->plugin->config[ static::MODULE_SLUG ][ $key ] ) ) { 53 | return $this->plugin->config[ static::MODULE_SLUG ][ $key ]; 54 | } else { 55 | return null; 56 | } 57 | } 58 | 59 | /** 60 | * Construct. 61 | * 62 | * @param Plugin $plugin Instance of plugin. 63 | */ 64 | function __construct( Plugin $plugin ) { 65 | $this->plugin = $plugin; 66 | add_action( 'customize_register', array( $this, 'init' ) ); 67 | } 68 | 69 | /** 70 | * Initialize plugin Customizer functionality. 71 | * 72 | * @param \WP_Customize_Manager $manager 73 | */ 74 | function init( \WP_Customize_Manager $manager ) { 75 | $this->manager = $manager; 76 | $has_json_encode_peak_memory_fix = method_exists( $this->manager, 'customize_pane_settings' ); 77 | $has_deferred_dom_widgets = method_exists( $this->manager->widgets, 'get_widget_control_parts' ); 78 | 79 | // The fix is already in Core, so no-op. 80 | if ( $has_json_encode_peak_memory_fix && $has_deferred_dom_widgets ) { 81 | return; 82 | } 83 | 84 | // Abort if widgets component is disabled. 85 | if ( ! isset( $manager->widgets ) ) { 86 | return; 87 | } 88 | 89 | add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 90 | 91 | if ( ! $has_json_encode_peak_memory_fix ) { 92 | add_action( 'customize_controls_print_footer_scripts', array( $this, 'defer_serializing_data_until_shutdown' ) ); 93 | } else { 94 | add_action( 'customize_controls_print_footer_scripts', array( $this, 'fixup_widget_control_params_for_dom_deferral' ), 1001 ); 95 | } 96 | 97 | // @todo Skip loading any widget settings or controls until after the page loads? This could cause problems. 98 | } 99 | 100 | /** 101 | * Enqueue scripts for Customizer controls. 102 | * 103 | * @action customize_controls_enqueue_scripts 104 | */ 105 | function enqueue_scripts() { 106 | wp_enqueue_script( $this->plugin->script_handles['deferred-customize-widgets'] ); 107 | } 108 | 109 | /** 110 | * Break up a widget control's content into its container wrapper, (outer) widget control, and (inner) widget form. 111 | * 112 | * For a Core merge, all of this should be made part of WP_Widget_Form_Customize_Control::json(). 113 | */ 114 | function fixup_widget_control_params_for_dom_deferral() { 115 | ?> 116 | 131 | customize_controls = array(); 146 | $controls = $this->manager->controls(); 147 | foreach ( $controls as $control ) { 148 | if ( $control instanceof \WP_Widget_Form_Customize_Control || $control instanceof \WP_Widget_Area_Customize_Control ) { 149 | $this->customize_controls[ $control->id ] = $control; 150 | $this->manager->remove_control( $control->id ); 151 | } 152 | } 153 | 154 | /* 155 | * Note: There is currently a Core dependency issue where the control for WP_Widget_Area_Customize_Control 156 | * must be processed after the control for WP_Widget_Form_Customize_Control, as otherwise the sidebar 157 | * does not initialize properly (specifically in regards to the reorder-toggle button. So this is why 158 | * we are including the WP_Widget_Area_Customize_Controls among those which are deferred. 159 | */ 160 | 161 | $this->customize_settings = array(); 162 | $settings = $this->manager->settings(); 163 | foreach ( $settings as $setting ) { 164 | if ( preg_match( '/^(widget_.+?\[\d+\]|sidebars_widgets\[.+?\])$/', $setting->id ) ) { 165 | $this->customize_settings[ $setting->id ] = $setting; 166 | $this->manager->remove_setting( $setting->id ); 167 | } 168 | } 169 | 170 | // We have to use shutdown because no action is triggered after _wpCustomizeSettings is written. 171 | add_action( 'shutdown', array( $this, 'export_data_with_peak_memory_usage_minimized' ), 10 ); 172 | add_action( 'shutdown', array( $this, 'fixup_widget_control_params_for_dom_deferral' ), 11 ); 173 | } 174 | 175 | /** 176 | * Amend the _wpCustomizeSettings JS object with the widget settings and controls 177 | * one-by-one in a loop using wp_json_encode() so that peak memory usage is kept low. 178 | * 179 | * This is only relevant in WordPress versions older than 4.4-alpha-33636-src, 180 | * with the changes introduced in Trac #33898. 181 | * 182 | * @link https://core.trac.wordpress.org/ticket/33898 183 | */ 184 | function export_data_with_peak_memory_usage_minimized() { 185 | // Re-add the constructs to WP_Customize_manager, in case they need to refer to each other. 186 | foreach ( $this->customize_settings as $setting ) { 187 | $this->manager->add_setting( $setting ); 188 | } 189 | foreach ( $this->customize_controls as $control ) { 190 | $this->manager->add_control( $control ); 191 | } 192 | 193 | echo ''; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /php/class-exception.php: -------------------------------------------------------------------------------- 1 | plugin = $plugin; 45 | 46 | add_action( 'init', array( $this, 'add_rewrite_rule' ) ); 47 | add_filter( 'query_vars', array( $this, 'filter_query_vars' ) ); 48 | add_filter( 'redirect_canonical', array( $this, 'enforce_trailingslashing' ) ); 49 | add_action( 'template_redirect', array( $this, 'handle_proxy_request' ) ); 50 | add_action( 'init', array( $this, 'add_proxy_filtering' ) ); 51 | add_filter( 'wp_unique_post_slug_is_bad_flat_slug', array( $this, 'reserve_api_endpoint' ), 10, 2 ); 52 | add_filter( 'wp_unique_post_slug_is_bad_hierarchical_slug', array( $this, 'reserve_api_endpoint' ), 10, 2 ); 53 | } 54 | 55 | /** 56 | * @return array 57 | */ 58 | static function default_config() { 59 | return array( 60 | 'endpoint' => 'wp-https-resource-proxy', 61 | 'min_cache_ttl' => 5 * MINUTE_IN_SECONDS, 62 | 'customize_preview_only' => true, 63 | 'logged_in_users_only' => true, 64 | 'request_timeout' => 3, 65 | 'trailingslash_srcs' => true, // web server configs may be configured to route apparent static file requests to 404 handler 66 | 'max_content_length' => 768 * 1024, // guard against 1MB Memcached Object Cache limit, so body + serialized request metadata 67 | ); 68 | } 69 | 70 | /** 71 | * Return the config entry for the supplied key, or all configs if not supplied. 72 | * 73 | * @param string $key 74 | * @return array|mixed 75 | */ 76 | function config( $key = null ) { 77 | if ( is_null( $key ) ) { 78 | return $this->plugin->config[ self::MODULE_SLUG ]; 79 | } elseif ( isset( $this->plugin->config[ self::MODULE_SLUG ][ $key ] ) ) { 80 | return $this->plugin->config[ self::MODULE_SLUG ][ $key ]; 81 | } else { 82 | return null; 83 | } 84 | } 85 | 86 | /** 87 | * Return whether the proxy is enabled. 88 | * 89 | * @return bool 90 | */ 91 | function is_proxy_enabled() { 92 | $enabled = ( 93 | is_ssl() 94 | && 95 | ! is_admin() 96 | && 97 | ( is_customize_preview() || ! $this->config( 'customize_preview_only' ) ) 98 | && 99 | ( is_user_logged_in() || ! $this->config( 'logged_in_users_only' ) ) 100 | ); 101 | return apply_filters( 'https_resource_proxy_filtering_enabled', $enabled, $this ); 102 | } 103 | 104 | /** 105 | * Add the filters for injecting the functionality into the page. 106 | * 107 | * @action init 108 | */ 109 | function add_proxy_filtering() { 110 | if ( ! $this->is_proxy_enabled() ) { 111 | return; 112 | } 113 | 114 | nocache_headers(); // we don't want to cache the resource URLs containing the nonces 115 | add_filter( 'script_loader_src', array( $this, 'filter_script_loader_src' ) ); 116 | add_filter( 'style_loader_src', array( $this, 'filter_style_loader_src' ) ); 117 | add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 118 | 119 | /* 120 | * On WordPress.com, prevent hostname in assets from being replaced with 121 | * the WPCOM CDN (e.g. w1.wp.com) as then the assets will 404. 122 | */ 123 | if ( $this->plugin->is_wpcom_vip_prod() ) { 124 | remove_filter( 'style_loader_src', 'staticize_subdomain' ); 125 | remove_filter( 'script_loader_src', 'staticize_subdomain' ); 126 | } 127 | } 128 | 129 | /** 130 | * @filter query_vars 131 | * @param $query_vars 132 | * 133 | * @return array 134 | */ 135 | function filter_query_vars( $query_vars ) { 136 | $query_vars[] = self::NONCE_QUERY_VAR; 137 | $query_vars[] = self::HOST_QUERY_VAR; 138 | $query_vars[] = self::PATH_QUERY_VAR; 139 | return $query_vars; 140 | } 141 | 142 | /** 143 | * @action init 144 | */ 145 | function add_rewrite_rule() { 146 | $this->rewrite_regex = preg_quote( $this->config( 'endpoint' ), self::REGEX_DELIMITER ) . '/(?P\w+)/(?P[^/]+)(?P/.+)'; 147 | 148 | $redirect_vars = array( 149 | self::NONCE_QUERY_VAR => '$matches[1]', 150 | self::HOST_QUERY_VAR => '$matches[2]', 151 | self::PATH_QUERY_VAR => '$matches[3]', 152 | ); 153 | $redirect_var_pairs = array(); 154 | foreach ( $redirect_vars as $name => $value ) { 155 | $redirect_var_pairs[] = $name . '=' . $value; 156 | } 157 | $redirect = 'index.php?' . join( '&', $redirect_var_pairs ); 158 | 159 | add_rewrite_rule( $this->rewrite_regex, $redirect, 'top' ); 160 | } 161 | 162 | /** 163 | * Reserve the API endpoint slugs. 164 | * 165 | * @param bool $is_bad Whether a post slug is available for use or not. 166 | * @param string $slug The post's slug. 167 | * 168 | * @return bool 169 | * 170 | * @filter wp_unique_post_slug_is_bad_flat_slug 171 | * @filter wp_unique_post_slug_is_bad_hierarchical_slug 172 | */ 173 | public function reserve_api_endpoint( $is_bad, $slug ) { 174 | if ( $this->config( 'endpoint' ) === $slug ) { 175 | $is_bad = true; 176 | } 177 | return $is_bad; 178 | } 179 | 180 | /** 181 | * @filter script_loader_src 182 | * @param $src 183 | * @return string 184 | */ 185 | function filter_script_loader_src( $src ) { 186 | return $this->filter_loader_src( $src ); 187 | } 188 | 189 | /** 190 | * @filter style_loader_src 191 | * @param $src 192 | * @return string 193 | */ 194 | function filter_style_loader_src( $src ) { 195 | return $this->filter_loader_src( $src ); 196 | } 197 | 198 | /** 199 | * @return string 200 | */ 201 | function get_base_url() { 202 | $proxied_src = trailingslashit( site_url( $this->config( 'endpoint' ) ) ); 203 | $proxied_src .= trailingslashit( wp_create_nonce( self::MODULE_SLUG ) ); 204 | return $proxied_src; 205 | } 206 | 207 | /** 208 | * Rewrite asset URLs to use the proxy when appropriate. 209 | * 210 | * @param string $src 211 | * @return string 212 | */ 213 | function filter_loader_src( $src ) { 214 | if ( ! isset( $this->rewrite_regex ) ) { 215 | $this->add_rewrite_rule(); 216 | } 217 | 218 | $parsed_url = parse_url( $src ); 219 | $regex = self::REGEX_DELIMITER . $this->rewrite_regex . self::REGEX_DELIMITER; 220 | $should_filter = ( 221 | isset( $parsed_url['scheme'] ) 222 | && 223 | 'http' === $parsed_url['scheme'] 224 | && 225 | ! preg_match( $regex, parse_url( $src, PHP_URL_PATH ) ) // prevent applying regex more than once 226 | ); 227 | if ( $should_filter ) { 228 | $proxied_src = $this->get_base_url(); 229 | $proxied_src .= $parsed_url['host']; 230 | $proxied_src .= $parsed_url['path']; 231 | 232 | /* 233 | * Now we trailingslash to account for web server configs that try 234 | * to optimize requests to non-existing static assets by sending 235 | * them straight to 404 instead of sending them to the WP router. 236 | */ 237 | if ( $this->config( 'trailingslash_srcs' ) ) { 238 | $proxied_src = trailingslashit( $proxied_src ); 239 | } 240 | 241 | if ( ! empty( $parsed_url['query'] ) ) { 242 | $proxied_src .= '?' . $parsed_url['query']; 243 | } 244 | $src = $proxied_src; 245 | } 246 | return $src; 247 | } 248 | 249 | /** 250 | * @action wp_enqueue_scripts 251 | */ 252 | function enqueue_scripts() { 253 | $data = array( 254 | 'baseUrl' => $this->get_base_url(), 255 | 'trailingslashSrcs' => $this->config( 'trailingslash_srcs' ), 256 | ); 257 | 258 | wp_scripts()->add_data( 259 | $this->plugin->script_handles['https-resource-proxy'], 260 | 'data', 261 | sprintf( 'var _httpsResourceProxyExports = %s', wp_json_encode( $data ) ) 262 | ); 263 | wp_enqueue_script( $this->plugin->script_handles['https-resource-proxy'] ); 264 | } 265 | 266 | /** 267 | * Enforce trailingslashing of proxied resource URLs. 268 | * 269 | * @filter redirect_canonical 270 | * @param string $redirect_url 271 | * @return string 272 | */ 273 | function enforce_trailingslashing( $redirect_url ) { 274 | if ( get_query_var( self::PATH_QUERY_VAR ) ) { 275 | if ( $this->config( 'trailingslash_srcs' ) ) { 276 | if ( false === strpos( $redirect_url, '?' ) ) { 277 | $redirect_url = trailingslashit( $redirect_url ); 278 | } else { 279 | $redirect_url = preg_replace( '#(?<=[^/])(?=\?)#', '/', $redirect_url ); 280 | } 281 | } else { 282 | $redirect_url = preg_replace( '#/(?=\?|$)#', '', $redirect_url ); 283 | } 284 | } 285 | return $redirect_url; 286 | } 287 | 288 | /** 289 | * Interrupt WP execution and serve response if called for. 290 | * 291 | * @see HTTPS_Resource_Proxy::send_proxy_response() 292 | * 293 | * @action template_redirect 294 | */ 295 | function handle_proxy_request() { 296 | $is_request = ( get_query_var( self::NONCE_QUERY_VAR ) && get_query_var( self::HOST_QUERY_VAR ) && get_query_var( self::PATH_QUERY_VAR ) ); 297 | if ( ! $is_request ) { 298 | return; 299 | } 300 | $params = array( 301 | 'nonce' => get_query_var( self::NONCE_QUERY_VAR ), 302 | 'host' => get_query_var( self::HOST_QUERY_VAR ), 303 | 'path' => get_query_var( self::PATH_QUERY_VAR ), 304 | 'query' => null, 305 | 'if_none_match' => null, 306 | 'if_modified_since' => null, 307 | ); 308 | if ( ! empty( $_SERVER['QUERY_STRING'] ) ) { // input var okay 309 | $params['query'] = wp_unslash( $_SERVER['QUERY_STRING'] ); // input var okay; sanitization okay 310 | } 311 | if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { // input var okay 312 | $params['if_modified_since'] = wp_unslash( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); // input var okay; sanitization okay 313 | } 314 | if ( isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ) { // input var okay 315 | $params['if_none_match'] = wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ); // input var okay; sanitization okay 316 | } 317 | 318 | try { 319 | $r = $this->send_proxy_response( $params ); 320 | 321 | $code = wp_remote_retrieve_response_code( $r ); 322 | $message = wp_remote_retrieve_response_message( $r ); 323 | if ( empty( $message ) ) { 324 | $message = get_status_header_desc( $code ); 325 | } 326 | $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : null; // input var okay; sanitization okay 327 | if ( 'HTTP/1.1' !== $protocol && 'HTTP/1.0' !== $protocol ) { 328 | $protocol = 'HTTP/1.0'; 329 | } 330 | $status_header = "$protocol $code $message"; 331 | header( $status_header, true, $code ); 332 | 333 | // Remove headers added by nocache_headers() 334 | foreach ( array_keys( wp_get_nocache_headers() ) as $name ) { 335 | header_remove( $name ); 336 | } 337 | foreach ( $r['headers'] as $name => $value ) { 338 | header( "$name: $value" ); 339 | } 340 | if ( 304 !== $code ) { 341 | echo wp_remote_retrieve_body( $r ); // xss ok (we're passing things through on purpose) 342 | } 343 | die(); 344 | } catch ( Exception $e ) { 345 | $code = $e->getCode(); 346 | if ( $code < 400 || $code >= 600 ) { 347 | $code = $e->getCode(); 348 | } 349 | status_header( $code ); 350 | die( esc_html( $e->getMessage() ) ); 351 | } 352 | } 353 | 354 | /** 355 | * Formulate the response based on the request $params. 356 | * 357 | * @param array $params { 358 | * @type string $nonce 359 | * @type string $host 360 | * @type string $path 361 | * @type string $query 362 | * @type string $if_none_match 363 | * @type string $if_modified_since 364 | * } 365 | * @return array $r { 366 | * @type array $response { 367 | * @type int $code 368 | * @type string $message 369 | * } 370 | * @type array $headers 371 | * @type string $body 372 | * } 373 | * 374 | * @throws Exception 375 | */ 376 | function send_proxy_response( array $params ) { 377 | $params = array_merge( 378 | array_fill_keys( array( 'nonce', 'host', 'path', 'query', 'if_none_match', 'if_modified_since' ), null ), 379 | $params 380 | ); 381 | 382 | if ( ! $this->is_proxy_enabled() ) { 383 | throw new Exception( 'proxy_not_enabled', 401 ); 384 | } 385 | if ( ! wp_verify_nonce( $params['nonce'], self::MODULE_SLUG ) ) { 386 | throw new Exception( 'bad_nonce', 403 ); 387 | } 388 | 389 | // Construct the proxy URL for the resource 390 | $url = 'http://' . $params['host'] . $params['path']; 391 | if ( $params['query'] ) { 392 | $url .= '?' . $params['query']; 393 | } 394 | 395 | $transient_key = sprintf( 'proxied_' . md5( $url ) ); 396 | if ( strlen( $transient_key ) > 40 ) { 397 | throw new Exception( 'transient_key_too_large', 500 ); 398 | } 399 | $r = get_transient( $transient_key ); 400 | if ( empty( $r ) ) { 401 | // @todo We eliminate transient expiration and send if-modified-since/if-none-match to server 402 | $timeout = $this->config( 'request_timeout' ); 403 | if ( function_exists( 'vip_safe_wp_remote_get' ) ) { 404 | $fallback_value = ''; 405 | $threshold = 3; 406 | $r = vip_safe_wp_remote_get( $url, $fallback_value, $threshold, $timeout ); 407 | } else { 408 | $args = compact( 'timeout' ); 409 | // @codingStandardsIgnoreStart 410 | $r = wp_remote_get( $url, $args ); 411 | // @codingStandardsIgnoreEnd 412 | } 413 | 414 | if ( is_wp_error( $r ) ) { 415 | $r = array( 416 | 'response' => array( 417 | 'code' => 400, 418 | 'message' => $r->get_error_code(), 419 | ), 420 | 'headers' => array( 421 | 'content-type' => 'text/plain', 422 | ), 423 | 'body' => $r->get_error_message(), 424 | ); 425 | } 426 | 427 | if ( ! isset( $r['headers']['content-length'] ) ) { 428 | $r['headers']['content-length'] = 0; 429 | } 430 | $r['headers']['content-length'] = max( $r['headers']['content-length'], strlen( wp_remote_retrieve_body( $r ) ) ); 431 | 432 | if ( $r['headers']['content-length'] > $this->config( 'max_content_length' ) ) { 433 | $r = array( 434 | 'response' => array( 435 | 'code' => 502, 436 | 'message' => 'Response Too Large', 437 | ), 438 | 'headers' => array( 439 | 'content-type' => 'text/plain', 440 | ), 441 | 'body' => sprintf( 442 | __( 'Response body (content-length: %1$d) too big for HTTPS Resource Proxy (max_content_length: %2$d).', 'customize-widgets-plus' ), 443 | $r['headers']['content-length'], 444 | $this->config( 'max_content_length' ) 445 | ), 446 | ); 447 | } 448 | 449 | if ( ! empty( $r['headers']['expires'] ) ) { 450 | $cache_ttl = strtotime( $r['headers']['expires'] ) - time(); 451 | } elseif ( ! empty( $r['headers']['cache-control'] ) && preg_match( '/max-age=(\d+)/', $r['headers']['cache-control'], $matches ) ) { 452 | $cache_ttl = intval( $matches[1] ); 453 | } else { 454 | $cache_ttl = -1; 455 | } 456 | $cache_ttl = max( $cache_ttl, $this->config( 'min_cache_ttl' ) ); 457 | $r['headers']['expires'] = str_replace( '+0000', 'GMT', gmdate( 'r', time() + $cache_ttl ) ); 458 | 459 | // @todo in addition to the checks for whether the user is logged-in and if in customizer, should we do a check to prevent too many resources from being cached? 460 | set_transient( $transient_key, $r, $cache_ttl ); 461 | } 462 | 463 | $is_not_modified = false; 464 | $response_code = wp_remote_retrieve_response_code( $r ); 465 | $response_message = wp_remote_retrieve_response_message( $r ); 466 | if ( 200 === $response_code ) { 467 | $is_etag_not_modified = ( 468 | ! empty( $params['if_none_match'] ) 469 | && 470 | isset( $r['headers']['etag'] ) 471 | && 472 | ( false !== strpos( $params['if_none_match'], $r['headers']['etag'] ) ) 473 | ); 474 | 475 | $is_last_modified_not_modified = ( 476 | ! empty( $params['if_modified_since'] ) 477 | && 478 | isset( $r['headers']['last-modified'] ) 479 | && 480 | strtotime( $r['headers']['last-modified'] ) <= strtotime( $params['if_modified_since'] ) 481 | ); 482 | $is_not_modified = ( $is_etag_not_modified || $is_last_modified_not_modified ); 483 | if ( $is_not_modified ) { 484 | $response_code = 304; 485 | $response_message = 'Not Modified'; 486 | } 487 | } else { 488 | unset( $r['headers']['last-modified'] ); 489 | unset( $r['headers']['etag'] ); 490 | } 491 | 492 | $body = ''; 493 | $forwarded_response_headers = array( 'content-type', 'last-modified', 'etag', 'expires' ); 494 | $headers = wp_array_slice_assoc( $r['headers'], $forwarded_response_headers ); 495 | 496 | if ( ! $is_not_modified ) { 497 | // @todo Content-Encoding deflate/gzip if requested 498 | $headers['content-length'] = strlen( wp_remote_retrieve_body( $r ) ); 499 | $body = wp_remote_retrieve_body( $r ); 500 | } 501 | 502 | return array( 503 | 'response' => array( 504 | 'code' => $response_code, 505 | 'message' => $response_message, 506 | ), 507 | 'headers' => $headers, 508 | 'body' => $body, 509 | ); 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /php/class-non-autoloaded-widget-options.php: -------------------------------------------------------------------------------- 1 | plugin = $plugin; 36 | 37 | add_action( 'widgets_init', array( $this, 'fix_widget_options' ), 90 ); // must be before 100 when widgets are rendered 38 | } 39 | 40 | /** 41 | * @action widget_init, 90 42 | */ 43 | function fix_widget_options() { 44 | 45 | /** 46 | * @var \wpdb $wpdb 47 | */ 48 | global $wpdb; 49 | 50 | /** 51 | * @var \WP_Widget_Factory $wp_widget_factory 52 | */ 53 | global $wp_widget_factory; 54 | 55 | $alloptions = wp_load_alloptions(); 56 | if ( ! is_array( $alloptions ) ) { 57 | $this->plugin->trigger_warning( sprintf( 'wp_load_alloptions() did not return an array but a "%s"; object cache may be corrupted and may need to be flushed. Please follow https://core.trac.wordpress.org/ticket/31245', gettype( $alloptions ) ) ); 58 | $alloptions = array(); 59 | } 60 | 61 | $autoloaded_option_names = array_keys( $alloptions ); 62 | $pending_unautoload_option_names = array(); 63 | 64 | foreach ( $wp_widget_factory->widgets as $widget_obj ) { 65 | /** 66 | * @var \WP_Widget $widget_obj 67 | */ 68 | 69 | $is_already_autoloaded = in_array( $widget_obj->option_name, $autoloaded_option_names, true ); 70 | 71 | if ( $is_already_autoloaded ) { 72 | // Add to list of options that we need to unautoload 73 | $pending_unautoload_option_names[] = $widget_obj->option_name; 74 | } else { 75 | // Preemptively do add_option() before the widget will ever have a chance to update_option() 76 | add_option( $widget_obj->option_name, array(), '', 'no' ); 77 | } 78 | } 79 | 80 | // Unautoload options and flush alloptions cache. 81 | if ( ! empty( $pending_unautoload_option_names ) ) { 82 | $sql_in = join( ',', array_fill( 0, count( $pending_unautoload_option_names ), '%s' ) ); 83 | $sql = "UPDATE $wpdb->options SET autoload = 'no' WHERE option_name IN ( $sql_in )"; 84 | // @codingStandardsIgnoreStart 85 | $wpdb->query( $wpdb->prepare( $sql, $pending_unautoload_option_names ) ); 86 | // @codingStandardsIgnoreStop 87 | wp_cache_delete( 'alloptions', 'options' ); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /php/class-optimized-widget-registration.php: -------------------------------------------------------------------------------- 1 | plugin = $plugin; 31 | 32 | $priority = 92; // Because Widget_Posts::prepare_widget_data() happens at 91. 33 | add_action( 'widgets_init', array( $this, 'capture_widget_instance_data' ), $priority ); 34 | 35 | if ( 'widgets.php' !== $pagenow ) { 36 | $priority = $this->plugin->disable_widgets_factory(); 37 | add_action( 'widgets_init', array( $this, 'register_initial_widgets' ), $priority ); 38 | if ( 'customize.php' === $pagenow ) { 39 | add_action( 'widgets_init', array( $this, 'register_all_sidebars_widgets' ), $priority ); 40 | } 41 | } 42 | 43 | add_action( 'customize_register', array( $this, 'capture_customize_manager' ), 1 ); 44 | 45 | /** 46 | * Note that this is at wp:9 so that it happens before WP_Customize_Widgets::customize_register() 47 | * in Customizer Preview so that any query-specific widgets will also be registered. 48 | * @see \WP_Customize_Widgets::schedule_customize_register() 49 | */ 50 | add_action( 'wp', array( $this, 'register_all_sidebars_widgets' ), 9 ); 51 | 52 | add_action( 'wp', array( $this, 'register_remaining_sidebars_widgets' ), 11 ); 53 | } 54 | 55 | /** 56 | * @param \WP_Customize_Manager $manager 57 | * @action customize_register, 1 58 | */ 59 | function capture_customize_manager( \WP_Customize_Manager $manager ) { 60 | $this->manager = $manager; 61 | } 62 | 63 | /** 64 | * Since at widgets_init,100 the single instances of widgets get copied out 65 | * to the many instances in $wp_registered_widgets, we capture all of the 66 | * registered widgets up front so we don't have to search through the big 67 | * list later. 68 | */ 69 | function capture_widget_instance_data() { 70 | foreach ( $this->plugin->widget_factory->widgets as $widget_obj ) { 71 | /** @var \WP_Widget $widget_obj */ 72 | $this->widget_objs[ $widget_obj->id_base ] = $widget_obj; 73 | } 74 | } 75 | 76 | /** 77 | * Register the widget instance used in Ajax requests. 78 | * 79 | * @see wp_ajax_save_widget() 80 | * @see wp_ajax_update_widget() 81 | * @return bool Whether the widget was registered. False if invalid or already registered. 82 | */ 83 | function register_ajax_request_widget() { 84 | false && check_ajax_referer(); // temp hack to get around PHP_CodeSniffer complaint 85 | 86 | if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX || ! isset( $_POST['action'] ) ) { // WPCS: input var ok. 87 | return false; 88 | } 89 | $is_ajax_widget_action = ( 'save-widget' === $_POST['action'] || 'update-widget' === $_POST['action'] ); // WPCS: input var ok, sanitization ok. 90 | if ( ! $is_ajax_widget_action ) { 91 | return false; 92 | } 93 | if ( ! isset( $_POST['widget-id'] ) ) { // WPCS: input var ok. 94 | return false; 95 | } 96 | $widget_id = sanitize_key( wp_unslash( $_POST['widget-id'] ) ); // WPCS: input var ok, [not needed after WPCS upgrade:] sanitization ok. 97 | return $this->register_single_widget( $widget_id ); 98 | } 99 | 100 | /** 101 | * Register just the widgets that are needed for the current request. 102 | * 103 | * @see \WP_Widget_Factory::_register_widgets() 104 | */ 105 | function register_initial_widgets() { 106 | global $pagenow, $wp_registered_widgets, $wp_widget_factory; 107 | 108 | $registered_id_bases = array_unique( array_map( '_get_widget_id_base', array_keys( $wp_registered_widgets ) ) ); 109 | 110 | foreach ( $wp_widget_factory->widgets as $widget_class => $widget_obj ) { 111 | /** @var \WP_Widget $widget_obj */ 112 | 113 | // Don't register new widget if old widget with the same id is already registered. 114 | if ( in_array( $widget_obj->id_base, $registered_id_bases, true ) ) { 115 | unset( $wp_widget_factory->widgets[ $widget_class ] ); 116 | continue; 117 | } 118 | 119 | if ( 'widgets.php' === $pagenow ) { 120 | // Register all widget instances since they will all be used on the widgets admin page. 121 | $widget_obj->_register(); 122 | } else { 123 | // Only register the template. Additional widgets will be registered later as needed. 124 | $widget_obj->_set( 1 ); 125 | $widget_obj->_register_one( 1 ); 126 | } 127 | } 128 | 129 | if ( 'widgets.php' !== $pagenow ) { 130 | $this->register_ajax_request_widget(); 131 | } 132 | } 133 | 134 | /** 135 | * Register widgets for all sidebars, other than inactive widgets and orphaned widgets. 136 | * 137 | * @return array Widget IDs newly-registered. 138 | */ 139 | function register_all_sidebars_widgets() { 140 | $registered_widget_ids = array(); 141 | $sidebars_widgets = wp_get_sidebars_widgets(); 142 | foreach ( $sidebars_widgets as $sidebar_id => $widget_ids ) { 143 | if ( empty( $widget_ids ) || preg_match( '/^(wp_inactive_widgets|orphaned_widgets_\d+)$/', $sidebar_id ) ) { 144 | continue; 145 | } 146 | foreach ( $widget_ids as $widget_id ) { 147 | if ( $this->register_single_widget( $widget_id ) ) { 148 | $registered_widget_ids[] = $widget_id; 149 | } 150 | } 151 | } 152 | return $registered_widget_ids; 153 | } 154 | 155 | /** 156 | * Register any additional widgets which may have been added to the sidebars. 157 | * 158 | * The Jetpack Widget Visibility conditions as well as the Customizer setting 159 | * previews for the sidebars will have applied by wp:10, so after this point 160 | * register Customizer settings and controls for any newly-recognized 161 | * widgets which are now available after having called preview() on the 162 | * sidebars_widgets Customizer settings. 163 | * 164 | * @see WP_Customize_Widgets::customize_register() 165 | */ 166 | function register_remaining_sidebars_widgets() { 167 | $newly_registered_widgets = $this->register_all_sidebars_widgets(); 168 | 169 | if ( ! empty( $this->manager ) && ! empty( $newly_registered_widgets ) ) { 170 | $this->customize_register_widgets( $newly_registered_widgets ); 171 | } 172 | } 173 | 174 | /** 175 | * Register Customizer settings and controls for the given widget IDs. 176 | * 177 | * @param array $register_widget_ids 178 | */ 179 | function customize_register_widgets( $register_widget_ids ) { 180 | global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars; 181 | 182 | $new_setting_ids = array(); 183 | 184 | $sidebars_widgets = wp_get_sidebars_widgets(); 185 | 186 | foreach ( $register_widget_ids as $widget_id ) { 187 | $setting_id = $this->manager->widgets->get_setting_id( $widget_id ); 188 | if ( ! $this->manager->get_setting( $setting_id ) ) { 189 | $setting_class = 'WP_Customize_Setting'; // This will likely get filtered to WP_Customize_Widget_Setting. 190 | $setting_args = $this->manager->widgets->get_setting_args( $setting_id ); 191 | 192 | /** This filter is documented in wp-includes/class-wp-customize-manager.php */ 193 | $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id ); 194 | 195 | /** This filter is documented in wp-includes/class-wp-customize-manager.php */ 196 | $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args ); 197 | 198 | $setting = new $setting_class( $this->manager, $setting_id, $setting_args ); 199 | $this->manager->add_setting( $setting ); 200 | $new_setting_ids[] = $setting_id; 201 | } 202 | } 203 | 204 | // Add a control for each active widget (located in a sidebar). 205 | foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) { 206 | if ( empty( $sidebar_widget_ids ) || ! isset( $wp_registered_sidebars[ $sidebar_id ] ) ) { 207 | continue; 208 | } 209 | 210 | foreach ( $sidebar_widget_ids as $i => $widget_id ) { 211 | $setting_id = $this->manager->widgets->get_setting_id( $widget_id ); 212 | $should_register = ( 213 | in_array( $widget_id, $register_widget_ids, true ) 214 | && 215 | isset( $wp_registered_widgets[ $widget_id ] ) 216 | && 217 | ! $this->manager->get_control( $setting_id ) 218 | ); 219 | if ( ! $should_register ) { 220 | continue; 221 | } 222 | 223 | $registered_widget = $wp_registered_widgets[ $widget_id ]; 224 | $id_base = $wp_registered_widget_controls[ $widget_id ]['id_base']; 225 | $control = new \WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array( 226 | 'label' => $registered_widget['name'], 227 | 'section' => sprintf( 'sidebar-widgets-%s', $sidebar_id ), 228 | 'sidebar_id' => $sidebar_id, 229 | 'widget_id' => $widget_id, 230 | 'widget_id_base' => $id_base, 231 | 'priority' => $i, 232 | 'width' => $wp_registered_widget_controls[ $widget_id ]['width'], 233 | 'height' => $wp_registered_widget_controls[ $widget_id ]['height'], 234 | 'is_wide' => $this->manager->widgets->is_wide_widget( $widget_id ), 235 | ) ); 236 | $this->manager->add_control( $control ); 237 | } 238 | } 239 | 240 | if ( ! $this->manager->doing_ajax( 'customize_save' ) ) { 241 | foreach ( $new_setting_ids as $new_setting_id ) { 242 | $this->manager->get_setting( $new_setting_id )->preview(); 243 | } 244 | } 245 | } 246 | 247 | /** 248 | * @param string $widget_id 249 | * 250 | * @return bool Whether the widget was registered. False if already registered or invalid. 251 | */ 252 | function register_single_widget( $widget_id ) { 253 | global $wp_registered_widgets; 254 | if ( isset( $wp_registered_widgets[ $widget_id ] ) ) { 255 | return false; 256 | } 257 | $parsed_widget_id = $this->plugin->parse_widget_id( $widget_id ); 258 | if ( empty( $parsed_widget_id ) || empty( $parsed_widget_id['widget_number'] ) ) { 259 | return false; 260 | } 261 | if ( ! isset( $this->widget_objs[ $parsed_widget_id['id_base'] ] ) ) { 262 | return false; 263 | } 264 | if ( $widget_id !== $parsed_widget_id['id_base'] . '-' . $parsed_widget_id['widget_number'] ) { 265 | return false; 266 | } 267 | $widget_obj = $this->widget_objs[ $parsed_widget_id['id_base'] ]; 268 | $settings = $widget_obj->get_settings(); // @todo We could improve performance if this is not called multiple times. 269 | if ( ! isset( $settings[ $parsed_widget_id['widget_number'] ] ) ) { 270 | return false; 271 | } 272 | $widget_obj->_set( $parsed_widget_id['widget_number'] ); 273 | $widget_obj->_register_one( $parsed_widget_id['widget_number'] ); 274 | return true; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /php/class-plugin-base.php: -------------------------------------------------------------------------------- 1 | locate_plugin(); 34 | $this->slug = $location['dir_basename']; 35 | $this->dir_path = $location['dir_path']; 36 | $this->dir_url = $location['dir_url']; 37 | spl_autoload_register( array( $this, 'autoload' ) ); 38 | } 39 | 40 | /** 41 | * @return \ReflectionObject 42 | */ 43 | function get_object_reflection() { 44 | static $reflection; 45 | if ( empty( $reflection ) ) { 46 | $reflection = new \ReflectionObject( $this ); 47 | } 48 | return $reflection; 49 | } 50 | 51 | protected $autoload_matches_cache = array(); 52 | 53 | /** 54 | * Autoload for classes that are in the same namespace as $this. 55 | * 56 | * @param string $class 57 | * @return void 58 | */ 59 | function autoload( $class ) { 60 | if ( ! isset( $this->autoload_matches_cache[ $class ] ) ) { 61 | if ( ! preg_match( '/^(?P.+)\\\\(?P[^\\\\]+)$/', $class, $matches ) ) { 62 | $matches = false; 63 | } 64 | $this->autoload_matches_cache[ $class ] = $matches; 65 | } else { 66 | $matches = $this->autoload_matches_cache[ $class ]; 67 | } 68 | if ( empty( $matches ) ) { 69 | return; 70 | } 71 | if ( $this->get_object_reflection()->getNamespaceName() !== $matches['namespace'] ) { 72 | return; 73 | } 74 | $class_name = $matches['class']; 75 | 76 | $class_path = \trailingslashit( $this->dir_path ); 77 | if ( $this->autoload_class_dir ) { 78 | $class_path .= \trailingslashit( $this->autoload_class_dir ); 79 | } 80 | $class_path .= sprintf( 'class-%s.php', strtolower( str_replace( '_', '-', $class_name ) ) ); 81 | if ( is_readable( $class_path ) ) { 82 | require_once $class_path; 83 | } 84 | } 85 | 86 | /** 87 | * Version of plugin_dir_url() which works for plugins installed in the plugins directory, 88 | * and for plugins bundled with themes. 89 | * 90 | * @throws \Exception 91 | * @return array 92 | */ 93 | public function locate_plugin() { 94 | $reflection = new \ReflectionObject( $this ); 95 | $file_name = $reflection->getFileName(); 96 | if ( '/' !== \DIRECTORY_SEPARATOR ) { 97 | $file_name = str_replace( \DIRECTORY_SEPARATOR, '/', $file_name ); // Windows compat 98 | } 99 | $plugin_dir = preg_replace( '#(.*plugins[^/]*/[^/]+)(/.*)?#', '$1', $file_name, 1, $count ); 100 | if ( 0 === $count ) { 101 | throw new \Exception( "Class not located within a directory tree containing 'plugins': $file_name" ); 102 | } 103 | 104 | // Make sure that we can reliably get the relative path inside of the content directory 105 | $content_dir = trailingslashit( WP_CONTENT_DIR ); 106 | if ( '/' !== \DIRECTORY_SEPARATOR ) { 107 | $content_dir = str_replace( \DIRECTORY_SEPARATOR, '/', $content_dir ); // Windows compat 108 | } 109 | if ( 0 !== strpos( $plugin_dir, $content_dir ) ) { 110 | throw new \Exception( 'Plugin dir is not inside of WP_CONTENT_DIR' ); 111 | } 112 | $content_sub_path = substr( $plugin_dir, strlen( $content_dir ) ); 113 | $dir_url = content_url( trailingslashit( $content_sub_path ) ); 114 | $dir_path = $plugin_dir; 115 | $dir_basename = basename( $plugin_dir ); 116 | return compact( 'dir_url', 'dir_path', 'dir_basename' ); 117 | } 118 | 119 | /** 120 | * Return whether we're on WordPress.com VIP production. 121 | * 122 | * @return bool 123 | */ 124 | public function is_wpcom_vip_prod() { 125 | return ( defined( '\WPCOM_IS_VIP_ENV' ) && \WPCOM_IS_VIP_ENV ); 126 | } 127 | 128 | /** 129 | * Call trigger_error() if not on VIP production. 130 | * 131 | * @param string $message 132 | * @param int $code 133 | */ 134 | public function trigger_warning( $message, $code = \E_USER_WARNING ) { 135 | if ( ! $this->is_wpcom_vip_prod() ) { 136 | trigger_error( esc_html( get_class( $this ) . ': ' . $message ), $code ); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /php/class-plugin.php: -------------------------------------------------------------------------------- 1 | false, 73 | 'disable_widgets_factory' => false, 74 | 'active_modules' => array( 75 | /* 76 | * Note that non_autoloaded_widget_options is disabled by default 77 | * on WordPress.com because it has no effect since all options 78 | * are autoloaded. We need to go deeper and stop storing widgets 79 | * in options altogether, e.g. in a custom post type. 80 | * See https://github.com/Automattic/vip-quickstart/issues/430#issuecomment-102183247 81 | */ 82 | 'non_autoloaded_widget_options' => ! $this->is_wpcom_vip_prod(), 83 | 84 | 'widget_number_incrementing' => true, 85 | 'https_resource_proxy' => true, 86 | 'widget_posts' => true, 87 | 'optimized_widget_registration' => false, 88 | 'deferred_customize_widgets' => true, 89 | ), 90 | 'https_resource_proxy' => HTTPS_Resource_Proxy::default_config(), 91 | 'widget_posts' => Widget_Posts::default_config(), 92 | 93 | 'memory_limit' => '256M', 94 | 'max_memory_usage_percentage' => 0.75, 95 | ); 96 | 97 | $this->config = array_merge( $default_config, $config ); 98 | } 99 | 100 | /** 101 | * Return whether the given module is active. 102 | * 103 | * @param string $module_name 104 | * @throws Exception 105 | * @return bool 106 | */ 107 | function is_module_active( $module_name ) { 108 | if ( ! array_key_exists( $module_name, $this->config['active_modules'] ) ) { 109 | throw new Exception( "Unrecognized module_name: $module_name" ); 110 | } 111 | return ! empty( $this->config['active_modules'][ $module_name ] ); 112 | } 113 | 114 | /** 115 | * @action after_setup_theme 116 | */ 117 | function init() { 118 | global $wp_widget_factory; 119 | $this->widget_factory = $wp_widget_factory; 120 | $this->config = apply_filters( 'customize_widgets_plus_plugin_config', $this->config, $this ); 121 | 122 | // Handle conflicting modules and dependencies. 123 | if ( $this->config['active_modules']['optimized_widget_registration'] ) { 124 | $this->config['active_modules']['widget_posts'] = true; 125 | } 126 | if ( $this->config['active_modules']['widget_posts'] ) { 127 | $this->config['active_modules']['non_autoloaded_widget_options'] = false; // The widget_posts module makes this obsolete. 128 | $this->config['active_modules']['widget_number_incrementing'] = true; // Dependency. 129 | } 130 | 131 | add_action( 'wp_default_scripts', array( $this, 'register_scripts' ), 11 ); 132 | add_action( 'wp_default_styles', array( $this, 'register_styles' ), 11 ); 133 | add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) ); 134 | add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); 135 | 136 | if ( $this->is_running_unit_tests() ) { 137 | $this->config['active_modules'] = array_fill_keys( array_keys( $this->config['active_modules'] ), false ); 138 | } 139 | 140 | if ( $this->config['disable_widgets_init'] ) { 141 | $this->disable_widgets_init(); 142 | } 143 | if ( $this->config['disable_widgets_factory'] ) { 144 | $this->disable_widgets_factory(); 145 | } 146 | 147 | if ( $this->is_module_active( 'non_autoloaded_widget_options' ) ) { 148 | $this->non_autoloaded_widget_options = new Non_Autoloaded_Widget_Options( $this ); 149 | } 150 | if ( $this->is_module_active( 'widget_number_incrementing' ) ) { 151 | $this->widget_number_incrementing = new Widget_Number_Incrementing( $this ); 152 | } 153 | if ( $this->is_module_active( 'https_resource_proxy' ) ) { 154 | $this->https_resource_proxy = new HTTPS_Resource_Proxy( $this ); 155 | } 156 | if ( $this->is_module_active( 'widget_posts' ) ) { 157 | $this->widget_posts = new Widget_Posts( $this ); 158 | } 159 | if ( $this->is_module_active( 'optimized_widget_registration' ) ) { 160 | $this->optimized_widget_registration = new Optimized_Widget_Registration( $this ); 161 | } 162 | if ( $this->is_module_active( 'deferred_customize_widgets' ) ) { 163 | $this->deferred_customize_widgets = new Deferred_Customize_Widgets( $this ); 164 | } 165 | } 166 | 167 | /** 168 | * Register scripts. 169 | * 170 | * @param \WP_Scripts $wp_scripts 171 | * @action wp_default_scripts 172 | */ 173 | function register_scripts( \WP_Scripts $wp_scripts ) { 174 | $slug = 'base'; 175 | $handle = "{$this->slug}-{$slug}"; 176 | $src = $this->dir_url . 'js/base.js'; 177 | $deps = array(); 178 | $wp_scripts->add( $handle, $src, $deps ); 179 | $this->script_handles[ $slug ] = $handle; 180 | 181 | $slug = 'widget-number-incrementing'; 182 | $handle = "{$this->slug}-{$slug}"; 183 | $src = $this->dir_url . 'js/widget-number-incrementing.js'; 184 | $deps = array( $this->script_handles['base'], 'wp-util' ); 185 | $wp_scripts->add( $handle, $src, $deps ); 186 | $this->script_handles[ $slug ] = $handle; 187 | 188 | $slug = 'widget-number-incrementing-customizer'; 189 | $handle = "{$this->slug}-{$slug}"; 190 | $src = $this->dir_url . 'js/widget-number-incrementing-customizer.js'; 191 | $deps = array( 'customize-widgets', $this->script_handles['widget-number-incrementing'] ); 192 | $wp_scripts->add( $handle, $src, $deps ); 193 | $this->script_handles[ $slug ] = $handle; 194 | 195 | $slug = 'https-resource-proxy'; 196 | $handle = "{$this->slug}-{$slug}"; 197 | $src = $this->dir_url . 'js/https-resource-proxy.js'; 198 | $deps = array( 'jquery' ); 199 | $wp_scripts->add( $handle, $src, $deps ); 200 | $this->script_handles[ $slug ] = $handle; 201 | 202 | $slug = 'deferred-customize-widgets'; 203 | $handle = "{$this->slug}-{$slug}"; 204 | $src = $this->dir_url . 'js/deferred-customize-widgets.js'; 205 | $deps = array( 'jquery', 'customize-widgets' ); 206 | $wp_scripts->add( $handle, $src, $deps ); 207 | $this->script_handles[ $slug ] = $handle; 208 | } 209 | 210 | /** 211 | * Register styles. 212 | * 213 | * @param \WP_Styles $wp_styles 214 | * @action wp_default_styles 215 | */ 216 | function register_styles( \WP_Styles $wp_styles ) { 217 | $slug = 'customize-widgets'; 218 | $handle = "{$this->slug}-{$slug}"; 219 | $src = $this->dir_url . 'css/customize-widgets.css'; 220 | $wp_styles->add( $handle, $src ); 221 | $this->style_handles[ $slug ] = $handle; 222 | 223 | $handle = "{$this->slug}-post-edit"; 224 | $src = $this->dir_url . 'css/post-edit.css'; 225 | $wp_styles->add( $handle, $src ); 226 | $this->style_handles['post_edit'] = $handle; 227 | } 228 | 229 | /** 230 | * @action customize_controls_enqueue_scripts 231 | */ 232 | function customize_controls_enqueue_scripts() { 233 | wp_enqueue_style( $this->style_handles['customize-widgets'] ); 234 | } 235 | 236 | /** 237 | * @action admin_enqueue_scripts 238 | */ 239 | function admin_enqueue_scripts() { 240 | wp_enqueue_style( $this->style_handles['post_edit'] ); 241 | } 242 | 243 | /** 244 | * Disable default widgets from being registered. 245 | * 246 | * @see wp_widgets_init() 247 | * @see Plugin::disable_widgets_factory() 248 | */ 249 | function disable_widgets_init() { 250 | $priority = has_action( 'init', 'wp_widgets_init' ); 251 | if ( false !== $priority ) { 252 | remove_action( 'init', 'wp_widgets_init', $priority ); 253 | } 254 | } 255 | 256 | /** 257 | * Disable the widget factory from registering widgets. 258 | * 259 | * @return int|false Priority that the widgets_init action was registered at. 260 | * @see \WP_Widget_Factory::_register_widgets() 261 | * @see Plugin::disable_widgets_init() 262 | */ 263 | function disable_widgets_factory() { 264 | $widgets_init_hook = 'widgets_init'; 265 | $callable = array( $this->widget_factory, '_register_widgets' ); 266 | $priority = has_action( $widgets_init_hook, $callable ); 267 | if ( false !== $priority ) { 268 | remove_action( $widgets_init_hook, $callable, $priority ); 269 | } 270 | return $priority; 271 | } 272 | 273 | /** 274 | * Return whether unit tests are currently running. 275 | * 276 | * @return bool 277 | */ 278 | function is_running_unit_tests() { 279 | return function_exists( 'tests_add_filter' ); 280 | } 281 | 282 | /** 283 | * Determine if a given registered widget is a normal multi widget. 284 | * 285 | * @param array $registered_widget { 286 | * @type string $name 287 | * @type string $id 288 | * @type callable $callback 289 | * @type array $params 290 | * @type string $classname 291 | * @type string $description 292 | * } 293 | * @return bool 294 | */ 295 | function is_registered_multi_widget( array $registered_widget ) { 296 | $is_multi_widget = ( is_array( $registered_widget['callback'] ) && $registered_widget['callback'][0] instanceof \WP_Widget ); 297 | if ( ! $is_multi_widget ) { 298 | return false; 299 | } 300 | /** @var \WP_Widget $widget_obj */ 301 | $widget_obj = $registered_widget['callback'][0]; 302 | $parsed_widget_id = $this->parse_widget_id( $registered_widget['id'] ); 303 | if ( ! $parsed_widget_id || $parsed_widget_id['id_base'] !== $widget_obj->id_base ) { 304 | return false; 305 | } 306 | if ( ! $this->is_normal_multi_widget( $widget_obj ) ) { 307 | return false; 308 | } 309 | return true; 310 | } 311 | 312 | /** 313 | * Get list of registered WP_Widgets keyed by id_base. 314 | * 315 | * @return \WP_Widget[] 316 | */ 317 | function get_registered_widget_objects() { 318 | global $wp_registered_widgets; 319 | $widget_objs = array(); 320 | foreach ( $wp_registered_widgets as $registered_widget ) { 321 | if ( $this->is_registered_multi_widget( $registered_widget ) ) { 322 | $widget_obj = $registered_widget['callback'][0]; 323 | $widget_class = get_class( $widget_obj ); 324 | if ( ! array_key_exists( $widget_class, $widget_objs ) ) { 325 | $widget_objs[ $widget_obj->id_base ] = $widget_obj; 326 | } 327 | } 328 | } 329 | return $widget_objs; 330 | } 331 | 332 | /** 333 | * @param string $id_base 334 | * @return bool 335 | */ 336 | function is_recognized_widget_id_base( $id_base ) { 337 | return array_key_exists( $id_base, $this->get_registered_widget_objects() ); 338 | } 339 | 340 | /** 341 | * Parse a widget ID into its components. 342 | * 343 | * @param string $widget_id 344 | * @return array|null { 345 | * @type string $id_base 346 | * @type int $widget_number 347 | * } 348 | */ 349 | function parse_widget_id( $widget_id ) { 350 | if ( preg_match( '/^(?P.+)-(?P\d+)$/', $widget_id, $matches ) ) { 351 | $matches['widget_number'] = intval( $matches['widget_number'] ); 352 | // @todo assert that id_base is valid 353 | return $matches; 354 | } else { 355 | return null; 356 | } 357 | } 358 | 359 | /** 360 | * Determine if an object is a WP_Widget has an id_base and option_name. 361 | * 362 | * @param \WP_Widget $widget_obj 363 | * @return bool 364 | */ 365 | function is_normal_multi_widget( $widget_obj ) { 366 | if ( ! ( $widget_obj instanceof \WP_Widget ) ) { 367 | return false; 368 | } 369 | if ( ! preg_match( '/^widget_(?P.+)/', $widget_obj->option_name, $matches ) ) { 370 | return false; 371 | } 372 | if ( $widget_obj->id_base !== $matches['id_base'] ) { 373 | return false; 374 | } 375 | return true; 376 | } 377 | 378 | 379 | /** 380 | * Get the memory limit in bytes. 381 | * 382 | * Uses memory_limit from php.ini, WP_MAX_MEMORY_LIMIT, and memory_limit 383 | * plugin config, whichever is smallest. Note that -1 is considered infinity. 384 | * 385 | * @return int 386 | */ 387 | function get_memory_limit() { 388 | $memory_limit = $this->parse_byte_size( ini_get( 'memory_limit' ) ); 389 | if ( $memory_limit <= 0 ) { 390 | $memory_limit = $this->parse_byte_size( \WP_MAX_MEMORY_LIMIT ); 391 | } 392 | if ( $memory_limit <= 0 ) { 393 | $memory_limit = \PHP_INT_MAX; 394 | } 395 | $memory_limit = min( 396 | $memory_limit, 397 | $this->parse_byte_size( $this->config['memory_limit'] ) 398 | ); 399 | return $memory_limit; 400 | } 401 | 402 | /** 403 | * Clear all of the caches for memory management 404 | * 405 | * Adapted from WPCOM_VIP_CLI_Command. 406 | * 407 | * @see \WPCOM_VIP_CLI_Command::stop_the_insanity() 408 | * 409 | * @return bool Whether memory was garbage-collected. 410 | */ 411 | function stop_the_insanity() { 412 | 413 | $memory_limit = $this->get_memory_limit(); 414 | $used_memory = memory_get_usage(); 415 | $used_memory_percentage = (float) $used_memory / $memory_limit; 416 | 417 | // Do nothing if we haven't reached the memory limit threshold 418 | if ( $used_memory_percentage < $this->config['max_memory_usage_percentage'] ) { 419 | return false; 420 | } 421 | 422 | /** 423 | * @var \WP_Object_Cache $wp_object_cache 424 | * @var \wpdb $wpdb 425 | */ 426 | global $wpdb, $wp_object_cache; 427 | 428 | $wpdb->queries = array(); // or define( 'WP_IMPORTING', true ); 429 | 430 | if ( is_object( $wp_object_cache ) ) { 431 | $wp_object_cache->group_ops = array(); 432 | $wp_object_cache->stats = array(); 433 | $wp_object_cache->memcache_debug = array(); 434 | $wp_object_cache->cache = array(); 435 | 436 | if ( method_exists( $wp_object_cache, '__remoteset' ) ) { 437 | $wp_object_cache->__remoteset(); // important 438 | } 439 | } 440 | 441 | return true; 442 | } 443 | 444 | /** 445 | * Obtain an integer byte size from a byte string like 1024K, 65M, 1G. 446 | * 447 | * @param int|string $bytes Integer or string like "1024K", "64M" or "1G" 448 | * @return int|null Number of bytes, or null if parse error 449 | */ 450 | public function parse_byte_size( $bytes ) { 451 | if ( is_int( $bytes ) ) { 452 | return $bytes; // already bytes, so no-op 453 | } 454 | if ( ! preg_match( '/^(-?\d+)([BKMG])?$/', strtoupper( $bytes ), $matches ) ) { 455 | return null; 456 | } 457 | $value = intval( $matches[1] ); 458 | $unit = empty( $matches[2] ) ? 'B' : $matches[2]; 459 | if ( 'K' === $unit ) { 460 | $value *= 1024; 461 | } elseif ( 'M' === $unit ) { 462 | $value *= pow( 1024, 2 ); 463 | } elseif ( 'G' === $unit ) { 464 | $value *= pow( 1024, 3 ); 465 | } 466 | return $value; 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /php/class-widget-number-incrementing.php: -------------------------------------------------------------------------------- 1 | plugin = $plugin; 35 | 36 | add_action( 'widgets_init', array( $this, 'store_widget_objects' ), 90 ); 37 | add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'ajax_incr_widget_number' ) ); 38 | add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) ); 39 | add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); 40 | add_filter( 'customize_refresh_nonces', array( $this, 'filter_customize_refresh_nonces' ) ); 41 | } 42 | 43 | /** 44 | * @action widgets_init, 90 45 | */ 46 | function store_widget_objects() { 47 | $this->widget_objs = array(); 48 | foreach ( $this->plugin->widget_factory->widgets as $widget_obj ) { 49 | /** @var \WP_Widget $widget_obj */ 50 | if ( "widget_{$widget_obj->id_base}" !== $widget_obj->option_name ) { 51 | continue; 52 | } 53 | $this->widget_objs[ $widget_obj->id_base ] = $widget_obj; 54 | } 55 | } 56 | 57 | /** 58 | * @see Widget_Management::gather_registered_widget_types() 59 | * @see Widget_Management::get_widget_number() 60 | * @see Widget_Management::set_widget_number() 61 | * @param string $id_base 62 | * 63 | * @throws Exception 64 | * @return string 65 | */ 66 | protected function get_option_key_for_widget_number( $id_base ) { 67 | $option_name = "{$id_base}_max_widget_number"; 68 | if ( strlen( $option_name ) > 64 ) { 69 | throw new Exception( "option_name is too long: $option_name" ); 70 | } 71 | return $option_name; 72 | } 73 | 74 | /** 75 | * Get the max existing widget number for a given id_base. 76 | * 77 | * @param $id_base 78 | * @see \next_widget_id_number() 79 | * @throws Exception 80 | * 81 | * @return int 82 | */ 83 | function get_max_existing_widget_number( $id_base ) { 84 | if ( empty( $this->widget_objs[ $id_base ] ) ) { 85 | throw new Exception( "No WP_Widget instance captured for $id_base" ); 86 | } 87 | $widget_obj = $this->widget_objs[ $id_base ]; 88 | // @todo There should be a pre_existing_widget_numbers, pre_max_existing_widget_number filter, or pre_existing_widget_ids to short circuit the expensive WP_Widget::get_settings() 89 | 90 | $settings = $widget_obj->get_settings(); 91 | if ( $settings instanceof \ArrayAccess && method_exists( $settings, 'getArrayCopy' ) ) { 92 | /** @see Widget_Settings */ 93 | $settings = $settings->getArrayCopy( $settings ); 94 | } 95 | 96 | $widget_numbers = array_keys( $settings ); 97 | $widget_numbers[] = 2; // multi-widgets start numbering at 2 98 | return max( $widget_numbers ); 99 | } 100 | 101 | /** 102 | * @param string $id_base 103 | * @return bool 104 | */ 105 | function add_widget_number_option( $id_base ) { 106 | $was_added = false; 107 | $option_name = $this->get_option_key_for_widget_number( $id_base ); 108 | if ( false === get_option( $option_name, false ) ) { 109 | $widget_number = $this->get_max_existing_widget_number( $id_base ); 110 | // Note: We must use non-autoloaded option due to https://core.trac.wordpress.org/ticket/31245 111 | $was_added = add_option( $this->get_option_key_for_widget_number( $id_base ), $widget_number, '', 'no' ); 112 | } 113 | return $was_added; 114 | } 115 | 116 | /** 117 | * Get the maximum widget number used for a given id_base. 118 | * 119 | * @param string $id_base 120 | * 121 | * @return int 122 | */ 123 | function get_widget_number( $id_base ) { 124 | $number = get_option( $this->get_option_key_for_widget_number( $id_base ), false ); 125 | if ( false === $number ) { 126 | return $this->get_max_existing_widget_number( $id_base ); 127 | } else { 128 | return intval( $number ); 129 | } 130 | } 131 | 132 | /** 133 | * Set the maximum widget number used for the given id_base. 134 | * 135 | * @param string $id_base 136 | * @param int $number 137 | * @throws Exception 138 | * @return int 139 | */ 140 | function set_widget_number( $id_base, $number ) { 141 | $this->add_widget_number_option( $id_base ); 142 | $existing_number = $this->get_widget_number( $id_base ); 143 | $number = max( 144 | 2, // multi-widget numbering starts here 145 | $this->get_widget_number( $id_base ), 146 | $this->get_max_existing_widget_number( $id_base ), 147 | $number 148 | ); 149 | if ( $existing_number !== $number ) { 150 | update_option( $this->get_option_key_for_widget_number( $id_base ), $number ); 151 | } 152 | return $number; 153 | } 154 | 155 | /** 156 | * Increment and return the widget number for the given id_base. 157 | * 158 | * @see \wp_cache_incr() 159 | * @param string $id_base 160 | * @throws Exception 161 | * @return int 162 | */ 163 | function incr_widget_number( $id_base ) { 164 | $this->add_widget_number_option( $id_base ); 165 | $number = max( 166 | 2, // multi-widget numbering starts here 167 | $this->get_widget_number( $id_base ), 168 | $this->get_max_existing_widget_number( $id_base ) 169 | ); 170 | $number += 1; 171 | if ( ! update_option( $this->get_option_key_for_widget_number( $id_base ), $number ) ) { 172 | throw new Exception( 'Unable to increment option.', 500 ); 173 | } 174 | return $number; 175 | } 176 | 177 | /** 178 | * Export data to JS. 179 | */ 180 | function export_script_data() { 181 | $exports = array( 182 | 'nonce' => wp_create_nonce( self::AJAX_ACTION ), 183 | 'action' => self::AJAX_ACTION, 184 | 'retryCount' => 5, 185 | ); 186 | 187 | wp_scripts()->add_data( 188 | $this->plugin->script_handles['widget-number-incrementing'], 189 | 'data', 190 | sprintf( 'var _customizeWidgetsPlusWidgetNumberIncrementingExports = %s;', wp_json_encode( $exports ) ) 191 | ); 192 | } 193 | 194 | /** 195 | * @action admin_enqueue_scripts 196 | */ 197 | function admin_enqueue_scripts() { 198 | if ( 'widgets' !== get_current_screen()->id ) { 199 | return; 200 | } 201 | 202 | $admin_widgets_script = wp_scripts()->registered['admin-widgets']; 203 | $admin_widgets_script->src = $this->plugin->dir_url . 'core-patched/wp-admin/js/widgets.js'; 204 | $admin_widgets_script->deps[] = $this->plugin->script_handles['widget-number-incrementing']; 205 | $admin_widgets_script->deps[] = 'wp-util'; 206 | $this->export_script_data(); 207 | } 208 | 209 | /** 210 | * Add script which integrates with Widget Customizer. 211 | * 212 | * @action customize_controls_enqueue_scripts 213 | */ 214 | function customize_controls_enqueue_scripts() { 215 | global $wp_customize; 216 | 217 | // Abort if widgets component is disabled or the user can't manage widgets. 218 | if ( ! isset( $wp_customize->widgets ) || ! current_user_can( 'edit_theme_options' ) ) { 219 | return; 220 | } 221 | 222 | wp_enqueue_script( $this->plugin->script_handles['widget-number-incrementing-customizer'] ); 223 | $this->export_script_data(); 224 | } 225 | 226 | /** 227 | * Handle Ajax request to increment the widget number. 228 | * 229 | * @see Widget_Management::request_incr_widget_number() 230 | */ 231 | function ajax_incr_widget_number() { 232 | try { 233 | if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] ) { // input var okay; sanitization ok 234 | throw new Exception( 'POST method required', 405 ); 235 | } 236 | false && check_ajax_referer(); // Bypass erroneous nonce verification complaint. 237 | $params = array( 238 | 'nonce' => isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : null, // WPCS: input var okay, [not needed after WPCS upgrade:] sanitization ok. 239 | 'id_base' => isset( $_POST['idBase'] ) ? sanitize_text_field( wp_unslash( $_POST['idBase'] ) ) : null, // WPCS: input var okay, [not needed after WPCS upgrade:] sanitization ok. 240 | ); 241 | wp_send_json_success( $this->request_incr_widget_number( $params ) ); 242 | } catch ( Exception $e ) { 243 | if ( $e->getCode() >= 400 && $e->getCode() < 600 ) { 244 | $code = $e->getCode(); 245 | $message = $e->getMessage(); 246 | } else { 247 | $code = 500; 248 | $message = 'Exception'; 249 | } 250 | wp_send_json_error( compact( 'code', 'message' ) ); 251 | } 252 | } 253 | 254 | /** 255 | * Do logic for incr_widget_number Ajax request. 256 | * 257 | * @see Widget_Management::ajax_incr_widget_number() 258 | * @param array $params 259 | * @return array 260 | * @throws Exception 261 | */ 262 | function request_incr_widget_number( array $params ) { 263 | if ( ! is_user_logged_in() ) { 264 | throw new Exception( 'not_logged_in', 403 ); 265 | } 266 | if ( ! current_user_can( 'edit_theme_options' ) ) { 267 | throw new Exception( 'unauthorized', 403 ); 268 | } 269 | if ( empty( $params['nonce'] ) ) { 270 | throw new Exception( 'missing_nonce_param', 400 ); 271 | } 272 | if ( empty( $params['id_base'] ) ) { 273 | throw new Exception( 'missing_id_base_param', 400 ); 274 | } 275 | if ( ! wp_verify_nonce( $params['nonce'], 'incr_widget_number' ) ) { 276 | throw new Exception( 'invalid_nonce', 403 ); 277 | } 278 | if ( ! $this->plugin->is_recognized_widget_id_base( $params['id_base'] ) ) { 279 | throw new Exception( 'unrecognized_id_base', 400 ); 280 | } 281 | 282 | $number = $this->incr_widget_number( $params['id_base'] ); 283 | return compact( 'number' ); 284 | } 285 | 286 | /** 287 | * @filter customize_refresh_nonces 288 | * @param array $nonces 289 | * @return array 290 | */ 291 | function filter_customize_refresh_nonces( $nonces ) { 292 | $nonces['incrWidgetNumber'] = wp_create_nonce( 'incr_widget_number' ); 293 | return $nonces; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /php/class-widget-posts-cli-command.php: -------------------------------------------------------------------------------- 1 | widget_posts ) ) { 29 | static::$plugin_instance->widget_posts = new Widget_Posts( static::$plugin_instance ); 30 | } 31 | return static::$plugin_instance->widget_posts; 32 | } 33 | 34 | /** 35 | * Enable looking for widgets in posts instead of options. You should run migrate first. 36 | */ 37 | public function enable() { 38 | if ( $this->get_widget_posts()->is_enabled() ) { 39 | \WP_CLI::warning( 'Widget Posts already enabled.' ); 40 | } else { 41 | $result = $this->get_widget_posts()->enable(); 42 | if ( $result ) { 43 | \WP_CLI::success( 'Widget Posts enabled.' ); 44 | } else { 45 | \WP_CLI::error( 'Failed to enable Widget Posts.' ); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Disable looking for widgets in posts instead of options. 52 | */ 53 | public function disable() { 54 | if ( ! $this->get_widget_posts()->is_enabled() ) { 55 | \WP_CLI::warning( 'Widget Posts already disabled.' ); 56 | } else { 57 | $result = $this->get_widget_posts()->disable(); 58 | if ( $result ) { 59 | \WP_CLI::success( 'Widget Posts disabled.' ); 60 | } else { 61 | \WP_CLI::error( 'Failed to disable Widget Posts.' ); 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Number of widgets updated. 68 | * 69 | * @var int 70 | */ 71 | protected $updated_count = 0; 72 | 73 | /** 74 | * Number of widgets skipped. 75 | * 76 | * @var int 77 | */ 78 | protected $skipped_count = 0; 79 | 80 | /** 81 | * Number of widgets inserted. 82 | * 83 | * @var int 84 | */ 85 | protected $inserted_count = 0; 86 | 87 | /** 88 | * Number of import-failed widgets. 89 | * 90 | * @var int 91 | */ 92 | protected $failed_count = 0; 93 | 94 | /** 95 | * @param array $options 96 | */ 97 | protected function add_import_actions( $options ) { 98 | $this->updated_count = 0; 99 | $this->skipped_count = 0; 100 | $this->inserted_count = 0; 101 | $this->failed_count = 0; 102 | 103 | add_action( 'widget_posts_import_skip_existing', function( $context ) use ( $options ) { 104 | /** 105 | * @var array $context { 106 | * @type string $widget_id 107 | * @type array $instance 108 | * @type int $widget_number 109 | * @type string $id_base 110 | * } 111 | */ 112 | \WP_CLI::line( "Skipping already-imported widget $context[widget_id] (to update, call with --update)." ); 113 | $this->skipped_count += 1; 114 | } ); 115 | 116 | add_action( 'widget_posts_import_success', function( $context ) use ( $options ) { 117 | /** 118 | * @var array $context { 119 | * @type string $widget_id 120 | * @type \WP_Post $post 121 | * @type array $instance 122 | * @type int $widget_number 123 | * @type string $id_base 124 | * @type bool $update 125 | * } 126 | */ 127 | if ( $context['update'] ) { 128 | $message = "Updated widget $context[widget_id]."; 129 | $this->updated_count += 1; 130 | } else { 131 | $message = "Inserted widget $context[widget_id]."; 132 | $this->inserted_count += 1; 133 | } 134 | if ( $options['dry-run'] ) { 135 | $message .= ' (DRY RUN)'; 136 | } 137 | \WP_CLI::success( $message ); 138 | } ); 139 | 140 | add_action( 'widget_posts_import_failure', function( $context ) { 141 | /** 142 | * @var array $context { 143 | * @type string $widget_id 144 | * @type Exception $exception 145 | * @type array $instance 146 | * @type int $widget_number 147 | * @type string $id_base 148 | * @type bool $update 149 | * } 150 | */ 151 | $exception = $context['exception']; 152 | \WP_CLI::warning( "Failed to import $context[widget_id]: " . $exception->getMessage() ); 153 | $this->failed_count += 1; 154 | } ); 155 | } 156 | 157 | /** 158 | * Remove actions added by add_import_actions(). 159 | * 160 | * @see Widget_Posts_CLI_Command::add_import_actions() 161 | */ 162 | protected function remove_import_actions() { 163 | remove_all_actions( 'widget_posts_import_skip_existing' ); 164 | remove_all_actions( 'widget_posts_import_success' ); 165 | remove_all_actions( 'widget_posts_import_failure' ); 166 | } 167 | 168 | /** 169 | * Write out summary of data collected by actions in add_import_actions(). 170 | * 171 | * @see Widget_Posts_CLI_Command::add_import_actions() 172 | */ 173 | protected function write_import_summary() { 174 | \WP_CLI::line(); 175 | \WP_CLI::line( "Skipped: $this->skipped_count" ); 176 | \WP_CLI::line( "Updated: $this->updated_count" ); 177 | \WP_CLI::line( "Inserted: $this->inserted_count" ); 178 | \WP_CLI::line( "Failed: $this->failed_count" ); 179 | } 180 | 181 | /** 182 | * Migrate widget instances from options into posts. Posts that already exist for given widget IDs will not be-imported unless --update is supplied. 183 | * 184 | * ## OPTIONS 185 | * 186 | * --update 187 | * : Update any widget instance posts already migrated/imported. This would override any changes made since the last migration. 188 | * 189 | * --dry-run 190 | * : Show what would be migrated. 191 | * 192 | * --verbose 193 | * : Show more info about what is going on. 194 | * 195 | * @param array [$id_bases] 196 | * @param array $options 197 | * @synopsis [...] [--dry-run] [--update] [--verbose] 198 | */ 199 | public function migrate( $id_bases, $options ) { 200 | try { 201 | if ( ! defined( 'WP_IMPORTING' ) ) { 202 | define( 'WP_IMPORTING', true ); 203 | } 204 | $widget_posts = $this->get_widget_posts(); 205 | 206 | $options = array_merge( 207 | array( 208 | 'update' => false, 209 | 'verbose' => false, 210 | 'dry-run' => false, 211 | ), 212 | $options 213 | ); 214 | if ( empty( $id_bases ) ) { 215 | $id_bases = array_keys( $widget_posts->widget_objs ); 216 | } else { 217 | foreach ( $id_bases as $id_base ) { 218 | if ( ! array_key_exists( $id_base, $widget_posts->widget_objs ) ) { 219 | \WP_CLI::error( "Unrecognized id_base: $id_base" ); 220 | } 221 | } 222 | } 223 | 224 | $this->add_import_actions( $options ); 225 | 226 | // Note we disable the pre_option filters because we need to get the underlying wp_options. 227 | $widget_posts->pre_option_filters_disabled = true; 228 | foreach ( $id_bases as $id_base ) { 229 | $widget_obj = $widget_posts->widget_objs[ $id_base ]; 230 | $instances = $widget_obj->get_settings(); 231 | $widget_posts->import_widget_instances( $id_base, $instances, $options ); 232 | } 233 | $widget_posts->pre_option_filters_disabled = false; 234 | 235 | $this->write_import_summary(); 236 | $this->remove_import_actions(); 237 | 238 | } catch ( \Exception $e ) { 239 | \WP_CLI::error( sprintf( '%s: %s', get_class( $e ), $e->getMessage() ) ); 240 | } 241 | } 242 | 243 | /** 244 | * Import widget instances from a JSON dump. 245 | * 246 | * JSON may be in either of two formats: 247 | * {"search-123":{"title":"Buscar"}} 248 | * or 249 | * {"search":{"123":{"title":"Buscar"}}} 250 | * or 251 | * {"widget_search":{"123":{"title":"Buscar"}}} 252 | * or 253 | * {"version":5,"options":{"widget_search":"a:1:{i:123;a:1:{s:5:\"title\";s:6:\"Buscar\";}}"}} 254 | * 255 | * Posts that already exist for given widget IDs will not be-imported unless --update is supplied. 256 | * 257 | * ## OPTIONS 258 | * 259 | * --update 260 | * : Update any widget instance posts already migrated/imported. This would override any changes made since the last migration. 261 | * 262 | * --dry-run 263 | * : Show what would be migrated. 264 | * 265 | * --verbose 266 | * : Show more info about what is going on. 267 | * 268 | * @param array [$args] 269 | * @param array $options 270 | * @synopsis [] [--dry-run] [--update] [--verbose] 271 | */ 272 | public function import( $args, $options ) { 273 | try { 274 | if ( ! defined( 'WP_IMPORTING' ) ) { 275 | define( 'WP_IMPORTING', true ); 276 | } 277 | $widget_posts = $this->get_widget_posts(); 278 | 279 | $file = array_shift( $args ); 280 | if ( '-' === $file ) { 281 | $file = 'php://stdin'; 282 | } 283 | $options = array_merge( 284 | array( 285 | 'update' => false, 286 | 'verbose' => false, 287 | 'dry-run' => false, 288 | ), 289 | $options 290 | ); 291 | 292 | // @codingStandardsIgnoreStart 293 | $json = file_get_contents( $file ); 294 | // @codingStandardsIgnoreSEnd 295 | if ( false === $json ) { 296 | throw new Exception( "$file could not be read" ); 297 | } 298 | 299 | $data = json_decode( $json, true ); 300 | if ( json_last_error() ) { 301 | throw new Exception( 'JSON parse error, code: ' . json_last_error() ); 302 | } 303 | if ( ! is_array( $data ) ) { 304 | throw new Exception( 'Expected array JSON to be an array.' ); 305 | } 306 | 307 | $this->add_import_actions( $options ); 308 | 309 | // Reformat the data structure into a format that import_widget_instances() accepts. 310 | $first_key = key( $data ); 311 | if ( ! filter_var( $first_key, FILTER_VALIDATE_INT ) ) { 312 | $is_options_export = ( 313 | isset( $data['version'] ) 314 | && 315 | 5 === $data['version'] 316 | && 317 | isset( $data['options'] ) 318 | && 319 | is_array( $data['options'] ) 320 | ); 321 | if ( $is_options_export ) { 322 | // Format: {"version":5,"options":{"widget_search":"a:1:{i:123;a:1:{s:5:\"title\";s:6:\"Buscar\";}}"}}. 323 | $instances_by_type = array(); 324 | foreach ( $data['options'] as $option_name => $option_value ) { 325 | if ( ! preg_match( '/^widget_(?P.+)/', $option_name, $matches ) ) { 326 | continue; 327 | } 328 | if ( ! is_serialized( $option_value, true ) ) { 329 | \WP_CLI::warning( "Option $option_name is not valid serialized data as expected." ); 330 | continue; 331 | } 332 | $instances_by_type[ $matches['id_base'] ] = unserialize( $option_value ); 333 | } 334 | 335 | } else if ( array_key_exists( $first_key, $widget_posts->widget_objs ) ) { 336 | // Format: {"search":{"123":{"title":"Buscar"}}}. 337 | $instances_by_type = $data; 338 | 339 | } else { 340 | // Format: {"widget_search":{"123":{"title":"Buscar"}}}. 341 | $instances_by_type = array(); 342 | foreach ( $data as $key => $value ) { 343 | if ( ! preg_match( '/^widget_(?P.+)/', $key, $matches ) ) { 344 | throw new Exception( "Unexpected key: $key" ); 345 | } 346 | $instances_by_type[ $matches['id_base'] ] = $value; 347 | } 348 | } 349 | } else { 350 | // Format: {"search-123":{"title":"Buscar"}}. 351 | $instances_by_type = array(); 352 | foreach ( $data as $widget_id => $instance ) { 353 | $parsed_widget_id = $widget_posts->plugin->parse_widget_id( $widget_id ); 354 | if ( empty( $parsed_widget_id ) || empty( $parsed_widget_id['widget_number'] ) ) { 355 | \WP_CLI::warning( "Rejecting instance with invalid widget ID: $widget_id" ); 356 | continue; 357 | } 358 | if ( ! isset( $instances_by_type[ $parsed_widget_id['id_base'] ] ) ) { 359 | $instances_by_type[ $parsed_widget_id['id_base'] ] = array(); 360 | } 361 | $instances_by_type[ $parsed_widget_id['id_base'] ][ $parsed_widget_id['widget_number'] ] = $instance; 362 | } 363 | } 364 | 365 | // Import each of the instances. 366 | foreach ( $instances_by_type as $id_base => $instances ) { 367 | if ( ! is_array( $instances ) ) { 368 | \WP_CLI::warning( "Expected array for $id_base instances. Skipping unknown number of widgets." ); 369 | continue; 370 | } 371 | try { 372 | $widget_posts->import_widget_instances( $id_base, $instances, $options ); 373 | } catch ( Exception $e ) { 374 | \WP_CLI::warning( 'Skipping: ' . $e->getMessage() ); 375 | if ( is_array( $instances ) ) { 376 | $this->skipped_count += count( $instances ); 377 | } 378 | } 379 | } 380 | 381 | $this->write_import_summary(); 382 | $this->remove_import_actions(); 383 | } catch ( \Exception $e ) { 384 | \WP_CLI::error( sprintf( '%s: %s', get_class( $e ), $e->getMessage() ) ); 385 | } 386 | } 387 | 388 | /** 389 | * Show the instance data for the given widget ID in JSON format. 390 | * 391 | * ## OPTIONS 392 | * 393 | * @param array [$args] 394 | * @param array $assoc_args 395 | * @synopsis 396 | * @alias show 397 | */ 398 | public function get( $args, $assoc_args ) { 399 | try { 400 | $widget_id = array_shift( $args ); 401 | unset( $assoc_args ); 402 | 403 | $widget_posts = $this->get_widget_posts(); 404 | $post = $widget_posts->get_widget_post( $widget_id ); 405 | if ( ! $post ) { 406 | \WP_CLI::warning( "Widget post $widget_id does not exist." ); 407 | } else { 408 | $data = $widget_posts->get_widget_instance_data( $post ); 409 | echo json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) . "\n"; 410 | } 411 | } catch ( \Exception $e ) { 412 | \WP_CLI::error( sprintf( '%s: %s', get_class( $e ), $e->getMessage() ) ); 413 | } 414 | } 415 | 416 | /** 417 | * Update database to store base64-encoded data in post_content instead of post_content_filtered. 418 | * 419 | * ## DESCRIPTION 420 | * 421 | * Storing the raw data in post_content is required for doing proper import/export via WXR. 422 | * 423 | * ## OPTIONS 424 | * 425 | * --dry-run 426 | * : Show what would be migrated. 427 | * 428 | * --sleep-interval= 429 | * : Number of posts processed before sleep()'ing. Default 50. 430 | * 431 | * --sleep-duration= 432 | * : Number of seconds to sleep for. Default 5. 433 | * 434 | * @param array $args 435 | * @param array $assoc_args 436 | * @subcommand move-encoded-data-to-post-content 437 | * @synopsis [--dry-run] [--sleep-interval=] [--sleep-duration=] 438 | */ 439 | public function move_encoded_data_to_post_content( $args, $assoc_args ) { 440 | $widget_posts = $this->get_widget_posts(); 441 | unset( $args ); 442 | 443 | add_action( 'widget_posts_content_moved_message', function( $args ) { 444 | if ( 'success' === $args['type'] ) { 445 | \WP_CLI::success( $args['text'] ); 446 | } else if ( 'warning' === $args['type'] ) { 447 | \WP_CLI::warning( $args['text'] ); 448 | } else { 449 | \WP_CLI::line( $args['text'] ); 450 | } 451 | } ); 452 | 453 | $widget_posts->move_encoded_data_to_post_content( $assoc_args ); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /php/class-widget-settings.php: -------------------------------------------------------------------------------- 1 | get_settings(); 20 | * \WP_Widget::update_callback(): $this->save_settings($all_instances); 21 | * \WP_Widget::form_callback(): $all_instances = $this->get_settings(); 22 | * \WP_Widget::_register(): if ( is_array($settings) ) { 23 | * \WP_Widget::_register(): foreach ( array_keys($settings) as $number ) { 24 | * \WP_Widget::display_callback(): $instance = $this->get_settings(); 25 | * 26 | * @package CustomizeWidgetsPlus 27 | */ 28 | class Widget_Settings extends \ArrayIterator { 29 | 30 | /** 31 | * Keep track of all widget instances that were unset, as they will be deleted. 32 | * 33 | * @var int[] $unset_widget_numbers 34 | */ 35 | public $unset_widget_numbers = array(); 36 | 37 | /** 38 | * @param array $array 39 | * @throws Exception 40 | */ 41 | function __construct( $array ) { 42 | // Widget numbers start at 2. 43 | unset( $array[0] ); 44 | unset( $array[1] ); 45 | 46 | parent::__construct( $array ); 47 | } 48 | 49 | /** 50 | * \WP_Widget::update_callback(): $old_instance = isset($all_instances[$number]) ? $all_instances[$number] : array(); 51 | * \WP_Widget::display_callback(): if ( array_key_exists( $this->number, $instance ) ) { 52 | * \WP_Widget::get_settings(): if ( !empty($settings) && !array_key_exists('_multiwidget', $settings) ) { 53 | * 54 | * @param int|string $key Array key. 55 | * @return bool 56 | */ 57 | public function offsetExists( $key ) { 58 | if ( '_multiwidget' === $key ) { 59 | return true; 60 | } 61 | return parent::offsetExists( $key ); 62 | } 63 | 64 | /** 65 | * \WP_Widget::update_callback(): $old_instance = isset($all_instances[$number]) ? $all_instances[$number] : array(); 66 | * \WP_Widget::display_callback(): $instance = $instance[$this->number]; 67 | * \WP_Widget::form_callback(): $instance = $all_instances[ $widget_args['number'] ]; 68 | * 69 | * @param int|string $key Array key. 70 | * @return array|int|null 71 | */ 72 | public function offsetGet( $key ) { 73 | if ( '_multiwidget' === $key ) { 74 | return 1; 75 | } 76 | if ( ! $this->offsetExists( $key ) ) { 77 | return null; 78 | } 79 | $value = parent::offsetGet( $key ); 80 | if ( is_int( $value ) ) { 81 | // Fetch the widget and store it in the array. 82 | $post = get_post( $value ); 83 | $value = Widget_Posts::get_post_content( $post ); 84 | $this->offsetSet( $key, $value ); 85 | } 86 | return $value; 87 | } 88 | 89 | /** 90 | * \WP_Widget::update_callback(): $all_instances[$number] = $instance; 91 | * \WP_Widget::save_settings(): $settings['_multiwidget'] = 1; 92 | * 93 | * @param int|string $key Array key. 94 | * @param mixed $value The array item value. 95 | */ 96 | public function offsetSet( $key, $value ) { 97 | if ( '_multiwidget' === $key || '__i__' === $key ) { 98 | return; 99 | } 100 | $key = filter_var( $key, FILTER_VALIDATE_INT ); 101 | if ( ! is_int( $key ) ) { 102 | // @todo _doing_it_wrong()? 103 | return; 104 | } 105 | if ( $key < 2 ) { 106 | // @todo _doing_it_wrong()? 107 | return; 108 | } 109 | if ( ! is_array( $value ) ) { 110 | // @todo _doing_it_wrong()? 111 | return; 112 | } 113 | parent::offsetSet( $key, $value ); 114 | } 115 | 116 | /** 117 | * \WP_Widget::update_callback(): unset($all_instances[$number]); 118 | * \WP_Widget::get_settings(): unset($settings['_multiwidget'], $settings['__i__']); 119 | * 120 | * @param int|string $key Array key. 121 | */ 122 | public function offsetUnset( $key ) { 123 | if ( '_multiwidget' === $key || '__i__' === $key ) { 124 | return; 125 | } 126 | $key = filter_var( $key, FILTER_VALIDATE_INT ); 127 | if ( $key < 2 ) { 128 | return; 129 | } 130 | $this->unset_widget_numbers[] = $key; 131 | parent::offsetUnset( $key ); 132 | } 133 | 134 | /** 135 | * @return array 136 | */ 137 | public function current() { 138 | return $this->offsetGet( $this->key() ); 139 | } 140 | 141 | /** 142 | * Serialize the settings into an array. 143 | * 144 | * @return string 145 | */ 146 | public function serialize() { 147 | return serialize( $this->getArrayCopy() ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /php/class-wp-customize-widget-setting.php: -------------------------------------------------------------------------------- 1 | widget_(?P.+?))(?:\[(?P\d+)\])?$/'; 18 | 19 | /** 20 | * Setting type. 21 | * 22 | * @var string 23 | */ 24 | public $type = 'widget'; 25 | 26 | /** 27 | * Widget ID Base. 28 | * 29 | * @see \WP_Widget::$id_base 30 | * 31 | * @var string 32 | */ 33 | public $widget_id_base; 34 | 35 | /** 36 | * The multi widget number for this setting. 37 | * 38 | * @var int 39 | */ 40 | public $widget_number; 41 | 42 | /** 43 | * Whether or not preview() was called. 44 | * 45 | * @see WP_Customize_Widget_Setting::preview() 46 | * 47 | * @var bool 48 | */ 49 | public $is_previewed = false; 50 | 51 | /** 52 | * Widget Posts. 53 | * 54 | * @var Widget_Posts 55 | */ 56 | public $widget_posts; 57 | 58 | /** 59 | * Constructor. 60 | * 61 | * Any supplied $args override class property defaults. 62 | * 63 | * @param \WP_Customize_Manager $manager Manager instance. 64 | * @param string $id An specific ID of the setting. Can be a 65 | * theme mod or option name. 66 | * @param array $args Setting arguments. 67 | * @throws Exception If $id is not valid for a widget. 68 | */ 69 | public function __construct( \WP_Customize_Manager $manager, $id, array $args = array() ) { 70 | unset( $args['type'] ); 71 | 72 | if ( ! preg_match( static::WIDGET_SETTING_ID_PATTERN, $id, $matches ) ) { 73 | throw new Exception( "Illegal widget setting ID: $id" ); 74 | } 75 | $this->widget_id_base = $matches['widget_id_base']; 76 | 77 | if ( isset( $matches['widget_number'] ) ) { 78 | $this->widget_number = intval( $matches['widget_number'] ); 79 | } 80 | 81 | parent::__construct( $manager, $id, $args ); 82 | 83 | if ( empty( $this->widget_posts ) ) { 84 | throw new Exception( 'Missing argument: widget_posts' ); 85 | } 86 | } 87 | 88 | /** 89 | * Get the instance data for a given widget setting. 90 | * 91 | * @return string 92 | * @throws Exception 93 | */ 94 | public function value() { 95 | $value = $this->default; 96 | 97 | if ( ! isset( $this->widget_posts->current_widget_type_values[ $this->widget_id_base ] ) ) { 98 | throw new Exception( "current_widget_type_values not set yet for $this->widget_id_base. Current action: " . current_action() ); 99 | } 100 | if ( isset( $this->widget_posts->current_widget_type_values[ $this->widget_id_base ][ $this->widget_number ] ) ) { 101 | $value = $this->widget_posts->current_widget_type_values[ $this->widget_id_base ][ $this->widget_number ]; 102 | } 103 | return $value; 104 | } 105 | 106 | /** 107 | * Handle previewing the widget setting. 108 | * 109 | * Because this can be called very early in the WordPress execution flow, as 110 | * early as after_setup_theme, if widgets_init hasn't been called yet then 111 | * the preview logic is deferred to the widgets_init action. 112 | * 113 | * @see WP_Customize_Widget_Setting::apply_preview() 114 | * @throws Exception 115 | * 116 | * @return void 117 | */ 118 | public function preview() { 119 | if ( $this->is_previewed ) { 120 | return; 121 | } 122 | $this->is_previewed = true; 123 | 124 | if ( did_action( 'widgets_init' ) ) { 125 | $this->apply_preview(); 126 | } else { 127 | $priority = has_action( 'widgets_init', array( $this->widget_posts, 'capture_widget_settings_for_customizer' ) ); 128 | if ( empty( $priority ) ) { 129 | throw new Exception( 'Expected widgets_init action to do Widget_Posts::capture_widget_settings_for_customizer()' ); 130 | } 131 | $priority += 1; 132 | add_action( 'widgets_init', array( $this, 'apply_preview' ), $priority ); 133 | } 134 | } 135 | 136 | /** 137 | * Optionally-deferred continuation logic from the preview method. 138 | * 139 | * @see WP_Customize_Widget_Setting::preview() 140 | * 141 | * @throws Exception 142 | * @return void 143 | */ 144 | public function apply_preview() { 145 | if ( ! isset( $this->_original_value ) ) { 146 | $this->_original_value = $this->value(); 147 | } 148 | if ( ! isset( $this->_previewed_blog_id ) ) { 149 | $this->_previewed_blog_id = get_current_blog_id(); 150 | } 151 | $value = $this->post_value(); 152 | $is_null_because_previewing_new_widget = ( 153 | ! isset( $this->widget_posts->current_widget_type_values[ $this->widget_id_base ][ $this->widget_number ] ) 154 | && 155 | is_null( $value ) 156 | && 157 | $this->manager->doing_ajax( 'update-widget' ) 158 | && 159 | isset( $_REQUEST['widget-id'] ) // WPCS: input var okay. 160 | && 161 | ( $this->id === $this->manager->widgets->get_setting_id( sanitize_text_field( wp_unslash( $_REQUEST['widget-id'] ) ) ) ) // WPCS: input var okay, [not needed after WPCS upgrade:] sanitization ok. 162 | ); 163 | if ( $is_null_because_previewing_new_widget ) { 164 | $value = array(); 165 | } 166 | if ( ! is_null( $value ) ) { 167 | $this->widget_posts->current_widget_type_values[ $this->widget_id_base ][ $this->widget_number ] = $value; 168 | } 169 | 170 | add_action( "customize_post_value_set_{$this->id}", array( $this, '_update_current_widget_type_value' ) ); 171 | } 172 | 173 | /** 174 | * Make sure that the cached widget preview value is updated when the post value is updated. 175 | * 176 | * @access private 177 | */ 178 | function _update_current_widget_type_value() { 179 | $value = $this->post_value(); 180 | if ( ! is_null( $value ) ) { 181 | $this->widget_posts->current_widget_type_values[ $this->widget_id_base ][ $this->widget_number ] = $value; 182 | } 183 | } 184 | 185 | /** 186 | * Save the value of the widget setting. 187 | * 188 | * @param array|mixed $value The value to update. 189 | * @return bool 190 | */ 191 | protected function update( $value ) { 192 | // @todo Maybe more elegant to do $sanitizing->widget_objs[ $this->widget_id_base ]->get_settings() or $sanitizing->current_widget_type_values[ $this->widget_id_base ] 193 | $option_name = "widget_{$this->widget_id_base}"; 194 | $option_value = get_option( $option_name, array() ); 195 | $option_value[ $this->widget_number ] = $value; 196 | $option_value['_multiwidget'] = 1; // Ensure this is set so that wp_convert_widget_settings() won't be called in WP_Widget::get_settings(). 197 | $result = update_option( $option_name, $option_value ); 198 | 199 | if ( false !== $result && ! $this->is_previewed ) { 200 | $this->widget_posts->current_widget_type_values[ $this->widget_id_base ][ $this->widget_number ] = $value; 201 | } 202 | return $result; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | tests/ 17 | 18 | 19 | 20 | 21 | customize-widgets-plus 22 | 23 | 24 | 25 | 26 | 27 | ./ 28 | 29 | ./dev-lib 30 | ./node_modules 31 | ./tests 32 | ./vendor 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Customize Widgets Plus 3 | 4 | Lab features and a testbed for improvements to Widgets and the Customizer. 5 | 6 | **Contributors:** [westonruter](https://profiles.wordpress.org/westonruter), [xwp](https://profiles.wordpress.org/xwp), [newscorpau](https://profiles.wordpress.org/newscorpau) 7 | **Tags:** [customizer](https://wordpress.org/plugins/tags/customizer), [customize](https://wordpress.org/plugins/tags/customize), [widgets](https://wordpress.org/plugins/tags/widgets) 8 | **Requires at least:** trunk 9 | **Tested up to:** trunk 10 | **Stable tag:** trunk (master) 11 | **License:** [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html) 12 | 13 | [![Build Status](https://travis-ci.org/xwp/wp-customize-widgets-plus.svg?branch=master)](https://travis-ci.org/xwp/wp-customize-widgets-plus) 14 | 15 | ## Description ## 16 | 17 | This plugin consists of lab features and a testbed for improvements to Widgets and the Customizer. 18 | 19 | Requires PHP 5.3+. 20 | 21 | **Development of this plugin is done [on GitHub](https://github.com/xwp/wp-customize-widgets-plus). Pull requests welcome. Please see [issues](https://github.com/xwp/wp-customize-widgets-plus/issues) reported there before going to the [plugin forum](https://wordpress.org/support/plugin/customize-widgets-plus).** 22 | 23 | Current features: 24 | ### Non-Autoloaded Widget Options ### 25 | Widgets are stored in options (normally). Multi-widgets store all of their instances in one big serialized array specific to that type. When there are many widgets of a given type, the size of the serialized array can grow very large. What's more is that `WP_Widget` does not explicitly `add_option(... 'no' )` before calling `update_option()`, and so all of the settings get added with autoloading. This is very bad when using Memcached Object Cache specifically because it can result in the total `alloptions` cache key to become larger than 1MB and result in Memcached failing to store the value in the cache. On WordPress.com the result is a ["Matt's fault" error](https://github.com/Automattic/vip-quickstart/blob/master/www/wp-content/mu-plugins/alloptions-limit.php) which has to be fixed by the VIP team. Widget settings should not be stored in serialized arrays to begin with; each widget instance should be stored in a custom post type. But until this is done we should stop autoloading options. See also [#26876](https://core.trac.wordpress.org/ticket/26876) and [#23909](https://core.trac.wordpress.org/ticket/23909). 26 | 27 | ### Widget Number Incrementing ### 28 | Implements fixes for Core issue [#32183](https://core.trac.wordpress.org/ticket/32183) (Widget ID auto-increments conflict for concurrent users). The stored widget_number option provides a centralized auto-increment number for whenever a widget is instantiated, even widgets in the Customizer that are not yet saved. 29 | 30 | ### Efficient Multidimensional Setting Sanitizing ### 31 | Settings for multidimensional options and theme_mods are extremely inefficient to sanitize in Core because all of the Customizer settings registered for the subsets of the `option` or `theme_mod` need filters that are added to the entire value, meaning sanitizing one single setting will result in all filters for all other settings in that `option`/`theme_mod` will also get applied. This functionality seeks to improve this as much as possible, especially for widgets which are the worst offenders. Implements partial fix for [#32103](https://core.trac.wordpress.org/ticket/32103). 32 | 33 | ### HTTPS Resource Proxy ### 34 | When `FORCE_SSL_ADMIN` is enabled (such as on WordPress.com), the Customizer will load the site into the preview iframe using HTTPS as well. If, however, external resources are being referenced which are not HTTPS, they will fail to load due to the browser's security model raise mixed content warnings. This functionality will attempt to rewrite any HTTP URLs to be HTTPS ones via a WordPress-based proxy. 35 | 36 | ### Widget Posts ### 37 | Store widget instances in posts instead of options. Requires trunk due to patch committed in [#32474](https://core.trac.wordpress.org/ticket/32474). More details forthcoming... 38 | 39 | ### Deferred Widget Embedding ### 40 | Backports [#33898](https://core.trac.wordpress.org/ticket/33898) and [#33901](https://core.trac.wordpress.org/ticket/33901) which are now in `trunk` for WordPress 4.4. 41 | 42 | 43 | ## Changelog ## 44 | 45 | ### 0.2 ### 46 | Add Widget Posts module. See [changelog](https://github.com/xwp/wp-customize-widgets-plus/compare/0.1...0.2). 47 | 48 | ### 0.1 ### 49 | Initial release. 50 | 51 | 52 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Customize Widgets Plus === 2 | Contributors: westonruter, xwp, newscorpau 3 | Requires at least: trunk 4 | Tested up to: trunk 5 | Stable tag: trunk 6 | License: GPLv2 or later 7 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 8 | Tags: customizer, customize, widgets 9 | 10 | Lab features and a testbed for improvements to Widgets and the Customizer. 11 | 12 | == Description == 13 | 14 | This plugin consists of lab features and a testbed for improvements to Widgets and the Customizer. 15 | 16 | Requires PHP 5.3+. 17 | 18 | **Development of this plugin is done [on GitHub](https://github.com/xwp/wp-customize-widgets-plus). Pull requests welcome. Please see [issues](https://github.com/xwp/wp-customize-widgets-plus/issues) reported there before going to the [plugin forum](https://wordpress.org/support/plugin/customize-widgets-plus).** 19 | 20 | Current features: 21 | 22 | = Non-Autoloaded Widget Options = 23 | 24 | Widgets are stored in options (normally). Multi-widgets store all of their instances in one big serialized array specific to that type. When there are many widgets of a given type, the size of the serialized array can grow very large. What's more is that `WP_Widget` does not explicitly `add_option(... 'no' )` before calling `update_option()`, and so all of the settings get added with autoloading. This is very bad when using Memcached Object Cache specifically because it can result in the total `alloptions` cache key to become larger than 1MB and result in Memcached failing to store the value in the cache. On WordPress.com the result is a ["Matt's fault" error](https://github.com/Automattic/vip-quickstart/blob/master/www/wp-content/mu-plugins/alloptions-limit.php) which has to be fixed by the VIP team. Widget settings should not be stored in serialized arrays to begin with; each widget instance should be stored in a custom post type. But until this is done we should stop autoloading options. See also [#26876](https://core.trac.wordpress.org/ticket/26876) and [#23909](https://core.trac.wordpress.org/ticket/23909). 25 | 26 | = Widget Number Incrementing = 27 | 28 | Implements fixes for Core issue [#32183](https://core.trac.wordpress.org/ticket/32183) (Widget ID auto-increments conflict for concurrent users). The stored widget_number option provides a centralized auto-increment number for whenever a widget is instantiated, even widgets in the Customizer that are not yet saved. 29 | 30 | = Efficient Multidimensional Setting Sanitizing = 31 | 32 | Settings for multidimensional options and theme_mods are extremely inefficient to sanitize in Core because all of the Customizer settings registered for the subsets of the `option` or `theme_mod` need filters that are added to the entire value, meaning sanitizing one single setting will result in all filters for all other settings in that `option`/`theme_mod` will also get applied. This functionality seeks to improve this as much as possible, especially for widgets which are the worst offenders. Implements partial fix for [#32103](https://core.trac.wordpress.org/ticket/32103). 33 | 34 | = HTTPS Resource Proxy = 35 | 36 | When `FORCE_SSL_ADMIN` is enabled (such as on WordPress.com), the Customizer will load the site into the preview iframe using HTTPS as well. If, however, external resources are being referenced which are not HTTPS, they will fail to load due to the browser's security model raise mixed content warnings. This functionality will attempt to rewrite any HTTP URLs to be HTTPS ones via a WordPress-based proxy. 37 | 38 | = Widget Posts = 39 | 40 | Store widget instances in posts instead of options. Requires trunk due to patch committed in [#32474](https://core.trac.wordpress.org/ticket/32474). More details forthcoming... 41 | 42 | = Deferred Widget Embedding = 43 | 44 | Backports [#33898](https://core.trac.wordpress.org/ticket/33898) and [#33901](https://core.trac.wordpress.org/ticket/33901) which are now in `trunk` for WordPress 4.4. 45 | 46 | == Changelog == 47 | 48 | = 0.2 = 49 | Add Widget Posts module. See [changelog](https://github.com/xwp/wp-customize-widgets-plus/compare/0.1...0.2). 50 | 51 | = 0.1 = 52 | Initial release. 53 | -------------------------------------------------------------------------------- /svn-url: -------------------------------------------------------------------------------- 1 | https://plugins.svn.wordpress.org/customize-widgets-plus/ -------------------------------------------------------------------------------- /tests/data/class-acme-widget.php: -------------------------------------------------------------------------------- 1 | 'Acme', 13 | ) 14 | ); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tests/test-class-https-resource-proxy.php: -------------------------------------------------------------------------------- 1 | plugin ); 21 | 22 | $this->assertEquals( 10, has_action( 'init', array( $instance, 'add_rewrite_rule' ) ) ); 23 | $this->assertEquals( 10, has_filter( 'query_vars', array( $instance, 'filter_query_vars' ) ) ); 24 | $this->assertEquals( 10, has_filter( 'redirect_canonical', array( $instance, 'enforce_trailingslashing' ) ) ); 25 | $this->assertEquals( 10, has_action( 'template_redirect', array( $instance, 'handle_proxy_request' ) ) ); 26 | $this->assertEquals( 10, has_action( 'init', array( $instance, 'add_proxy_filtering' ) ) ); 27 | } 28 | 29 | /** 30 | * @see HTTPS_Resource_Proxy::default_config() 31 | */ 32 | function test_default_config() { 33 | $default_config = HTTPS_Resource_Proxy::default_config(); 34 | $this->assertArrayHasKey( 'min_cache_ttl', $default_config ); 35 | $this->assertArrayHasKey( 'customize_preview_only', $default_config ); 36 | $this->assertArrayHasKey( 'logged_in_users_only', $default_config ); 37 | $this->assertArrayHasKey( 'max_content_length', $default_config ); 38 | } 39 | 40 | /** 41 | * @see HTTPS_Resource_Proxy::config() 42 | */ 43 | function test_config() { 44 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 45 | $this->assertInternalType( 'int', $instance->config( 'min_cache_ttl' ) ); 46 | $this->assertInternalType( 'bool', $instance->config( 'customize_preview_only' ) ); 47 | $this->assertInternalType( 'bool', $instance->config( 'logged_in_users_only' ) ); 48 | $this->assertInternalType( 'int', $instance->config( 'max_content_length' ) ); 49 | } 50 | 51 | /** 52 | * @see HTTPS_Resource_Proxy::is_proxy_enabled() 53 | */ 54 | function test_is_proxy_enabled() { 55 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 56 | $this->assertInternalType( 'bool', $instance->is_proxy_enabled() ); 57 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_false', 5 ); 58 | $this->assertFalse( $instance->is_proxy_enabled() ); 59 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_true', 10 ); 60 | $this->assertTrue( $instance->is_proxy_enabled() ); 61 | } 62 | 63 | /** 64 | * @see HTTPS_Resource_Proxy::add_proxy_filtering() 65 | */ 66 | function test_add_proxy_filtering() { 67 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 68 | 69 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_true' ); 70 | $instance->add_proxy_filtering(); 71 | $this->assertEquals( 10, has_filter( 'script_loader_src', array( $instance, 'filter_script_loader_src' ) ) ); 72 | $this->assertEquals( 10, has_filter( 'style_loader_src', array( $instance, 'filter_style_loader_src' ) ) ); 73 | } 74 | 75 | /** 76 | * @see HTTPS_Resource_Proxy::filter_query_vars() 77 | */ 78 | function test_filter_query_vars() { 79 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 80 | $query_vars = $instance->filter_query_vars( array() ); 81 | $this->assertContains( HTTPS_Resource_Proxy::NONCE_QUERY_VAR, $query_vars ); 82 | $this->assertContains( HTTPS_Resource_Proxy::HOST_QUERY_VAR, $query_vars ); 83 | $this->assertContains( HTTPS_Resource_Proxy::PATH_QUERY_VAR, $query_vars ); 84 | } 85 | 86 | /** 87 | * @see HTTPS_Resource_Proxy::add_rewrite_rule() 88 | */ 89 | function test_add_rewrite_rule() { 90 | /** @var \WP_Rewrite $wp_rewrite */ 91 | global $wp_rewrite; 92 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 93 | $this->assertEmpty( $instance->rewrite_regex ); 94 | $instance->add_rewrite_rule(); 95 | $this->assertNotEmpty( $instance->rewrite_regex ); 96 | $this->assertArrayHasKey( $instance->rewrite_regex, $wp_rewrite->extra_rules_top ); 97 | } 98 | 99 | /** 100 | * @see HTTPS_Resource_Proxy::reserve_api_endpoint() 101 | */ 102 | function test_reserve_api_endpoint() { 103 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 104 | $instance->add_rewrite_rule(); 105 | $this->assertTrue( $instance->reserve_api_endpoint( false, $instance->config( 'endpoint' ) ) ); 106 | $this->assertFalse( $instance->reserve_api_endpoint( false, 'prefix-' . $instance->config( 'endpoint' ) . '-suffix' ) ); 107 | } 108 | 109 | /** 110 | * @see HTTPS_Resource_Proxy::filter_loader_src() 111 | * @see HTTPS_Resource_Proxy::filter_script_loader_src() 112 | * @see HTTPS_Resource_Proxy::filter_style_loader_src() 113 | */ 114 | function test_filter_loader_src() { 115 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 116 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_true' ); 117 | $instance->add_proxy_filtering(); 118 | 119 | $src = 'https://example.org/main.css?ver=1'; 120 | $this->assertEquals( $src, $instance->filter_loader_src( $src ) ); 121 | 122 | $src = 'http://example.org/main.css?ver=2'; 123 | $parsed_src = parse_url( $src ); 124 | 125 | $filtered_src = $instance->filter_loader_src( $src ); 126 | $this->assertNotEquals( $src, $filtered_src ); 127 | 128 | $this->assertNotEmpty( preg_match( '#' . $instance->rewrite_regex . '#', $filtered_src, $matches ) ); 129 | 130 | $this->assertNotEmpty( $matches['nonce'] ); 131 | $this->assertNotEmpty( $matches['host'] ); 132 | $this->assertNotEmpty( $matches['path'] ); 133 | 134 | $this->assertEquals( wp_create_nonce( HTTPS_Resource_Proxy::MODULE_SLUG ), $matches['nonce'] ); 135 | $this->assertEquals( $parsed_src['host'], $matches['host'] ); 136 | $this->assertEquals( '/main.css', $parsed_src['path'] ); 137 | $this->assertStringEndsWith( '?ver=2', $filtered_src ); 138 | 139 | $src = 'http://example.org/main.css?ver=2'; 140 | $this->assertEquals( $instance->filter_loader_src( $src ), $instance->filter_style_loader_src( $src ) ); 141 | $src = 'http://example.org/main.js?ver=2'; 142 | $this->assertEquals( $instance->filter_loader_src( $src ), $instance->filter_script_loader_src( $src ) ); 143 | } 144 | 145 | /** 146 | * @see HTTPS_Resource_Proxy::enqueue_scripts() 147 | */ 148 | function test_enqueue_scripts() { 149 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 150 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_true' ); 151 | $instance->add_proxy_filtering(); 152 | 153 | unset( $instance ); 154 | $wp_scripts = wp_scripts(); // and fire wp_default_scripts 155 | wp_styles(); // fire wp_default_styles 156 | do_action( 'wp_enqueue_scripts' ); 157 | $this->assertContains( $this->plugin->script_handles['https-resource-proxy'], $wp_scripts->queue ); 158 | $this->assertContains( 'var _httpsResourceProxyExports', $wp_scripts->get_data( $this->plugin->script_handles['https-resource-proxy'], 'data' ) ); 159 | } 160 | 161 | /** 162 | * @see HTTPS_Resource_Proxy::enforce_trailingslashing() 163 | */ 164 | function test_enforce_trailingslashing() { 165 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 166 | set_query_var( HTTPS_Resource_Proxy::PATH_QUERY_VAR, '/main.css' ); 167 | $instance->plugin->config[ HTTPS_Resource_Proxy::MODULE_SLUG ]['trailingslash_srcs'] = true; 168 | $this->assertStringEndsWith( '.js/', $instance->enforce_trailingslashing( 'http://example.com/main.js' ) ); 169 | $this->assertStringEndsWith( '.js/?ver=1', $instance->enforce_trailingslashing( 'http://example.com/main.js?ver=1' ) ); 170 | $instance->plugin->config[ HTTPS_Resource_Proxy::MODULE_SLUG ]['trailingslash_srcs'] = false; 171 | $this->assertStringEndsWith( '.js', $instance->enforce_trailingslashing( 'http://example.com/main.js/' ) ); 172 | $this->assertStringEndsWith( '.js?ver=1', $instance->enforce_trailingslashing( 'http://example.com/main.js/?ver=1' ) ); 173 | set_query_var( HTTPS_Resource_Proxy::PATH_QUERY_VAR, null ); 174 | } 175 | 176 | /** 177 | * @see HTTPS_Resource_Proxy::send_proxy_response() 178 | * @see HTTPS_Resource_Proxy::handle_proxy_request() 179 | */ 180 | function test_send_proxy_response_not_enabled() { 181 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 182 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_false' ); 183 | 184 | $exception = null; 185 | try { 186 | $instance->send_proxy_response( array() ); 187 | } catch ( Exception $e ) { 188 | $exception = $e; 189 | } 190 | $this->assertInstanceOf( __NAMESPACE__ . '\\Exception', $exception ); 191 | $this->assertEquals( 'proxy_not_enabled', $exception->getMessage() ); 192 | } 193 | 194 | /** 195 | * @see HTTPS_Resource_Proxy::send_proxy_response() 196 | * @see HTTPS_Resource_Proxy::handle_proxy_request() 197 | */ 198 | function test_send_proxy_response_bad_nonce() { 199 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 200 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_true' ); 201 | 202 | $exception = null; 203 | try { 204 | $instance->send_proxy_response( array( 'nonce' => 'food' ) ); 205 | } catch ( Exception $e ) { 206 | $exception = $e; 207 | } 208 | $this->assertInstanceOf( __NAMESPACE__ . '\\Exception', $exception ); 209 | $this->assertEquals( 'bad_nonce', $exception->getMessage() ); 210 | } 211 | 212 | /** 213 | * @see HTTPS_Resource_Proxy::send_proxy_response() 214 | * @see HTTPS_Resource_Proxy::handle_proxy_request() 215 | */ 216 | function test_send_proxy_response_remote_get_error() { 217 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 218 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_true' ); 219 | 220 | $params = array( 221 | 'nonce' => wp_create_nonce( HTTPS_Resource_Proxy::MODULE_SLUG ), 222 | 'host' => 'bad.tldnotexisting', 223 | 'path' => '/main.js', 224 | ); 225 | $r = $instance->send_proxy_response( $params ); 226 | 227 | $this->assertEquals( 400, wp_remote_retrieve_response_code( $r ) ); 228 | $this->assertEquals( 'http_request_failed', wp_remote_retrieve_response_message( $r ) ); 229 | } 230 | 231 | function filter_pre_http_request( $r ) { 232 | $r = array_merge( 233 | array( 234 | 'response' => array( 235 | 'code' => 200, 236 | 'message' => 'OK', 237 | ), 238 | 'headers' => array(), 239 | 'body' => '', 240 | ), 241 | $r 242 | ); 243 | $r['headers']['content-length'] = strlen( $r['body'] ); 244 | add_filter( 'pre_http_request', function() use ( $r ) { 245 | return $r; 246 | } ); 247 | } 248 | 249 | /** 250 | * @see HTTPS_Resource_Proxy::send_proxy_response() 251 | * @see HTTPS_Resource_Proxy::handle_proxy_request() 252 | */ 253 | function test_send_proxy_response_too_large() { 254 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 255 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_true' ); 256 | 257 | $this->filter_pre_http_request( array( 258 | 'body' => str_repeat( '*', $instance->config( 'max_content_length' ) + 1 ), 259 | ) ); 260 | 261 | $params = array( 262 | 'nonce' => wp_create_nonce( HTTPS_Resource_Proxy::MODULE_SLUG ), 263 | 'host' => 'example.org', 264 | 'path' => '/main.js', 265 | ); 266 | $r = $instance->send_proxy_response( $params ); 267 | 268 | $this->assertEquals( 502, wp_remote_retrieve_response_code( $r ) ); 269 | $this->assertEquals( 'Response Too Large', wp_remote_retrieve_response_message( $r ) ); 270 | } 271 | 272 | /** 273 | * @see HTTPS_Resource_Proxy::send_proxy_response() 274 | * @see HTTPS_Resource_Proxy::handle_proxy_request() 275 | */ 276 | function test_send_proxy_response_successes() { 277 | $instance = new HTTPS_Resource_Proxy( $this->plugin ); 278 | add_filter( 'https_resource_proxy_filtering_enabled', '__return_true' ); 279 | 280 | $params = array( 281 | 'nonce' => wp_create_nonce( HTTPS_Resource_Proxy::MODULE_SLUG ), 282 | 'host' => 'example.org', 283 | 'path' => '/main.js', 284 | ); 285 | $this->filter_pre_http_request( array( 286 | 'headers' => array( 287 | 'content-type' => 'text/javascript', 288 | 'etag' => '"abc123"', 289 | 'last-modified' => gmdate( 'r', time() - 10 ), 290 | 'expires' => gmdate( 'r', time() + 10 ), 291 | 'x-foo' => 'bar', 292 | ), 293 | 'body' => 'alert( 1 );', 294 | ) ); 295 | $r1 = $instance->send_proxy_response( $params ); 296 | $this->assertArrayHasKey( 'response', $r1 ); 297 | $this->assertArrayHasKey( 'headers', $r1 ); 298 | $this->assertArrayHasKey( 'body', $r1 ); 299 | $this->assertEquals( 200, wp_remote_retrieve_response_code( $r1 ) ); 300 | $this->assertNotEmpty( wp_remote_retrieve_body( $r1 ) ); 301 | $this->assertArrayHasKey( 'etag', $r1['headers'] ); 302 | $this->assertArrayNotHasKey( 'x-foo', $r1['headers'] ); 303 | 304 | remove_all_filters( 'pre_http_request' ); 305 | add_filter( 'pre_http_request', function() { 306 | throw new Exception( 'pre_http_request should not have been called due to transient' ); 307 | } ); 308 | $r2 = $instance->send_proxy_response( $params ); 309 | $this->assertEquals( $r1, $r2 ); 310 | 311 | $params['if_modified_since'] = $r1['headers']['last-modified']; 312 | $r3 = $instance->send_proxy_response( $params ); 313 | $this->assertEquals( 304, wp_remote_retrieve_response_code( $r3 ) ); 314 | $this->assertEmpty( wp_remote_retrieve_body( $r3 ) ); 315 | 316 | unset( $params['if_modified_since'] ); 317 | $params['if_none_match'] = '"abc123"'; 318 | $r4 = $instance->send_proxy_response( $params ); 319 | $this->assertEquals( 304, wp_remote_retrieve_response_code( $r4 ) ); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /tests/test-class-non-autoloaded-widget-options.php: -------------------------------------------------------------------------------- 1 | wpdb = $GLOBALS['wpdb']; 17 | require_once __DIR__ . '/data/class-acme-widget.php'; 18 | 19 | parent::setUp(); 20 | $this->assertEmpty( $this->plugin->widget_factory->widgets ); 21 | } 22 | 23 | /** 24 | * @see Non_Autoloaded_Widget_Options::__construct() 25 | */ 26 | function test_construct() { 27 | $instance = new Non_Autoloaded_Widget_Options( $this->plugin ); 28 | $this->assertEquals( 90, has_action( 'widgets_init', array( $instance, 'fix_widget_options' ) ) ); 29 | } 30 | 31 | /** 32 | * @see Non_Autoloaded_Widget_Options::fix_widget_options() 33 | */ 34 | function test_fix_widget_options_for_previously_registered_widget() { 35 | wp_widgets_init(); 36 | $wpdb = $this->wpdb; 37 | 38 | $registered_option_names = wp_list_pluck( $this->plugin->get_registered_widget_objects(), 'option_name' ); 39 | $sql_option_names_in = join( ',', array_fill( 0, count( $registered_option_names ), '%s' ) ); 40 | // @codingStandardsIgnoreStart 41 | $unautoloaded_widget_option_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->options WHERE autoload = 'no' AND option_name IN ( $sql_option_names_in )", $registered_option_names ) ); // db call okay; cache okay 42 | $this->assertEquals( 0, $unautoloaded_widget_option_count ); 43 | 44 | $autoloaded_option_names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE autoload = 'yes' AND option_name IN ( $sql_option_names_in )", $registered_option_names ) ); // db call okay; cache okay 45 | $this->assertEmpty( array_diff( $autoloaded_option_names, $registered_option_names ), 'Expected autoloaded options to be subset of registered options.' ); 46 | 47 | $this->plugin->non_autoloaded_widget_options = new Non_Autoloaded_Widget_Options( $this->plugin ); 48 | $this->plugin->non_autoloaded_widget_options->fix_widget_options(); 49 | 50 | $unautoloaded_widget_option_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->options WHERE autoload = 'no' AND option_name IN ( $sql_option_names_in )", $registered_option_names ) ); // db call okay; cache okay 51 | $this->assertGreaterThan( 0, $unautoloaded_widget_option_count ); 52 | // @codingStandardsIgnoreStop 53 | } 54 | 55 | /** 56 | * @see Non_Autoloaded_Widget_Options::fix_widget_options() 57 | */ 58 | function test_fix_widget_options_for_newly_registered_widget() { 59 | 60 | $this->plugin->non_autoloaded_widget_options = new Non_Autoloaded_Widget_Options( $this->plugin ); 61 | $this->plugin->non_autoloaded_widget_options->fix_widget_options(); 62 | 63 | add_action( 'widgets_init', function() { 64 | register_widget( __NAMESPACE__ . '\\Acme_Widget' ); 65 | } ); 66 | wp_widgets_init(); 67 | $wpdb = $this->wpdb; 68 | 69 | $autoload = $wpdb->get_var( "SELECT autoload FROM $wpdb->options WHERE autoload = 'no' AND option_name = 'widget_acme'" ); // db call okay; cache okay 70 | $this->assertEquals( 'no', $autoload ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/test-class-optimized-widget-registration.php: -------------------------------------------------------------------------------- 1 | factory->user->create( array( 'role' => 'administrator' ) ) ); 17 | $this->wp_customize_manager = new \WP_Customize_Manager(); 18 | } 19 | 20 | /** 21 | * @see Optimized_Widget_Registration::__construct() 22 | */ 23 | function test_construct() { 24 | $this->markTestIncomplete( 'Tests needed for methods in Optimized_Widget_Registration' ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/test-class-plugin.php: -------------------------------------------------------------------------------- 1 | plugin = get_plugin_instance(); 17 | parent::setUp(); 18 | } 19 | 20 | /** 21 | * @see Plugin::__construct() 22 | */ 23 | function test_construct() { 24 | $this->assertInternalType( 'array', $this->plugin->config ); 25 | $this->assertArrayHasKey( 'disable_widgets_init', $this->plugin->config ); 26 | $this->assertArrayHasKey( 'disable_widgets_factory', $this->plugin->config ); 27 | $this->assertArrayHasKey( 'active_modules', $this->plugin->config ); 28 | $this->assertEquals( 9, has_action( 'after_setup_theme', array( $this->plugin, 'init' ) ) ); 29 | } 30 | 31 | /** 32 | * @see Plugin::is_module_active() 33 | */ 34 | function test_is_module_active() { 35 | foreach ( $this->plugin->config['active_modules'] as $module_name => $is_active ) { 36 | $this->assertEquals( $is_active, $this->plugin->is_module_active( $module_name ) ); 37 | } 38 | } 39 | 40 | /** 41 | * @see Plugin::init() 42 | */ 43 | function test_init() { 44 | $this->plugin->init(); 45 | $this->assertInstanceOf( 'WP_Widget_Factory', $this->plugin->widget_factory ); 46 | $this->assertNotFalse( has_action( 'wp_default_scripts', array( $this->plugin, 'register_scripts' ) ) ); 47 | } 48 | 49 | /** 50 | * @see Plugin::register_scripts() 51 | */ 52 | function test_register_scripts() { 53 | $wp_scripts = wp_scripts(); 54 | 55 | $handles = array( 56 | 'customize-widgets-plus-base', 57 | 'customize-widgets-plus-widget-number-incrementing', 58 | 'customize-widgets-plus-widget-number-incrementing-customizer', 59 | ); 60 | foreach ( $handles as $handle ) { 61 | $this->assertArrayHasKey( $handle, $wp_scripts->registered ); 62 | } 63 | } 64 | 65 | /** 66 | * @see Plugin::disable_widgets_init() 67 | */ 68 | function test_disable_widgets_init() { 69 | if ( ! has_action( 'init', 'wp_widgets_init' ) ) { 70 | add_action( 'init', 'wp_widgets_init', 1 ); 71 | } 72 | $this->assertNotFalse( has_action( 'init', 'wp_widgets_init' ) ); 73 | $this->plugin->disable_widgets_init(); 74 | $this->assertFalse( has_action( 'init', 'wp_widgets_init' ) ); 75 | } 76 | 77 | /** 78 | * @see Plugin::disable_widgets_factory() 79 | */ 80 | function test_disable_widgets_factory() { 81 | $callable = array( $this->plugin->widget_factory, '_register_widgets' ); 82 | $this->assertNotFalse( has_action( 'widgets_init', $callable ) ); 83 | $this->plugin->disable_widgets_factory(); 84 | $this->assertFalse( has_action( 'widgets_init', $callable ) ); 85 | } 86 | 87 | /** 88 | * @see Plugin::is_running_unit_tests() 89 | */ 90 | function test_is_running_unit_tests() { 91 | $this->assertTrue( $this->plugin->is_running_unit_tests() ); 92 | } 93 | 94 | /** 95 | * @see Plugin::is_normal_multi_widget() 96 | */ 97 | function test_is_normal_multi_widget() { 98 | $widget_obj = new \WP_Widget( 'foo', 'Foo' ); 99 | $this->assertTrue( $this->plugin->is_normal_multi_widget( $widget_obj ) ); 100 | 101 | $widget_obj = new \WP_Widget( 'bar', 'Bar' ); 102 | $widget_obj->option_name = 'baz'; 103 | $this->assertFalse( $this->plugin->is_normal_multi_widget( $widget_obj ) ); 104 | 105 | $widget_obj = new \WP_Widget( 'baz', 'Baz' ); 106 | $widget_obj->id_base = 'baaaaz'; 107 | $this->assertFalse( $this->plugin->is_normal_multi_widget( $widget_obj ) ); 108 | } 109 | 110 | /** 111 | * @see Plugin::is_registered_multi_widget() 112 | */ 113 | function test_is_registered_multi_widget() { 114 | global $wp_registered_widgets; 115 | wp_widgets_init(); 116 | 117 | $registered_widget = $wp_registered_widgets[ key( $wp_registered_widgets ) ]; 118 | $this->assertTrue( $this->plugin->is_registered_multi_widget( $registered_widget ) ); 119 | 120 | $not_multi_registered_widget = $registered_widget; 121 | $not_multi_registered_widget['callback'] = '__return_empty_string'; 122 | $this->assertFalse( $this->plugin->is_registered_multi_widget( $not_multi_registered_widget ) ); 123 | } 124 | 125 | /** 126 | * @see Plugin::get_registered_widget_objects() 127 | */ 128 | function test_get_registered_widget_objects() { 129 | wp_widgets_init(); 130 | $registered_widget_objs = $this->plugin->get_registered_widget_objects(); 131 | $this->assertInternalType( 'array', $registered_widget_objs ); 132 | foreach ( $registered_widget_objs as $key => $registered_widget_obj ) { 133 | $this->assertInstanceOf( 'WP_Widget', $registered_widget_obj ); 134 | $this->assertEquals( $registered_widget_obj->id_base, $key ); 135 | } 136 | } 137 | 138 | /** 139 | * @see Plugin::is_recognized_widget_id_base() 140 | */ 141 | function test_is_recognized_widget_id_base() { 142 | wp_widgets_init(); 143 | $id_bases = array_keys( $this->plugin->get_registered_widget_objects() ); 144 | foreach ( $id_bases as $id_base ) { 145 | $this->assertTrue( $this->plugin->is_recognized_widget_id_base( $id_base ) ); 146 | } 147 | $this->assertFalse( $this->plugin->is_recognized_widget_id_base( 'bad' ) ); 148 | } 149 | 150 | /** 151 | * @see Plugin::parse_widget_id() 152 | */ 153 | function test_parse_widget_id() { 154 | $this->assertNull( $this->plugin->parse_widget_id( 'foo' ) ); 155 | $parsed = $this->plugin->parse_widget_id( 'bar-3' ); 156 | $this->assertInternalType( 'array', $parsed ); 157 | $this->assertEquals( 3, $parsed['widget_number'] ); 158 | $this->assertEquals( 'bar', $parsed['id_base'] ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/test-class-widget-number-incrementing.php: -------------------------------------------------------------------------------- 1 | plugin ); 20 | 21 | $this->assertEquals( 10, has_action( 'wp_ajax_' . Widget_Number_Incrementing::AJAX_ACTION, array( $instance, 'ajax_incr_widget_number' ) ) ); 22 | $this->assertEquals( 10, has_action( 'customize_controls_enqueue_scripts', array( $instance, 'customize_controls_enqueue_scripts' ) ) ); 23 | $this->assertEquals( 10, has_action( 'admin_enqueue_scripts', array( $instance, 'admin_enqueue_scripts' ) ) ); 24 | $this->assertEquals( 10, has_action( 'customize_refresh_nonces', array( $instance, 'filter_customize_refresh_nonces' ) ) ); 25 | } 26 | 27 | /** 28 | * @see Widget_Number_Incrementing::get_max_existing_widget_number() 29 | */ 30 | function test_get_max_existing_widget_number() { 31 | $instance = new Widget_Number_Incrementing( $this->plugin ); 32 | wp_widgets_init(); 33 | $widgets_sidebars = call_user_func_array( 'array_merge', wp_get_sidebars_widgets() ); 34 | $widget_id = array_shift( $widgets_sidebars ); 35 | $parsed_widget_id = $this->plugin->parse_widget_id( $widget_id ); 36 | $id_base = $parsed_widget_id['id_base']; 37 | 38 | $registered_widget_objs = $this->plugin->get_registered_widget_objects(); 39 | $settings = $registered_widget_objs[ $id_base ]->get_settings(); 40 | $max_widget_number = max( array_keys( $settings ) ); 41 | $this->assertEquals( $max_widget_number, $instance->get_max_existing_widget_number( $id_base ) ); 42 | } 43 | 44 | /** 45 | * @see Widget_Number_Incrementing::add_widget_number_option() 46 | * @see Widget_Number_Incrementing::get_widget_number() 47 | */ 48 | function test_add_and_get_widget_number_option() { 49 | $instance = new Widget_Number_Incrementing( $this->plugin ); 50 | add_action( 'widgets_init', function() { 51 | register_widget( __NAMESPACE__ . '\\Acme_Widget' ); 52 | } ); 53 | wp_widgets_init(); 54 | $this->assertTrue( $instance->add_widget_number_option( Acme_Widget::ID_BASE ) ); 55 | $this->assertFalse( $instance->add_widget_number_option( Acme_Widget::ID_BASE ) ); 56 | $this->assertEquals( 2, $instance->get_widget_number( Acme_Widget::ID_BASE ) ); 57 | } 58 | 59 | /** 60 | * @see Widget_Number_Incrementing::set_widget_number() 61 | */ 62 | function test_set_widget_number() { 63 | $instance = new Widget_Number_Incrementing( $this->plugin ); 64 | add_action( 'widgets_init', function() { 65 | register_widget( __NAMESPACE__ . '\\Acme_Widget' ); 66 | } ); 67 | wp_widgets_init(); 68 | 69 | $widget_number = $instance->get_widget_number( Acme_Widget::ID_BASE ); 70 | $this->assertInternalType( 'int', $widget_number ); 71 | $new_widget_number = $instance->set_widget_number( Acme_Widget::ID_BASE, $widget_number - 1 ); 72 | $this->assertEquals( $widget_number, $instance->get_widget_number( acme_Widget::ID_BASE ) ); 73 | $this->assertEquals( $new_widget_number, $widget_number ); 74 | $new_widget_number = $instance->set_widget_number( Acme_Widget::ID_BASE, $widget_number + 1 ); 75 | $this->assertEquals( $new_widget_number, $widget_number + 1 ); 76 | $this->assertEquals( $widget_number + 1, $instance->get_widget_number( Acme_Widget::ID_BASE ) ); 77 | 78 | } 79 | 80 | /** 81 | * @see Widget_Number_Incrementing::incr_widget_number() 82 | */ 83 | function test_incr_widget_number() { 84 | $instance = new Widget_Number_Incrementing( $this->plugin ); 85 | wp_widgets_init(); 86 | $registered_widget_objs = $this->plugin->get_registered_widget_objects(); 87 | $id_base = key( $registered_widget_objs ); 88 | 89 | $initial_widget_number = $instance->get_widget_number( $id_base ); 90 | $this->assertInternalType( 'int', $initial_widget_number ); 91 | $iteration_count = 10; 92 | for ( $i = 1; $i <= $iteration_count; $i += 1 ) { 93 | $this->assertEquals( $initial_widget_number + $i, $instance->incr_widget_number( $id_base ) ); 94 | $this->assertEquals( $initial_widget_number + $i, $instance->get_widget_number( $id_base ) ); 95 | } 96 | } 97 | 98 | /** 99 | * @see Widget_Number_Incrementing::request_incr_widget_number() 100 | */ 101 | function test_request_incr_widget_number() { 102 | $instance = new Widget_Number_Incrementing( $this->plugin ); 103 | wp_widgets_init(); 104 | $registered_widget_objs = $this->plugin->get_registered_widget_objects(); 105 | $id_base = key( $registered_widget_objs ); 106 | 107 | // ----- 108 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'contributor' ) ) ); 109 | $params = array( 110 | 'nonce' => wp_create_nonce( 'incr_widget_number' ), 111 | 'id_base' => $id_base, 112 | ); 113 | try { 114 | $exception = null; 115 | $instance->request_incr_widget_number( $params ); 116 | } catch ( Exception $e ) { 117 | $exception = $e; 118 | } 119 | $this->assertNotNull( $exception ); 120 | $this->assertEquals( 403, $exception->getCode() ); 121 | 122 | // ----- 123 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); 124 | $params['nonce'] = 'bad_nonce'; 125 | try { 126 | $exception = null; 127 | $instance->request_incr_widget_number( $params ); 128 | } catch ( Exception $e ) { 129 | $exception = $e; 130 | } 131 | $this->assertNotNull( $exception ); 132 | $this->assertEquals( 403, $exception->getCode() ); 133 | 134 | // ----- 135 | try { 136 | $exception = null; 137 | $instance->request_incr_widget_number( array() ); 138 | } catch ( Exception $e ) { 139 | $exception = $e; 140 | } 141 | $this->assertNotNull( $exception ); 142 | $this->assertEquals( 400, $exception->getCode() ); 143 | 144 | // ----- 145 | $params['nonce'] = wp_create_nonce( 'incr_widget_number' ); 146 | try { 147 | $exception = null; 148 | $instance->request_incr_widget_number( array_merge( 149 | $params, 150 | array( 151 | 'id_base' => 'bad_id', 152 | ) 153 | ) ); 154 | } catch ( Exception $e ) { 155 | $exception = $e; 156 | } 157 | $this->assertNotNull( $exception ); 158 | $this->assertEquals( 400, $exception->getCode() ); 159 | 160 | // ---- 161 | $previous_widget_number = $instance->get_widget_number( $id_base ); 162 | $result = $instance->request_incr_widget_number( $params ); 163 | $this->assertArrayHasKey( 'number', $result ); 164 | $this->assertEquals( $result['number'], $previous_widget_number + 1 ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /tests/test-class-widget-posts-with-customizer.php: -------------------------------------------------------------------------------- 1 | array( 31 | 'title' => 'Search', 32 | ), 33 | 3 => array( 34 | 'title' => 'Find', 35 | ), 36 | '_multiwidget' => 1, 37 | ); 38 | update_option( 'widget_search', $initial_search_widgets ); 39 | 40 | $this->widget_posts = new Widget_Posts( $this->plugin ); 41 | $this->plugin->widget_number_incrementing = new Widget_Number_Incrementing( $this->plugin ); 42 | $this->widget_posts->init(); 43 | wp_widgets_init(); 44 | $this->widget_posts->register_instance_post_type(); 45 | $this->widget_posts->migrate_widgets_from_options(); 46 | 47 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); 48 | $wp_customize = $this->customize_manager = new \WP_Customize_Manager(); 49 | $this->assertTrue( $this->widget_posts->add_customize_hooks() ); // This did nothing in init since Customizer was not loaded. 50 | 51 | // Make sure the widgets get re-registered now using the new settings source. 52 | global $wp_registered_widgets; 53 | $wp_registered_widgets = array(); 54 | wp_widgets_init(); 55 | } 56 | 57 | /** 58 | * @see \WP_Customize_Setting::preview() 59 | */ 60 | function test_customize_widgets_preview() { 61 | $initial_search_widgets = get_option( 'widget_search' ); 62 | $previewed_setting_id = 'widget_search[2]'; 63 | $override_widget_data = array( 64 | 'title' => 'Buscar', 65 | ); 66 | $sanitized_widget_instance = $this->customize_manager->widgets->sanitize_widget_js_instance( $override_widget_data ); 67 | $this->customize_manager->set_post_value( $previewed_setting_id, $sanitized_widget_instance ); 68 | 69 | $this->customize_manager->widgets->customize_register(); // This calls preview() on the WP_Widget_Setting class, after which we cannot set_post_value() 70 | 71 | $previewed_setting = $this->customize_manager->get_setting( 'widget_search[2]' ); 72 | $unchanged_setting = $this->customize_manager->get_setting( 'widget_search[3]' ); 73 | $this->assertEquals( $override_widget_data, $previewed_setting->value() ); 74 | $this->assertEquals( $initial_search_widgets[3], $unchanged_setting->value() ); 75 | } 76 | 77 | /** 78 | * @see \WP_Customize_Setting::save() 79 | */ 80 | function test_customize_widgets_save() { 81 | $initial_search_widgets = get_option( 'widget_search' ); 82 | 83 | $widget_id = 'search-2'; 84 | $setting_id = 'widget_search[2]'; 85 | $override_widget_data = array( 86 | 'title' => 'Buscar', 87 | ); 88 | $sanitized_widget_instance = $this->customize_manager->widgets->sanitize_widget_js_instance( $override_widget_data ); 89 | $this->customize_manager->set_post_value( $setting_id, $sanitized_widget_instance ); 90 | $this->customize_manager->add_dynamic_settings( array( $setting_id ) ); 91 | 92 | $widget_post = $this->widget_posts->get_widget_post( $widget_id ); 93 | $this->assertEquals( $initial_search_widgets[2], $this->widget_posts->get_widget_instance_data( $widget_post ) ); 94 | 95 | do_action( 'customize_save', $this->customize_manager ); 96 | foreach ( $this->customize_manager->settings() as $setting ) { 97 | /** @var \WP_Customize_Setting $setting */ 98 | $setting->save(); 99 | } 100 | do_action( 'customize_save_after', $this->customize_manager ); 101 | 102 | $saved_data = $this->widget_posts->get_widget_instance_data( $this->widget_posts->get_widget_post( $widget_id ) ); 103 | $this->assertEquals( $override_widget_data, $saved_data, 'Overridden widget data should be saved.' ); 104 | 105 | $this->assertEquals( 106 | $initial_search_widgets[3], 107 | $this->widget_posts->get_widget_instance_data( $this->widget_posts->get_widget_post( 'search-3' ) ), 108 | 'Untouched widget instance should remain intact.' 109 | ); 110 | } 111 | 112 | /** 113 | * @see Widget_Posts::add_customize_hooks() 114 | */ 115 | function test_add_customize_hooks() { 116 | $this->assertEquals( 10, has_filter( 'customize_dynamic_setting_class', array( $this->widget_posts, 'filter_dynamic_setting_class' ) ) ); 117 | $this->assertNotFalse( has_action( 'wp_loaded', array( $this->widget_posts, 'register_widget_instance_settings_early' ) ) ); 118 | } 119 | 120 | /** 121 | * @see Widget_Posts::capture_widget_settings_for_customizer() 122 | * @see Widget_Posts::filter_pre_option_widget_settings() 123 | * @see Widget_Posts::disable_filtering_pre_option_widget_settings() 124 | * @see Widget_Posts::enable_filtering_pre_option_widget_settings() 125 | */ 126 | function test_capture_widget_settings_for_customizer() { 127 | $module = $this->widget_posts; 128 | $this->assertNotEmpty( $module->current_widget_type_values ); 129 | 130 | foreach ( $module->current_widget_type_values as $id_base => $instances ) { 131 | $this->assertArrayHasKey( $id_base, $module->widget_objs ); 132 | $widget_obj = $module->widget_objs[ $id_base ]; 133 | $this->assertInstanceOf( __NAMESPACE__ . '\\Widget_Settings', $instances ); 134 | $this->assertArrayHasKey( '_multiwidget', $instances ); 135 | 136 | $this->assertEquals( true, has_filter( "pre_option_widget_{$id_base}" ) ); 137 | $this->assertEquals( $instances, get_option( "widget_{$id_base}" ) ); 138 | 139 | unset( $instances['_multiwidget'] ); 140 | $settings = $widget_obj->get_settings(); 141 | $this->assertEquals( $instances, $settings ); 142 | 143 | $instances[100] = array( 'text' => 'Hello World' ); 144 | $module->current_widget_type_values[ $id_base ] = $instances; 145 | $settings = $widget_obj->get_settings(); 146 | $this->assertArrayHasKey( 100, $settings ); 147 | $this->assertEquals( $instances[100], $settings[100] ); 148 | 149 | $module->disable_filtering_pre_option_widget_settings(); 150 | $settings = $widget_obj->get_settings(); 151 | $this->assertArrayNotHasKey( 100, $settings ); 152 | 153 | $module->enable_filtering_pre_option_widget_settings(); 154 | $this->assertArrayHasKey( 100, $widget_obj->get_settings() ); 155 | } 156 | } 157 | 158 | /** 159 | * @see Widget_Posts::filter_dynamic_setting_class() 160 | */ 161 | function test_filter_dynamic_setting_class() { 162 | $setting_class = apply_filters( 'customize_dynamic_setting_class', 'WP_Customize_Setting', 'widget_text[2]', array() ); 163 | $this->assertEquals( '\\' . __NAMESPACE__ . '\\WP_Customize_Widget_Setting', $setting_class ); 164 | $this->assertEquals( $setting_class, $this->widget_posts->filter_dynamic_setting_class( 'WP_Customize_Setting', 'widget_text[2]' ) ); 165 | 166 | $this->assertEquals( 'WP_Customize_Setting', $this->widget_posts->filter_dynamic_setting_class( 'WP_Customize_Setting', 'foo' ) ); 167 | } 168 | 169 | /** 170 | * @see Widget_Posts::register_widget_instance_settings_early() 171 | * @see Widget_Posts::register_widget_settings() 172 | */ 173 | function test_register_widget_instance_settings_early() { 174 | $wp = new \WP(); 175 | $wp->main(); 176 | 177 | foreach ( $this->customize_manager->settings() as $setting_id => $setting ) { 178 | if ( preg_match( '/^widget_/', $setting_id ) ) { 179 | $this->assertInstanceOf( __NAMESPACE__ . '\\WP_Customize_Widget_Setting', $setting ); 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /tests/test-class-widget-settings.php: -------------------------------------------------------------------------------- 1 | plugin->widget_number_incrementing = new Widget_Number_Incrementing( $this->plugin ); 13 | $this->plugin->widget_posts = new Widget_Posts( $this->plugin ); 14 | wp_widgets_init(); 15 | } 16 | 17 | /** 18 | * @param string $id_base 19 | * @param int $count 20 | * 21 | * @return Widget_Settings 22 | */ 23 | function create_widget_settings( $id_base, $count = 3 ) { 24 | $instances = array(); 25 | for ( $i = 2; $i < 2 + $count; $i += 1 ) { 26 | $post = $this->plugin->widget_posts->insert_widget( $id_base, array( 27 | 'title' => "Hello world for widget_posts $i", 28 | ) ); 29 | $instances[ $i ] = $post->ID; 30 | } 31 | $settings = new Widget_Settings( $instances ); 32 | return $settings; 33 | } 34 | 35 | /** 36 | * @see Widget_Settings::offsetGet() 37 | */ 38 | function test_offset_get() { 39 | $settings = $this->create_widget_settings( 'meta', 3 ); 40 | $shallow_settings = $settings->getArrayCopy(); 41 | foreach ( $shallow_settings as $widget_number => $post_id ) { 42 | $this->assertInternalType( 'int', $widget_number ); 43 | $this->assertInternalType( 'int', $post_id ); 44 | } 45 | 46 | $this->assertEquals( 1, $settings['_multiwidget'] ); 47 | $this->assertNull( $settings[100] ); 48 | $instance = $settings[2]; 49 | $this->assertInternalType( 'array', $instance ); 50 | $this->assertContains( 'widget_posts', $instance['title'] ); 51 | 52 | foreach ( array_keys( $shallow_settings ) as $widget_number ) { 53 | $instance = $settings[ $widget_number ]; 54 | $this->assertInternalType( 'array', $instance ); 55 | $this->assertContains( 'widget_posts', $instance['title'] ); 56 | } 57 | } 58 | 59 | /** 60 | * @see Widget_Settings::offsetSet() 61 | */ 62 | function test_offset_set() { 63 | $settings = $this->create_widget_settings( 'meta', 3 ); 64 | $before = $settings->getArrayCopy(); 65 | $settings['_multiwidget'] = 1; 66 | $this->assertEquals( $before, $settings->getArrayCopy() ); 67 | 68 | $before = $settings->getArrayCopy(); 69 | $settings['sdasd'] = array( 'title' => 'as' ); 70 | $this->assertEquals( $before, $settings->getArrayCopy() ); 71 | 72 | $before = $settings->getArrayCopy(); 73 | $settings[4] = 'asdasd'; 74 | $this->assertEquals( $before, $settings->getArrayCopy() ); 75 | 76 | $before = $settings->getArrayCopy(); 77 | $settings[50] = array( 'title' => 'Set' ); 78 | $this->assertNotEquals( $before, $settings->getArrayCopy() ); 79 | } 80 | 81 | /** 82 | * @see Widget_Settings::offsetExists() 83 | */ 84 | function test_exists() { 85 | $settings = $this->create_widget_settings( 'meta', 3 ); 86 | $this->assertFalse( isset( $settings[1000] ) ); 87 | $this->assertTrue( isset( $settings['_multiwidget'] ) ); 88 | $this->assertTrue( isset( $settings[2] ) ); 89 | $this->assertFalse( isset( $settings[100] ) ); 90 | } 91 | 92 | /** 93 | * @see Widget_Settings::offsetUnset() 94 | */ 95 | function test_offset_unset() { 96 | $settings = $this->create_widget_settings( 'meta', 3 ); 97 | $this->assertTrue( isset( $settings['_multiwidget'] ) ); 98 | $before = $settings->getArrayCopy(); 99 | unset( $settings['_multiwidget'] ); // A no-op. 100 | $this->assertTrue( isset( $settings['_multiwidget'] ) ); 101 | $this->assertEquals( $before, $settings->getArrayCopy() ); 102 | } 103 | 104 | /** 105 | * @see Widget_Settings::current() 106 | */ 107 | function test_current() { 108 | $settings = $this->create_widget_settings( 'meta', 3 ); 109 | $shallow_settings = $settings->getArrayCopy(); 110 | 111 | foreach ( $settings as $widget_number => $instance ) { 112 | $this->assertInternalType( 'array', $instance ); 113 | $this->assertContains( 'widget_posts', $instance['title'] ); 114 | $this->assertEquals( $instance, Widget_Posts::get_post_content( get_post( $shallow_settings[ $widget_number ] ) ) ); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/test-class-wp-customize-widget-setting.php: -------------------------------------------------------------------------------- 1 | widget_posts = new Widget_Posts( $this->plugin ); 29 | $this->plugin->widget_number_incrementing = new Widget_Number_Incrementing( $this->plugin ); 30 | $this->widget_posts->init(); 31 | wp_widgets_init(); 32 | $this->widget_posts->register_instance_post_type(); 33 | $this->widget_posts->migrate_widgets_from_options(); 34 | 35 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); 36 | $wp_customize = $this->customize_manager = new \WP_Customize_Manager(); 37 | $this->widget_posts->add_customize_hooks(); // This did nothing in init since Customizer was not loaded. 38 | 39 | // Make sure the widgets get re-registered now using the new settings source. 40 | global $wp_registered_widgets; 41 | $wp_registered_widgets = array(); 42 | wp_widgets_init(); 43 | do_action( 'wp_loaded' ); 44 | } 45 | 46 | /** 47 | * @param string $id_base 48 | * @return array 49 | */ 50 | function get_sample_widget_instance_data( $id_base ) { 51 | $instances = get_option( "widget_{$id_base}" ); 52 | $number = key( $instances ); 53 | $instance = $instances[ $number ]; 54 | $widget_id = "$id_base-$number"; 55 | $setting_id = "widget_{$id_base}[$number]"; 56 | $this->assertArrayHasKey( 'title', $instance ); 57 | return compact( 'instances', 'number', 'instance', 'setting_id', 'widget_id' ); 58 | } 59 | 60 | /** 61 | * @see WP_Customize_Widget_Setting::__construct() 62 | */ 63 | function test_construct() { 64 | $id_base = 'categories'; 65 | $sample_data = $this->get_sample_widget_instance_data( $id_base ); 66 | $args = $this->customize_manager->widgets->get_setting_args( $sample_data['setting_id'] ); 67 | $new_setting = new WP_Customize_Widget_Setting( $this->customize_manager, $sample_data['setting_id'], $args ); 68 | $this->assertEquals( 'widget', $new_setting->type ); 69 | $this->assertEquals( $id_base, $new_setting->widget_id_base ); 70 | $this->assertEquals( $sample_data['number'], $new_setting->widget_number ); 71 | } 72 | 73 | /** 74 | * @see WP_Customize_Widget_Setting::value() 75 | */ 76 | function test_value() { 77 | $id_base = 'categories'; 78 | $sample_data = $this->get_sample_widget_instance_data( $id_base ); 79 | $old_setting = new \WP_Customize_Setting( $this->customize_manager, $sample_data['setting_id'], array( 80 | 'type' => 'option', 81 | ) ); 82 | $args = $this->customize_manager->widgets->get_setting_args( $sample_data['setting_id'] ); 83 | $new_setting = new WP_Customize_Widget_Setting( $this->customize_manager, $sample_data['setting_id'], $args ); 84 | 85 | $this->assertEquals( $sample_data['instance'], $old_setting->value() ); 86 | $this->assertEquals( $old_setting->value(), $new_setting->value() ); 87 | } 88 | 89 | /** 90 | * @see WP_Customize_Widget_Setting::preview() 91 | */ 92 | function test_preview() { 93 | $widget_data = $this->get_sample_widget_instance_data( 'archives' ); 94 | 95 | $args = $this->customize_manager->widgets->get_setting_args( $widget_data['setting_id'] ); 96 | $setting = new WP_Customize_Widget_Setting( $this->customize_manager, $widget_data['setting_id'], $args ); 97 | $value = $setting->value(); 98 | $override_title = 'BAR PREVIEWED VALUE'; 99 | $this->assertNotEquals( $value['title'], $override_title ); 100 | 101 | $value['title'] = $override_title; 102 | $this->customize_manager->set_post_value( $setting->id, $value ); 103 | $setting->preview(); 104 | $this->assertEquals( $value['title'], $override_title ); 105 | } 106 | 107 | /** 108 | * @see WP_Customize_Widget_Setting::update() 109 | */ 110 | function test_save() { 111 | $widget_data = $this->get_sample_widget_instance_data( 'archives' ); 112 | 113 | $args = $this->customize_manager->widgets->get_setting_args( $widget_data['setting_id'] ); 114 | $setting = new WP_Customize_Widget_Setting( $this->customize_manager, $widget_data['setting_id'], $args ); 115 | $value = $setting->value(); 116 | $override_title = 'BAR UPDATED VALUE'; 117 | $this->assertNotEquals( $value['title'], $override_title ); 118 | 119 | $value['title'] = $override_title; 120 | $this->customize_manager->set_post_value( $setting->id, $this->customize_manager->widgets->sanitize_widget_js_instance( $value ) ); 121 | $setting->save(); 122 | $saved_value = $setting->value(); 123 | 124 | $this->assertEquals( $override_title, $saved_value['title'] ); 125 | 126 | $instances = get_option( 'widget_archives' ); 127 | $this->assertEquals( $saved_value, $instances[ $widget_data['number'] ] ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/test-core-customize-widgets-with-widget-posts.php: -------------------------------------------------------------------------------- 1 | css_concat_init_priority = has_action( 'init', 'css_concat_init' ); 41 | if ( $this->css_concat_init_priority ) { 42 | remove_action( 'init', 'css_concat_init', $this->css_concat_init_priority ); 43 | } 44 | $this->js_concat_init_priority = has_action( 'init', 'js_concat_init' ); 45 | if ( $this->js_concat_init_priority ) { 46 | remove_action( 'init', 'js_concat_init', $this->js_concat_init_priority ); 47 | } 48 | 49 | $wp_registered_widget_updates = array(); 50 | $wp_registered_widget_controls = array(); 51 | $wp_registered_widgets = array(); 52 | wp_widgets_init(); 53 | 54 | parent::setUp(); 55 | 56 | $this->plugin = new Plugin(); 57 | $this->plugin->widget_factory = $wp_widget_factory; 58 | $this->plugin->widget_number_incrementing = new Widget_Number_Incrementing( $this->plugin ); 59 | $this->plugin->widget_posts = new Widget_Posts( $this->plugin ); 60 | 61 | $widgets_init_hook = 'widgets_init'; 62 | $callable = array( $wp_widget_factory, '_register_widgets' ); 63 | $priority = has_action( $widgets_init_hook, $callable ); 64 | if ( false !== $priority ) { 65 | remove_action( $widgets_init_hook, $callable, $priority ); 66 | } 67 | wp_widgets_init(); 68 | if ( false !== $priority ) { 69 | add_action( $widgets_init_hook, $callable, $priority ); 70 | } 71 | 72 | $this->plugin->widget_posts->enable(); 73 | $this->plugin->widget_posts->migrate_widgets_from_options(); 74 | $this->plugin->widget_posts->init(); 75 | $this->plugin->widget_posts->prepare_widget_data(); // Has to be called here because of wp_widgets_init() footwork done above. 76 | $this->plugin->widget_posts->register_instance_post_type(); // Normally called at init action. 77 | $this->plugin->widget_posts->capture_widget_settings_for_customizer(); // Normally called in widgets_init 78 | } 79 | 80 | function test_register_widget() { 81 | global $wp_registered_widget_updates, $wp_registered_widget_controls, $wp_registered_widgets; 82 | $wp_registered_widgets = array(); 83 | $wp_registered_widget_updates = array(); 84 | $wp_registered_widget_controls = array(); 85 | 86 | register_widget( 'WP_Widget_Search' ); 87 | wp_widgets_init(); 88 | $this->assertArrayHasKey( 'search-2', $wp_registered_widgets ); 89 | $this->assertArrayHasKey( 'search', $wp_registered_widget_updates ); 90 | $this->assertArrayHasKey( 'search-2', $wp_registered_widget_controls ); 91 | } 92 | 93 | function test_register_settings() { 94 | parent::test_register_settings(); 95 | $this->assertInstanceOf( __NAMESPACE__ . '\\WP_Customize_Widget_Setting', $this->manager->get_setting( 'widget_categories[2]' ) ); 96 | $this->assertEquals( 'widget', $this->manager->get_setting( 'widget_categories[2]' )->type ); 97 | 98 | $this->assertInstanceOf( __NAMESPACE__ . '\\Widget_Settings', get_option( 'widget_categories' ) ); 99 | } 100 | 101 | /** 102 | * Test test_customize_register_with_deleted_sidebars. 103 | */ 104 | function test_customize_register_with_deleted_sidebars() { 105 | global $wp_registered_sidebars; 106 | $wp_registered_sidebars = array(); // WPCS: Global override OK. 107 | parent::test_customize_register_with_deleted_sidebars(); 108 | } 109 | 110 | function tearDown() { 111 | parent::tearDown(); 112 | if ( $this->css_concat_init_priority ) { 113 | add_action( 'init', 'css_concat_init', $this->css_concat_init_priority ); 114 | } 115 | if ( $this->js_concat_init_priority ) { 116 | add_action( 'init', 'js_concat_init', $this->js_concat_init_priority ); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/test-core-widgets-with-widget-posts.php: -------------------------------------------------------------------------------- 1 | css_concat_init_priority = has_action( 'init', 'css_concat_init' ); 35 | if ( $this->css_concat_init_priority ) { 36 | remove_action( 'init', 'css_concat_init', $this->css_concat_init_priority ); 37 | } 38 | $this->js_concat_init_priority = has_action( 'init', 'js_concat_init' ); 39 | if ( $this->js_concat_init_priority ) { 40 | remove_action( 'init', 'js_concat_init', $this->js_concat_init_priority ); 41 | } 42 | 43 | parent::setUp(); 44 | 45 | $this->plugin = new Plugin(); 46 | $this->plugin->widget_factory = $wp_widget_factory; 47 | $this->plugin->widget_number_incrementing = new Widget_Number_Incrementing( $this->plugin ); 48 | $this->plugin->widget_posts = new Widget_Posts( $this->plugin ); 49 | 50 | $widgets_init_hook = 'widgets_init'; 51 | $callable = array( $wp_widget_factory, '_register_widgets' ); 52 | $priority = has_action( $widgets_init_hook, $callable ); 53 | if ( false !== $priority ) { 54 | remove_action( $widgets_init_hook, $callable, $priority ); 55 | } 56 | wp_widgets_init(); 57 | if ( false !== $priority ) { 58 | add_action( $widgets_init_hook, $callable, $priority ); 59 | } 60 | 61 | $this->plugin->widget_posts->migrate_widgets_from_options(); 62 | $this->plugin->widget_posts->init(); 63 | $this->plugin->widget_posts->prepare_widget_data(); // Has to be called here because of wp_widgets_init() footwork done above. 64 | $this->plugin->widget_posts->register_instance_post_type(); // Normally called at init action. 65 | } 66 | 67 | /** 68 | * @see \Tests_Widgets::test_wp_widget_get_settings() 69 | * @link https://github.com/xwp/wordpress-develop/pull/85 70 | */ 71 | function test_wp_widget_get_settings() { 72 | if ( ! method_exists( '\Tests_Widgets', 'test_wp_widget_get_settings' ) ) { 73 | $this->markTestSkipped( 'Test requires Core patch from https://github.com/xwp/wordpress-develop/pull/85 to be applied.' ); 74 | return; 75 | } 76 | 77 | add_filter( 'pre_option_widget_search', function( $value ) { 78 | $this->assertNotFalse( $value, 'Expected widget_search option to have been short-circuited.' ); 79 | return $value; 80 | }, 1000 ); 81 | 82 | parent::test_wp_widget_get_settings(); 83 | } 84 | 85 | /** 86 | * @see \Tests_Widgets::test_wp_widget_save_settings() 87 | * @link https://github.com/xwp/wordpress-develop/pull/85 88 | */ 89 | function test_wp_widget_save_settings() { 90 | if ( ! method_exists( '\Tests_Widgets', 'test_wp_widget_save_settings' ) ) { 91 | $this->markTestSkipped( 'Test requires Core patch from https://github.com/xwp/wordpress-develop/pull/85 to be applied.' ); 92 | return; 93 | } 94 | 95 | parent::test_wp_widget_save_settings(); 96 | 97 | $this->assertEquals( 0, did_action( 'update_option_widget_search' ), 'Expected update_option( "widget_meta" ) to short-circuit.' ); 98 | } 99 | 100 | /** 101 | * @see \Tests_Widgets::test_wp_widget_save_settings_delete() 102 | * @link https://github.com/xwp/wordpress-develop/pull/85 103 | */ 104 | function test_wp_widget_save_settings_delete() { 105 | if ( ! method_exists( '\Tests_Widgets', 'test_wp_widget_save_settings_delete' ) ) { 106 | $this->markTestSkipped( 'Test requires Core patch from https://github.com/xwp/wordpress-develop/pull/85 to be applied.' ); 107 | return; 108 | } 109 | 110 | $deleted_widget_id = null; 111 | add_action( 'delete_post', function( $post_id ) use ( &$deleted_widget_id ) { 112 | $post = get_post( $post_id ); 113 | $this->assertContains( $post->post_type, array( Widget_Posts::INSTANCE_POST_TYPE, 'revision' ) ); 114 | $deleted_widget_id = $post->post_name; 115 | }, 10, 2 ); 116 | 117 | parent::test_wp_widget_save_settings_delete(); 118 | 119 | $this->assertEquals( 'search-2', $deleted_widget_id ); 120 | } 121 | 122 | /** 123 | * Test that registering a widget class and registering a widget instance work together. 124 | */ 125 | function test_register_and_unregister_widget_instance() { 126 | global $wp_widget_factory, $wp_registered_widgets; 127 | $wp_widget_factory->widgets = $wp_registered_widgets = array(); // WPCS: Global override ok. 128 | parent::test_register_and_unregister_widget_instance(); 129 | } 130 | 131 | function tearDown() { 132 | parent::tearDown(); 133 | if ( $this->css_concat_init_priority ) { 134 | add_action( 'init', 'css_concat_init', $this->css_concat_init_priority ); 135 | } 136 | if ( $this->js_concat_init_priority ) { 137 | add_action( 'init', 'js_concat_init', $this->js_concat_init_priority ); 138 | } 139 | } 140 | } 141 | --------------------------------------------------------------------------------