Skip to content

Commit e8edb07

Browse files
authored
Merge pull request #63678 from nextcloud/backport/63510/stable34
[stable34] fix(dav): only derive the write size from a PUT Content-Length
2 parents ea90cb0 + 1256f4c commit e8edb07

2 files changed

Lines changed: 78 additions & 3 deletions

File tree

apps/dav/lib/Connector/Sabre/File.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,15 @@ public function put($data) {
207207
}
208208
}
209209

210-
$lengthHeader = $this->request->getHeader('content-length');
211-
$expected = $lengthHeader !== '' ? (int)$lengthHeader : null;
210+
// Methods other than PUT carry no Content-Length describing the data written
211+
// here: the chunked upload assembly is a MOVE or COPY with no body of its own.
212+
$expected = null;
213+
if ($this->request->getMethod() === 'PUT') {
214+
$lengthHeader = $this->request->getHeader('content-length');
215+
if ($lengthHeader !== '') {
216+
$expected = (int)$lengthHeader;
217+
}
218+
}
212219

213220
if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
214221
$isEOF = false;
@@ -264,7 +271,6 @@ public function put($data) {
264271
// compare expected and actual size
265272
if ($expected !== null
266273
&& $expected !== $count
267-
&& $this->request->getMethod() === 'PUT'
268274
) {
269275
throw new BadRequest(
270276
$this->l10n->t(

apps/dav/tests/unit/Connector/Sabre/FileTest.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,75 @@ function ($path) use ($storage) {
215215
$this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
216216
}
217217

218+
public static function expectedSizeProvider(): array {
219+
return [
220+
'PUT with a length passes it through' => ['PUT', ['CONTENT_LENGTH' => '9'], 9],
221+
'PUT of an empty body still expects zero' => ['PUT', ['CONTENT_LENGTH' => '0'], 0],
222+
'PUT without the header expects nothing' => ['PUT', [], null],
223+
// The chunked upload assembly reaches put() as a MOVE or COPY, where the
224+
// Content-Length describes the request rather than the assembled stream.
225+
'MOVE ignores a zero length' => ['MOVE', ['CONTENT_LENGTH' => '0'], null],
226+
'MOVE ignores a non-zero length' => ['MOVE', ['CONTENT_LENGTH' => '9'], null],
227+
'COPY ignores a zero length' => ['COPY', ['CONTENT_LENGTH' => '0'], null],
228+
'COPY ignores a non-zero length' => ['COPY', ['CONTENT_LENGTH' => '9'], null],
229+
];
230+
}
231+
232+
/**
233+
* The expected size handed to IWriteStreamStorage::writeStream() may only come from
234+
* the Content-Length of a PUT body. Passing it on for other methods makes storages
235+
* that only measure the stream when given no size - ObjectStoreStorage among them -
236+
* write a truncated or empty file.
237+
*/
238+
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'expectedSizeProvider')]
239+
public function testPutExpectedSizeOnlyComesFromPutContentLength(string $method, array $server, ?int $expectedSize): void {
240+
$storage = $this->getMockBuilder(Local::class)
241+
->onlyMethods(['writeStream'])
242+
->setConstructorArgs([['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]])
243+
->getMock();
244+
Filesystem::mount($storage, [], $this->user . '/');
245+
246+
/** @var View&MockObject $view */
247+
$view = $this->getMockBuilder(View::class)
248+
->onlyMethods(['getRelativePath', 'resolvePath'])
249+
->getMock();
250+
$view->expects($this->atLeastOnce())
251+
->method('resolvePath')
252+
->willReturnCallback(fn ($path) => [$storage, $path]);
253+
$view->expects($this->any())
254+
->method('getRelativePath')
255+
->willReturnArgument(0);
256+
257+
$receivedSize = false;
258+
$storage->expects($this->once())
259+
->method('writeStream')
260+
->willReturnCallback(function (string $path, $stream, ?int $size = null) use (&$receivedSize): int {
261+
$receivedSize = $size;
262+
return (int)stream_copy_to_stream($stream, fopen('php://temp', 'r+'));
263+
});
264+
265+
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
266+
'permissions' => Constants::PERMISSION_ALL,
267+
'type' => FileInfo::TYPE_FOLDER,
268+
], null);
269+
270+
$request = new Request([
271+
'server' => $server,
272+
'method' => $method,
273+
], $this->requestId, $this->config, null);
274+
275+
$file = new File($view, $info, null, $request);
276+
277+
try {
278+
$file->put($this->getStream('test data'));
279+
} catch (\Exception $e) {
280+
// Whatever happens after the write - size checks, renaming the part file - is
281+
// not what this test is about.
282+
}
283+
284+
$this->assertSame($expectedSize, $receivedSize);
285+
}
286+
218287
/**
219288
* Simulate putting a file to the given path.
220289
*

0 commit comments

Comments
 (0)