Skip to content

Commit fd6a1bc

Browse files
authored
Merge pull request #63765 from nextcloud/backport/63510/stable32
[stable32] fix(dav): only derive the write size from a PUT Content-Length
2 parents 644cf2f + af494d7 commit fd6a1bc

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
@@ -205,8 +205,15 @@ public function put($data) {
205205
}
206206
}
207207

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

211218
if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
212219
$isEOF = false;
@@ -256,7 +263,6 @@ public function put($data) {
256263
// compare expected and actual size
257264
if ($expected !== null
258265
&& $expected !== $count
259-
&& $this->request->getMethod() === 'PUT'
260266
) {
261267
throw new BadRequest(
262268
$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
@@ -207,6 +207,75 @@ function ($path) use ($storage) {
207207
$this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
208208
}
209209

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

0 commit comments

Comments
 (0)