Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
<?php

namespace EE\Migration;

use EE;
use EE\Model\Site;

/**
* Removes session.sid_length and session.sid_bits_per_character, deprecated as of PHP 8.4, from the php.ini of PHP 8.4 and 8.5 sites.
*
* A site's php.ini comes from its image at creation and from the version's archive on a PHP switch, so existing sites keep them otherwise.
*/
class DropDeprecatedSessionSidSettings extends Base {

/** `latest` sites run the php8.4 image. */
const PHP_VERSIONS = [ '8.4', '8.5', 'latest' ];

/** Values EasyEngine shipped; any other value was set by the user. */
const SHIPPED_VALUES = [
'session.sid_length' => '26',
'session.sid_bits_per_character' => '5',
];

/** Comments shipped right above each directive, removed together with it. */
const SHIPPED_COMMENTS = [
'session.sid_length' => [
'; Set session ID character length. This value could be between 22 to 256.',
'; Shorter length than default is supported only for compatibility reason.',
'; Users should use 32 or more chars.',
'; http://php.net/session.sid-length',
'; Default Value: 32',
'; Development Value: 26',
'; Production Value: 26',
],
'session.sid_bits_per_character' => [
'; Define how many bits are stored in each character when converting',
'; the binary hash data to something readable.',
'; Possible values:',
'; 4 (4 bits: 0-9, a-f)',
'; 5 (5 bits: 0-9, a-v)',
'; 6 (6 bits: 0-9, a-z, A-Z, "-", ",")',
'; Default Value: 4',
'; Development Value: 5',
'; Production Value: 5',
'; http://php.net/session.hash-bits-per-character',
],
];

/** Entry of the quick reference at the top of the shipped php.ini. */
const SHIPPED_QUICK_REFERENCE = [
'; session.sid_bits_per_character',
'; Default Value: 4',
'; Development Value: 5',
'; Production Value: 5',
];

private $sites;

/** @var array Files changed by up(), restored by down(). */
private $changed = [];

public function __construct() {

parent::__construct();
$this->sites = Site::all();
if ( $this->is_first_execution || ! $this->sites ) {
$this->skip_this_migration = true;
}
}

/**
* @throws EE\ExitException
*/
public function up() {

if ( $this->skip_this_migration ) {
EE::debug( 'Skipping drop-deprecated-session-sid-settings migration as it is not needed.' );

return;
}

foreach ( $this->sites as $site ) {
if ( ! in_array( $site->site_type, [ 'php', 'wp' ], true ) || ! in_array( (string) $site->php_version, self::PHP_VERSIONS, true ) ) {
continue;
}

// One broken site must not block the upgrade of the others.
try {
$this->migrate_site( $site );
} catch ( \Throwable $e ) {
EE::warning( sprintf( 'Could not remove the deprecated session ID settings from the php.ini of %s: %s', $site->site_url, $e->getMessage() ) );
}
}
}

/**
* Restores the files changed by up().
*
* @throws EE\ExitException
*/
public function down() {

foreach ( array_reverse( $this->changed ) as $change ) {
try {
$old = file_get_contents( $change['backup'] );
if ( false === $old ) {
throw new \Exception( 'Could not read ' . $change['backup'] );
}
$handle = self::open_ini( $change['ini'] );
if ( ! $handle ) {
throw new \Exception( $change['ini'] . ' is gone.' );
}
try {
self::rewrite( $handle, $old, $change['ini'] );
} finally {
fclose( $handle );
}
$this->fs->remove( $change['backup'] );
self::reload_fpm( $change['site'] );
} catch ( \Throwable $e ) {
EE::warning( sprintf( 'Could not restore the php.ini of %s from %s: %s', $change['site']->site_url, $change['backup'], $e->getMessage() ) );
}
}
$this->changed = [];
}

/**
* @param Site $site PHP 8.4 or 8.5 site.
*
* @throws \Exception
*/
private function migrate_site( $site ) {

$ini = $site->site_fs_path . '/config/php/php/php.ini';

// Missing on macOS, where php.ini stays in the container.
$handle = self::open_ini( $ini );
if ( ! $handle ) {
EE::debug( "Skipping $site->site_url: no php.ini at $ini." );

return;
}

try {
$content = stream_get_contents( $handle );
if ( false === $content ) {
throw new \Exception( "Could not read $ini." );
}

list( $new_content, $removed ) = self::strip_settings( $content );
if ( $new_content === $content ) {
EE::debug( "$ini has no deprecated session ID settings." );

return;
}

$backup = EE_BACKUP_DIR . '/' . $site->site_url . '/php.ini.with-session-sid-settings';
$this->fs->dumpFile( $backup, $content );

try {
self::rewrite( $handle, $new_content, $ini );
} catch ( \Exception $e ) {
self::rewrite( $handle, $content, $ini );
throw $e;
}
} finally {
fclose( $handle );
}

$this->changed[] = [
'site' => $site,
'ini' => $ini,
'backup' => $backup,
];
EE::debug( "Removed the deprecated session ID settings from $ini; the previous file is $backup." );

foreach ( $removed as $line => $shipped ) {
if ( ! $shipped ) {
EE::warning( sprintf( 'Removed `%s` from the php.ini of %s: it is deprecated as of PHP 8.4, and PHP now uses its default. The previous file is saved as %s.', $line, $site->site_url, $backup ) );
}
}

self::reload_fpm( $site );
}

/**
* Removes the two directives, wherever they are set, and the shipped comments that document them.
* Every other line is kept byte for byte.
*
* @param string $content php.ini content.
*
* @return array New content, and the removed directive lines => whether the value is the shipped one.
*/
public static function strip_settings( $content ) {

$lines = preg_split( '/(?<=\n)/', $content, -1, PREG_SPLIT_NO_EMPTY );
$norm = array_map(
function ( $line ) {
return rtrim( $line, " \t\r\n" );
},
$lines
);
$drop = [];
$removed = [];

$quick_count = count( self::SHIPPED_QUICK_REFERENCE );
foreach ( array_keys( $norm ) as $i ) {
if ( array_slice( $norm, $i, $quick_count ) === self::SHIPPED_QUICK_REFERENCE ) {
$drop += array_fill( $i, $quick_count, true );
if ( isset( $norm[ $i + $quick_count ] ) && '' === $norm[ $i + $quick_count ] ) {
$drop[ $i + $quick_count ] = true;
}
}
}

foreach ( $norm as $i => $line ) {
if ( ! preg_match( '/^\s*(session\.sid_length|session\.sid_bits_per_character)\s*=(.*)$/', $line, $matches ) ) {
continue;
}
$name = $matches[1];
$drop[ $i ] = true;

// Inline comment and quotes don't change the value.
$value = trim( preg_replace( '/;.*$/', '', $matches[2] ), " \t\"'" );
$removed[ trim( $line ) ] = self::SHIPPED_VALUES[ $name ] === $value;

$comments = self::SHIPPED_COMMENTS[ $name ];
$count = count( $comments );
if ( $i >= $count && array_slice( $norm, $i - $count, $count ) === $comments ) {
$drop += array_fill( $i - $count, $count, true );
if ( isset( $norm[ $i + 1 ] ) && '' === $norm[ $i + 1 ] ) {
$drop[ $i + 1 ] = true;
}
}
}

return [ implode( '', array_diff_key( $lines, $drop ) ), $removed ];
}

/**
* Opens php.ini for rewriting in place, so no other path is created, followed or re-resolved.
* The directory is mounted into the site's php container, so nothing in it is trusted.
*
* @param string $file php.ini path.
*
* @return resource|null Null when there is no regular file.
* @throws \Exception
*/
private static function open_ini( $file ) {

if ( is_link( $file ) || ! is_file( $file ) ) {
return null;
}

$handle = fopen( $file, 'r+' );
if ( false === $handle ) {
throw new \Exception( "Could not open $file." );
}

// Swapped for a symlink after the check, or a hard link to another file.
$opened = fstat( $handle );
clearstatcache( true, $file );
$linked = lstat( $file );
if ( ! $opened || ! $linked || $opened['dev'] !== $linked['dev'] || $opened['ino'] !== $linked['ino'] || 0100000 !== ( $linked['mode'] & 0170000 ) || 1 !== $opened['nlink'] ) {
fclose( $handle );
throw new \Exception( "$file is not a regular file." );
}

return $handle;
}

/**
* The removal only shortens the file, so writing over it needs no new space; mode and owner stay.
*
* @param resource $handle Handle from open_ini().
* @param string $content New content.
* @param string $file php.ini path, for the error.
*
* @throws \Exception
*/
private static function rewrite( $handle, $content, $file ) {

if ( ! rewind( $handle ) || strlen( $content ) !== fwrite( $handle, $content ) || ! fflush( $handle ) || ! ftruncate( $handle, strlen( $content ) ) ) {
throw new \Exception( "Could not write $file." );
}
}

/**
* PHP-FPM reads php.ini only when it starts; a graceful reload (as `ee site reload --php` does) applies it.
* Stopped containers read it when they start.
*
* @param Site $site Site.
*/
private static function reload_fpm( $site ) {

if ( ! $site->site_enabled || ! chdir( $site->site_fs_path ) ) {
return;
}

$compose = \EE_DOCKER::docker_compose_with_custom();
$id = trim( EE::launch( "$compose ps -q php" )->stdout );
if ( '' === $id || 'running' !== \EE_DOCKER::container_status( $id ) ) {
EE::debug( "The php container of $site->site_url is not running." );

return;
}

if ( EE::launch( "$compose exec -T php bash -c 'kill -USR2 1'" )->return_code ) {
EE::warning( sprintf( 'Could not reload PHP-FPM of %1$s. Run `ee site restart %1$s --php` to apply its new php.ini.', $site->site_url ) );
}
}
}
Binary file modified templates/config/php-fpm/php81.zip
Binary file not shown.
Binary file modified templates/config/php-fpm/php82.zip
Binary file not shown.
Binary file modified templates/config/php-fpm/php83.zip
Binary file not shown.
Binary file modified templates/config/php-fpm/php84.zip
Binary file not shown.
Binary file modified templates/config/php-fpm/php85.zip
Binary file not shown.
Binary file removed templates/config/php-fpm/phplatest.zip
Binary file not shown.
Loading