Limit the images that previews keep.
A preview that fails the captcha or the form token keeps no image. A session keeps one image, the directory has a size limit, and abandoned images go at the next upload.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
namespace Xxx\Tests\Pages;
|
||||
|
||||
use Xxx\Tests\Support\PageTestCase;
|
||||
use Xxx\Tests\Support\TestCase;
|
||||
|
||||
// Sending a story from publish/ as a visitor
|
||||
class SubmitTest extends PageTestCase {
|
||||
@@ -125,4 +126,81 @@ class SubmitTest extends PageTestCase {
|
||||
$this->assertStringNotContainsString('<script>alert(1)</script>', $page['body']);
|
||||
$this->assertStringContainsString('value=""><script>alert(1)</script>"', $page['body']);
|
||||
}
|
||||
|
||||
// Preview the story with an image; returns the answer
|
||||
private function previewImage(array $fields = array()): array {
|
||||
$token = $this->csrfToken($this->get('/publish')['body']);
|
||||
return $this->postFiles('/publish', $this->story($fields + array('action' => 'preview', 'csrf_token' => $token, 'g-recaptcha-response' => 'pass')),
|
||||
array('upload' => array('a.png', TestCase::png())));
|
||||
}
|
||||
|
||||
public function testAPreviewKeepsTheImageForTheSession(): void {
|
||||
$page = $this->previewImage();
|
||||
$this->assertStringContainsString('/display?pending=', $page['body']);
|
||||
$uploads = $this->uploads();
|
||||
$this->assertCount(1, $uploads);
|
||||
$this->assertSame(basename($uploads[0]), $this->session()['pending_upload']);
|
||||
}
|
||||
|
||||
public function testANewPreviewReplacesTheImage(): void {
|
||||
$this->previewImage();
|
||||
$first = $this->uploads();
|
||||
$this->postFiles('/publish', $this->story(array('action' => 'preview', 'csrf_token' => $this->session()['csrf_token'])),
|
||||
array('upload' => array('b.png', TestCase::png())));
|
||||
$second = $this->uploads();
|
||||
$this->assertCount(1, $second);
|
||||
$this->assertNotSame($first, $second);
|
||||
}
|
||||
|
||||
public function testAFailedCaptchaKeepsNoImage(): void {
|
||||
$page = $this->previewImage(array('g-recaptcha-response' => 'fail'));
|
||||
$this->assertStringContainsString('Please complete the captcha.', $page['body']);
|
||||
$this->assertSame(array(), $this->uploads());
|
||||
$this->assertArrayNotHasKey('pending_upload', $this->session());
|
||||
}
|
||||
|
||||
public function testAFailedTokenKeepsNoImage(): void {
|
||||
$page = $this->previewImage(array('csrf_token' => 'wrong'));
|
||||
$this->assertStringContainsString('Your session has expired.', $page['body']);
|
||||
$this->assertSame(array(), $this->uploads());
|
||||
}
|
||||
|
||||
public function testAFullUploadDirectoryKeepsNoImage(): void {
|
||||
// tests/php/fixtures/test.env sets UPLOAD_DIR_LIMIT_MB=1
|
||||
mkdir(self::$state . '/tmp/uploads', 0700);
|
||||
$filler = self::$state . '/tmp/uploads/' . str_repeat('f', 32);
|
||||
file_put_contents($filler, str_repeat('x', 1024 * 1024));
|
||||
$page = $this->previewImage();
|
||||
$this->assertStringContainsString('The site cannot keep more images just now.', $page['body']);
|
||||
$this->assertSame(array($filler), $this->uploads());
|
||||
}
|
||||
|
||||
public function testAbandonedUploadsGoAtTheNextUpload(): void {
|
||||
mkdir(self::$state . '/tmp/uploads', 0700);
|
||||
$old = self::$state . '/tmp/uploads/' . str_repeat('a', 32);
|
||||
$recent = self::$state . '/tmp/uploads/' . str_repeat('b', 32);
|
||||
file_put_contents($old, 'x');
|
||||
file_put_contents($recent, 'x');
|
||||
touch($old, time() - 86401);
|
||||
$this->previewImage();
|
||||
$this->assertFileDoesNotExist($old);
|
||||
$this->assertFileExists($recent);
|
||||
$this->assertCount(2, $this->uploads());
|
||||
}
|
||||
|
||||
public function testThePreviewedImageIsSavedThenRemoved(): void {
|
||||
$this->previewImage();
|
||||
$this->post('/publish', $this->story(array('action' => 'submit', 'uploaded' => 'pending', 'csrf_token' => $this->session()['csrf_token'])));
|
||||
$this->assertSame(TestCase::png(), $this->tables()['queue'][0]['img']);
|
||||
$this->assertSame(array(), $this->uploads());
|
||||
$this->assertArrayNotHasKey('pending_upload', $this->session());
|
||||
}
|
||||
|
||||
public function testSubmittingWithAnImageKeepsNoFile(): void {
|
||||
$token = $this->csrfToken($this->get('/publish')['body']);
|
||||
$this->postFiles('/publish', $this->story(array('action' => 'submit', 'csrf_token' => $token, 'g-recaptcha-response' => 'pass')),
|
||||
array('upload' => array('a.png', TestCase::png())));
|
||||
$this->assertSame(TestCase::png(), $this->tables()['queue'][0]['img']);
|
||||
$this->assertSame(array(), $this->uploads());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,13 +161,24 @@ abstract class PageTestCase extends \PHPUnit\Framework\TestCase {
|
||||
* name => list of values, 'body' => ...). Cookies go and come back as a
|
||||
* browser sends them, without regard to domain or the secure flag.
|
||||
*/
|
||||
protected function request(string $method, string $path, array $post = array(), array $headers = array()): array {
|
||||
protected function request(string $method, string $path, array $post = array(), array $headers = array(), array $files = array()): array {
|
||||
if ($this->cookies) {
|
||||
$headers['Cookie'] = implode('; ', array_map(function ($name, $value) { return "$name=" . rawurlencode($value); },
|
||||
array_keys($this->cookies), $this->cookies));
|
||||
}
|
||||
$options = array('method' => $method, 'follow_location' => 0, 'ignore_errors' => TRUE, 'timeout' => 30);
|
||||
if ($method === 'POST') {
|
||||
if ($method === 'POST' && $files) {
|
||||
// A multipart form; $files holds field => array(file name, content)
|
||||
$boundary = 'xxx-' . bin2hex(random_bytes(8));
|
||||
$body = '';
|
||||
foreach ($post as $name => $value)
|
||||
$body .= "--$boundary\r\nContent-Disposition: form-data; name=\"$name\"\r\n\r\n$value\r\n";
|
||||
foreach ($files as $name => list($filename, $content))
|
||||
$body .= "--$boundary\r\nContent-Disposition: form-data; name=\"$name\"; filename=\"$filename\"\r\n"
|
||||
. "Content-Type: application/octet-stream\r\n\r\n$content\r\n";
|
||||
$options['content'] = $body . "--$boundary--\r\n";
|
||||
$headers['Content-Type'] = "multipart/form-data; boundary=$boundary";
|
||||
} else if ($method === 'POST') {
|
||||
$options['content'] = http_build_query($post);
|
||||
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
@@ -207,6 +218,16 @@ abstract class PageTestCase extends \PHPUnit\Framework\TestCase {
|
||||
return $this->request('POST', $path, $post, $headers);
|
||||
}
|
||||
|
||||
// A POST with files: $files holds field => array(file name, content)
|
||||
protected function postFiles(string $path, array $post, array $files): array {
|
||||
return $this->request('POST', $path, $post, array(), $files);
|
||||
}
|
||||
|
||||
// The files in the upload directory of the server
|
||||
protected function uploads(): array {
|
||||
return glob(self::$state . '/tmp/uploads/*') ?: array();
|
||||
}
|
||||
|
||||
protected function header(array $response, string $name): ?string {
|
||||
return $response['headers'][strtolower($name)][0] ?? NULL;
|
||||
}
|
||||
|
||||
@@ -88,28 +88,64 @@ class UploadTest extends TestCase {
|
||||
$this->assertFalse(save_uploaded_file());
|
||||
}
|
||||
|
||||
public function testPruneDeletesTheSavedFileAndOldUploads(): void {
|
||||
$saved = $this->pending();
|
||||
$old = upload_dir() . '/' . str_repeat('c', 32);
|
||||
$new = upload_dir() . '/' . str_repeat('d', 32);
|
||||
$other = upload_dir() . '/keep-me';
|
||||
foreach (array($old, $new, $other) as $file)
|
||||
file_put_contents($file, 'x');
|
||||
touch($old, time() - 86401);
|
||||
touch($other, time() - 86401);
|
||||
// Files in the upload directory: an old upload, a new one and a file that is not an upload
|
||||
private function others(): array {
|
||||
$files = array('old' => upload_dir() . '/' . str_repeat('c', 32), 'new' => upload_dir() . '/' . str_repeat('d', 32),
|
||||
'other' => upload_dir() . '/keep-me');
|
||||
foreach ($files as $file)
|
||||
file_put_contents($file, 'xx');
|
||||
touch($files['old'], time() - 86401);
|
||||
touch($files['other'], time() - 86401);
|
||||
return $files;
|
||||
}
|
||||
|
||||
prune_images($saved);
|
||||
public function testPruneImagesDeletesTheSessionUploadAndOldUploads(): void {
|
||||
$saved = $this->pending();
|
||||
$files = $this->others();
|
||||
prune_images();
|
||||
$this->assertFileDoesNotExist($saved);
|
||||
$this->assertFileDoesNotExist($old);
|
||||
$this->assertFileExists($new);
|
||||
$this->assertFileExists($other);
|
||||
$this->assertFileDoesNotExist($files['old']);
|
||||
$this->assertFileExists($files['new']);
|
||||
$this->assertFileExists($files['other']);
|
||||
$this->assertArrayNotHasKey('pending_upload', $_SESSION);
|
||||
}
|
||||
|
||||
public function testPruneLeavesFilesOutsideTheUploadDirectory(): void {
|
||||
$outside = $this->tmp . '/' . str_repeat('e', 32);
|
||||
file_put_contents($outside, 'x');
|
||||
prune_images($outside);
|
||||
$this->assertFileExists($outside);
|
||||
public function testPruneUploadsKeepsTheSessionUploadUntilItIsOld(): void {
|
||||
$pending = $this->pending();
|
||||
prune_uploads();
|
||||
$this->assertFileExists($pending);
|
||||
touch($pending, time() - 86401);
|
||||
prune_uploads();
|
||||
$this->assertFileDoesNotExist($pending);
|
||||
}
|
||||
|
||||
public function testRemovePendingUploadTouchesOnlyTheSessionFile(): void {
|
||||
$files = $this->others();
|
||||
remove_pending_upload();
|
||||
$pending = $this->pending();
|
||||
remove_pending_upload();
|
||||
$this->assertFileDoesNotExist($pending);
|
||||
$this->assertArrayNotHasKey('pending_upload', $_SESSION);
|
||||
foreach ($files as $file)
|
||||
$this->assertFileExists($file);
|
||||
}
|
||||
|
||||
public function testUploadDirSizeCountsOnlyUploads(): void {
|
||||
$this->others();
|
||||
$this->pending('12345');
|
||||
$this->assertSame(2 + 2 + 5, upload_dir_size());
|
||||
}
|
||||
|
||||
public function testStoreUploadPrunesAndDropsTheLastUploadFirst(): void {
|
||||
$last = $this->pending();
|
||||
$files = $this->others();
|
||||
$file = $this->tmp . '/local';
|
||||
file_put_contents($file, 'x');
|
||||
// Not a real upload, so nothing is kept, but the old files still go
|
||||
$this->assertFalse(store_upload($file));
|
||||
$this->assertFileDoesNotExist($last);
|
||||
$this->assertFileDoesNotExist($files['old']);
|
||||
$this->assertFileExists($files['new']);
|
||||
$this->assertArrayNotHasKey('pending_upload', $_SESSION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ RECAPTCHA_SITE_KEY=test-site-key
|
||||
RECAPTCHA_SECRET=test-secret
|
||||
GOOGLE_ANALYTICS_ID=
|
||||
UPLOAD_DIR=
|
||||
UPLOAD_DIR_LIMIT_MB=1
|
||||
DISPLAY_ERRORS=
|
||||
|
||||
Reference in New Issue
Block a user