Skip to content
Draft
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
106 changes: 55 additions & 51 deletions lib/private/Files/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -617,66 +617,70 @@ protected function emit_file_hooks_post(bool $exists, string $path): void {
* @throws LockedException
*/
public function file_put_contents($path, $data) {
if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
if (Filesystem::isValidPath($path)
&& !Filesystem::isFileBlacklisted($path)
) {
$path = $this->getRelativePath($absolutePath);
if ($path === null) {
throw new InvalidPathException("Path $absolutePath is not in the expected root");
}
// not having to deal with streams in file_put_contents makes life easier
if (!is_resource($data)) {
$hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
return $this->basicOperation('file_put_contents', $path, $hooks, $data);
}

$this->lockFile($path, ILockingProvider::LOCK_SHARED);
$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
if (!Filesystem::isValidPath($path)
|| Filesystem::isFileBlacklisted($path)
) {
return false;
}

$path = $this->getRelativePath($absolutePath);
if ($path === null) {
throw new InvalidPathException("Path $absolutePath is not in the expected root");
}

$exists = $this->file_exists($path);
if ($this->shouldEmitHooks($path)) {
$run = true;
$this->emit_file_hooks_pre($exists, $path, $run);
if (!$run) {
$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
return false;
}
}
$this->lockFile($path, ILockingProvider::LOCK_SHARED);
$lockType = ILockingProvider::LOCK_SHARED;

try {
$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
} catch (\Exception $e) {
// Release the shared lock before throwing.
$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
throw $e;
try {
$exists = $this->file_exists($path);
if ($this->shouldEmitHooks($path)) {
$run = true;
$this->emit_file_hooks_pre($exists, $path, $run);
if (!$run) {
return false;
}
}

/** @var Storage $storage */
[$storage, $internalPath] = $this->resolvePath($path);
$target = $storage->fopen($internalPath, 'w');
if ($target) {
$result = stream_copy_to_stream($data, $target);
if ($result !== false) {
$result = true;
}
fclose($target);
fclose($data);
$this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
$lockType = ILockingProvider::LOCK_EXCLUSIVE;

$this->writeUpdate($storage, $internalPath);
/** @var Storage $storage */
[$storage, $internalPath] = $this->resolvePath($path);
$target = $storage->fopen($internalPath, 'w');
if (!$target) {
return false;
}

$this->changeLock($path, ILockingProvider::LOCK_SHARED);
try {
$result = stream_copy_to_stream($data, $target);
} finally {
fclose($target);
fclose($data);
}

if ($this->shouldEmitHooks($path) && $result !== false) {
$this->emit_file_hooks_post($exists, $path);
}
$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
return $result;
} else {
$this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
return false;
}
} else {
return false;
if ($result !== false) {
$result = true;
}
} else {
$hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
return $this->basicOperation('file_put_contents', $path, $hooks, $data);

$this->writeUpdate($storage, $internalPath);

$this->changeLock($path, ILockingProvider::LOCK_SHARED);
$lockType = ILockingProvider::LOCK_SHARED;

if ($this->shouldEmitHooks($path) && $result !== false) {
$this->emit_file_hooks_post($exists, $path);
}

return $result;
} finally {
$this->unlockFile($path, $lockType);
}
}

Expand Down
35 changes: 35 additions & 0 deletions tests/lib/Files/ViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2017,6 +2017,41 @@ function () use ($view, $path, &$lockTypeDuring) {
$this->assertNull($this->getFileLockType($view, $path));
}

public function testLockFilePutContentsWithStreamUnlocksAfterStorageException(): void {
$view = new View('/' . self::$user . '/files/');
$path = 'failed-stream-upload.txt';

/** @var Temporary&MockObject $storage */
$storage = $this->getMockBuilder(Temporary::class)
->onlyMethods(['fopen'])
->getMock();

Filesystem::mount($storage, [], self::$user . '/');
$storage->mkdir('files');

$storage->expects($this->once())
->method('fopen')
->willThrowException(new \Exception('Simulated stream upload failure'));

$source = fopen('php://temp', 'r+');

try {
$view->file_put_contents($path, $source);
$this->fail('Expected the storage exception to be rethrown');
} catch (\Exception $e) {
$this->assertSame('Simulated stream upload failure', $e->getMessage());
} finally {
if (is_resource($source)) {
fclose($source);
}
}

$this->assertNull(
$this->getFileLockType($view, $path),
'The stream-upload lock must be released after a storage exception'
);
}

/**
* Test locks for fopen with fclose at the end
*/
Expand Down
Loading