array('7', 7), 'spaces around' => array(' 7 ', 7), 'negative' => array('-3', 0), 'above the maximum' => array('500', 100), 'zero' => array('0', 0), 'decimal' => array('7.5', NULL), 'trailing text' => array('7abc', NULL), 'leading zero' => array('07', NULL), 'plus sign' => array('+7', 7), 'exponent' => array('1e3', NULL), 'SQL' => array('1; DROP TABLE xxx', NULL), 'empty' => array('', NULL), ); } #[DataProvider('intArgs')] public function testGetIntArg(string $value, ?int $expected): void { $this->request(array('n' => $value)); $this->assertSame($expected, get_int_arg('n', 0, 100)); } public function testGetIntArgMissingOrNotAString(): void { $this->assertNull(get_int_arg('n', 0)); $this->request(array('n' => array('1'))); $this->assertNull(get_int_arg('n', 0)); } public function testGetIntArgHasNoMaximumByDefault(): void { $this->request(array('n' => '123456789')); $this->assertSame(123456789, get_int_arg('n', 0)); } public function testShowNumComesFromTheRequest(): void { $this->request(array('show' => '12')); $this->assertSame(12, get_show_num()); } public function testShowNumIsCappedAndAtLeastOne(): void { $this->request(array('show' => '100000')); $this->assertSame(MAX_SHOW, get_show_num()); $this->request(array('show' => '0')); $this->assertSame(1, get_show_num()); } public function testShowNumFallsBackToThePageSetting(): void { $GLOBALS['num_stories'] = 5; $this->assertSame(5, get_show_num()); $this->request(array('show' => 'many')); $this->assertSame(5, get_show_num()); } public function testShowNumFallsBackToOne(): void { $this->assertSame(1, get_show_num()); } public function testStartNum(): void { $this->assertSame(0, get_start_num()); $this->request(array('start' => '15')); $this->assertSame(15, get_start_num()); $this->request(array('start' => '-15')); $this->assertSame(0, get_start_num()); $this->request(array('start' => '15 OR 1=1')); $this->assertSame(0, get_start_num()); } public static function sorts(): array { return array( array(NULL, 'date'), array('author', 'author'), array('title', 'title'), array('date', 'date'), array('id', 'date'), array('title; DROP TABLE xxx', 'date'), array('TITLE', 'date'), ); } #[DataProvider('sorts')] public function testSortArgAllowsOnlyKnownColumns(?string $sort, string $expected): void { if ($sort !== NULL) $this->request(array('sort' => $sort)); $this->assertSame($expected, get_sort_arg()); } public function testSortArgIgnoresArrays(): void { $this->request(array('sort' => array('title'))); $this->assertSame('date', get_sort_arg()); } public static function orders(): array { return array( 'date default' => array(array(), 'desc'), 'title default' => array(array('sort' => 'title'), 'asc'), 'author default' => array(array('sort' => 'author'), 'asc'), 'asc' => array(array('order' => 'asc'), 'asc'), 'DESC' => array(array('order' => 'DESC', 'sort' => 'title'), 'DESC'), 'injection' => array(array('order' => 'desc, (SELECT 1)'), 'desc'), 'unknown' => array(array('order' => 'sideways', 'sort' => 'title'), 'asc'), 'array' => array(array('order' => array('asc')), 'desc'), ); } #[DataProvider('orders')] public function testOrderAllowsOnlyAscAndDesc(array $get, string $expected): void { $this->request($get); $this->assertSame($expected, get_order()); } public function testSearchArgs(): void { $this->assertNull(get_search_args()); $this->request(array('search' => "O'Brien ")); $this->assertSame("O'Brien ", get_search_args()); $this->request(array('search' => array('x'))); $this->assertNull(get_search_args()); } public function testIdMustBeNumeric(): void { $this->assertNull(get_id()); $this->request(array('id' => 'abc')); $this->assertNull(get_id()); $this->request(array('id' => '1 OR 1=1')); $this->assertNull(get_id()); $this->request(array('id' => '42')); $this->assertSame('42', get_id()); } public function testIdComesFromPostToo(): void { $this->request(array(), array('id' => '9')); $this->assertSame('9', get_id()); } public function testIdKeepsAGlobalValue(): void { $GLOBALS['id'] = 5; $this->request(array('id' => '42')); $this->assertSame(5, get_id()); } public function testIdIgnoresArrays(): void { $this->request(array('id' => array('1'))); $this->assertNull(get_id()); } public function testQueueIsOffByDefault(): void { $this->assertFalse(get_queue()); } public function testQueueComesFromTheRequest(): void { $this->request(array('queue' => '1')); $this->assertTrue(get_queue()); $GLOBALS['queue'] = NULL; $this->request(array('queue' => '0')); $this->assertFalse(get_queue()); $GLOBALS['queue'] = NULL; $this->request(array('queue' => 'yes')); $this->assertFalse(get_queue()); $GLOBALS['queue'] = NULL; $this->request(array('queue' => array('1'))); $this->assertFalse(get_queue()); } public function testQueueIsOnForSaveAndSubmit(): void { foreach (array('save', 'submit') as $action) { $GLOBALS['action'] = $action; $this->request(array('queue' => '0')); $this->assertTrue(get_queue(), $action); } } public function testQueueKeepsAGlobalValue(): void { $GLOBALS['queue'] = TRUE; $this->assertTrue(get_queue()); } public static function tables(): array { // is_admin, queue, move, action => table return array( 'visitor reads the live table' => array(FALSE, FALSE, FALSE, 'read', 'xxx'), 'visitor cannot read the queue' => array(FALSE, TRUE, FALSE, 'read', 'xxx'), 'editor reads the queue' => array(TRUE, TRUE, FALSE, 'read', 'queue'), 'editor reads the live table' => array(TRUE, FALSE, FALSE, 'read', 'xxx'), 'visitor post goes to the queue' => array(FALSE, FALSE, FALSE, 'post', 'queue'), 'visitor save goes to the queue' => array(FALSE, FALSE, FALSE, 'save', 'queue'), 'visitor submit goes to the queue' => array(FALSE, FALSE, FALSE, 'submit', 'queue'), 'visitor delete only touches the queue' => array(FALSE, FALSE, TRUE, 'delete', 'queue'), 'editor post goes live' => array(TRUE, TRUE, TRUE, 'post', 'xxx'), 'editor save goes to the queue' => array(TRUE, FALSE, FALSE, 'save', 'queue'), 'editor submit goes to the queue' => array(TRUE, FALSE, FALSE, 'submit', 'queue'), 'editor moving deletes from the queue' => array(TRUE, TRUE, TRUE, 'delete', 'queue'), 'editor deletes from the live table' => array(TRUE, FALSE, FALSE, 'delete', 'xxx'), 'editor deletes from the queue' => array(TRUE, TRUE, FALSE, 'delete', 'queue'), ); } #[DataProvider('tables')] public function testWhichTable(bool $isAdmin, bool $queue, bool $move, string $action, string $table): void { $GLOBALS['is_admin'] = $isAdmin; $GLOBALS['queue'] = $queue; $this->assertSame($table, which_table($move, $action)); } public function testPostDataHasEveryField(): void { $this->request(array(), array('title' => 'T', 'author' => 'A', 'email' => 'e@x.org', 'story' => '
S
', 'comments' => 'C', 'date' => '2020-01-01', 'hidden' => 'on', 'uploaded' => 'pending', 'editor' => 'ed', 'remove_img' => '1', 'id' => '4', 'ignored' => 'x')); $entry = get_post_data()[0]; $this->assertSame(array('id', 'title', 'author', 'email', 'upload', 'story', 'comments', 'date', 'hidden', 'uploaded', 'editor', 'remove_img', 'queue_url'), array_keys($entry)); $this->assertSame('T', $entry['title']); $this->assertSame(1, $entry['remove_img']); $this->assertNull($entry['upload']); $this->assertSame(0, $entry['queue_url']); } public function testPostDataAsStrings(): void { $this->request(array(), array('title' => array('not', 'a string'), 'remove_img' => '0', 'id' => '4')); $entry = get_post_data(TRUE)[0]; $this->assertSame('', $entry['title']); $this->assertSame('', $entry['author']); $this->assertSame('0', $entry['remove_img']); $this->assertSame('4', $entry['id']); foreach ($entry as $value) $this->assertIsString($value); } public function testPostDataEscapedForHtml(): void { $this->request(array(), array('title' => '"T" & co', 'story' => 'Wait…')); $entry = get_post_data(TRUE, TRUE)[0]; $this->assertSame('<b>"T" & co</b>', $entry['title']); $this->assertSame('Wait…', $entry['story']); } public function testPostDataQueueUrlFollowsTheQueue(): void { $GLOBALS['queue'] = TRUE; $this->assertSame(1, get_post_data()[0]['queue_url']); } public function testMapHelpers(): void { $this->assertSame('', map_string(NULL)); $this->assertSame('', map_string(array(1))); $this->assertSame('0', map_string(0)); $this->assertSame('1', map_string(TRUE)); $this->assertSame('<a href="x">', map_html('')); $this->assertSame('', unmap_html(map_html(''))); $this->assertSame("it's", unmap_html('it's')); } }