''], '/^
getContent());
if (!empty($regexp)) {
$this->assertMatchesRegularExpression($regexp, $response->getContent());
}
}
/**
* callMediaupload must normalize the namespace with cleanID() before it is used.
*
* regression test for XSS reflection and passing unclened data to the ACL check
*/
public function test_mediaupload_reflects_cleaned_namespace() {
$request = new TestRequest();
$response = $request->post(
['call' => 'mediaupload', 'ns' => 'Foo">'],
'/lib/exe/ajax.php'
);
$result = json_decode($response->getContent(), true);
$this->assertIsArray($result);
$this->assertSame(
'foo_script_x_script',
$result['ns'],
'the raw namespace must be cleaned before it is used'
);
}
/**
* Compute a security token for the given user, matching what same-process code (the
* request) computes for the same user and the shared test session, without leaking the
* temporary REMOTE_USER into the global state other tests rely on.
*
* @param string $user
* @return string
*/
protected function validTokenFor($user) {
global $INPUT;
$oldServer = $_SERVER;
$oldInput = $INPUT;
$_SERVER['REMOTE_USER'] = $user;
$INPUT = new \dokuwiki\Input\Input();
$token = getSecurityToken();
$_SERVER = $oldServer;
$INPUT = $oldInput;
return $token;
}
/**
* The happy path: a logged in user with a valid token takes the page lock and the
* posted text is stored as a retrievable draft.
*
* Doubles as the "valid token is accepted" case: if the CSRF gate wrongly rejected a
* valid token the lock would stay '0' and this test would fail.
*/
public function test_lock_takesLockAndSavesDraft() {
$id = 'lock:happy';
$text = 'some draft text';
$request = new TestRequest();
$request->setServer('REMOTE_USER', 'testuser');
$response = $request->post(
['call' => 'lock', 'id' => $id, 'sectok' => $this->validTokenFor('testuser'), 'wikitext' => $text],
'/lib/exe/ajax.php'
);
$result = json_decode($response->getContent(), true);
$this->assertIsArray($result);
$this->assertSame([], $result['errors']);
$this->assertEquals('1', $result['lock'], 'the lock must be taken');
$this->assertNotEmpty($result['draft'], 'a draft-saved message must be returned');
// the lock is actually held on disk by the user
$this->assertFileExists(wikiLockFN($id));
$this->assertEquals('testuser', io_readFile(wikiLockFN($id)));
// the draft is retrievable and round-trips the posted text
$draft = new \dokuwiki\Draft($id, 'testuser');
$this->assertTrue($draft->isDraftAvailable());
$this->assertStringContainsString($text, $draft->getDraftText());
}
/**
* The lock call takes a page lock and writes a draft, so for a logged in user it
* must be protected against CSRF. A request carrying an invalid security token must
* be rejected before any lock is taken.
*/
public function test_lock_rejectsInvalidSecurityToken() {
$id = 'lock:reject';
$request = new TestRequest();
$request->setServer('REMOTE_USER', 'testuser');
$response = $request->post(
['call' => 'lock', 'id' => $id, 'sectok' => 'not-the-real-token', 'wikitext' => 'x'],
'/lib/exe/ajax.php'
);
$result = json_decode($response->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result['errors'], 'an invalid security token must be rejected');
$this->assertEquals('0', $result['lock'], 'no lock may be taken on a rejected request');
$this->assertFileDoesNotExist(wikiLockFN($id), 'no lock file may be written on a rejected request');
}
/**
* When the user lacks write permission the call refuses to lock or save a draft.
*/
public function test_lock_deniedWhenNotWritable() {
global $conf, $AUTH_ACL;
$id = 'lock:denied';
$oldAcl = $AUTH_ACL;
$conf['useacl'] = 1;
$AUTH_ACL = ['* @ALL 0']; // deny everyone
try {
// anonymous: no security token needed, the write ACL is what must block this
$request = new TestRequest();
$response = $request->post(
['call' => 'lock', 'id' => $id, 'wikitext' => 'x'],
'/lib/exe/ajax.php'
);
$result = json_decode($response->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result['errors'], 'a denied write must be reported');
$this->assertEquals('0', $result['lock']);
$this->assertSame('', $result['draft'], 'no draft may be saved when not writable');
$this->assertFileDoesNotExist(wikiLockFN($id));
} finally {
$AUTH_ACL = $oldAcl;
}
}
/**
* A lock already held by someone else must not be reported as freshly taken and must
* be left untouched.
*/
public function test_lock_doesNotStealForeignLock() {
$id = 'lock:foreign';
// someone else already holds the lock
io_saveFile(wikiLockFN($id), 'someoneelse');
$request = new TestRequest();
$request->setServer('REMOTE_USER', 'testuser');
$response = $request->post(
['call' => 'lock', 'id' => $id, 'sectok' => $this->validTokenFor('testuser')],
'/lib/exe/ajax.php'
);
$result = json_decode($response->getContent(), true);
$this->assertIsArray($result);
$this->assertEquals('0', $result['lock'], 'a foreign lock must not be reported as freshly taken');
$this->assertEquals('someoneelse', io_readFile(wikiLockFN($id)), 'the foreign lock must be left untouched');
}
public function test_CallNotProvided() {
$request = new TestRequest();
$response = $request->post([], '/lib/exe/ajax.php');
$this->assertEquals('', $response->getContent());
}
public function test_UnknownCall() {
$call = 'unknownCALL';
$request = new TestRequest();
$response = $request->post(['call'=> $call], '/lib/exe/ajax.php');
$this->assertEquals("AJAX call '$call' unknown!\n", $response->getContent());
}
public function test_EventOnUnknownCall() {
global $EVENT_HANDLER;
$call = 'unknownCALL';
$request = new TestRequest();
// referenced data from event hook
$hookTriggered = false;
$eventDataTriggered = '';
$dataTriggered = '';
$postTriggered = '';
$hookTriggered_AFTER = false;
$eventDataTriggered_AFTER = '';
$dataTriggered_AFTER = '';
$postTriggered_AFTER = '';
$EVENT_HANDLER->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', null,
function($event, $data) use (&$hookTriggered, &$dataTriggered, &$eventDataTriggered, &$postTriggered) {
/** @var Doku_Event $event */
$hookTriggered = true;
$dataTriggered = $data;
$eventDataTriggered = $event->data;
$postTriggered = $GLOBALS['INPUT']->post->str('q');
$event->preventDefault();
$event->stopPropagation();
echo "captured event BEFORE\n";
}, 'some passed data'
);
$EVENT_HANDLER->register_hook('AJAX_CALL_UNKNOWN', 'AFTER', null,
function($event, $data) use (&$hookTriggered_AFTER , &$dataTriggered_AFTER , &$eventDataTriggered_AFTER , &$postTriggered_AFTER ) {
/** @var Doku_Event $event */
$hookTriggered_AFTER = true;
$dataTriggered_AFTER = $data;
$eventDataTriggered_AFTER = $event->data;
$postTriggered_AFTER = $GLOBALS['INPUT']->post->str('q');
$event->preventDefault();
$event->stopPropagation();
echo "captured event AFTER";
}, 'some passed data AFTER'
);
$response = $request->post(['call'=> $call, 'q' => 'some-post-param'], '/lib/exe/ajax.php');
// BEFORE
$this->assertEquals(true, $hookTriggered, 'Testing plugin did not trigger!');
$this->assertEquals('some passed data', $dataTriggered);
$this->assertEquals($call, $eventDataTriggered, 'Must pass call name as event data');
$this->assertEquals('some-post-param', $postTriggered);
// AFTER
$this->assertEquals(true, $hookTriggered_AFTER, 'Testing plugin did not trigger!');
$this->assertEquals('some passed data AFTER', $dataTriggered_AFTER);
$this->assertEquals($call, $eventDataTriggered_AFTER, 'Must pass call name as event data');
$this->assertEquals('some-post-param', $postTriggered_AFTER);
//output
$this->assertEquals("captured event BEFORE\ncaptured event AFTER", $response->getContent());
}
}