├── .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( '', 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 | [](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 |
--------------------------------------------------------------------------------