[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> site-health.js (source)

   1  /**
   2   * @output wp-admin/js/site-health.js
   3   */
   4  
   5  /* global ajaxurl, ClipboardJS, SiteHealth, wp */
   6  
   7  /**
   8   * Handles the interactions used by the Site Health modules in WordPress.
   9   *
  10   * @param {JQueryStatic} $ The jQuery object.
  11   */
  12  jQuery( function( $ ) {
  13  
  14      var __ = wp.i18n.__,
  15          _n = wp.i18n._n,
  16          sprintf = wp.i18n.sprintf,
  17          clipboard = new ClipboardJS( '.site-health-copy-buttons .copy-button' ),
  18          isStatusTab = $( '.health-check-body.health-check-status-tab' ).length,
  19          isDebugTab = $( '.health-check-body.health-check-debug-tab' ).length,
  20          pathsSizesSection = $( '#health-check-accordion-block-wp-paths-sizes' ),
  21          menuCounterWrapper = $( '#adminmenu .site-health-counter' ),
  22          menuCounter = $( '#adminmenu .site-health-counter .count' ),
  23          successTimeout;
  24  
  25      // Debug information copy section.
  26      clipboard.on( 'success', function( e ) {
  27          var triggerElement = $( e.trigger ),
  28              successElement = $( '.success', triggerElement.closest( 'div' ) );
  29  
  30          // Clear the selection and move focus back to the trigger.
  31          e.clearSelection();
  32  
  33          // Show success visual feedback.
  34          clearTimeout( successTimeout );
  35          successElement.removeClass( 'hidden' );
  36  
  37          // Hide success visual feedback after 3 seconds since last success.
  38          successTimeout = setTimeout( function() {
  39              successElement.addClass( 'hidden' );
  40          }, 3000 );
  41  
  42          // Handle success audible feedback.
  43          wp.a11y.speak( __( 'Site information has been copied to your clipboard.' ) );
  44      } );
  45  
  46      // Accordion handling in various areas.
  47      $( '.health-check-accordion' ).on( 'click', '.health-check-accordion-trigger', function() {
  48          var isExpanded = ( 'true' === $( this ).attr( 'aria-expanded' ) );
  49  
  50          if ( $( this ).prop( 'id' ) ) {
  51              window.location.hash = $( this ).prop( 'id' );
  52          }
  53  
  54          if ( isExpanded ) {
  55              $( this ).attr( 'aria-expanded', 'false' );
  56              $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', true );
  57          } else {
  58              $( this ).attr( 'aria-expanded', 'true' );
  59              $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', false );
  60          }
  61      } );
  62  
  63      /* global setTimeout */
  64      wp.domReady( function() {
  65          // Get hash from query string and open the related accordion.
  66          var hash = window.location.hash;
  67  
  68          if ( hash ) {
  69              var requestedPanel = $( hash );
  70  
  71              if ( requestedPanel.is( '.health-check-accordion-trigger' ) ) {
  72                  requestedPanel.trigger( 'click' );
  73              }
  74          }
  75      } );
  76  
  77      // Site Health test handling.
  78  
  79      $( '.site-health-view-passed' ).on( 'click', function() {
  80          var goodIssuesWrapper = $( '#health-check-issues-good' );
  81  
  82          goodIssuesWrapper.toggleClass( 'hidden' );
  83          $( this ).attr( 'aria-expanded', ! goodIssuesWrapper.hasClass( 'hidden' ) );
  84      } );
  85  
  86      /**
  87       * Validates the Site Health test result format.
  88       *
  89       * @since 5.6.0
  90       *
  91       * @param {Object} issue The issue data to validate.
  92       *
  93       * @return {boolean} True if the issue data is valid, false otherwise.
  94       */
  95  	function validateIssueData( issue ) {
  96          // Expected minimum format of a valid SiteHealth test response.
  97          var minimumExpected = {
  98                  test: 'string',
  99                  label: 'string',
 100                  description: 'string'
 101              },
 102              passed = true,
 103              key, value, subKey, subValue;
 104  
 105          // If the issue passed is not an object, return a `false` state early.
 106          if ( 'object' !== typeof( issue ) ) {
 107              return false;
 108          }
 109  
 110          // Loop over expected data and match the data types.
 111          for ( key in minimumExpected ) {
 112              value = minimumExpected[ key ];
 113  
 114              if ( 'object' === typeof( value ) ) {
 115                  for ( subKey in value ) {
 116                      subValue = value[ subKey ];
 117  
 118                      if ( 'undefined' === typeof( issue[ key ] ) ||
 119                          'undefined' === typeof( issue[ key ][ subKey ] ) ||
 120                          subValue !== typeof( issue[ key ][ subKey ] )
 121                      ) {
 122                          passed = false;
 123                      }
 124                  }
 125              } else {
 126                  if ( 'undefined' === typeof( issue[ key ] ) ||
 127                      value !== typeof( issue[ key ] )
 128                  ) {
 129                      passed = false;
 130                  }
 131              }
 132          }
 133  
 134          return passed;
 135      }
 136  
 137      /**
 138       * Appends a new issue to the issue list.
 139       *
 140       * @since 5.2.0
 141       *
 142       * @param {Object} issue The issue data.
 143       * @return {void|boolean} True if the issue was appended, false otherwise.
 144       */
 145  	function appendIssue( issue ) {
 146          var template = wp.template( 'health-check-issue' ),
 147              issueWrapper = $( '#health-check-issues-' + issue.status ),
 148              heading,
 149              count;
 150  
 151          /*
 152           * Validate the issue data format before using it.
 153           * If the output is invalid, discard it.
 154           */
 155          if ( ! validateIssueData( issue ) ) {
 156              return false;
 157          }
 158  
 159          SiteHealth.site_status.issues[ issue.status ]++;
 160  
 161          count = SiteHealth.site_status.issues[ issue.status ];
 162  
 163          // If no test name is supplied, append a placeholder for markup references.
 164          if ( typeof issue.test === 'undefined' ) {
 165              issue.test = issue.status + count;
 166          }
 167  
 168          if ( 'critical' === issue.status ) {
 169              heading = sprintf(
 170                  _n( '%s critical issue', '%s critical issues', count ),
 171                  '<span class="issue-count">' + count + '</span>'
 172              );
 173          } else if ( 'recommended' === issue.status ) {
 174              heading = sprintf(
 175                  _n( '%s recommended improvement', '%s recommended improvements', count ),
 176                  '<span class="issue-count">' + count + '</span>'
 177              );
 178          } else if ( 'good' === issue.status ) {
 179              heading = sprintf(
 180                  _n( '%s item with no issues detected', '%s items with no issues detected', count ),
 181                  '<span class="issue-count">' + count + '</span>'
 182              );
 183          }
 184  
 185          if ( heading ) {
 186              $( '.site-health-issue-count-title', issueWrapper ).html( heading );
 187          }
 188  
 189          menuCounter.text( SiteHealth.site_status.issues.critical );
 190  
 191          if ( 0 < parseInt( SiteHealth.site_status.issues.critical, 0 ) ) {
 192              $( '#health-check-issues-critical' ).removeClass( 'hidden' );
 193  
 194              menuCounterWrapper.removeClass( 'count-0' );
 195          } else {
 196              menuCounterWrapper.addClass( 'count-0' );
 197          }
 198          if ( 0 < parseInt( SiteHealth.site_status.issues.recommended, 0 ) ) {
 199              $( '#health-check-issues-recommended' ).removeClass( 'hidden' );
 200          }
 201  
 202          $( '.issues', '#health-check-issues-' + issue.status ).append( template( issue ) );
 203      }
 204  
 205      /**
 206       * Updates site health status indicator as asynchronous tests are run and returned.
 207       *
 208       * @since 5.2.0
 209       */
 210  	function recalculateProgression() {
 211          var r, c, pct;
 212          var $progress = $( '.site-health-progress' );
 213          var $wrapper = $progress.closest( '.site-health-progress-wrapper' );
 214          var $progressLabel = $( '.site-health-progress-label', $wrapper );
 215          var $circle = $( '.site-health-progress svg #bar' );
 216          var totalTests = parseInt( SiteHealth.site_status.issues.good, 0 ) +
 217              parseInt( SiteHealth.site_status.issues.recommended, 0 ) +
 218              ( parseInt( SiteHealth.site_status.issues.critical, 0 ) * 1.5 );
 219          var failedTests = ( parseInt( SiteHealth.site_status.issues.recommended, 0 ) * 0.5 ) +
 220              ( parseInt( SiteHealth.site_status.issues.critical, 0 ) * 1.5 );
 221          var val = 100 - Math.ceil( ( failedTests / totalTests ) * 100 );
 222  
 223          if ( 0 === totalTests ) {
 224              $progress.addClass( 'hidden' );
 225              return;
 226          }
 227  
 228          $wrapper.removeClass( 'loading' );
 229  
 230          r = $circle.attr( 'r' );
 231          c = Math.PI * ( r * 2 );
 232  
 233          if ( 0 > val ) {
 234              val = 0;
 235          }
 236          if ( 100 < val ) {
 237              val = 100;
 238          }
 239  
 240          pct = ( ( 100 - val ) / 100 ) * c + 'px';
 241  
 242          $circle.css( { strokeDashoffset: pct } );
 243  
 244          if ( 80 <= val && 0 === parseInt( SiteHealth.site_status.issues.critical, 0 ) ) {
 245              $wrapper.addClass( 'green' ).removeClass( 'orange' );
 246  
 247              $progressLabel.text( __( 'Good' ) );
 248              announceTestsProgression( 'good' );
 249          } else {
 250              $wrapper.addClass( 'orange' ).removeClass( 'green' );
 251  
 252              $progressLabel.text( __( 'Should be improved' ) );
 253              announceTestsProgression( 'improvable' );
 254          }
 255  
 256          if ( isStatusTab ) {
 257              $.post(
 258                  ajaxurl,
 259                  {
 260                      'action': 'health-check-site-status-result',
 261                      '_wpnonce': SiteHealth.nonce.site_status_result,
 262                      'counts': SiteHealth.site_status.issues
 263                  }
 264              );
 265  
 266              if ( 100 === val ) {
 267                  $( '.site-status-all-clear' ).removeClass( 'hide' );
 268                  $( '.site-status-has-issues' ).addClass( 'hide' );
 269              }
 270          }
 271      }
 272  
 273      /**
 274       * Queues the next asynchronous test when we're ready to run it.
 275       *
 276       * @since 5.2.0
 277       */
 278  	function maybeRunNextAsyncTest() {
 279          var doCalculation = true;
 280  
 281          if ( 1 <= SiteHealth.site_status.async.length ) {
 282              $.each( SiteHealth.site_status.async, function() {
 283                  var data = {
 284                      'action': 'health-check-' + this.test.replace( '_', '-' ),
 285                      '_wpnonce': SiteHealth.nonce.site_status
 286                  };
 287  
 288                  if ( this.completed ) {
 289                      return true;
 290                  }
 291  
 292                  doCalculation = false;
 293  
 294                  this.completed = true;
 295  
 296                  if ( 'undefined' !== typeof( this.has_rest ) && this.has_rest ) {
 297                      wp.apiRequest( {
 298                          url: wp.url.addQueryArgs( this.test, { _locale: 'user' } ),
 299                          headers: this.headers
 300                      } )
 301                          .done( function( response ) {
 302                              /** This filter is documented in wp-admin/includes/class-wp-site-health.php */
 303                              appendIssue( wp.hooks.applyFilters( 'site_status_test_result', response ) );
 304                          } )
 305                          .fail( function( response ) {
 306                              var description;
 307  
 308                              if ( 'undefined' !== typeof( response.responseJSON ) && 'undefined' !== typeof( response.responseJSON.message ) ) {
 309                                  description = response.responseJSON.message;
 310                              } else {
 311                                  description = __( 'No details available' );
 312                              }
 313  
 314                              addFailedSiteHealthCheckNotice( this.url, description );
 315                          } )
 316                          .always( function() {
 317                              maybeRunNextAsyncTest();
 318                          } );
 319                  } else {
 320                      $.post(
 321                          ajaxurl,
 322                          data
 323                      ).done( function( response ) {
 324                          /** This filter is documented in wp-admin/includes/class-wp-site-health.php */
 325                          appendIssue( wp.hooks.applyFilters( 'site_status_test_result', response.data ) );
 326                      } ).fail( function( response ) {
 327                          var description;
 328  
 329                          if ( 'undefined' !== typeof( response.responseJSON ) && 'undefined' !== typeof( response.responseJSON.message ) ) {
 330                              description = response.responseJSON.message;
 331                          } else {
 332                              description = __( 'No details available' );
 333                          }
 334  
 335                          addFailedSiteHealthCheckNotice( this.url, description );
 336                      } ).always( function() {
 337                          maybeRunNextAsyncTest();
 338                      } );
 339                  }
 340  
 341                  return false;
 342              } );
 343          }
 344  
 345          if ( doCalculation ) {
 346              recalculateProgression();
 347          }
 348      }
 349  
 350      /**
 351       * Add the details of a failed asynchronous test to the list of test results.
 352       *
 353       * @param {string} url         The URL of the failed test.
 354       * @param {string} description The description of the failed test.
 355       * @since 5.6.0
 356       */
 357  	function addFailedSiteHealthCheckNotice( url, description ) {
 358          var issue;
 359  
 360          issue = {
 361              'status': 'recommended',
 362              'label': __( 'A test is unavailable' ),
 363              'badge': {
 364                  'color': 'red',
 365                  'label': __( 'Unavailable' )
 366              },
 367              'description': '<p>' + url + '</p><p>' + description + '</p>',
 368              'actions': ''
 369          };
 370  
 371          /** This filter is documented in wp-admin/includes/class-wp-site-health.php */
 372          appendIssue( wp.hooks.applyFilters( 'site_status_test_result', issue ) );
 373      }
 374  
 375      if ( 'undefined' !== typeof SiteHealth ) {
 376          if ( 0 === SiteHealth.site_status.direct.length && 0 === SiteHealth.site_status.async.length ) {
 377              recalculateProgression();
 378          } else {
 379              SiteHealth.site_status.issues = {
 380                  'good': 0,
 381                  'recommended': 0,
 382                  'critical': 0
 383              };
 384          }
 385  
 386          if ( 0 < SiteHealth.site_status.direct.length ) {
 387              $.each( SiteHealth.site_status.direct, function() {
 388                  appendIssue( this );
 389              } );
 390          }
 391  
 392          if ( 0 < SiteHealth.site_status.async.length ) {
 393              maybeRunNextAsyncTest();
 394          } else {
 395              recalculateProgression();
 396          }
 397      }
 398  
 399      /**
 400       * Get the sizes of the directories in the Site Health Info section.
 401       */
 402  	function getDirectorySizes() {
 403          var timestamp = ( new Date().getTime() );
 404  
 405          // After 3 seconds announce that we're still waiting for directory sizes.
 406          var timeout = window.setTimeout( function() {
 407              announceTestsProgression( 'waiting-for-directory-sizes' );
 408          }, 3000 );
 409  
 410          wp.apiRequest( {
 411              path: '/wp-site-health/v1/directory-sizes'
 412          } ).done( function( response ) {
 413              updateDirSizes( response || {} );
 414          } ).always( function() {
 415              var delay = ( new Date().getTime() ) - timestamp;
 416  
 417              $( '.health-check-wp-paths-sizes.spinner' ).css( 'visibility', 'hidden' );
 418  
 419              if ( delay > 3000 ) {
 420                  /*
 421                   * We have announced that we're waiting.
 422                   * Announce that we're ready after giving at least 3 seconds
 423                   * for the first announcement to be read out, or the two may collide.
 424                   */
 425                  if ( delay > 6000 ) {
 426                      delay = 0;
 427                  } else {
 428                      delay = 6500 - delay;
 429                  }
 430  
 431                  window.setTimeout( function() {
 432                      recalculateProgression();
 433                  }, delay );
 434              } else {
 435                  // Cancel the announcement.
 436                  window.clearTimeout( timeout );
 437              }
 438  
 439              $( document ).trigger( 'site-health-info-dirsizes-done' );
 440          } );
 441      }
 442  
 443      /**
 444       * Updates the directory sizes in the Site Health Info section.
 445       *
 446       * @param {Object} data The directory sizes data.
 447       */
 448  	function updateDirSizes( data ) {
 449          var copyButton = $( 'button.button.copy-button' );
 450          var clipboardText = copyButton.attr( 'data-clipboard-text' );
 451  
 452          $.each( data, function( name, value ) {
 453              var text = value.debug || value.size;
 454  
 455              if ( typeof text !== 'undefined' ) {
 456                  clipboardText = clipboardText.replace( name + ': loading...', name + ': ' + text );
 457              }
 458          } );
 459  
 460          copyButton.attr( 'data-clipboard-text', clipboardText );
 461  
 462          pathsSizesSection.find( 'td[class]' ).each( function( i, element ) {
 463              var td = $( element );
 464              var name = td.attr( 'class' );
 465  
 466              if ( data.hasOwnProperty( name ) && data[ name ].size ) {
 467                  td.text( data[ name ].size );
 468              }
 469          } );
 470      }
 471  
 472      if ( isDebugTab ) {
 473          if ( pathsSizesSection.length ) {
 474              getDirectorySizes();
 475          } else {
 476              recalculateProgression();
 477          }
 478      }
 479  
 480      // Trigger a class toggle when the extended menu button is clicked.
 481      $( '.health-check-offscreen-nav-wrapper' ).on( 'click', function() {
 482          $( this ).toggleClass( 'visible' );
 483      } );
 484  
 485      /**
 486       * Announces to assistive technologies the tests progression status.
 487       *
 488       * @since 6.4.0
 489       *
 490       * @param {string} type The type of message to be announced.
 491       *
 492       * @return {void}
 493       */
 494  	function announceTestsProgression( type ) {
 495          // Only announce the messages in the Site Health pages.
 496          if ( 'site-health' !== SiteHealth.screen ) {
 497              return;
 498          }
 499  
 500          switch ( type ) {
 501              case 'good':
 502                  wp.a11y.speak( __( 'All site health tests have finished running. Your site is looking good.' ) );
 503                  break;
 504              case 'improvable':
 505                  wp.a11y.speak( __( 'All site health tests have finished running. There are items that should be addressed.' ) );
 506                  break;
 507              case 'waiting-for-directory-sizes':
 508                  wp.a11y.speak( __( 'Running additional tests... please wait.' ) );
 509                  break;
 510              default:
 511                  return;
 512          }
 513      }
 514  } );


Generated : Sun Sep 13 08:20:28 2026 Cross-referenced by PHPXref