WordPress.org

Make WordPress Core

Ticket #46573: 46573.6.diff

File 46573.6.diff, 135.0 KB (added by earnjam, 17 months ago)
  • Gruntfile.js

    diff --git Gruntfile.js Gruntfile.js
    index a27fc2b735..f40fa336cf 100644
    module.exports = function(grunt) { 
    263263                                        [ WORKING_DIR + 'wp-admin/js/tags-box.js' ]: [ './src/js/_enqueues/admin/tags-box.js' ],
    264264                                        [ WORKING_DIR + 'wp-admin/js/tags-suggest.js' ]: [ './src/js/_enqueues/admin/tags-suggest.js' ],
    265265                                        [ WORKING_DIR + 'wp-admin/js/tags.js' ]: [ './src/js/_enqueues/admin/tags.js' ],
     266                                        [ WORKING_DIR + 'wp-admin/js/site-health.js' ]: [ './src/js/_enqueues/admin/site-health.js' ],
    266267                                        [ WORKING_DIR + 'wp-admin/js/theme-plugin-editor.js' ]: [ './src/js/_enqueues/wp/theme-plugin-editor.js' ],
    267268                                        [ WORKING_DIR + 'wp-admin/js/theme.js' ]: [ './src/js/_enqueues/wp/theme.js' ],
    268269                                        [ WORKING_DIR + 'wp-admin/js/updates.js' ]: [ './src/js/_enqueues/wp/updates.js' ],
  • new file src/js/_enqueues/admin/site-health.js

    diff --git src/js/_enqueues/admin/site-health.js src/js/_enqueues/admin/site-health.js
    new file mode 100644
    index 0000000000..77d5182e22
    - +  
     1/**
     2 * Interactions used by the Site Health modules in WordPress.
     3 *
     4 * @output wp-admin/js/site-health.js
     5 */
     6
     7/* global ajaxurl, SiteHealth, wp */
     8
     9jQuery( document ).ready( function( $ ) {
     10
     11        var data;
     12
     13        // Debug information copy section.
     14        $( '.health-check-copy-field' ).click( function( e ) {
     15                var $textarea = $( '#system-information-' + $( this ).data( 'copy-field' ) + '-copy-field' ),
     16                        $wrapper = $( this ).closest( 'div' );
     17
     18                e.preventDefault();
     19
     20                $textarea.select();
     21
     22                if ( document.execCommand( 'copy' ) ) {
     23                        $( '.copy-field-success', $wrapper ).addClass( 'visible' );
     24                        $( this ).focus();
     25
     26                        wp.a11y.speak( SiteHealth.string.site_info_copied, 'polite' );
     27                }
     28        });
     29
     30        $( '.health-check-toggle-copy-section' ).click( function( e ) {
     31                var $copySection = $( '.system-information-copy-wrapper' );
     32
     33                e.preventDefault();
     34
     35                if ( $copySection.hasClass( 'hidden' ) ) {
     36                        $copySection.removeClass( 'hidden' );
     37
     38                        $( this ).text( SiteHealth.string.site_info_hide_copy );
     39                } else {
     40                        $copySection.addClass( 'hidden' );
     41
     42                        $( this ).text( SiteHealth.string.site_info_show_copy );
     43                }
     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 ( isExpanded ) {
     51                        $( this ).attr( 'aria-expanded', 'false' );
     52                        $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', true );
     53                } else {
     54                        $( this ).attr( 'aria-expanded', 'true' );
     55                        $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', false );
     56                }
     57        });
     58
     59        $( '.health-check-accordion' ).on( 'keyup', '.health-check-accordion-trigger', function( e ) {
     60                if ( '38' === e.keyCode.toString() ) {
     61                        $( '.health-check-accordion-trigger', $( this ).closest( 'dt' ).prevAll( 'dt' ) ).focus();
     62                } else if ( '40' === e.keyCode.toString() ) {
     63                        $( '.health-check-accordion-trigger', $( this ).closest( 'dt' ).nextAll( 'dt' ) ).focus();
     64                }
     65        });
     66
     67        // Site Health test handling.
     68
     69        $( '.site-health-view-passed' ).on( 'click', function() {
     70                var goodIssuesWrapper = $( '#health-check-issues-good' );
     71
     72                goodIssuesWrapper.toggleClass( 'hidden' );
     73                $( this ).attr( 'aria-expanded', ! goodIssuesWrapper.hasClass( 'hidden' ) );
     74        });
     75
     76        function AppendIssue( issue ) {
     77                var htmlOutput = _.template( $( '#issue-template' ).html() )( issue ),
     78                        issueWrapper = $( '#health-check-issues-' + issue.status ),
     79                        issueCounter = $( '.issue-count', issueWrapper );
     80
     81                SiteHealth.site_status.issues[ issue.status ]++;
     82
     83                issueCounter.text( SiteHealth.site_status.issues[ issue.status ]);
     84                $( '.issues', '#health-check-issues-' + issue.status ).append( htmlOutput );
     85        }
     86
     87        function RecalculateProgression() {
     88                var r, c, pct;
     89                var $progressBar = $( '#progressbar' );
     90                var $circle = $( '#progressbar svg #bar' );
     91                var totalTests = parseInt( SiteHealth.site_status.issues.good, 0 ) + parseInt( SiteHealth.site_status.issues.recommended, 0 ) + ( parseInt( SiteHealth.site_status.issues.critical, 0 ) * 1.5 );
     92                var failedTests = parseInt( SiteHealth.site_status.issues.recommended, 0 ) + ( parseInt( SiteHealth.site_status.issues.critical, 0 ) * 1.5 );
     93                var val = 100 - Math.ceil( ( failedTests / totalTests ) * 100 );
     94
     95                if ( 0 === totalTests ) {
     96                        $progressBar.addClass( 'hidden' );
     97                        return;
     98                }
     99
     100                $progressBar.removeClass( 'loading' );
     101
     102                r = $circle.attr( 'r' );
     103                c = Math.PI * ( r * 2 );
     104
     105                if ( 0 > val ) {
     106                        val = 0;
     107                }
     108                if ( 100 < val ) {
     109                        val = 100;
     110                }
     111
     112                pct = ( ( 100 - val ) / 100 ) * c;
     113
     114                $circle.css({ strokeDashoffset: pct });
     115
     116                if ( 1 > parseInt( SiteHealth.site_status.issues.critical, 0 ) ) {
     117                        $( '#health-check-issues-critical' ).addClass( 'hidden' );
     118                }
     119
     120                if ( 1 > parseInt( SiteHealth.site_status.issues.recommended, 0 ) ) {
     121                        $( '#health-check-issues-recommended' ).addClass( 'hidden' );
     122                }
     123
     124                if ( 50 <= val ) {
     125                        $circle.addClass( 'orange' ).removeClass( 'red' );
     126                }
     127
     128                if ( 90 <= val ) {
     129                        $circle.addClass( 'green' ).removeClass( 'orange' );
     130                }
     131
     132                if ( 100 === val ) {
     133                        $( '.site-status-all-clear' ).removeClass( 'hide' );
     134                        $( '.site-status-has-issues' ).addClass( 'hide' );
     135                }
     136
     137                $progressBar.attr( 'data-pct', val );
     138                $progressBar.attr( 'aria-valuenow', val );
     139
     140                $( '.health-check-body' ).attr( 'aria-hidden', false );
     141
     142                $.post(
     143                        ajaxurl,
     144                        {
     145                                'action': 'health-check-site-status-result',
     146                                '_wpnonce': SiteHealth.nonce.site_status_result,
     147                                'counts': SiteHealth.site_status.issues
     148                        }
     149                );
     150
     151                wp.a11y.speak( SiteHealth.string.site_health_complete_screen_reader.replace( '%s', val + '%' ), 'polite' );
     152        }
     153
     154        function maybeRunNextAsyncTest() {
     155                var doCalculation = true;
     156
     157                if ( 1 <= SiteHealth.site_status.async.length ) {
     158                        $.each( SiteHealth.site_status.async, function() {
     159                                var data = {
     160                                        'action': 'health-check-' + this.test.replace( '_', '-' ),
     161                                        '_wpnonce': SiteHealth.nonce.site_status
     162                                };
     163
     164                                if ( this.completed ) {
     165                                        return true;
     166                                }
     167
     168                                doCalculation = false;
     169
     170                                this.completed = true;
     171
     172                                $.post(
     173                                        ajaxurl,
     174                                        data,
     175                                        function( response ) {
     176                                                AppendIssue( response.data );
     177                                                maybeRunNextAsyncTest();
     178                                        }
     179                                );
     180
     181                                return false;
     182                        });
     183                }
     184
     185                if ( doCalculation ) {
     186                        RecalculateProgression();
     187                }
     188        }
     189
     190        if ( 'undefined' !== typeof SiteHealth ) {
     191                if ( 0 === SiteHealth.site_status.direct.length && 0 === SiteHealth.site_status.async.length ) {
     192                        RecalculateProgression();
     193                } else {
     194                        SiteHealth.site_status.issues = {
     195                                'good': 0,
     196                                'recommended': 0,
     197                                'critical': 0
     198                        };
     199                }
     200
     201                if ( 0 < SiteHealth.site_status.direct.length ) {
     202                        $.each( SiteHealth.site_status.direct, function() {
     203                                AppendIssue( this );
     204                        });
     205                }
     206
     207                if ( 0 < SiteHealth.site_status.async.length ) {
     208                        data = {
     209                                'action': 'health-check-' + SiteHealth.site_status.async[0].test.replace( '_', '-' ),
     210                                '_wpnonce': SiteHealth.nonce.site_status
     211                        };
     212
     213                        SiteHealth.site_status.async[0].completed = true;
     214
     215                        $.post(
     216                                ajaxurl,
     217                                data,
     218                                function( response ) {
     219                                        AppendIssue( response.data );
     220                                        maybeRunNextAsyncTest();
     221                                }
     222                        );
     223                } else {
     224                        RecalculateProgression();
     225                }
     226        }
     227
     228});
  • src/wp-admin/admin-ajax.php

    diff --git src/wp-admin/admin-ajax.php src/wp-admin/admin-ajax.php
    index 93c32393e0..e0194ceba8 100644
    $core_actions_post = array( 
    131131        'edit-theme-plugin-file',
    132132        'wp-privacy-export-personal-data',
    133133        'wp-privacy-erase-personal-data',
     134        'health-check-site-status-result',
     135        'health-check-dotorg-communication',
     136        'health-check-is-in-debug-mode',
     137        'health-check-background-updates',
     138        'health-check-loopback-requests',
    134139);
    135140
    136141// Deprecated
  • new file src/wp-admin/css/site-health.css

    diff --git src/wp-admin/css/site-health.css src/wp-admin/css/site-health.css
    new file mode 100644
    index 0000000000..332afab5db
    - +  
     1body.site-health {
     2        background: #fff;
     3}
     4
     5body.site-health #wpcontent {
     6        padding-left: 0;
     7}
     8
     9body.site-health .wrap {
     10        margin-right: 0;
     11        margin-left: 0;
     12}
     13
     14body.site-health .wrap h2 {
     15        padding: 1rem 0.5rem;
     16        font-weight: 600;
     17}
     18
     19@media all and (min-width: 960px) {
     20        body.site-health .wrap h2 {
     21                padding: 1rem 0;
     22        }
     23}
     24
     25body.site-health ul li,
     26body.site-health ol li {
     27        line-height: 1.5;
     28}
     29
     30body.site-health .health-check-header {
     31        text-align: center;
     32        margin-bottom: 1rem;
     33        border-bottom: 1px solid #e2e4e7;
     34}
     35
     36body.site-health .health-check-header .title-section {
     37        display: flex;
     38        align-items: center;
     39        justify-content: center;
     40}
     41
     42body.site-health .health-check-header .title-section h1 {
     43        display: inline-block;
     44        font-weight: 600;
     45        margin: 1rem 0.8rem 1rem 0.8rem;
     46}
     47
     48body.site-health .health-check-header .title-section #progressbar {
     49        display: inline-block;
     50        height: 40px;
     51        width: 40px;
     52        margin: 0;
     53        border-radius: 100%;
     54        position: relative;
     55        font-weight: 600;
     56        font-size: 0.4rem;
     57}
     58
     59body.site-health .health-check-header .title-section #progressbar:after {
     60        position: absolute;
     61        display: block;
     62        height: 80px;
     63        width: 80px;
     64        left: 50%;
     65        top: 50%;
     66        content: attr(data-pct) "%";
     67        margin-top: -40px;
     68        margin-left: -40px;
     69        border-radius: 100%;
     70        line-height: 80px;
     71        font-size: 2em;
     72}
     73
     74body.site-health .health-check-header .title-section #progressbar.hidden {
     75        display: none;
     76}
     77
     78body.site-health .health-check-header .title-section #progressbar.loading:after {
     79        animation: loadingEllipsis 3s infinite ease-in-out;
     80}
     81
     82body.site-health .health-check-header .title-section #progressbar.loading svg #bar {
     83        stroke-dashoffset: 0;
     84        stroke: #adc5d2;
     85        animation: loadingPulse 3s infinite ease-in-out;
     86}
     87
     88body.site-health .health-check-header .title-section #progressbar svg circle {
     89        stroke-dashoffset: 0;
     90        transition: stroke-dashoffset 1s linear;
     91        stroke: #ccc;
     92        stroke-width: 2em;
     93}
     94
     95body.site-health .health-check-header .title-section #progressbar svg #bar {
     96        stroke-dashoffset: 565;
     97        stroke: #dc3232;
     98}
     99
     100body.site-health .health-check-header .title-section #progressbar svg #bar.green {
     101        stroke: #46b450;
     102}
     103
     104body.site-health .health-check-header .title-section #progressbar svg #bar.orange {
     105        stroke: #ffb900;
     106}
     107
     108@keyframes loadingPulse {
     109        0% {
     110                stroke: #adc5d2;
     111        }
     112        50% {
     113                stroke: #00a0d2;
     114        }
     115        100% {
     116                stroke: #adc5d2;
     117        }
     118}
     119
     120@keyframes loadingEllipsis {
     121        0% {
     122                content: ".";
     123        }
     124        50% {
     125                content: "..";
     126        }
     127        100% {
     128                content: "...";
     129        }
     130}
     131
     132body.site-health .health-check-header .tabs-wrapper {
     133        display: block;
     134}
     135
     136body.site-health .health-check-header .tabs-wrapper .tab {
     137        float: none;
     138        display: inline-block;
     139        text-decoration: none;
     140        color: inherit;
     141        padding: 0.5rem 1rem 1rem;
     142        margin: 0 1rem;
     143        transition: box-shadow 0.5s ease-in-out;
     144}
     145
     146body.site-health .health-check-header .tabs-wrapper .tab.active {
     147        box-shadow: inset 0 -3px #007cba;
     148        font-weight: 600;
     149}
     150
     151body.site-health .health-check-body {
     152        max-width: 800px;
     153        width: 100%;
     154        margin: 0 auto;
     155}
     156
     157body.site-health .health-check-table thead th:first-child,
     158body.site-health .health-check-table thead td:first-child {
     159        width: 30%;
     160}
     161
     162body.site-health .health-check-table tbody td {
     163        width: 70%;
     164}
     165
     166body.site-health .health-check-table tbody td:first-child {
     167        width: 30%;
     168}
     169
     170body.site-health .health-check-table tbody td ul,
     171body.site-health .health-check-table tbody td ol {
     172        margin: 0;
     173}
     174
     175body.site-health .pass:before,
     176body.site-health .good:before {
     177        content: "\f147";
     178        display: inline-block;
     179        color: #46b450;
     180        font-family: dashicons;
     181}
     182
     183body.site-health .warning:before {
     184        content: "\f460";
     185        display: inline-block;
     186        color: #ffb900;
     187        font-family: dashicons;
     188}
     189
     190body.site-health .info:before {
     191        content: "\f348";
     192        display: inline-block;
     193        color: #00a0d2;
     194        font-family: dashicons;
     195}
     196
     197body.site-health .fail:before,
     198body.site-health .error:before {
     199        content: "\f335";
     200        display: inline-block;
     201        color: #dc3232;
     202        font-family: dashicons;
     203}
     204
     205body.site-health .spinner {
     206        float: none;
     207}
     208
     209body.site-health .system-information-copy-wrapper {
     210        display: block;
     211        margin: 1rem 0;
     212}
     213
     214body.site-health .system-information-copy-wrapper.hidden {
     215        display: none;
     216}
     217
     218body.site-health .system-information-copy-wrapper textarea {
     219        width: 100%;
     220}
     221
     222body.site-health .system-information-copy-wrapper .copy-button-wrapper {
     223        margin: 0.5rem 0 1rem;
     224}
     225
     226body.site-health .copy-field-success {
     227        display: none;
     228        color: #40860a;
     229        line-height: 1.8;
     230        margin-left: 0.5rem;
     231}
     232
     233body.site-health .copy-field-success.visible {
     234        display: inline-block;
     235}
     236
     237body.site-health .site-status-has-issues {
     238        display: block;
     239}
     240
     241body.site-health .site-status-has-issues.hide {
     242        display: none;
     243}
     244
     245body.site-health h3 {
     246        padding: 0 0.5rem;
     247        font-weight: 600;
     248}
     249
     250body.site-health .view-more {
     251        text-align: center;
     252}
     253
     254body.site-health .issues-wrapper {
     255        margin-bottom: 5rem;
     256}
     257
     258body.site-health .site-status-all-clear {
     259        display: flex;
     260        flex-direction: column;
     261        align-items: center;
     262        justify-content: center;
     263        text-align: center;
     264        height: 100%;
     265        width: 100%;
     266        margin-top: 0;
     267}
     268
     269@media all and (min-width: 784px) {
     270        body.site-health .site-status-all-clear {
     271                margin: 5rem 0;
     272        }
     273}
     274
     275body.site-health .site-status-all-clear.hide {
     276        display: none;
     277}
     278
     279body.site-health .site-status-all-clear .dashicons {
     280        font-size: 150px;
     281        height: 130px;
     282        width: 150px;
     283}
     284
     285body.site-health .site-status-all-clear .encouragement {
     286        font-size: 1.5rem;
     287        font-weight: 600;
     288}
     289
     290body.site-health .site-status-all-clear p {
     291        margin: 0;
     292}
     293
     294body .health-check-accordion {
     295        border: 1px solid #d1d1d1;
     296}
     297
     298body .health-check-accordion dt {
     299        font-weight: 600;
     300        border-top: 1px solid #d1d1d1;
     301}
     302
     303body .health-check-accordion dt:first-child {
     304        border-radius: 0.3em 0.3em 0 0;
     305        border-top: none;
     306}
     307
     308body .health-check-accordion .health-check-accordion-trigger {
     309        background: #fff;
     310        border: 0;
     311        color: #212121;
     312        cursor: pointer;
     313        display: block;
     314        font-weight: 400;
     315        margin: 0;
     316        padding: 1em 1.5em;
     317        position: relative;
     318        text-align: left;
     319        width: 100%;
     320}
     321
     322body .health-check-accordion .health-check-accordion-trigger:hover, body .health-check-accordion .health-check-accordion-trigger:focus, body .health-check-accordion .health-check-accordion-trigger:active {
     323        background: #dedede;
     324}
     325
     326body .health-check-accordion .health-check-accordion-trigger .title {
     327        display: inline-block;
     328        pointer-events: none;
     329        font-weight: 600;
     330}
     331
     332body .health-check-accordion .health-check-accordion-trigger .icon {
     333        border: solid #191e23;
     334        border-width: 0 2px 2px 0;
     335        height: 0.5rem;
     336        pointer-events: none;
     337        position: absolute;
     338        right: 1.5em;
     339        top: 50%;
     340        transform: translateY(-60%) rotate(45deg);
     341        width: 0.5rem;
     342}
     343
     344body .health-check-accordion .health-check-accordion-trigger .badge {
     345        display: inline-block;
     346        padding: 0.1rem 0.5rem;
     347        background-color: #dcdcdc;
     348        color: #000;
     349        font-weight: 600;
     350        margin: 0 0.5rem;
     351}
     352
     353body .health-check-accordion .health-check-accordion-trigger .badge.blue {
     354        background-color: #0073af;
     355        color: #fff;
     356}
     357
     358body .health-check-accordion .health-check-accordion-trigger .badge.orange {
     359        background-color: #ffb900;
     360        color: #000;
     361}
     362
     363body .health-check-accordion .health-check-accordion-trigger .badge.red {
     364        background-color: #dc3232;
     365        color: #fff;
     366}
     367
     368body .health-check-accordion .health-check-accordion-trigger .badge.green {
     369        background-color: #40860a;
     370        color: #fff;
     371}
     372
     373body .health-check-accordion .health-check-accordion-trigger .badge.pink {
     374        background-color: #f4b0fc;
     375        color: #000;
     376}
     377
     378body .health-check-accordion .health-check-accordion-trigger .badge.gray {
     379        background-color: #ccc;
     380        color: #000;
     381}
     382
     383body .health-check-accordion .health-check-accordion-trigger .badge.light-blue {
     384        background-color: #10e9fb;
     385        color: #000;
     386}
     387
     388body .health-check-accordion .health-check-accordion-trigger .badge.light-green {
     389        background-color: #60f999;
     390        color: #000;
     391}
     392
     393body .health-check-accordion .health-check-accordion-trigger[aria-expanded="true"] .icon {
     394        transform: translateY(-50%) rotate(-135deg);
     395}
     396
     397body .health-check-accordion .health-check-accordion-panel {
     398        margin: 0;
     399        padding: 1em 1.5em;
     400        background: #fff;
     401}
     402
     403body .health-check-accordion .health-check-accordion-panel > div {
     404        display: block;
     405}
     406
     407body .health-check-accordion .health-check-accordion-panel[hidden] {
     408        display: none;
     409}
     410
     411body .health-check-accordion dl dd {
     412        margin: 0 0 0.5em 2em;
     413}
  • src/wp-admin/css/wp-admin.css

    diff --git src/wp-admin/css/wp-admin.css src/wp-admin/css/wp-admin.css
    index 14c10f9cd9..b475cf0aaf 100644
     
    1212@import url(widgets.css);
    1313@import url(site-icon.css);
    1414@import url(l10n.css);
     15@import url(site-health.css);
  • src/wp-admin/includes/ajax-actions.php

    diff --git src/wp-admin/includes/ajax-actions.php src/wp-admin/includes/ajax-actions.php
    index ea5090b588..6fb2b91469 100644
    function wp_ajax_wp_privacy_erase_personal_data() { 
    48534853
    48544854        wp_send_json_success( $response );
    48554855}
     4856
     4857function wp_ajax_health_check_dotorg_communication() {
     4858        check_ajax_referer( 'health-check-site-status' );
     4859
     4860        if ( ! current_user_can( 'manage_options' ) ) {
     4861                wp_send_json_error();
     4862        }
     4863
     4864        if ( ! class_exists( 'WP_Site_Health' ) ) {
     4865                require_once( ABSPATH . 'wp-admin/includes/class-wp-site-health.php' );
     4866        }
     4867
     4868        $site_health = new WP_Site_Health();
     4869        wp_send_json_success( $site_health->get_test_dotorg_communication() );
     4870}
     4871
     4872function wp_ajax_health_check_is_in_debug_mode() {
     4873        wp_verify_nonce( 'health-check-site-status' );
     4874
     4875        if ( ! current_user_can( 'manage_options' ) ) {
     4876                wp_send_json_error();
     4877        }
     4878
     4879        if ( ! class_exists( 'WP_Site_Health' ) ) {
     4880                require_once( ABSPATH . 'wp-admin/includes/class-wp-site-health.php' );
     4881        }
     4882
     4883        $site_health = new WP_Site_Health();
     4884        wp_send_json_success( $site_health->get_test_is_in_debug_mode() );
     4885}
     4886
     4887function wp_ajax_health_check_background_updates() {
     4888        check_ajax_referer( 'health-check-site-status' );
     4889
     4890        if ( ! current_user_can( 'manage_options' ) ) {
     4891                wp_send_json_error();
     4892        }
     4893
     4894        if ( ! class_exists( 'WP_Site_Health' ) ) {
     4895                require_once( ABSPATH . 'wp-admin/includes/class-wp-site-health.php' );
     4896        }
     4897
     4898        $site_health = new WP_Site_Health();
     4899        wp_send_json_success( $site_health->get_test_background_updates() );
     4900}
     4901
     4902
     4903function wp_ajax_health_check_loopback_requests() {
     4904        check_ajax_referer( 'health-check-site-status' );
     4905
     4906        if ( ! current_user_can( 'manage_options' ) ) {
     4907                wp_send_json_error();
     4908        }
     4909
     4910        if ( ! class_exists( 'WP_Site_Health' ) ) {
     4911                require_once( ABSPATH . 'wp-admin/includes/class-wp-site-health.php' );
     4912        }
     4913
     4914        $site_health = new WP_Site_Health();
     4915        wp_send_json_success( $site_health->get_test_loopback_requests() );
     4916}
     4917
     4918function wp_ajax_health_check_site_status_result() {
     4919        check_ajax_referer( 'health-check-site-status-result' );
     4920
     4921        if ( ! current_user_can( 'manage_options' ) ) {
     4922                wp_send_json_error();
     4923        }
     4924
     4925        set_transient( 'health-check-site-status-result', wp_json_encode( $_POST['counts'] ) );
     4926
     4927        wp_send_json_success();
     4928}
  • new file src/wp-admin/includes/class-wp-debug-data.php

    diff --git src/wp-admin/includes/class-wp-debug-data.php src/wp-admin/includes/class-wp-debug-data.php
    new file mode 100644
    index 0000000000..5c10ef66fb
    - +  
     1<?php
     2/**
     3 * Class for providing debug data based on a users WordPress environment.
     4 *
     5 * @package WordPress
     6 * @subpackage Site_Health
     7 * @since 5.2.0
     8 */
     9
     10/**
     11 * Class WP_Debug_Data
     12 */
     13class WP_Debug_Data {
     14
     15        /**
     16         * Calls all core functions to check for updates.
     17         *
     18         * @return void
     19         */
     20        static function check_for_updates() {
     21
     22                wp_version_check();
     23                wp_update_plugins();
     24                wp_update_themes();
     25
     26        }
     27
     28        /**
     29         * Static function for generating debug data when required.
     30         *
     31         * @param string $locale Default null. An ISO formatted language code to provide debug translations in.
     32         *
     33         * @return array
     34         * @throws ImagickException
     35         */
     36        static function debug_data( $locale = null ) {
     37                if ( ! empty( $locale ) ) {
     38                        // Change the language used for translations
     39                        if ( function_exists( 'switch_to_locale' ) ) {
     40                                $original_locale = get_locale();
     41                                $switched_locale = switch_to_locale( $locale );
     42                        }
     43                }
     44                global $wpdb;
     45
     46                $upload_dir = wp_upload_dir();
     47                if ( file_exists( ABSPATH . 'wp-config.php' ) ) {
     48                        $wp_config_path = ABSPATH . 'wp-config.php';
     49                        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
     50                } elseif ( @file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! @file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
     51                        $wp_config_path = dirname( ABSPATH ) . '/wp-config.php';
     52                }
     53
     54                $core_current_version = get_bloginfo( 'version' );
     55                $core_updates         = get_core_updates();
     56                $core_update_needed   = '';
     57
     58                foreach ( $core_updates as $core => $update ) {
     59                        if ( 'upgrade' === $update->response ) {
     60                                // translators: %s: Latest WordPress version number.
     61                                $core_update_needed = ' ' . sprintf( __( '( Latest version: %s )' ), $update->version );
     62                        } else {
     63                                $core_update_needed = '';
     64                        }
     65                }
     66
     67                // Set up the array that holds all debug information.
     68                $info = array(
     69                        'wp-core'             => array(
     70                                'label'  => __( 'WordPress' ),
     71                                'fields' => array(
     72                                        'version'                => array(
     73                                                'label' => __( 'Version' ),
     74                                                'value' => $core_current_version . $core_update_needed,
     75                                        ),
     76                                        'language'               => array(
     77                                                'label' => __( 'Language' ),
     78                                                'value' => ( ! empty( $locale ) ? $original_locale : get_locale() ),
     79                                        ),
     80                                        'home_url'               => array(
     81                                                'label'   => __( 'Home URL' ),
     82                                                'value'   => get_bloginfo( 'url' ),
     83                                                'private' => true,
     84                                        ),
     85                                        'site_url'               => array(
     86                                                'label'   => __( 'Site URL' ),
     87                                                'value'   => get_bloginfo( 'wpurl' ),
     88                                                'private' => true,
     89                                        ),
     90                                        'permalink'              => array(
     91                                                'label' => __( 'Permalink structure' ),
     92                                                'value' => get_option( 'permalink_structure' ),
     93                                        ),
     94                                        'https_status'           => array(
     95                                                'label' => __( 'Is this site using HTTPS?' ),
     96                                                'value' => ( is_ssl() ? __( 'Yes' ) : __( 'No' ) ),
     97                                        ),
     98                                        'user_registration'      => array(
     99                                                'label' => __( 'Can anyone register on this site?' ),
     100                                                'value' => ( get_option( 'users_can_register' ) ? __( 'Yes' ) : __( 'No' ) ),
     101                                        ),
     102                                        'default_comment_status' => array(
     103                                                'label' => __( 'Default comment status' ),
     104                                                'value' => get_option( 'default_comment_status' ),
     105                                        ),
     106                                        'multisite'              => array(
     107                                                'label' => __( 'Is this a multisite?' ),
     108                                                'value' => ( is_multisite() ? __( 'Yes' ) : __( 'No' ) ),
     109                                        ),
     110                                ),
     111                        ),
     112                        'wp-dropins'          => array(
     113                                'label'       => __( 'Drop-ins' ),
     114                                'description' => __( 'Drop-ins are single files that replace or enhance WordPress features in ways that are not possible for traditional plugins' ),
     115                                'fields'      => array(),
     116                        ),
     117                        'wp-active-theme'     => array(
     118                                'label'  => __( 'Active Theme' ),
     119                                'fields' => array(),
     120                        ),
     121                        'wp-themes'           => array(
     122                                'label'      => __( 'Other themes' ),
     123                                'show_count' => true,
     124                                'fields'     => array(),
     125                        ),
     126                        'wp-mu-plugins'       => array(
     127                                'label'      => __( 'Must Use Plugins' ),
     128                                'show_count' => true,
     129                                'fields'     => array(),
     130                        ),
     131                        'wp-plugins-active'   => array(
     132                                'label'      => __( 'Active Plugins' ),
     133                                'show_count' => true,
     134                                'fields'     => array(),
     135                        ),
     136                        'wp-plugins-inactive' => array(
     137                                'label'      => __( 'Inactive Plugins' ),
     138                                'show_count' => true,
     139                                'fields'     => array(),
     140                        ),
     141                        'wp-media'            => array(
     142                                'label'  => __( 'Media handling' ),
     143                                'fields' => array(),
     144                        ),
     145                        'wp-server'           => array(
     146                                'label'       => __( 'Server' ),
     147                                'description' => __( 'The options shown below relate to your server setup. If changes are required, you may need your web host\'s assistance.' ),
     148                                'fields'      => array(),
     149                        ),
     150                        'wp-database'         => array(
     151                                'label'  => __( 'Database' ),
     152                                'fields' => array(),
     153                        ),
     154                        'wp-constants'        => array(
     155                                'label'       => __( 'WordPress Constants' ),
     156                                'description' => __( 'These values represent values set in your websites code which affect WordPress in various ways that may be of importance when seeking help with your site.' ),
     157                                'fields'      => array(
     158                                        'ABSPATH'             => array(
     159                                                'label'   => 'ABSPATH',
     160                                                'value'   => ( ! defined( 'ABSPATH' ) ? __( 'Undefined' ) : ABSPATH ),
     161                                                'private' => true,
     162                                        ),
     163                                        'WP_HOME'             => array(
     164                                                'label' => 'WP_HOME',
     165                                                'value' => ( ! defined( 'WP_HOME' ) ? __( 'Undefined' ) : WP_HOME ),
     166                                        ),
     167                                        'WP_SITEURL'          => array(
     168                                                'label' => 'WP_SITEURL',
     169                                                'value' => ( ! defined( 'WP_SITEURL' ) ? __( 'Undefined' ) : WP_SITEURL ),
     170                                        ),
     171                                        'WP_DEBUG'            => array(
     172                                                'label' => 'WP_DEBUG',
     173                                                'value' => ( ! defined( 'WP_DEBUG' ) ? __( 'Undefined' ) : ( WP_DEBUG ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     174                                        ),
     175                                        'WP_MAX_MEMORY_LIMIT' => array(
     176                                                'label' => 'WP_MAX_MEMORY_LIMIT',
     177                                                'value' => ( ! defined( 'WP_MAX_MEMORY_LIMIT' ) ? __( 'Undefined' ) : WP_MAX_MEMORY_LIMIT ),
     178                                        ),
     179                                        'WP_DEBUG_DISPLAY'    => array(
     180                                                'label' => 'WP_DEBUG_DISPLAY',
     181                                                'value' => ( ! defined( 'WP_DEBUG_DISPLAY' ) ? __( 'Undefined' ) : ( WP_DEBUG_DISPLAY ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     182                                        ),
     183                                        'WP_DEBUG_LOG'        => array(
     184                                                'label' => 'WP_DEBUG_LOG',
     185                                                'value' => ( ! defined( 'WP_DEBUG_LOG' ) ? __( 'Undefined' ) : ( WP_DEBUG_LOG ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     186                                        ),
     187                                        'SCRIPT_DEBUG'        => array(
     188                                                'label' => 'SCRIPT_DEBUG',
     189                                                'value' => ( ! defined( 'SCRIPT_DEBUG' ) ? __( 'Undefined' ) : ( SCRIPT_DEBUG ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     190                                        ),
     191                                        'WP_CACHE'            => array(
     192                                                'label' => 'WP_CACHE',
     193                                                'value' => ( ! defined( 'WP_CACHE' ) ? __( 'Undefined' ) : ( WP_CACHE ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     194                                        ),
     195                                        'CONCATENATE_SCRIPTS' => array(
     196                                                'label' => 'CONCATENATE_SCRIPTS',
     197                                                'value' => ( ! defined( 'CONCATENATE_SCRIPTS' ) ? __( 'Undefined' ) : ( CONCATENATE_SCRIPTS ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     198                                        ),
     199                                        'COMPRESS_SCRIPTS'    => array(
     200                                                'label' => 'COMPRESS_SCRIPTS',
     201                                                'value' => ( ! defined( 'COMPRESS_SCRIPTS' ) ? __( 'Undefined' ) : ( COMPRESS_SCRIPTS ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     202                                        ),
     203                                        'COMPRESS_CSS'        => array(
     204                                                'label' => 'COMPRESS_CSS',
     205                                                'value' => ( ! defined( 'COMPRESS_CSS' ) ? __( 'Undefined' ) : ( COMPRESS_CSS ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     206                                        ),
     207                                        'WP_LOCAL_DEV'        => array(
     208                                                'label' => 'WP_LOCAL_DEV',
     209                                                'value' => ( ! defined( 'WP_LOCAL_DEV' ) ? __( 'Undefined' ) : ( WP_LOCAL_DEV ? __( 'Enabled' ) : __( 'Disabled' ) ) ),
     210                                        ),
     211                                ),
     212                        ),
     213                        'wp-filesystem'       => array(
     214                                'label'       => __( 'Filesystem Permissions' ),
     215                                'description' => __( 'The status of various locations WordPress needs to write files in various scenarios.' ),
     216                                'fields'      => array(
     217                                        'all'        => array(
     218                                                'label' => __( 'The main WordPress directory' ),
     219                                                'value' => ( wp_is_writable( ABSPATH ) ? __( 'Writable' ) : __( 'Not writable' ) ),
     220                                        ),
     221                                        'wp-content' => array(
     222                                                'label' => __( 'The wp-content directory' ),
     223                                                'value' => ( wp_is_writable( WP_CONTENT_DIR ) ? __( 'Writable' ) : __( 'Not writable' ) ),
     224                                        ),
     225                                        'uploads'    => array(
     226                                                'label' => __( 'The uploads directory' ),
     227                                                'value' => ( wp_is_writable( $upload_dir['basedir'] ) ? __( 'Writable' ) : __( 'Not writable' ) ),
     228                                        ),
     229                                        'plugins'    => array(
     230                                                'label' => __( 'The plugins directory' ),
     231                                                'value' => ( wp_is_writable( WP_PLUGIN_DIR ) ? __( 'Writable' ) : __( 'Not writable' ) ),
     232                                        ),
     233                                        'themes'     => array(
     234                                                'label' => __( 'The themes directory' ),
     235                                                'value' => ( wp_is_writable( get_template_directory() . '/..' ) ? __( 'Writable' ) : __( 'Not writable' ) ),
     236                                        ),
     237                                ),
     238                        ),
     239                );
     240
     241                // Conditionally add debug information for multisite setups.
     242                if ( is_multisite() ) {
     243                        $network_query = new WP_Network_Query();
     244                        $network_ids   = $network_query->query(
     245                                array(
     246                                        'fields'        => 'ids',
     247                                        'number'        => 100,
     248                                        'no_found_rows' => false,
     249                                )
     250                        );
     251
     252                        $site_count = 0;
     253                        foreach ( $network_ids as $network_id ) {
     254                                $site_count += get_blog_count( $network_id );
     255                        }
     256
     257                        $info['wp-core']['fields']['user_count']    = array(
     258                                'label' => __( 'User Count' ),
     259                                'value' => get_user_count(),
     260                        );
     261                        $info['wp-core']['fields']['site_count']    = array(
     262                                'label' => __( 'Site Count' ),
     263                                'value' => $site_count,
     264                        );
     265                        $info['wp-core']['fields']['network_count'] = array(
     266                                'label' => __( 'Network Count' ),
     267                                'value' => $network_query->found_networks,
     268                        );
     269                } else {
     270                        $user_count = count_users();
     271
     272                        $info['wp-core']['fields']['user_count'] = array(
     273                                'label' => __( 'User Count' ),
     274                                'value' => $user_count['total_users'],
     275                        );
     276                }
     277
     278                // WordPress features requiring processing.
     279                $wp_dotorg = wp_remote_get(
     280                        'https://wordpress.org',
     281                        array(
     282                                'timeout' => 10,
     283                        )
     284                );
     285                if ( ! is_wp_error( $wp_dotorg ) ) {
     286                        $info['wp-core']['fields']['dotorg_communication'] = array(
     287                                'label' => __( 'Communication with WordPress.org' ),
     288                                'value' => sprintf(
     289                                        __( 'WordPress.org is reachable' )
     290                                ),
     291                        );
     292                } else {
     293                        $info['wp-core']['fields']['dotorg_communication'] = array(
     294                                'label' => __( 'Communication with WordPress.org' ),
     295                                'value' => sprintf(
     296                                        // translators: %1$s: The IP address WordPress.org resolves to. %2$s: The error returned by the lookup.
     297                                        __( 'Unable to reach WordPress.org at %1$s: %2$s' ),
     298                                        gethostbyname( 'wordpress.org' ),
     299                                        $wp_dotorg->get_error_message()
     300                                ),
     301                        );
     302                }
     303
     304                // Get a list of all drop-in replacements.
     305                $dropins            = get_dropins();
     306                $dropin_description = _get_dropins();
     307                foreach ( $dropins as $dropin_key => $dropin ) {
     308                        $info['wp-dropins']['fields'][ sanitize_key( $dropin_key ) ] = array(
     309                                'label' => $dropin_key,
     310                                'value' => $dropin_description[ $dropin_key ][0],
     311                        );
     312                }
     313
     314                // Populate the media fields.
     315                $info['wp-media']['fields']['image_editor'] = array(
     316                        'label' => __( 'Active editor' ),
     317                        'value' => _wp_image_editor_choose(),
     318                );
     319
     320                // Get ImageMagic information, if available.
     321                if ( class_exists( 'Imagick' ) ) {
     322                        // Save the Imagick instance for later use.
     323                        $imagick         = new Imagick();
     324                        $imagick_version = $imagick->getVersion();
     325                } else {
     326                        $imagick_version = __( 'Imagick not available' );
     327                }
     328                $info['wp-media']['fields']['imagick_module_version'] = array(
     329                        'label' => __( 'Imagick Module Version' ),
     330                        'value' => ( is_array( $imagick_version ) ? $imagick_version['versionNumber'] : $imagick_version ),
     331                );
     332                $info['wp-media']['fields']['imagemagick_version']    = array(
     333                        'label' => __( 'ImageMagick Version' ),
     334                        'value' => ( is_array( $imagick_version ) ? $imagick_version['versionString'] : $imagick_version ),
     335                );
     336
     337                // If Imagick is used as our editor, provide some more information about its limitations.
     338                if ( 'WP_Image_Editor_Imagick' === _wp_image_editor_choose() && isset( $imagick ) && $imagick instanceof Imagick ) {
     339                        $limits = array(
     340                                'area'   => ( defined( 'imagick::RESOURCETYPE_AREA' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_AREA ) ) : __( 'Not Available' ) ),
     341                                'disk'   => ( defined( 'imagick::RESOURCETYPE_DISK' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_DISK ) : __( 'Not Available' ) ),
     342                                'file'   => ( defined( 'imagick::RESOURCETYPE_FILE' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_FILE ) : __( 'Not Available' ) ),
     343                                'map'    => ( defined( 'imagick::RESOURCETYPE_MAP' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MAP ) ) : __( 'Not Available' ) ),
     344                                'memory' => ( defined( 'imagick::RESOURCETYPE_MEMORY' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MEMORY ) ) : __( 'Not Available' ) ),
     345                                'thread' => ( defined( 'imagick::RESOURCETYPE_THREAD' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_THREAD ) : __( 'Not Available' ) ),
     346                        );
     347
     348                        $info['wp-media']['fields']['imagick_limits'] = array(
     349                                'label' => __( 'Imagick Resource Limits' ),
     350                                'value' => $limits,
     351                        );
     352                }
     353
     354                // Get GD information, if available.
     355                if ( function_exists( 'gd_info' ) ) {
     356                        $gd = gd_info();
     357                } else {
     358                        $gd = false;
     359                }
     360                $info['wp-media']['fields']['gd_version'] = array(
     361                        'label' => __( 'GD Version' ),
     362                        'value' => ( is_array( $gd ) ? $gd['GD Version'] : __( 'GD not available' ) ),
     363                );
     364
     365                // Get Ghostscript information, if available.
     366                if ( function_exists( 'exec' ) ) {
     367                        $gs = exec( 'gs --version' );
     368                        $gs = ( ! empty( $gs ) ? $gs : __( 'Not available' ) );
     369                } else {
     370                        $gs = __( 'Unable to determine if Ghostscript is installed' );
     371                }
     372                $info['wp-media']['fields']['ghostscript_version'] = array(
     373                        'label' => __( 'Ghostscript Version' ),
     374                        'value' => $gs,
     375                );
     376
     377                // Populate the server debug fields.
     378                $info['wp-server']['fields']['server_architecture'] = array(
     379                        'label' => __( 'Server architecture' ),
     380                        'value' => ( ! function_exists( 'php_uname' ) ? __( 'Unable to determine server architecture' ) : sprintf( '%s %s %s', php_uname( 's' ), php_uname( 'r' ), php_uname( 'm' ) ) ),
     381                );
     382                $info['wp-server']['fields']['httpd_software']      = array(
     383                        'label' => __( 'Web Server Software' ),
     384                        'value' => ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : __( 'Unable to determine what web server software is used' ) ),
     385                );
     386                $info['wp-server']['fields']['php_version']         = array(
     387                        'label' => __( 'PHP Version' ),
     388                        'value' => ( ! function_exists( 'phpversion' ) ? __( 'Unable to determine PHP version' ) : sprintf(
     389                                '%s %s',
     390                                phpversion(),
     391                                ( 64 === PHP_INT_SIZE * 8 ? __( '(Supports 64bit values)' ) : __( '(Does not support 64bit values)' ) )
     392                        )
     393                        ),
     394                );
     395                $info['wp-server']['fields']['php_sapi']            = array(
     396                        'label' => __( 'PHP SAPI' ),
     397                        'value' => ( ! function_exists( 'php_sapi_name' ) ? __( 'Unable to determine PHP SAPI' ) : php_sapi_name() ),
     398                );
     399
     400                // Some servers disable `ini_set()` and `ini_get()`, we check this before trying to get configuration values.
     401                if ( ! function_exists( 'ini_get' ) ) {
     402                        $info['wp-server']['fields']['ini_get'] = array(
     403                                'label' => __( 'Server settings' ),
     404                                'value' => __( 'Unable to determine some settings as the ini_get() function has been disabled' ),
     405                        );
     406                } else {
     407                        $info['wp-server']['fields']['max_input_variables'] = array(
     408                                'label' => __( 'PHP max input variables' ),
     409                                'value' => ini_get( 'max_input_vars' ),
     410                        );
     411                        $info['wp-server']['fields']['time_limit']          = array(
     412                                'label' => __( 'PHP time limit' ),
     413                                'value' => ini_get( 'max_execution_time' ),
     414                        );
     415                        $info['wp-server']['fields']['memory_limit']        = array(
     416                                'label' => __( 'PHP memory limit' ),
     417                                'value' => ini_get( 'memory_limit' ),
     418                        );
     419                        $info['wp-server']['fields']['max_input_time']      = array(
     420                                'label' => __( 'Max input time' ),
     421                                'value' => ini_get( 'max_input_time' ),
     422                        );
     423                        $info['wp-server']['fields']['upload_max_size']     = array(
     424                                'label' => __( 'Upload max filesize' ),
     425                                'value' => ini_get( 'upload_max_filesize' ),
     426                        );
     427                        $info['wp-server']['fields']['php_post_max_size']   = array(
     428                                'label' => __( 'PHP post max size' ),
     429                                'value' => ini_get( 'post_max_size' ),
     430                        );
     431                }
     432
     433                if ( function_exists( 'curl_version' ) ) {
     434                        $curl                                        = curl_version();
     435                        $info['wp-server']['fields']['curl_version'] = array(
     436                                'label' => __( 'cURL Version' ),
     437                                'value' => sprintf( '%s %s', $curl['version'], $curl['ssl_version'] ),
     438                        );
     439                } else {
     440                        $info['wp-server']['fields']['curl_version'] = array(
     441                                'label' => __( 'cURL Version' ),
     442                                'value' => __( 'Your server does not support cURL' ),
     443                        );
     444                }
     445
     446                $info['wp-server']['fields']['suhosin'] = array(
     447                        'label' => __( 'SUHOSIN installed' ),
     448                        'value' => ( ( extension_loaded( 'suhosin' ) || ( defined( 'SUHOSIN_PATCH' ) && constant( 'SUHOSIN_PATCH' ) ) ) ? __( 'Yes' ) : __( 'No' ) ),
     449                );
     450
     451                $info['wp-server']['fields']['imagick_availability'] = array(
     452                        'label' => __( 'Is the Imagick library available' ),
     453                        'value' => ( extension_loaded( 'imagick' ) ? __( 'Yes' ) : __( 'No' ) ),
     454                );
     455
     456                // Check if a .htaccess file exists.
     457                if ( is_file( ABSPATH . '/.htaccess' ) ) {
     458                        // If the file exists, grab the content of it.
     459                        $htaccess_content = file_get_contents( ABSPATH . '/.htaccess' );
     460
     461                        // Filter away the core WordPress rules.
     462                        $filtered_htaccess_content = trim( preg_replace( '/\# BEGIN WordPress[\s\S]+?# END WordPress/si', '', $htaccess_content ) );
     463
     464                        $info['wp-server']['fields']['htaccess_extra_rules'] = array(
     465                                'label' => __( 'htaccess rules' ),
     466                                'value' => ( ! empty( $filtered_htaccess_content ) ? __( 'Custom rules have been added to your htaccess file' ) : __( 'Your htaccess file only contains core WordPress features' ) ),
     467                        );
     468                }
     469
     470                // Populate the database debug fields.
     471                if ( is_resource( $wpdb->dbh ) ) {
     472                        // Old mysql extension.
     473                        $extension = 'mysql';
     474                } elseif ( is_object( $wpdb->dbh ) ) {
     475                        // mysqli or PDO.
     476                        $extension = get_class( $wpdb->dbh );
     477                } else {
     478                        // Unknown sql extension.
     479                        $extension = null;
     480                }
     481
     482                /*
     483                 * Check what database engine is used, this will throw compatibility
     484                 * warnings from PHP compatibility testers, but `mysql_*` is
     485                 * still valid in PHP 5.6, so we need to account for that.
     486                 */
     487                if ( method_exists( $wpdb, 'db_version' ) ) {
     488                        if ( $wpdb->use_mysqli ) {
     489                                // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_server_info
     490                                $server = mysqli_get_server_info( $wpdb->dbh );
     491                        } else {
     492                                // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_get_server_info
     493                                $server = mysql_get_server_info( $wpdb->dbh );
     494                        }
     495                } else {
     496                        $server = null;
     497                }
     498
     499                if ( isset( $wpdb->use_mysqli ) && $wpdb->use_mysqli ) {
     500                        $client_version = $wpdb->dbh->client_info;
     501                } else {
     502                        // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_get_client_info
     503                        if ( preg_match( '|[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}|', mysql_get_client_info(), $matches ) ) {
     504                                $client_version = $matches[0];
     505                        } else {
     506                                $client_version = null;
     507                        }
     508                }
     509
     510                $info['wp-database']['fields']['extension']       = array(
     511                        'label' => __( 'Extension' ),
     512                        'value' => $extension,
     513                );
     514                $info['wp-database']['fields']['server_version']  = array(
     515                        'label' => __( 'Server version' ),
     516                        'value' => $server,
     517                );
     518                $info['wp-database']['fields']['client_version']  = array(
     519                        'label' => __( 'Client version' ),
     520                        'value' => $client_version,
     521                );
     522                $info['wp-database']['fields']['database_user']   = array(
     523                        'label'   => __( 'Database user' ),
     524                        'value'   => $wpdb->dbuser,
     525                        'private' => true,
     526                );
     527                $info['wp-database']['fields']['database_host']   = array(
     528                        'label'   => __( 'Database host' ),
     529                        'value'   => $wpdb->dbhost,
     530                        'private' => true,
     531                );
     532                $info['wp-database']['fields']['database_name']   = array(
     533                        'label'   => __( 'Database name' ),
     534                        'value'   => $wpdb->dbname,
     535                        'private' => true,
     536                );
     537                $info['wp-database']['fields']['database_prefix'] = array(
     538                        'label'   => __( 'Database prefix' ),
     539                        'value'   => $wpdb->prefix,
     540                        'private' => true,
     541                );
     542
     543                // List must use plugins if there are any.
     544                $mu_plugins = get_mu_plugins();
     545
     546                foreach ( $mu_plugins as $plugin_path => $plugin ) {
     547                        $plugin_version = $plugin['Version'];
     548                        $plugin_author  = $plugin['Author'];
     549
     550                        $plugin_version_string = __( 'No version or author information available' );
     551
     552                        if ( ! empty( $plugin_version ) && ! empty( $plugin_author ) ) {
     553                                // translators: %1$s: Plugin version number. %2$s: Plugin author name.
     554                                $plugin_version_string = sprintf( __( 'Version %1$s by %2$s' ), $plugin_version, $plugin_author );
     555                        }
     556                        if ( empty( $plugin_version ) && ! empty( $plugin_author ) ) {
     557                                // translators: %s: Plugin author name.
     558                                $plugin_version_string = sprintf( __( 'By %s' ), $plugin_author );
     559                        }
     560                        if ( ! empty( $plugin_version ) && empty( $plugin_author ) ) {
     561                                // translators: %s: Plugin version number.
     562                                $plugin_version_string = sprintf( __( 'Version %s' ), $plugin_version );
     563                        }
     564
     565                        $info['wp-mu-plugins']['fields'][ sanitize_key( $plugin['Name'] ) ] = array(
     566                                'label' => $plugin['Name'],
     567                                'value' => $plugin_version_string,
     568                        );
     569                }
     570
     571                // List all available plugins.
     572                $plugins        = get_plugins();
     573                $plugin_updates = get_plugin_updates();
     574
     575                foreach ( $plugins as $plugin_path => $plugin ) {
     576                        $plugin_part = ( is_plugin_active( $plugin_path ) ) ? 'wp-plugins-active' : 'wp-plugins-inactive';
     577
     578                        $plugin_version = $plugin['Version'];
     579                        $plugin_author  = $plugin['Author'];
     580
     581                        $plugin_version_string = __( 'No version or author information available' );
     582
     583                        if ( ! empty( $plugin_version ) && ! empty( $plugin_author ) ) {
     584                                // translators: %1$s: Plugin version number. %2$s: Plugin author name.
     585                                $plugin_version_string = sprintf( __( 'Version %1$s by %2$s' ), $plugin_version, $plugin_author );
     586                        }
     587                        if ( empty( $plugin_version ) && ! empty( $plugin_author ) ) {
     588                                // translators: %s: Plugin author name.
     589                                $plugin_version_string = sprintf( __( 'By %s' ), $plugin_author );
     590                        }
     591                        if ( ! empty( $plugin_version ) && empty( $plugin_author ) ) {
     592                                // translators: %s: Plugin version number.
     593                                $plugin_version_string = sprintf( __( 'Version %s' ), $plugin_version );
     594                        }
     595
     596                        if ( array_key_exists( $plugin_path, $plugin_updates ) ) {
     597                                // translators: %s: Latest plugin version number.
     598                                $plugin_update_needed = ' ' . sprintf( __( '( Latest version: %s )' ), $plugin_updates[ $plugin_path ]->update->new_version );
     599                        } else {
     600                                $plugin_update_needed = '';
     601                        }
     602
     603                        $info[ $plugin_part ]['fields'][ sanitize_key( $plugin['Name'] ) ] = array(
     604                                'label' => $plugin['Name'],
     605                                'value' => $plugin_version_string . $plugin_update_needed,
     606                        );
     607                }
     608
     609                // Populate the section for the currently active theme.
     610                global $_wp_theme_features;
     611                $theme_features = array();
     612                if ( ! empty( $_wp_theme_features ) ) {
     613                        foreach ( $_wp_theme_features as $feature => $options ) {
     614                                $theme_features[] = $feature;
     615                        }
     616                }
     617
     618                $active_theme  = wp_get_theme();
     619                $theme_updates = get_theme_updates();
     620
     621                if ( array_key_exists( $active_theme->stylesheet, $theme_updates ) ) {
     622                        // translators: %s: Latest theme version number.
     623                        $theme_update_needed_active = ' ' . sprintf( __( '( Latest version: %s )' ), $theme_updates[ $active_theme->stylesheet ]->update['new_version'] );
     624                } else {
     625                        $theme_update_needed_active = '';
     626                }
     627
     628                $info['wp-active-theme']['fields'] = array(
     629                        'name'           => array(
     630                                'label' => __( 'Name' ),
     631                                // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
     632                                'value' => $active_theme->Name,
     633                        ),
     634                        'version'        => array(
     635                                'label' => __( 'Version' ),
     636                                // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
     637                                'value' => $active_theme->Version . $theme_update_needed_active,
     638                        ),
     639                        'author'         => array(
     640                                'label' => __( 'Author' ),
     641                                // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
     642                                'value' => wp_kses( $active_theme->Author, array() ),
     643                        ),
     644                        'author_website' => array(
     645                                'label' => __( 'Author website' ),
     646                                'value' => ( $active_theme->offsetGet( 'Author URI' ) ? $active_theme->offsetGet( 'Author URI' ) : __( 'Undefined' ) ),
     647                        ),
     648                        'parent_theme'   => array(
     649                                'label' => __( 'Parent theme' ),
     650                                'value' => ( $active_theme->parent_theme ? $active_theme->parent_theme : __( 'Not a child theme' ) ),
     651                        ),
     652                        'theme_features' => array(
     653                                'label' => __( 'Supported theme features' ),
     654                                'value' => implode( ', ', $theme_features ),
     655                        ),
     656                );
     657
     658                // Populate a list of all themes available in the install.
     659                $all_themes = wp_get_themes();
     660
     661                foreach ( $all_themes as $theme_slug => $theme ) {
     662                        // Ignore the currently active theme from the list of all themes.
     663                        if ( $active_theme->stylesheet == $theme_slug ) {
     664                                continue;
     665                        }
     666                        // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
     667                        $theme_version = $theme->Version;
     668                        // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
     669                        $theme_author = $theme->Author;
     670
     671                        $theme_version_string = __( 'No version or author information available' );
     672
     673                        if ( ! empty( $theme_version ) && ! empty( $theme_author ) ) {
     674                                // translators: %1$s: Theme version number. %2$s: Theme author name.
     675                                $theme_version_string = sprintf( __( 'Version %1$s by %2$s' ), $theme_version, wp_kses( $theme_author, array() ) );
     676                        }
     677                        if ( empty( $theme_version ) && ! empty( $theme_author ) ) {
     678                                // translators: %s: Theme author name.
     679                                $theme_version_string = sprintf( __( 'By %s' ), wp_kses( $theme_author, array() ) );
     680                        }
     681                        if ( ! empty( $theme_version ) && empty( $theme_author ) ) {
     682                                // translators: %s: Theme version number.
     683                                $theme_version_string = sprintf( __( 'Version %s' ), $theme_version );
     684                        }
     685
     686                        if ( array_key_exists( $theme_slug, $theme_updates ) ) {
     687                                // translators: %s: Latest theme version number.
     688                                $theme_update_needed = ' ' . sprintf( __( '( Latest version: %s )' ), $theme_updates[ $theme_slug ]->update['new_version'] );
     689                        } else {
     690                                $theme_update_needed = '';
     691                        }
     692
     693                        $info['wp-themes']['fields'][ sanitize_key( $theme->Name ) ] = array(
     694                                'label' => sprintf(
     695                                        // translators: %1$s: Theme name. %2$s: Theme slug.
     696                                        __( '%1$s (%2$s)' ),
     697                                        // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
     698                                        $theme->Name,
     699                                        $theme_slug
     700                                ),
     701                                'value' => $theme_version_string . $theme_update_needed,
     702                        );
     703                }
     704
     705                // Add more filesystem checks
     706                if ( defined( 'WPMU_PLUGIN_DIR' ) && is_dir( WPMU_PLUGIN_DIR ) ) {
     707                        $info['wp-filesystem']['fields']['mu_plugin_directory'] = array(
     708                                'label' => __( 'The Must Use Plugins directory' ),
     709                                'value' => ( wp_is_writable( WPMU_PLUGIN_DIR ) ? __( 'Writable' ) : __( 'Not writable' ) ),
     710                        );
     711                }
     712
     713                /**
     714                 * Add or modify new debug sections.
     715                 *
     716                 * Plugin or themes may wish to introduce their own debug information without creating additional admin pages for this
     717                 * kind of information as it is rarely needed, they can then utilize this filter to introduce their own sections.
     718                 *
     719                 * Array keys added by core are all prefixed with `wp-`, plugins and themes are encouraged to use their own slug as
     720                 * a prefix, both for consistency as well as avoiding key collisions.
     721                 *
     722                 * @since 5.2.0
     723                 *
     724                 * @param array $args {
     725                 *     The debug information to be added to the core information page.
     726                 *
     727                 *     @type string  $label        The title for this section of the debug output.
     728                 *     @type string  $description  Optional. A description for your information section which may contain basic HTML
     729                 *                                 markup: `em`, `strong` and `a` for linking to documentation or putting emphasis.
     730                 *     @type boolean $show_count   Optional. If set to `true` the amount of fields will be included in the title for
     731                 *                                 this section.
     732                 *     @type boolean $private      Optional. If set to `true` the section and all associated fields will be excluded
     733                 *                                 from the copy-paste text area.
     734                 *     @type array   $fields {
     735                 *         An associative array containing the data to be displayed.
     736                 *
     737                 *         @type string  $label    The label for this piece of information.
     738                 *         @type string  $value    The output that is of interest for this field.
     739                 *         @type boolean $private  Optional. If set to `true` the field will not be included in the copy-paste text area
     740                 *                                 on top of the page, allowing you to show, for example, API keys here.
     741                 *     }
     742                 * }
     743                 */
     744                $info = apply_filters( 'debug_information', $info );
     745
     746                if ( ! empty( $locale ) ) {
     747                        // Change the language used for translations
     748                        if ( function_exists( 'restore_previous_locale' ) && $switched_locale ) {
     749                                restore_previous_locale();
     750                        }
     751                }
     752
     753                return $info;
     754        }
     755
     756        /**
     757         * Print the formatted variation of the information gathered for debugging, in a manner
     758         * suitable for a text area that can be instantly copied to a forum or support ticket.
     759         *
     760         * @param array $info_array Information gathered from the `WP_Debug_Data::debug_data` function.
     761         *
     762         * @return void
     763         */
     764        public static function textarea_format( $info_array ) {
     765                echo "`\n";
     766
     767                foreach ( $info_array as $section => $details ) {
     768                        // Skip this section if there are no fields, or the section has been declared as private.
     769                        if ( empty( $details['fields'] ) || ( isset( $details['private'] ) && $details['private'] ) ) {
     770                                continue;
     771                        }
     772
     773                        printf(
     774                                "### %s%s ###\n\n",
     775                                $details['label'],
     776                                ( isset( $details['show_count'] ) && $details['show_count'] ? sprintf( ' (%d)', count( $details['fields'] ) ) : '' )
     777                        );
     778
     779                        foreach ( $details['fields'] as $field ) {
     780                                if ( isset( $field['private'] ) && true === $field['private'] ) {
     781                                        continue;
     782                                }
     783
     784                                $values = $field['value'];
     785                                if ( is_array( $field['value'] ) ) {
     786                                        $values = '';
     787
     788                                        foreach ( $field['value'] as $name => $value ) {
     789                                                $values .= sprintf(
     790                                                        "\n\t%s: %s",
     791                                                        $name,
     792                                                        $value
     793                                                );
     794                                        }
     795                                }
     796
     797                                printf(
     798                                        "%s: %s\n",
     799                                        $field['label'],
     800                                        $values
     801                                );
     802                        }
     803                        echo "\n";
     804                }
     805                echo '`';
     806        }
     807
     808        /**
     809         * Return the size of a directory, including all subdirectories.
     810         *
     811         * @param string $path
     812         *
     813         * @return int
     814         */
     815        public static function get_directory_size( $path ) {
     816                $size = 0;
     817
     818                foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path ) ) as $file ) {
     819                        $size += $file->getSize();
     820                }
     821
     822                return $size;
     823        }
     824
     825        /**
     826         * Fetch the total size of all the database tables for the active database user.
     827         *
     828         * @return int
     829         */
     830        public static function get_database_size() {
     831                global $wpdb;
     832                $size = 0;
     833                $rows = $wpdb->get_results( 'SHOW TABLE STATUS', ARRAY_A );
     834
     835                if ( $wpdb->num_rows > 0 ) {
     836                        foreach ( $rows as $row ) {
     837                                $size += $row['Data_length'] + $row['Index_length'];
     838                        }
     839                }
     840
     841                return $size;
     842        }
     843}
  • new file src/wp-admin/includes/class-wp-site-health-auto-updates.php

    diff --git src/wp-admin/includes/class-wp-site-health-auto-updates.php src/wp-admin/includes/class-wp-site-health-auto-updates.php
    new file mode 100644
    index 0000000000..808167f2b2
    - +  
     1<?php
     2/**
     3 * Class for testing automatic updates in the WordPress code.
     4 *
     5 * @package WordPress
     6 * @subpackage Site_Health
     7 * @since 5.2.0
     8 */
     9
     10/**
     11 * Class WP_Site_Health_Auto_Updates
     12 */
     13class WP_Site_Health_Auto_Updates {
     14        /**
     15         * Health_Check_Auto_Updates constructor.
     16         * @return void
     17         */
     18        public function __construct() {
     19                $this->init();
     20        }
     21
     22        /**
     23         * Initiate the plugin class.
     24         *
     25         * @return void
     26         */
     27        public function init() {
     28                include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
     29        }
     30
     31        /**
     32         * Run tests to determine if auto-updates can run.
     33         *
     34         * @return array
     35         */
     36        public function run_tests() {
     37                $tests = array();
     38
     39                foreach ( get_class_methods( $this ) as $method ) {
     40                        if ( 'test_' !== substr( $method, 0, 5 ) ) {
     41                                continue;
     42                        }
     43
     44                        $result = call_user_func( array( $this, $method ) );
     45
     46                        if ( false === $result || null === $result ) {
     47                                continue;
     48                        }
     49
     50                        $result = (object) $result;
     51
     52                        if ( empty( $result->severity ) ) {
     53                                $result->severity = 'warning';
     54                        }
     55
     56                        $tests[ $method ] = $result;
     57                }
     58
     59                return $tests;
     60        }
     61
     62        /**
     63         * Test if file modifications are possible.
     64         *
     65         * @return array
     66         */
     67        function test_constant_FILE_MODS() {
     68                if ( defined( 'DISALLOW_FILE_MODS' ) && DISALLOW_FILE_MODS ) {
     69                        return array(
     70                                'description' => sprintf(
     71                                        /* translators: %s: Name of the constant used. */
     72                                        __( 'The %s constant is defined and enabled.' ),
     73                                        '<code>DISALLOW_FILE_MODS</code>'
     74                                ),
     75                                'severity'    => 'fail',
     76                        );
     77                }
     78        }
     79
     80        /**
     81         * Check if automatic updates are disabled with a constant.
     82         *
     83         * @return array
     84         */
     85        function test_constant_AUTOMATIC_UPDATER_DISABLED() {
     86                if ( defined( 'AUTOMATIC_UPDATER_DISABLED' ) && AUTOMATIC_UPDATER_DISABLED ) {
     87                        return array(
     88                                'description' => sprintf(
     89                                        /* translators: %s: Name of the constant used. */
     90                                        __( 'The %s constant is defined and enabled.' ),
     91                                        '<code>AUTOMATIC_UPDATER_DISABLED</code>'
     92                                ),
     93                                'severity'    => 'fail',
     94                        );
     95                }
     96        }
     97
     98        /**
     99         * Check if automatic core updates are disabled with a constant.
     100         *
     101         * @return array
     102         */
     103        function test_constant_WP_AUTO_UPDATE_CORE() {
     104                if ( defined( 'WP_AUTO_UPDATE_CORE' ) && false === WP_AUTO_UPDATE_CORE ) {
     105                        return array(
     106                                'description' => sprintf(
     107                                        /* translators: %s: Name of the constant used. */
     108                                        __( 'The %s constant is defined and enabled.' ),
     109                                        '<code>WP_AUTO_UPDATE_CORE</code>'
     110                                ),
     111                                'severity'    => 'fail',
     112                        );
     113                }
     114        }
     115
     116        /**
     117         * Check if updates are intercepted by a filter.
     118         *
     119         * @return array
     120         */
     121        function test_wp_version_check_attached() {
     122                $cookies = wp_unslash( $_COOKIE );
     123                $timeout = 10;
     124                $headers = array(
     125                        'Cache-Control' => 'no-cache',
     126                );
     127
     128                // Include Basic auth in loopback requests.
     129                if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
     130                        $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
     131                }
     132
     133                $url = add_query_arg(
     134                        array(
     135                                'health-check-test-wp_version_check' => true,
     136                        ),
     137                        admin_url()
     138                );
     139
     140                $test = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout' ) );
     141
     142                $response = wp_remote_retrieve_body( $test );
     143
     144                if ( 'yes' !== $response ) {
     145                        return array(
     146                                'description' => sprintf(
     147                                        /* translators: %s: Name of the filter used. */
     148                                        __( 'A plugin has prevented updates by disabling %s.' ),
     149                                        '<code>wp_version_check()</code>'
     150                                ),
     151                                'severity'    => 'fail',
     152                        );
     153                }
     154        }
     155
     156        /**
     157         * Check if automatic updates are disabled by a filter.
     158         *
     159         * @return array
     160         */
     161        function test_filters_automatic_updater_disabled() {
     162                if ( apply_filters( 'automatic_updater_disabled', false ) ) {
     163                        return array(
     164                                'description' => sprintf(
     165                                        /* translators: %s: Name of the filter used. */
     166                                        __( 'The %s filter is enabled.' ),
     167                                        '<code>automatic_updater_disabled</code>'
     168                                ),
     169                                'severity'    => 'fail',
     170                        );
     171                }
     172        }
     173
     174        /**
     175         * Check if automatic updates have tried to run, but failed, previously.
     176         *
     177         * @return array|bool
     178         */
     179        function test_if_failed_update() {
     180                $failed = get_site_option( 'auto_core_update_failed' );
     181
     182                if ( ! $failed ) {
     183                        return false;
     184                }
     185
     186                if ( ! empty( $failed['critical'] ) ) {
     187                        $description  = __( 'A previous automatic background update ended with a critical failure, so updates are now disabled.' );
     188                        $description .= ' ' . __( 'You would have received an email because of this.' );
     189                        $description .= ' ' . __( "When you've been able to update using the \"Update Now\" button on Dashboard > Updates, we'll clear this error for future update attempts." );
     190                        $description .= ' ' . sprintf(
     191                                /* translators: %s: Code of error shown. */
     192                                __( 'The error code was %s.' ),
     193                                '<code>' . $failed['error_code'] . '</code>'
     194                        );
     195                        return array(
     196                                'description' => $description,
     197                                'severity'    => 'warning',
     198                        );
     199                }
     200
     201                $description = __( 'A previous automatic background update could not occur.' );
     202                if ( empty( $failed['retry'] ) ) {
     203                        $description .= ' ' . __( 'You would have received an email because of this.' );
     204                }
     205
     206                $description .= ' ' . __( "We'll try again with the next release." );
     207                $description .= ' ' . sprintf(
     208                        /* translators: %s: Code of error shown. */
     209                        __( 'The error code was %s.' ),
     210                        '<code>' . $failed['error_code'] . '</code>'
     211                );
     212                return array(
     213                        'description' => $description,
     214                        'severity'    => 'warning',
     215                );
     216        }
     217
     218        /**
     219         * Check if WordPress is controlled by a VCS (Git, Subversion etc).
     220         *
     221         * @param string $context The path to check from.
     222         *
     223         * @return array
     224         */
     225        function _test_is_vcs_checkout( $context ) {
     226                $context_dirs = array( ABSPATH );
     227                $vcs_dirs     = array( '.svn', '.git', '.hg', '.bzr' );
     228                $check_dirs   = array();
     229
     230                foreach ( $context_dirs as $context_dir ) {
     231                        // Walk up from $context_dir to the root.
     232                        do {
     233                                $check_dirs[] = $context_dir;
     234
     235                                // Once we've hit '/' or 'C:\', we need to stop. dirname will keep returning the input here.
     236                                if ( dirname( $context_dir ) == $context_dir ) {
     237                                        break;
     238                                }
     239
     240                                // Continue one level at a time.
     241                        } while ( $context_dir = dirname( $context_dir ) );
     242                }
     243
     244                $check_dirs = array_unique( $check_dirs );
     245
     246                // Search all directories we've found for evidence of version control.
     247                foreach ( $vcs_dirs as $vcs_dir ) {
     248                        foreach ( $check_dirs as $check_dir ) {
     249                                // phpcs:ignore
     250                                if ( $checkout = @is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" ) ) {
     251                                        break 2;
     252                                }
     253                        }
     254                }
     255
     256                if ( $checkout && ! apply_filters( 'automatic_updates_is_vcs_checkout', true, $context ) ) {
     257                        return array(
     258                                'description' => sprintf(
     259                                        // translators: %1$s: Folder name. %2$s: Version control directory. %3$s: Filter name.
     260                                        __( 'The folder %1$s was detected as being under version control (%2$s), but the %3$s filter is allowing updates.' ),
     261                                        '<code>' . $check_dir . '</code>',
     262                                        "<code>$vcs_dir</code>",
     263                                        '<code>automatic_updates_is_vcs_checkout</code>'
     264                                ),
     265                                'severity'    => 'info',
     266                        );
     267                }
     268
     269                if ( $checkout ) {
     270                        return array(
     271                                'description' => sprintf(
     272                                        // translators: %1$s: Folder name. %2$s: Version control directory.
     273                                        __( 'The folder %1$s was detected as being under version control (%2$s).' ),
     274                                        '<code>' . $check_dir . '</code>',
     275                                        "<code>$vcs_dir</code>"
     276                                ),
     277                                'severity'    => 'fail',
     278                        );
     279                }
     280
     281                return array(
     282                        'description' => __( 'No version control systems were detected.' ),
     283                        'severity'    => 'pass',
     284                );
     285        }
     286
     287        /**
     288         * Check if the absolute path is under Version Control.
     289         *
     290         * @return array
     291         */
     292        function test_vcs_ABSPATH() {
     293                $result = $this->_test_is_vcs_checkout( ABSPATH );
     294                return $result;
     295        }
     296
     297        /**
     298         * Check if we can access files without providing credentials.
     299         *
     300         * @return array
     301         */
     302        function test_check_wp_filesystem_method() {
     303                $skin    = new Automatic_Upgrader_Skin;
     304                $success = $skin->request_filesystem_credentials( false, ABSPATH );
     305
     306                if ( ! $success ) {
     307                        $description  = __( 'Your installation of WordPress prompts for FTP credentials to perform updates.' );
     308                        $description .= ' ' . __( '(Your site is performing updates over FTP due to file ownership. Talk to your hosting company.)' );
     309
     310                        return array(
     311                                'description' => $description,
     312                                'severity'    => 'fail',
     313                        );
     314                }
     315
     316                return array(
     317                        'description' => __( "Your installation of WordPress doesn't require FTP credentials to perform updates." ),
     318                        'severity'    => 'pass',
     319                );
     320        }
     321
     322        /**
     323         * Check if core files are writable by the web user/group.
     324         *
     325         * @return array|bool
     326         */
     327        function test_all_files_writable() {
     328                global $wp_filesystem;
     329                include ABSPATH . WPINC . '/version.php'; // $wp_version; // x.y.z
     330
     331                $skin    = new Automatic_Upgrader_Skin;
     332                $success = $skin->request_filesystem_credentials( false, ABSPATH );
     333
     334                if ( ! $success ) {
     335                        return false;
     336                }
     337
     338                WP_Filesystem();
     339
     340                if ( 'direct' != $wp_filesystem->method ) {
     341                        return false;
     342                }
     343
     344                $checksums = get_core_checksums( $wp_version, 'en_US' );
     345                $dev       = ( false !== strpos( $wp_version, '-' ) );
     346                // Get the last stable version's files and test against that
     347                if ( ! $checksums && $dev ) {
     348                        $checksums = get_core_checksums( (float) $wp_version - 0.1, 'en_US' );
     349                }
     350
     351                // There aren't always checksums for development releases, so just skip the test if we still can't find any
     352                if ( ! $checksums && $dev ) {
     353                        return false;
     354                }
     355
     356                if ( ! $checksums ) {
     357                        $description = sprintf(
     358                                // translators: %s: WordPress version
     359                                __( "Couldn't retrieve a list of the checksums for WordPress %s." ),
     360                                $wp_version
     361                        );
     362                        $description .= ' ' . __( 'This could mean that connections are failing to WordPress.org.' );
     363                        return array(
     364                                'description' => $description,
     365                                'severity'    => 'warning',
     366                        );
     367                }
     368
     369                $unwritable_files = array();
     370                foreach ( array_keys( $checksums ) as $file ) {
     371                        if ( 'wp-content' == substr( $file, 0, 10 ) ) {
     372                                continue;
     373                        }
     374                        if ( ! file_exists( ABSPATH . '/' . $file ) ) {
     375                                continue;
     376                        }
     377                        if ( ! is_writable( ABSPATH . '/' . $file ) ) {
     378                                $unwritable_files[] = $file;
     379                        }
     380                }
     381
     382                if ( $unwritable_files ) {
     383                        if ( count( $unwritable_files ) > 20 ) {
     384                                $unwritable_files   = array_slice( $unwritable_files, 0, 20 );
     385                                $unwritable_files[] = '...';
     386                        }
     387                        return array(
     388                                'description' => __( 'Some files are not writable by WordPress:' ) . ' <ul><li>' . implode( '</li><li>', $unwritable_files ) . '</li></ul>',
     389                                'severity'    => 'fail',
     390                        );
     391                } else {
     392                        return array(
     393                                'description' => __( 'All of your WordPress files are writable.' ),
     394                                'severity'    => 'pass',
     395                        );
     396                }
     397        }
     398
     399        /**
     400         * Check if the install is using a development branch and can use nightly packages.
     401         *
     402         * @return array|bool
     403         */
     404        function test_accepts_dev_updates() {
     405                include ABSPATH . WPINC . '/version.php'; // $wp_version; // x.y.z
     406                // Only for dev versions
     407                if ( false === strpos( $wp_version, '-' ) ) {
     408                        return false;
     409                }
     410
     411                if ( defined( 'WP_AUTO_UPDATE_CORE' ) && ( 'minor' === WP_AUTO_UPDATE_CORE || false === WP_AUTO_UPDATE_CORE ) ) {
     412                        return array(
     413                                'description' => sprintf(
     414                                        /* translators: %s: Name of the constant used. */
     415                                        __( 'WordPress development updates are blocked by the %s constant.' ),
     416                                        '<code>WP_AUTO_UPDATE_CORE</code>'
     417                                ),
     418                                'severity'    => 'fail',
     419                        );
     420                }
     421
     422                if ( ! apply_filters( 'allow_dev_auto_core_updates', $wp_version ) ) {
     423                        return array(
     424                                'description' => sprintf(
     425                                        /* translators: %s: Name of the filter used. */
     426                                        __( 'WordPress development updates are blocked by the %s filter.' ),
     427                                        '<code>allow_dev_auto_core_updates</code>'
     428                                ),
     429                                'severity'    => 'fail',
     430                        );
     431                }
     432        }
     433
     434        /**
     435         * Check if the site supports automatic minor updates.
     436         *
     437         * @return array
     438         */
     439        function test_accepts_minor_updates() {
     440                if ( defined( 'WP_AUTO_UPDATE_CORE' ) && false === WP_AUTO_UPDATE_CORE ) {
     441                        return array(
     442                                'description' => sprintf(
     443                                        /* translators: %s: Name of the constant used. */
     444                                        __( 'WordPress security and maintenance releases are blocked by %s.' ),
     445                                        "<code>define( 'WP_AUTO_UPDATE_CORE', false );</code>"
     446                                ),
     447                                'severity'    => 'fail',
     448                        );
     449                }
     450
     451                if ( ! apply_filters( 'allow_minor_auto_core_updates', true ) ) {
     452                        return array(
     453                                'description' => sprintf(
     454                                        /* translators: %s: Name of the filter used. */
     455                                        __( 'WordPress security and maintenance releases are blocked by the %s filter.' ),
     456                                        '<code>allow_minor_auto_core_updates</code>'
     457                                ),
     458                                'severity'    => 'fail',
     459                        );
     460                }
     461        }
     462}
  • new file src/wp-admin/includes/class-wp-site-health.php

    diff --git src/wp-admin/includes/class-wp-site-health.php src/wp-admin/includes/class-wp-site-health.php
    new file mode 100644
    index 0000000000..02efdd56d0
    - +  
     1<?php
     2/**
     3 * Class for looking up a sites health based on a users WordPress environment.
     4 *
     5 * @package WordPress
     6 * @subpackage Site_Health
     7 * @since 5.2.0
     8 */
     9
     10class WP_Site_Health {
     11        private $php_min_version_check       = '5.6';
     12        private $php_supported_version_check = '5.6';
     13        private $php_rec_version_check       = '7.3';
     14
     15        private $mysql_min_version_check;
     16        private $mysql_rec_version_check;
     17
     18        public  $mariadb                        = false;
     19        private $mysql_server_version           = null;
     20        private $health_check_mysql_rec_version = null;
     21
     22        public $schedules;
     23        public $crons;
     24        public $last_missed_cron = null;
     25
     26        public function __construct() {
     27                $this->init();
     28        }
     29
     30        /**
     31         * Initiate the class.
     32         */
     33        public function init() {
     34                $this->prepare_sql_data();
     35
     36                add_action( 'wp_loaded', array( $this, 'check_wp_version_check_exists' ) );
     37
     38                add_action( 'admin_enqueue_scripts', array( $this, 'enqueues' ) );
     39        }
     40
     41        /**
     42         * Set up JavaScript classes on the Site Health pages.
     43         */
     44        public function enqueues() {
     45                $screen = get_current_screen();
     46                if ( 'site-health' !== $screen->id ) {
     47                        return;
     48                }
     49
     50                $health_check_js_variables = array(
     51                        'screen'      => $screen->id,
     52                        'string'      => array(
     53                                'please_wait'                        => __( 'Please wait...' ),
     54                                'copied'                             => __( 'Copied' ),
     55                                'running_tests'                      => __( 'Currently being tested...' ),
     56                                'site_health_complete'               => __( 'All site health tests have finished running.' ),
     57                                'site_info_show_copy'                => __( 'Show options for copying this information' ),
     58                                'site_info_hide_copy'                => __( 'Hide options for copying this information' ),
     59                                // translators: %s: The percentage score for the tests.
     60                                'site_health_complete_screen_reader' => __( 'All site health tests have finished running. Your site scored %s, and the results are now available on the page.' ),
     61                                'site_info_copied'                   => __( 'Site information has been added to your clipboard.' ),
     62                        ),
     63                        'nonce'       => array(
     64                                'site_status'        => wp_create_nonce( 'health-check-site-status' ),
     65                                'site_status_result' => wp_create_nonce( 'health-check-site-status-result' ),
     66                        ),
     67                        'site_status' => array(
     68                                'direct' => array(),
     69                                'async'  => array(),
     70                                'issues' => array(
     71                                        'good'        => 0,
     72                                        'recommended' => 0,
     73                                        'critical'    => 0,
     74                                ),
     75                        ),
     76                );
     77
     78                $issue_counts = get_transient( 'health-check-site-status-result' );
     79
     80                if ( false !== $issue_counts ) {
     81                        $issue_counts = json_decode( $issue_counts );
     82
     83                        $health_check_js_variables['site_status']['issues'] = $issue_counts;
     84                }
     85
     86                if ( 'site-health' === $screen->id && ! isset( $_GET['tab'] ) ) {
     87                        $tests = WP_Site_Health::get_tests();
     88                        foreach ( $tests['direct'] as $test ) {
     89                                $test_function = sprintf(
     90                                        'get_test_%s',
     91                                        $test['test']
     92                                );
     93
     94                                if ( method_exists( $this, $test_function ) && is_callable( array( $this, $test_function ) ) ) {
     95                                        $health_check_js_variables['site_status']['direct'][] = call_user_func( array( $this, $test_function ) );
     96                                } else {
     97                                        $health_check_js_variables['site_status']['direct'][] = call_user_func( $test['test'] );
     98                                }
     99                        }
     100
     101                        foreach ( $tests['async'] as $test ) {
     102                                $health_check_js_variables['site_status']['async'][] = array(
     103                                        'test'      => $test['test'],
     104                                        'completed' => false,
     105                                );
     106                        }
     107                }
     108
     109                wp_localize_script( 'site-health', 'SiteHealth', $health_check_js_variables );
     110        }
     111
     112        /**
     113         * Run the SQL version checks.
     114         *
     115         * These values are used in later tests, but the part of
     116         * preparing them is more easily managed early in the class
     117         * for ease of access and discovery.
     118         */
     119        private function prepare_sql_data() {
     120                global $wpdb;
     121
     122                if ( method_exists( $wpdb, 'db_version' ) ) {
     123                        if ( $wpdb->use_mysqli ) {
     124                                // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_server_info
     125                                $mysql_server_type = mysqli_get_server_info( $wpdb->dbh );
     126                        } else {
     127                                // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_get_server_info
     128                                $mysql_server_type = mysql_get_server_info( $wpdb->dbh );
     129                        }
     130
     131                        $this->mysql_server_version = $wpdb->get_var( 'SELECT VERSION()' );
     132                }
     133
     134                $this->health_check_mysql_rec_version = '5.6';
     135
     136                if ( stristr( $mysql_server_type, 'mariadb' ) ) {
     137                        $this->mariadb                        = true;
     138                        $this->health_check_mysql_rec_version = '10.0';
     139                }
     140
     141                $this->mysql_min_version_check = version_compare( '5.5', $this->mysql_server_version, '<=' );
     142                $this->mysql_rec_version_check = version_compare( $this->health_check_mysql_rec_version, $this->mysql_server_version, '<=' );
     143        }
     144
     145        /**
     146         * Test if `wp_version_check` is blocked.
     147         *
     148         * It's possible to block updates with the `wp_version_check` filter,
     149         * but this can't be checked during an AJAX call, as the filter is
     150         * never introduced then.
     151         *
     152         * This filter overrides a normal page request
     153         * if it's made by an admin through the AJAX call with the right query
     154         * argument to check for this.
     155         */
     156        public function check_wp_version_check_exists() {
     157                if ( ! is_admin() || ! is_user_logged_in() || ! current_user_can( 'manage_options' ) || ! isset( $_GET['health-check-test-wp_version_check'] ) ) {
     158                        return;
     159                }
     160
     161                echo ( has_filter( 'wp_version_check', 'wp_version_check' ) ? 'yes' : 'no' );
     162
     163                die();
     164        }
     165
     166        /**
     167         * Tests for WordPress version and outputs it.
     168         *
     169         * Gives various results depending on what kind of updates
     170         * are available, if any, to encourage the user to install
     171         * security updates as a priority.
     172         *
     173         * @return array
     174         */
     175        public function get_test_wordpress_version() {
     176                $result = array(
     177                        'label'       => '',
     178                        'status'      => '',
     179                        'badge'       => array(
     180                                'label' => 'Security',
     181                                'color' => 'red',
     182                        ),
     183                        'description' => '',
     184                        'actions'     => '',
     185                        'test'        => 'wordpress_version',
     186                );
     187
     188                $core_current_version = get_bloginfo( 'version' );
     189                $core_updates         = get_core_updates();
     190
     191                if ( ! is_array( $core_updates ) ) {
     192                        $result['status'] = 'recommended';
     193
     194                        $result['label'] = sprintf(
     195                                // translators: %s: Your current version of WordPress.
     196                                __( 'WordPress version %s' ),
     197                                $core_current_version
     198                        );
     199
     200                        $result['description'] = sprintf(
     201                                '<p>%s</p>',
     202                                __( 'We were unable to check if any new versions of WordPress are available.' )
     203                        );
     204
     205                        $result['actions'] = sprintf(
     206                                '<a href="%s">%s</a>',
     207                                esc_url( admin_url( 'update-core.php?force-check=1' ) ),
     208                                __( 'Check for updates manually' )
     209                        );
     210                } else {
     211                        foreach ( $core_updates as $core => $update ) {
     212                                if ( 'upgrade' === $update->response ) {
     213                                        $current_version = explode( '.', $core_current_version );
     214                                        $new_version     = explode( '.', $update->version );
     215
     216                                        $current_major = $current_version[0] . '.' . $current_version[1];
     217                                        $new_major     = $new_version[0] . '.' . $new_version[1];
     218
     219                                        $result['label'] = sprintf(
     220                                                // translators: %s: The latest version of WordPress available.
     221                                                __( 'WordPress update available (%s)' ),
     222                                                $update->version
     223                                        );
     224
     225                                        $result['actions'] = sprintf(
     226                                                '<a href="%s">%s</a>',
     227                                                esc_url( admin_url( 'update-core.php' ) ),
     228                                                __( 'Install the latest version of WordPress' )
     229                                        );
     230
     231                                        if ( $current_major !== $new_major ) {
     232                                                // This is a major version mismatch.
     233                                                $result['status']      = 'recommended';
     234                                                $result['description'] = sprintf(
     235                                                        '<p>%s</p>',
     236                                                        __( 'A new version of WordPress is available.' )
     237                                                );
     238                                        } else {
     239                                                // This is a minor version, sometimes considered more critical.
     240                                                $result['status']      = 'critical';
     241                                                $result['description'] = sprintf(
     242                                                        '<p>%s</p>',
     243                                                        __( 'A new minor update is available for your site. Because minor updates often address security, it\'s important to install them.' )
     244                                                );
     245                                        }
     246                                } else {
     247                                        $result['status'] = 'good';
     248                                        $result['label']  = sprintf(
     249                                                // translators: %s: The current version of WordPress installed on this site.
     250                                                __( 'Your WordPress version is up to date (%s)' ),
     251                                                $core_current_version
     252                                        );
     253
     254                                        $result['description'] = sprintf(
     255                                                '<p>%s</p>',
     256                                                __( 'You are currently running the latest version of WordPress available, keep it up!' )
     257                                        );
     258                                }
     259                        }
     260                }
     261
     262                return $result;
     263        }
     264
     265        /**
     266         * Middleman function for passing AJAX requests on to the direct test runner.
     267         */
     268        public function json_wordpress_version() {
     269                wp_send_json_success( $this->get_test_wordpress_version() );
     270        }
     271
     272        /**
     273         * Test if plugins are outdated, or unnecessary.
     274         *
     275         * The tests checks if your plugins are up to date, and
     276         * encourages you to remove any that are not in use.
     277         *
     278         * @return array
     279         */
     280        public function get_test_plugin_version() {
     281                $result = array(
     282                        'label'       => __( 'Your plugins are up to date' ),
     283                        'status'      => 'good',
     284                        'badge'       => array(
     285                                'label' => 'Security',
     286                                'color' => 'red',
     287                        ),
     288                        'description' => sprintf(
     289                                '<p>%s</p>',
     290                                __( 'Plugins extend your site\'s functionality with things like contact forms, ecommerce and much more. That means they have deep access to your site, so it\'s vital to keep them up to date.' )
     291                        ),
     292                        'actions'     => '',
     293                        'test'        => 'plugin_version',
     294                );
     295
     296                $plugins        = get_plugins();
     297                $plugin_updates = get_plugin_updates();
     298
     299                $plugins_have_updates = false;
     300                $plugins_active       = 0;
     301                $plugins_total        = 0;
     302                $plugins_needs_update = 0;
     303
     304                // Loop over the available plugins and check their versions and active state.
     305                foreach ( $plugins as $plugin_path => $plugin ) {
     306                        $plugins_total++;
     307
     308                        if ( is_plugin_active( $plugin_path ) ) {
     309                                $plugins_active++;
     310                        }
     311
     312                        $plugin_version = $plugin['Version'];
     313
     314                        if ( array_key_exists( $plugin_path, $plugin_updates ) ) {
     315                                $plugins_needs_update++;
     316                                $plugins_have_updates = true;
     317                        }
     318                }
     319
     320                // Add a notice if there are outdated plugins.
     321                if ( $plugins_needs_update > 0 ) {
     322                        $result['status'] = 'critical';
     323
     324                        $result['label'] = __( 'You have plugins waiting to be updated' );
     325
     326                        $result['description'] .= sprintf(
     327                                '<p>%s</p>',
     328                                sprintf(
     329                                        // translators: %d: The amount of outdated plugins.
     330                                        esc_html(
     331                                                _n(
     332                                                        'Your site has %d plugin waiting to be updated.',
     333                                                        'Your site has %d plugins waiting for updates.',
     334                                                        $plugins_needs_update
     335                                                )
     336                                        ),
     337                                        $plugins_needs_update
     338                                )
     339                        );
     340                } else {
     341                        $result['description'] .= sprintf(
     342                                '<p>%s</p>',
     343                                sprintf(
     344                                        // translators: %d: The amount of plugins.
     345                                        esc_html(
     346                                                _n(
     347                                                        'Your site has %d active plugin, and it is up to date.',
     348                                                        'Your site has %d active plugins, and they are all up to date.',
     349                                                        $plugins_active
     350                                                )
     351                                        ),
     352                                        $plugins_active
     353                                )
     354                        );
     355                }
     356
     357                // Check if there are inactive plugins.
     358                if ( $plugins_total > $plugins_active ) {
     359                        $unused_plugins = $plugins_total - $plugins_active;
     360
     361                        $result['status'] = 'recommended';
     362
     363                        $result['label'] = __( 'Inactive plugins should be removed' );
     364
     365                        $result['description'] .= sprintf(
     366                                '<p>%s</p>',
     367                                sprintf(
     368                                        // translators: %d: The amount of inactive plugins.
     369                                        esc_html(
     370                                                _n(
     371                                                        'Your site has %d inactive plugin. Inactive plugins are tempting targets for attackers. if you\'re not going to use a plugin, we recommend you remove it.',
     372                                                        'Your site has %d inactive plugins. Inactive plugins are tempting targets for attackers. if you\'re not going to use a plugin, we recommend you remove it.',
     373                                                        $unused_plugins
     374                                                )
     375                                        ),
     376                                        $unused_plugins
     377                                )
     378                        );
     379                }
     380
     381                return $result;
     382        }
     383
     384        /**
     385         * Test if themes are outdated, or unnecessary.
     386         *
     387         * The tests checks if your site has a default theme (to fall back
     388         * on if there is a need), if your themes are up to date and, finally,
     389         * encourages you to remove any themes that are not needed.
     390         *
     391         * @return array
     392         */
     393        public function get_test_theme_version() {
     394                $result = array(
     395                        'label'       => __( 'Your themes are up to date' ),
     396                        'status'      => 'good',
     397                        'badge'       => array(
     398                                'label' => 'Security',
     399                                'color' => 'red',
     400                        ),
     401                        'description' => sprintf(
     402                                '<p>%s</p>',
     403                                __( 'Themes add your site\'s look and feel. It\'s important to keep them up to date -- to stay consistent with your brand and keep your site secure.' )
     404                        ),
     405                        'actions'     => '',
     406                        'test'        => 'theme_version',
     407                );
     408
     409                $theme_updates = get_theme_updates();
     410
     411                $themes_total        = 0;
     412                $themes_need_updates = 0;
     413                $themes_inactive     = 0;
     414
     415                // This value is changed during processing to determine how many themes are considered a reasonable amount.
     416                $allowed_theme_count = 1;
     417
     418                $has_default_theme   = false;
     419                $has_unused_themes   = false;
     420                $show_unused_themes  = true;
     421                $using_default_theme = false;
     422
     423                // Populate a list of all themes available in the install.
     424                $all_themes   = wp_get_themes();
     425                $active_theme = wp_get_theme();
     426
     427                foreach ( $all_themes as $theme_slug => $theme ) {
     428                        $themes_total++;
     429
     430                        if ( WP_DEFAULT_THEME === $theme_slug ) {
     431                                $has_default_theme = true;
     432
     433                                if ( $theme_slug === get_stylesheet() ) {
     434                                        $using_default_theme = true;
     435                                }
     436                        }
     437
     438                        if ( array_key_exists( $theme_slug, $theme_updates ) ) {
     439                                $themes_need_updates++;
     440                        }
     441                }
     442
     443                // If this is a child theme, increase the allowed theme count by one, to account for the parent.
     444                if ( $active_theme->parent() ) {
     445                        $allowed_theme_count++;
     446                }
     447
     448                // If there's a default theme installed, we count that as allowed as well.
     449                if ( $has_default_theme ) {
     450                        $allowed_theme_count++;
     451                }
     452
     453                if ( $themes_total > $allowed_theme_count ) {
     454                        $has_unused_themes = true;
     455                        $themes_inactive   = ( $themes_total - $allowed_theme_count );
     456                }
     457
     458                // Check if any themes need to be updated.
     459                if ( $themes_need_updates > 0 ) {
     460                        $result['status'] = 'critical';
     461
     462                        $result['label'] = __( 'You have themes waiting to be updated' );
     463
     464                        $result['description'] .= sprintf(
     465                                '<p>%s</p>',
     466                                sprintf(
     467                                        // translators: %d: The amount of outdated themes.
     468                                        esc_html(
     469                                                _n(
     470                                                        'Your site has %d theme waiting to be updated.',
     471                                                        'Your site has %d themes waiting to be updated.',
     472                                                        $themes_need_updates
     473                                                )
     474                                        ),
     475                                        $themes_need_updates
     476                                )
     477                        );
     478                } else {
     479                        // Give positive feedback about the site being good about keeping things up to date.
     480                        $result['description'] .= sprintf(
     481                                '<p>%s</p>',
     482                                sprintf(
     483                                        // translators: %d: The amount of themes.
     484                                        esc_html(
     485                                                _n(
     486                                                        'Your site has %d installed theme, and it is up to date.',
     487                                                        'Your site has %d installed themes, and they are all up to date.',
     488                                                        $themes_total
     489                                                )
     490                                        ),
     491                                        $themes_total
     492                                )
     493                        );
     494                }
     495
     496                if ( $has_unused_themes && $show_unused_themes ) {
     497
     498                        // This is a child theme, so we want to be a bit more explicit in our messages.
     499                        if ( $active_theme->parent() ) {
     500                                // Recommend removing inactive themes, except a default theme, your current one, and the parent theme.
     501                                $result['status'] = 'recommended';
     502
     503                                $result['label'] = __( 'You should remove inactive themes' );
     504
     505                                if ( $using_default_theme ) {
     506                                        $result['description'] .= sprintf(
     507                                                '<p>%s</p>',
     508                                                sprintf(
     509                                                        // translators: %1$d: The amount of inactive themes. %2$s: The currently active theme. %3$s: The active themes parent theme.
     510                                                        esc_html(
     511                                                                _n(
     512                                                                        'Your site has %1$d inactive theme. To enhance your site’s security, we recommend you remove any themes you\'re not using. You should keep your current theme, %2$s, and %3$s, its parent theme.',
     513                                                                        'Your site has %1$d inactive themes. To enhance your site’s security, we recommend you remove any themes you\'re not using. You should keep your current theme, %2$s, and %3$s, its parent theme.',
     514                                                                        $themes_inactive
     515                                                                )
     516                                                        ),
     517                                                        $themes_inactive,
     518                                                        $active_theme->name,
     519                                                        $active_theme->parent()->name
     520                                                )
     521                                        );
     522                                } else {
     523                                        $result['description'] .= sprintf(
     524                                                '<p>%s</p>',
     525                                                sprintf(
     526                                                        // translators: %1$d: The amount of inactive themes. %2$s: The default theme for WordPress. %3$s: The currently active theme. %4$s: The active themes parent theme.
     527                                                        esc_html(
     528                                                                _n(
     529                                                                        'Your site has %1$d inactive theme. To enhance your site’s security, we recommend you remove any themes you\'re not using. You should keep %2$s, the default WordPress theme, %3$s, your current theme and %4$s, its parent theme.',
     530                                                                        'Your site has %1$d inactive themes. To enhance your site’s security, we recommend you remove any themes you\'re not using. You should keep %2$s, the default WordPress theme, %3$s, your current theme and %4$s, its parent theme.',
     531                                                                        $themes_inactive
     532                                                                )
     533                                                        ),
     534                                                        $themes_inactive,
     535                                                        WP_DEFAULT_THEME,
     536                                                        $active_theme->name,
     537                                                        $active_theme->parent()->name
     538                                                )
     539                                        );
     540                                }
     541                        } else {
     542                                // Recommend removing all inactive themes.
     543                                $result['status'] = 'recommended';
     544
     545                                $result['label'] = __( 'You should remove inactive themes' );
     546
     547                                if ( $using_default_theme ) {
     548                                        $result['description'] .= sprintf(
     549                                                '<p>%s</p>',
     550                                                sprintf(
     551                                                        // translators: %1$d: The amount of inactive themes. %2$s: The currently active theme.
     552                                                        esc_html(
     553                                                                _n(
     554                                                                        'Your site has %1$d inactive theme, other than %2$s, your active theme. We recommend removing any unused themes to enhance your sites security.',
     555                                                                        'Your site has %1$d inactive themes, other than %2$s, your active theme. We recommend removing any unused themes to enhance your sites security.',
     556                                                                        $themes_inactive
     557                                                                )
     558                                                        ),
     559                                                        $themes_inactive,
     560                                                        $active_theme->name
     561                                                )
     562                                        );
     563                                } else {
     564                                        $result['description'] .= sprintf(
     565                                                '<p>%s</p>',
     566                                                sprintf(
     567                                                        // translators: %1$d: The amount of inactive themes. %2$s: The default theme for WordPress. %3$s: The currently active theme.
     568                                                        esc_html(
     569                                                                _n(
     570                                                                        'Your site has %1$d inactive theme, other than %2$s, the default WordPress theme, and %3$s, your active theme. We recommend removing any unused themes to enhance your sites security.',
     571                                                                        'Your site has %1$d inactive themes, other than %2$s, the default WordPress theme, and %3$s, your active theme. We recommend removing any unused themes to enhance your sites security.',
     572                                                                        $themes_inactive
     573                                                                )
     574                                                        ),
     575                                                        $themes_inactive,
     576                                                        WP_DEFAULT_THEME,
     577                                                        $active_theme->name
     578                                                )
     579                                        );
     580                                }
     581                        }
     582                }
     583
     584                // If not default Twenty* theme exists.
     585                if ( ! $has_default_theme ) {
     586                        $result['status'] = 'recommended';
     587
     588                        $result['label'] = __( 'Have a default theme available' );
     589
     590                        $result['description'] .= sprintf(
     591                                '<p>%s</p>',
     592                                __( 'Your site does not have any default theme. Default themes are used by WordPress automatically if anything is wrong with your normal theme.' )
     593                        );
     594                }
     595
     596                return $result;
     597        }
     598
     599        /**
     600         * Test if the supplied PHP version is supported
     601         *
     602         * Builds on the ServeHappy project by providing information about minimum
     603         * required PHP versions for core, as well as recommended versions and what is
     604         * still officially supported by the PHP project.
     605         *
     606         * @return array
     607         */
     608        public function get_test_php_version() {
     609                $result = array(
     610                        'label'       => sprintf(
     611                                // translators: %s: The current PHP version.
     612                                __( 'PHP is up to date (%s)' ),
     613                                PHP_VERSION
     614                        ),
     615                        'status'      => 'good',
     616                        'badge'       => array(
     617                                'label' => 'Security',
     618                                'color' => 'red',
     619                        ),
     620                        'description' => sprintf(
     621                                '<p>%s</p>',
     622                                __( 'PHP is the language your web server runs. WordPress uses it to get content from the database and build your site\'s pages in real time.' )
     623                        ),
     624                        'actions'     => '',
     625                        'test'        => 'php_version',
     626                );
     627
     628                // The version requirements were populated during class initialization.
     629                if ( ! $this->php_min_version_check ) {
     630                        $result['status'] = 'critical';
     631
     632                        $result['label'] = __( 'Your PHP version requires an update' );
     633
     634                        $result['actions'] = sprintf(
     635                                '<a href="%s">%s</a>',
     636                                esc_url(
     637                                        _x( 'https://wordpress.org/support/upgrade-php/', 'The link to the Update PHP page, which may be localized.' )
     638                                ),
     639                                __( 'Learn more about why you should update PHP' )
     640                        );
     641
     642                        $result['description'] .= sprintf(
     643                                '<p>%s</p>',
     644                                sprintf(
     645                                        // translators: %1$s: Current PHP version. %2$s: Recommended PHP version. %3$s: Minimum PHP version.
     646                                        __( 'Your version of PHP, %1$s, is very outdated and no longer getting security updates, exposing your site to attack. Please contact your host and get an upgrade to %2$s, which is the version WordPress recommends. If that\'s not possible, your site will run with version %3$s or newer.' ),
     647                                        PHP_VERSION,
     648                                        HEALTH_CHECK_PHP_REC_VERSION,
     649                                        HEALTH_CHECK_PHP_MIN_VERSION
     650                                )
     651                        );
     652                } elseif ( ! $this->php_supported_version_check ) {
     653                        $result['status'] = 'recommended';
     654
     655                        $result['label'] = __( 'Your PHP version should be updated' );
     656
     657                        $result['actions'] = sprintf(
     658                                '<a href="%s">%s</a>',
     659                                esc_url(
     660                                        _x( 'https://wordpress.org/support/upgrade-php/', 'The link to the Update PHP page, which may be localized.' )
     661                                ),
     662                                __( 'Learn more about why you should update PHP' )
     663                        );
     664
     665                        $result['description'] .= sprintf(
     666                                '<p>%s</p>',
     667                                sprintf(
     668                                        // translators: %1$s: Current PHP version. %2$s: Recommended PHP version.
     669                                        __( 'Your version of PHP, %1$s, is very outdated and no longer receiving security updates. Please contact your host for an upgrade. WordPress recommends PHP version %2$s.' ),
     670                                        PHP_VERSION,
     671                                        HEALTH_CHECK_PHP_REC_VERSION
     672                                )
     673                        );
     674                } elseif ( ! $this->php_rec_version_check ) {
     675                        $result['status'] = 'recommended';
     676
     677                        $result['label'] = __( 'We recommend that you update PHP' );
     678
     679                        $result['actions'] = sprintf(
     680                                '<a href="%s">%s</a>',
     681                                esc_url(
     682                                        _x( 'https://wordpress.org/support/upgrade-php/', 'The link to the Update PHP page, which may be localized.' )
     683                                ),
     684                                __( 'Learn more about why you should update PHP' )
     685                        );
     686
     687                        $result['description'] .= sprintf(
     688                                '<p>%s</p>',
     689                                sprintf(
     690                                        // translators: %s: Recommended PHP version
     691                                        __( 'For best performance we recommend using PHP %s or higher.' ),
     692                                        HEALTH_CHECK_PHP_REC_VERSION
     693                                )
     694                        );
     695                }
     696
     697                return $result;
     698        }
     699
     700        /**
     701         * Check if the passed extension or function are available.
     702         *
     703         * Make the check for available PHP modules into a simple boolean
     704         * operator for a cleaner test runner.
     705         *
     706         * @param string $extension
     707         * @param string $function
     708         *
     709         * @return bool
     710         */
     711        public function child_test_php_extension_availability( $extension = null, $function = null ) {
     712                // If no extension or function is passed, claim to fail testing, as we have nothing to test against.
     713                if ( null === $extension && null === $function ) {
     714                        return false;
     715                }
     716
     717                $available = true;
     718
     719                if ( null !== $extension && ! extension_loaded( $extension ) ) {
     720                        $available = false;
     721                }
     722                if ( null !== $function && ! function_exists( $function ) ) {
     723                        $available = false;
     724                }
     725
     726                return $available;
     727        }
     728
     729        /**
     730         * Test if required PHP modules are installed on the host.
     731         *
     732         * This test builds on the recommendations made by the WordPress Hosting Team
     733         * as seen at https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions
     734         *
     735         * @return array
     736         */
     737        public function get_test_php_extensions() {
     738                $result = array(
     739                        'label'       => __( 'Required and recommended modules are installed' ),
     740                        'status'      => 'good',
     741                        'badge'       => array(
     742                                'label' => 'Performance',
     743                                'color' => 'orange',
     744                        ),
     745                        'description' => sprintf(
     746                                '<p>%s</p><p>%s</p>',
     747                                __( 'PHP modules perform most of the tasks on the server that make your site run.' ),
     748                                sprintf(
     749                                        // translators: %s: Link to the hosting group page about recommended PHP modules.
     750                                        __( 'The Hosting team maintains a list of those modules, both recommended and required, in %s.' ),
     751                                        sprintf(
     752                                                '<a href="%s">%s</a>',
     753                                                esc_url( _x( 'https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions', 'The address to describe PHP modules and their use.' ) ),
     754                                                __( 'the team handbook' )
     755                                        )
     756                                )
     757                        ),
     758                        'actions'     => '',
     759                        'test'        => 'php_extensions',
     760                );
     761
     762                $modules = array(
     763                        'bcmath'    => array(
     764                                'function' => 'bcadd',
     765                                'required' => false,
     766                        ),
     767                        'curl'      => array(
     768                                'function' => 'curl_version',
     769                                'required' => false,
     770                        ),
     771                        'exif'      => array(
     772                                'function' => 'exif_read_data',
     773                                'required' => false,
     774                        ),
     775                        'filter'    => array(
     776                                'function' => 'filter_list',
     777                                'required' => false,
     778                        ),
     779                        'fileinfo'  => array(
     780                                'function' => 'finfo_file',
     781                                'required' => false,
     782                        ),
     783                        'mod_xml'   => array(
     784                                'extension' => 'libxml',
     785                                'required'  => false,
     786                        ),
     787                        'mysqli'    => array(
     788                                'function' => 'mysqli_connect',
     789                                'required' => false,
     790                        ),
     791                        'libsodium' => array(
     792                                'function'            => 'sodium_compare',
     793                                'required'            => false,
     794                                'php_bundled_version' => '7.2.0',
     795                        ),
     796                        'openssl'   => array(
     797                                'function' => 'openssl_encrypt',
     798                                'required' => false,
     799                        ),
     800                        'pcre'      => array(
     801                                'function' => 'preg_match',
     802                                'required' => false,
     803                        ),
     804                        'imagick'   => array(
     805                                'extension' => 'imagick',
     806                                'required'  => false,
     807                        ),
     808                        'gd'        => array(
     809                                'extension'    => 'gd',
     810                                'required'     => false,
     811                                'fallback_for' => 'imagick',
     812                        ),
     813                        'mcrypt'    => array(
     814                                'extension'    => 'mcrypt',
     815                                'required'     => false,
     816                                'fallback_for' => 'libsodium',
     817                        ),
     818                        'xmlreader' => array(
     819                                'extension'    => 'xmlreader',
     820                                'required'     => false,
     821                                'fallback_for' => 'xml',
     822                        ),
     823                        'zlib'      => array(
     824                                'extension'    => 'zlib',
     825                                'required'     => false,
     826                                'fallback_for' => 'zip',
     827                        ),
     828                );
     829
     830                /**
     831                 * An array representing all the modules we wish to test for.
     832                 *
     833                 * array $modules {
     834                 *     An associated array of modules to test for.
     835                 *
     836                 *     array $module {
     837                 *         An associated array of module properties used during testing.
     838                 *         One of either `$function` or `$extension` must be provided, or they will fail by default.
     839                 *
     840                 *         string $function     Optional. A function name to test for the existence of.
     841                 *         string $extension    Optional. An extension to check if is loaded in PHP.
     842                 *         bool   $required     Is this a required feature or not.
     843                 *         string $fallback_for Optional. The module this module replaces as a fallback.
     844                 *     }
     845                 * }
     846                 */
     847                $modules = apply_filters( 'site_status_test_php_modules', $modules );
     848
     849                $failures = array();
     850
     851                foreach ( $modules as $library => $module ) {
     852                        $extension = ( isset( $module['extension'] ) ? $module['extension'] : null );
     853                        $function  = ( isset( $module['function'] ) ? $module['function'] : null );
     854
     855                        // If this module is a fallback for another function, check if that other function passed.
     856                        if ( isset( $module['fallback_for'] ) ) {
     857                                /*
     858                                 * If that other function has a failure, mark this module as required for normal operations.
     859                                 * If that other function hasn't failed, skip this test as it's only a fallback.
     860                                 */
     861                                if ( isset( $failures[ $module['fallback_for'] ] ) ) {
     862                                        $module['required'] = true;
     863                                } else {
     864                                        continue;
     865                                }
     866                        }
     867
     868                        if ( ! $this->child_test_php_extension_availability( $extension, $function ) && ( ! isset( $module['php_bundled_version'] ) || version_compare( PHP_VERSION, $module['php_bundled_version'], '<' ) ) ) {
     869                                if ( $module['required'] ) {
     870                                        $result['status'] = 'critical';
     871                                }
     872
     873                                if ( ! $module['required'] && 'good' === $result['status'] ) {
     874                                        $result['status'] = 'recommended';
     875                                }
     876
     877                                $failures[ $library ] = sprintf(
     878                                        '<span class="%s"><span class="screen-reader-text">%s</span></span> %s',
     879                                        ( $module['required'] ? 'error' : 'warning' ),
     880                                        ( $module['required'] ? __( 'Error' ) : __( 'Warning' ) ),
     881                                        sprintf(
     882                                                // translators: %1$2: If a module is required or recommended. %2$s: The module name.
     883                                                __( 'The %1$s module, %2$s, is not installed, or has been disabled.' ),
     884                                                ( $module['required'] ? __( 'required' ) : __( 'optional' ) ),
     885                                                $library
     886                                        )
     887                                );
     888                        }
     889                }
     890
     891                if ( ! empty( $failures ) ) {
     892                        $output = '<ul>';
     893
     894                        foreach ( $failures as $failure ) {
     895                                $output .= sprintf(
     896                                        '<li>%s</li>',
     897                                        $failure
     898                                );
     899                        }
     900
     901                        $output .= '</ul>';
     902                }
     903
     904                if ( 'good' !== $result['status'] ) {
     905                        if ( 'recommended' === $result['status'] ) {
     906                                $result['label'] = __( 'One or more recommended modules are missing' );
     907                        }
     908                        if ( 'critical' === $result['status'] ) {
     909                                $result['label'] = __( 'One or more required modules are missing' );
     910                        }
     911
     912                        $result['description'] .= sprintf(
     913                                '<p>%s</p>',
     914                                $output
     915                        );
     916                }
     917
     918                return $result;
     919        }
     920
     921        /**
     922         * Test if the SQL server is up to date.
     923         *
     924         * @return array
     925         */
     926        public function get_test_sql_server() {
     927                $result = array(
     928                        'label'       => __( 'SQL server is up to date' ),
     929                        'status'      => 'good',
     930                        'badge'       => array(
     931                                'label' => 'Security',
     932                                'color' => 'red',
     933                        ),
     934                        'description' => sprintf(
     935                                '<p>%s</p>',
     936                                __( 'The SQL server is the database where WordPress stores all your site’s content and settings' )
     937                        ),
     938                        'actions'     => '',
     939                        'test'        => 'sql_server',
     940                );
     941
     942                $db_dropin = file_exists( WP_CONTENT_DIR . '/db.php' );
     943
     944                if ( ! $this->mysql_rec_version_check ) {
     945                        $result['status'] = 'recommended';
     946
     947                        $result['label'] = __( 'Outdated SQL server' );
     948
     949                        $result['description'] .= sprintf(
     950                                '<p>%s</p>',
     951                                sprintf(
     952                                        // translators: %1$s: The database engine in use (MySQL or MariaDB). %2$s: Database server recommended version number.
     953                                        __( 'For optimal performance and security reasons, we recommend running %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
     954                                        ( $this->mariadb ? 'MariaDB' : 'MySQL' ),
     955                                        $this->health_check_mysql_rec_version
     956                                )
     957                        );
     958                }
     959
     960                if ( ! $this->mysql_min_version_check ) {
     961                        $result['status'] = 'critical';
     962
     963                        $result['label'] = __( 'Severely outdated SQL server' );
     964
     965                        $result['description'] .= sprintf(
     966                                '<p>%s</p>',
     967                                sprintf(
     968                                        // translators: %1$s: The database engine in use (MySQL or MariaDB). %2$s: Database server minimum version number.
     969                                        __( 'WordPress requires %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
     970                                        ( $this->mariadb ? 'MariaDB' : 'MySQL' ),
     971                                        HEALTH_CHECK_MYSQL_MIN_VERSION
     972                                )
     973                        );
     974                }
     975
     976                if ( $db_dropin ) {
     977                        // translators: %s: The database engine in use (MySQL or MariaDB).
     978                        $result['description'] .= sprintf(
     979                                '<p>%s</p>',
     980                                wp_kses(
     981                                        sprintf(
     982                                                // translators: %s: The name of the database engine being used.
     983                                                __( 'You are using a <code>wp-content/db.php</code> drop-in which might mean that a %s database is not being used.' ),
     984                                                ( $this->mariadb ? 'MariaDB' : 'MySQL' )
     985                                        ),
     986                                        array(
     987                                                'code' => true,
     988                                        )
     989                                )
     990                        );
     991                }
     992
     993                return $result;
     994        }
     995
     996        /**
     997         * Test if the server is capable of using utf8mb4
     998         *
     999         * @return array
     1000         */
     1001        public function get_test_utf8mb4_support() {
     1002                global $wpdb;
     1003
     1004                $result = array(
     1005                        'label'       => __( 'UTF8MB4 is supported' ),
     1006                        'status'      => 'good',
     1007                        'badge'       => array(
     1008                                'label' => 'Performance',
     1009                                'color' => 'orange',
     1010                        ),
     1011                        'description' => sprintf(
     1012                                '<p>%s</p>',
     1013                                __( 'UTF8MB4 is a database storage attribute that makes sure your site can store non-English text and other strings (for instance emoticons) without unexpected problems.' )
     1014                        ),
     1015                        'actions'     => '',
     1016                        'test'        => 'utf8mb4_support',
     1017                );
     1018
     1019                if ( ! $this->mariadb ) {
     1020                        if ( version_compare( $this->mysql_server_version, '5.5.3', '<' ) ) {
     1021                                $result['status'] = 'recommended';
     1022
     1023                                $result['label'] = __( 'UTF8MB4 requires an SQL update' );
     1024
     1025                                $result['description'] .= sprintf(
     1026                                        '<p>%s</p>',
     1027                                        sprintf(
     1028                                                // translators: %1$s: Database engine name. %2$s: Version number.
     1029                                                __( 'WordPress\' utf8mb4 support requires %1$s version %2$s or greater' ),
     1030                                                'MySQL',
     1031                                                '5.5.3'
     1032                                        )
     1033                                );
     1034                        } else {
     1035                                $result['description'] .= sprintf(
     1036                                        '<p>%s</p>',
     1037                                        __( 'Your MySQL version supports utf8mb4' )
     1038                                );
     1039                        }
     1040                } else { // MariaDB introduced utf8mb4 support in 5.5.0
     1041                        if ( version_compare( $this->mysql_server_version, '5.5.0', '<' ) ) {
     1042                                $result['status'] = 'recommended';
     1043
     1044                                $result['label'] = __( 'UTF8MB4 requires an SQL update' );
     1045
     1046                                $result['description'] .= sprintf(
     1047                                        '<p>%s</p>',
     1048                                        sprintf(
     1049                                                // translators: %1$s: Database engine name. %2$s: Version number.
     1050                                                __( 'WordPress\' utf8mb4 support requires %1$s version %2$s or greater' ),
     1051                                                'MariaDB',
     1052                                                '5.5.0'
     1053                                        )
     1054                                );
     1055                        } else {
     1056                                $result['description'] .= sprintf(
     1057                                        '<p>%s</p>',
     1058                                        __( 'Your MariaDB version supports utf8mb4' )
     1059                                );
     1060                        }
     1061                }
     1062
     1063                if ( $wpdb->use_mysqli ) {
     1064                        // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_client_info
     1065                        $mysql_client_version = mysqli_get_client_info();
     1066                } else {
     1067                        // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_get_client_info
     1068                        $mysql_client_version = mysql_get_client_info();
     1069                }
     1070
     1071                /*
     1072                 * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server.
     1073                 * mysqlnd has supported utf8mb4 since 5.0.9.
     1074                 */
     1075                if ( false !== strpos( $mysql_client_version, 'mysqlnd' ) ) {
     1076                        $mysql_client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $mysql_client_version );
     1077                        if ( version_compare( $mysql_client_version, '5.0.9', '<' ) ) {
     1078                                $result['status'] = 'recommended';
     1079
     1080                                $result['label'] = __( 'UTF8MB4 requires a newer client library' );
     1081
     1082                                $result['description'] .= sprintf(
     1083                                        '<p>%s</p>',
     1084                                        sprintf(
     1085                                                // translators: %1$s: Name of the library, %2$s: Number of version.
     1086                                                __( 'WordPress\' utf8mb4 support requires MySQL client library (%1$s) version %2$s or newer.' ),
     1087                                                'mysqlnd',
     1088                                                '5.0.9'
     1089                                        )
     1090                                );
     1091                        }
     1092                } else {
     1093                        if ( version_compare( $mysql_client_version, '5.5.3', '<' ) ) {
     1094                                $result['status'] = 'recommended';
     1095
     1096                                $result['label'] = __( 'UTF8MB4 requires a newer client library' );
     1097
     1098                                $result['description'] .= sprintf(
     1099                                        '<p>%s</p>',
     1100                                        sprintf(
     1101                                                // translators: %1$s: Name of the library, %2$s: Number of version.
     1102                                                __( 'WordPress\' utf8mb4 support requires MySQL client library (%1$s) version %2$s or newer.' ),
     1103                                                'libmysql',
     1104                                                '5.5.3'
     1105                                        )
     1106                                );
     1107                        }
     1108                }
     1109
     1110                return $result;
     1111        }
     1112
     1113        /**
     1114         * Test if the site can communicate with WordPress.org
     1115         *
     1116         * @return array
     1117         */
     1118        public function get_test_dotorg_communication() {
     1119                $result = array(
     1120                        'label'       => __( 'Can communicate with WordPress.org' ),
     1121                        'status'      => '',
     1122                        'badge'       => array(
     1123                                'label' => 'Security',
     1124                                'color' => 'red',
     1125                        ),
     1126                        'description' => sprintf(
     1127                                '<p>%s</p>',
     1128                                __( 'Communicating with the WordPress servers is used to check for new versions, and to both install and update WordPress core, themes or plugins.' )
     1129                        ),
     1130                        'actions'     => '',
     1131                        'test'        => 'dotorg_communication',
     1132                );
     1133
     1134                $wp_dotorg = wp_remote_get(
     1135                        'https://wordpress.org',
     1136                        array(
     1137                                'timeout' => 10,
     1138                        )
     1139                );
     1140                if ( ! is_wp_error( $wp_dotorg ) ) {
     1141                        $result['status'] = 'good';
     1142                } else {
     1143                        $result['status'] = 'critical';
     1144
     1145                        $result['label'] = __( 'Could not reach WordPress.org' );
     1146
     1147                        $result['description'] .= sprintf(
     1148                                '<p>%s</p>',
     1149                                sprintf(
     1150                                        '<span class="error"><span class="screen-reader-text">%s</span></span> %s',
     1151                                        __( 'Error' ),
     1152                                        sprintf(
     1153                                                // translators: %1$s: The IP address WordPress.org resolves to. %2$s: The error returned by the lookup.
     1154                                                __( 'Your site is unable to reach WordPress.org at %1$s, and returned the error: %2$s' ),
     1155                                                gethostbyname( 'wordpress.org' ),
     1156                                                $wp_dotorg->get_error_message()
     1157                                        )
     1158                                )
     1159                        );
     1160                }
     1161
     1162                return $result;
     1163        }
     1164
     1165        /**
     1166         * Test if debug information is enabled.
     1167         *
     1168         * When WP_DEBUG is enabled, errors and information may be disclosed to
     1169         * site visitors, or it may be logged to a publicly accessible file.
     1170         *
     1171         * Debugging is also frequently left enabled after looking for errors on
     1172         * a site, as site owners do not understand the implications of this.
     1173         *
     1174         * @return array
     1175         */
     1176        public function get_test_is_in_debug_mode() {
     1177                $result = array(
     1178                        'label'       => __( 'Your site is not set to output debug information' ),
     1179                        'status'      => 'good',
     1180                        'badge'       => array(
     1181                                'label' => 'Security',
     1182                                'color' => 'red',
     1183                        ),
     1184                        'description' => sprintf(
     1185                                '<p>%s</p>',
     1186                                __( 'Debug mode is often enabled to gather more details about an error or site failure, but may contain sensitive information which should not be available on a publicly available website.' )
     1187                        ),
     1188                        'actions'     => '',
     1189                        'test'        => 'is_in_debug_mode',
     1190                );
     1191
     1192                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
     1193                        if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
     1194                                $result['label'] = __( 'Your site is set to log errors to a potentially public file.' );
     1195
     1196                                $result['status'] = 'critical';
     1197
     1198                                $result['description'] .= sprintf(
     1199                                        '<p>%s</p>',
     1200                                        __( 'The value, WP_DEBUG_LOG, has been added to this websites configuration file. This means any errors on the site will be written to a file which is potentially available to normal users.' )
     1201                                );
     1202                        }
     1203
     1204                        if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
     1205                                $result['label'] = __( 'Your site is set to display errors to site visitors.' );
     1206
     1207                                $result['status'] = 'critical';
     1208
     1209                                $result['description'] .= sprintf(
     1210                                        '<p>%s</p>',
     1211                                        __( 'The value, WP_DEBUG_DISPLAY, has either been added to your configuration file, or left with its default value. This will make errors display on the front end of your site.' )
     1212                                );
     1213                        }
     1214                }
     1215
     1216                return $result;
     1217        }
     1218
     1219        /**
     1220         * Test if your site is serving content over HTTPS.
     1221         *
     1222         * Many sites have varying degrees of HTTPS suppoort, the most common
     1223         * of which is sites that have it enabled, but only if you visit the
     1224         * right site address.
     1225         *
     1226         * @return array
     1227         */
     1228        public function get_test_https_status() {
     1229                $result = array(
     1230                        'label'       => '',
     1231                        'status'      => '',
     1232                        'badge'       => array(
     1233                                'label' => 'Security',
     1234                                'color' => 'red',
     1235                        ),
     1236                        'description' => '',
     1237                        'actions'     => '',
     1238                        'test'        => 'https_status',
     1239                );
     1240
     1241                if ( is_ssl() ) {
     1242                        $wp_url   = get_bloginfo( 'wpurl' );
     1243                        $site_url = get_bloginfo( 'url' );
     1244
     1245                        if ( 'https' !== substr( $wp_url, 0, 5 ) || 'https' !== substr( $site_url, 0, 5 ) ) {
     1246                                $result['status'] = 'recommended';
     1247
     1248                                $result['label'] = __( 'Only parts of your site are using HTTPS' );
     1249
     1250                                $result['description'] = sprintf(
     1251                                        '<p>%s</p>',
     1252                                        sprintf(
     1253                                                // translators: %s: URL to Settings > General to change options.
     1254                                                __( 'You are accessing this website using HTTPS, but your <a href="%s">WordPress Address</a> is not set up to use HTTPS by default.' ),
     1255                                                esc_url( admin_url( 'options-general.php' ) )
     1256                                        )
     1257                                );
     1258
     1259                                $result['actions'] = sprintf(
     1260                                        '<a href="%s">%s</a>',
     1261                                        esc_url( admin_url( 'options-general.php' ) ),
     1262                                        __( 'Update your site addresses' )
     1263                                );
     1264                        } else {
     1265                                $result['status'] = 'good';
     1266
     1267                                $result['label'] = __( 'Your website is using an active HTTPS connection.' );
     1268                        }
     1269                } else {
     1270                        $result['status'] = 'recommended';
     1271
     1272                        $result['label'] = __( 'Your site does not use HTTPS' );
     1273
     1274                        $result['description'] = sprintf(
     1275                                '<p>%s</p>',
     1276                                __( 'An HTTPS connection is needed for many features on the web today, it also gains the trust of your visitors by helping to protecting their online privacy.' )
     1277                        );
     1278
     1279                        $result['actions'] = sprintf(
     1280                                '<a href="%s">%s</a>',
     1281                                esc_url(
     1282                                        // translators: Website for explaining HTTPS and why it should be used.
     1283                                        __( 'https://www.cloudflare.com/learning/security/why-use-https/' )
     1284                                ),
     1285                                __( 'Read more about why you should use HTTPS' )
     1286                        );
     1287                }
     1288
     1289                return $result;
     1290        }
     1291
     1292        /**
     1293         * Check if the HTTP API can handle SSL/TLS requests.
     1294         *
     1295         * @return array
     1296         */
     1297        public function get_test_ssl_support() {
     1298                $result = array(
     1299                        'label'       => '',
     1300                        'status'      => '',
     1301                        'badge'       => array(
     1302                                'label' => 'Security',
     1303                                'color' => 'red',
     1304                        ),
     1305                        'description' => sprintf(
     1306                                '<p>%s</p>',
     1307                                __( 'Securely communicating between servers are needed for transactions such as fetching files, conducting sales on store sites, and much more.' )
     1308                        ),
     1309                        'actions'     => '',
     1310                        'test'        => 'ssl_support',
     1311                );
     1312
     1313                $supports_https = wp_http_supports( array( 'ssl' ) );
     1314
     1315                if ( $supports_https ) {
     1316                        $result['status'] = 'good';
     1317
     1318                        $result['label'] = __( 'Your site can communicate securely with other services.' );
     1319                } else {
     1320                        $result['status'] = 'critical';
     1321
     1322                        $result['label'] = __( 'Your site is unable to communicate securely with other services.' );
     1323
     1324                        $result['description'] .= sprintf(
     1325                                '<p>%s</p>',
     1326                                __( 'Talk to your web host about OpenSSL support for PHP' )
     1327                        );
     1328                }
     1329
     1330                return $result;
     1331        }
     1332
     1333        /**
     1334         * Test if scheduled events run as intended.
     1335         *
     1336         * If scheduled events are not running, this may indicate something
     1337         * with WP_Cron is not working as intended, or that there are orphaned
     1338         * events hanging around from older code.
     1339         *
     1340         * @return array
     1341         */
     1342        public function get_test_scheduled_events() {
     1343                $result = array(
     1344                        'label'       => __( 'Scheduled events are running' ),
     1345                        'status'      => 'good',
     1346                        'badge'       => array(
     1347                                'label' => 'Performance',
     1348                                'color' => 'orange',
     1349                        ),
     1350                        'description' => sprintf(
     1351                                '<p>%s</p>',
     1352                                __( 'Scheduled events are what periodically looks for updates to plugins, themes and WordPress it self. It is also what makes sure scheduled posts are published on time. It may also be used by various plugins to make sure that planned actions are executed.' )
     1353                        ),
     1354                        'actions'     => '',
     1355                        'test'        => 'scheduled_events',
     1356                );
     1357
     1358                $this->wp_schedule_test_init();
     1359
     1360                if ( is_wp_error( $this->has_missed_cron() ) ) {
     1361                        $result['status'] = 'critical';
     1362
     1363                        $result['label'] = __( 'It was not possible to check your scheduled events' );
     1364
     1365                        $result['description'] = sprintf(
     1366                                '<p>%s</p>',
     1367                                sprintf(
     1368                                        // translators: %s: The error message returned while from the cron scheduler.
     1369                                        __( 'While trying to test your sites scheduled events, the following error was returned: %s' ),
     1370                                        $this->has_missed_cron()->get_error_message()
     1371                                )
     1372                        );
     1373                } else {
     1374                        if ( $this->has_missed_cron() ) {
     1375                                $result['status'] = 'recommended';
     1376
     1377                                $result['label'] = __( 'A scheduled event has failed' );
     1378
     1379                                $result['description'] = sprintf(
     1380                                        '<p>%s</p>',
     1381                                        sprintf(
     1382                                                // translators: %s: The name of the failed cron event.
     1383                                                __( 'The scheduled event, %s, failed to run. Your site still works, but this may indicate that scheduling posts or automated updates may not work as intended.' ),
     1384                                                $this->last_missed_cron
     1385                                        )
     1386                                );
     1387                        }
     1388                }
     1389
     1390                return $result;
     1391        }
     1392
     1393        /**
     1394         * Test if WordPress can run automated background updates.
     1395         *
     1396         * Background updates in WordPress are primarely used for minor releases
     1397         * and security updates. It's important to either have these working,
     1398         * or be aware that they are intentionally disabled for whatever reason.
     1399         *
     1400         * @return array
     1401         */
     1402        public function get_test_background_updates() {
     1403                $result = array(
     1404                        'label'       => __( 'Background updates are working' ),
     1405                        'status'      => 'good',
     1406                        'badge'       => array(
     1407                                'label' => 'Security',
     1408                                'color' => 'red',
     1409                        ),
     1410                        'description' => sprintf(
     1411                                '<p>%s</p>',
     1412                                __( 'Background updates ensure that WordPress can auto-update if a security update is released for the version you are currently using.' )
     1413                        ),
     1414                        'actions'     => '',
     1415                        'test'        => 'background_updates',
     1416                );
     1417
     1418                if ( ! class_exists( 'WP_Site_Health_Auto_Updates' ) ) {
     1419                        require_once( ABSPATH . 'wp-admin/includes/class-wp-site-health-auto-updates.php' );
     1420                }
     1421
     1422                // Run the auto-update tests in a separate class,
     1423                // as there are many considerations to be made.
     1424                $automatic_updates = new WP_Site_Health_Auto_Updates();
     1425                $tests             = $automatic_updates->run_tests();
     1426
     1427                $output = '<ul>';
     1428
     1429                foreach ( $tests as $test ) {
     1430                        $severity_string = __( 'Passed' );
     1431
     1432                        if ( 'fail' === $test->severity ) {
     1433                                $result['label'] = __( 'Background updates are not working as expected' );
     1434
     1435                                $result['status'] = 'critical';
     1436
     1437                                $severity_string = __( 'Error' );
     1438                        }
     1439
     1440                        if ( 'warning' === $test->severity && 'good' === $result['status'] ) {
     1441                                $result['label'] = __( 'Background updates may not be working properly' );
     1442
     1443                                $result['status'] = 'recommended';
     1444
     1445                                $severity_string = __( 'Warning' );
     1446                        }
     1447
     1448                        $output .= sprintf(
     1449                                '<li><span class="%s"><span class="screen-reader-text">%s</span></span> %s</li>',
     1450                                esc_attr( $test->severity ),
     1451                                $severity_string,
     1452                                $test->description
     1453                        );
     1454                }
     1455
     1456                $output .= '</ul>';
     1457
     1458                if ( 'good' !== $result['status'] ) {
     1459                        $result['description'] .= sprintf(
     1460                                '<p>%s</p>',
     1461                                $output
     1462                        );
     1463                }
     1464
     1465                return $result;
     1466        }
     1467
     1468        /**
     1469         * Test if loopbacks work as expected.
     1470         *
     1471         * A loopback is when WordPress queries it self, for example to start
     1472         * a new WP_Cron instance, or when editing a plugin or theme.
     1473         * This has shown it self to be a recurring issue as code can very
     1474         * easily break this interaction.
     1475         *
     1476         * @return array
     1477         */
     1478        public function get_test_loopback_requests() {
     1479                $result = array(
     1480                        'label'       => __( 'Your site can perform loopback requests' ),
     1481                        'status'      => 'good',
     1482                        'badge'       => array(
     1483                                'label' => 'Performance',
     1484                                'color' => 'orange',
     1485                        ),
     1486                        'description' => sprintf(
     1487                                '<p>%s</p>',
     1488                                __( 'Loopback requests are used to run scheduled events, and are also used by the built-in editors for themes and plugins to verify code stability.' )
     1489                        ),
     1490                        'actions'     => '',
     1491                        'test'        => 'loopback_requests',
     1492                );
     1493
     1494                $check_loopback = $this->can_perform_loopback();
     1495
     1496                $result['status'] = $check_loopback->status;
     1497
     1498                if ( 'good' !== $check_loopback->status ) {
     1499                        $result['label'] = __( 'Your site could not complete a loopback request' );
     1500
     1501                        $result['description'] .= sprintf(
     1502                                '<p>%s</p>',
     1503                                $check_loopback->message
     1504                        );
     1505                }
     1506
     1507                return $result;
     1508        }
     1509
     1510        /**
     1511         * Test if HTTP requests are blocked.
     1512         *
     1513         * It's possible to block all outgoing communication (with the
     1514         * possibility of whitelisting hosts) via the HTTP API.
     1515         *
     1516         * This may create problems for users as many features are
     1517         * running as services these days.
     1518         *
     1519         * @return array
     1520         */
     1521        public function get_test_http_requests() {
     1522                $result = array(
     1523                        'label'       => __( 'HTTP requests seem to be working as expected' ),
     1524                        'status'      => 'good',
     1525                        'badge'       => array(
     1526                                'label' => 'Performance',
     1527                                'color' => 'orange',
     1528                        ),
     1529                        'description' => sprintf(
     1530                                '<p>%s</p>',
     1531                                __( 'It is possible for site maintainers to block all, or some, communication to other sites and services. If set up incorrectly, this may prevent plugins and themes from working as intended.' )
     1532                        ),
     1533                        'actions'     => '',
     1534                        'test'        => 'http_requests',
     1535                );
     1536
     1537                $blocked = false;
     1538                $hosts   = array();
     1539
     1540                if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) ) {
     1541                        $blocked = true;
     1542                }
     1543
     1544                if ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) {
     1545                        $hosts = explode( ',', WP_ACCESSIBLE_HOSTS );
     1546                }
     1547
     1548                if ( $blocked && 0 === sizeof( $hosts ) ) {
     1549                        $result['status'] = 'critical';
     1550
     1551                        $result['label'] = __( 'HTTP requests are blocked' );
     1552
     1553                        $result['description'] .= sprintf(
     1554                                '<p>%s</p>',
     1555                                __( 'HTTP requests have been blocked by the WP_HTTP_BLOCK_EXTERNAL constant, with no allowed hosts.' )
     1556                        );
     1557                }
     1558
     1559                if ( $blocked && 0 < sizeof( $hosts ) ) {
     1560                        $result['status'] = 'recommended';
     1561
     1562                        $result['label'] = __( 'HTTP requests are partially blocked' );
     1563
     1564                        $result['description'] .= sprintf(
     1565                                '<p>%s</p>',
     1566                                sprintf(
     1567                                        // translators: %s: List of hostnames whitelisted.
     1568                                        __( 'HTTP requests have been blocked by the WP_HTTP_BLOCK_EXTERNAL constant, with some hosts whitelisted: %s.' ),
     1569                                        implode( ',', $hosts )
     1570                                )
     1571                        );
     1572                }
     1573
     1574                return $result;
     1575        }
     1576
     1577        /**
     1578         * Test if the REST API is accessible.
     1579         *
     1580         * Various security measures may block the REST API from working,
     1581         * or it may have been disabled in general. This is required for the
     1582         * new block editor to work, so we explicitly test for this.
     1583         *
     1584         * @return array
     1585         */
     1586        public function get_test_rest_availability() {
     1587                $result = array(
     1588                        'label'       => __( 'The REST API is available' ),
     1589                        'status'      => 'good',
     1590                        'badge'       => array(
     1591                                'label' => 'Performance',
     1592                                'color' => 'orange',
     1593                        ),
     1594                        'description' => sprintf(
     1595                                '<p>%s</p>',
     1596                                __( 'The REST API is one way WordPress, and other applications, communicate with the server. One example is the block editor screen, which relies on this to display, and save, your posts and pages.' )
     1597                        ),
     1598                        'actions'     => '',
     1599                        'test'        => 'rest_availability',
     1600                );
     1601
     1602                $cookies = wp_unslash( $_COOKIE );
     1603                $timeout = 10;
     1604                $headers = array(
     1605                        'Cache-Control' => 'no-cache',
     1606                        'X-WP-Nonce'    => wp_create_nonce( 'wp_rest' ),
     1607                );
     1608
     1609                // Include Basic auth in loopback requests.
     1610                if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
     1611                        $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
     1612                }
     1613
     1614                $url = rest_url( 'wp/v2/types/post' );
     1615
     1616                // The context for this is editing with the new block editor.
     1617                $url = add_query_arg(
     1618                        array(
     1619                                'context' => 'edit',
     1620                        ),
     1621                        $url
     1622                );
     1623
     1624                $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout' ) );
     1625
     1626                if ( is_wp_error( $r ) ) {
     1627                        $result['status'] = 'critical';
     1628
     1629                        $result['label'] = __( 'The REST API encountered an error' );
     1630
     1631                        $result['description'] .= sprintf(
     1632                                '<p>%s</p>',
     1633                                sprintf(
     1634                                        '%s<br>%s',
     1635                                        __( 'The REST API request failed due to an error.' ),
     1636                                        sprintf(
     1637                                                // translators: %1$d: The HTTP response code. %2$s: The error message returned.
     1638                                                __( 'Error encountered: (%1$d) %2$s' ),
     1639                                                wp_remote_retrieve_response_code( $r ),
     1640                                                $r->get_error_message()
     1641                                        )
     1642                                )
     1643                        );
     1644                } elseif ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
     1645                        $result['status'] = 'recommended';
     1646
     1647                        $result['label'] = __( 'The REST API encountered an unexpected result' );
     1648
     1649                        $result['description'] .= sprintf(
     1650                                '<p>%s</p>',
     1651                                sprintf(
     1652                                        // translators: %1$d: The HTTP response code returned. %2$s: The error message returned.
     1653                                        __( 'The REST API call gave the following unexpected result: (%1$d) %2$s.' ),
     1654                                        wp_remote_retrieve_response_code( $r ),
     1655                                        wp_remote_retrieve_body( $r )
     1656                                )
     1657                        );
     1658                } else {
     1659                        $json = json_decode( wp_remote_retrieve_body( $r ), true );
     1660
     1661                        if ( false !== $json && ! isset( $json['capabilities'] ) ) {
     1662                                $result['status'] = 'recommended';
     1663
     1664                                $result['label'] = __( 'The REST API did not behave correctly' );
     1665
     1666                                $result['description'] .= sprintf(
     1667                                        '<p>%s</p>',
     1668                                        __( 'The REST API did not process the \'context\' query parameter correctly.' )
     1669                                );
     1670                        }
     1671                }
     1672
     1673                return $result;
     1674        }
     1675
     1676        /**
     1677         * Return a set of tests that belong to the site status page.
     1678         *
     1679         * Each site status test is defined here, they may be `direct` tests, that run on page load,
     1680         * or `async` tests which will run later down the line via JavaScript calls to improve page
     1681         * performance and hopefully also user experiences.
     1682         *
     1683         * @return array
     1684         */
     1685        public static function get_tests() {
     1686                $tests = array(
     1687                        'direct' => array(
     1688                                'wordpress_version' => array(
     1689                                        'label' => __( 'WordPress Version' ),
     1690                                        'test'  => 'wordpress_version',
     1691                                ),
     1692                                'plugin_version'    => array(
     1693                                        'label' => __( 'Plugin Versions' ),
     1694                                        'test'  => 'plugin_version',
     1695                                ),
     1696                                'theme_version'     => array(
     1697                                        'label' => __( 'Theme Versions' ),
     1698                                        'test'  => 'theme_version',
     1699                                ),
     1700                                'php_version'       => array(
     1701                                        'label' => __( 'PHP Version' ),
     1702                                        'test'  => 'php_version',
     1703                                ),
     1704                                'sql_server'        => array(
     1705                                        'label' => __( 'Database Server version' ),
     1706                                        'test'  => 'sql_server',
     1707                                ),
     1708                                'php_extensions'    => array(
     1709                                        'label' => __( 'PHP Extensions' ),
     1710                                        'test'  => 'php_extensions',
     1711                                ),
     1712                                'utf8mb4_support'   => array(
     1713                                        'label' => __( 'MySQL utf8mb4 support' ),
     1714                                        'test'  => 'utf8mb4_support',
     1715                                ),
     1716                                'https_status'      => array(
     1717                                        'label' => __( 'HTTPS status' ),
     1718                                        'test'  => 'https_status',
     1719                                ),
     1720                                'ssl_support'       => array(
     1721                                        'label' => __( 'Secure communication' ),
     1722                                        'test'  => 'ssl_support',
     1723                                ),
     1724                                'scheduled_events'  => array(
     1725                                        'label' => __( 'Scheduled events' ),
     1726                                        'test'  => 'scheduled_events',
     1727                                ),
     1728                                'http_requests'     => array(
     1729                                        'label' => __( 'HTTP Requests' ),
     1730                                        'test'  => 'http_requests',
     1731                                ),
     1732                                'debug_enabled'     => array(
     1733                                        'label' => __( 'Debugging enabled' ),
     1734                                        'test'  => 'is_in_debug_mode',
     1735                                ),
     1736                        ),
     1737                        'async'  => array(
     1738                                'dotorg_communication' => array(
     1739                                        'label' => __( 'Communication with WordPress.org' ),
     1740                                        'test'  => 'dotorg_communication',
     1741                                ),
     1742                                'background_updates'   => array(
     1743                                        'label' => __( 'Background updates' ),
     1744                                        'test'  => 'background_updates',
     1745                                ),
     1746                                'loopback_requests'    => array(
     1747                                        'label' => __( 'Loopback request' ),
     1748                                        'test'  => 'loopback_requests',
     1749                                ),
     1750                        ),
     1751                );
     1752
     1753                // Conditionally include REST rules if the function for it exists.
     1754                if ( function_exists( 'rest_url' ) ) {
     1755                        $tests['direct']['rest_availability'] = array(
     1756                                'label' => __( 'REST API availability' ),
     1757                                'test'  => 'rest_availability',
     1758                        );
     1759                }
     1760
     1761                /**
     1762                 * Add or modify which site status tests are ran on a site.
     1763                 *
     1764                 * The site health is determined by a set of tests based on best practices from
     1765                 * both the WordPress Hosting Team, but also web standards in general.
     1766                 *
     1767                 * Some sites may not have the same requirements, for example the automatic update
     1768                 * checks may be handled by a host, and are therefore disabled in core.
     1769                 * Or maybe you want to introduce a new test, is caching enabled/disabled/stale for example.
     1770                 *
     1771                 * Test may be added either as direct, or asynchronous ones. Any test that may require some time
     1772                 * to complete should run asynchronously, to avoid extended loading periods within wp-admin.
     1773                 *
     1774                 * @since 5.2.0
     1775                 *
     1776                 * @param array $test_type {
     1777                 *     An associative arraay, where the `$test_type` is either `direct` or
     1778                 *     `async`, to declare if the test should run via AJAX calls after page load.
     1779                 *
     1780                 *     @type array $identifier {
     1781                 *         `$identifier` should be a unque identifier for the test that should run.
     1782                 *         Plugins and themes are encouraged to prefix test identifiers with their slug
     1783                 *         to avoid any collisions between tests.
     1784                 *
     1785                 *         @type string $label A friendly label for your test to identify it by.
     1786                 *         @type string $test  The ajax action to be called to perform the tests.
     1787                 *     }
     1788                 * }
     1789                 */
     1790                $tests = apply_filters( 'site_status_tests', $tests );
     1791
     1792                return $tests;
     1793        }
     1794
     1795        /**
     1796         * Add a class to the body HTML tag.
     1797         *
     1798         * Filters the `body_class` string for admin pages and adds our own class for easier styling.
     1799         *
     1800         * @param string $body_class
     1801         *
     1802         * @return string
     1803         */
     1804        public static function admin_body_class( $body_class ) {
     1805                $body_class .= ' site-health';
     1806
     1807                return $body_class;
     1808        }
     1809
     1810        /**
     1811         * Initiate the WP_Cron schedule test cases.
     1812         *
     1813         * @return void
     1814         */
     1815        public function wp_schedule_test_init() {
     1816                $this->schedules = wp_get_schedules();
     1817                $this->get_cron_tasks();
     1818        }
     1819
     1820        /**
     1821         * Populate our list of cron events and store them to a class-wide variable.
     1822         *
     1823         * Derived from `get_cron_events()` in WP Crontrol (https://plugins.svn.wordpress.org/wp-crontrol)
     1824         * by John Blackburn.
     1825         *
     1826         * @return void
     1827         */
     1828        private function get_cron_tasks() {
     1829                $cron_tasks = _get_cron_array();
     1830
     1831                if ( empty( $cron_tasks ) ) {
     1832                        $this->crons = new WP_Error( 'no_tasks', __( 'No scheduled events exist on this site.' ) );
     1833                        return;
     1834                }
     1835
     1836                $this->crons = array();
     1837
     1838                foreach ( $cron_tasks as $time => $cron ) {
     1839                        foreach ( $cron as $hook => $dings ) {
     1840                                foreach ( $dings as $sig => $data ) {
     1841
     1842                                        $this->crons[ "$hook-$sig-$time" ] = (object) array(
     1843                                                'hook'     => $hook,
     1844                                                'time'     => $time,
     1845                                                'sig'      => $sig,
     1846                                                'args'     => $data['args'],
     1847                                                'schedule' => $data['schedule'],
     1848                                                'interval' => isset( $data['interval'] ) ? $data['interval'] : null,
     1849                                        );
     1850
     1851                                }
     1852                        }
     1853                }
     1854        }
     1855
     1856        /**
     1857         * Check if any scheduled tasks have been missed.
     1858         *
     1859         * Returns a boolean value of `true` if a scheduled task has been missed and ends processing.
     1860         * If the list of crons is an instance of WP_Error, return the instance instead of a boolean value.
     1861         *
     1862         * @return bool|WP_Error
     1863         */
     1864        public function has_missed_cron() {
     1865                if ( is_wp_error( $this->crons ) ) {
     1866                        return $this->crons;
     1867                }
     1868
     1869                foreach ( $this->crons as $id => $cron ) {
     1870                        if ( ( $cron->time - time() ) < 0 ) {
     1871                                $this->last_missed_cron = $cron->hook;
     1872                                return true;
     1873                        }
     1874                }
     1875
     1876                return false;
     1877        }
     1878
     1879        /**
     1880         * Run a loopback test on our site.
     1881         *
     1882         * Loopbacks are what WordPress uses to communicate with it self to start up WP_Cron,
     1883         * scheduled posts, make sure plugin or theme edits dont cause site failures and similar.
     1884         *
     1885         * @return object
     1886         */
     1887        function can_perform_loopback() {
     1888                $cookies = wp_unslash( $_COOKIE );
     1889                $timeout = 10;
     1890                $headers = array(
     1891                        'Cache-Control' => 'no-cache',
     1892                );
     1893
     1894                // Include Basic auth in loopback requests.
     1895                if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
     1896                        $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
     1897                }
     1898
     1899                $url = admin_url();
     1900
     1901                $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout' ) );
     1902
     1903                if ( is_wp_error( $r ) ) {
     1904                        return (object) array(
     1905                                'status'  => 'critical',
     1906                                'message' => sprintf(
     1907                                        '%s<br>%s',
     1908                                        __( 'The loopback request to your site failed, this means features relying on them are not currently working as expected.' ),
     1909                                        sprintf(
     1910                                                // translators: %1$d: The HTTP response code. %2$s: The error message returned.
     1911                                                __( 'Error encountered: (%1$d) %2$s' ),
     1912                                                wp_remote_retrieve_response_code( $r ),
     1913                                                $r->get_error_message()
     1914                                        )
     1915                                ),
     1916                        );
     1917                }
     1918
     1919                if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
     1920                        return (object) array(
     1921                                'status'  => 'recommended',
     1922                                'message' => sprintf(
     1923                                        // translators: %d: The HTTP response code returned.
     1924                                        __( 'The loopback request returned an unexpected http status code, %d, it was not possible to determine if this will prevent features from working as expected.' ),
     1925                                        wp_remote_retrieve_response_code( $r )
     1926                                ),
     1927                        );
     1928                }
     1929
     1930                return (object) array(
     1931                        'status'  => 'good',
     1932                        'message' => __( 'The loopback request to your site completed successfully.' ),
     1933                );
     1934        }
     1935}
  • src/wp-admin/menu.php

    diff --git src/wp-admin/menu.php src/wp-admin/menu.php
    index f4490b7bf3..e76bb8a16d 100644
    if ( ! is_multisite() ) { 
    4747        unset( $cap );
    4848}
    4949
     50$submenu['index.php'][11] = array( __( 'Site Health' ), 'manage_options', 'site-health.php' );
     51
    5052$menu[4] = array( '', 'read', 'separator1', '', 'wp-menu-separator' );
    5153
    5254// $menu[5] = Posts
  • new file src/wp-admin/site-health-info.php

    diff --git src/wp-admin/site-health-info.php src/wp-admin/site-health-info.php
    new file mode 100644
    index 0000000000..dbd3375b50
    - +  
     1<?php
     2/**
     3 * Tools Administration Screen.
     4 *
     5 * @package WordPress
     6 * @subpackage Administration
     7 */
     8
     9/** WordPress Administration Bootstrap */
     10require_once( dirname( __FILE__ ) . '/admin.php' );
     11
     12if ( ! current_user_can( 'manage_options' ) ) {
     13        wp_die( __( 'Sorry, you do not have permission to access the debug data.' ), '', array( 'reponse' => 401 ) );
     14}
     15
     16wp_enqueue_style( 'site-health' );
     17wp_enqueue_script( 'site-health' );
     18
     19if ( ! class_exists( 'WP_Debug_Data' ) ) {
     20        require_once( ABSPATH . 'wp-admin/includes/class-wp-debug-data.php' );
     21}
     22if ( ! class_exists( 'WP_Site_Health' ) ) {
     23        require_once( ABSPATH . 'wp-admin/includes/class-wp-site-health.php' );
     24}
     25
     26$health_check_site_status = new WP_Site_Health();
     27
     28add_filter( 'admin_body_class', array( 'WP_Site_Health', 'admin_body_class' ) );
     29
     30require_once( ABSPATH . 'wp-admin/admin-header.php' );
     31?>
     32
     33        <div class="wrap health-check-header">
     34                <div class="title-section">
     35                        <h1>
     36                                <?php _ex( 'Site Health', 'Menu, Section and Page Title' ); ?>
     37                        </h1>
     38
     39                        <div id="progressbar" class="loading" data-pct="0" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-valuetext="<?php esc_attr_e( 'Site tests are running, please wait a moment.' ); ?>">
     40                                <svg width="100%" height="100%" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg">
     41                                        <circle r="90" cx="100" cy="100" fill="transparent" stroke-dasharray="565.48" stroke-dashoffset="0"></circle>
     42                                        <circle id="bar" r="90" cx="100" cy="100" fill="transparent" stroke-dasharray="565.48" stroke-dashoffset="0"></circle>
     43                                </svg>
     44                        </div>
     45                </div>
     46
     47                <nav class="tabs-wrapper" aria-label="<?php esc_attr_e( 'Secondary menu' ); ?>">
     48                        <a href="<?php echo esc_url( admin_url( 'site-health.php' ) ); ?>" class="tab">
     49                                <?php _e( 'Status' ); ?>
     50                        </a>
     51
     52                        <a href="<?php echo esc_url( admin_url( 'site-health.php?tab=debug' ) ); ?>" class="tab active" aria-current="true">
     53                                <?php _e( 'Info' ); ?>
     54                        </a>
     55                </nav>
     56
     57                <div class="wp-clearfix"></div>
     58        </div>
     59
     60        <div class="wrap health-check-body">
     61                <?php
     62                WP_Debug_Data::check_for_updates();
     63
     64                $info = WP_Debug_Data::debug_data();
     65                ?>
     66
     67                <h2>
     68                        <?php _e( 'Site Info' ); ?>
     69                </h2>
     70
     71                <p>
     72                        <?php _e( 'You can export the information on this page so it can be easily copied and pasted in support requests such as on the WordPress.org forums, or shared with your website / theme / plugin developers.' ); ?>
     73                </p>
     74
     75                <p>
     76                        <button type="button" class="button button-link health-check-toggle-copy-section">
     77                                <?php _e( 'Show options for copying this information' ); ?>
     78                        </button>
     79                </p>
     80
     81                <div class="system-information-copy-wrapper hidden">
     82                        <textarea id="system-information-default-copy-field" rows="10"><?php WP_Debug_Data::textarea_format( $info ); ?></textarea>
     83
     84                        <?php
     85                        if ( 'en_US' !== get_locale() && version_compare( get_bloginfo( 'version' ), '4.7', '>=' ) ) :
     86
     87                                $english_info = WP_Debug_Data::debug_data( 'en_US' );
     88                                ?>
     89                                <textarea id="system-information-english-copy-field" class="system-information-copy-wrapper" rows="10"><?php WP_Debug_Data::textarea_format( $english_info ); ?></textarea>
     90
     91                        <?php endif; ?>
     92
     93                        <div class="copy-button-wrapper">
     94                                <button type="button" class="button button-primary health-check-copy-field" data-copy-field="default"><?php _e( 'Copy to clipboard' ); ?></button>
     95                                <span class="copy-field-success" aria-hidden="true">Copied!</span>
     96                        </div>
     97                        <?php if ( 'en_US' !== get_locale() && version_compare( get_bloginfo( 'version' ), '4.7', '>=' ) ) : ?>
     98                                <div class="copy-button-wrapper">
     99                                        <button type="button" class="button health-check-copy-field" data-copy-field="english"><?php _e( 'Copy to clipboard (English)' ); ?></button>
     100                                        <span class="copy-field-success" aria-hidden="true">Copied!</span>
     101                                </div>
     102                        <?php endif; ?>
     103                </div>
     104
     105                <dl id="health-check-debug" role="presentation" class="health-check-accordion">
     106
     107                        <?php
     108                        foreach ( $info as $section => $details ) {
     109                                if ( ! isset( $details['fields'] ) || empty( $details['fields'] ) ) {
     110                                        continue;
     111                                }
     112                                ?>
     113                                <dt role="heading" aria-level="3">
     114                                        <button aria-expanded="false" class="health-check-accordion-trigger" aria-controls="health-check-accordion-block-<?php echo esc_attr( $section ); ?>" id="health-check-accordion-heading-<?php echo esc_attr( $section ); ?>" type="button">
     115                        <span class="title">
     116                                <?php echo esc_html( $details['label'] ); ?>
     117
     118                                <?php if ( isset( $details['show_count'] ) && $details['show_count'] ) : ?>
     119                                        <?php printf( '(%d)', count( $details['fields'] ) ); ?>
     120                                <?php endif; ?>
     121                        </span>
     122                                                <span class="icon"></span>
     123                                        </button>
     124                                </dt>
     125
     126                                <dd id="health-check-accordion-block-<?php echo esc_attr( $section ); ?>" role="region" aria-labelledby="health-check-accordion-heading-<?php echo esc_attr( $section ); ?>" class="health-check-accordion-panel" hidden="hidden">
     127                                        <?php
     128                                        if ( isset( $details['description'] ) && ! empty( $details['description'] ) ) {
     129                                                printf(
     130                                                        '<p>%s</p>',
     131                                                        wp_kses(
     132                                                                $details['description'],
     133                                                                array(
     134                                                                        'a'      => array(
     135                                                                                'href' => true,
     136                                                                        ),
     137                                                                        'strong' => true,
     138                                                                        'em'     => true,
     139                                                                )
     140                                                        )
     141                                                );
     142                                        }
     143                                        ?>
     144                                        <table class="widefat striped health-check-table">
     145                                                <tbody>
     146                                                <?php
     147                                                foreach ( $details['fields'] as $field ) {
     148                                                        if ( is_array( $field['value'] ) ) {
     149                                                                $values = '';
     150                                                                foreach ( $field['value'] as $name => $value ) {
     151                                                                        $values .= sprintf(
     152                                                                                '<li>%s: %s</li>',
     153                                                                                esc_html( $name ),
     154                                                                                esc_html( $value )
     155                                                                        );
     156                                                                }
     157                                                        } else {
     158                                                                $values = esc_html( $field['value'] );
     159                                                        }
     160
     161                                                        printf(
     162                                                                '<tr><td>%s</td><td>%s</td></tr>',
     163                                                                esc_html( $field['label'] ),
     164                                                                $values
     165                                                        );
     166                                                }
     167                                                ?>
     168                                                </tbody>
     169                                        </table>
     170                                </dd>
     171                        <?php } ?>
     172                </dl>
     173        </div>
     174
     175<?php
     176include( ABSPATH . 'wp-admin/admin-footer.php' );
  • new file src/wp-admin/site-health.php

    diff --git src/wp-admin/site-health.php src/wp-admin/site-health.php
    new file mode 100644
    index 0000000000..5239ba3fd7
    - +  
     1<?php
     2/**
     3 * Tools Administration Screen.
     4 *
     5 * @package WordPress
     6 * @subpackage Administration
     7 */
     8
     9if ( isset( $_GET['tab'] ) && 'debug' === $_GET['tab'] ) {
     10        require_once( dirname( __FILE__ ) . '/site-health-info.php' );
     11        return;
     12}
     13
     14/** WordPress Administration Bootstrap */
     15require_once( dirname( __FILE__ ) . '/admin.php' );
     16
     17if ( ! current_user_can( 'manage_options' ) ) {
     18        wp_die( __( 'Sorry, you do not have permission to access site health information.' ), '', array( 'reponse' => 401 ) );
     19}
     20
     21wp_enqueue_style( 'site-health' );
     22wp_enqueue_script( 'site-health' );
     23
     24if ( ! class_exists( 'WP_Site_Health' ) ) {
     25        require_once( ABSPATH . 'wp-admin/includes/class-wp-site-health.php' );
     26}
     27
     28add_filter( 'admin_body_class', array( 'WP_Site_Health', 'admin_body_class' ) );
     29
     30$health_check_site_status = new WP_Site_Health();
     31
     32require_once( ABSPATH . 'wp-admin/admin-header.php' );
     33?>
     34
     35<div class="wrap health-check-header">
     36        <div class="title-section">
     37                <h1>
     38                        <?php _ex( 'Site Health', 'Menu, Section and Page Title' ); ?>
     39                </h1>
     40
     41                <div id="progressbar" class="loading" data-pct="0" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-valuetext="<?php esc_attr_e( 'Site tests are running, please wait a moment.' ); ?>">
     42                        <svg width="100%" height="100%" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg">
     43                                <circle r="90" cx="100" cy="100" fill="transparent" stroke-dasharray="565.48" stroke-dashoffset="0"></circle>
     44                                <circle id="bar" r="90" cx="100" cy="100" fill="transparent" stroke-dasharray="565.48" stroke-dashoffset="0"></circle>
     45                        </svg>
     46                </div>
     47        </div>
     48
     49        <nav class="tabs-wrapper" aria-label="<?php esc_attr_e( 'Secondary menu' ); ?>">
     50                <a href="<?php echo esc_url( admin_url( 'site-health.php' ) ); ?>" class="tab active" aria-current="true">
     51                        <?php _e( 'Status' ); ?>
     52                </a>
     53
     54                <a href="<?php echo esc_url( admin_url( 'site-health.php?tab=debug' ) ); ?>" class="tab">
     55                        <?php _e( 'Info' ); ?>
     56                </a>
     57        </nav>
     58
     59        <div class="wp-clearfix"></div>
     60</div>
     61
     62<div class="wrap health-check-body">
     63        <div class="site-status-all-clear hide">
     64                <p class="icon">
     65                        <span class="dashicons dashicons-yes"></span>
     66                </p>
     67
     68                <p class="encouragement">
     69                        <?php _e( 'Great job!' ); ?>
     70                </p>
     71
     72                <p>
     73                        <?php _e( 'Everything is running smoothly here.' ); ?>
     74                </p>
     75        </div>
     76
     77        <div class="site-status-has-issues">
     78                <h2>
     79                        <?php _e( 'Site Health Status' ); ?>
     80                </h2>
     81
     82                <div class="issues-wrapper" id="health-check-issues-critical">
     83                        <h3>
     84                                <span class="issue-count">0</span> <?php _e( 'Critical issues' ); ?>
     85                        </h3>
     86
     87                        <dl id="health-check-site-status-critical" role="presentation" class="health-check-accordion issues"></dl>
     88                </div>
     89
     90                <div class="issues-wrapper" id="health-check-issues-recommended">
     91                        <h3>
     92                                <span class="issue-count">0</span> <?php _e( 'Recommended improvements' ); ?>
     93                        </h3>
     94
     95                        <dl id="health-check-site-status-recommended" role="presentation" class="health-check-accordion issues"></dl>
     96                </div>
     97        </div>
     98
     99        <div class="view-more">
     100                <button type="button" class="button button-link site-health-view-passed" aria-expanded="false">
     101                        <?php _e( 'Show passed tests' ); ?>
     102                </button>
     103        </div>
     104
     105        <div class="issues-wrapper hidden" id="health-check-issues-good">
     106                <h3>
     107                        <span class="issue-count">0</span> <?php _e( 'Items with no issues detected' ); ?>
     108                </h3>
     109
     110                <dl id="health-check-site-status-good" role="presentation" class="health-check-accordion issues"></dl>
     111        </div>
     112</div>
     113
     114<script id="issue-template" type="text/template">
     115        <dt role="heading" aria-level="4">
     116                <button aria-expanded="false" class="health-check-accordion-trigger" aria-controls="health-check-accordion-block-<%= test %>" id="health-check-accordion-heading-<%= test %>" type="button">
     117                        <span class="title"><%= label %></span>
     118                        <span class="badge <%= badge.color %>"><%= badge.label %></span>
     119                        <span class="icon"></span>
     120                </button>
     121        </dt>
     122        <dd id="health-check-accordion-block-<%= test %>" aria-labelledby="health-check-accordion-heading-<%= test %>" role="region" class="health-check-accordion-panel" hidden="hidden">
     123                <%= description %>
     124                <div class="actions">
     125                        <p><%= actions %></p>
     126                </div>
     127        </dd>
     128</script>
     129
     130<?php
     131include( ABSPATH . 'wp-admin/admin-footer.php' );
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index a3ca60e75e..8f971b105e 100644
    function wp_default_scripts( &$scripts ) { 
    16881688                        )
    16891689                );
    16901690
     1691                $scripts->add( 'site-health', "/wp-admin/js/site-health$suffix.js", array( 'jquery', 'underscore', 'wp-a11y' ), false, 1 );
     1692
    16911693                $scripts->add( 'updates', "/wp-admin/js/updates$suffix.js", array( 'jquery', 'wp-util', 'wp-a11y' ), false, 1 );
    16921694                did_action( 'init' ) && $scripts->localize(
    16931695                        'updates',
    function wp_default_styles( &$styles ) { 
    19331935        $styles->add( 'site-icon', "/wp-admin/css/site-icon$suffix.css" );
    19341936        $styles->add( 'l10n', "/wp-admin/css/l10n$suffix.css" );
    19351937        $styles->add( 'code-editor', "/wp-admin/css/code-editor$suffix.css", array( 'wp-codemirror' ) );
     1938        $styles->add( 'site-health', "/wp-admin/css/site-health$suffix.css" );
    19361939
    19371940        $styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n' ) );
    19381941