Problem/Motivation
#3308744: Fix magic connection property access from \Drupal\Core\FileTransfer\FileTransfer::__get() Introduced magic methods to access Drupal\Core\FileTransfer\FTPExtension->connection
however now FTPExtension->connect()
always throws an exception (which is caught) due to nested calls to __get()
.
Error:
Notice: Undefined property: Drupal\Core\FileTransfer\FTPExtension::$connection in Drupal\Core\FileTransfer\FTPExtension->connect() (line 16 of core/lib/Drupal/Core/FileTransfer/FTPExtension.php).
Cannot connect to FTP Server, check settings
Why:
1. Call FTPExtension->connect();
in code.
2. FTPExtension->connect()
sets the $connection
property
3. $connection
is set via parent class FileTransfer's private property $connectionHandle
via __set()
4. FTPExtension->connect()
then checks if $connection
property was set to non-Falsey value.
5. This triggers FileTransfer's __get()
which calls FTPExtension->connect()
4. Repeat step 2 ( FTPExtension->connect()
sets the $connection
property)
5. Repeat step 3 ( $connection
is set via parent class FileTransfer's private property $connectionHandle
via __set()
)
6. Repeat step 4 ( FTPExtension->connect()
then checks if $connection
property was set to non-Falsey value.)
7. FileTransfer's __get()
is not triggered as __get()
has already been called in the call stack with the same parameters and so PHP treats it as regular property which is undefined (since the real property is stored in private property $connectionHandle
), so the else condition is triggered which is throwing an exception. (If it didn't it, would be an infinite loop which is equally bad)
Supporting information regarding Zend PHP on nested magic methods.
@see https://github.com/facebook/hhvm/issues/1312
For reference, the PHP behaviour allows calls to magic methods to recurse as long as there isn't an identical call on the stack, this means you can still trigger a magic method from within a magic method as long as the call itself is different.
Steps to reproduce
How to throw the error in custom code:
use Drupal\Core\FileTransfer\FileTransferException;
$data = NULL;
$url = 'ftp://ftp.myserver.com/';
$info = drupal_get_filetransfer_info();
$class = $info['ftp']['class'];
$ftp = $class::factory(DRUPAL_ROOT, [
'advanced' => [
'hostname' => parse_url($url, PHP_URL_HOST),
],
'username' => 'anonymous',
'port' => 21,
]);
try {
// Login, throws exception if a login error, else defines $ftp->connection.
// Exceptio nis thrown here.
$ftp->connect();
// CODE DOES NOT GET TO THIS POINT, demo purposes to get file content.
ftp_pasv($ftp->connection, TRUE);
ob_start();
if (ftp_get($ftp->connection, 'php://output', parse_url($url, PHP_URL_PATH), FTP_BINARY)) {
$data = ob_get_contents();
ob_end_clean();
}
ftp_close($ftp->connection);
}
catch (FileTransferException $e) {
\Drupal::logger()->error($e->getMessage());
\Drupal::messenger()->addError($e->getMessage());
}
// Echo file data if successful.
if ($data) {
echo $data;
}
Mock code showing the issue:
class PropertyTest
{
/** Location for overloaded data. */
private $myPrivate;
public function __set($name, $value) {
if ($name == 'myVar') {
echo "Setting 'myVar' to '$value'\n";
$this->myPrivate = $value;
}
else {
echo "SET FAILED\n";
}
}
public function __get($name){
echo "Getting 'myVar'\n";
$this->connect();
return ($name == 'myVar') ? $this->myPrivate : 'GET FAILED';
}
}
class PropertyTestchild extends PropertyTest {
public function connect() {
$this->myVar = 'TEST';
echo "Parent connect run\n";
if (!$this->myVar) {
echo "This property 'myVar' was not set?!\n";
}
}
}
echo "<pre>\n";
$obj = new PropertyTestchild();
$obj->connect();
echo "</pre>\n";
Output:
Setting 'myVar' to 'TEST'
Parent connect run
Getting 'myVar'
Setting 'myVar' to 'TEST'
Parent connect run
Warning: Undefined property: PropertyTestchild::$myVar in /home/user/scripts/code.php on line 26
This property 'myVar' was not set?!
Proposed resolution
Only run `connect()` once or revert to previous commit.
One run once dummy code:
class PropertyTest
{
/** Location for overloaded data. */
private $myPrivate;
public function __set($name, $value) {
if ($name == 'myVar') {
echo "Setting 'myVar' to '$value'\n";
$this->myPrivate = $value;
}
else {
echo "SET FAILED\n";
}
}
public function __get($name){
echo "Getting 'data'\n";
$this->connect();
return ($name == 'myVar') ? $this->myPrivate : 'GET FAILED';
}
}
class PropertyTestchild extends PropertyTest {
private $hasConnectRun = FALSE;
public function connect() {
if ($this->hasConnectRun) {
return;
}
$this->hasConnectRun = TRUE;
$this->myVar = 'TEST';
echo "Parent connect run\n";
if (!$this->myVar) {
echo "This property 'myVar' was not set?!\n";
}
}
}
echo "<pre>\n";
$obj = new PropertyTestchild();
$obj->connect();
echo "</pre>\n";
Output:
Setting 'myVar' to 'TEST'
Parent connect run
Getting 'data'
Remaining tasks
User interface changes
API changes
Data model changes
Release notes snippet