PluginProbe
Plugin Check (PCP) / 1.9.0
Plugin Check (PCP) v1.9.0
2.1.0 trunk 0.1 0.2.0 0.2.1 0.2.2 0.2.3 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 1.9.0 2.0.0 ci-artifacts
plugin-check / assets / js / plugin-check-admin.js

plugin-check-admin.js in Plugin Check (PCP) 1.9.0, at assets/js/plugin-check-admin.js

766 lines 18.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function ( pluginCheck ) {
2 const checkItButton = document.getElementById( 'plugin-check__submit' );
3 const resultsContainer = document.getElementById( 'plugin-check__results' );
4 const exportContainer = document.getElementById(
5 'plugin-check__export-controls'
6 );
7 const spinner = document.getElementById( 'plugin-check__spinner' );
8 const pluginsList = document.getElementById(
9 'plugin-check__plugins-dropdown'
10 );
11 const categoriesList = document.querySelectorAll(
12 'input[name=categories]'
13 );
14 const typesList = document.querySelectorAll( 'input[name=types]' );
15 const templates = {};
16
17 // Return early if the elements cannot be found on the page.
18 if (
19 ! checkItButton ||
20 ! pluginsList ||
21 ! resultsContainer ||
22 ! exportContainer ||
23 ! spinner ||
24 ! categoriesList.length ||
25 ! typesList.length
26 ) {
27 console.error( 'Missing form elements on page' );
28 return;
29 }
30
31 let aggregatedResults = createEmptyAggregatedResults();
32 let checksCompleted = false;
33 exportContainer.classList.add( 'is-hidden' );
34 exportContainer.addEventListener( 'click', onExportContainerClick );
35
36 const includeExperimental = document.getElementById(
37 'plugin-check__include-experimental'
38 );
39
40 // Handle disabling the Check it button when a plugin is not selected.
41 function canRunChecks() {
42 if ( '' === pluginsList.value ) {
43 checkItButton.disabled = true;
44 } else {
45 checkItButton.disabled = false;
46 }
47 }
48
49 // Run on page load to test if dropdown is auto populated.
50 canRunChecks();
51 pluginsList.addEventListener( 'change', canRunChecks );
52
53 function saveUserSettings() {
54 const selectedCategories = [];
55
56 // Assuming you have a list of category checkboxes, find the selected ones.
57 categoriesList.forEach( function ( checkbox ) {
58 if ( checkbox.checked ) {
59 selectedCategories.push( checkbox.value );
60 }
61 } );
62
63 // Join the selected category slugs with '__' and save it as a user setting.
64 const settingValue = selectedCategories.join( '__' );
65 window.setUserSetting(
66 'plugin_check_category_preferences',
67 settingValue
68 );
69 }
70
71 // Attach the saveUserSettings function when a category checkbox is clicked.
72 categoriesList.forEach( function ( checkbox ) {
73 checkbox.addEventListener( 'change', saveUserSettings );
74 } );
75
76 // When the Check it button is clicked.
77 checkItButton.addEventListener( 'click', ( e ) => {
78 e.preventDefault();
79
80 resetResults();
81 checkItButton.disabled = true;
82 pluginsList.disabled = true;
83 spinner.classList.add( 'is-active' );
84 for ( let i = 0; i < categoriesList.length; i++ ) {
85 categoriesList[ i ].disabled = true;
86 }
87 for ( let i = 0; i < typesList.length; i++ ) {
88 typesList[ i ].disabled = true;
89 }
90
91 getChecksToRun()
92 .then( setUpEnvironment )
93 .then( runChecks )
94 .then( cleanUpEnvironment )
95 .then( ( data ) => {
96 console.log( data.message );
97
98 resetForm();
99 } )
100 .catch( ( error ) => {
101 console.error( error );
102
103 resetForm();
104 } );
105 } );
106
107 /**
108 * Reset the results container.
109 *
110 * @since 1.0.0
111 */
112 function resetResults() {
113 // Empty the results container.
114 resultsContainer.innerText = '';
115 exportContainer.innerHTML = '';
116 exportContainer.classList.add( 'is-hidden' );
117 resetAggregatedResults();
118 checksCompleted = false;
119 }
120
121 /**
122 * Resets the form controls once checks have completed or failed.
123 *
124 * @since 1.0.0
125 */
126 function resetForm() {
127 spinner.classList.remove( 'is-active' );
128 checkItButton.disabled = false;
129 pluginsList.disabled = false;
130 for ( let i = 0; i < categoriesList.length; i++ ) {
131 categoriesList[ i ].disabled = false;
132 }
133 for ( let i = 0; i < typesList.length; i++ ) {
134 typesList[ i ].disabled = false;
135 }
136 }
137
138 function createEmptyAggregatedResults() {
139 return {
140 errors: {},
141 warnings: {},
142 };
143 }
144
145 function resetAggregatedResults() {
146 aggregatedResults = createEmptyAggregatedResults();
147 }
148
149 function mergeAggregatedResults( results ) {
150 if ( results.errors ) {
151 mergeResultTree( aggregatedResults.errors, results.errors );
152 }
153 if ( results.warnings ) {
154 mergeResultTree( aggregatedResults.warnings, results.warnings );
155 }
156 }
157
158 function hasOwn( object, key ) {
159 return Object.prototype.hasOwnProperty.call( object, key );
160 }
161
162 function mergeResultTree( target, source ) {
163 for ( const file of Object.keys( source ) ) {
164 if ( ! hasOwn( target, file ) ) {
165 target[ file ] = {};
166 }
167
168 const sourceFile = source[ file ];
169 const targetFile = target[ file ];
170
171 for ( const line of Object.keys( sourceFile ) ) {
172 if ( ! hasOwn( targetFile, line ) ) {
173 targetFile[ line ] = {};
174 }
175
176 const sourceLine = sourceFile[ line ];
177 const targetLine = targetFile[ line ];
178
179 for ( const column of Object.keys( sourceLine ) ) {
180 if ( ! hasOwn( targetLine, column ) ) {
181 targetLine[ column ] = [];
182 }
183
184 for ( const entry of sourceLine[ column ] ) {
185 targetLine[ column ].push( cloneResultEntry( entry ) );
186 }
187 }
188 }
189 }
190 }
191
192 function cloneResultEntry( entry ) {
193 return { ...entry };
194 }
195
196 function hasAggregatedResults() {
197 return (
198 hasEntries( aggregatedResults.errors ) ||
199 hasEntries( aggregatedResults.warnings )
200 );
201 }
202
203 function hasEntries( tree ) {
204 for ( const file of Object.keys( tree ) ) {
205 const lines = tree[ file ] || {};
206
207 for ( const line of Object.keys( lines ) ) {
208 const columns = lines[ line ] || {};
209
210 for ( const column of Object.keys( columns ) ) {
211 if ( ( columns[ column ] || [] ).length > 0 ) {
212 return true;
213 }
214 }
215 }
216 }
217
218 return false;
219 }
220
221 function defaultString( key ) {
222 if (
223 pluginCheck.strings &&
224 Object.prototype.hasOwnProperty.call( pluginCheck.strings, key )
225 ) {
226 return pluginCheck.strings[ key ];
227 }
228 // Return empty string if localized string is missing.
229 return '';
230 }
231
232 function renderExportButtons() {
233 exportContainer.innerHTML = '';
234 if ( ! checksCompleted ) {
235 exportContainer.classList.add( 'is-hidden' );
236 return;
237 }
238
239 exportContainer.classList.remove( 'is-hidden' );
240
241 const exportButtonConfigs = [
242 {
243 format: 'csv',
244 label: defaultString( 'exportCsv' ),
245 },
246 {
247 format: 'json',
248 label: defaultString( 'exportJson' ),
249 },
250 {
251 format: 'markdown',
252 label: defaultString( 'exportMarkdown' ),
253 },
254 ];
255
256 exportButtonConfigs.forEach( ( item ) => {
257 const button = document.createElement( 'button' );
258 button.type = 'button';
259 button.classList.add(
260 'button',
261 'button-secondary',
262 'plugin-check__export-button'
263 );
264 button.textContent = item.label;
265 button.setAttribute( 'data-export-format', item.format );
266 exportContainer.appendChild( button );
267 } );
268 }
269
270 function announce( message ) {
271 if ( window.wp && window.wp.a11y && window.wp.a11y.speak ) {
272 window.wp.a11y.speak( message );
273 return;
274 }
275
276 console.warn( message );
277 }
278
279 function onExportContainerClick( event ) {
280 const button = event.target.closest( '[data-export-format]' );
281 if ( ! button || button.disabled ) {
282 return;
283 }
284
285 event.preventDefault();
286 handleExport( button );
287 }
288
289 function handleExport( button ) {
290 if ( ! hasAggregatedResults() ) {
291 announce( defaultString( 'noResults' ) );
292 return;
293 }
294
295 const format = button.getAttribute( 'data-export-format' );
296 if ( ! format ) {
297 return;
298 }
299
300 const originalText = button.textContent;
301 button.disabled = true;
302 button.textContent = defaultString( 'exporting' );
303
304 requestExport( format )
305 .then( ( payload ) => {
306 downloadExport( payload );
307 } )
308 .catch( ( error ) => {
309 console.error( error );
310 const failureMessage = defaultString( 'exportError' );
311 announce( failureMessage );
312 } )
313 .finally( () => {
314 button.disabled = false;
315 button.textContent = originalText;
316 } );
317 }
318
319 function requestExport( format ) {
320 const payload = new FormData();
321 payload.append( 'nonce', pluginCheck.nonce );
322 payload.append( 'action', pluginCheck.actionExportResults );
323 payload.append( 'format', format );
324 if ( pluginsList.value ) {
325 payload.append( 'plugin', pluginsList.value );
326 }
327 payload.append( 'plugin_label', getSelectedPluginLabel() );
328 payload.append( 'results', JSON.stringify( aggregatedResults ) );
329
330 return fetch( ajaxurl, {
331 method: 'POST',
332 credentials: 'same-origin',
333 body: payload,
334 } )
335 .then( ( response ) => response.json() )
336 .then( ( responseData ) => {
337 if ( ! responseData ) {
338 throw new Error( 'Response contains no data' );
339 }
340
341 if ( ! responseData.success ) {
342 const defaultExportErrorMessage =
343 defaultString( 'exportError' );
344 let message = defaultExportErrorMessage;
345 if ( responseData.data && responseData.data.message ) {
346 message = responseData.data.message;
347 }
348 throw new Error( message );
349 }
350
351 if (
352 ! responseData.data ||
353 ! responseData.data.content ||
354 ! responseData.data.filename
355 ) {
356 throw new Error( 'Export payload is incomplete' );
357 }
358
359 return responseData.data;
360 } );
361 }
362
363 function downloadExport( exportPayload ) {
364 const blob = new Blob( [ exportPayload.content ], {
365 type: exportPayload.mime_type || 'text/plain',
366 } );
367 const downloadLink = document.createElement( 'a' );
368 downloadLink.href = window.URL.createObjectURL( blob );
369 downloadLink.download = exportPayload.filename;
370 document.body.appendChild( downloadLink );
371 downloadLink.click();
372 document.body.removeChild( downloadLink );
373 window.URL.revokeObjectURL( downloadLink.href );
374 }
375
376 function getSelectedPluginLabel() {
377 const selectedIndex = pluginsList.selectedIndex;
378 if ( selectedIndex < 0 ) {
379 return '';
380 }
381 return pluginsList.options[ selectedIndex ].text;
382 }
383
384 /**
385 * Setup the runtime environment if needed.
386 *
387 * @since 1.0.0
388 *
389 * @param {Object} data Data object with props passed to form data.
390 */
391 function setUpEnvironment( data ) {
392 const pluginCheckData = new FormData();
393 pluginCheckData.append( 'nonce', pluginCheck.nonce );
394 pluginCheckData.append( 'plugin', data.plugin );
395 pluginCheckData.append(
396 'action',
397 pluginCheck.actionSetUpRuntimeEnvironment
398 );
399 pluginCheckData.append(
400 'include-experimental',
401 includeExperimental && includeExperimental.checked ? 1 : 0
402 );
403
404 for ( let i = 0; i < data.checks.length; i++ ) {
405 pluginCheckData.append( 'checks[]', data.checks[ i ] );
406 }
407
408 return fetch( ajaxurl, {
409 method: 'POST',
410 credentials: 'same-origin',
411 body: pluginCheckData,
412 } )
413 .then( ( response ) => {
414 return response.json();
415 } )
416 .then( handleDataErrors )
417 .then( ( responseData ) => {
418 if ( ! responseData.data || ! responseData.data.message ) {
419 throw new Error( 'Response contains no data.' );
420 }
421
422 console.log( responseData.data.message );
423
424 return responseData.data;
425 } );
426 }
427
428 /**
429 * Cleanup the runtime environment.
430 *
431 * @since 1.0.0
432 *
433 * @return {Object} The response data.
434 */
435 function cleanUpEnvironment() {
436 const pluginCheckData = new FormData();
437 pluginCheckData.append( 'nonce', pluginCheck.nonce );
438 pluginCheckData.append(
439 'action',
440 pluginCheck.actionCleanUpRuntimeEnvironment
441 );
442
443 return fetch( ajaxurl, {
444 method: 'POST',
445 credentials: 'same-origin',
446 body: pluginCheckData,
447 } )
448 .then( ( response ) => {
449 return response.json();
450 } )
451 .then( handleDataErrors )
452 .then( ( responseData ) => {
453 if ( ! responseData.data || ! responseData.data.message ) {
454 throw new Error( 'Response contains no data.' );
455 }
456
457 return responseData.data;
458 } );
459 }
460
461 /**
462 * Get the Checks to run.
463 *
464 * @since 1.0.0
465 */
466 function getChecksToRun() {
467 const pluginCheckData = new FormData();
468 pluginCheckData.append( 'nonce', pluginCheck.nonce );
469 pluginCheckData.append( 'plugin', pluginsList.value );
470 pluginCheckData.append( 'action', pluginCheck.actionGetChecksToRun );
471 pluginCheckData.append(
472 'include-experimental',
473 includeExperimental && includeExperimental.checked ? 1 : 0
474 );
475
476 for ( let i = 0; i < categoriesList.length; i++ ) {
477 if ( categoriesList[ i ].checked ) {
478 pluginCheckData.append(
479 'categories[]',
480 categoriesList[ i ].value
481 );
482 }
483 }
484
485 return fetch( ajaxurl, {
486 method: 'POST',
487 credentials: 'same-origin',
488 body: pluginCheckData,
489 } )
490 .then( ( response ) => {
491 return response.json();
492 } )
493 .then( handleDataErrors )
494 .then( ( responseData ) => {
495 if (
496 ! responseData.data ||
497 ! responseData.data.plugin ||
498 ! responseData.data.checks
499 ) {
500 throw new Error(
501 'Plugin and Checks are missing from the response.'
502 );
503 }
504
505 return responseData.data;
506 } );
507 }
508
509 /**
510 * Run Checks.
511 *
512 * @since 1.0.0
513 *
514 * @param {Object} data The response data.
515 */
516 async function runChecks( data ) {
517 let isSuccessMessage = true;
518 for ( let i = 0; i < data.checks.length; i++ ) {
519 try {
520 const results = await runCheck( data.plugin, data.checks[ i ] );
521 const errorsLength = Object.values( results.errors ).length;
522 const warningsLength = Object.values( results.warnings ).length;
523 if (
524 isSuccessMessage &&
525 ( errorsLength > 0 || warningsLength > 0 )
526 ) {
527 isSuccessMessage = false;
528 }
529 mergeAggregatedResults( results );
530 renderResults( results );
531 } catch ( e ) {
532 // Ignore for now.
533 }
534 }
535
536 renderResultsMessage( isSuccessMessage );
537 }
538
539 /**
540 * Renders result message.
541 *
542 * @since 1.0.0
543 *
544 * @param {boolean} isSuccessMessage Whether the message is a success message.
545 */
546 function renderResultsMessage( isSuccessMessage ) {
547 const messageType = isSuccessMessage ? 'success' : 'error';
548 const messageText = isSuccessMessage
549 ? pluginCheck.successMessage
550 : pluginCheck.errorMessage;
551
552 resultsContainer.innerHTML =
553 renderTemplate( 'plugin-check-results-complete', {
554 type: messageType,
555 message: messageText,
556 } ) + resultsContainer.innerHTML;
557
558 checksCompleted = true;
559 renderExportButtons();
560 }
561
562 /**
563 * Run a single check.
564 *
565 * @since 1.0.0
566 *
567 * @param {string} plugin The plugin to check.
568 * @param {string} check The check to run.
569 * @return {Object} The check results.
570 */
571 function runCheck( plugin, check ) {
572 const pluginCheckData = new FormData();
573 pluginCheckData.append( 'nonce', pluginCheck.nonce );
574 pluginCheckData.append( 'plugin', plugin );
575 pluginCheckData.append( 'checks[]', check );
576 pluginCheckData.append( 'action', pluginCheck.actionRunChecks );
577 pluginCheckData.append(
578 'include-experimental',
579 includeExperimental && includeExperimental.checked ? 1 : 0
580 );
581
582 for ( let i = 0; i < typesList.length; i++ ) {
583 if ( typesList[ i ].checked ) {
584 pluginCheckData.append( 'types[]', typesList[ i ].value );
585 }
586 }
587
588 return fetch( ajaxurl, {
589 method: 'POST',
590 credentials: 'same-origin',
591 body: pluginCheckData,
592 } )
593 .then( ( response ) => {
594 return response.json();
595 } )
596 .then( handleDataErrors )
597 .then( ( responseData ) => {
598 // If the response is successful and there is no message in the response.
599 if ( ! responseData.data || ! responseData.data.message ) {
600 throw new Error( 'Response contains no data' );
601 }
602
603 return responseData.data;
604 } );
605 }
606
607 /**
608 * Handles any errors in the data returned from the response.
609 *
610 * @since 1.0.0
611 *
612 * @param {Object} data The response data.
613 * @return {Object} The response data.
614 */
615 function handleDataErrors( data ) {
616 if ( ! data ) {
617 throw new Error( 'Response contains no data' );
618 }
619
620 if ( ! data.success ) {
621 // If not successful and no message in the response.
622 if ( ! data.data || ! data.data[ 0 ].message ) {
623 throw new Error( 'Response contains no data' );
624 }
625
626 // If not successful and there is a message in the response.
627 throw new Error( data.data[ 0 ].message );
628 }
629
630 return data;
631 }
632
633 /**
634 * Renders results for each check on the page.
635 *
636 * @since 1.0.0
637 *
638 * @param {Object} results The results object.
639 */
640 function renderResults( results ) {
641 const { errors, warnings } = results;
642 // Render errors and warnings for files.
643 for ( const file in errors ) {
644 if ( warnings[ file ] ) {
645 renderFileResults( file, errors[ file ], warnings[ file ] );
646 delete warnings[ file ];
647 } else {
648 renderFileResults( file, errors[ file ], [] );
649 }
650 }
651
652 // Render remaining files with only warnings.
653 for ( const file in warnings ) {
654 renderFileResults( file, [], warnings[ file ] );
655 }
656 }
657
658 /**
659 * Renders the file results table.
660 *
661 * @since 1.0.0
662 *
663 * @param {string} file The file name for the results.
664 * @param {Object} errors The file errors.
665 * @param {Object} warnings The file warnings.
666 */
667 function renderFileResults( file, errors, warnings ) {
668 const index =
669 Date.now().toString( 36 ) +
670 Math.random().toString( 36 ).substr( 2 );
671
672 // Check if any errors or warnings have links.
673 const hasLinks =
674 hasLinksInResults( errors ) || hasLinksInResults( warnings );
675
676 // Render the file table.
677 resultsContainer.innerHTML += renderTemplate(
678 'plugin-check-results-table',
679 { file, index, hasLinks }
680 );
681 const resultsTable = document.getElementById(
682 'plugin-check__results-body-' + index
683 );
684
685 // Render results to the table.
686 renderResultRows( 'ERROR', errors, resultsTable, hasLinks );
687 renderResultRows( 'WARNING', warnings, resultsTable, hasLinks );
688 }
689
690 /**
691 * Checks if there are any links in the results object.
692 *
693 * @since 1.0.0
694 *
695 * @param {Object} results The results object.
696 * @return {boolean} True if there are links, false otherwise.
697 */
698 function hasLinksInResults( results ) {
699 for ( const line in results ) {
700 for ( const column in results[ line ] ) {
701 for ( let i = 0; i < results[ line ][ column ].length; i++ ) {
702 if ( results[ line ][ column ][ i ].link ) {
703 return true;
704 }
705 }
706 }
707 }
708 return false;
709 }
710
711 /**
712 * Renders a result row onto the file table.
713 *
714 * @since 1.0.0
715 *
716 * @param {string} type The result type. Either ERROR or WARNING.
717 * @param {Object} results The results object.
718 * @param {Object} table The HTML table to append a result row to.
719 * @param {boolean} hasLinks Whether any result has links.
720 */
721 function renderResultRows( type, results, table, hasLinks ) {
722 // Loop over each result by the line, column and messages.
723 for ( const line in results ) {
724 for ( const column in results[ line ] ) {
725 for ( let i = 0; i < results[ line ][ column ].length; i++ ) {
726 const message = results[ line ][ column ][ i ].message;
727 const docs = results[ line ][ column ][ i ].docs;
728 const code = results[ line ][ column ][ i ].code;
729 const link = results[ line ][ column ][ i ].link;
730
731 table.innerHTML += renderTemplate(
732 'plugin-check-results-row',
733 {
734 line,
735 column,
736 type,
737 message,
738 docs,
739 code,
740 link,
741 hasLinks,
742 }
743 );
744 }
745 }
746 }
747 }
748
749 /**
750 * Renders the template with data.
751 *
752 * @since 1.0.0
753 *
754 * @param {string} templateSlug The template slug
755 * @param {Object} data Template data.
756 * @return {string} Template HTML.
757 */
758 function renderTemplate( templateSlug, data ) {
759 if ( ! templates[ templateSlug ] ) {
760 templates[ templateSlug ] = wp.template( templateSlug );
761 }
762 const template = templates[ templateSlug ];
763 return template( data );
764 }
765 } )( PLUGIN_CHECK ); /* global PLUGIN_CHECK */
766