array(array('id' => 1, 'title' => 'Live', 'date' => '2020-01-01 10:00:00', 'story' => '

Old.

')), 'queue' => array(array('id' => 5, 'title' => 'Waiting', 'date' => '2020-03-01 10:00:00', 'story' => '', 'img' => 'QUEUED IMAGE', 'mime' => 'image/gif')), ); protected function setUp(): void { parent::setUp(); SiteData::install(self::TABLES); open_db('open'); } // Save as an editor, with $action and the POST fields given private function save(string $action, array $post, array $get = array()): string { $this->asEditor(); $GLOBALS['action'] = $action; $this->request($get, $post + array('title' => 'Title', 'author' => 'Ann', 'story' => '

Text.

')); return (string)save_data(); } private function pendingImage(): string { $name = bin2hex(random_bytes(16)); file_put_contents(upload_dir() . "/$name", self::png()); $_SESSION['pending_upload'] = $name; return upload_dir() . "/$name"; } public function testEditorSavesANewStoryToTheQueue(): void { $id = $this->save('save', array('email' => 'ann@example.org', 'comments' => 'Note', 'editor' => 'ed', 'hidden' => '1', 'date' => '2021-05-06 07:08:09')); $this->assertSame('6', $id); $row = SiteData::find('queue', 6); $this->assertSame(array('Title', 'Ann', 'ann@example.org', '

Text.

', 'Note', 'ed', 1, '2021-05-06 07:08:09'), array($row['title'], $row['author'], $row['email'], $row['story'], $row['comments'], $row['editor'], $row['hidden'], $row['date'])); $this->assertCount(1, SiteData::$tables['xxx']); } public function testANewStoryGetsTheDateNow(): void { $this->save('save', array()); $date = SiteData::find('queue', 6)['date']; $this->assertEqualsWithDelta(time(), strtotime($date . ' UTC'), 5); } public function testEditorUpdatesAQueuedStoryInPlace(): void { $id = $this->save('save', array('id' => '5', 'title' => 'Changed', 'date' => '2030-01-01 00:00:00')); $this->assertSame('5', $id); $this->assertSame('Changed', SiteData::find('queue', 5)['title']); // An update keeps the date $this->assertSame('2020-03-01 10:00:00', SiteData::find('queue', 5)['date']); $update = FakeDb::matching('/^UPDATE queue/')[0]; $this->assertStringEndsWith('WHERE `id` = ? LIMIT 1', $update['sql']); $this->assertSame(5, end($update['params'])); } public function testEditorUpdatesALiveStory(): void { $this->assertSame('1', $this->save('post', array('id' => '1', 'title' => 'Edited'))); $this->assertSame('Edited', SiteData::find('xxx', 1)['title']); $this->assertCount(1, SiteData::$tables['xxx']); } public function testPostingFromTheQueueMovesTheStory(): void { $id = $this->save('post', array('id' => '5', 'title' => 'Now live', 'date' => '2021-01-01 00:00:00'), array('queue' => '1')); $this->assertSame('6', $id); $row = SiteData::find('xxx', 6); $this->assertSame(array('Now live', '2021-01-01 00:00:00', '

Text.

', NULL), array($row['title'], $row['date'], $row['story'], $row['img'])); $this->assertNull(SiteData::find('queue', 5)); $this->assertFalse($GLOBALS['queue']); } public function testPostingFromTheQueueCanKeepTheQueuedImage(): void { $this->save('post', array('id' => '5', 'uploaded' => '5'), array('queue' => '1')); $row = SiteData::find('xxx', 6); $this->assertSame(array('QUEUED IMAGE', 'image/gif', ''), array($row['img'], $row['mime'], $row['story'])); } public function testAPendingUploadBecomesTheImage(): void { $file = $this->pendingImage(); $this->save('save', array('uploaded' => 'pending')); $row = SiteData::find('queue', 6); $this->assertSame(array(self::png(), 'image/png', ''), array($row['img'], $row['mime'], $row['story'])); $this->assertFileDoesNotExist($file); $this->assertArrayNotHasKey('pending_upload', $_SESSION); } public function testRemovingTheImageKeepsTheText(): void { $this->save('save', array('id' => '5', 'remove_img' => '1', 'story' => '

Words.

')); $row = SiteData::find('queue', 5); $this->assertSame(array(NULL, NULL, '

Words.

'), array($row['img'], $row['mime'], $row['story'])); } public function testTheStoryIsSanitised(): void { $this->save('save', array('story' => '

a -- b

')); $this->assertSame('

a — b

', SiteData::find('queue', 6)['story']); } public function testValuesAreBoundAsParameters(): void { $this->save('save', array('title' => "O'Brien\"; DROP TABLE xxx; --")); $insert = FakeDb::matching('/^INSERT INTO queue/')[0]; $this->assertStringNotContainsString('DROP', $insert['sql']); $this->assertContains("O'Brien\"; DROP TABLE xxx; --", $insert['params']); } public static function failedQueries(): array { return array( 'visitor insert' => array(FALSE, 'post', array(), array(), '/^INSERT INTO queue/'), 'editor insert' => array(TRUE, 'submit', array(), array(), '/^INSERT INTO queue/'), 'queue update' => array(TRUE, 'submit', array('id' => '5'), array(), '/^UPDATE queue/'), 'live update' => array(TRUE, 'post', array('id' => '1'), array(), '/^UPDATE xxx/'), 'move from queue' => array(TRUE, 'post', array('id' => '5'), array('queue' => '1'), '/^INSERT INTO xxx/'), 'queue image lookup' => array(TRUE, 'post', array('id' => '5', 'uploaded' => '5'), array('queue' => '1'), '/^SELECT img, mime FROM queue/'), ); } #[DataProvider('failedQueries')] public function testAFailedQueryKeepsTheStoryAndUploadWithoutSendingMail(bool $admin, string $action, array $post, array $get, string $pattern): void { $pending = $this->pendingImage(); $session = $_SESSION + array('captcha_ok' => time()); $state = $this->tmp . '/failed-save'; file_put_contents($state, serialize(array( 'tables' => self::TABLES, 'admin' => $admin, 'action' => $action, 'pattern' => $pattern, 'post' => $post + array('title' => 'Title', 'author' => 'Ann', 'story' => '

Text.

', 'uploaded' => 'pending'), 'get' => $get, 'session' => $session, ))); $file = var_export($state, TRUE); $code = << \$before, 'tables' => Xxx\Tests\Support\SiteData::\$tables, 'session' => \$_SESSION, 'queries' => Xxx\Tests\Support\FakeDb::queries(), 'returned' => \$returned, 'status' => http_response_code(), ))); }); save_data(); \$returned = TRUE; PHP; $result = Mail::run($code, array('UPLOAD_DIR' => $this->tmp . '/uploads')); $this->assertSame(0, $result['status'], $result['stderr']); $this->assertSame('', $result['stderr']); $this->assertSame('

Sorry, something went wrong. Please try again later.

', $result['stdout']); $this->assertStringContainsString('xxx: database error at post.lib.php:', $result['log']); $this->assertStringNotContainsString('Warning', $result['log']); $this->assertStringNotContainsString('Notice', $result['log']); $this->assertSame(array(), $result['mails']); $saved = unserialize(file_get_contents($state)); $this->assertFalse($saved['returned']); $this->assertSame(500, $saved['status']); $this->assertSame($saved['before'], $saved['tables']); $this->assertSame($session, $saved['session']); $this->assertFileExists($pending); $this->assertSame(self::png(), file_get_contents($pending)); $this->assertCount(1, preg_grep($pattern, $saved['queries'])); $this->assertMatchesRegularExpression($pattern, end($saved['queries']), 'no queries run after the failure'); } // Visitors always add to the queue, and each story sends mail public function testVisitorsCanOnlyAddToTheQueue(): void { $state = $this->tmp . '/state'; file_put_contents($state, serialize(array( 'tables' => self::TABLES, 'post' => array('id' => '1', 'title' => 'Mine', 'author' => 'Vi', 'story' => '

Hi.

', 'comments' => 'sneaky', 'editor' => 'ed', 'hidden' => '1', 'date' => '1999-01-01 00:00:00'), 'session' => array('captcha_ok' => time()), ))); $file = var_export($state, TRUE); $code = << \$id, 'tables' => Xxx\Tests\Support\SiteData::\$tables, 'session' => \$_SESSION, 'action' => \$action))); PHP; $result = Mail::run($code, array('UPLOAD_DIR' => $this->tmp . '/uploads')); $this->assertSame(0, $result['status'], $result['stderr']); $this->assertSame('', $result['log']); $saved = unserialize(file_get_contents($state)); $this->assertSame('6', (string)$saved['id']); $this->assertSame('submit', $saved['action']); $this->assertSame(self::TABLES['xxx'][0]['title'], $saved['tables']['xxx'][0]['title']); $row = end($saved['tables']['queue']); $this->assertSame(array('6', 'Mine', 'original author', 0, '', '

Hi.

'), array($row['id'], $row['title'], $row['editor'], $row['hidden'], $row['comments'], $row['story'])); $this->assertNotSame('1999-01-01 00:00:00', $row['date']); $this->assertSame(array('allow_queue_image' => '6'), $saved['session']); $this->assertCount(1, $result['mails']); $this->assertSame('New submission: Mine', $result['mails'][0]['subject']); $this->assertStringContainsString('Vi has sent the following new submission', $result['mails'][0]['body']); } }