Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
afa4eb7
fix(restore): keep session upload progress off after restoring an old…
mrrobot47 Oct 23, 2026
671cf81
fix(restore): find PATH and HOST sections with any header layout
mrrobot47 Oct 23, 2026
b1792c3
fix(restore): name each section header from the header itself
mrrobot47 Oct 23, 2026
5e0b969
fix(restore): detect section headers where the parser expects an entry
mrrobot47 Oct 23, 2026
987a58f
fix(migrations): turn off session upload progress where the v4.13.3 c…
mrrobot47 Oct 23, 2026
fff8880
fix(restore): count entries under a bare [PATH] or [HOST] header as g…
mrrobot47 Oct 23, 2026
463ac65
fix(migrations): never follow a swapped custom.ini in the recheck mig…
mrrobot47 Oct 23, 2026
50432dc
fix(migrations): check the directories above custom.ini in the rechec…
mrrobot47 Oct 23, 2026
30264f5
fix(restore): re-add the upload-progress line through a checked handle
mrrobot47 Oct 23, 2026
16beb4d
fix(migrations): use the checked file helpers in the recheck migration
mrrobot47 Oct 23, 2026
51f14ff
fix(restore): remove and chown created config files from inside their…
mrrobot47 Oct 23, 2026
0c36b0c
fix(migrations): bound the recheck migration's reads and chown throug…
mrrobot47 Oct 23, 2026
3684b8f
fix(restore): don't create a missing custom.ini, bound the write check
mrrobot47 Oct 23, 2026
afda75a
fix(migrations): skip a missing custom.ini in the recheck migration
mrrobot47 Oct 23, 2026
af79546
fix(helpers): open container files without blocking on a FIFO
mrrobot47 Oct 26, 2026
b97ac06
ci(behat): test this checkout and set up a local rclone remote
mrrobot47 Oct 26, 2026
115cd7d
test(behat): cover the upload progress re-check and the restore re-apply
mrrobot47 Oct 26, 2026
e59c6db
test(behat): guard the destructive steps and check that the migration…
mrrobot47 Oct 26, 2026
3b1e877
fix(migrations): print the manual fix when the recheck fails on a site
mrrobot47 Oct 26, 2026
3ba48a8
test(behat): set upload progress On in the fixtures' php.ini
mrrobot47 Oct 26, 2026
9e0e422
Merge branch 'develop' into fix/restore-keeps-upload-progress-off
mrrobot47 Nov 4, 2026
00004b6
ci: retrigger checks
mrrobot47 Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/helper/Site_Backup_Restore.php
Original file line number Diff line number Diff line change
Expand Up @@ -1693,6 +1693,54 @@ private function restore_php_conf( $backup_dir ) {
$restore_command = sprintf( 'rsync -a %s/php/php/conf.d/custom.ini %s/config/php/php/conf.d/custom.ini', $backup_dir, EE_ROOT_DIR . '/sites/' . $this->site_data['site_url'] );
EE::exec( $restore_command );
}

$this->keep_session_upload_progress_off();
}

/**
* A backup taken before v4.13.3 brings back a custom.ini without `session.upload_progress.enabled = Off`, so add it
* again unless the restored file sets the directive itself. The site reload at the end of the restore applies it.
*/
private function keep_session_upload_progress_off() {
$site_url = $this->site_data['site_url'];
$conf_dir = EE_ROOT_DIR . '/sites/' . $site_url . '/config/php/php/conf.d';
$file = $conf_dir . '/custom.ini';
$manual = "Add 'session.upload_progress.enabled = Off' to $file and run: ee site reload $site_url";

$created = ! file_exists( $file ) && ! is_link( $file );
if ( is_link( $file ) || ( ! $created && ! is_file( $file ) ) || ( $created && ! is_dir( $conf_dir ) ) ) {
EE::warning( "Could not check session.upload_progress in the restored custom.ini: it is not a regular file or its directory is missing. $manual" );

return;
}

$content = $created ? '' : @file_get_contents( $file );
if ( false === $content ) {
EE::warning( "Could not read the restored custom.ini to check session.upload_progress. $manual" );

return;
}

$new_content = \EE\Site\Utils\add_session_upload_progress_off( $content );
if ( null === $new_content ) {
return;
}

// Written in place: keeps owner, mode and inode (macOS bind-mounts this single file).
$written = @file_put_contents( $file, $new_content );
Comment thread
mrrobot47 marked this conversation as resolved.
Resolved
clearstatcache( true, $file );
if ( strlen( $new_content ) !== $written ) {
EE::warning( "Could not turn off session.upload_progress in the restored custom.ini. $manual" );
Comment thread
mrrobot47 marked this conversation as resolved.
Resolved

return;
}
if ( $created ) {
@chmod( $file, 0644 );
@chown( $file, fileowner( $conf_dir ) );
@chgrp( $file, filegroup( $conf_dir ) );
}

EE::log( 'The restored custom.ini did not set session.upload_progress.enabled, so it was set to Off again. Set it to On there only if your app needs it.' );
}

/**
Expand Down
89 changes: 89 additions & 0 deletions src/helper/site-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -1623,3 +1623,92 @@ function get_wp_core_download_command( string $path, array $args = [] ) {
// docker_compose_exec() wraps the command in bash -c "...".
return addcslashes( $script, '"$`\\' );
}

/**
* Add `session.upload_progress.enabled = Off` to a site's custom.ini content, unless the content already sets it globally.
*
* Same rule as the v4.13.3 upload-progress migrations: an effective global line, On or Off, is the user's choice and is kept.
*
* @param string $content custom.ini content.
*
* @return string|null The new content, or null when it already sets the directive.
*/
function add_session_upload_progress_off( string $content ) {

$bom = '';
if ( 0 === strpos( $content, "\xEF\xBB\xBF" ) ) {
$bom = "\xEF\xBB\xBF";
$content = substr( $content, 3 );
}

if ( ini_sets_globally( $content, 'session.upload_progress.enabled' ) ) {
return null;
}

$eol = false !== strpos( $content, "\r\n" ) ? "\r\n" : "\n";
$lines = '; Session upload progress is off for security; set it to On only if your app needs it.' . $eol . 'session.upload_progress.enabled = Off' . $eol;

if ( '' === trim( $content ) ) {
return $bom . $lines;
}

// At the very top, so neither a [PATH=]/[HOST=] section nor a multi-line quoted value can capture it.
return $bom . $lines . $eol . $content;
}

/**
* Whether ini content sets a directive outside any [PATH=]/[HOST=] section, read the way PHP reads php.ini.
*
* @param string $content ini content without BOM.
* @param string $key Directive name (case-sensitive, like PHP).
*
* @return bool
*/
function ini_sets_globally( string $content, string $key ) {

$global = ini_global_part( $content );
$ini = @parse_ini_string( $global, true, INI_SCANNER_NORMAL );
// PHP applies the lines above a syntax error, so only that part counts.
for ( $lines = preg_split( '/(?<=\n)/', $global ), $n = count( $lines ) - 1; false === $ini && $n > 0; $n-- ) {
$ini = @parse_ini_string( implode( '', array_slice( $lines, 0, $n ) ), true, INI_SCANNER_NORMAL );
}
if ( false === $ini ) {
return false;
}

foreach ( $ini as $name => $value ) {
if ( is_array( $value ) ? array_key_exists( $key, $value ) : $key === $name ) {
return true;
}
}

return false;
Comment thread
mrrobot47 marked this conversation as resolved.
Resolved
}

/**
* The part of ini content before the first section whose name starts with PATH or HOST.
*
* PHP scopes every entry after such a header to it, even under later ordinary headers. The parser decides what a header's
* name is, so CRLF line endings and quoted names like ["PATH=/x"] are handled the way PHP handles them.
*
* @param string $content ini content without BOM.
*
* @return string
*/
function ini_global_part( string $content ) {

if ( ! preg_match_all( '/^[ \t]*\[/m', $content, $matches, PREG_OFFSET_CAPTURE ) ) {
return $content;
}

foreach ( $matches[0] as $match ) {
$eol = strpos( $content, "\n", $match[1] );
// Text inside a multi-line quoted value doesn't parse as the last section.
$ini = @parse_ini_string( false === $eol ? $content : substr( $content, 0, $eol ), true, INI_SCANNER_NORMAL );
if ( false !== $ini && is_array( end( $ini ) ) && preg_match( '/^(PATH|HOST)/i', (string) key( $ini ) ) ) {
Comment thread
mrrobot47 marked this conversation as resolved.
Resolved
return substr( $content, 0, $match[1] );
}
}

return $content;
}
Loading