__( 'ALPEG Forms upload directory', 'alpeg-forms' ), 'test' => array( __CLASS__, 'site_health_test' ), ); return $tests; } /** * Result of the check: protected | open | unknown (cached for 12 hours). * * @param bool $fresh Ignore the cache. * @return string */ public static function status( $fresh = false ) { $cached = $fresh ? false : get_transient( self::CACHE ); if ( is_string( $cached ) && '' !== $cached ) { return $cached; } $status = self::run_check(); set_transient( self::CACHE, $status, 12 * HOUR_IN_SECONDS ); return $status; } /** * Perform the loopback request. * * @return string protected | open | unknown */ private static function run_check() { Files::protect_directory(); $uploads = wp_upload_dir( null, false ); $name = 'alpeg-forms-probe-' . wp_generate_uuid4() . '.txt'; $body = self::PROBE_TXT . '-' . wp_generate_uuid4(); $file = Files::base_dir() . '/' . $name; // Read-only positive control: a known public WordPress asset, never a file written to the uploads root. $control = ABSPATH . WPINC . '/js/jquery/jquery.min.js'; $args = array( 'timeout' => 5, 'redirection' => 0, 'headers' => array( 'Cache-Control' => 'no-cache' ), /** This filter is documented in wp-includes/class-wp-http-streams.php */ 'sslverify' => apply_filters( 'https_local_ssl_verify', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Core filter. ); try { if ( ! is_readable( $control ) ) { return 'unknown'; } $control_body = file_get_contents( $control ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Read-only core asset used as a loopback control. if ( false === $control_body || '' === $control_body ) { return 'unknown'; } $res = wp_remote_get( includes_url( 'js/jquery/jquery.min.js' ), $args ); if ( is_wp_error( $res ) || 200 !== (int) wp_remote_retrieve_response_code( $res ) || ! hash_equals( hash( 'sha256', $control_body ), hash( 'sha256', (string) wp_remote_retrieve_body( $res ) ) ) ) { return 'unknown'; } if ( false === file_put_contents( $file, $body ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Temporary probe inside the protected plugin upload directory, removed in finally. return 'unknown'; } $res = wp_remote_get( trailingslashit( $uploads['baseurl'] ) . 'alpeg-forms/' . $name, $args ); if ( is_wp_error( $res ) ) { return 'unknown'; } $code = (int) wp_remote_retrieve_response_code( $res ); if ( 200 === $code && $body === (string) wp_remote_retrieve_body( $res ) ) { return 'open'; } return in_array( $code, array( 403, 404 ), true ) ? 'protected' : 'unknown'; } finally { wp_delete_file( $file ); } } /** * Is the web server nginx? * * @return bool */ public static function is_nginx() { return isset( $_SERVER['SERVER_SOFTWARE'] ) && false !== stripos( sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ), 'nginx' ); } /** * The rule to add on nginx. * * @return string */ public static function nginx_rule() { $uploads = wp_upload_dir( null, false ); $path = wp_parse_url( trailingslashit( $uploads['baseurl'] ) . 'alpeg-forms/', PHP_URL_PATH ); return 'location ^~ ' . $path . ' { deny all; }'; } /** * How to fix an open directory (HTML). * * @return string */ public static function fix_html() { if ( self::is_nginx() ) { return '
' . esc_html__( 'Your server runs nginx, which ignores the protection file (.htaccess). Add this rule to the server block of your website, or ask your host to do it:', 'alpeg-forms' ) . '
' . esc_html( self::nginx_rule() ) . '
' . esc_html__( 'The protection file (.htaccess) in the upload directory is ignored by your server – usually because the server does not allow .htaccess files (AllowOverride None). Please ask your host to deny direct access to this directory:', 'alpeg-forms' ) . '
' . esc_html( str_replace( ABSPATH, '/', Files::base_dir() ) ) . '
' . esc_html__( 'Files uploaded through ALPEG Forms cannot be opened directly from the web; administrators download them through the dashboard.', 'alpeg-forms' ) . '
', 'actions' => '', 'test' => 'alpeg_forms_uploads', ); if ( 'open' === $status ) { $result['label'] = __( 'Uploaded form files can be opened directly from the web', 'alpeg-forms' ); $result['status'] = 'critical'; $result['badge']['color'] = 'red'; $result['description'] = '' . esc_html__( 'The random file names make guessing very hard, but the directory itself is not protected.', 'alpeg-forms' ) . '
' . self::fix_html(); } elseif ( 'unknown' === $status ) { $result['label'] = __( 'The protection of uploaded form files could not be checked', 'alpeg-forms' ); $result['status'] = 'recommended'; $result['description'] = '' . esc_html__( 'Your website could not request its own upload directory (loopback request). This is often caused by firewalls or local development environments. Your files are still stored with random names.', 'alpeg-forms' ) . '
'; } return $result; } /** * "Check again" button in the settings. */ public static function handle_recheck() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'alpeg-forms' ), 403 ); } check_admin_referer( 'alpeg_forms_upload_check' ); self::status( true ); wp_safe_redirect( admin_url( 'admin.php?page=alpeg-forms-settings#uploads' ) ); exit; } }