`. Under php-fpm
* behind `mod_proxy_fcgi` — the common modern setup — no PHP module is
* loaded, so both blocks were skipped and nothing turned the interpreter off.
* `RemoveHandler` covers the `AddHandler` mappings a host may have set.
*
* It does **not** undo a vhost-level `SetHandler "proxy:fcgi://..."`, which is
* what php-fpm normally uses, and a `.htaccess` cannot. The protection that
* actually holds under php-fpm is the `Require all denied` clause below,
* which refuses the request before any handler is consulted.
* - The pattern missed `pht`, `phtm`, `htm`, `html`, `xhtml`, `svg`, `svgz`,
* `jsp` and `user.ini`, and `php[0-9]` stops matching at `php10`.
* - `Options -Indexes` only covers Apache, so an `index.php` is still written.
*/
private static function htaccess(): string {
$deny = array(
"\t",
"\t\tRequire all denied",
"\t",
"\t",
"\t\tOrder allow,deny",
"\t\tDeny from all",
"\t",
);
return implode(
"\n",
array_merge(
array(
'# Added by FormPilot — Smart Form Builder. Uploads are data, never code.',
'',
),
$deny,
array(
'',
'',
'',
),
$deny,
array(
'',
'',
'# Works whether PHP runs as a module, via CGI, or over FastCGI. Wrapped, because',
'# RemoveHandler and RemoveType need AllowOverride FileInfo: unguarded, a host',
'# that grants only Limit answers 500 for this whole directory, which would make',
'# every already-uploaded file undownloadable.',
'',
"\tRemoveHandler .php .php3 .php4 .php5 .php7 .php8 .phtml .pht .phar",
"\tRemoveType .php .php3 .php4 .php5 .php7 .php8 .phtml .pht .phar",
'',
'',
'',
"\tphp_flag engine off",
'',
'',
'',
"\tphp_flag engine off",
'',
'',
'Options -Indexes',
'',
)
)
);
}
/**
* Returns the IIS equivalent of the execution rules.
*
* `OrphanCleaner::PROTECTED_FILES` has always listed `web.config`, so one was
* clearly intended; none was ever written.
*/
private static function web_config(): string {
$blocked = array();
foreach ( array( '.php', '.phtml', '.phar', '.asp', '.aspx', '.svg', '.html', '.htm' ) as $extension ) {
$blocked[] = "\t\t\t\t\t" . '';
}
return implode(
"\n",
array_merge(
array(
'',
'',
'',
"\t",
"\t\t" . '',
"\t\t",
"\t\t\t",
"\t\t\t\t" . '',
),
$blocked,
array(
"\t\t\t\t",
"\t\t\t",
"\t\t",
"\t\t" . '',
"\t",
'',
'',
)
)
);
}
/**
* Writes a protective file when it is missing or out of date.
*
* Reports what happened. The original returned void and bailed silently when
* `$wp_filesystem` was unavailable, and its one caller discarded the answer —
* so uploads proceeded into a directory that had never been hardened, with
* nothing anywhere recording that it had not been.
*
* @param string $path Absolute file path.
* @param string $contents Desired content.
*
* @return bool True when the file is present with the expected content.
*/
private static function write( string $path, string $contents ): bool {
global $wp_filesystem;
if ( is_file( $path ) && file_get_contents( $path ) === $contents ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Comparing a file the plugin wrote itself.
return true;
}
if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();
if ( $wp_filesystem instanceof \WP_Filesystem_Base ) {
return (bool) $wp_filesystem->put_contents( $path, $contents, FS_CHMOD_FILE );
}
/*
* No abstraction available — `FS_METHOD` is set to FTP or SSH and there are
* no credentials in a front-end request. Writing directly is correct here:
* the alternative is an unprotected directory.
*
* Through core's direct transport rather than raw PHP calls, which is what
* the branch above already is on most hosts. Every writer puts the same
* bytes at the same offsets, so two concurrent first uploads cannot leave an
* interleaved .htaccess behind.
*/
if ( ! class_exists( 'WP_Filesystem_Direct' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
}
return ( new \WP_Filesystem_Direct( null ) )->put_contents( $path, $contents, self::file_mode() );
}
/**
* Returns the permissions a guard file is written with.
*
* `WP_Filesystem()` only defines `FS_CHMOD_FILE` after it has connected, and the
* fallback above runs precisely when it could not. Reading the constant there
* was an `Undefined constant` fatal on the first upload to an FTP-configured
* site, so the value is worked out here the way core works it out.
*/
private static function file_mode(): int {
if ( defined( 'FS_CHMOD_FILE' ) ) {
return (int) FS_CHMOD_FILE;
}
return fileperms( ABSPATH . 'index.php' ) & 0777 | 0644;
}
}