', $body);
$this->assertStringContainsString('wordcounter.hidden = false;', $body);
}
public function testTheImageTabKeepsTheSpaceOfTheNote(): void {
$html = new \DOMDocument();
$html->loadHTML($this->get('/publish')['body'], LIBXML_NOERROR);
$xpath = new \DOMXPath($html);
$note = $xpath->query('//div[@id="textparent"]/div[@class="note"]');
$spacer = $xpath->query('//div[@id="uploadparent"]/div[@class="note spacer"][@aria-hidden="true"]');
$this->assertCount(1, $note);
$this->assertCount(1, $spacer);
$this->assertSame($note[0]->textContent, $spacer[0]->textContent);
$this->assertCount(1, $xpath->query('//div[@id="uploadparent"]/div[@class="upload"]//input[@id="upload"]'));
// Both tabs sit in the same column
$this->assertCount(2, $xpath->query('//div[@class="textarea rightcolumn"]/div[@id="textparent" or @id="uploadparent"]'));
}
public function testEditorsGetNoCaptcha(): void {
$this->loginAs();
$body = $this->get('/publish')['body'];
$this->assertStringNotContainsString('recaptcha', $body);
}
public function testPreviewThenSubmitAddsTheStoryToTheQueue(): void {
$token = $this->csrfToken($this->get('/publish')['body']);
$preview = $this->post('/publish', $this->story(array('action' => 'preview', 'csrf_token' => $token, 'g-recaptcha-response' => 'pass')));
$this->assertStringContainsString('Please review your submission.', $preview['body']);
// The preview shows the purified story; the typography comes when it is saved
$this->assertStringContainsString('
Once -- upon a "time".
', $preview['body']);
$this->assertStringContainsString('
', $preview['body']);
// The solved captcha lasts until the story is saved
$this->assertStringNotContainsString('g-recaptcha', $preview['body']);
$this->assertSame(array(), $this->queries('/^INSERT/'));
$this->post('/publish', $this->story(array('action' => 'submit', 'csrf_token' => $token)));
$tables = $this->tables();
$this->assertCount(1, $tables['xxx']);
$this->assertCount(1, $tables['queue']);
$row = $tables['queue'][0];
$this->assertSame(array('My story', 'Vi', 'vi@example.org', 'Once — upon a “time”.
', 'original author', 0),
array($row['title'], $row['author'], $row['email'], $row['story'], $row['editor'], $row['hidden']));
$session = $this->session();
$this->assertArrayNotHasKey('captcha_ok', $session);
$this->assertSame($row['id'], (string)$session['allow_queue_image']);
$mails = $this->mails();
$this->assertCount(1, $mails);
$this->assertSame('New submission: My story', $mails[0]['subject']);
$this->assertStringContainsString('Once — upon a “time”.', $mails[0]['body']);
}
public function testVisitorsCannotPostOrOverwrite(): void {
$token = $this->csrfToken($this->get('/publish')['body']);
$this->post('/publish', $this->story(array('action' => 'post', 'id' => '1', 'hidden' => 'on', 'editor' => 'ed',
'comments' => 'x', 'csrf_token' => $token, 'g-recaptcha-response' => 'pass')));
$tables = $this->tables();
$this->assertSame('Live', $tables['xxx'][0]['title']);
$this->assertCount(1, $tables['xxx']);
$this->assertCount(1, $tables['queue']);
$this->assertSame(array('original author', 0, ''), array($tables['queue'][0]['editor'], $tables['queue'][0]['hidden'], $tables['queue'][0]['comments']));
}
public function testVisitorsCannotLoadAStoryIntoTheForm(): void {
$this->setTables(array('xxx' => array(array('id' => 7, 'title' => 'Live one', 'email' => 'author@example.org'))));
$token = $this->csrfToken($this->get('/publish')['body']);
foreach (array('edit', 'choose', 'other') as $action) {
$page = $this->post('/publish', array('action' => $action, 'id' => '7', 'csrf_token' => $token));
$this->assertStringNotContainsString('author@example.org', $page['body']);
$this->assertStringNotContainsString('Live one', $page['body']);
}
$this->assertSame(array(), $this->queries('/^SELECT \* FROM xxx/'));
}
public function testAPostWithoutTheTokenOnlyGetsAPreview(): void {
$this->get('/publish');
$page = $this->post('/publish', $this->story(array('action' => 'submit', 'csrf_token' => 'wrong', 'g-recaptcha-response' => 'pass')));
$this->assertStringContainsString('Your session has expired. Please check your story, then send it again.', $page['body']);
$this->assertSame(array(), $this->queries('/^INSERT/'));
$this->assertSame(array(), $this->mails());
}
public function testAFailedCaptchaStopsTheStory(): void {
$token = $this->csrfToken($this->get('/publish')['body']);
$page = $this->post('/publish', $this->story(array('action' => 'submit', 'csrf_token' => $token, 'g-recaptcha-response' => 'fail')));
$this->assertStringContainsString('Please complete the captcha.', $page['body']);
$this->assertStringContainsString('g-recaptcha', $page['body']);
$this->assertSame(array(), $this->queries('/^INSERT/'));
}
public static function failedAssessments(): array {
return array(array('wrong-host'), array('wrong-action'), array('api-error'), array('unreachable'));
}
#[DataProvider('failedAssessments')]
public function testAFailedAssessmentBlocksTheSubmission(string $response): void {
$token = $this->csrfToken($this->get('/publish')['body']);
$page = $this->post('/publish', $this->story(array('action' => 'submit', 'csrf_token' => $token, 'g-recaptcha-response' => $response)));
$this->assertStringContainsString('Please complete the captcha.', $page['body']);
$this->assertSame(array(), $this->queries('/^INSERT/'));
$this->assertSame(array(), $this->mails());
$this->assertArrayNotHasKey('captcha_ok', $this->session());
}
public function testMissingFieldsAreShown(): void {
$token = $this->csrfToken($this->get('/publish')['body']);
$page = $this->post('/publish', array('action' => 'submit', 'csrf_token' => $token, 'g-recaptcha-response' => 'pass', 'story' => 'x
'));
$this->assertStringContainsString('You have not submitted an author. ', $page['body']);
$this->assertStringContainsString('', $page['body']);
$this->assertSame(array(), $this->queries('/^INSERT/'));
}
public function testATooLargeImageIsRefused(): void {
$page = $this->previewImage(array(), str_pad(TestCase::png(), 1024 * 1024 + 1, "\0"));
$this->assertStringContainsString('a.png is larger than 1 MB. Please choose a smaller image.', $page['body']);
$this->assertSame(array(), $this->uploads());
}
public function testAnArrayActionIsIgnored(): void {
$page = $this->post('/publish', array('action' => array('post')));
$this->assertSame(200, $page['status']);
$this->assertStringNotContainsString('Please review your submission.', $page['body']);
$this->assertSame(array(), $this->queries('/^INSERT/'));
}
public function testFieldsAreEscapedInTheForm(): void {
$token = $this->csrfToken($this->get('/publish')['body']);
$page = $this->post('/publish', $this->story(array('action' => 'preview', 'csrf_token' => $token, 'g-recaptcha-response' => 'pass',
'title' => '">')));
$this->assertStringNotContainsString('', $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(), ?string $image = NULL): 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', $image ?? 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);
}
#[DataProvider('uploadActions')]
public function testANestedUploadIsRejectedWithoutSaving(string $action): void {
$token = $this->csrfToken($this->get('/publish')['body']);
$before = $this->tables();
$page = $this->postFiles('/publish', $this->story(array('action' => $action, 'csrf_token' => $token,
'g-recaptcha-response' => 'pass')),
array('upload[]' => array('a.png', TestCase::png())));
$this->assertSame(200, $page['status']);
$this->assertStringContainsString('The upload is invalid. Please choose the image again.', $page['body']);
$this->assertSame($before, $this->tables());
$this->assertSame(array(), $this->uploads());
$this->assertArrayNotHasKey('pending_upload', $this->session());
}
public static function uploadActions(): array {
return array('preview' => array('preview'), 'submit' => array('submit'));
}
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());
}
}