Plugin Directory

Changeset 3492634


Ignore:
Timestamp:
03/27/2026 12:15:52 PM (19 hours ago)
Author:
Marc4
Message:

v2.1.0

Location:
security-hardener
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • security-hardener/assets/blueprints/blueprint.json

    r3487909 r3492634  
    1616      "pluginZipFile": {
    1717        "resource": "url",
    18         "url": "https://downloads.wordpress.org/plugin/security-hardener.2.0.2.zip"
     18        "url": "https://downloads.wordpress.org/plugin/security-hardener.2.1.0.zip"
    1919      },
    2020      "options": {
  • security-hardener/readme.txt

    r3487909 r3492634  
    55Tested up to: 6.9
    66Requires PHP: 8.2
    7 Stable tag: 2.0.2
     7Stable tag: 2.1.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    163163
    164164== Changelog ==
     165
     166= 2.1.0 - 2026-03-26 =
     167* Added: Interactive hardening checklist — users can mark each recommendation as done.
     168* Improved: File permissions check moved from floating admin notice into the settings page.
     169* Fixed: "Enable preload" description now clarifies it only adds the preload directive to the HSTS header.
     170* Fixed: "Clean wp_head" description no longer uses HTML entities that rendered as literal text in some contexts.
     171* Fixed: wp-includes recommendation text no longer implies a code snippet follows.
    165172
    166173= 2.0.2 - 2026-03-21 =
  • security-hardener/security-hardener.php

    r3487909 r3492634  
    44Plugin URI: https://wordpress.org/plugins/security-hardener/
    55Description: Basic hardening: secure headers, disable XML-RPC/pingbacks, hide version, block user enumeration, generic login errors, and IP-based rate limiting.
    6 Version: 2.0.2
     6Version: 2.1.0
    77Requires at least: 6.9
    88Tested up to: 6.9
     
    2020
    2121// Plugin constants
    22 define( 'WPSH_VERSION', '2.0.2' );
     22define( 'WPSH_VERSION', '2.1.0' );
    2323define( 'WPSH_FILE', __FILE__ );
    2424define( 'WPSH_BASENAME', plugin_basename( __FILE__ ) );
     
    3535         */
    3636        const OPTION_NAME = 'wpsh_options';
     37
     38        /**
     39         * Checklist state option name in database
     40         */
     41        const CHECKLIST_OPTION = 'wpsh_checklist';
    3742
    3843        /**
     
    140145                add_action( 'admin_notices', array( $this, 'show_admin_notices' ) );
    141146                add_filter( 'plugin_action_links_' . WPSH_BASENAME, array( $this, 'add_settings_link' ) );
     147                add_action( 'wp_ajax_wpsh_toggle_checklist', array( $this, 'ajax_toggle_checklist' ) );
     148                add_action( 'wp_ajax_wpsh_reset_checklist', array( $this, 'ajax_reset_checklist' ) );
    142149            }
    143150        }
     
    808815                .wpsh-recommendations li{margin-bottom:4px;}
    809816                .wpsh-save-bar{margin:20px 0 4px;}
     817                .wpsh-cl-header{display:flex;align-items:center;justify-content:space-between;margin-top:24px;}
     818                .wpsh-cl-progress{display:flex;align-items:center;gap:8px;}
     819                .wpsh-cl-bar{height:6px;width:120px;background:#c3c4c7;border-radius:3px;overflow:hidden;}
     820                .wpsh-cl-bar-fill{height:100%;background:#2271b1;border-radius:3px;transition:width .2s;}
     821                .wpsh-cl-pct{font-size:12px;color:#646970;white-space:nowrap;}
     822                .wpsh-cl-item{display:flex;align-items:flex-start;gap:10px;padding:10px 16px;border-bottom:1px solid #f0f0f1;cursor:pointer;transition:background .1s;}
     823                .wpsh-cl-item:last-child{border-bottom:none;}
     824                .wpsh-cl-item:hover{background:#f6f7f7;}
     825                .wpsh-cl-item:focus{outline:2px solid #2271b1;outline-offset:-2px;}
     826                .wpsh-cl-item.wpsh-cl-done{background:#f0f6f0;}
     827                .wpsh-cl-check{flex-shrink:0;width:18px;height:18px;border:1.5px solid #c3c4c7;border-radius:3px;margin-top:1px;display:flex;align-items:center;justify-content:center;transition:all .15s;}
     828                .wpsh-cl-item.wpsh-cl-done .wpsh-cl-check{background:#2271b1;border-color:#2271b1;}
     829                .wpsh-cl-check svg{display:none;}
     830                .wpsh-cl-item.wpsh-cl-done .wpsh-cl-check svg{display:block;}
     831                .wpsh-cl-label{font-size:13px;line-height:1.45;color:#1d2327;}
     832                .wpsh-cl-item.wpsh-cl-done .wpsh-cl-label{color:#646970;text-decoration:line-through;text-decoration-color:#c3c4c7;}
    810833                @media(max-width:1200px){.wpsh-grid{grid-template-columns:repeat(2,minmax(0,1fr));}}
    811834                @media(max-width:782px){.wpsh-grid{grid-template-columns:minmax(0,1fr);}}
     
    884907
    885908        /**
     909         * AJAX handler — toggle a single checklist item.
     910         *
     911         * Expects POST: nonce, item_id (int 0-13), checked (0|1).
     912         */
     913        public function ajax_toggle_checklist(): void {
     914            check_ajax_referer( 'wpsh_checklist_nonce', 'nonce' );
     915
     916            if ( ! current_user_can( 'manage_options' ) ) {
     917                wp_send_json_error( 'Unauthorized', 403 );
     918            }
     919
     920            $item_id = isset( $_POST['item_id'] ) ? absint( $_POST['item_id'] ) : null;
     921            $checked = isset( $_POST['checked'] ) && '1' === $_POST['checked'];
     922
     923            if ( null === $item_id || $item_id > 13 ) {
     924                wp_send_json_error( 'Invalid item', 400 );
     925            }
     926
     927            $state = get_option( self::CHECKLIST_OPTION, [] );
     928
     929            if ( $checked ) {
     930                $state[ $item_id ] = true;
     931            } else {
     932                unset( $state[ $item_id ] );
     933            }
     934
     935            update_option( self::CHECKLIST_OPTION, $state, false );
     936            wp_send_json_success( [ 'done' => count( $state ), 'total' => 14 ] );
     937        }
     938
     939        /**
     940         * AJAX handler — reset all checklist items.
     941         *
     942         * Expects POST: nonce.
     943         */
     944        public function ajax_reset_checklist(): void {
     945            check_ajax_referer( 'wpsh_checklist_nonce', 'nonce' );
     946
     947            if ( ! current_user_can( 'manage_options' ) ) {
     948                wp_send_json_error( 'Unauthorized', 403 );
     949            }
     950
     951            update_option( self::CHECKLIST_OPTION, [], false );
     952            wp_send_json_success( [ 'done' => 0, 'total' => 14 ] );
     953        }
     954
     955        /**
     956         * Render the interactive hardening recommendations checklist.
     957         *
     958         * State is persisted in wpsh_checklist via AJAX (no page reload needed).
     959         */
     960        private function render_checklist(): void {
     961            $state = get_option( self::CHECKLIST_OPTION, [] );
     962            $done  = count( $state );
     963            $total = 14;
     964            $pct   = $total > 0 ? round( $done / $total * 100 ) : 0;
     965
     966            $items = [
     967                __( 'Use strong passwords and enable two-factor authentication', 'security-hardener' ),
     968                __( 'Keep WordPress, themes, and plugins updated', 'security-hardener' ),
     969                __( 'Use HTTPS (SSL/TLS) for your entire site', 'security-hardener' ),
     970                __( 'Regular backups stored off-site', 'security-hardener' ),
     971                __( 'Limit login attempts at the server/firewall level', 'security-hardener' ),
     972                __( 'Use security plugins for malware scanning', 'security-hardener' ),
     973                __( 'Restrict file permissions (directories: 755, files: 644)', 'security-hardener' ),
     974                __( 'Consider using a Web Application Firewall (WAF)', 'security-hardener' ),
     975                __( 'Protect the wp-admin directory with an additional HTTP authentication layer (BasicAuth)', 'security-hardener' ),
     976                __( 'Change the default database table prefix from wp_ to a custom value', 'security-hardener' ),
     977                __( 'Rename the default admin account to a non-obvious username', 'security-hardener' ),
     978                __( 'Restrict database user privileges to SELECT, INSERT, UPDATE and DELETE only', 'security-hardener' ),
     979                __( 'Protect wp-config.php by moving it one directory above the WordPress root or restricting access via .htaccess', 'security-hardener' ),
     980                __( 'Block direct access to files in wp-includes/ via .htaccess rules — see the WordPress Hardening Guide for the full snippet.', 'security-hardener' ),
     981            ];
     982
     983            $nonce = wp_create_nonce( 'wpsh_checklist_nonce' );
     984            ?>
     985            <hr style="margin: 24px 0;">
     986
     987            <div class="wpsh-cl-header">
     988                <h2 style="margin:0;"><?php esc_html_e( 'Additional Hardening Recommendations', 'security-hardener' ); ?></h2>
     989                <div class="wpsh-cl-progress">
     990                    <div class="wpsh-cl-bar">
     991                        <div class="wpsh-cl-bar-fill" id="wpsh-bar-fill" style="width:<?php echo absint( $pct ); ?>%"></div>
     992                    </div>
     993                    <span class="wpsh-cl-pct" id="wpsh-cl-pct">
     994                        <?php
     995                        printf(
     996                            /* translators: 1: completed items, 2: total items */
     997                            esc_html__( '%1$d / %2$d done', 'security-hardener' ),
     998                            absint( $done ),
     999                            absint( $total )
     1000                        );
     1001                        ?>
     1002                    </span>
     1003                </div>
     1004            </div>
     1005
     1006            <div class="wpsh-card" style="margin-top:12px;" id="wpsh-checklist">
     1007                <?php foreach ( $items as $id => $label ) : ?>
     1008                    <div class="wpsh-cl-item<?php echo isset( $state[ $id ] ) ? ' wpsh-cl-done' : ''; ?>"
     1009                        data-id="<?php echo absint( $id ); ?>"
     1010                        data-nonce="<?php echo esc_attr( $nonce ); ?>"
     1011                        role="checkbox"
     1012                        aria-checked="<?php echo isset( $state[ $id ] ) ? 'true' : 'false'; ?>"
     1013                        tabindex="0">
     1014                        <div class="wpsh-cl-check" aria-hidden="true">
     1015                            <svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
     1016                                <path d="M1 4l3 3 5-6" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
     1017                            </svg>
     1018                        </div>
     1019                        <span class="wpsh-cl-label"><?php echo esc_html( $label ); ?></span>
     1020                    </div>
     1021                <?php endforeach; ?>
     1022            </div>
     1023
     1024            <div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px;">
     1025                <p style="margin:0;font-size:12px;color:#646970;">
     1026                    <?php esc_html_e( 'Your progress is saved automatically.', 'security-hardener' ); ?>
     1027                </p>
     1028                <button type="button" id="wpsh-cl-reset" class="button-link"
     1029                    data-nonce="<?php echo esc_attr( $nonce ); ?>"
     1030                    style="font-size:12px;color:#2271b1;">
     1031                    <?php esc_html_e( 'Reset all', 'security-hardener' ); ?>
     1032                </button>
     1033            </div>
     1034
     1035            <p style="margin-top:16px;">
     1036                <?php
     1037                printf(
     1038                    /* translators: %s: URL to WordPress hardening guide */
     1039                    wp_kses_post( __( 'For more information, see the official <a href="%s" target="_blank">WordPress Hardening Guide</a>.', 'security-hardener' ) ),
     1040                    'https://developer.wordpress.org/advanced-administration/security/hardening/'
     1041                );
     1042                ?>
     1043            </p>
     1044
     1045            <script>
     1046            (function() {
     1047                var ajaxUrl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
     1048                var total   = <?php echo absint( $total ); ?>;
     1049
     1050                function updateProgress( done ) {
     1051                    var pct = total > 0 ? Math.round( done / total * 100 ) : 0;
     1052                    document.getElementById( 'wpsh-bar-fill' ).style.width = pct + '%';
     1053                    document.getElementById( 'wpsh-cl-pct' ).textContent    = done + ' / ' + total + ' done';
     1054                }
     1055
     1056                function sendToggle( id, checked, nonce ) {
     1057                    var fd = new FormData();
     1058                    fd.append( 'action',  'wpsh_toggle_checklist' );
     1059                    fd.append( 'nonce',   nonce );
     1060                    fd.append( 'item_id', id );
     1061                    fd.append( 'checked', checked ? '1' : '0' );
     1062                    fetch( ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' } )
     1063                        .then( function( r ) { return r.json(); } )
     1064                        .then( function( data ) { if ( data.success ) updateProgress( data.data.done ); } );
     1065                }
     1066
     1067                document.querySelectorAll( '.wpsh-cl-item' ).forEach( function( el ) {
     1068                    function toggle() {
     1069                        var done    = el.classList.toggle( 'wpsh-cl-done' );
     1070                        var checked = el.classList.contains( 'wpsh-cl-done' );
     1071                        el.setAttribute( 'aria-checked', checked ? 'true' : 'false' );
     1072                        sendToggle( el.dataset.id, checked, el.dataset.nonce );
     1073                    }
     1074                    el.addEventListener( 'click', toggle );
     1075                    el.addEventListener( 'keydown', function( e ) {
     1076                        if ( e.key === ' ' || e.key === 'Enter' ) { e.preventDefault(); toggle(); }
     1077                    } );
     1078                } );
     1079
     1080                var resetBtn = document.getElementById( 'wpsh-cl-reset' );
     1081                if ( resetBtn ) {
     1082                    resetBtn.addEventListener( 'click', function() {
     1083                        var fd = new FormData();
     1084                        fd.append( 'action', 'wpsh_reset_checklist' );
     1085                        fd.append( 'nonce',  resetBtn.dataset.nonce );
     1086                        fetch( ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' } )
     1087                            .then( function( r ) { return r.json(); } )
     1088                            .then( function( data ) {
     1089                                if ( data.success ) {
     1090                                    document.querySelectorAll( '.wpsh-cl-item' ).forEach( function( el ) {
     1091                                        el.classList.remove( 'wpsh-cl-done' );
     1092                                        el.setAttribute( 'aria-checked', 'false' );
     1093                                    } );
     1094                                    updateProgress( 0 );
     1095                                }
     1096                            } );
     1097                    } );
     1098                }
     1099            })();
     1100            </script>
     1101            <?php
     1102        }
     1103
     1104        /**
    8861105         * Render settings page
    8871106         */
     
    10661285                                    sprintf(
    10671286                                        /* translators: %s: URL to HSTS preload list */
    1068                                         wp_kses_post( __( 'Submit to the <a href="%s" target="_blank">HSTS Preload List</a>. Requires 1 year max-age.', 'security-hardener' ) ),
     1287                                        wp_kses_post( __( 'Adds the preload directive to the HSTS header. Required before submitting manually to <a href="%s" target="_blank">hstspreload.org</a>.', 'security-hardener' ) ),
    10691288                                        'https://hstspreload.org/'
    10701289                                    )
     
    10891308                                    'clean_head',
    10901309                                    __( 'Clean wp_head', 'security-hardener' ),
    1091                                     __( 'Removes RSD link, Windows Live Writer manifest, shortlink, and emoji scripts from &lt;head&gt;.', 'security-hardener' )
     1310                                    __( 'Removes RSD link, Windows Live Writer manifest, shortlink, and emoji scripts from the page head.', 'security-hardener' )
    10921311                                );
    10931312                                $this->render_toggle_row(
     
    11161335                <?php $this->render_security_logs(); ?>
    11171336
    1118                 <hr style="margin: 24px 0;">
    1119 
    1120                 <h2><?php esc_html_e( 'Additional Hardening Recommendations', 'security-hardener' ); ?></h2>
    1121                 <ul class="wpsh-recommendations" style="list-style: disc; padding-left: 20px;">
    1122                     <li><?php esc_html_e( 'Use strong passwords and enable two-factor authentication', 'security-hardener' ); ?></li>
    1123                     <li><?php esc_html_e( 'Keep WordPress, themes, and plugins updated', 'security-hardener' ); ?></li>
    1124                     <li><?php esc_html_e( 'Use HTTPS (SSL/TLS) for your entire site', 'security-hardener' ); ?></li>
    1125                     <li><?php esc_html_e( 'Regular backups stored off-site', 'security-hardener' ); ?></li>
    1126                     <li><?php esc_html_e( 'Limit login attempts at the server/firewall level', 'security-hardener' ); ?></li>
    1127                     <li><?php esc_html_e( 'Use security plugins for malware scanning', 'security-hardener' ); ?></li>
    1128                     <li><?php esc_html_e( 'Restrict file permissions (directories: 755, files: 644)', 'security-hardener' ); ?></li>
    1129                     <li><?php esc_html_e( 'Consider using a Web Application Firewall (WAF)', 'security-hardener' ); ?></li>
    1130                     <li><?php esc_html_e( 'Protect the wp-admin directory with an additional HTTP authentication layer (BasicAuth)', 'security-hardener' ); ?></li>
    1131                     <li><?php esc_html_e( 'Change the default database table prefix from wp_ to a custom value', 'security-hardener' ); ?></li>
    1132                     <li><?php esc_html_e( 'Rename the default admin account to a non-obvious username', 'security-hardener' ); ?></li>
    1133                     <li><?php esc_html_e( 'Restrict database user privileges to SELECT, INSERT, UPDATE and DELETE only', 'security-hardener' ); ?></li>
    1134                     <li><?php esc_html_e( 'Protect wp-config.php by moving it one directory above the WordPress root or restricting access via .htaccess', 'security-hardener' ); ?></li>
    1135                     <li><?php esc_html_e( 'Block direct access to files in wp-includes/ by adding the following rules to your .htaccess file (outside the WordPress tags).', 'security-hardener' ); ?></li>
    1136                 </ul>
    1137                 <p>
    1138                     <?php
    1139                     printf(
    1140                         /* translators: %s: URL to WordPress hardening guide */
    1141                         wp_kses_post( __( 'For more information, see the official <a href="%s" target="_blank">WordPress Hardening Guide</a>.', 'security-hardener' ) ),
    1142                         'https://developer.wordpress.org/advanced-administration/security/hardening/'
    1143                     );
    1144                     ?>
    1145                 </p>
     1337                <?php $this->render_file_permissions(); ?>
     1338
     1339                <?php $this->render_checklist(); ?>
    11461340
    11471341            </div><!-- .wrap -->
     
    12211415            }
    12221416
    1223             // Check file permissions
    1224             $this->check_file_permissions_notice();
    1225         }
    1226 
    1227         /**
    1228          * Check file permissions and show notice
    1229          */
    1230         private function check_file_permissions_notice(): void {
    1231             // Only show on plugin settings page
    1232             $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
    1233             if ( ! $screen || 'settings_page_security-hardener' !== $screen->id ) {
    1234                 return;
    1235             }
    1236 
     1417            // Check file permissions is now rendered inline in render_settings_page()
     1418        }
     1419
     1420        /**
     1421         * Render file permissions check as an inline section on the settings page.
     1422         *
     1423         * Shows a success notice when all paths are correct, or a table listing
     1424         * only the paths with issues when problems are found.
     1425         */
     1426        private function render_file_permissions(): void {
    12371427            if ( ! function_exists( 'get_home_path' ) ) {
    12381428                require_once ABSPATH . 'wp-admin/includes/file.php';
     
    12431433                array(
    12441434                    'path'        => ABSPATH . 'wp-config.php',
    1245                     'type'        => 'file',
    12461435                    'recommended' => array( '0600', '0640', '0644' ),
    12471436                    'label'       => 'wp-config.php',
     
    12491438                array(
    12501439                    'path'        => get_home_path(),
    1251                     'type'        => 'dir',
    12521440                    'recommended' => array( '0755', '0750' ),
    12531441                    'label'       => __( 'WordPress root directory', 'security-hardener' ),
     
    12551443                array(
    12561444                    'path'        => WP_CONTENT_DIR,
    1257                     'type'        => 'dir',
    12581445                    'recommended' => array( '0755', '0750' ),
    12591446                    'label'       => 'wp-content',
     
    12611448                array(
    12621449                    'path'        => $upload_dir['basedir'],
    1263                     'type'        => 'dir',
    12641450                    'recommended' => array( '0755', '0750' ),
    12651451                    'label'       => __( 'Uploads directory', 'security-hardener' ),
     
    12731459                    continue;
    12741460                }
    1275 
    12761461                $perms = substr( sprintf( '%o', fileperms( $check['path'] ) ), -4 );
    1277 
    12781462                if ( ! in_array( $perms, $check['recommended'], true ) ) {
    1279                     $issues[] = sprintf(
    1280                         /* translators: 1: file/directory name, 2: current permissions, 3: recommended permissions */
    1281                         __( '%1$s has permissions %2$s (recommended: %3$s)', 'security-hardener' ),
    1282                         '<code>' . esc_html( $check['label'] ) . '</code>',
    1283                         '<code>' . esc_html( $perms ) . '</code>',
    1284                         '<code>' . esc_html( implode( ', ', $check['recommended'] ) ) . '</code>'
     1463                    $issues[] = array(
     1464                        'label'       => $check['label'],
     1465                        'current'     => $perms,
     1466                        'recommended' => implode( ', ', $check['recommended'] ),
    12851467                    );
    12861468                }
    12871469            }
    1288 
    1289             if ( ! empty( $issues ) ) {
     1470            ?>
     1471            <hr style="margin:24px 0;">
     1472            <h2><?php esc_html_e( 'File Permissions', 'security-hardener' ); ?></h2>
     1473
     1474            <?php if ( empty( $issues ) ) : ?>
     1475                <div style="display:flex;align-items:center;gap:8px;padding:10px 14px;background:#f0f6f0;border:1px solid #c3c4c7;border-radius:4px;font-size:13px;color:#1d2327;">
     1476                    <span style="width:8px;height:8px;border-radius:50%;background:#00a32a;flex-shrink:0;display:inline-block;"></span>
     1477                    <?php esc_html_e( 'All checked paths have correct file permissions.', 'security-hardener' ); ?>
     1478                </div>
     1479            <?php else : ?>
     1480                <div style="border:1px solid #c3c4c7;border-radius:4px;overflow:hidden;">
     1481                    <table class="wp-list-table widefat fixed striped">
     1482                        <thead>
     1483                            <tr>
     1484                                <th><?php esc_html_e( 'Path', 'security-hardener' ); ?></th>
     1485                                <th style="width:120px;"><?php esc_html_e( 'Current', 'security-hardener' ); ?></th>
     1486                                <th style="width:200px;"><?php esc_html_e( 'Recommended', 'security-hardener' ); ?></th>
     1487                                <th style="width:140px;"><?php esc_html_e( 'Status', 'security-hardener' ); ?></th>
     1488                            </tr>
     1489                        </thead>
     1490                        <tbody>
     1491                            <?php foreach ( $issues as $issue ) : ?>
     1492                                <tr>
     1493                                    <td><code><?php echo esc_html( $issue['label'] ); ?></code></td>
     1494                                    <td><code><?php echo esc_html( $issue['current'] ); ?></code></td>
     1495                                    <td><code><?php echo esc_html( $issue['recommended'] ); ?></code></td>
     1496                                    <td>
     1497                                        <span style="display:inline-flex;align-items:center;gap:5px;">
     1498                                            <span style="width:7px;height:7px;border-radius:50%;background:#d63638;display:inline-block;flex-shrink:0;"></span>
     1499                                            <?php esc_html_e( 'Too permissive', 'security-hardener' ); ?>
     1500                                        </span>
     1501                                    </td>
     1502                                </tr>
     1503                            <?php endforeach; ?>
     1504                        </tbody>
     1505                    </table>
     1506                </div>
     1507            <?php endif; ?>
     1508
     1509            <p style="font-size:12px;color:#646970;margin-top:8px;">
     1510                <?php
     1511                printf(
     1512                    /* translators: %s: URL to file permissions documentation */
     1513                    wp_kses_post( __( 'Learn more about <a href="%s" target="_blank">WordPress file permissions</a>.', 'security-hardener' ) ),
     1514                    'https://developer.wordpress.org/advanced-administration/server/file-permissions/'
     1515                );
    12901516                ?>
    1291                 <div class="notice notice-warning">
    1292                     <p><strong><?php esc_html_e( 'File Permission Issues Detected:', 'security-hardener' ); ?></strong></p>
    1293                     <ul style="list-style: disc; padding-left: 20px;">
    1294                         <?php foreach ( $issues as $issue ) : ?>
    1295                             <li><?php echo wp_kses_post( $issue ); ?></li>
    1296                         <?php endforeach; ?>
    1297                     </ul>
    1298                     <p>
    1299                         <?php
    1300                         printf(
    1301                             /* translators: %s: URL to file permissions documentation */
    1302                             wp_kses_post( __( 'Learn more about <a href="%s" target="_blank">WordPress file permissions</a>.', 'security-hardener' ) ),
    1303                             'https://developer.wordpress.org/advanced-administration/server/file-permissions/'
    1304                         );
    1305                         ?>
    1306                     </p>
    1307                 </div>
    1308                 <?php
    1309             }
     1517            </p>
     1518            <?php
    13101519        }
    13111520
  • security-hardener/trunk/readme.txt

    r3487909 r3492634  
    55Tested up to: 6.9
    66Requires PHP: 8.2
    7 Stable tag: 2.0.2
     7Stable tag: 2.1.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    163163
    164164== Changelog ==
     165
     166= 2.1.0 - 2026-03-26 =
     167* Added: Interactive hardening checklist — users can mark each recommendation as done.
     168* Improved: File permissions check moved from floating admin notice into the settings page.
     169* Fixed: "Enable preload" description now clarifies it only adds the preload directive to the HSTS header.
     170* Fixed: "Clean wp_head" description no longer uses HTML entities that rendered as literal text in some contexts.
     171* Fixed: wp-includes recommendation text no longer implies a code snippet follows.
    165172
    166173= 2.0.2 - 2026-03-21 =
  • security-hardener/trunk/security-hardener.php

    r3487909 r3492634  
    44Plugin URI: https://wordpress.org/plugins/security-hardener/
    55Description: Basic hardening: secure headers, disable XML-RPC/pingbacks, hide version, block user enumeration, generic login errors, and IP-based rate limiting.
    6 Version: 2.0.2
     6Version: 2.1.0
    77Requires at least: 6.9
    88Tested up to: 6.9
     
    2020
    2121// Plugin constants
    22 define( 'WPSH_VERSION', '2.0.2' );
     22define( 'WPSH_VERSION', '2.1.0' );
    2323define( 'WPSH_FILE', __FILE__ );
    2424define( 'WPSH_BASENAME', plugin_basename( __FILE__ ) );
     
    3535         */
    3636        const OPTION_NAME = 'wpsh_options';
     37
     38        /**
     39         * Checklist state option name in database
     40         */
     41        const CHECKLIST_OPTION = 'wpsh_checklist';
    3742
    3843        /**
     
    140145                add_action( 'admin_notices', array( $this, 'show_admin_notices' ) );
    141146                add_filter( 'plugin_action_links_' . WPSH_BASENAME, array( $this, 'add_settings_link' ) );
     147                add_action( 'wp_ajax_wpsh_toggle_checklist', array( $this, 'ajax_toggle_checklist' ) );
     148                add_action( 'wp_ajax_wpsh_reset_checklist', array( $this, 'ajax_reset_checklist' ) );
    142149            }
    143150        }
     
    808815                .wpsh-recommendations li{margin-bottom:4px;}
    809816                .wpsh-save-bar{margin:20px 0 4px;}
     817                .wpsh-cl-header{display:flex;align-items:center;justify-content:space-between;margin-top:24px;}
     818                .wpsh-cl-progress{display:flex;align-items:center;gap:8px;}
     819                .wpsh-cl-bar{height:6px;width:120px;background:#c3c4c7;border-radius:3px;overflow:hidden;}
     820                .wpsh-cl-bar-fill{height:100%;background:#2271b1;border-radius:3px;transition:width .2s;}
     821                .wpsh-cl-pct{font-size:12px;color:#646970;white-space:nowrap;}
     822                .wpsh-cl-item{display:flex;align-items:flex-start;gap:10px;padding:10px 16px;border-bottom:1px solid #f0f0f1;cursor:pointer;transition:background .1s;}
     823                .wpsh-cl-item:last-child{border-bottom:none;}
     824                .wpsh-cl-item:hover{background:#f6f7f7;}
     825                .wpsh-cl-item:focus{outline:2px solid #2271b1;outline-offset:-2px;}
     826                .wpsh-cl-item.wpsh-cl-done{background:#f0f6f0;}
     827                .wpsh-cl-check{flex-shrink:0;width:18px;height:18px;border:1.5px solid #c3c4c7;border-radius:3px;margin-top:1px;display:flex;align-items:center;justify-content:center;transition:all .15s;}
     828                .wpsh-cl-item.wpsh-cl-done .wpsh-cl-check{background:#2271b1;border-color:#2271b1;}
     829                .wpsh-cl-check svg{display:none;}
     830                .wpsh-cl-item.wpsh-cl-done .wpsh-cl-check svg{display:block;}
     831                .wpsh-cl-label{font-size:13px;line-height:1.45;color:#1d2327;}
     832                .wpsh-cl-item.wpsh-cl-done .wpsh-cl-label{color:#646970;text-decoration:line-through;text-decoration-color:#c3c4c7;}
    810833                @media(max-width:1200px){.wpsh-grid{grid-template-columns:repeat(2,minmax(0,1fr));}}
    811834                @media(max-width:782px){.wpsh-grid{grid-template-columns:minmax(0,1fr);}}
     
    884907
    885908        /**
     909         * AJAX handler — toggle a single checklist item.
     910         *
     911         * Expects POST: nonce, item_id (int 0-13), checked (0|1).
     912         */
     913        public function ajax_toggle_checklist(): void {
     914            check_ajax_referer( 'wpsh_checklist_nonce', 'nonce' );
     915
     916            if ( ! current_user_can( 'manage_options' ) ) {
     917                wp_send_json_error( 'Unauthorized', 403 );
     918            }
     919
     920            $item_id = isset( $_POST['item_id'] ) ? absint( $_POST['item_id'] ) : null;
     921            $checked = isset( $_POST['checked'] ) && '1' === $_POST['checked'];
     922
     923            if ( null === $item_id || $item_id > 13 ) {
     924                wp_send_json_error( 'Invalid item', 400 );
     925            }
     926
     927            $state = get_option( self::CHECKLIST_OPTION, [] );
     928
     929            if ( $checked ) {
     930                $state[ $item_id ] = true;
     931            } else {
     932                unset( $state[ $item_id ] );
     933            }
     934
     935            update_option( self::CHECKLIST_OPTION, $state, false );
     936            wp_send_json_success( [ 'done' => count( $state ), 'total' => 14 ] );
     937        }
     938
     939        /**
     940         * AJAX handler — reset all checklist items.
     941         *
     942         * Expects POST: nonce.
     943         */
     944        public function ajax_reset_checklist(): void {
     945            check_ajax_referer( 'wpsh_checklist_nonce', 'nonce' );
     946
     947            if ( ! current_user_can( 'manage_options' ) ) {
     948                wp_send_json_error( 'Unauthorized', 403 );
     949            }
     950
     951            update_option( self::CHECKLIST_OPTION, [], false );
     952            wp_send_json_success( [ 'done' => 0, 'total' => 14 ] );
     953        }
     954
     955        /**
     956         * Render the interactive hardening recommendations checklist.
     957         *
     958         * State is persisted in wpsh_checklist via AJAX (no page reload needed).
     959         */
     960        private function render_checklist(): void {
     961            $state = get_option( self::CHECKLIST_OPTION, [] );
     962            $done  = count( $state );
     963            $total = 14;
     964            $pct   = $total > 0 ? round( $done / $total * 100 ) : 0;
     965
     966            $items = [
     967                __( 'Use strong passwords and enable two-factor authentication', 'security-hardener' ),
     968                __( 'Keep WordPress, themes, and plugins updated', 'security-hardener' ),
     969                __( 'Use HTTPS (SSL/TLS) for your entire site', 'security-hardener' ),
     970                __( 'Regular backups stored off-site', 'security-hardener' ),
     971                __( 'Limit login attempts at the server/firewall level', 'security-hardener' ),
     972                __( 'Use security plugins for malware scanning', 'security-hardener' ),
     973                __( 'Restrict file permissions (directories: 755, files: 644)', 'security-hardener' ),
     974                __( 'Consider using a Web Application Firewall (WAF)', 'security-hardener' ),
     975                __( 'Protect the wp-admin directory with an additional HTTP authentication layer (BasicAuth)', 'security-hardener' ),
     976                __( 'Change the default database table prefix from wp_ to a custom value', 'security-hardener' ),
     977                __( 'Rename the default admin account to a non-obvious username', 'security-hardener' ),
     978                __( 'Restrict database user privileges to SELECT, INSERT, UPDATE and DELETE only', 'security-hardener' ),
     979                __( 'Protect wp-config.php by moving it one directory above the WordPress root or restricting access via .htaccess', 'security-hardener' ),
     980                __( 'Block direct access to files in wp-includes/ via .htaccess rules — see the WordPress Hardening Guide for the full snippet.', 'security-hardener' ),
     981            ];
     982
     983            $nonce = wp_create_nonce( 'wpsh_checklist_nonce' );
     984            ?>
     985            <hr style="margin: 24px 0;">
     986
     987            <div class="wpsh-cl-header">
     988                <h2 style="margin:0;"><?php esc_html_e( 'Additional Hardening Recommendations', 'security-hardener' ); ?></h2>
     989                <div class="wpsh-cl-progress">
     990                    <div class="wpsh-cl-bar">
     991                        <div class="wpsh-cl-bar-fill" id="wpsh-bar-fill" style="width:<?php echo absint( $pct ); ?>%"></div>
     992                    </div>
     993                    <span class="wpsh-cl-pct" id="wpsh-cl-pct">
     994                        <?php
     995                        printf(
     996                            /* translators: 1: completed items, 2: total items */
     997                            esc_html__( '%1$d / %2$d done', 'security-hardener' ),
     998                            absint( $done ),
     999                            absint( $total )
     1000                        );
     1001                        ?>
     1002                    </span>
     1003                </div>
     1004            </div>
     1005
     1006            <div class="wpsh-card" style="margin-top:12px;" id="wpsh-checklist">
     1007                <?php foreach ( $items as $id => $label ) : ?>
     1008                    <div class="wpsh-cl-item<?php echo isset( $state[ $id ] ) ? ' wpsh-cl-done' : ''; ?>"
     1009                        data-id="<?php echo absint( $id ); ?>"
     1010                        data-nonce="<?php echo esc_attr( $nonce ); ?>"
     1011                        role="checkbox"
     1012                        aria-checked="<?php echo isset( $state[ $id ] ) ? 'true' : 'false'; ?>"
     1013                        tabindex="0">
     1014                        <div class="wpsh-cl-check" aria-hidden="true">
     1015                            <svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
     1016                                <path d="M1 4l3 3 5-6" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
     1017                            </svg>
     1018                        </div>
     1019                        <span class="wpsh-cl-label"><?php echo esc_html( $label ); ?></span>
     1020                    </div>
     1021                <?php endforeach; ?>
     1022            </div>
     1023
     1024            <div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px;">
     1025                <p style="margin:0;font-size:12px;color:#646970;">
     1026                    <?php esc_html_e( 'Your progress is saved automatically.', 'security-hardener' ); ?>
     1027                </p>
     1028                <button type="button" id="wpsh-cl-reset" class="button-link"
     1029                    data-nonce="<?php echo esc_attr( $nonce ); ?>"
     1030                    style="font-size:12px;color:#2271b1;">
     1031                    <?php esc_html_e( 'Reset all', 'security-hardener' ); ?>
     1032                </button>
     1033            </div>
     1034
     1035            <p style="margin-top:16px;">
     1036                <?php
     1037                printf(
     1038                    /* translators: %s: URL to WordPress hardening guide */
     1039                    wp_kses_post( __( 'For more information, see the official <a href="%s" target="_blank">WordPress Hardening Guide</a>.', 'security-hardener' ) ),
     1040                    'https://developer.wordpress.org/advanced-administration/security/hardening/'
     1041                );
     1042                ?>
     1043            </p>
     1044
     1045            <script>
     1046            (function() {
     1047                var ajaxUrl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
     1048                var total   = <?php echo absint( $total ); ?>;
     1049
     1050                function updateProgress( done ) {
     1051                    var pct = total > 0 ? Math.round( done / total * 100 ) : 0;
     1052                    document.getElementById( 'wpsh-bar-fill' ).style.width = pct + '%';
     1053                    document.getElementById( 'wpsh-cl-pct' ).textContent    = done + ' / ' + total + ' done';
     1054                }
     1055
     1056                function sendToggle( id, checked, nonce ) {
     1057                    var fd = new FormData();
     1058                    fd.append( 'action',  'wpsh_toggle_checklist' );
     1059                    fd.append( 'nonce',   nonce );
     1060                    fd.append( 'item_id', id );
     1061                    fd.append( 'checked', checked ? '1' : '0' );
     1062                    fetch( ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' } )
     1063                        .then( function( r ) { return r.json(); } )
     1064                        .then( function( data ) { if ( data.success ) updateProgress( data.data.done ); } );
     1065                }
     1066
     1067                document.querySelectorAll( '.wpsh-cl-item' ).forEach( function( el ) {
     1068                    function toggle() {
     1069                        var done    = el.classList.toggle( 'wpsh-cl-done' );
     1070                        var checked = el.classList.contains( 'wpsh-cl-done' );
     1071                        el.setAttribute( 'aria-checked', checked ? 'true' : 'false' );
     1072                        sendToggle( el.dataset.id, checked, el.dataset.nonce );
     1073                    }
     1074                    el.addEventListener( 'click', toggle );
     1075                    el.addEventListener( 'keydown', function( e ) {
     1076                        if ( e.key === ' ' || e.key === 'Enter' ) { e.preventDefault(); toggle(); }
     1077                    } );
     1078                } );
     1079
     1080                var resetBtn = document.getElementById( 'wpsh-cl-reset' );
     1081                if ( resetBtn ) {
     1082                    resetBtn.addEventListener( 'click', function() {
     1083                        var fd = new FormData();
     1084                        fd.append( 'action', 'wpsh_reset_checklist' );
     1085                        fd.append( 'nonce',  resetBtn.dataset.nonce );
     1086                        fetch( ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' } )
     1087                            .then( function( r ) { return r.json(); } )
     1088                            .then( function( data ) {
     1089                                if ( data.success ) {
     1090                                    document.querySelectorAll( '.wpsh-cl-item' ).forEach( function( el ) {
     1091                                        el.classList.remove( 'wpsh-cl-done' );
     1092                                        el.setAttribute( 'aria-checked', 'false' );
     1093                                    } );
     1094                                    updateProgress( 0 );
     1095                                }
     1096                            } );
     1097                    } );
     1098                }
     1099            })();
     1100            </script>
     1101            <?php
     1102        }
     1103
     1104        /**
    8861105         * Render settings page
    8871106         */
     
    10661285                                    sprintf(
    10671286                                        /* translators: %s: URL to HSTS preload list */
    1068                                         wp_kses_post( __( 'Submit to the <a href="%s" target="_blank">HSTS Preload List</a>. Requires 1 year max-age.', 'security-hardener' ) ),
     1287                                        wp_kses_post( __( 'Adds the preload directive to the HSTS header. Required before submitting manually to <a href="%s" target="_blank">hstspreload.org</a>.', 'security-hardener' ) ),
    10691288                                        'https://hstspreload.org/'
    10701289                                    )
     
    10891308                                    'clean_head',
    10901309                                    __( 'Clean wp_head', 'security-hardener' ),
    1091                                     __( 'Removes RSD link, Windows Live Writer manifest, shortlink, and emoji scripts from &lt;head&gt;.', 'security-hardener' )
     1310                                    __( 'Removes RSD link, Windows Live Writer manifest, shortlink, and emoji scripts from the page head.', 'security-hardener' )
    10921311                                );
    10931312                                $this->render_toggle_row(
     
    11161335                <?php $this->render_security_logs(); ?>
    11171336
    1118                 <hr style="margin: 24px 0;">
    1119 
    1120                 <h2><?php esc_html_e( 'Additional Hardening Recommendations', 'security-hardener' ); ?></h2>
    1121                 <ul class="wpsh-recommendations" style="list-style: disc; padding-left: 20px;">
    1122                     <li><?php esc_html_e( 'Use strong passwords and enable two-factor authentication', 'security-hardener' ); ?></li>
    1123                     <li><?php esc_html_e( 'Keep WordPress, themes, and plugins updated', 'security-hardener' ); ?></li>
    1124                     <li><?php esc_html_e( 'Use HTTPS (SSL/TLS) for your entire site', 'security-hardener' ); ?></li>
    1125                     <li><?php esc_html_e( 'Regular backups stored off-site', 'security-hardener' ); ?></li>
    1126                     <li><?php esc_html_e( 'Limit login attempts at the server/firewall level', 'security-hardener' ); ?></li>
    1127                     <li><?php esc_html_e( 'Use security plugins for malware scanning', 'security-hardener' ); ?></li>
    1128                     <li><?php esc_html_e( 'Restrict file permissions (directories: 755, files: 644)', 'security-hardener' ); ?></li>
    1129                     <li><?php esc_html_e( 'Consider using a Web Application Firewall (WAF)', 'security-hardener' ); ?></li>
    1130                     <li><?php esc_html_e( 'Protect the wp-admin directory with an additional HTTP authentication layer (BasicAuth)', 'security-hardener' ); ?></li>
    1131                     <li><?php esc_html_e( 'Change the default database table prefix from wp_ to a custom value', 'security-hardener' ); ?></li>
    1132                     <li><?php esc_html_e( 'Rename the default admin account to a non-obvious username', 'security-hardener' ); ?></li>
    1133                     <li><?php esc_html_e( 'Restrict database user privileges to SELECT, INSERT, UPDATE and DELETE only', 'security-hardener' ); ?></li>
    1134                     <li><?php esc_html_e( 'Protect wp-config.php by moving it one directory above the WordPress root or restricting access via .htaccess', 'security-hardener' ); ?></li>
    1135                     <li><?php esc_html_e( 'Block direct access to files in wp-includes/ by adding the following rules to your .htaccess file (outside the WordPress tags).', 'security-hardener' ); ?></li>
    1136                 </ul>
    1137                 <p>
    1138                     <?php
    1139                     printf(
    1140                         /* translators: %s: URL to WordPress hardening guide */
    1141                         wp_kses_post( __( 'For more information, see the official <a href="%s" target="_blank">WordPress Hardening Guide</a>.', 'security-hardener' ) ),
    1142                         'https://developer.wordpress.org/advanced-administration/security/hardening/'
    1143                     );
    1144                     ?>
    1145                 </p>
     1337                <?php $this->render_file_permissions(); ?>
     1338
     1339                <?php $this->render_checklist(); ?>
    11461340
    11471341            </div><!-- .wrap -->
     
    12211415            }
    12221416
    1223             // Check file permissions
    1224             $this->check_file_permissions_notice();
    1225         }
    1226 
    1227         /**
    1228          * Check file permissions and show notice
    1229          */
    1230         private function check_file_permissions_notice(): void {
    1231             // Only show on plugin settings page
    1232             $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
    1233             if ( ! $screen || 'settings_page_security-hardener' !== $screen->id ) {
    1234                 return;
    1235             }
    1236 
     1417            // Check file permissions is now rendered inline in render_settings_page()
     1418        }
     1419
     1420        /**
     1421         * Render file permissions check as an inline section on the settings page.
     1422         *
     1423         * Shows a success notice when all paths are correct, or a table listing
     1424         * only the paths with issues when problems are found.
     1425         */
     1426        private function render_file_permissions(): void {
    12371427            if ( ! function_exists( 'get_home_path' ) ) {
    12381428                require_once ABSPATH . 'wp-admin/includes/file.php';
     
    12431433                array(
    12441434                    'path'        => ABSPATH . 'wp-config.php',
    1245                     'type'        => 'file',
    12461435                    'recommended' => array( '0600', '0640', '0644' ),
    12471436                    'label'       => 'wp-config.php',
     
    12491438                array(
    12501439                    'path'        => get_home_path(),
    1251                     'type'        => 'dir',
    12521440                    'recommended' => array( '0755', '0750' ),
    12531441                    'label'       => __( 'WordPress root directory', 'security-hardener' ),
     
    12551443                array(
    12561444                    'path'        => WP_CONTENT_DIR,
    1257                     'type'        => 'dir',
    12581445                    'recommended' => array( '0755', '0750' ),
    12591446                    'label'       => 'wp-content',
     
    12611448                array(
    12621449                    'path'        => $upload_dir['basedir'],
    1263                     'type'        => 'dir',
    12641450                    'recommended' => array( '0755', '0750' ),
    12651451                    'label'       => __( 'Uploads directory', 'security-hardener' ),
     
    12731459                    continue;
    12741460                }
    1275 
    12761461                $perms = substr( sprintf( '%o', fileperms( $check['path'] ) ), -4 );
    1277 
    12781462                if ( ! in_array( $perms, $check['recommended'], true ) ) {
    1279                     $issues[] = sprintf(
    1280                         /* translators: 1: file/directory name, 2: current permissions, 3: recommended permissions */
    1281                         __( '%1$s has permissions %2$s (recommended: %3$s)', 'security-hardener' ),
    1282                         '<code>' . esc_html( $check['label'] ) . '</code>',
    1283                         '<code>' . esc_html( $perms ) . '</code>',
    1284                         '<code>' . esc_html( implode( ', ', $check['recommended'] ) ) . '</code>'
     1463                    $issues[] = array(
     1464                        'label'       => $check['label'],
     1465                        'current'     => $perms,
     1466                        'recommended' => implode( ', ', $check['recommended'] ),
    12851467                    );
    12861468                }
    12871469            }
    1288 
    1289             if ( ! empty( $issues ) ) {
     1470            ?>
     1471            <hr style="margin:24px 0;">
     1472            <h2><?php esc_html_e( 'File Permissions', 'security-hardener' ); ?></h2>
     1473
     1474            <?php if ( empty( $issues ) ) : ?>
     1475                <div style="display:flex;align-items:center;gap:8px;padding:10px 14px;background:#f0f6f0;border:1px solid #c3c4c7;border-radius:4px;font-size:13px;color:#1d2327;">
     1476                    <span style="width:8px;height:8px;border-radius:50%;background:#00a32a;flex-shrink:0;display:inline-block;"></span>
     1477                    <?php esc_html_e( 'All checked paths have correct file permissions.', 'security-hardener' ); ?>
     1478                </div>
     1479            <?php else : ?>
     1480                <div style="border:1px solid #c3c4c7;border-radius:4px;overflow:hidden;">
     1481                    <table class="wp-list-table widefat fixed striped">
     1482                        <thead>
     1483                            <tr>
     1484                                <th><?php esc_html_e( 'Path', 'security-hardener' ); ?></th>
     1485                                <th style="width:120px;"><?php esc_html_e( 'Current', 'security-hardener' ); ?></th>
     1486                                <th style="width:200px;"><?php esc_html_e( 'Recommended', 'security-hardener' ); ?></th>
     1487                                <th style="width:140px;"><?php esc_html_e( 'Status', 'security-hardener' ); ?></th>
     1488                            </tr>
     1489                        </thead>
     1490                        <tbody>
     1491                            <?php foreach ( $issues as $issue ) : ?>
     1492                                <tr>
     1493                                    <td><code><?php echo esc_html( $issue['label'] ); ?></code></td>
     1494                                    <td><code><?php echo esc_html( $issue['current'] ); ?></code></td>
     1495                                    <td><code><?php echo esc_html( $issue['recommended'] ); ?></code></td>
     1496                                    <td>
     1497                                        <span style="display:inline-flex;align-items:center;gap:5px;">
     1498                                            <span style="width:7px;height:7px;border-radius:50%;background:#d63638;display:inline-block;flex-shrink:0;"></span>
     1499                                            <?php esc_html_e( 'Too permissive', 'security-hardener' ); ?>
     1500                                        </span>
     1501                                    </td>
     1502                                </tr>
     1503                            <?php endforeach; ?>
     1504                        </tbody>
     1505                    </table>
     1506                </div>
     1507            <?php endif; ?>
     1508
     1509            <p style="font-size:12px;color:#646970;margin-top:8px;">
     1510                <?php
     1511                printf(
     1512                    /* translators: %s: URL to file permissions documentation */
     1513                    wp_kses_post( __( 'Learn more about <a href="%s" target="_blank">WordPress file permissions</a>.', 'security-hardener' ) ),
     1514                    'https://developer.wordpress.org/advanced-administration/server/file-permissions/'
     1515                );
    12901516                ?>
    1291                 <div class="notice notice-warning">
    1292                     <p><strong><?php esc_html_e( 'File Permission Issues Detected:', 'security-hardener' ); ?></strong></p>
    1293                     <ul style="list-style: disc; padding-left: 20px;">
    1294                         <?php foreach ( $issues as $issue ) : ?>
    1295                             <li><?php echo wp_kses_post( $issue ); ?></li>
    1296                         <?php endforeach; ?>
    1297                     </ul>
    1298                     <p>
    1299                         <?php
    1300                         printf(
    1301                             /* translators: %s: URL to file permissions documentation */
    1302                             wp_kses_post( __( 'Learn more about <a href="%s" target="_blank">WordPress file permissions</a>.', 'security-hardener' ) ),
    1303                             'https://developer.wordpress.org/advanced-administration/server/file-permissions/'
    1304                         );
    1305                         ?>
    1306                     </p>
    1307                 </div>
    1308                 <?php
    1309             }
     1517            </p>
     1518            <?php
    13101519        }
    13111520
  • security-hardener/trunk/uninstall.php

    r3474999 r3492634  
    3232delete_option( 'wpsh_security_logs' );
    3333
     34// Delete hardening checklist state.
     35delete_option( 'wpsh_checklist' );
     36
    3437// Delete login rate-limiting transients.
    3538// We cannot enumerate every hashed IP key ahead of time, so we retrieve
  • security-hardener/uninstall.php

    r3487781 r3492634  
    3232delete_option( 'wpsh_security_logs' );
    3333
     34// Delete hardening checklist state.
     35delete_option( 'wpsh_checklist' );
     36
    3437// Delete login rate-limiting transients.
    3538// We cannot enumerate every hashed IP key ahead of time, so we retrieve
Note: See TracChangeset for help on using the changeset viewer.