Merge pull request #3361 from ssahara/revisionHandle3

Hierarchical class Ui\(Revisions,Diff), new class File\PageFile
This commit is contained in:
Gerrit Uitslag 2021-12-30 17:32:09 +01:00 committed by GitHub
commit 8a10303f9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 3129 additions and 1914 deletions

View File

@ -195,7 +195,7 @@ abstract class DokuWikiTest extends PHPUnit\Framework\TestCase {
* Waits until a new second has passed
*
* This tried to be clever about the passing of time and return early if possible. Unfortunately
* this never worked reliably fo unknown reasons. To avoid flaky tests, this now always simply
* this never worked reliably for unknown reasons. To avoid flaky tests, this now always simply
* sleeps for a full second on every call.
*
* @param bool $init no longer used

View File

@ -34,6 +34,7 @@ class XmlRpcServerTest extends DokuWikiTest
$file = wikiFN($pageName);
$timestamp = filemtime($file);
$ixrModifiedTime = (new DateTime('@' . $timestamp))->format(DateTime::ATOM);
$author = '127.0.0.1'; // read from changelog, $info['user'] or $info['ip']
$request = <<<EOD
<?xml version="1.0"?>
@ -54,7 +55,7 @@ EOD;
<struct>
<member><name>name</name><value><string>wiki:dokuwiki</string></value></member>
<member><name>lastModified</name><value><dateTime.iso8601>$ixrModifiedTime</dateTime.iso8601></value></member>
<member><name>author</name><value><string></string></value></member>
<member><name>author</name><value><string>$author</string></value></member>
<member><name>version</name><value><int>$timestamp</int></value></member>
</struct>
</value>

View File

@ -35,12 +35,10 @@ class changelog_getrevisionsaround_test extends DokuWikiTest {
parent::setup();
global $cache_revinfo;
$cache =& $cache_revinfo;
if(isset($cache['nonexist'])) {
unset($cache['nonexist']);
}
if(isset($cache['mailinglist'])) {
unset($cache['mailinglist']);
}
unset($cache['nonexist']);
unset($cache['mailinglist']);
// fix filemtime of page source
touch(wikiFN($this->pageid), $this->revsexpected[0]);
}
/**

View File

@ -18,12 +18,8 @@ class changelog_getlastrevisionat_test extends DokuWikiTest {
parent::setup();
global $cache_revinfo;
$cache =& $cache_revinfo;
if(isset($cache['nonexist'])) {
unset($cache['nonexist']);
}
if(isset($cache['mailinglist'])) {
unset($cache['mailinglist']);
}
unset($cache['nonexist']);
unset($cache['mailinglist']);
}

View File

@ -18,12 +18,8 @@ class changelog_getrelativerevision_test extends DokuWikiTest {
parent::setup();
global $cache_revinfo;
$cache =& $cache_revinfo;
if(isset($cache['nonexist'])) {
unset($cache['nonexist']);
}
if(isset($cache['mailinglist'])) {
unset($cache['mailinglist']);
}
unset($cache['nonexist']);
unset($cache['mailinglist']);
}
/**

View File

@ -19,12 +19,8 @@ class changelog_getrevisionsinfo_test extends DokuWikiTest {
parent::setup();
global $cache_revinfo;
$cache =& $cache_revinfo;
if(isset($cache['nonexist'])) {
unset($cache['nonexist']);
}
if(isset($cache['mailinglist'])) {
unset($cache['mailinglist']);
}
unset($cache['nonexist']);
unset($cache['mailinglist']);
}
/**

View File

@ -35,12 +35,10 @@ class changelog_getrevisions_test extends DokuWikiTest {
parent::setup();
global $cache_revinfo;
$cache =& $cache_revinfo;
if(isset($cache['nonexist'])) {
unset($cache['nonexist']);
}
if(isset($cache['mailinglist'])) {
unset($cache['mailinglist']);
}
unset($cache['nonexist']);
unset($cache['mailinglist']);
// fix filemtime of page source
touch(wikiFN($this->pageid), $this->revsexpected[0]);
}
/**

View File

@ -13,7 +13,7 @@ class changelog_hasrevisions_test extends DokuWikiTest {
*/
function test_hasrevisions() {
$id = 'mailinglist';
$pagelog = new PageChangeLog($id);
$result = $pagelog->hasRevisions();
$this->assertTrue($result);
@ -24,7 +24,7 @@ class changelog_hasrevisions_test extends DokuWikiTest {
*/
function test_norevisions() {
$id = 'nonexist';
$pagelog = new PageChangeLog($id);
$result = $pagelog->hasRevisions();
$this->assertFalse($result);

View File

@ -51,7 +51,7 @@ class common_pageinfo_test extends DokuWikiTest {
/**
* check info keys and values for a non-existent page & admin user
*/
function test_basic_nonexistentpage(){
function test_basic_nonexistentpage() {
global $ID,$conf;
$ID = 'wiki:start';
@ -66,12 +66,16 @@ class common_pageinfo_test extends DokuWikiTest {
/**
* check info keys and values for a existing page & admin user
*/
function test_basic_existingpage(){
function test_basic_existingpage() {
global $ID,$conf;
$ID = 'wiki:syntax';
$filename = $conf['datadir'].'/wiki/syntax.txt';
$rev = filemtime($filename);
// run once to prepare meta/wiki/syntax.change file for existing page
// because pageinfo() set $info['meta']['last_change'] entry
pageinfo();
$info = $this->_get_expected_pageinfo();
$info['id'] = 'wiki:syntax';
$info['namespace'] = 'wiki';
@ -80,6 +84,13 @@ class common_pageinfo_test extends DokuWikiTest {
$info['lastmod'] = $rev;
$info['currentrev'] = $rev;
$info['meta'] = p_get_metadata($ID);
// set from revinfo, $pagelog->getRevisionInfo($info['lastmod'])
$info = array_merge($info, array(
'ip' => '127.0.0.1',
'user' => '',
'sum' => 'created - external edit',
));
$info['editor'] = '127.0.0.1';
$this->assertEquals($info, pageinfo());
}
@ -87,7 +98,7 @@ class common_pageinfo_test extends DokuWikiTest {
/**
* check info keys and values for anonymous user
*/
function test_anonymoususer(){
function test_anonymoususer() {
global $ID,$conf,$REV;
unset($_SERVER['REMOTE_USER']);
@ -106,7 +117,15 @@ class common_pageinfo_test extends DokuWikiTest {
$info['currentrev'] = $rev;
$info['meta'] = p_get_metadata($ID);
$info['rev'] = '';
// set from revinfo, $pagelog->getRevisionInfo($info['lastmod'])
$info = array_merge($info, array(
'ip' => '127.0.0.1',
'user' => '',
'sum' => 'created - external edit',
));
$info['editor'] = '127.0.0.1';
// anonymous user
$info = array_merge($info, array(
'isadmin' => false,
'ismanager' => false,
@ -114,6 +133,7 @@ class common_pageinfo_test extends DokuWikiTest {
'client' => '1.2.3.4',
));
unset($info['userinfo']);
$this->assertEquals($info, pageinfo());
}
@ -121,7 +141,7 @@ class common_pageinfo_test extends DokuWikiTest {
* check info keys and values with $REV
* (also see $RANGE tests)
*/
function test_rev(){
function test_rev() {
global $ID,$conf,$REV;
$ID = 'wiki:syntax';
@ -129,7 +149,8 @@ class common_pageinfo_test extends DokuWikiTest {
$rev = filemtime($filename);
$REV = $rev - 100;
$ext = '.txt';
if($conf['compression']) { //compression in $info['filepath'] determined by wikiFN depends also on if the page exist
if ($conf['compression']) {
//compression in $info['filepath'] determined by wikiFN depends also on if the page exist
$ext .= "." . $conf['compression']; //.gz or .bz2
}
@ -148,7 +169,7 @@ class common_pageinfo_test extends DokuWikiTest {
/**
* check info keys and values with $RANGE
*/
function test_range(){
function test_range() {
global $ID,$conf,$REV,$RANGE;
$ID = 'wiki:syntax';
@ -164,6 +185,13 @@ class common_pageinfo_test extends DokuWikiTest {
$info['currentrev'] = $rev;
$info['meta'] = p_get_metadata($ID);
$info['filepath'] = $filename;
// set from revinfo, $pagelog->getRevisionInfo($info['lastmod'])
$info = array_merge($info, array(
'ip' => '127.0.0.1',
'user' => '',
'sum' => 'created - external edit',
));
$info['editor'] = '127.0.0.1';
// check $RANGE without $REV
// expected result $RANGE unchanged
@ -171,7 +199,7 @@ class common_pageinfo_test extends DokuWikiTest {
$this->assertEquals($info, pageinfo());
$this->assertFalse(isset($REV));
$this->assertEquals($range,$RANGE);
$this->assertEquals($range, $RANGE);
// check $RANGE with $REV = current
// expected result: $RANGE unchanged, $REV cleared
@ -180,21 +208,21 @@ class common_pageinfo_test extends DokuWikiTest {
$this->assertEquals($info, pageinfo());
$this->assertEquals('',$REV);
$this->assertEquals($range,$RANGE);
$this->assertEquals($range, $RANGE);
// check with a real $REV
// expected result: $REV and $RANGE are cleared
$REV = $rev - 100;
$this->assertEquals($info, pageinfo());
$this->assertEquals('',$REV);
$this->assertEquals('',$RANGE);
$this->assertEquals('', $REV);
$this->assertEquals('', $RANGE);
}
/**
* test editor entry and external edit
*/
function test_editor_and_externaledits(){
function test_editor_and_externaledits() {
global $ID,$conf;
$ID = 'wiki:syntax';
$filename = $conf['datadir'].'/wiki/syntax.txt';
@ -216,24 +244,24 @@ class common_pageinfo_test extends DokuWikiTest {
addLogEntry($rev, $ID);
$info['meta'] = p_get_metadata($ID);
$info['editor'] = $_SERVER['REMOTE_USER'];
$info['user'] = $_SERVER['REMOTE_USER'];
$info['ip'] = $_SERVER['REMOTE_ADDR'];
$info['user'] = $_SERVER['REMOTE_USER'];
$info['sum'] = '';
$info['editor'] = $info['user'];
// with an editor ...
$this->assertEquals($info, pageinfo());
// clear the meta['last_change'] value, pageinfo should restore it
p_set_metadata($ID,array('last_change' => false));
p_set_metadata($ID, array('last_change' => false));
$this->assertEquals($info, pageinfo());
$this->assertEquals($info['meta']['last_change'], p_get_metadata($ID,'last_change'));
// fake an external edit, pageinfo should clear the last change from meta data
// and not return any editor data
$now = time()+10;
touch($filename,$now);
$now = time() + 10;
touch($filename, $now);
$info['lastmod'] = $now;
$info['currentrev'] = $now;
@ -250,12 +278,16 @@ class common_pageinfo_test extends DokuWikiTest {
/**
* check draft
*/
function test_draft(){
function test_draft() {
global $ID,$conf;
$ID = 'wiki:syntax';
$filename = $conf['datadir'].'/wiki/syntax.txt';
$rev = filemtime($filename);
// run once to prepare meta/wiki/syntax.change file for existing page
// because pageinfo() set $info['meta']['last_change'] entry
pageinfo();
$info = $this->_get_expected_pageinfo();
$info['id'] = 'wiki:syntax';
$info['namespace'] = 'wiki';
@ -264,11 +296,23 @@ class common_pageinfo_test extends DokuWikiTest {
$info['lastmod'] = $rev;
$info['currentrev'] = $rev;
$info['meta'] = p_get_metadata($ID);
// $info['ip'] = $_SERVER['REMOTE_ADDR'];
// $info['user'] = $_SERVER['REMOTE_USER'];
// $info['sum'] = '';
// $info['editor'] = $info['user'];
// set from revinfo, $pagelog->getRevisionInfo($info['lastmod'])
$info = array_merge($info, array(
'ip' => '127.0.0.1',
'user' => '',
'sum' => 'external edit',
));
$info['editor'] = '127.0.0.1';
// setup a draft, make it more recent than the current page
// - pageinfo should recognise it and keep it
$draft = getCacheName($info['client']."\n".$ID,'.draft');
touch($draft,$rev + 10);
touch($draft, $rev + 10);
$info['draft'] = $draft;
@ -287,7 +331,7 @@ class common_pageinfo_test extends DokuWikiTest {
/**
* check ismobile
*/
function test_ismobile(){
function test_ismobile() {
global $ID,$conf;
$ID = 'wiki:start';

View File

@ -2,7 +2,11 @@
use dokuwiki\ChangeLog\PageChangeLog;
/**
* saveWikiText() stores files in pages/, attic/ and adds entries to changelog
*/
class common_saveWikiText_test extends DokuWikiTest {
/** Delay writes of old revisions by a second. */
public function handle_write(Doku_Event $event, $param) {
if ($event->data[3] !== false) {
@ -11,161 +15,266 @@ class common_saveWikiText_test extends DokuWikiTest {
}
/**
* Execute a whole bunch of saves on the same page and check the results
* assertions against changelog entries and attic after saveWikiText()
*/
function test_savesequence() {
private function checkChangeLogAfterNormalSave(
PageChangeLog $pagelog,
$expectedRevs, // @param int
&$expectedLastEntry, // @param array, pass by reference
$expected2ndLastEntry = null // @param array (optional)
) {
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertCount($expectedRevs, $revisions);
$this->assertCount($expectedRevs, array_unique($revisions), 'date duplicated in changelog');
// last revision
$lastRevInfo = $pagelog->getRevisionInfo($revisions[0]);
$expectedLastEntry += $lastRevInfo;
$this->assertEquals($expectedLastEntry, $lastRevInfo);
// current revision
$currentRevInfo = $pagelog->getCurrentRevisionInfo();
$this->assertEquals($currentRevInfo, $lastRevInfo, 'current & last revs should be identical');
// attic
$attic = wikiFN($lastRevInfo['id'], $lastRevInfo['date']);
$this->assertFileExists($attic, 'file missing in attic');
$files = count(glob(dirname($attic).'/'.noNS($lastRevInfo['id']).'.*'));
$this->assertLessThanOrEqual($expectedRevs, $files, 'detectExternalEdit() should not add too often old revs');
// second to last revision (optional), intended to check logline of previous external edits
if ($expected2ndLastEntry && count($revisions) > 1) {
$prevRevInfo = $pagelog->getRevisionInfo($revisions[1]);
unset($expected2ndLastEntry['timestamp']); // drop timestamp key
$this->assertEquals($expected2ndLastEntry, $prevRevInfo);
}
}
/**
* assertions against changelog entries and attic after external edit, create or deletion of page
*/
private function checkChangeLogAfterExternalEdit(
PageChangeLog $pagelog,
$expectedRevs, // @param int
$expectedLastEntry, // @param array
&$expectedCurrentEntry // @param array, pass by reference
) {
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertCount($expectedRevs, $revisions);
$this->assertCount($expectedRevs, array_unique($revisions), 'date duplicated in changelog');
// last revision
if ($expectedRevs > 0) {
$lastRevInfo = $pagelog->getRevisionInfo($revisions[0]);
$expectedLastEntry += $lastRevInfo;
$this->assertEquals($expectedLastEntry, $lastRevInfo);
} else {
$this->assertFalse($pagelog->lastRevision(), 'changelog file does not yet exist');
}
// current revision
$currentRevInfo = $pagelog->getCurrentRevisionInfo();
$this->assertArrayHasKey('timestamp', $currentRevInfo, 'should be external revision');
$expectedCurrentEntry += $currentRevInfo;
if ($expectedRevs > 0) {
$this->assertEquals($expectedCurrentEntry, $currentRevInfo);
}
// attic
$attic = wikiFN($currentRevInfo['id'], $currentRevInfo['date']);
$this->assertFileNotExists($attic, 'page does not yet exist in attic');
}
/**
* Execute a whole bunch of saves on the same page and check the results
* TEST 1
* 1.1 create a page
* 1.2 save with same content should be ignored
* 1.3 update the page with new text
* 1.4 add a minor edit (unauthenticated, minor not allowable)
* 1.5 add a minor edit (authenticated)
* 1.6 delete
* 1.7 restore
* 1.8 external edit
* 1.9 edit and save on top of external edit
*/
function test_savesequence1() {
global $REV;
$page = 'page';
$file = wikiFN($page);
// create the page
$this->assertFileNotExists($file);
saveWikiText($page, 'teststring', 'first save', false);
// 1.1 create a page
saveWikiText($page, 'teststring', '1st save', false);
clearstatcache(false, $file);
$this->assertFileExists($file);
$lastmod = filemtime($file);
$expectedRevs = 1;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE,
'sum' => '1st save',
'sizechange' => 10, // = strlen('teststring')
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(1, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('first save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_CREATE, $revinfo['type']);
$this->assertEquals(10, $revinfo['sizechange']);
$this->assertFileExists(wikiFN($page, $revinfo['date']));
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(true); // wait for new revision ID
// save with same content should be ignored
saveWikiText($page, 'teststring', 'second save', false);
// 1.2 save with same content should be ignored
saveWikiText($page, 'teststring', '2nd save', false);
clearstatcache(false, $file);
$this->assertEquals($lastmod, filemtime($file));
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(1, count($revisions));
$this->assertCount(1, $revisions);
// update the page with new text
saveWikiText($page, 'teststring2long', 'third save', false);
// 1.3 update the page with new text
saveWikiText($page, 'teststring2long', '3rd save', false);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 2;
$expectPrev = $expect;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => '3rd save',
'sizechange' => 5,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(2, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('third save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(5, $revinfo['sizechange']);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect, $expectPrev);
$this->waitForTick(); // wait for new revision ID
// add a minor edit (unauthenticated)
saveWikiText($page, 'teststring3long', 'fourth save', true);
// 1.4 add a minor edit (unauthenticated, minor not allowable)
saveWikiText($page, 'teststring3long', '4th save', true);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 3;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => '4th save',
'sizechange' => 0,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(3, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('fourth save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(0, $revinfo['sizechange']);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(); // wait for new revision ID
// add a minor edit (authenticated)
// 1.5 add a minor edit (authenticated)
$_SERVER['REMOTE_USER'] = 'user';
saveWikiText($page, 'teststring4', 'fifth save', true);
saveWikiText($page, 'teststring4', '5th save', true);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 4;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_MINOR_EDIT,
'sum' => '5th save',
'sizechange' => -4,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(4, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('fifth save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_MINOR_EDIT, $revinfo['type']);
$this->assertEquals(-4, $revinfo['sizechange']);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(); // wait for new revision ID
// delete
saveWikiText($page, '', 'sixth save', false);
// 1.6 delete
saveWikiText($page, '', '6th save', false);
clearstatcache(false, $file);
$this->assertFileNotExists($file);
$expectedRevs = 5;
$expect = array(
//'date' => $lastmod, // ignore from lastRev assertion, but confirm attic file existence
'type' => DOKU_CHANGE_TYPE_DELETE,
'sum' => '6th save',
'sizechange' => -11,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(5, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('sixth save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_DELETE, $revinfo['type']);
$this->assertEquals(-11, $revinfo['sizechange']);
$this->assertFileExists(wikiFN($page, $revinfo['date']));
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(); // wait for new revision ID
// restore
// 1.7 restore
$REV = $lastmod;
saveWikiText($page, 'teststring4', 'seventh save', true);
saveWikiText($page, 'teststring4', '7th save', true);
clearstatcache(false, $file);
$this->assertFileExists($file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 6;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_REVERT,
'sum' => '7th save',
'sizechange' => 11,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(6, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('seventh save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_REVERT, $revinfo['type']);
$this->assertEquals($REV, $revinfo['extra']);
$this->assertEquals(11, $revinfo['sizechange']);
$this->assertFileExists(wikiFN($page, $revinfo['date']));
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$REV = '';
$this->waitForTick(); // wait for new revision ID
// create external edit
file_put_contents($file, 'teststring5');
$this->waitForTick(); // wait for new revision ID
// save on top of external edit
saveWikiText($page, 'teststring6', 'eigth save', false);
// 1.8 external edit
file_put_contents($file, 'teststring5 external edit');
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 6; // external edit is not yet in changelog
$expectExternal = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'external edit',
'sizechange' => 14,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(8, count($revisions)); // two more revisions now!
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('eigth save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(0, $revinfo['sizechange']);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
$revinfo = $pagelog->getRevisionInfo($revisions[1]);
$this->assertEquals('external edit', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(0, $revinfo['sizechange']);
$this->waitForTick(); // wait for new revision ID
// 1.9 save on top of external edit
saveWikiText($page, 'teststring6', '8th save', false);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 8;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => '8th save',
'sizechange' => -14,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect, $expectExternal);
}
/**
* Execute a whole bunch of saves on the same page and check the results
* using $this->handle_write() in event IO_WIKIPAGE_WRITE
* TEST 2 - create a page externally in 2.3, while external edit in Test 1.8
* 2.1 create a page
* 2.2 delete
* 2.3 externally create the page
* 2.4 edit and save on top of external edit
* 2.5 external edit
* 2.6 edit and save on top of external edit, again
*/
function test_savesequencedeleteexternalrevision() {
function test_savesequence2() {
// add an additional delay when saving files to make sure
// nobody relies on the saving happening in the same second
/** @var $EVENT_HANDLER \dokuwiki\Extension\EventHandler */
@ -174,251 +283,405 @@ class common_saveWikiText_test extends DokuWikiTest {
$page = 'page2';
$file = wikiFN($page);
// create the page
$this->assertFileNotExists($file);
saveWikiText($page, 'teststring', 'first save', false);
// 2.1 create a page
saveWikiText($page, 'teststring', 'Test 2, 1st save', false);
clearstatcache(false, $file);
$this->assertFileExists($file);
$lastmod = filemtime($file);
$expectedRevs = 1;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE,
'sum' => 'Test 2, 1st save',
'sizechange' => 10, // = strlen('teststring')
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(1, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('first save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_CREATE, $revinfo['type']);
$this->assertEquals(10, $revinfo['sizechange']);
$this->assertFileExists(wikiFN($page, $revinfo['date']));
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(true); // wait for new revision ID
// delete
saveWikiText($page, '', 'second save', false);
// 2.2 delete
saveWikiText($page, '', 'Test 2, 2nd save', false);
clearstatcache(false, $file);
$this->assertFileNotExists($file);
$expectedRevs = 2;
$expect = array(
//'date' => $lastmod, // ignore from lastRev assertion, but confirm attic file existence
'type' => DOKU_CHANGE_TYPE_DELETE,
'sum' => 'Test 2, 2nd save',
'sizechange' => -10,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(2, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('second save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_DELETE, $revinfo['type']);
$this->assertEquals(-10, $revinfo['sizechange']);
$this->assertFileExists(wikiFN($page, $revinfo['date']));
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(); // wait for new revision ID
// create external edit
// 2.3 externally create the page
file_put_contents($file, 'teststring5');
clearstatcache(false, $file);
$lastmod = filemtime($file);
$expectedRevs = 2; // external edit is not yet in changelog
$expectExternal = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE,
'sum' => 'created - external edit',
'sizechange' => 11,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
$this->waitForTick(); // wait for new revision ID
// save on top of external edit
saveWikiText($page, 'teststring6', 'third save', false);
// 2.4 save on top of external edit
saveWikiText($page, 'teststring6', 'Test 2, 3rd save', false);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 4; // two more revisions now!
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'Test 2, 3rd save',
'sizechange' => 0,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(4, count($revisions)); // two more revisions now!
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('third save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(0, $revinfo['sizechange']);
$this->assertFileExists(wikiFN($page, $revinfo['date']));
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect, $expectExternal);
$revinfo = $pagelog->getRevisionInfo($revisions[1]);
$this->assertEquals('external edit', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(11, $revinfo['sizechange']);
$this->assertFileExists(wikiFN($page, $revinfo['date']));
$this->waitForTick(); // wait for new revision ID
// 2.5 external edit
file_put_contents($file, 'teststring7 external edit2');
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 4; // external edit is not yet in changelog
$expectExternal = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'external edit',
'sizechange' => 15,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
$this->waitForTick(); // wait for new revision ID
// 2.6 save on top of external edit, again
saveWikiText($page, 'teststring8', 'Test 2, 4th save', false);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 6; // two more revisions now!
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'Test 2, 4th save',
'sizechange' => -15,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect, $expectExternal);
}
/**
* Execute a whole bunch of saves on the same page and check the results
* TEST 3 - typical page life of bundled page such as wiki:syntax
* 3.1 externally create a page
* 3.2 external edit
* 3.3 edit and save on top of external edit
* 3.4 externally delete the page
*/
function test_saveexternalasfirst() {
function test_savesequence3() {
$page = 'page3';
$file = wikiFN($page);
// create the page
// 3.1 externally create a page
$this->assertFileNotExists($file);
// create external edit
file_put_contents($file, 'teststring');
clearstatcache(false, $file);
$lastmod = filemtime($file);
$expectedRevs = 0; // external edit is not yet in changelog
$expect = false;
$expectExternal = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE,
'sum' => 'created - external edit',
'sizechange' => 10,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
$this->waitForTick(true); // wait for new revision ID
// save on top of external edit
saveWikiText($page, 'teststring6', 'first save', false);
// 3.2 external edit (repeated, still no changelog exists)
file_put_contents($file, 'teststring external edit');
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 0; // external edit is not yet in changelog
$expectExternal = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE, // not DOKU_CHANGE_TYPE_EDIT
'sum' => 'created - external edit',
'sizechange' => 24,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(2, count($revisions)); // two more revisions now!
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('first save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(1, $revinfo['sizechange']);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
$revinfo = $pagelog->getRevisionInfo($revisions[1]);
$this->assertEquals('external edit', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(10, $revinfo['sizechange']);
$this->waitForTick(true); // wait for new revision ID
// 3.3 save on top of external edit
saveWikiText($page, 'teststring1', 'Test 3, first save', false);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 2; // two more revisions now!
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'Test 3, first save',
'sizechange' => -13,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect, $expectExternal);
$this->waitForTick(true); // wait for new revision ID
// 3.4 externally delete the page
unlink($file);
$expectedRevs = 2;
$expectExternal = array(
//'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_DELETE,
'sum' => 'removed - external edit (Unknown date)',
'sizechange' => -11,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
}
/**
* Execute a whole bunch of saves on the same page and check the results
* TEST 4 - typical page life of bundled page such as wiki:syntax
* 4.1 externally create a page
* 4.2 edit and save
* 4.3 externally edit as a result of a file which has older timestamp than last revision
*/
function test_savesequenceexternaldeleteedit() {
function test_savesequence4() {
$page = 'page4';
$file = wikiFN($page);
// create the page
// 4.1 externally create a page
$this->assertFileNotExists($file);
saveWikiText($page, 'teststring', 'first save', false);
$this->assertFileExists($file);
file_put_contents($file, 'teststring');
clearstatcache(false, $file);
$lastmod = filemtime($file);
$expectedRevs = 0; // external edit is not yet in changelog
$expect = false;
$expectExternal = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE,
'sum' => 'created - external edit',
'sizechange' => 10,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(1, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('first save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_CREATE, $revinfo['type']);
$this->assertEquals(10, $revinfo['sizechange']);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
$this->waitForTick(true); // wait for new revision ID
// create external delete
unlink($file);
clearstatcache(false, $file);
$this->waitForTick(); // wait for new revision ID
// save on top of external delete. save is seen as creation
saveWikiText($page, 'teststring6', 'second save', false);
// 4.2 edit and save
saveWikiText($page, 'teststring1', 'Test 4, first save', false);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 2; // two more revisions now!
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'Test 4, first save',
'sizechange' => 1,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(2, count($revisions)); // one more revisions now!
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('second save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_CREATE, $revinfo['type']);
$this->assertEquals(11, $revinfo['sizechange']);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect, $expectExternal);
$revinfo = $pagelog->getRevisionInfo($revisions[1]);
$this->assertEquals('first save', $revinfo['sum']);
$this->waitForTick(true); // wait for new revision ID
// 4.3 externally edit as a result of a file which has older timestamp than last revision
unlink($file);
file_put_contents($file, 'teststring fake 1 hout past');
touch($file, filemtime($file) -3600); // change file modification time to 1 hour past
clearstatcache();
$newmod = filemtime($file);
$this->assertLessThan($lastmod, $newmod); // file must be older than previous for this test
$expectedRevs = 2; // external edit is not yet in changelog
$expectExternal = array(
'date' => $lastmod + 1,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'external edit (Unknown date)',
'sizechange' => 16,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
}
/**
* Execute a whole bunch of saves on the same page and check the results
* TEST 5 - page creation and deletion
* 5.1 create a page
* 5.2 external edit
* 5.3 edit and save on top of external edit
* 5.4 delete
* 5.5 create a page, second time
* 5.6 externally delete
* 5.7 create a page, third time
*/
function test_savesequencerevert() {
global $REV;
function test_savesequence5() {
$page = 'page5';
$file = wikiFN($page);
// create the page
$this->assertFileNotExists($file);
saveWikiText($page, 'teststring', 'first save', false);
// 5.1 create a page
saveWikiText($page, 'teststring', 'Test 5, 1st save', false);
$this->assertFileExists($file);
$lastmod = filemtime($file);
$expectedRevs = 1;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE,
'sum' => 'Test 5, 1st save',
'sizechange' => 10, // = strlen('teststring')
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(1, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('first save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_CREATE, $revinfo['type']);
$this->assertEquals(10, $revinfo['sizechange']);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(true); // wait for new revision ID
// save with same content should be ignored
saveWikiText($page, 'teststring', 'second save', false);
clearstatcache(false, $file);
$this->assertEquals($lastmod, filemtime($file));
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(1, count($revisions));
// update the page with new text
saveWikiText($page, 'teststring2long', 'third save', false);
// 5.2 external edit
file_put_contents($file, 'teststring external edit');
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$revertrev = $newmod;
$expectedRevs = 1; // external edit is not yet in changelog
$expectExternal = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'external edit',
'sizechange' => 14,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(2, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('third save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(5, $revinfo['sizechange']);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
$this->waitForTick(); // wait for new revision ID
// add a minor edit (unauthenticated)
saveWikiText($page, 'teststring3long', 'fourth save', true);
// 5.3 edit and save on top of external edit
saveWikiText($page, 'teststring normal edit', 'Test 5, 2nd save', false);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$expectedRevs = 3; // two more revisions now!
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_EDIT,
'sum' => 'Test 5, 2nd save',
'sizechange' => -2,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(3, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('fourth save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_EDIT, $revinfo['type']);
$this->assertEquals(0, $revinfo['sizechange']);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect, $expectExternal);
$this->waitForTick(); // wait for new revision ID
// add a minor edit (authenticated)
$_SERVER['REMOTE_USER'] = 'user';
saveWikiText($page, 'teststring4', 'fifth save', true);
// 5.4 delete
saveWikiText($page, '', 'Test 5 3rd save', false);
clearstatcache(false, $file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = $newmod;
$this->assertFileNotExists($file);
$expectedRevs = 4;
$expect = array(
//'date' => $lastmod, // ignore from lastRev assertion, but confirm attic file existence
'type' => DOKU_CHANGE_TYPE_DELETE,
'sum' => 'Test 5 3rd save',
'sizechange' => -22,
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(4, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('fifth save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_MINOR_EDIT, $revinfo['type']);
$this->assertEquals(-4, $revinfo['sizechange']);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(); // wait for new revision ID
// restore
$REV = $revertrev;
saveWikiText($page, 'teststring2long', 'sixth save', true);
// 5.5 create a page, second time
$this->assertFileNotExists($file);
saveWikiText($page, 'teststring revived', 'Test 5, 4th save', false);
$this->assertFileExists($file);
$lastmod = filemtime($file);
$expectedRevs = 5;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE,
'sum' => 'Test 5, 4th save',
'sizechange' => 18, // = strlen('teststring revived')
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect);
$this->waitForTick(true); // wait for new revision ID
// 5.6 externally delete
unlink($file);
$this->assertFileNotExists($file);
$expectedRevs = 5;
$expectExternal = array(
//'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_DELETE,
'sum' => 'removed - external edit (Unknown date)',
'sizechange' => -18,
);
$pagelog = new PageChangeLog($page);
$this->checkChangeLogAfterExternalEdit($pagelog, $expectedRevs, $expect, $expectExternal);
$this->waitForTick(true); // wait for new revision ID
// 5.7 create a page, third time
$this->assertFileNotExists($file);
saveWikiText($page, 'teststring revived 2', 'Test 5, 5th save', false);
clearstatcache(false, $file);
$this->assertFileExists($file);
$newmod = filemtime($file);
$this->assertNotEquals($lastmod, $newmod);
$lastmod = filemtime($file);
$expectedRevs = 7;
$expect = array(
'date' => $lastmod,
'type' => DOKU_CHANGE_TYPE_CREATE,
'sum' => 'Test 5, 5th save',
'sizechange' => 20, // = strlen('teststring revived 2')
);
$pagelog = new PageChangeLog($page);
$revisions = $pagelog->getRevisions(-1, 200);
$this->assertEquals(5, count($revisions));
$revinfo = $pagelog->getRevisionInfo($revisions[0]);
$this->assertEquals('sixth save', $revinfo['sum']);
$this->assertEquals(DOKU_CHANGE_TYPE_REVERT, $revinfo['type']);
$this->assertEquals($REV, $revinfo['extra']);
$this->assertEquals(4, $revinfo['sizechange']);
$REV = '';
$this->checkChangeLogAfterNormalSave($pagelog, $expectedRevs, $expect, $expectExternal);
}
}

View File

@ -34,7 +34,8 @@ class Diff extends AbstractAction
/** @inheritdoc */
public function tplContent()
{
(new Ui\Diff())->show();
global $INFO;
(new Ui\PageDiff($INFO['id']))->preference('showIntro', true)->show();
}
}

View File

@ -22,7 +22,7 @@ class Revisions extends AbstractAction
/** @inheritdoc */
public function tplContent()
{
global $INPUT;
(new Ui\Revisions($INPUT->int('first')))->show();
global $INFO, $INPUT;
(new Ui\PageRevisions($INFO['id']))->show($INPUT->int('first'));
}
}

View File

@ -243,14 +243,11 @@ class Ajax {
* @author Kate Arzamastseva <pshns@ukr.net>
*/
protected function callMediadiff() {
global $NS;
global $INPUT;
$image = '';
if($INPUT->has('image')) $image = cleanID($INPUT->str('image'));
$NS = getNS($image);
$auth = auth_quickaclcheck("$NS:*");
media_diff($image, $NS, $auth, true);
(new Ui\MediaDiff($image))->preference('fromAjax', true)->show();
}
/**

View File

@ -2,16 +2,19 @@
namespace dokuwiki\ChangeLog;
use dokuwiki\Logger;
/**
* methods for handling of changelog of pages or media files
* ChangeLog Prototype; methods for handling changelog
*/
abstract class ChangeLog
{
use ChangeLogTrait;
/** @var string */
protected $id;
/** @var int */
protected $chunk_size;
protected $currentRevision;
/** @var array */
protected $cache;
@ -32,29 +35,10 @@ abstract class ChangeLog
$this->id = $id;
$this->setChunkSize($chunk_size);
// set property currentRevision and cache prior to getRevisionInfo($currentRev) call
$this->getCurrentRevisionInfo();
}
/**
* Set chunk size for file reading
* Chunk size zero let read whole file at once
*
* @param int $chunk_size maximum block size read from file
*/
public function setChunkSize($chunk_size)
{
if (!is_numeric($chunk_size)) $chunk_size = 0;
$this->chunk_size = (int)max($chunk_size, 0);
}
/**
* Returns path to changelog
*
* @return string path to file
*/
abstract protected function getChangelogFilename();
/**
* Returns path to current page/media
*
@ -63,7 +47,74 @@ abstract class ChangeLog
abstract protected function getFilename();
/**
* Get the changelog information for a specific page id and revision (timestamp)
* Check whether given revision is the current page
*
* @param int $rev timestamp of current page
* @return bool true if $rev is current revision, otherwise false
*/
public function isCurrentRevision($rev)
{
return $rev == $this->currentRevision();
}
/**
* Checks if the revision is last revision
*
* @param int $rev revision timestamp
* @return bool true if $rev is last revision, otherwise false
*/
public function isLastRevision($rev = null)
{
return $rev === $this->lastRevision();
}
/**
* Return the current revision identifer
*
* The "current" revision means current version of the page or media file. It is either
* identical with or newer than the "last" revision, that depends on whether the file
* has modified, created or deleted outside of DokuWiki.
* The value of identifier can be determined by timestamp as far as the file exists,
* otherwise it must be assigned larger than any other revisions to keep them sortable.
*
* @return int|false revision timestamp
*/
public function currentRevision()
{
if (!isset($this->currentRevision)) {
// set ChangeLog::currentRevision property
$this->getCurrentRevisionInfo();
}
return $this->currentRevision;
}
/**
* Return the last revision identifer, date value of the last entry of the changelog
*
* @return int|false revision timestamp
*/
public function lastRevision()
{
$revs = $this->getRevisions(-1, 1);
return empty($revs) ? false : $revs[0];
}
/**
* Save revision info to the cache pool
*
* @param array $info Revision info structure
* @return bool
*/
protected function cacheRevisionInfo($info)
{
if (!is_array($info)) return false;
//$this->cache[$this->id][$info['date']] ??= $info; // since php 7.4
$this->cache[$this->id][$info['date']] = $this->cache[$this->id][$info['date']] ?? $info;
return true;
}
/**
* Get the changelog information for a specific revision (timestamp)
*
* Adjacent changelog lines are optimistically parsed and cached to speed up
* consecutive calls to getRevisionInfo. For large changelog files, only the chunk
@ -78,13 +129,15 @@ abstract class ChangeLog
* - user: user name
* - sum: edit summary (or action reason)
* - extra: extra data (varies by line type)
* - sizechange: change of filesize
*
* @author Ben Coburn <btcoburn@silicodon.net>
* @author Kate Arzamastseva <pshns@ukr.net>
*/
public function getRevisionInfo($rev)
{
$rev = max($rev, 0);
$rev = max(0, $rev);
if (!$rev) return false;
// check if it's already in the memory cache
if (isset($this->cache[$this->id]) && isset($this->cache[$this->id][$rev])) {
@ -100,10 +153,8 @@ abstract class ChangeLog
// parse and cache changelog lines
foreach ($lines as $value) {
$tmp = parseChangelogLine($value);
if ($tmp !== false) {
$this->cache[$this->id][$tmp['date']] = $tmp;
}
$info = $this->parseLogLine($value);
$this->cacheRevisionInfo($info);
}
if (!isset($this->cache[$this->id][$rev])) {
return false;
@ -140,6 +191,9 @@ abstract class ChangeLog
$lines = array();
$count = 0;
$logfile = $this->getChangelogFilename();
if (!file_exists($logfile)) return $revs;
$num = max($num, 0);
if ($num == 0) {
return $revs;
@ -148,26 +202,22 @@ abstract class ChangeLog
if ($first < 0) {
$first = 0;
} else {
if (file_exists($this->getFilename())) {
// skip current revision if the page exists
$fileLastMod = $this->getFilename();
if (file_exists($fileLastMod) && $this->isLastRevision(filemtime($fileLastMod))) {
// skip last revision if the page exists
$first = max($first + 1, 0);
}
}
$file = $this->getChangelogFilename();
if (!file_exists($file)) {
return $revs;
}
if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
if (filesize($logfile) < $this->chunk_size || $this->chunk_size == 0) {
// read whole file
$lines = file($file);
$lines = file($logfile);
if ($lines === false) {
return $revs;
}
} else {
// read chunks backwards
$fp = fopen($file, 'rb'); // "file pointer"
$fp = fopen($logfile, 'rb'); // "file pointer"
if ($fp === false) {
return $revs;
}
@ -221,20 +271,17 @@ abstract class ChangeLog
$num = max(min(count($lines) - $first, $num), 0);
if ($first > 0 && $num > 0) {
$lines = array_slice($lines, max(count($lines) - $first - $num, 0), $num);
} else {
if ($first > 0 && $num == 0) {
$lines = array_slice($lines, 0, max(count($lines) - $first, 0));
} elseif ($first == 0 && $num > 0) {
$lines = array_slice($lines, max(count($lines) - $num, 0));
}
} elseif ($first > 0 && $num == 0) {
$lines = array_slice($lines, 0, max(count($lines) - $first, 0));
} elseif ($first == 0 && $num > 0) {
$lines = array_slice($lines, max(count($lines) - $num, 0));
}
// handle lines in reverse order
for ($i = count($lines) - 1; $i >= 0; $i--) {
$tmp = parseChangelogLine($lines[$i]);
if ($tmp !== false) {
$this->cache[$this->id][$tmp['date']] = $tmp;
$revs[] = $tmp['date'];
$info = $this->parseLogLine($lines[$i]);
if ($this->cacheRevisionInfo($info)) {
$revs[] = $info['date'];
}
}
@ -250,8 +297,10 @@ abstract class ChangeLog
* Adjacent changelog lines are optimistically parsed and cached to speed up
* consecutive calls to getRevisionInfo.
*
* @param int $rev revision timestamp used as startdate (doesn't need to be revisionnumber)
* @param int $direction give position of returned revision with respect to $rev; positive=next, negative=prev
* @param int $rev revision timestamp used as startdate
* (doesn't need to be exact revision number)
* @param int $direction give position of returned revision with respect to $rev;
positive=next, negative=prev
* @return bool|int
* timestamp of the requested revision
* otherwise false
@ -270,13 +319,13 @@ abstract class ChangeLog
list($fp, $lines, $head, $tail, $eof) = $this->readloglines($rev);
if (empty($lines)) return false;
// look for revisions later/earlier then $rev, when founded count till the wanted revision is reached
// look for revisions later/earlier than $rev, when founded count till the wanted revision is reached
// also parse and cache changelog lines for getRevisionInfo().
$revcounter = 0;
$relativerev = false;
$checkotherchunck = true; //always runs once
while (!$relativerev && $checkotherchunck) {
$tmp = array();
$info = array();
//parse in normal or reverse order
$count = count($lines);
if ($direction > 0) {
@ -287,14 +336,13 @@ abstract class ChangeLog
$step = -1;
}
for ($i = $start; $i >= 0 && $i < $count; $i = $i + $step) {
$tmp = parseChangelogLine($lines[$i]);
if ($tmp !== false) {
$this->cache[$this->id][$tmp['date']] = $tmp;
$info = $this->parseLogLine($lines[$i]);
if ($this->cacheRevisionInfo($info)) {
//look for revs older/earlier then reference $rev and select $direction-th one
if (($direction > 0 && $tmp['date'] > $rev) || ($direction < 0 && $tmp['date'] < $rev)) {
if (($direction > 0 && $info['date'] > $rev) || ($direction < 0 && $info['date'] < $rev)) {
$revcounter++;
if ($revcounter == abs($direction)) {
$relativerev = $tmp['date'];
$relativerev = $info['date'];
}
}
}
@ -302,7 +350,7 @@ abstract class ChangeLog
//true when $rev is found, but not the wanted follow-up.
$checkotherchunck = $fp
&& ($tmp['date'] == $rev || ($revcounter > 0 && !$relativerev))
&& ($info['date'] == $rev || ($revcounter > 0 && !$relativerev))
&& !(($tail == $eof && $direction > 0) || ($head == 0 && $direction < 0));
if ($checkotherchunck) {
@ -329,7 +377,7 @@ abstract class ChangeLog
*/
public function getRevisionsAround($rev1, $rev2, $max = 50)
{
$max = floor(abs($max) / 2) * 2 + 1;
$max = intval(abs($max) / 2) * 2 + 1;
$rev1 = max($rev1, 0);
$rev2 = max($rev2, 0);
@ -341,8 +389,7 @@ abstract class ChangeLog
}
} else {
//empty right side means a removed page. Look up last revision.
$revs = $this->getRevisions(-1, 1);
$rev2 = $revs[0];
$rev2 = $this->currentRevision();
}
//collect revisions around rev2
list($revs2, $allrevs, $fp, $lines, $head, $tail) = $this->retrieveRevisionsAround($rev2, $max);
@ -357,171 +404,31 @@ abstract class ChangeLog
if (empty($revs1)) $revs1 = array();
} else {
//revisions overlaps, reuse revisions around rev2
$lastrev = array_pop($allrevs); //keep last entry that could be external edit
$revs1 = $allrevs;
while ($head > 0) {
for ($i = count($lines) - 1; $i >= 0; $i--) {
$tmp = parseChangelogLine($lines[$i]);
if ($tmp !== false) {
$this->cache[$this->id][$tmp['date']] = $tmp;
$revs1[] = $tmp['date'];
$info = $this->parseLogLine($lines[$i]);
if ($this->cacheRevisionInfo($info)) {
$revs1[] = $info['date'];
$index++;
if ($index > floor($max / 2)) break 2;
if ($index > intval($max / 2)) break 2;
}
}
list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
}
sort($revs1);
$revs1[] = $lastrev; //push back last entry
//return wanted selection
$revs1 = array_slice($revs1, max($index - floor($max / 2), 0), $max);
$revs1 = array_slice($revs1, max($index - intval($max / 2), 0), $max);
}
return array(array_reverse($revs1), array_reverse($revs2));
}
/**
* Checks if the ID has old revisons
* @return boolean
*/
public function hasRevisions() {
$file = $this->getChangelogFilename();
return file_exists($file);
}
/**
* Returns lines from changelog.
* If file larger than $chuncksize, only chunck is read that could contain $rev.
*
* @param int $rev revision timestamp
* @return array|false
* if success returns array(fp, array(changeloglines), $head, $tail, $eof)
* where fp only defined for chuck reading, needs closing.
* otherwise false
*/
protected function readloglines($rev)
{
$file = $this->getChangelogFilename();
if (!file_exists($file)) {
return false;
}
$fp = null;
$head = 0;
$tail = 0;
$eof = 0;
if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
// read whole file
$lines = file($file);
if ($lines === false) {
return false;
}
} else {
// read by chunk
$fp = fopen($file, 'rb'); // "file pointer"
if ($fp === false) {
return false;
}
$head = 0;
fseek($fp, 0, SEEK_END);
$eof = ftell($fp);
$tail = $eof;
// find chunk
while ($tail - $head > $this->chunk_size) {
$finger = $head + floor(($tail - $head) / 2.0);
$finger = $this->getNewlinepointer($fp, $finger);
$tmp = fgets($fp);
if ($finger == $head || $finger == $tail) {
break;
}
$tmp = parseChangelogLine($tmp);
$finger_rev = $tmp['date'];
if ($finger_rev > $rev) {
$tail = $finger;
} else {
$head = $finger;
}
}
if ($tail - $head < 1) {
// cound not find chunk, assume requested rev is missing
fclose($fp);
return false;
}
$lines = $this->readChunk($fp, $head, $tail);
}
return array(
$fp,
$lines,
$head,
$tail,
$eof,
);
}
/**
* Read chunk and return array with lines of given chunck.
* Has no check if $head and $tail are really at a new line
*
* @param resource $fp resource filepointer
* @param int $head start point chunck
* @param int $tail end point chunck
* @return array lines read from chunck
*/
protected function readChunk($fp, $head, $tail)
{
$chunk = '';
$chunk_size = max($tail - $head, 0); // found chunk size
$got = 0;
fseek($fp, $head);
while ($got < $chunk_size && !feof($fp)) {
$tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0));
if ($tmp === false) { //error state
break;
}
$got += strlen($tmp);
$chunk .= $tmp;
}
$lines = explode("\n", $chunk);
array_pop($lines); // remove trailing newline
return $lines;
}
/**
* Set pointer to first new line after $finger and return its position
*
* @param resource $fp filepointer
* @param int $finger a pointer
* @return int pointer
*/
protected function getNewlinepointer($fp, $finger)
{
fseek($fp, $finger);
$nl = $finger;
if ($finger > 0) {
fgets($fp); // slip the finger forward to a new line
$nl = ftell($fp);
}
return $nl;
}
/**
* Check whether given revision is the current page
*
* @param int $rev timestamp of current page
* @return bool true if $rev is current revision, otherwise false
*/
public function isCurrentRevision($rev)
{
return $rev == @filemtime($this->getFilename());
}
/**
* Return an existing revision for a specific date which is
* the current one or younger or equal then the date
@ -531,8 +438,9 @@ abstract class ChangeLog
*/
public function getLastRevisionAt($date_at)
{
$fileLastMod = $this->getFilename();
//requested date_at(timestamp) younger or equal then modified_time($this->id) => load current
if (file_exists($this->getFilename()) && $date_at >= @filemtime($this->getFilename())) {
if (file_exists($fileLastMod) && $date_at >= @filemtime($fileLastMod)) {
return '';
} else {
if ($rev = $this->getRelativeRevision($date_at + 1, -1)) { //+1 to get also the requested date revision
@ -543,51 +451,13 @@ abstract class ChangeLog
}
}
/**
* Returns the next lines of the changelog of the chunck before head or after tail
*
* @param resource $fp filepointer
* @param int $head position head of last chunk
* @param int $tail position tail of last chunk
* @param int $direction positive forward, negative backward
* @return array with entries:
* - $lines: changelog lines of readed chunk
* - $head: head of chunk
* - $tail: tail of chunk
*/
protected function readAdjacentChunk($fp, $head, $tail, $direction)
{
if (!$fp) return array(array(), $head, $tail);
if ($direction > 0) {
//read forward
$head = $tail;
$tail = $head + floor($this->chunk_size * (2 / 3));
$tail = $this->getNewlinepointer($fp, $tail);
} else {
//read backward
$tail = $head;
$head = max($tail - $this->chunk_size, 0);
while (true) {
$nl = $this->getNewlinepointer($fp, $head);
// was the chunk big enough? if not, take another bite
if ($nl > 0 && $tail <= $nl) {
$head = max($head - $this->chunk_size, 0);
} else {
$head = $nl;
break;
}
}
}
//load next chunck
$lines = $this->readChunk($fp, $head, $tail);
return array($lines, $head, $tail);
}
/**
* Collect the $max revisions near to the timestamp $rev
*
* Ideally, half of retrieved timestamps are older than $rev, another half are newer.
* The returned array $requestedrevs may not contain the reference timestamp $rev
* when it does not match any revision value recorded in changelog.
*
* @param int $rev revision timestamp
* @param int $max maximum number of revisions to be returned
* @return bool|array
@ -602,38 +472,49 @@ abstract class ChangeLog
*/
protected function retrieveRevisionsAround($rev, $max)
{
//get lines from changelog
list($fp, $lines, $starthead, $starttail, /* $eof */) = $this->readloglines($rev);
if (empty($lines)) return false;
//parse chunk containing $rev, and read forward more chunks until $max/2 is reached
$head = $starthead;
$tail = $starttail;
$revs = array();
$aftercount = $beforecount = 0;
//get lines from changelog
list($fp, $lines, $starthead, $starttail, $eof) = $this->readloglines($rev);
if (empty($lines)) return false;
//parse changelog lines in chunk, and read forward more chunks until $max/2 is reached
$head = $starthead;
$tail = $starttail;
while (count($lines) > 0) {
foreach ($lines as $line) {
$tmp = parseChangelogLine($line);
if ($tmp !== false) {
$this->cache[$this->id][$tmp['date']] = $tmp;
$revs[] = $tmp['date'];
if ($tmp['date'] >= $rev) {
$info = $this->parseLogLine($line);
if ($this->cacheRevisionInfo($info)) {
$revs[] = $info['date'];
if ($info['date'] >= $rev) {
//count revs after reference $rev
$aftercount++;
if ($aftercount == 1) $beforecount = count($revs);
}
//enough revs after reference $rev?
if ($aftercount > floor($max / 2)) break 2;
if ($aftercount > intval($max / 2)) break 2;
}
}
//retrieve next chunk
list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, 1);
}
if ($aftercount == 0) return false;
$lasttail = $tail;
//read additional chuncks backward until $max/2 is reached and total number of revs is equal to $max
// add a possible revision of external edit, create or deletion
if ($lasttail == $eof && $aftercount <= intval($max / 2) &&
count($revs) && !$this->isCurrentRevision($revs[count($revs)-1])
) {
$revs[] = $this->currentRevision;
$aftercount++;
}
if ($aftercount == 0) {
//given timestamp $rev is newer than the most recent line in chunk
return false; //FIXME: or proceed to collect older revisions?
}
//read more chunks backward until $max/2 is reached and total number of revs is equal to $max
$lines = array();
$i = 0;
if ($aftercount > 0) {
@ -643,24 +524,138 @@ abstract class ChangeLog
list($lines, $head, $tail) = $this->readAdjacentChunk($fp, $head, $tail, -1);
for ($i = count($lines) - 1; $i >= 0; $i--) {
$tmp = parseChangelogLine($lines[$i]);
if ($tmp !== false) {
$this->cache[$this->id][$tmp['date']] = $tmp;
$revs[] = $tmp['date'];
$info = $this->parseLogLine($lines[$i]);
if ($this->cacheRevisionInfo($info)) {
$revs[] = $info['date'];
$beforecount++;
//enough revs before reference $rev?
if ($beforecount > max(floor($max / 2), $max - $aftercount)) break 2;
if ($beforecount > max(intval($max / 2), $max - $aftercount)) break 2;
}
}
}
}
sort($revs);
//keep only non-parsed lines
$lines = array_slice($lines, 0, $i);
sort($revs);
//trunk desired selection
$requestedrevs = array_slice($revs, -$max, $max);
return array($requestedrevs, $revs, $fp, $lines, $head, $lasttail);
}
/**
* Get the current revision information, considering external edit, create or deletion
*
* When the file has not modified since its last revision, the infomation of the last
* change that had already recorded in the changelog is returned as current change info.
* Otherwise, the change infomation since the last revision caused outside DokuWiki
* should be returned, which is referred as "external revision".
*
* The change date of the file can be determined by timestamp as far as the file exists,
* however this is not possible when the file has already deleted outside of DokuWiki.
* In such case we assign 1 sec before current time() for the external deletion.
* As a result, the value of current revision identifier may change each time because:
* 1) the file has again modified outside of DokuWiki, or
* 2) the value is essentially volatile for deleted but once existed files.
*
* @return bool|array false when page had never existed or array with entries:
* - date: revision identifier (timestamp or last revision +1)
* - ip: IPv4 address (127.0.0.1)
* - type: log line type
* - id: id of page or media
* - user: user name
* - sum: edit summary (or action reason)
* - extra: extra data (varies by line type)
* - sizechange: change of filesize
* - timestamp: unix timestamp or false (key set only for external edit occurred)
*
* @author Satoshi Sahara <sahara.satoshi@gmail.com>
*/
public function getCurrentRevisionInfo()
{
global $lang;
if (isset($this->currentRevision)) return $this->getRevisionInfo($this->currentRevision);
// get revision id from the item file timestamp and chagelog
$fileLastMod = $this->getFilename();
$fileRev = @filemtime($fileLastMod); // false when the file not exist
$lastRev = $this->lastRevision(); // false when no changelog
if (!$fileRev && !$lastRev) { // has never existed
$this->currentRevision = false;
return false;
} elseif ($fileRev === $lastRev) { // not external edit
$this->currentRevision = $lastRev;
return $this->getRevisionInfo($lastRev);
}
if (!$fileRev && $lastRev) { // item file does not exist
// check consistency against changelog
$revInfo = $this->getRevisionInfo($lastRev);
if ($revInfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
$this->currentRevision = $lastRev;
return $this->getRevisionInfo($lastRev);
}
// externally deleted, set revision date as late as possible
$revInfo = [
'date' => max($lastRev +1, time() -1), // 1 sec before now or new page save
'ip' => '127.0.0.1',
'type' => DOKU_CHANGE_TYPE_DELETE,
'id' => $this->id,
'user' => '',
'sum' => $lang['deleted'].' - '.$lang['external_edit'].' ('.$lang['unknowndate'].')',
'extra' => '',
'sizechange' => -io_getSizeFile($this->getFilename($lastRev)),
'timestamp' => false,
];
} elseif ($fileRev) { // item file exist
// here, file timestamp is different with last revision in changelog
$isJustCreated = $lastRev === false || (
$fileRev > $lastRev &&
$this->getRevisionInfo($lastRev)['type'] == DOKU_CHANGE_TYPE_DELETE
);
$filesize_new = filesize($this->getFilename());
$filesize_old = $isJustCreated ? 0 : io_getSizeFile($this->getFilename($lastRev));
$sizechange = $filesize_new - $filesize_old;
if ($isJustCreated) {
$timestamp = $fileRev;
$sum = $lang['created'].' - '.$lang['external_edit'];
} elseif ($fileRev > $lastRev) {
$timestamp = $fileRev;
$sum = $lang['external_edit'];
} else {
// $fileRev is older than $lastRev, that is erroneous/incorrect occurence.
$msg = "Warning: current file modification time is older than last revision date";
$details = 'File revision: '.$fileRev.' '.strftime("%Y-%m-%d %H:%M:%S", $fileRev)."\n"
.'Last revision: '.$lastRev.' '.strftime("%Y-%m-%d %H:%M:%S", $lastRev);
Logger::error($msg, $details, $this->getFilename());
$timestamp = false;
$sum = $lang['external_edit'].' ('.$lang['unknowndate'].')';
}
// externally created or edited
$revInfo = [
'date' => $timestamp ?: $lastRev +1,
'ip' => '127.0.0.1',
'type' => $isJustCreated ? DOKU_CHANGE_TYPE_CREATE : DOKU_CHANGE_TYPE_EDIT,
'id' => $this->id,
'user' => '',
'sum' => $sum,
'extra' => '',
'sizechange' => $sizechange,
'timestamp' => $timestamp,
];
}
// cache current revision information of external edition
$this->currentRevision = $revInfo['date'];
$this->cache[$this->id][$this->currentRevision] = $revInfo;
return $this->getRevisionInfo($this->currentRevision);
}
}

View File

@ -0,0 +1,268 @@
<?php
namespace dokuwiki\ChangeLog;
/**
* Provides methods for handling of changelog
*/
trait ChangeLogTrait
{
/**
* Adds an entry to the changelog file
*
* @return array added logline as revision info
*/
abstract public function addLogEntry(array $info, $timestamp = null);
/**
* Parses a changelog line into it's components
*
* @author Ben Coburn <btcoburn@silicodon.net>
*
* @param string $line changelog line
* @return array|bool parsed line or false
*/
public static function parseLogLine($line)
{
$info = explode("\t", rtrim($line, "\n"));
if ($info !== false && count($info) > 1) {
return $entry = array(
'date' => (int)$info[0], // unix timestamp
'ip' => $info[1], // IPv4 address (127.0.0.1)
'type' => $info[2], // log line type
'id' => $info[3], // page id
'user' => $info[4], // user name
'sum' => $info[5], // edit summary (or action reason)
'extra' => $info[6], // extra data (varies by line type)
'sizechange' => (isset($info[7]) && $info[7] !== '') ? (int)$info[7] : null, //
);
} else {
return false;
}
}
/**
* Build a changelog line from it's components
*
* @param array $info Revision info structure
* @param int $timestamp logline date (optional)
* @return string changelog line
*/
public static function buildLogLine(array &$info, $timestamp = null)
{
$strip = ["\t", "\n"];
$entry = array(
'date' => $timestamp ?? $info['date'],
'ip' => $info['ip'],
'type' => str_replace($strip, '', $info['type']),
'id' => $info['id'],
'user' => $info['user'],
'sum' => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $info['sum']), 0, 255),
'extra' => str_replace($strip, '', $info['extra']),
'sizechange' => $info['sizechange'],
);
$info = $entry;
return $line = implode("\t", $entry) ."\n";
}
/**
* Returns path to changelog
*
* @return string path to file
*/
abstract protected function getChangelogFilename();
/**
* Checks if the ID has old revisons
* @return boolean
*/
public function hasRevisions()
{
$logfile = $this->getChangelogFilename();
return file_exists($logfile);
}
/** @var int */
protected $chunk_size;
/**
* Set chunk size for file reading
* Chunk size zero let read whole file at once
*
* @param int $chunk_size maximum block size read from file
*/
public function setChunkSize($chunk_size)
{
if (!is_numeric($chunk_size)) $chunk_size = 0;
$this->chunk_size = (int)max($chunk_size, 0);
}
/**
* Returns lines from changelog.
* If file larger than $chuncksize, only chunck is read that could contain $rev.
*
* When reference timestamp $rev is outside time range of changelog, readloglines() will return
* lines in first or last chunk, but they obviously does not contain $rev.
*
* @param int $rev revision timestamp
* @return array|false
* if success returns array(fp, array(changeloglines), $head, $tail, $eof)
* where fp only defined for chuck reading, needs closing.
* otherwise false
*/
protected function readloglines($rev)
{
$file = $this->getChangelogFilename();
if (!file_exists($file)) {
return false;
}
$fp = null;
$head = 0;
$tail = 0;
$eof = 0;
if (filesize($file) < $this->chunk_size || $this->chunk_size == 0) {
// read whole file
$lines = file($file);
if ($lines === false) {
return false;
}
} else {
// read by chunk
$fp = fopen($file, 'rb'); // "file pointer"
if ($fp === false) {
return false;
}
$head = 0;
fseek($fp, 0, SEEK_END);
$eof = ftell($fp);
$tail = $eof;
// find chunk
while ($tail - $head > $this->chunk_size) {
$finger = $head + intval(($tail - $head) / 2);
$finger = $this->getNewlinepointer($fp, $finger);
$tmp = fgets($fp);
if ($finger == $head || $finger == $tail) {
break;
}
$info = $this->parseLogLine($tmp);
$finger_rev = $info['date'];
if ($finger_rev > $rev) {
$tail = $finger;
} else {
$head = $finger;
}
}
if ($tail - $head < 1) {
// cound not find chunk, assume requested rev is missing
fclose($fp);
return false;
}
$lines = $this->readChunk($fp, $head, $tail);
}
return array(
$fp,
$lines,
$head,
$tail,
$eof,
);
}
/**
* Read chunk and return array with lines of given chunck.
* Has no check if $head and $tail are really at a new line
*
* @param resource $fp resource filepointer
* @param int $head start point chunck
* @param int $tail end point chunck
* @return array lines read from chunck
*/
protected function readChunk($fp, $head, $tail)
{
$chunk = '';
$chunk_size = max($tail - $head, 0); // found chunk size
$got = 0;
fseek($fp, $head);
while ($got < $chunk_size && !feof($fp)) {
$tmp = @fread($fp, max(min($this->chunk_size, $chunk_size - $got), 0));
if ($tmp === false) { //error state
break;
}
$got += strlen($tmp);
$chunk .= $tmp;
}
$lines = explode("\n", $chunk);
array_pop($lines); // remove trailing newline
return $lines;
}
/**
* Set pointer to first new line after $finger and return its position
*
* @param resource $fp filepointer
* @param int $finger a pointer
* @return int pointer
*/
protected function getNewlinepointer($fp, $finger)
{
fseek($fp, $finger);
$nl = $finger;
if ($finger > 0) {
fgets($fp); // slip the finger forward to a new line
$nl = ftell($fp);
}
return $nl;
}
/**
* Returns the next lines of the changelog of the chunck before head or after tail
*
* @param resource $fp filepointer
* @param int $head position head of last chunk
* @param int $tail position tail of last chunk
* @param int $direction positive forward, negative backward
* @return array with entries:
* - $lines: changelog lines of readed chunk
* - $head: head of chunk
* - $tail: tail of chunk
*/
protected function readAdjacentChunk($fp, $head, $tail, $direction)
{
if (!$fp) return array(array(), $head, $tail);
if ($direction > 0) {
//read forward
$head = $tail;
$tail = $head + intval($this->chunk_size * (2 / 3));
$tail = $this->getNewlinepointer($fp, $tail);
} else {
//read backward
$tail = $head;
$head = max($tail - $this->chunk_size, 0);
while (true) {
$nl = $this->getNewlinepointer($fp, $head);
// was the chunk big enough? if not, take another bite
if ($nl > 0 && $tail <= $nl) {
$head = max($head - $this->chunk_size, 0);
} else {
$head = $nl;
break;
}
}
}
//load next chunck
$lines = $this->readChunk($fp, $head, $tail);
return array($lines, $head, $tail);
}
}

View File

@ -3,7 +3,7 @@
namespace dokuwiki\ChangeLog;
/**
* handles changelog of a media file
* Class MediaChangeLog; handles changelog of a media file
*/
class MediaChangeLog extends ChangeLog
{
@ -21,10 +21,40 @@ class MediaChangeLog extends ChangeLog
/**
* Returns path to current page/media
*
* @param string|int $rev empty string or revision timestamp
* @return string path to file
*/
protected function getFilename()
protected function getFilename($rev = '')
{
return mediaFN($this->id);
return mediaFN($this->id, $rev);
}
/**
* Adds an entry to the changelog
*
* @param array $info Revision info structure of a media file
* @param int $timestamp logline date (optional)
* @return array revision info of added logline
*
* @see also addMediaLogEntry() in inc/changelog.php file
*/
public function addLogEntry(array $info, $timestamp = null)
{
global $conf;
if (isset($timestamp)) unset($this->cache[$this->id][$info['date']]);
// add changelog lines
$logline = $this->buildLogLine($info, $timestamp);
io_saveFile(mediaMetaFN($this->id,'.changes'), $logline, $append = true);
io_saveFile($conf['media_changelog'], $logline, $append = true); //global changelog cache
// update cache
$this->currentRevision = $info['date'];
$this->cache[$this->id][$this->currentRevision] = $info;
return $info;
}
}

View File

@ -3,7 +3,7 @@
namespace dokuwiki\ChangeLog;
/**
* handles changelog of a wiki page
* Class PageChangeLog; handles changelog of a wiki page
*/
class PageChangeLog extends ChangeLog
{
@ -21,10 +21,40 @@ class PageChangeLog extends ChangeLog
/**
* Returns path to current page/media
*
* @param string|int $rev empty string or revision timestamp
* @return string path to file
*/
protected function getFilename()
protected function getFilename($rev = '')
{
return wikiFN($this->id);
return wikiFN($this->id, $rev);
}
/**
* Adds an entry to the changelog
*
* @param array $info Revision info structure of a page
* @param int $timestamp logline date (optional)
* @return array revision info of added logline
*
* @see also addLogEntry() in inc/changelog.php file
*/
public function addLogEntry(array $info, $timestamp = null)
{
global $conf;
if (isset($timestamp)) unset($this->cache[$this->id][$info['date']]);
// add changelog lines
$logline = $this->buildLogLine($info, $timestamp);
io_saveFile(metaFN($this->id,'.changes'), $logline, $append = true);
io_saveFile($conf['changelog'], $logline, $append = true); //global changelog cache
// update cache
$this->currentRevision = $info['date'];
$this->cache[$this->id][$this->currentRevision] = $info;
return $info;
}
}

332
inc/File/PageFile.php Normal file
View File

@ -0,0 +1,332 @@
<?php
namespace dokuwiki\File;
use dokuwiki\Cache\CacheInstructions;
use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\Extension\Event;
use dokuwiki\Logger;
/**
* Class PageFile : handles wiki text file and its change management for specific page
*/
class PageFile
{
protected $id;
/* @var PageChangeLog $changelog */
public $changelog;
/* @var array $data initial data when event COMMON_WIKIPAGE_SAVE triggered */
protected $data;
/**
* PageFile constructor.
*
* @param string $id
*/
public function __construct($id)
{
$this->id = $id;
$this->changelog = new PageChangeLog($this->id);
}
/** @return string */
public function getId()
{
return $this->id;
}
/** @return string */
public function getPath($rev = '')
{
return wikiFN($this->id, $rev);
}
/**
* Get raw WikiText of the page, considering change type at revision date
* similar to function rawWiki($id, $rev = '')
*
* @param int|false $rev timestamp when a revision of wikitext is desired
* @return string
*/
public function rawWikiText($rev = null)
{
if ($rev !== null) {
$revInfo = $rev ? $this->changelog->getRevisionInfo($rev) : false;
return (!$revInfo || $revInfo['type'] == DOKU_CHANGE_TYPE_DELETE)
? '' // attic stores complete last page version for a deleted page
: io_readWikiPage($this->getPath($rev), $this->id, $rev); // retrieve from attic
} else {
return io_readWikiPage($this->getPath(), $this->id, '');
}
}
/**
* Saves a wikitext by calling io_writeWikiPage.
* Also directs changelog and attic updates.
*
* @author Andreas Gohr <andi@splitbrain.org>
* @author Ben Coburn <btcoburn@silicodon.net>
*
* @param string $text wikitext being saved
* @param string $summary summary of text update
* @param bool $minor mark this saved version as minor update
* @return array data of event COMMON_WIKIPAGE_SAVE
*/
public function saveWikiText($text, $summary, $minor = false)
{
/* Note to developers:
This code is subtle and delicate. Test the behavior of
the attic and changelog with dokuwiki and external edits
after any changes. External edits change the wiki page
directly without using php or dokuwiki.
*/
global $conf;
global $lang;
global $REV;
/* @var Input $INPUT */
global $INPUT;
// prevent recursive call
if (isset($this->data)) return;
$pagefile = $this->getPath();
$currentRevision = @filemtime($pagefile); // int or false
$currentContent = $this->rawWikiText();
$currentSize = file_exists($pagefile) ? filesize($pagefile) : 0;
// prepare data for event COMMON_WIKIPAGE_SAVE
$data = array(
'id' => $this->id, // should not be altered by any handlers
'file' => $pagefile, // same above
'changeType' => null, // set prior to event, and confirm later
'revertFrom' => $REV,
'oldRevision' => $currentRevision,
'oldContent' => $currentContent,
'newRevision' => 0, // only available in the after hook
'newContent' => $text,
'summary' => $summary,
'contentChanged' => (bool)($text != $currentContent), // confirm later
'changeInfo' => '', // automatically determined by revertFrom
'sizechange' => strlen($text) - strlen($currentContent), // TBD
);
// determine tentatively change type and relevant elements of event data
if ($data['revertFrom']) {
// new text may differ from exact revert revision
$data['changeType'] = DOKU_CHANGE_TYPE_REVERT;
$data['changeInfo'] = $REV;
} elseif (trim($data['newContent']) == '') {
// empty or whitespace only content deletes
$data['changeType'] = DOKU_CHANGE_TYPE_DELETE;
} elseif (!file_exists($pagefile)) {
$data['changeType'] = DOKU_CHANGE_TYPE_CREATE;
} else {
// minor edits allowable only for logged in users
$is_minor_change = ($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER'));
$data['changeType'] = $is_minor_change
? DOKU_CHANGE_TYPE_MINOR_EDIT
: DOKU_CHANGE_TYPE_EDIT;
}
$this->data = $data;
$data['page'] = $this; // allow event handlers to use this class methods
$event = new Event('COMMON_WIKIPAGE_SAVE', $data);
if (!$event->advise_before()) return;
// if the content has not been changed, no save happens (plugins may override this)
if (!$data['contentChanged']) return;
// Check whether the pagefile has modified during $event->advise_before()
clearstatcache();
$fileRev = @filemtime($pagefile);
if ($fileRev === $currentRevision) {
// pagefile has not touched by plugin's event handler
// add a potential external edit entry to changelog and store it into attic
$this->detectExternalEdit();
$filesize_old = $currentSize;
} else {
// pagefile has modified by plugin's event handler, confirm sizechange
$filesize_old = (
$data['changeType'] == DOKU_CHANGE_TYPE_CREATE || (
$data['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($pagefile))
) ? 0 : filesize($pagefile);
}
// make change to the current file
if ($data['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
// nothing to do when the file has already deleted
if (!file_exists($pagefile)) return;
// autoset summary on deletion
if (blank($data['summary'])) {
$data['summary'] = $lang['deleted'];
}
// send "update" event with empty data, so plugins can react to page deletion
$ioData = array([$pagefile, '', false], getNS($this->id), noNS($this->id), false);
Event::createAndTrigger('IO_WIKIPAGE_WRITE', $ioData);
// pre-save deleted revision
@touch($pagefile);
clearstatcache();
$data['newRevision'] = $this->saveOldRevision();
// remove empty file
@unlink($pagefile);
$filesize_new = 0;
// don't remove old meta info as it should be saved, plugins can use
// IO_WIKIPAGE_WRITE for removing their metadata...
// purge non-persistant meta data
p_purge_metadata($this->id);
// remove empty namespaces
io_sweepNS($this->id, 'datadir');
io_sweepNS($this->id, 'mediadir');
} else {
// save file (namespace dir is created in io_writeWikiPage)
io_writeWikiPage($pagefile, $data['newContent'], $this->id);
// pre-save the revision, to keep the attic in sync
$data['newRevision'] = $this->saveOldRevision();
$filesize_new = filesize($pagefile);
}
$data['sizechange'] = $filesize_new - $filesize_old;
$event->advise_after();
unset($data['page']);
// adds an entry to the changelog and saves the metadata for the page
$logEntry = $this->changelog->addLogEntry([
'date' => $data['newRevision'],
'ip' => clientIP(true),
'type' => $data['changeType'],
'id' => $this->id,
'user' => $INPUT->server->str('REMOTE_USER'),
'sum' => $data['summary'],
'extra' => $data['changeInfo'],
'sizechange' => $data['sizechange'],
]);
// update metadata
$this->updateMetadata($logEntry);
// update the purgefile (timestamp of the last time anything within the wiki was changed)
io_saveFile($conf['cachedir'].'/purgefile', time());
return $data;
}
/**
* Checks if the current page version is newer than the last entry in the page's changelog.
* If so, we assume it has been an external edit and we create an attic copy and add a proper
* changelog line.
*
* This check is only executed when the page is about to be saved again from the wiki,
* triggered in @see saveWikiText()
*/
public function detectExternalEdit()
{
$revInfo = $this->changelog->getCurrentRevisionInfo();
// only interested in external revision
if (empty($revInfo) || !array_key_exists('timestamp', $revInfo)) return;
if ($revInfo['type'] != DOKU_CHANGE_TYPE_DELETE && !$revInfo['timestamp']) {
// file is older than last revision, that is erroneous/incorrect occurence.
// try to change file modification time
$fileLastMod = $this->getPath();
$wrong_timestamp = filemtime($fileLastMod);
if (touch($fileLastMod, $revInfo['date'])) {
clearstatcache();
$msg = "detectExternalEdit($id): timestamp successfully modified";
$details = '('.$wrong_timestamp.' -> '.$revInfo['date'].')';
Logger::error($msg, $details, $fileLastMod);
} else {
// runtime error
$msg = "detectExternalEdit($id): page file should be newer than last revision "
.'('.filemtime($fileLastMod).' < '. $this->changelog->lastRevision() .')';
throw new \RuntimeException($msg);
}
}
// keep at least 1 sec before new page save
if ($revInfo['date'] == time()) sleep(1); // wait a tick
// store externally edited file to the attic folder
$this->saveOldRevision();
// add a changelog entry for externally edited file
$revInfo = $this->changelog->addLogEntry($revInfo);
// remove soon to be stale instructions
$cache = new CacheInstructions($this->id, $this->getPath());
$cache->removeCache();
}
/**
* Moves the current version to the attic and returns its revision date
*
* @author Andreas Gohr <andi@splitbrain.org>
*
* @return int|string revision timestamp
*/
public function saveOldRevision()
{
$oldfile = $this->getPath();
if (!file_exists($oldfile)) return '';
$date = filemtime($oldfile);
$newfile = $this->getPath($date);
io_writeWikiPage($newfile, $this->rawWikiText(), $this->id, $date);
return $date;
}
/**
* Update metadata of changed page
*
* @param array $logEntry changelog entry
*/
public function updateMetadata(array $logEntry)
{
global $INFO;
list(
'date' => $date,
'type' => $changeType,
'user' => $user,
) = $logEntry;
$wasRemoved = ($changeType === DOKU_CHANGE_TYPE_DELETE);
$wasCreated = ($changeType === DOKU_CHANGE_TYPE_CREATE);
$wasReverted = ($changeType === DOKU_CHANGE_TYPE_REVERT);
$wasMinorEdit = ($changeType === DOKU_CHANGE_TYPE_MINOR_EDIT);
$createdDate = @filectime($this->getPath());
if ($wasRemoved) return;
$oldmeta = p_read_metadata($this->id)['persistent'];
$meta = array();
if ($wasCreated &&
(empty($oldmeta['date']['created']) || $oldmeta['date']['created'] === $createdDate)
) {
// newly created
$meta['date']['created'] = $createdDate;
if ($user) {
$meta['creator'] = $INFO['userinfo']['name'] ?? null;
$meta['user'] = $user;
}
} elseif (($wasCreated || $wasReverted) && !empty($oldmeta['date']['created'])) {
// re-created / restored
$meta['date']['created'] = $oldmeta['date']['created'];
$meta['date']['modified'] = $createdDate; // use the files ctime here
$meta['creator'] = $oldmeta['creator'] ?? null;
if ($user) {
$meta['contributor'][$user] = $INFO['userinfo']['name'] ?? null;
}
} elseif (!$wasMinorEdit) { // non-minor modification
$meta['date']['modified'] = $date;
if ($user) {
$meta['contributor'][$user] = $INFO['userinfo']['name'] ?? null;
}
}
$meta['last_change'] = $logEntry;
p_set_metadata($this->id, $meta);
}
}

View File

@ -87,7 +87,8 @@ class BulkSubscriptionSender extends SubscriptionSender
$n = 0;
while (!is_null($rev) && $rev['date'] >= $lastupdate &&
($INPUT->server->str('REMOTE_USER') === $rev['user'] ||
$rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT)) {
$rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT)
) {
$pagelog = new PageChangeLog($rev['id']);
$rev = $pagelog->getRevisions($n++, 1);
$rev = (count($rev) > 0) ? $rev[0] : null;

View File

@ -5,6 +5,7 @@ namespace dokuwiki;
use dokuwiki\Extension\Event;
use dokuwiki\Sitemap\Mapper;
use dokuwiki\Subscriptions\BulkSubscriptionSender;
use dokuwiki\ChangeLog\ChangeLog;
/**
* Class TaskRunner
@ -120,7 +121,7 @@ class TaskRunner
$out_lines = [];
$old_lines = [];
for ($i = 0; $i < count($lines); $i++) {
$log = parseChangelogLine($lines[$i]);
$log = ChangeLog::parseLogLine($lines[$i]);
if ($log === false) {
continue; // discard junk
}

View File

@ -2,502 +2,137 @@
namespace dokuwiki\Ui;
use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\ChangeLog\MediaChangeLog;
use dokuwiki\Extension\Event;
use dokuwiki\Form\Form;
use dokuwiki\ChangeLog\ChangeLog;
/**
* DokuWiki Diff Interface
* parent class of PageDiff and MediaDiff
*
* @package dokuwiki\Ui
*/
class Diff extends Ui
abstract class Diff extends Ui
{
protected $text;
protected $showIntro;
protected $difftype;
/* @var string */
protected $id; // page id or media id
/* @var int */
protected $oldRev; // timestamp of older revision
protected $newRev; // timestamp of newer revision
/* @var array */
protected $preference = [];
/* @var ChangeLog */
protected $changelog; // PageChangeLog or MediaChangeLog object
/**
* Diff Ui constructor
*
* @param string $text when non-empty: compare with this text with most current version
* @param bool $showIntro display the intro text
* @param string $difftype diff view type (inline or sidebyside)
* @param string $id page id or media id
*/
public function __construct($text = '', $showIntro = true, $difftype = null)
public function __construct($id)
{
$this->text = $text;
$this->showIntro = $showIntro;
// determine diff view type
if (isset($difftype)) {
$this->difftype = $difftype;
} else {
global $INPUT;
global $INFO;
$this->difftype = $INPUT->str('difftype') ?: get_doku_pref('difftype', $difftype);
if (empty($this->difftype) && $INFO['ismobile']) {
$this->difftype = 'inline';
}
}
if ($this->difftype !== 'inline') $this->difftype = 'sidebyside';
$this->id = $id;
$this->setChangeLog();
}
/**
* Show diff
* between current page version and provided $text
* or between the revisions provided via GET or POST
* set class property changelog
*/
abstract protected function setChangeLog();
/**
* Prepare revision info of comparison pair
*/
abstract protected function preProcess();
/**
* Set a pair of revisions to be compared
*
* @author Andreas Gohr <andi@splitbrain.org>
* @param int $oldRev
* @param int $newRev
* @return $this
*/
public function compare($oldRev, $newRev)
{
if ($oldRev < $newRev) {
[$this->oldRev, $this->newRev] = [$oldRev, $newRev];
} else {
[$this->oldRev, $this->newRev] = [$newRev, $oldRev];
}
return $this;
}
/**
* Gets or Sets preference of the Ui\Diff object
*
* @param string|array $prefs a key name or key-value pair(s)
* @param mixed $value value used when the first args is string
* @return array|$this
*/
public function preference($prefs = null, $value = null)
{
// set
if (is_string($prefs) && isset($value)) {
$this->preference[$prefs] = $value;
return $this;
} elseif (is_array($prefs)) {
foreach ($prefs as $name => $value) {
$this->preference[$name] = $value;
}
return $this;
}
// get
return $this->preference;
}
/**
* Handle requested revision(s)
*
* @return void
*/
public function show()
protected function handle()
{
global $ID;
global $REV;
global $lang;
global $INPUT;
global $INFO;
$pagelog = new PageChangeLog($ID);
/*
* Determine requested revision(s)
*/
// we're trying to be clever here, revisions to compare can be either
// given as rev and rev2 parameters, with rev2 being optional. Or in an
// array in rev2.
$rev1 = $REV;
$rev2 = $INPUT->ref('rev2');
if (is_array($rev2)) {
$rev1 = (int) $rev2[0];
$rev2 = (int) $rev2[1];
if (!$rev1) {
$rev1 = $rev2;
unset($rev2);
}
} else {
$rev2 = $INPUT->int('rev2');
// difflink icon click, eg. ?rev=123456789&do=diff
if ($INPUT->has('rev')) {
$this->oldRev = $INPUT->int('rev');
$this->newRev = $this->changelog->currentRevision();
}
/*
* Determine left and right revision, its texts and the header
*/
$r_minor = '';
$l_minor = '';
if ($this->text) { // compare text to the most current revision
$l_rev = '';
$l_text = rawWiki($ID, '');
$l_head = '<a class="wikilink1" href="'. wl($ID) .'">'
. $ID .' '. dformat((int) @filemtime(wikiFN($ID))) .'</a> '
. $lang['current'];
$r_rev = '';
$r_text = cleanText($this->text);
$r_head = $lang['yours'];
} else {
if ($rev1 && isset($rev2) && $rev2) { // two specific revisions wanted
// make sure order is correct (older on the left)
if ($rev1 < $rev2) {
$l_rev = $rev1;
$r_rev = $rev2;
} else {
$l_rev = $rev2;
$r_rev = $rev1;
}
} elseif ($rev1) { // single revision given, compare to current
$r_rev = '';
$l_rev = $rev1;
} else { // no revision was given, compare previous to current
$r_rev = '';
$revs = $pagelog->getRevisions(0, 1);
$l_rev = $revs[0];
$REV = $l_rev; // store revision back in $REV
}
// when both revisions are empty then the page was created just now
if (!$l_rev && !$r_rev) {
$l_text = '';
// submit button with two checked boxes
$rev2 = $INPUT->arr('rev2', []);
if (count($rev2) > 1) {
if ($rev2[0] < $rev2[1]) {
[$this->oldRev, $this->newRev] = [$rev2[0], $rev2[1]];
} else {
$l_text = rawWiki($ID, $l_rev);
[$this->oldRev, $this->newRev] = [$rev2[1], $rev2[0]];
}
$r_text = rawWiki($ID, $r_rev);
list($l_head, $r_head, $l_minor, $r_minor) = $this->diffHead(
$l_rev, $r_rev, null, false, ($this->difftype == 'inline')
);
}
/*
* Build navigation
*/
$l_nav = '';
$r_nav = '';
if (!$this->text) {
list($l_nav, $r_nav) = $this->diffNavigation($pagelog, $l_rev, $r_rev);
if (!isset($this->oldRev, $this->newRev)) {
// no revision was given, compare previous to current
$revs = $this->changelog->getRevisions(-1, 2);
$this->newRev = $this->changelog->currentRevision();
$this->oldRev = ($revs[0] == $this->newRev) ? $revs[1] : $revs[0];
}
/*
* Create diff object and the formatter
*/
$diff = new \Diff(explode("\n", $l_text), explode("\n", $r_text));
if ($this->difftype == 'inline') {
$diffformatter = new \InlineDiffFormatter();
} else {
$diffformatter = new \TableDiffFormatter();
}
/*
* Display intro
*/
if ($this->showIntro) print p_locale_xhtml('diff');
/*
* Display type and exact reference
*/
if (!$this->text) {
print '<div class="diffoptions group">';
// create the form to select difftype
$form = new Form(['action' => wl()]);
$form->setHiddenField('id', $ID);
$form->setHiddenField('rev2[0]', $l_rev);
$form->setHiddenField('rev2[1]', $r_rev);
$form->setHiddenField('do', 'diff');
$options = array(
'sidebyside' => $lang['diff_side'],
'inline' => $lang['diff_inline']
);
$input = $form->addDropdown('difftype', $options, $lang['diff_type'])
->val($this->difftype)->addClass('quickselect');
$input->useInput(false); // inhibit prefillInput() during toHTML() process
$form->addButton('do[diff]', 'Go')->attr('type','submit');
print $form->toHTML();
print '<p>';
// link to exactly this view FS#2835
print $this->diffViewlink('difflink', $l_rev, ($r_rev ?: $INFO['currentrev']));
print '</p>';
print '</div>'; // .diffoptions
}
/*
* Display diff view table
*/
print '<div class="table">';
print '<table class="diff diff_'. $this->difftype .'">';
//navigation and header
if ($this->difftype == 'inline') {
if (!$this->text) {
print '<tr>'
. '<td class="diff-lineheader">-</td>'
. '<td class="diffnav">'. $l_nav .'</td>'
. '</tr>';
print '<tr>'
. '<th class="diff-lineheader">-</th>'
. '<th '. $l_minor .'>'. $l_head .'</th>'
.'</tr>';
}
print '<tr>'
. '<td class="diff-lineheader">+</td>'
. '<td class="diffnav">'. $r_nav .'</td>'
.'</tr>';
print '<tr>'
. '<th class="diff-lineheader">+</th>'
. '<th '. $r_minor .'>'. $r_head .'</th>'
. '</tr>';
} else {
if (!$this->text) {
print '<tr>'
. '<td colspan="2" class="diffnav">'. $l_nav .'</td>'
. '<td colspan="2" class="diffnav">'. $r_nav .'</td>'
. '</tr>';
}
print '<tr>'
. '<th colspan="2" '. $l_minor .'>'. $l_head .'</th>'
. '<th colspan="2" '. $r_minor .'>'. $r_head .'</th>'
. '</tr>';
}
//diff view
print $this->insertSoftbreaks($diffformatter->format($diff));
print '</table>';
print '</div>';
}
/**
* Get header of diff HTML
* Build header of diff HTML
*
* @param string $l_rev Left revisions
* @param string $r_rev Right revision
* @param string $id Page id, if null $ID is used
* @param bool $media If it is for media files
* @param bool $inline Return the header on a single line
* @return string[] HTML snippets for diff header
* @deprecated 2020-12-31
*/
public function diffHead($l_rev, $r_rev, $id = null, $media = false, $inline = false)
public function buildDiffHead($l_rev, $r_rev)
{
global $lang;
if ($id === null) {
global $ID;
$id = $ID;
}
$head_separator = $inline ? ' ' : '<br />';
$media_or_wikiFN = $media ? 'mediaFN' : 'wikiFN';
$ml_or_wl = $media ? 'ml' : 'wl';
$l_minor = $r_minor = '';
if ($media) {
$changelog = new MediaChangeLog($id);
} else {
$changelog = new PageChangeLog($id);
}
if (!$l_rev) {
$l_head = '&mdash;';
} else {
$l_info = $changelog->getRevisionInfo($l_rev);
if ($l_info['user']) {
$l_user = '<bdi>'.editorinfo($l_info['user']).'</bdi>';
if (auth_ismanager()) $l_user .= ' <bdo dir="ltr">('.$l_info['ip'].')</bdo>';
} else {
$l_user = '<bdo dir="ltr">'.$l_info['ip'].'</bdo>';
}
$l_user = '<span class="user">'.$l_user.'</span>';
$l_sum = ($l_info['sum']) ? '<span class="sum"><bdi>'.hsc($l_info['sum']).'</bdi></span>' : '';
if ($l_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $l_minor = 'class="minor"';
$l_head_title = ($media) ? dformat($l_rev) : $id.' ['.dformat($l_rev).']';
$l_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$l_rev").'">'
. $l_head_title.'</a></bdi>'.$head_separator.$l_user.' '.$l_sum;
}
if ($r_rev) {
$r_info = $changelog->getRevisionInfo($r_rev);
if ($r_info['user']) {
$r_user = '<bdi>'.editorinfo($r_info['user']).'</bdi>';
if (auth_ismanager()) $r_user .= ' <bdo dir="ltr">('.$r_info['ip'].')</bdo>';
} else {
$r_user = '<bdo dir="ltr">'.$r_info['ip'].'</bdo>';
}
$r_user = '<span class="user">'.$r_user.'</span>';
$r_sum = ($r_info['sum']) ? '<span class="sum"><bdi>'.hsc($r_info['sum']).'</bdi></span>' : '';
if ($r_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"';
$r_head_title = ($media) ? dformat($r_rev) : $id.' ['.dformat($r_rev).']';
$r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$r_rev").'">'
. $r_head_title.'</a></bdi>'.$head_separator.$r_user.' '.$r_sum;
} elseif ($_rev = @filemtime($media_or_wikiFN($id))) {
$_info = $changelog->getRevisionInfo($_rev);
if ($_info['user']) {
$_user = '<bdi>'.editorinfo($_info['user']).'</bdi>';
if (auth_ismanager()) $_user .= ' <bdo dir="ltr">('.$_info['ip'].')</bdo>';
} else {
$_user = '<bdo dir="ltr">'.$_info['ip'].'</bdo>';
}
$_user = '<span class="user">'.$_user.'</span>';
$_sum = ($_info['sum']) ? '<span class="sum"><bdi>'.hsc($_info['sum']).'</span></bdi>' : '';
if ($_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"';
$r_head_title = ($media) ? dformat($_rev) : $id.' ['.dformat($_rev).']';
$r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id).'">'
. $r_head_title.'</a></bdi> '.'('.$lang['current'].')'.$head_separator.$_user.' '.$_sum;
}else{
$r_head = '&mdash; ('.$lang['current'].')';
}
return array($l_head, $r_head, $l_minor, $r_minor);
}
/**
* Create html for revision navigation
*
* @param PageChangeLog $pagelog changelog object of current page
* @param int $l_rev left revision timestamp
* @param int $r_rev right revision timestamp
* @return string[] html of left and right navigation elements
*/
protected function diffNavigation($pagelog, $l_rev, $r_rev)
{
global $INFO, $ID;
// last timestamp is not in changelog, retrieve timestamp from metadata
// note: when page is removed, the metadata timestamp is zero
if (!$r_rev) {
if (isset($INFO['meta']['last_change']['date'])) {
$r_rev = $INFO['meta']['last_change']['date'];
} else {
$r_rev = 0;
}
}
//retrieve revisions with additional info
list($l_revs, $r_revs) = $pagelog->getRevisionsAround($l_rev, $r_rev);
$l_revisions = array();
if (!$l_rev) {
//no left revision given, add dummy
$l_revisions[0]= array('label' => '', 'attrs' => []);
}
foreach ($l_revs as $rev) {
$info = $pagelog->getRevisionInfo($rev);
$l_revisions[$rev] = array(
'label' => dformat($info['date']) .' '. editorinfo($info['user'], true) .' '. $info['sum'],
'attrs' => ['title' => $rev],
);
if ($r_rev ? $rev >= $r_rev : false) $l_revisions[$rev]['attrs']['disabled'] = 'disabled';
}
$r_revisions = array();
if (!$r_rev) {
//no right revision given, add dummy
$r_revisions[0] = array('label' => '', 'attrs' => []);
}
foreach ($r_revs as $rev) {
$info = $pagelog->getRevisionInfo($rev);
$r_revisions[$rev] = array(
'label' => dformat($info['date']) .' '. editorinfo($info['user'], true) .' '. $info['sum'],
'attrs' => ['title' => $rev],
);
if ($rev <= $l_rev) $r_revisions[$rev]['attrs']['disabled'] = 'disabled';
}
//determine previous/next revisions
$l_index = array_search($l_rev, $l_revs);
$l_prev = $l_index < count($l_revs) - 1 ? $l_revs[$l_index + 1] : null;
$l_next = $l_index > 1 ? $l_revs[$l_index - 1] : null;
if ($r_rev) {
$r_index = array_search($r_rev, $r_revs);
$r_prev = $r_index < count($r_revs) - 1 ? $r_revs[$r_index + 1] : null;
$r_next = $r_index > 1 ? $r_revs[$r_index - 1] : null;
} else {
//removed page
if ($l_next) {
$r_prev = $r_revs[0];
} else {
$r_prev = null;
}
$r_next = null;
}
/*
* Left side:
*/
$l_nav = '';
//move back
if ($l_prev) {
$l_nav .= $this->diffViewlink('diffbothprevrev', $l_prev, $r_prev);
$l_nav .= $this->diffViewlink('diffprevrev', $l_prev, $r_rev);
}
//dropdown
$form = new Form(['action' => wl()]);
$form->setHiddenField('id', $ID);
$form->setHiddenField('difftype', $this->difftype);
$form->setHiddenField('rev2[1]', $r_rev);
$form->setHiddenField('do', 'diff');
$input = $form->addDropdown('rev2[0]', $l_revisions)->val($l_rev)->addClass('quickselect');
$input->useInput(false); // inhibit prefillInput() during toHTML() process
$form->addButton('do[diff]', 'Go')->attr('type','submit');
$l_nav .= $form->toHTML();
//move forward
if ($l_next && ($l_next < $r_rev || !$r_rev)) {
$l_nav .= $this->diffViewlink('diffnextrev', $l_next, $r_rev);
}
/*
* Right side:
*/
$r_nav = '';
//move back
if ($l_rev < $r_prev) {
$r_nav .= $this->diffViewlink('diffprevrev', $l_rev, $r_prev);
}
//dropdown
$form = new Form(['action' => wl()]);
$form->setHiddenField('id', $ID);
$form->setHiddenField('rev2[0]', $l_rev);
$form->setHiddenField('difftype', $this->difftype);
$form->setHiddenField('do', 'diff');
$input = $form->addDropdown('rev2[1]', $r_revisions)->val($r_rev)->addClass('quickselect');
$input->useInput(false); // inhibit prefillInput() during toHTML() process
$form->addButton('do[diff]', 'Go')->attr('type','submit');
$r_nav .= $form->toHTML();
//move forward
if ($r_next) {
if ($pagelog->isCurrentRevision($r_next)) {
//last revision is diff with current page
$r_nav .= $this->diffViewlink('difflastrev', $l_rev);
} else {
$r_nav .= $this->diffViewlink('diffnextrev', $l_rev, $r_next);
}
} else {
$r_nav .= $this->diffViewlink('diffbothnextrev', $l_next, $r_next);
}
return array($l_nav, $r_nav);
}
/**
* Create html link to a diff view defined by two revisions
*
* @param string $linktype
* @param int $lrev oldest revision
* @param int $rrev newest revision or null for diff with current revision
* @return string html of link to a diff view
*/
protected function diffViewlink($linktype, $lrev, $rrev = null)
{
global $ID, $lang;
if ($rrev === null) {
$urlparam = array(
'do' => 'diff',
'rev' => $lrev,
'difftype' => $this->difftype,
);
} else {
$urlparam = array(
'do' => 'diff',
'rev2[0]' => $lrev,
'rev2[1]' => $rrev,
'difftype' => $this->difftype,
);
}
return '<a class="'. $linktype .'" href="'. wl($ID, $urlparam) .'" title="'. $lang[$linktype] .'">'
. '<span>'. $lang[$linktype] .'</span>'
. '</a>';
}
/**
* Insert soft breaks in diff html
*
* @param string $diffhtml
* @return string
*/
public function insertSoftbreaks($diffhtml)
{
// search the diff html string for both:
// - html tags, so these can be ignored
// - long strings of characters without breaking characters
return preg_replace_callback('/<[^>]*>|[^<> ]{12,}/', function ($match) {
// if match is an html tag, return it intact
if ($match[0][0] == '<') return $match[0];
// its a long string without a breaking character,
// make certain characters into breaking characters by inserting a
// word break opportunity (<wbr> tag) in front of them.
$regex = <<< REGEX
(?(?= # start a conditional expression with a positive look ahead ...
&\#?\\w{1,6};) # ... for html entities - we don't want to split them (ok to catch some invalid combinations)
&\#?\\w{1,6}; # yes pattern - a quicker match for the html entity, since we know we have one
|
[?/,&\#;:] # no pattern - any other group of 'special' characters to insert a breaking character after
)+ # end conditional expression
REGEX;
return preg_replace('<'.$regex.'>xu', '\0<wbr>', $match[0]);
}, $diffhtml);
dbg_deprecated('not used see '. \dokuwiki\Ui\PageDiff::class .'::show()');
}
}

351
inc/Ui/MediaDiff.php Normal file
View File

@ -0,0 +1,351 @@
<?php
namespace dokuwiki\Ui;
use dokuwiki\ChangeLog\MediaChangeLog;
use dokuwiki\Ui\MediaRevisions;
use dokuwiki\Extension\Event;
use dokuwiki\Form\Form;
use JpegMeta;
/**
* DokuWiki MediaDiff Interface
*
* @package dokuwiki\Ui
*/
class MediaDiff extends Diff
{
/* @var MediaChangeLog */
protected $changelog;
/* @var array */
protected $oldRevInfo;
protected $newRevInfo;
/* @var bool */
protected $is_img;
/**
* MediaDiff Ui constructor
*
* @param string $id media id
*/
public function __construct($id)
{
if (!isset($id)) {
throw new \InvalidArgumentException('media id should not be empty!');
}
// init preference
$this->preference['fromAjax'] = false; // see dokuwiki\Ajax::callMediadiff()
$this->preference['showIntro'] = false;
$this->preference['difftype'] = 'both'; // diff view type: both, opacity or portions
parent::__construct($id);
}
/** @inheritdoc */
protected function setChangeLog()
{
$this->changelog = new MediaChangeLog($this->id);
}
/**
* Handle requested revision(s) and diff view preferences
*
* @return void
*/
protected function handle()
{
global $INPUT;
// requested rev or rev2
parent::handle();
// requested diff view type
if ($INPUT->has('difftype')) {
$this->preference['difftype'] = $INPUT->str('difftype');
}
}
/**
* Prepare revision info of comparison pair
*/
protected function preProcess()
{
$changelog =& $this->changelog;
// revision info of older file (left side)
$this->oldRevInfo = $changelog->getRevisionInfo($this->oldRev);
// revision info of newer file (right side)
$this->newRevInfo = $changelog->getRevisionInfo($this->newRev);
$this->is_img = preg_match('/\.(jpe?g|gif|png)$/', $this->id);
foreach ([&$this->oldRevInfo, &$this->newRevInfo] as &$revInfo) {
// use timestamp and '' properly as $rev for the current file
$isCurrent = $changelog->isCurrentRevision($revInfo['date']);
$revInfo += [
'current' => $isCurrent,
'rev' => $isCurrent ? '' : $revInfo['date'],
];
// headline in the Diff view navigation
$revInfo['navTitle'] = $this->revisionTitle($revInfo);
if ($this->is_img) {
$rev = $revInfo['rev'];
$meta = new JpegMeta(mediaFN($this->id, $rev));
// get image width and height for the mediamanager preview panel
$revInfo['previewSize'] = media_image_preview_size($this->id, $rev, $meta);
}
}
unset($revInfo);
// re-check image, ensure minimum image width for showImageDiff()
$this->is_img = ($this->is_img
&& ($this->oldRevInfo['previewSize'][0] ?? 0) >= 30
&& ($this->newRevInfo['previewSize'][0] ?? 0) >= 30
);
// adjust requested diff view type
if (!$this->is_img) {
$this->preference['difftype'] = 'both';
}
}
/**
* Shows difference between two revisions of media
*
* @author Kate Arzamastseva <pshns@ukr.net>
*/
public function show()
{
global $conf;
$ns = getNS($this->id);
$auth = auth_quickaclcheck("$ns:*");
if ($auth < AUTH_READ || !$this->id || !$conf['mediarevisions']) return '';
// retrieve form parameters: rev, rev2, difftype
$this->handle();
// prepare revision info of comparison pair
$this->preProcess();
// display intro
if ($this->preference['showIntro']) echo p_locale_xhtml('diff');
// print form to choose diff view type
if ($this->is_img && !$this->preference['fromAjax']) {
$this->showDiffViewSelector();
echo '<div id="mediamanager__diff" >';
}
switch ($this->preference['difftype']) {
case 'opacity':
case 'portions':
$this->showImageDiff();
break;
case 'both':
default:
$this->showFileDiff();
break;
}
if ($this->is_img && !$this->preference['fromAjax']) {
echo '</div>';
}
}
/**
* Print form to choose diff view type
* the dropdown is to be added through JavaScript, see lib/scripts/media.js
*/
protected function showDiffViewSelector()
{
// use timestamp for current revision
[$oldRev, $newRev] = [(int)$this->oldRevInfo['date'], (int)$this->newRevInfo['date']];
echo '<div class="diffoptions group">';
$form = new Form([
'id' => 'mediamanager__form_diffview',
'action' => media_managerURL([], '&'),
'method' => 'get',
'class' => 'diffView',
]);
$form->addTagOpen('div')->addClass('no');
$form->setHiddenField('sectok', null);
$form->setHiddenField('mediado', 'diff');
$form->setHiddenField('rev2[0]', $oldRev);
$form->setHiddenField('rev2[1]', $newRev);
$form->addTagClose('div');
echo $form->toHTML();
echo '</div>'; // .diffoptions
}
/**
* Prints two images side by side
* and slider
*
* @author Kate Arzamastseva <pshns@ukr.net>
*/
protected function showImageDiff()
{
// diff view type: opacity or portions
$type = $this->preference['difftype'];
// use '' for current revision
[$oldRev, $newRev] = [$this->oldRevInfo['rev'], $this->newRevInfo['rev']];
// adjust image width, right side (newer) has priority
$oldRevSize = $this->oldRevInfo['previewSize'];
$newRevSize = $this->newRevInfo['previewSize'];
if ($oldRevSize != $newRevSize) {
if ($newRevSize[0] > $oldRevSize[0]) {
$oldRevSize = $newRevSize;
}
}
$oldRevSrc = ml($this->id, ['rev' => $oldRev, 'h' => $oldRevSize[1], 'w' => $oldRevSize[0]]);
$newRevSrc = ml($this->id, ['rev' => $newRev, 'h' => $oldRevSize[1], 'w' => $oldRevSize[0]]);
// slider
echo '<div class="slider" style="max-width: '.($oldRevSize[0]-20).'px;" ></div>';
// two images in divs
echo '<div class="imageDiff '.$type.'">';
echo '<div class="image1" style="max-width: '.$oldRevSize[0].'px;">';
echo '<img src="'.$oldRevSrc.'" alt="" />';
echo '</div>';
echo '<div class="image2" style="max-width: '.$oldRevSize[0].'px;">';
echo '<img src="'.$newRevSrc.'" alt="" />';
echo '</div>';
echo '</div>';
}
/**
* Shows difference between two revisions of media file
*
* @author Kate Arzamastseva <pshns@ukr.net>
*/
protected function showFileDiff()
{
global $lang;
$changelog =& $this->changelog;
$ns = getNS($this->id);
$auth = auth_quickaclcheck("$ns:*");
// use '' for current revision
[$oldRev, $newRev] = [$this->oldRevInfo['rev'], $this->newRevInfo['rev']];
$oldRevMeta = new JpegMeta(mediaFN($this->id, $oldRev));
$newRevMeta = new JpegMeta(mediaFN($this->id, $newRev));
// display diff view table
echo '<div class="table">';
echo '<table>';
echo '<tr>';
echo '<th>'. $this->oldRevInfo['navTitle'] .'</th>';
echo '<th>'. $this->newRevInfo['navTitle'] .'</th>';
echo '</tr>';
echo '<tr class="image">';
echo '<td>';
media_preview($this->id, $auth, $oldRev, $oldRevMeta); // $auth not used in media_preview()?
echo '</td>';
echo '<td>';
media_preview($this->id, $auth, $newRev, $newRevMeta);
echo '</td>';
echo '</tr>';
echo '<tr class="actions">';
echo '<td>';
media_preview_buttons($this->id, $auth, $oldRev); // $auth used in media_preview_buttons()
echo '</td>';
echo '<td>';
media_preview_buttons($this->id, $auth, $newRev);
echo '</td>';
echo '</tr>';
$l_tags = media_file_tags($oldRevMeta);
$r_tags = media_file_tags($newRevMeta);
// FIXME r_tags-only stuff
foreach ($l_tags as $key => $l_tag) {
if ($l_tag['value'] != $r_tags[$key]['value']) {
$r_tags[$key]['highlighted'] = true;
$l_tags[$key]['highlighted'] = true;
} elseif (!$l_tag['value'] || !$r_tags[$key]['value']) {
unset($r_tags[$key]);
unset($l_tags[$key]);
}
}
echo '<tr>';
foreach (array($l_tags, $r_tags) as $tags) {
echo '<td>';
echo '<dl class="img_tags">';
foreach ($tags as $tag) {
$value = cleanText($tag['value']);
if (!$value) $value = '-';
echo '<dt>'.$lang[$tag['tag'][1]].'</dt>';
echo '<dd>';
if ($tag['highlighted']) echo '<strong>';
if ($tag['tag'][2] == 'date') {
echo dformat($value);
} else {
echo hsc($value);
}
if ($tag['highlighted']) echo '</strong>';
echo '</dd>';
}
echo '</dl>';
echo '</td>';
}
echo '</tr>';
echo '</table>';
echo '</div>';
}
/**
* Revision Title for MediaDiff table headline
*
* @param array $info Revision info structure of a media file
* @return string
*/
protected function revisionTitle(array $info)
{
global $lang, $INFO;
if (isset($info['date'])) {
$rev = $info['date'];
$title = '<bdi><a class="wikilink1" href="'.ml($this->id, ['rev' => $rev]).'">'
. dformat($rev).'</a></bdi>';
} else {
$rev = false;
$title = '&mdash;';
}
if (isset($info['current']) || ($rev && $rev == $INFO['currentrev'])) {
$title .= '&nbsp;('.$lang['current'].')';
}
// append separator
$title .= ($this->preference['difftype'] === 'inline') ? ' ' : '<br />';
// supplement
if (isset($info['date'])) {
$objRevInfo = (new MediaRevisions($this->id))->getObjRevInfo($info);
$title .= $objRevInfo->editSummary().' '.$objRevInfo->editor();
}
return $title;
}
}

118
inc/Ui/MediaRevisions.php Normal file
View File

@ -0,0 +1,118 @@
<?php
namespace dokuwiki\Ui;
use dokuwiki\ChangeLog\MediaChangeLog;
use dokuwiki\Form\Form;
/**
* DokuWiki MediaRevisions Interface
*
* @package dokuwiki\Ui
*/
class MediaRevisions extends Revisions
{
/* @var MediaChangeLog */
protected $changelog;
/**
* MediaRevisions Ui constructor
*
* @param string $id id of media
*/
public function __construct($id)
{
if (!$id) {
throw new \InvalidArgumentException('media id should not be empty!');
}
parent::__construct($id);
}
/** @inheritdoc */
protected function setChangeLog()
{
$this->changelog = new MediaChangeLog($this->id);
}
/**
* Display a list of Media Revisions in the MediaManager
*
* @author Andreas Gohr <andi@splitbrain.org>
* @author Ben Coburn <btcoburn@silicodon.net>
* @author Kate Arzamastseva <pshns@ukr.net>
* @author Satoshi Sahara <sahara.satoshi@gmail.com>
*
* @param int $first skip the first n changelog lines
* @return void
*/
public function show($first = 0)
{
global $lang;
$changelog =& $this->changelog;
// get revisions, and set correct pagenation parameters (first, hasNext)
if ($first === null) $first = 0;
$hasNext = false;
$revisions = $this->getRevisions($first, $hasNext);
// create the form
$form = new Form([
'id' => 'page__revisions', // must not be "media__revisions"
'action' => media_managerURL(['image' => $this->id], '&'),
'class' => 'changes',
]);
$form->setHiddenField('mediado', 'diff'); // required for media revisions
$form->addTagOpen('div')->addClass('no');
// start listing
$form->addTagOpen('ul');
foreach ($revisions as $info) {
$rev = $info['date'];
$info['current'] = $changelog->isCurrentRevision($rev);
$class = ($info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) ? 'minor' : '';
$form->addTagOpen('li')->addClass($class);
$form->addTagOpen('div')->addClass('li');
if (isset($info['current'])) {
$form->addCheckbox('rev2[]')->val($rev);
} elseif (file_exists(mediaFN($this->id, $rev))) {
$form->addCheckbox('rev2[]')->val($rev);
} else {
$form->addCheckbox('')->val($rev)->attr('disabled','disabled');
}
$form->addHTML(' ');
$objRevInfo = $this->getObjRevInfo($info);
$html = implode(' ', [
$objRevInfo->editDate(), // edit date and time
$objRevInfo->difflink(), // link to diffview icon
$objRevInfo->itemName(), // name of page or media
'<div>',
$objRevInfo->editSummary(), // edit summary
$objRevInfo->editor(), // editor info
$objRevInfo->sizechange(), // size change indicator
$objRevInfo->currentIndicator(), // current indicator (only when k=1)
'</div>',
]);
$form->addHTML($html);
$form->addTagClose('div');
$form->addTagClose('li');
}
$form->addTagClose('ul'); // end of revision list
// show button for diff view
$form->addButton('do[diff]', $lang['diff2'])->attr('type', 'submit');
$form->addTagClose('div'); // close div class=no
print $form->toHTML('Revisions');
// provide navigation for pagenated revision list (of pages and/or media files)
print $this->navigation($first, $hasNext, function ($n) {
return media_managerURL(['first' => $n], '&', false, true);
});
}
}

View File

@ -35,7 +35,7 @@ class PageConflict extends Ui
*/
public function show()
{
global $ID;
global $INFO;
global $lang;
// print intro
@ -44,7 +44,7 @@ class PageConflict extends Ui
// create the form
$form = new Form(['id' => 'dw__editform']);
$form->addTagOpen('div')->addClass('no');
$form->setHiddenField('id', $ID);
$form->setHiddenField('id', $INFO['id']);
$form->setHiddenField('wikitext', $this->text);
$form->setHiddenField('summary', $this->summary);
@ -56,7 +56,8 @@ class PageConflict extends Ui
print '<br /><br /><br /><br />';
(new Diff($this->text, false))->show();
// print difference
(new PageDiff($INFO['id']))->compareWith($this->text)->preference('showIntro', false)->show();
}
}

554
inc/Ui/PageDiff.php Normal file
View File

@ -0,0 +1,554 @@
<?php
namespace dokuwiki\Ui;
use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\Form\Form;
/**
* DokuWiki PageDiff Interface
*
* @author Andreas Gohr <andi@splitbrain.org>
* @author Satoshi Sahara <sahara.satoshi@gmail.com>
* @package dokuwiki\Ui
*/
class PageDiff extends Diff
{
/* @var PageChangeLog */
protected $changelog;
/* @var array */
protected $oldRevInfo;
protected $newRevInfo;
/* @var string */
protected $text;
/**
* PageDiff Ui constructor
*
* @param string $id page id
*/
public function __construct($id = null)
{
global $INFO;
if (!isset($id)) $id = $INFO['id'];
// init preference
$this->preference['showIntro'] = true;
$this->preference['difftype'] = 'sidebyside'; // diff view type: inline or sidebyside
parent::__construct($id);
}
/** @inheritdoc */
protected function setChangeLog()
{
$this->changelog = new PageChangeLog($this->id);
}
/**
* Set text to be compared with most current version
* when it has been externally edited
* exclusively use of the compare($old, $new) method
*
* @param string $text
* @return $this
*/
public function compareWith($text = null)
{
global $lang;
if (isset($text)) {
$this->text = $text;
$changelog =& $this->changelog;
// revision info of older file (left side)
$this->oldRevInfo = $changelog->getCurrentRevisionInfo() + [
'current' => true,
'rev' => '',
'navTitle' => $this->revisionTitle($changelog->getCurrentRevisionInfo()),
'text' => rawWiki($this->id, ''),
];
// revision info of newer file (right side)
$this->newRevInfo = [
'date' => null,
//'ip' => '127.0.0.1',
//'type' => DOKU_CHANGE_TYPE_CREATE,
'id' => $this->id,
//'user' => '',
//'sum' => '',
//'extra' => '',
'sizechange' => strlen($this->text) - io_getSizeFile(wikiFN($this->id, '')),
'timestamp' => false,
'current' => false,
'rev' => false,
'navTitle' => $lang['yours'],
'text' => cleanText($this->text),
];
}
return $this;
}
/**
* Handle requested revision(s) and diff view preferences
*
* @return void
*/
protected function handle()
{
global $INPUT;
// requested rev or rev2
if (!isset($this->oldRevInfo, $this->newRevInfo)) {
parent::handle();
}
// requested diff view type
if ($INPUT->has('difftype')) {
$this->preference['difftype'] = $INPUT->str('difftype');
} else {
// read preference from DokuWiki cookie. PageDiff only
$mode = get_doku_pref('difftype', $mode = null);
if (isset($mode)) $this->preference['difftype'] = $mode;
}
if (!$INPUT->has('rev') && !$INPUT->has('rev2')) {
global $INFO, $REV;
if ($this->id == $INFO['id'])
$REV = $this->oldRev; // store revision back in $REV
}
}
/**
* Prepare revision info of comparison pair
*/
protected function preProcess()
{
$changelog =& $this->changelog;
// revision info of older file (left side)
$this->oldRevInfo = $changelog->getRevisionInfo($this->oldRev);
// revision info of newer file (right side)
$this->newRevInfo = $changelog->getRevisionInfo($this->newRev);
foreach ([&$this->oldRevInfo, &$this->newRevInfo] as &$revInfo) {
// use timestamp and '' properly as $rev for the current file
$isCurrent = $changelog->isCurrentRevision($revInfo['date']);
$revInfo += [
'current' => $isCurrent,
'rev' => $isCurrent ? '' : $revInfo['date'],
];
// headline in the Diff view navigation
$revInfo['navTitle'] = $this->revisionTitle($revInfo);
if ($revInfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
//attic stores complete last page version for a deleted page
$revInfo['text'] = '';
} else {
$revInfo['text'] = rawWiki($this->id, $revInfo['rev']);
}
}
}
/**
* Show diff
* between current page version and provided $text
* or between the revisions provided via GET or POST
*
* @author Andreas Gohr <andi@splitbrain.org>
*
* @return void
*/
public function show()
{
$changelog =& $this->changelog;
if (!isset($this->oldRevInfo, $this->newRevInfo)) {
// retrieve form parameters: rev, rev2, difftype
$this->handle();
// prepare revision info of comparison pair, except PageConfrict or PageDraft
$this->preProcess();
}
// create difference engine object
$Difference = new \Diff(
explode("\n", $this->oldRevInfo['text']),
explode("\n", $this->newRevInfo['text'])
);
// build paired navigation
[$navOlderRevisions, $navNewerRevisions] = $this->buildRevisionsNavigation();
// display intro
if ($this->preference['showIntro']) echo p_locale_xhtml('diff');
// print form to choose diff view type, and exact url reference to the view
if ($this->newRevInfo['rev'] !== false) {
$this->showDiffViewSelector();
}
// assign minor edit checker to the variable
$classEditType = function ($info) {
return ($info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) ? ' class="minor"' : '';
};
// display diff view table
echo '<div class="table">';
echo '<table class="diff diff_'.$this->preference['difftype'] .'">';
//navigation and header
switch ($this->preference['difftype']) {
case 'inline':
if ($this->newRevInfo['rev'] !== false) {
echo '<tr>'
.'<td class="diff-lineheader">-</td>'
.'<td class="diffnav">'. $navOlderRevisions .'</td>'
.'</tr>';
echo '<tr>'
.'<th class="diff-lineheader">-</th>'
.'<th'.$classEditType($this->oldRevInfo).'>'.$this->oldRevInfo['navTitle'].'</th>'
.'</tr>';
}
echo '<tr>'
.'<td class="diff-lineheader">+</td>'
.'<td class="diffnav">'. $navNewerRevisions .'</td>'
.'</tr>';
echo '<tr>'
.'<th class="diff-lineheader">+</th>'
.'<th'.$classEditType($this->newRevInfo).'>'.$this->newRevInfo['navTitle'].'</th>'
.'</tr>';
// create formatter object
$DiffFormatter = new \InlineDiffFormatter();
break;
case 'sidebyside':
default:
if ($this->newRevInfo['rev'] !== false) {
echo '<tr>'
.'<td colspan="2" class="diffnav">'. $navOlderRevisions .'</td>'
.'<td colspan="2" class="diffnav">'. $navNewerRevisions .'</td>'
.'</tr>';
}
echo '<tr>'
.'<th colspan="2"'.$classEditType($this->oldRevInfo).'>'.$this->oldRevInfo['navTitle'].'</th>'
.'<th colspan="2"'.$classEditType($this->newRevInfo).'>'.$this->newRevInfo['navTitle'].'</th>'
.'</tr>';
// create formatter object
$DiffFormatter = new \TableDiffFormatter();
break;
}
// output formatted difference
echo $this->insertSoftbreaks($DiffFormatter->format($Difference));
echo '</table>';
echo '</div>';
}
/**
* Revision Title for PageDiff table headline
*
* @param array $info Revision info structure of a page
* @return string
*/
protected function revisionTitle(array $info)
{
global $lang;
// use designated title when compare current page source with given text
if (array_key_exists('date', $info) && is_null($info['date'])) {
return $lang['yours'];
}
// revision info may have timestamp key when external edits occurred
$info['timestamp'] = $info['timestamp'] ?? true;
if (isset($info['date'])) {
$rev = $info['date'];
if ($info['timestamp'] === false) {
// exteranlly deleted or older file restored
$title = '<bdi><a class="wikilink2" href="'.wl($this->id).'">'
. $this->id .' ['. $lang['unknowndate'] .']'.'</a></bdi>';
} else {
$title = '<bdi><a class="wikilink1" href="'.wl($this->id, ['rev' => $rev]).'">'
. $this->id .' ['. dformat($rev) .']'.'</a></bdi>';
}
} else {
$rev = false;
$title = '&mdash;';
}
if ($info['current']) {
$title .= '&nbsp;('.$lang['current'].')';
}
// append separator
$title .= ($this->preference['difftype'] === 'inline') ? ' ' : '<br />';
// supplement
if (isset($info['date'])) {
$objRevInfo = (new PageRevisions($this->id))->getObjRevInfo($info);
$title .= $objRevInfo->editSummary().' '.$objRevInfo->editor();
}
return $title;
}
/**
* Print form to choose diff view type, and exact url reference to the view
*/
protected function showDiffViewSelector()
{
global $lang;
// use timestamp for current revision
[$oldRev, $newRev] = [(int)$this->oldRevInfo['date'], (int)$this->newRevInfo['date']];
echo '<div class="diffoptions group">';
// create the form to select difftype
$form = new Form(['action' => wl()]);
$form->setHiddenField('id', $this->id);
$form->setHiddenField('rev2[0]', $oldRev);
$form->setHiddenField('rev2[1]', $newRev);
$form->setHiddenField('do', 'diff');
$options = array(
'sidebyside' => $lang['diff_side'],
'inline' => $lang['diff_inline'],
);
$input = $form->addDropdown('difftype', $options, $lang['diff_type'])
->val($this->preference['difftype'])
->addClass('quickselect');
$input->useInput(false); // inhibit prefillInput() during toHTML() process
$form->addButton('do[diff]', 'Go')->attr('type','submit');
echo $form->toHTML();
// show exact url reference to the view when it is meaningful
echo '<p>';
if ($oldRev && $newRev) {
// link to exactly this view FS#2835
$viewUrl = $this->diffViewlink('difflink', $oldRev, $newRev);
}
echo $viewUrl ?? '<br />';
echo '</p>';
echo '</div>'; // .diffoptions
}
/**
* Create html for revision navigation
*
* The navigation consists of older and newer revisions selectors, each
* state mutually depends on the selected revision of opposite side.
*
* @return string[] html of navigation for both older and newer sides
*/
protected function buildRevisionsNavigation()
{
$changelog =& $this->changelog;
if ($this->newRevInfo['rev'] === false) {
// no revisions selector for PageConflict or PageDraft
return array('', '');
}
// use timestamp for current revision
[$oldRev, $newRev] = [(int)$this->oldRevInfo['date'], (int)$this->newRevInfo['date']];
// retrieve revisions with additional info
[$oldRevs, $newRevs] = $changelog->getRevisionsAround($oldRev, $newRev);
// build options for dropdown selector
$olderRevisions = $this->buildRevisionOptions('older', $oldRevs);
$newerRevisions = $this->buildRevisionOptions('newer', $newRevs);
// determine previous/next revisions
$index = array_search($oldRev, $oldRevs);
$oldPrevRev = ($index +1 < count($oldRevs)) ? $oldRevs[$index +1] : false;
$oldNextRev = ($index > 0) ? $oldRevs[$index -1] : false;
$index = array_search($newRev, $newRevs);
$newPrevRev = ($index +1 < count($newRevs)) ? $newRevs[$index +1] : false;
$newNextRev = ($index > 0) ? $newRevs[$index -1] : false;
/*
* navigation UI for older revisions / Left side:
*/
$navOlderRevs = '';
// move backward both side: ◀◀
if ($oldPrevRev && $newPrevRev)
$navOlderRevs .= $this->diffViewlink('diffbothprevrev', $oldPrevRev, $newPrevRev);
// move backward left side: ◀
if ($oldPrevRev)
$navOlderRevs .= $this->diffViewlink('diffprevrev', $oldPrevRev, $newRev);
// dropdown
$navOlderRevs .= $this->buildDropdownSelector('older', $olderRevisions);
// move forward left side: ▶
if ($oldNextRev && ($oldNextRev < $newRev))
$navOlderRevs .= $this->diffViewlink('diffnextrev', $oldNextRev, $newRev);
/*
* navigation UI for newer revisions / Right side:
*/
$navNewerRevs = '';
// move backward right side: ◀
if ($newPrevRev && ($oldRev < $newPrevRev))
$navNewerRevs .= $this->diffViewlink('diffprevrev', $oldRev, $newPrevRev);
// dropdown
$navNewerRevs .= $this->buildDropdownSelector('newer', $newerRevisions);
// move forward right side: ▶
if ($newNextRev) {
if ($changelog->isCurrentRevision($newNextRev)) {
$navNewerRevs .= $this->diffViewlink('difflastrev', $oldRev, $newNextRev);
} else {
$navNewerRevs .= $this->diffViewlink('diffnextrev', $oldRev, $newNextRev);
}
}
// move forward both side: ▶▶
if ($oldNextRev && $newNextRev)
$navNewerRevs .= $this->diffViewlink('diffbothnextrev', $oldNextRev, $newNextRev);
return array($navOlderRevs, $navNewerRevs);
}
/**
* prepare options for dropdwon selector
*
* @params string $side "older" or "newer"
* @params array $revs list of revsion
* @return array
*/
protected function buildRevisionOptions($side, $revs)
{
global $lang;
$changelog =& $this->changelog;
$revisions = array();
// use timestamp for current revision
[$oldRev, $newRev] = [(int)$this->oldRevInfo['date'], (int)$this->newRevInfo['date']];
foreach ($revs as $rev) {
$info = $changelog->getRevisionInfo($rev);
// revision info may have timestamp key when external edits occurred
$info['timestamp'] = $info['timestamp'] ?? true;
$date = dformat($info['date']);
if ($info['timestamp'] === false) {
// exteranlly deleted or older file restored
$date = preg_replace('/[0-9a-zA-Z]/','_', $date);
}
$revisions[$rev] = array(
'label' => implode(' ', [
$date,
editorinfo($info['user'], true),
$info['sum'],
]),
'attrs' => ['title' => $rev],
);
if (($side == 'older' && ($newRev && $rev >= $newRev))
||($side == 'newer' && ($rev <= $oldRev))
) {
$revisions[$rev]['attrs']['disabled'] = 'disabled';
}
}
return $revisions;
}
/**
* build Dropdown form for revisions navigation
*
* @params string $side "older" or "newer"
* @params array $options dropdown options
* @return string
*/
protected function buildDropdownSelector($side, $options)
{
$form = new Form(['action' => wl($this->id)]);
$form->setHiddenField('id', $this->id);
$form->setHiddenField('do', 'diff');
$form->setHiddenField('difftype', $this->preference['difftype']);
// use timestamp for current revision
[$oldRev, $newRev] = [(int)$this->oldRevInfo['date'], (int)$this->newRevInfo['date']];
switch ($side) {
case 'older': // left side
$form->setHiddenField('rev2[1]', $newRev);
$input = $form->addDropdown('rev2[0]', $options)
->val($oldRev)->addClass('quickselect');
$input->useInput(false); // inhibit prefillInput() during toHTML() process
break;
case 'newer': // right side
$form->setHiddenField('rev2[0]', $oldRev);
$input = $form->addDropdown('rev2[1]', $options)
->val($newRev)->addClass('quickselect');
$input->useInput(false); // inhibit prefillInput() during toHTML() process
break;
}
$form->addButton('do[diff]', 'Go')->attr('type','submit');
return $form->toHTML();
}
/**
* Create html link to a diff view defined by two revisions
*
* @param string $linktype
* @param int $oldRev older revision
* @param int $newRev newer revision or null for diff with current revision
* @return string html of link to a diff view
*/
protected function diffViewlink($linktype, $oldRev, $newRev = null)
{
global $lang;
if ($newRev === null) {
$urlparam = array(
'do' => 'diff',
'rev' => $oldRev,
'difftype' => $this->preference['difftype'],
);
} else {
$urlparam = array(
'do' => 'diff',
'rev2[0]' => $oldRev,
'rev2[1]' => $newRev,
'difftype' => $this->preference['difftype'],
);
}
$attr = array(
'class' => $linktype,
'href' => wl($this->id, $urlparam, true, '&'),
'title' => $lang[$linktype],
);
return '<a '. buildAttributes($attr) .'><span>'. $lang[$linktype] .'</span></a>';
}
/**
* Insert soft breaks in diff html
*
* @param string $diffhtml
* @return string
*/
public function insertSoftbreaks($diffhtml)
{
// search the diff html string for both:
// - html tags, so these can be ignored
// - long strings of characters without breaking characters
return preg_replace_callback('/<[^>]*>|[^<> ]{12,}/', function ($match) {
// if match is an html tag, return it intact
if ($match[0][0] == '<') return $match[0];
// its a long string without a breaking character,
// make certain characters into breaking characters by inserting a
// word break opportunity (<wbr> tag) in front of them.
$regex = <<< REGEX
(?(?= # start a conditional expression with a positive look ahead ...
&\#?\\w{1,6};) # ... for html entities - we don't want to split them (ok to catch some invalid combinations)
&\#?\\w{1,6}; # yes pattern - a quicker match for the html entity, since we know we have one
|
[?/,&\#;:] # no pattern - any other group of 'special' characters to insert a breaking character after
)+ # end conditional expression
REGEX;
return preg_replace('<'.$regex.'>xu', '\0<wbr>', $match[0]);
}, $diffhtml);
}
}

View File

@ -22,21 +22,21 @@ class PageDraft extends Ui
public function show()
{
global $INFO;
global $ID;
global $lang;
$draft = new \dokuwiki\Draft($ID, $INFO['client']);
$draft = new \dokuwiki\Draft($INFO['id'], $INFO['client']);
$text = $draft->getDraftText();
// print intro
print p_locale_xhtml('draft');
(new Diff($text, false))->show();
// print difference
(new PageDiff($INFO['id']))->compareWith($text)->preference('showIntro', false)->show();
// create the draft form
$form = new Form(['id' => 'dw__editform']);
$form->addTagOpen('div')->addClass('no');
$form->setHiddenField('id', $ID);
$form->setHiddenField('id', $INFO['id']);
$form->setHiddenField('date', $draft->getDraftDate());
$form->setHiddenField('wikitext', $text);

116
inc/Ui/PageRevisions.php Normal file
View File

@ -0,0 +1,116 @@
<?php
namespace dokuwiki\Ui;
use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\Form\Form;
/**
* DokuWiki PageRevisions Interface
*
* @package dokuwiki\Ui
*/
class PageRevisions extends Revisions
{
/* @var PageChangeLog */
protected $changelog;
/**
* PageRevisions Ui constructor
*
* @param string $id id of page
*/
public function __construct($id = null)
{
global $INFO;
if (!isset($id)) $id = $INFO['id'];
parent::__construct($id);
}
/** @inheritdoc */
protected function setChangeLog()
{
$this->changelog = new PageChangeLog($this->id);
}
/**
* Display list of old revisions of the page
*
* @author Andreas Gohr <andi@splitbrain.org>
* @author Ben Coburn <btcoburn@silicodon.net>
* @author Kate Arzamastseva <pshns@ukr.net>
* @author Satoshi Sahara <sahara.satoshi@gmail.com>
*
* @param int $first skip the first n changelog lines
* @return void
*/
public function show($first = 0)
{
global $lang, $REV;
$changelog =& $this->changelog;
// get revisions, and set correct pagenation parameters (first, hasNext)
if ($first === null) $first = 0;
$hasNext = false;
$revisions = $this->getRevisions($first, $hasNext);
// print intro
print p_locale_xhtml('revisions');
// create the form
$form = new Form([
'id' => 'page__revisions',
'class' => 'changes',
]);
$form->addTagOpen('div')->addClass('no');
// start listing
$form->addTagOpen('ul');
foreach ($revisions as $info) {
$rev = $info['date'];
$info['current'] = $changelog->isCurrentRevision($rev);
$class = ($info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) ? 'minor' : '';
$form->addTagOpen('li')->addClass($class);
$form->addTagOpen('div')->addClass('li');
if (isset($info['current'])) {
$form->addCheckbox('rev2[]')->val($rev);
} elseif ($rev == $REV) {
$form->addCheckbox('rev2[]')->val($rev)->attr('checked','checked');
} elseif (page_exists($this->id, $rev)) {
$form->addCheckbox('rev2[]')->val($rev);
} else {
$form->addCheckbox('')->val($rev)->attr('disabled','disabled');
}
$form->addHTML(' ');
$objRevInfo = $this->getObjRevInfo($info);
$html = implode(' ', [
$objRevInfo->editDate(), // edit date and time
$objRevInfo->difflink(), // link to diffview icon
$objRevInfo->itemName(), // name of page or media
$objRevInfo->editSummary(), // edit summary
$objRevInfo->editor(), // editor info
$objRevInfo->sizechange(), // size change indicator
$objRevInfo->currentIndicator(), // current indicator (only when k=1)
]);
$form->addHTML($html);
$form->addTagClose('div');
$form->addTagClose('li');
}
$form->addTagClose('ul'); // end of revision list
// show button for diff view
$form->addButton('do[diff]', $lang['diff2'])->attr('type', 'submit');
$form->addTagClose('div'); // close div class=no
print $form->toHTML('Revisions');
// provide navigation for paginated revision list (of pages and/or media files)
print $this->navigation($first, $hasNext, function ($n) {
return array('do' => 'revisions', 'first' => $n);
});
}
}

View File

@ -2,6 +2,7 @@
namespace dokuwiki\Ui;
use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\ChangeLog\MediaChangeLog;
use dokuwiki\Form\Form;
@ -72,6 +73,10 @@ class Recent extends Ui
// start listing of recent items
$form->addTagOpen('ul');
foreach ($recents as $recent) {
// check possible external edition for current page or media
$this->checkCurrentRevision($recent);
$recent['current'] = true;
$objRevInfo = $this->getObjRevInfo($recent);
$class = ($recent['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) ? 'minor': '';
$form->addTagOpen('li')->addClass($class);
@ -140,6 +145,31 @@ class Recent extends Ui
return $recents;
}
/**
* Check possible external deletion for current page or media
*
* To keep sort order in the recent list, we ignore externally modification.
* It is not possible to know when external deletion had happened,
* $info['date'] is to be incremented 1 second when such deletion detected.
*/
protected function checkCurrentRevision(array &$info)
{
$itemType = strrpos($info['id'], '.') ? 'media' : 'page';
if ($itemType == 'page') {
$changelog = new PageChangelog($info['id']);
} else {
$changelog = new MediaChangelog($info['id']);
}
if (!$changelog->isCurrentRevision($info['date'])) {
$currentRevInfo = $changelog->getCurrentRevisionInfo();
if ($currentRevInfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
// the page or media file was externally deleted
$info = array_merge($info, $currentRevInfo);
}
}
unset($changelog);
}
/**
* Navigation buttons for Pagenation (prev/next)
*
@ -209,6 +239,8 @@ class Recent extends Ui
public function __construct(array $info)
{
$info['item'] = strrpos($info['id'], '.') ? 'media' : 'page';
$info['current'] = $info['current'] ?? false;
$this->info = $info;
}
@ -216,10 +248,12 @@ class Recent extends Ui
public function itemIcon()
{
$id = $this->info['id'];
if (isset($this->info['media'])) {
$html = media_printicon($id);
} else {
$html = '<img class="icon" src="'.DOKU_BASE.'lib/images/fileicons/file.png" alt="'.$id.'" />';
switch ($this->info['item']) {
case 'media': // media file revision
$html = media_printicon($id);
break;
case 'page': // page revision
$html = '<img class="icon" src="'.DOKU_BASE.'lib/images/fileicons/file.png" alt="'.$id.'" />';
}
return $html;
}
@ -254,14 +288,17 @@ class Recent extends Ui
public function itemName()
{
$id = $this->info['id'];
if (isset($this->info['media'])) {
$href = media_managerURL(['tab_details'=>'view', 'image'=> $id, 'ns'=> getNS($id)], '&');
$class = file_exists(mediaFN($id)) ? 'wikilink1' : 'wikilink2';
$html = '<a href="'.$href.'" class="'.$class.'">'.$id.'</a>';
} else {
$html = html_wikilink(':'.$id, (useHeading('navigation') ? null : $id));
switch ($this->info['item']) {
case 'media': // media file revision
$href = media_managerURL(['tab_details'=>'view', 'image'=> $id, 'ns'=> getNS($id)], '&');
$class = file_exists(mediaFN($id)) ? 'wikilink1' : 'wikilink2';
$html = '<a href="'.$href.'" class="'.$class.'">'.$id.'</a>';
return $html;
case 'page': // page revision
$html = html_wikilink(':'.$id, (useHeading('navigation') ? null : $id));
return $html;
}
return $html;
return '';
}
// icon difflink
@ -270,18 +307,20 @@ class Recent extends Ui
global $lang;
$id = $this->info['id'];
if (isset($this->info['media'])) {
$revs = (new MediaChangeLog($id))->getRevisions(0, 1);
$diff = (count($revs) && file_exists(mediaFN($id)));
if ($diff) {
$href = media_managerURL(
['tab_details'=>'history', 'mediado'=>'diff', 'image'=> $id, 'ns'=> getNS($id)], '&'
);
} else {
$href = '';
}
} else {
$href = wl($id, "do=diff", false, '&');
switch ($this->info['item']) {
case 'media': // media file revision
$revs = (new MediaChangeLog($id))->getRevisions(0, 1);
$diff = (count($revs) && file_exists(mediaFN($id)));
if ($diff) {
$href = media_managerURL(
['tab_details'=>'history', 'mediado'=>'diff', 'image'=> $id, 'ns'=> getNS($id)], '&'
);
} else {
$href = '';
}
break;
case 'page': // page revision
$href = wl($id, "do=diff", false, '&');
}
if ($href) {
@ -300,10 +339,12 @@ class Recent extends Ui
{
global $lang;
$id = $this->info['id'];
if (isset($this->info['media'])) {
$href = media_managerURL(['tab_details'=>'history', 'image'=> $id, 'ns'=> getNS($id)], '&');
} else {
$href = wl($id, "do=revisions", false, '&');
switch ($this->info['item']) {
case 'media': // media file revision
$href = media_managerURL(['tab_details'=>'history', 'image'=> $id, 'ns'=> getNS($id)], '&');
break;
case 'page': // page revision
$href = wl($id, "do=revisions", false, '&');
}
$html = '<a href="'.$href.'" class="revisions_link">'
. '<img src="'.DOKU_BASE.'lib/images/history.png" width="12" height="14"'

View File

@ -2,193 +2,40 @@
namespace dokuwiki\Ui;
use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\ChangeLog\MediaChangeLog;
use dokuwiki\Form\Form;
use dokuwiki\ChangeLog\ChangeLog;
/**
* DokuWiki Revisions Interface
* parent class of PageRevisions and MediaRevisions
*
* @package dokuwiki\Ui
*/
class Revisions extends Ui
abstract class Revisions extends Ui
{
protected $first;
protected $media_id;
/* @var string */
protected $id; // page id or media id
/* @var ChangeLog */
protected $changelog; // PageChangeLog or MediaChangeLog object
/**
* Revisions Ui constructor
*
* @param int $first skip the first n changelog lines
* @param bool|string $media_id id of media, or false for current page
* @param string $id page id or media id
*/
public function __construct($first = 0, $media_id = false)
public function __construct($id)
{
$this->first = $first;
$this->media_id = $media_id;
$this->id = $id;
$this->setChangeLog();
}
/**
* Display list of old revisions
*
* @author Andreas Gohr <andi@splitbrain.org>
* @author Ben Coburn <btcoburn@silicodon.net>
* @author Kate Arzamastseva <pshns@ukr.net>
* @author Satoshi Sahara <sahara.satoshi@gmail.com>
*
* @return void
* set class property changelog
*/
public function show()
{
global $ID;
if ($this->media_id) {
return $this->showMediaRevisions($this->media_id);
} else {
return $this->showPageRevisions($ID);
}
}
abstract protected function setChangeLog();
/**
* Display a list of Media Revisions in the MediaManager
*
* @param string $id media id
* @return void
*/
protected function showMediaRevisions($id)
{
global $lang;
// get revisions, and set correct pagenation parameters (first, hasNext)
$first = $this->first;
$hasNext = false;
$revisions = $this->getRevisions($first, $hasNext);
// create the form
$form = new Form([
'id' => 'page__revisions', // must not be "media__revisions"
'action' => media_managerURL(['image' => $id], '&'),
'class' => 'changes',
]);
$form->setHiddenField('mediado', 'diff'); // required for media revisions
$form->addTagOpen('div')->addClass('no');
// start listing
$form->addTagOpen('ul');
foreach ($revisions as $info) {
$rev = $info['date'];
$class = ($info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) ? 'minor' : '';
$form->addTagOpen('li')->addClass($class);
$form->addTagOpen('div')->addClass('li');
if (isset($info['current'])) {
$form->addCheckbox('rev2[]')->val('current');
} elseif (file_exists(mediaFN($id, $rev))) {
$form->addCheckbox('rev2[]')->val($rev);
} else {
$form->addCheckbox('')->val($rev)->attr('disabled','disabled');
}
$form->addHTML(' ');
$objRevInfo = $this->getObjRevInfo($info);
$html = implode(' ', [
$objRevInfo->editDate(), // edit date and time
$objRevInfo->difflink(), // link to diffview icon
$objRevInfo->itemName(), // name of page or media
'<div>',
$objRevInfo->editSummary(), // edit summary
$objRevInfo->editor(), // editor info
html_sizechange($info['sizechange']), // size change indicator
$objRevInfo->currentIndicator(), // current indicator (only when k=1)
'</div>',
]);
$form->addHTML($html);
$form->addTagClose('div');
$form->addTagClose('li');
}
$form->addTagClose('ul'); // end of revision list
// show button for diff view
$form->addButton('do[diff]', $lang['diff2'])->attr('type', 'submit');
$form->addTagClose('div'); // close div class=no
print $form->toHTML('Revisions');
// provide navigation for pagenated revision list (of pages and/or media files)
print $this->htmlNavigation($id, $first, $hasNext);
}
/**
* Display a list of Page Revisions
*
* @return void
*/
protected function showPageRevisions($id)
{
global $lang;
// get revisions, and set correct pagenation parameters (first, hasNext)
$first = $this->first;
$hasNext = false;
$revisions = $this->getRevisions($first, $hasNext);
// print intro
print p_locale_xhtml('revisions');
// create the form
$form = new Form([
'id' => 'page__revisions',
'class' => 'changes',
]);
$form->addTagOpen('div')->addClass('no');
// start listing
$form->addTagOpen('ul');
foreach ($revisions as $info) {
$rev = $info['date'];
$class = ($info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) ? 'minor' : '';
$form->addTagOpen('li')->addClass($class);
$form->addTagOpen('div')->addClass('li');
if (page_exists($id, $rev)) {
$form->addCheckbox('rev2[]')->val($rev);
} else {
$form->addCheckbox('')->val($rev)->attr('disabled','disabled');
}
$form->addHTML(' ');
$objRevInfo = $this->getObjRevInfo($info);
$html = implode(' ', [
$objRevInfo->editDate(), // edit date and time
$objRevInfo->difflink(), // link to diffview icon
$objRevInfo->itemName(), // name of page or media
$objRevInfo->editSummary(), // edit summary
$objRevInfo->editor(), // editor info
$objRevInfo->sizechange(), // size change indicator
$objRevInfo->currentIndicator(), // current indicator (only when k=1)
]);
$form->addHTML($html);
$form->addTagClose('div');
$form->addTagClose('li');
}
$form->addTagClose('ul'); // end of revision list
// show button for diff view
$form->addButton('do[diff]', $lang['diff2'])->attr('type', 'submit');
$form->addTagClose('div'); // close div class=no
print $form->toHTML('Revisions');
// provide navigation for pagenated revision list (of pages and/or media files)
print $this->htmlNavigation($id, $first, $hasNext);
}
/**
* Get revisions, and set correct pagenation parameters (first, hasNext)
* Get revisions, and set correct pagination parameters (first, hasNext)
*
* @param int $first
* @param bool $hasNext
@ -197,81 +44,46 @@ class Revisions extends Ui
*/
protected function getRevisions(&$first, &$hasNext)
{
global $INFO, $conf;
if ($this->media_id) {
$changelog = new MediaChangeLog($this->media_id);
} else {
$changelog = new PageChangeLog($INFO['id']);
}
global $conf;
$changelog =& $this->changelog;
$revisions = [];
$currentRevInfo = $changelog->getCurrentRevisionInfo();
if (!$currentRevInfo) return $revisions;
$num = $conf['recent'];
if ($first == 0) {
// add extrenal or existing last revision that is excluded from $changelog->getRevisions()
if (array_key_exists('timestamp', $currentRevInfo) || (
$currentRevInfo['type'] != DOKU_CHANGE_TYPE_DELETE &&
$currentRevInfo['date'] == $changelog->lastRevision() )
) {
$revisions[] = $currentRevInfo;
$num = $num - 1;
}
}
/* we need to get one additional log entry to be able to
* decide if this is the last page or is there another one.
* see also Ui\Recent::getRecents()
*/
$revlist = $changelog->getRevisions($first, $conf['recent'] +1);
if (count($revlist) == 0 && $first != 0) {
$revlist = $changelog->getRevisions($first, $num + 1);
if (count($revlist) == 0 && $first > 0) {
// resets to zero if $first requested a too high number
$first = 0;
$revlist = $changelog->getRevisions($first, $conf['recent'] +1);
}
$exists = ($this->media_id) ? file_exists(mediaFN($this->media_id)) : $INFO['exists'];
if ($first === 0 && $exists) {
// add current page or media as revision[0]
if ($this->media_id) {
$rev = filemtime(fullpath(mediaFN($this->media_id)));
$changelog->setChunkSize(1024);
$revinfo = $changelog->getRevisionInfo($rev) ?: array(
'date' => $rev,
'ip' => null,
'type' => null,
'id' => $this->media_id,
'user' => null,
'sum' => null,
'extra' => null,
'sizechange' => null,
);
$revisions[] = $revinfo + array(
'media' => true,
'current' => true,
);
} else {
if (isset($INFO['meta']['last_change'])) {
$type = $INFO['meta']['last_change']['type'];
$sizechange = $INFO['meta']['last_change']['sizechange'];
} else {
$type = $sizechange = null;
}
$revisions[] = array(
'date' => $INFO['lastmod'],
'ip' => null,
'type' => $type,
'id' => $INFO['id'],
'user' => $INFO['editor'],
'sum' => $INFO['sum'],
'extra' => null,
'sizechange' => $sizechange,
'current' => true,
);
}
return $this->getRevisions($first, $hasNext);
}
// decide if this is the last page or is there another one
$hasNext = false;
if (count($revlist) > $conf['recent']) {
if (count($revlist) > $num) {
$hasNext = true;
array_pop($revlist); // remove one additional log entry
}
// append each revison info array to the revisions
foreach ($revlist as $rev) {
if ($this->media_id) {
$revisions[] = $changelog->getRevisionInfo($rev) + array('media' => true);
} else {
$revisions[] = $changelog->getRevisionInfo($rev);
}
$revisions[] = $changelog->getRevisionInfo($rev);
}
return $revisions;
}
@ -279,12 +91,12 @@ class Revisions extends Ui
/**
* Navigation buttons for Pagenation (prev/next)
*
* @param string $id page id or media id
* @param int $first
* @param bool $hasNext
* @return array html
* @param callable $callback returns array of hidden fields for the form button
* @return string html
*/
protected function htmlNavigation($id, $first, $hasNext)
protected function navigation($first, $hasNext, $callback)
{
global $conf;
@ -293,20 +105,12 @@ class Revisions extends Ui
if ($first > 0) {
$first = max($first - $conf['recent'], 0);
$html.= '<div class="pagenav-prev">';
if ($this->media_id) {
$html.= html_btn('newer', $id, "p", media_managerURL(['first' => $first], '&', false, true));
} else {
$html.= html_btn('newer', $id, "p" ,['do' => 'revisions', 'first' => $first]);
}
$html.= html_btn('newer', $this->id, "p", $callback($first));
$html.= '</div>';
}
if ($hasNext) {
$html.= '<div class="pagenav-next">';
if ($this->media_id) {
$html.= html_btn('older', $id, "n", media_managerURL(['first' => $last], '&', false, true));
} else {
$html.= html_btn('older', $id, "n", ['do' => 'revisions', 'first' => $last]);
}
$html.= html_btn('older', $this->id, "n", $callback($last));
$html.= '</div>';
}
$html.= '</div>';
@ -319,7 +123,7 @@ class Revisions extends Ui
* @param array $info Revision info structure of a page or media file
* @return objRevInfo object (anonymous class)
*/
protected function getObjRevInfo(array $info)
public function getObjRevInfo(array $info)
{
return new class ($info) // anonymous class (objRevInfo)
{
@ -327,9 +131,10 @@ class Revisions extends Ui
public function __construct(array $info)
{
if (!isset($info['current'])) {
$info['current'] = false;
}
$info['item'] = strrpos($info['id'], '.') ? 'media' : 'page';
$info['current'] = $info['current'] ?? false;
// revision info may have timestamp key when external edits occurred
$info['timestamp'] = $info['timestamp'] ?? true;
$this->info = $info;
}
@ -343,7 +148,13 @@ class Revisions extends Ui
// edit date and time of the page or media file
public function editDate()
{
return '<span class="date">'. dformat($this->info['date']) .'</span>';
global $lang;
$date = dformat($this->info['date']);
if ($this->info['timestamp'] === false) {
// externally deleted or older file restored
$date = preg_replace('/[0-9a-zA-Z]/','_', $date);
}
return '<span class="date">'. $date .'</span>';
}
// edit summary
@ -377,30 +188,34 @@ class Revisions extends Ui
$id = $this->info['id'];
$rev = $this->info['date'];
if (isset($this->info['media'])) {
// media file revision
if (isset($this->info['current'])) {
$href = media_managerURL(['image'=> $id, 'tab_details'=> 'view'], '&');
$html = '<a href="'.$href.'" class="wikilink1">'.$id.'</a>';
} elseif (file_exists(mediaFN($id, $rev))) {
$href = media_managerURL(['image'=> $id, 'tab_details'=> 'view', 'rev'=> $rev], '&');
$html = '<a href="'.$href.'" class="wikilink1">'.$id.'</a>';
} else {
$html = $id;
}
return $html;
} else {
// page revision
$display_name = useHeading('navigation') ? hsc(p_get_first_heading($id)) : $id;
if (!$display_name) $display_name = $id;
if ($this->info['current'] || page_exists($id, $rev)) {
$href = wl($id, "rev=$rev", false, '&');
$html = '<a href="'.$href.'" class="wikilink1">'.$display_name.'</a>';
} else {
$html = $display_name;
}
return $html;
switch ($this->info['item']) {
case 'media': // media file revision
if ($this->info['current']) {
$href = media_managerURL(['image'=> $id, 'tab_details'=> 'view'], '&');
$html = '<a href="'.$href.'" class="wikilink1">'.$id.'</a>';
} elseif (file_exists(mediaFN($id, $rev))) {
$href = media_managerURL(['image'=> $id, 'tab_details'=> 'view', 'rev'=> $rev], '&');
$html = '<a href="'.$href.'" class="wikilink1">'.$id.'</a>';
} else {
$html = $id;
}
return $html;
case 'page': // page revision
$display_name = useHeading('navigation') ? hsc(p_get_first_heading($id)) : $id;
if (!$display_name) $display_name = $id;
if ($this->info['type'] == DOKU_CHANGE_TYPE_DELETE) {
// externally deleted or older file restored
$href = wl($id, "", false, '&');
$html = '<a href="'.$href.'" class="wikilink2">'.$display_name.'</a>';
} elseif ($this->info['current'] || page_exists($id, $rev)) {
$href = wl($id, "rev=$rev", false, '&');
$html = '<a href="'.$href.'" class="wikilink1">'.$display_name.'</a>';
} else {
$html = $display_name;
}
return $html;
}
return '';
}
// icon difflink
@ -410,31 +225,31 @@ class Revisions extends Ui
$id = $this->info['id'];
$rev = $this->info['date'];
if (isset($this->info['media'])) {
// media file revision
if (isset($this->info['current']) || !file_exists(mediaFN($id, $rev))) {
$html = '<img src="'.DOKU_BASE.'lib/images/blank.gif" width="15" height="11" alt="" />';
} else {
$href = media_managerURL(['image'=> $id, 'rev'=> $rev, 'mediado'=>'diff'], '&');
$html = '<a href="'.$href.'" class="diff_link">'
. '<img src="'.DOKU_BASE.'lib/images/diff.png" width="15" height="11"'
. ' title="'. $lang['diff'] .'" alt="'.$lang['diff'] .'" />'
. '</a> ';
}
return $html;
} else {
// page revision
if ($this->info['current'] || !page_exists($id, $rev)) {
$html = '<img src="'.DOKU_BASE.'lib/images/blank.gif" width="15" height="11" alt="" />';
} else {
$href = wl($id, "rev=$rev,do=diff", false, '&');
$html = '<a href="'.$href.'" class="diff_link">'
. '<img src="'.DOKU_BASE.'lib/images/diff.png" width="15" height="11"'
. ' title="'.$lang['diff'].'" alt="'.$lang['diff'].'" />'
. '</a>';
}
return $html;
switch ($this->info['item']) {
case 'media': // media file revision
if ($this->info['current'] || !file_exists(mediaFN($id, $rev))) {
$html = '<img src="'.DOKU_BASE.'lib/images/blank.gif" width="15" height="11" alt="" />';
} else {
$href = media_managerURL(['image'=> $id, 'rev'=> $rev, 'mediado'=>'diff'], '&');
$html = '<a href="'.$href.'" class="diff_link">'
. '<img src="'.DOKU_BASE.'lib/images/diff.png" width="15" height="11"'
. ' title="'. $lang['diff'] .'" alt="'.$lang['diff'] .'" />'
. '</a> ';
}
return $html;
case 'page': // page revision
if ($this->info['current'] || !page_exists($id, $rev)) {
$html = '<img src="'.DOKU_BASE.'lib/images/blank.gif" width="15" height="11" alt="" />';
} else {
$href = wl($id, "rev=$rev,do=diff", false, '&');
$html = '<a href="'.$href.'" class="diff_link">'
. '<img src="'.DOKU_BASE.'lib/images/diff.png" width="15" height="11"'
. ' title="'.$lang['diff'].'" alt="'.$lang['diff'].'" />'
. '</a>';
}
return $html;
}
return '';
}
// size change

View File

@ -6,6 +6,9 @@
* @author Andreas Gohr <andi@splitbrain.org>
*/
use dokuwiki\ChangeLog\ChangeLog;
use dokuwiki\File\PageFile;
/**
* parses a changelog line into it's components
*
@ -15,31 +18,15 @@
* @return array|bool parsed line or false
*/
function parseChangelogLine($line) {
$line = rtrim($line, "\n");
$tmp = explode("\t", $line);
if ($tmp!==false && count($tmp)>1) {
$info = array();
$info['date'] = (int)$tmp[0]; // unix timestamp
$info['ip'] = $tmp[1]; // IPv4 address (127.0.0.1)
$info['type'] = $tmp[2]; // log line type
$info['id'] = $tmp[3]; // page id
$info['user'] = $tmp[4]; // user name
$info['sum'] = $tmp[5]; // edit summary (or action reason)
$info['extra'] = $tmp[6]; // extra data (varies by line type)
if(isset($tmp[7]) && $tmp[7] !== '') { //last item has line-end||
$info['sizechange'] = (int) $tmp[7];
} else {
$info['sizechange'] = null;
}
return $info;
} else {
return false;
}
return ChangeLog::parseLogLine($line);
}
/**
* Adds an entry to the changelog and saves the metadata for the page
*
* Note: timestamp of the change might not be unique especially after very quick
* repeated edits (e.g. change checkbox via do plugin)
*
* @param int $date Timestamp of the change
* @param String $id Name of the affected page
* @param String $type Type of the change see DOKU_CHANGE_TYPE_*
@ -53,79 +40,50 @@ function parseChangelogLine($line) {
* @author Andreas Gohr <andi@splitbrain.org>
* @author Esther Brunner <wikidesign@gmail.com>
* @author Ben Coburn <btcoburn@silicodon.net>
* @deprecated 2021-11-28
*/
function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extra='', $flags=null, $sizechange = null){
function addLogEntry(
$date,
$id,
$type = DOKU_CHANGE_TYPE_EDIT,
$summary = '',
$extra = '',
$flags = null,
$sizechange = null)
{
// no more used in DokuWiki core, but left for third-party plugins
dbg_deprecated('see '. \dokuwiki\File\PageFile::class .'::saveWikiText()');
global $conf, $INFO;
/** @var Input $INPUT */
global $INPUT;
// check for special flags as keys
if (!is_array($flags)) { $flags = array(); }
if (!is_array($flags)) $flags = array();
$flagExternalEdit = isset($flags['ExternalEdit']);
$id = cleanid($id);
$file = wikiFN($id);
$created = @filectime($file);
$minor = ($type===DOKU_CHANGE_TYPE_MINOR_EDIT);
$wasRemoved = ($type===DOKU_CHANGE_TYPE_DELETE);
if(!$date) $date = time(); //use current time if none supplied
$remote = (!$flagExternalEdit)?clientIP(true):'127.0.0.1';
$user = (!$flagExternalEdit)?$INPUT->server->str('REMOTE_USER'):'';
if($sizechange === null) {
$sizechange = '';
} else {
$sizechange = (int) $sizechange;
}
if (!$date) $date = time(); //use current time if none supplied
$remote = (!$flagExternalEdit) ? clientIP(true) : '127.0.0.1';
$user = (!$flagExternalEdit) ? $INPUT->server->str('REMOTE_USER') : '';
$sizechange = ($sizechange === null) ? '' : (int)$sizechange;
$strip = array("\t", "\n");
$logline = array(
// update changelog file and get the added entry that is also to be stored in metadata
$pageFile = new PageFile($id);
$logEntry = $pageFile->changelog->addLogEntry([
'date' => $date,
'ip' => $remote,
'type' => str_replace($strip, '', $type),
'type' => $type,
'id' => $id,
'user' => $user,
'sum' => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255),
'extra' => str_replace($strip, '', $extra),
'sizechange' => $sizechange
);
'sum' => $summary,
'extra' => $extra,
'sizechange' => $sizechange,
]);
$wasCreated = ($type===DOKU_CHANGE_TYPE_CREATE);
$wasReverted = ($type===DOKU_CHANGE_TYPE_REVERT);
// update metadata
if (!$wasRemoved) {
$oldmeta = p_read_metadata($id)['persistent'];
$meta = array();
if (
$wasCreated && (
empty($oldmeta['date']['created']) ||
$oldmeta['date']['created'] === $created
)
){
// newly created
$meta['date']['created'] = $created;
if ($user){
$meta['creator'] = isset($INFO) ? $INFO['userinfo']['name'] : null;
$meta['user'] = $user;
}
} elseif (($wasCreated || $wasReverted) && !empty($oldmeta['date']['created'])) {
// re-created / restored
$meta['date']['created'] = $oldmeta['date']['created'];
$meta['date']['modified'] = $created; // use the files ctime here
$meta['creator'] = isset($oldmeta['creator']) ? $oldmeta['creator'] : null;
if ($user) $meta['contributor'][$user] = isset($INFO) ? $INFO['userinfo']['name'] : null;
} elseif (!$minor) { // non-minor modification
$meta['date']['modified'] = $date;
if ($user) $meta['contributor'][$user] = isset($INFO) ? $INFO['userinfo']['name'] : null;
}
$meta['last_change'] = $logline;
p_set_metadata($id, $meta);
}
// add changelog lines
$logline = implode("\t", $logline)."\n";
io_saveFile(metaFN($id,'.changes'),$logline,true); //page changelog
io_saveFile($conf['changelog'],$logline,true); //global changelog cache
$pageFile->updateMetadata($logEntry);
}
/**
@ -149,48 +107,42 @@ function addLogEntry($date, $id, $type=DOKU_CHANGE_TYPE_EDIT, $summary='', $extr
function addMediaLogEntry(
$date,
$id,
$type=DOKU_CHANGE_TYPE_EDIT,
$summary='',
$extra='',
$flags=null,
$type = DOKU_CHANGE_TYPE_EDIT,
$summary = '',
$extra = '',
$flags = null,
$sizechange = null)
{
global $conf;
/** @var Input $INPUT */
global $INPUT;
// check for special flags as keys
if (!is_array($flags)) $flags = array();
$flagExternalEdit = isset($flags['ExternalEdit']);
$id = cleanid($id);
if(!$date) $date = time(); //use current time if none supplied
$remote = clientIP(true);
$user = $INPUT->server->str('REMOTE_USER');
if($sizechange === null) {
$sizechange = '';
} else {
$sizechange = (int) $sizechange;
}
if (!$date) $date = time(); //use current time if none supplied
$remote = (!$flagExternalEdit) ? clientIP(true) : '127.0.0.1';
$user = (!$flagExternalEdit) ? $INPUT->server->str('REMOTE_USER') : '';
$sizechange = ($sizechange === null) ? '' : (int)$sizechange;
$strip = array("\t", "\n");
$logline = array(
// update changelog file and get the added entry
$logEntry = (new MediaChangeLog($id, 1024))->addLogEntry([
'date' => $date,
'ip' => $remote,
'type' => str_replace($strip, '', $type),
'type' => $type,
'id' => $id,
'user' => $user,
'sum' => \dokuwiki\Utf8\PhpString::substr(str_replace($strip, '', $summary), 0, 255),
'extra' => str_replace($strip, '', $extra),
'sizechange' => $sizechange
);
// add changelog lines
$logline = implode("\t", $logline)."\n";
io_saveFile($conf['media_changelog'],$logline,true); //global media changelog cache
io_saveFile(mediaMetaFN($id,'.changes'),$logline,true); //media file's changelog
'sum' => $summary,
'extra' => $extra,
'sizechange' => $sizechange,
]);
}
/**
* returns an array of recently changed files using the
* changelog
* returns an array of recently changed files using the changelog
*
* The following constants can be used to control which changes are
* included. Add them together as needed.
@ -211,12 +163,12 @@ function addMediaLogEntry(
* @author Ben Coburn <btcoburn@silicodon.net>
* @author Kate Arzamastseva <pshns@ukr.net>
*/
function getRecents($first,$num,$ns='',$flags=0){
function getRecents($first, $num, $ns = '', $flags = 0) {
global $conf;
$recent = array();
$count = 0;
if(!$num)
if (!$num)
return $recent;
// read all recent changes. (kept short)
@ -228,7 +180,7 @@ function getRecents($first,$num,$ns='',$flags=0){
if (!is_array($lines)) {
$lines = array();
}
$lines_position = count($lines)-1;
$lines_position = count($lines) - 1;
$media_lines_position = 0;
$media_lines = array();
@ -237,13 +189,13 @@ function getRecents($first,$num,$ns='',$flags=0){
if (!is_array($media_lines)) {
$media_lines = array();
}
$media_lines_position = count($media_lines)-1;
$media_lines_position = count($media_lines) - 1;
}
$seen = array(); // caches seen lines, _handleRecent() skips them
// handle lines
while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >=0)) {
while ($lines_position >= 0 || (($flags & RECENTS_MEDIA_PAGES_MIXED) && $media_lines_position >= 0)) {
if (empty($rec) && $lines_position >= 0) {
$rec = _handleRecent(@$lines[$lines_position], $ns, $flags, $seen);
if (!$rec) {
@ -274,11 +226,11 @@ function getRecents($first,$num,$ns='',$flags=0){
if ($flags & RECENTS_MEDIA_CHANGES) $x['media'] = true;
$rec = false;
}
if(--$first >= 0) continue; // skip first entries
if (--$first >= 0) continue; // skip first entries
$recent[] = $x;
$count++;
// break when we have enough entries
if($count >= $num){ break; }
if ($count >= $num) { break; }
}
return $recent;
}
@ -305,11 +257,11 @@ function getRecents($first,$num,$ns='',$flags=0){
* @author Michael Hamann <michael@content-space.de>
* @author Ben Coburn <btcoburn@silicodon.net>
*/
function getRecentsSince($from,$to=null,$ns='',$flags=0){
function getRecentsSince($from, $to = null, $ns = '', $flags = 0) {
global $conf;
$recent = array();
if($to && $to < $from)
if ($to && $to < $from)
return $recent;
// read all recent changes. (kept short)
@ -318,7 +270,7 @@ function getRecentsSince($from,$to=null,$ns='',$flags=0){
} else {
$lines = @file($conf['changelog']);
}
if(!$lines) return $recent;
if (!$lines) return $recent;
// we start searching at the end of the list
$lines = array_reverse($lines);
@ -326,9 +278,9 @@ function getRecentsSince($from,$to=null,$ns='',$flags=0){
// handle lines
$seen = array(); // caches seen lines, _handleRecent() skips them
foreach($lines as $line){
foreach ($lines as $line) {
$rec = _handleRecent($line, $ns, $flags, $seen);
if($rec !== false) {
if ($rec !== false) {
if ($rec['date'] >= $from) {
if (!$to || $rec['date'] <= $to) {
$recent[] = $rec;
@ -357,30 +309,30 @@ function getRecentsSince($from,$to=null,$ns='',$flags=0){
* @param array $seen listing of seen pages
* @return array|bool false or array with info about a change
*/
function _handleRecent($line,$ns,$flags,&$seen){
if(empty($line)) return false; //skip empty lines
function _handleRecent($line, $ns, $flags, &$seen) {
if (empty($line)) return false; //skip empty lines
// split the line into parts
$recent = parseChangelogLine($line);
if ($recent===false) { return false; }
$recent = ChangeLog::parseLogLine($line);
if ($recent === false) return false;
// skip seen ones
if(isset($seen[$recent['id']])) return false;
if (isset($seen[$recent['id']])) return false;
// skip changes, of only new items are requested
if($recent['type']!==DOKU_CHANGE_TYPE_CREATE && ($flags & RECENTS_ONLY_CREATION)) return false;
if ($recent['type'] !== DOKU_CHANGE_TYPE_CREATE && ($flags & RECENTS_ONLY_CREATION)) return false;
// skip minors
if($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
if ($recent['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT && ($flags & RECENTS_SKIP_MINORS)) return false;
// remember in seen to skip additional sights
$seen[$recent['id']] = 1;
// check if it's a hidden page
if(isHiddenPage($recent['id'])) return false;
if (isHiddenPage($recent['id'])) return false;
// filter namespace
if (($ns) && (strpos($recent['id'],$ns.':') !== 0)) return false;
if (($ns) && (strpos($recent['id'], $ns.':') !== 0)) return false;
// exclude subnamespaces
if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($recent['id']) != $ns)) return false;
@ -394,10 +346,11 @@ function _handleRecent($line,$ns,$flags,&$seen){
if ($recent['perms'] < AUTH_READ) return false;
// check existance
if($flags & RECENTS_SKIP_DELETED){
if ($flags & RECENTS_SKIP_DELETED) {
$fn = (($flags & RECENTS_MEDIA_CHANGES) ? mediaFN($recent['id']) : wikiFN($recent['id']));
if(!file_exists($fn)) return false;
if (!file_exists($fn)) return false;
}
return $recent;
}

View File

@ -9,6 +9,8 @@
use dokuwiki\Cache\CacheInstructions;
use dokuwiki\Cache\CacheRenderer;
use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\File\PageFile;
use dokuwiki\Logger;
use dokuwiki\Subscriptions\PageSubscriptionSender;
use dokuwiki\Subscriptions\SubscriberManager;
use dokuwiki\Extension\AuthPlugin;
@ -215,11 +217,12 @@ function pageinfo() {
$info['filepath'] = wikiFN($ID);
$info['exists'] = file_exists($info['filepath']);
$info['currentrev'] = @filemtime($info['filepath']);
if($REV) {
if ($REV) {
//check if current revision was meant
if($info['exists'] && ($info['currentrev'] == $REV)) {
if ($info['exists'] && ($info['currentrev'] == $REV)) {
$REV = '';
} elseif($RANGE) {
} elseif ($RANGE) {
//section editing does not work with old revisions!
$REV = '';
$RANGE = '';
@ -231,9 +234,8 @@ function pageinfo() {
}
}
$info['rev'] = $REV;
if($info['exists']) {
$info['writable'] = (is_writable($info['filepath']) &&
($info['perm'] >= AUTH_EDIT));
if ($info['exists']) {
$info['writable'] = (is_writable($info['filepath']) && $info['perm'] >= AUTH_EDIT);
} else {
$info['writable'] = ($info['perm'] >= AUTH_CREATE);
}
@ -245,41 +247,37 @@ function pageinfo() {
//who's the editor
$pagelog = new PageChangeLog($ID, 1024);
if($REV) {
if ($REV) {
$revinfo = $pagelog->getRevisionInfo($REV);
} else {
if(!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) {
if (!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) {
$revinfo = $info['meta']['last_change'];
} else {
$revinfo = $pagelog->getRevisionInfo($info['lastmod']);
// cache most recent changelog line in metadata if missing and still valid
if($revinfo !== false) {
if ($revinfo !== false) {
$info['meta']['last_change'] = $revinfo;
p_set_metadata($ID, array('last_change' => $revinfo));
}
}
}
//and check for an external edit
if($revinfo !== false && $revinfo['date'] != $info['lastmod']) {
if ($revinfo !== false && $revinfo['date'] != $info['lastmod']) {
// cached changelog line no longer valid
$revinfo = false;
$info['meta']['last_change'] = $revinfo;
p_set_metadata($ID, array('last_change' => $revinfo));
}
if($revinfo !== false){
if ($revinfo !== false) {
$info['ip'] = $revinfo['ip'];
$info['user'] = $revinfo['user'];
$info['sum'] = $revinfo['sum'];
// See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
// Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
if($revinfo['user']) {
$info['editor'] = $revinfo['user'];
} else {
$info['editor'] = $revinfo['ip'];
}
}else{
$info['editor'] = $revinfo['user'] ?: $revinfo['ip'];
} else {
$info['ip'] = null;
$info['user'] = null;
$info['sum'] = null;
@ -317,7 +315,7 @@ function jsinfo() {
*
* @return array with info about current media item
*/
function mediainfo(){
function mediainfo() {
global $NS;
global $IMG;
@ -1244,45 +1242,11 @@ function con($pre, $text, $suf, $pretty = false) {
* wiki, triggered in @see saveWikiText()
*
* @param string $id the page ID
* @deprecated 2021-11-28
*/
function detectExternalEdit($id) {
global $lang;
$fileLastMod = wikiFN($id);
$lastMod = @filemtime($fileLastMod); // from page
$pagelog = new PageChangeLog($id, 1024);
$lastRev = $pagelog->getRevisions(-1, 1); // from changelog
$lastRev = (int) (empty($lastRev) ? 0 : $lastRev[0]);
if(!file_exists(wikiFN($id, $lastMod)) && file_exists($fileLastMod) && $lastMod >= $lastRev) {
// add old revision to the attic if missing
saveOldRevision($id);
// add a changelog entry if this edit came from outside dokuwiki
if($lastMod > $lastRev) {
$fileLastRev = wikiFN($id, $lastRev);
$revinfo = $pagelog->getRevisionInfo($lastRev);
if(empty($lastRev) || !file_exists($fileLastRev) || $revinfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
$filesize_old = 0;
} else {
$filesize_old = io_getSizeFile($fileLastRev);
}
$filesize_new = filesize($fileLastMod);
$sizechange = $filesize_new - $filesize_old;
addLogEntry(
$lastMod,
$id,
DOKU_CHANGE_TYPE_EDIT,
$lang['external_edit'],
'',
array('ExternalEdit' => true),
$sizechange
);
// remove soon to be stale instructions
$cache = new CacheInstructions($id, $fileLastMod);
$cache->removeCache();
}
}
dbg_deprecated(\dokuwiki\File\PageFile::class .'::detectExternalEdit()');
(new PageFile($id))->detectExternalEdit();
}
/**
@ -1298,117 +1262,19 @@ function detectExternalEdit($id) {
* @param bool $minor mark this saved version as minor update
*/
function saveWikiText($id, $text, $summary, $minor = false) {
/* Note to developers:
This code is subtle and delicate. Test the behavior of
the attic and changelog with dokuwiki and external edits
after any changes. External edits change the wiki page
directly without using php or dokuwiki.
*/
global $conf;
global $lang;
global $REV;
/* @var Input $INPUT */
global $INPUT;
// prepare data for event
$svdta = array();
$svdta['id'] = $id;
$svdta['file'] = wikiFN($id);
$svdta['revertFrom'] = $REV;
$svdta['oldRevision'] = @filemtime($svdta['file']);
$svdta['newRevision'] = 0;
$svdta['newContent'] = $text;
$svdta['oldContent'] = rawWiki($id);
$svdta['summary'] = $summary;
$svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']);
$svdta['changeInfo'] = '';
$svdta['changeType'] = DOKU_CHANGE_TYPE_EDIT;
$svdta['sizechange'] = null;
// select changelog line type
if($REV) {
$svdta['changeType'] = DOKU_CHANGE_TYPE_REVERT;
$svdta['changeInfo'] = $REV;
} else if(!file_exists($svdta['file'])) {
$svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE;
} else if(trim($text) == '') {
// empty or whitespace only content deletes
$svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE;
// autoset summary on deletion
if(blank($svdta['summary'])) {
$svdta['summary'] = $lang['deleted'];
}
} else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
//minor edits only for logged in users
$svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT;
}
$event = new Event('COMMON_WIKIPAGE_SAVE', $svdta);
if(!$event->advise_before()) return;
// if the content has not been changed, no save happens (plugins may override this)
if(!$svdta['contentChanged']) return;
detectExternalEdit($id);
if(
$svdta['changeType'] == DOKU_CHANGE_TYPE_CREATE ||
($svdta['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($svdta['file']))
) {
$filesize_old = 0;
} else {
$filesize_old = filesize($svdta['file']);
}
if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
// Send "update" event with empty data, so plugins can react to page deletion
$data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false);
Event::createAndTrigger('IO_WIKIPAGE_WRITE', $data);
// pre-save deleted revision
@touch($svdta['file']);
clearstatcache();
$svdta['newRevision'] = saveOldRevision($id);
// remove empty file
@unlink($svdta['file']);
$filesize_new = 0;
// don't remove old meta info as it should be saved, plugins can use
// IO_WIKIPAGE_WRITE for removing their metadata...
// purge non-persistant meta data
p_purge_metadata($id);
// remove empty namespaces
io_sweepNS($id, 'datadir');
io_sweepNS($id, 'mediadir');
} else {
// save file (namespace dir is created in io_writeWikiPage)
io_writeWikiPage($svdta['file'], $svdta['newContent'], $id);
// pre-save the revision, to keep the attic in sync
$svdta['newRevision'] = saveOldRevision($id);
$filesize_new = filesize($svdta['file']);
}
$svdta['sizechange'] = $filesize_new - $filesize_old;
$event->advise_after();
addLogEntry(
$svdta['newRevision'],
$svdta['id'],
$svdta['changeType'],
$svdta['summary'],
$svdta['changeInfo'],
null,
$svdta['sizechange']
);
// get COMMON_WIKIPAGE_SAVE event data
$data = (new PageFile($id))->saveWikiText($text, $summary, $minor);
// send notify mails
notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
// update the purgefile (timestamp of the last time anything within the wiki was changed)
io_saveFile($conf['cachedir'].'/purgefile', time());
list('oldRevision' => $rev, 'newRevision' => $new_rev, 'summary' => $summary) = $data;
notify($id, 'admin', $rev, $summary, $minor, $new_rev);
notify($id, 'subscribers', $rev, $summary, $minor, $new_rev);
// if useheading is enabled, purge the cache of all linking pages
if(useHeading('content')) {
if (useHeading('content')) {
$pages = ft_backlinks($id, true);
foreach($pages as $page) {
foreach ($pages as $page) {
$cache = new CacheRenderer($page, wikiFN($page), 'xhtml');
$cache->removeCache();
}
@ -1416,21 +1282,17 @@ function saveWikiText($id, $text, $summary, $minor = false) {
}
/**
* moves the current version to the attic and returns its
* revision date
* moves the current version to the attic and returns its revision date
*
* @author Andreas Gohr <andi@splitbrain.org>
*
* @param string $id page id
* @return int|string revision timestamp
* @deprecated 2021-11-28
*/
function saveOldRevision($id) {
$oldf = wikiFN($id);
if(!file_exists($oldf)) return '';
$date = filemtime($oldf);
$newf = wikiFN($id, $date);
io_writeWikiPage($newf, rawWiki($id), $id, $date);
return $date;
dbg_deprecated(\dokuwiki\File\PageFile::class .'::saveOldRevision()');
return (new PageFile($id))->saveOldRevision();
}
/**
@ -1438,7 +1300,7 @@ function saveOldRevision($id) {
*
* @param string $id The changed page
* @param string $who Who to notify (admin|subscribers|register)
* @param int|string $rev Old page revision
* @param int|string $rev Old page revision
* @param string $summary What changed
* @param boolean $minor Is this a minor edit?
* @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value
@ -1453,20 +1315,20 @@ function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace =
global $INPUT;
// decide if there is something to do, eg. whom to mail
if($who == 'admin') {
if(empty($conf['notify'])) return false; //notify enabled?
if ($who == 'admin') {
if (empty($conf['notify'])) return false; //notify enabled?
$tpl = 'mailtext';
$to = $conf['notify'];
} elseif($who == 'subscribers') {
if(!actionOK('subscribe')) return false; //subscribers enabled?
if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
} elseif ($who == 'subscribers') {
if (!actionOK('subscribe')) return false; //subscribers enabled?
if ($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
$data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace);
Event::createAndTrigger(
'COMMON_NOTIFY_ADDRESSLIST', $data,
array(new SubscriberManager(), 'notifyAddresses')
);
$to = $data['addresslist'];
if(empty($to)) return false;
if (empty($to)) return false;
$tpl = 'subscr_single';
} else {
return false; //just to be safe

View File

@ -297,12 +297,17 @@ function html_locked() {
* @author Kate Arzamastseva <pshns@ukr.net>
*
* @param int $first skip the first n changelog lines
* @param bool|string $media_id id of media, or false for current page
* @param string $media_id id of media, or empty for current page
* @deprecated 2020-07-18
*/
function html_revisions($first=0, $media_id = false) {
dbg_deprecated(\dokuwiki\Ui\Revisions::class .'::show()');
(new dokuwiki\Ui\Revisions($first, $media_id))->show();
function html_revisions($first = 0, $media_id = '') {
dbg_deprecated(\dokuwiki\Ui\PageRevisions::class .'::show()');
if ($media_id) {
(new dokuwiki\Ui\MediaRevisions($media_id))->show($first);
} else {
global $INFO;
(new dokuwiki\Ui\PageRevisions($INFO['id']))->show($first);
}
}
/**
@ -494,7 +499,7 @@ function html_backlinks() {
* @deprecated 2020-07-18
*/
function html_diff_head($l_rev, $r_rev, $id = null, $media = false, $inline = false) {
dbg_deprecated('see '. \dokuwiki\Ui\Diff::class .'::diffHead()');
dbg_deprecated('see '. \dokuwiki\Ui\PageDiff::class .'::buildDiffHead()');
}
/**
@ -509,8 +514,12 @@ function html_diff_head($l_rev, $r_rev, $id = null, $media = false, $inline = fa
* @deprecated 2020-07-18
*/
function html_diff($text = '', $intro = true, $type = null) {
dbg_deprecated(\dokuwiki\Ui\Diff::class .'::show()');
(new dokuwiki\Ui\Diff($text, $intro, $type))->show();
dbg_deprecated(\dokuwiki\Ui\PageDiff::class .'::show()');
global $INFO;
(new dokuwiki\Ui\PageDiff($INFO['id']))->compareWith($text)->preference([
'showIntro' => $intro,
'difftype' => $type,
])->show();
}
/**
@ -524,7 +533,7 @@ function html_diff($text = '', $intro = true, $type = null) {
* @deprecated 2020-07-18
*/
function html_diff_navigation($pagelog, $type, $l_rev, $r_rev) {
dbg_deprecated('see '. \dokuwiki\Ui\Diff::class .'::diffNavigation()');
dbg_deprecated('see '. \dokuwiki\Ui\PageDiff::class .'::buildRevisionsNavigation()');
}
/**
@ -538,7 +547,7 @@ function html_diff_navigation($pagelog, $type, $l_rev, $r_rev) {
* @deprecated 2020-07-18
*/
function html_diff_navigationlink($difftype, $linktype, $lrev, $rrev = null) {
dbg_deprecated('see '. \dokuwiki\Ui\Diff::class .'::diffViewlink()');
dbg_deprecated('see '. \dokuwiki\Ui\PageDiff::class .'::diffViewlink()');
}
/**
@ -549,8 +558,8 @@ function html_diff_navigationlink($difftype, $linktype, $lrev, $rrev = null) {
* @deprecated 2020-07-18
*/
function html_insert_softbreaks($diffhtml) {
dbg_deprecated(\dokuwiki\Ui\Diff::class .'::insertSoftbreaks()');
return (new dokuwiki\Ui\Diff())->insertSoftbreaks($diffhtml);
dbg_deprecated(\dokuwiki\Ui\PageDiff::class .'::insertSoftbreaks()');
return (new dokuwiki\Ui\PageDiff)->insertSoftbreaks($diffhtml);
}
/**
@ -563,8 +572,8 @@ function html_insert_softbreaks($diffhtml) {
* @deprecated 2020-07-18
*/
function html_conflict($text, $summary) {
dbg_deprecated(\dokuwiki\Ui\Conflict::class .'::show()');
(new dokuwiki\Ui\Conflict($text, $summary))->show();
dbg_deprecated(\dokuwiki\Ui\PageConflict::class .'::show()');
(new dokuwiki\Ui\PageConflict($text, $summary))->show();
}
/**

View File

@ -226,6 +226,7 @@ $lang['created'] = 'created';
$lang['restored'] = 'old revision restored (%s)';
$lang['external_edit'] = 'external edit';
$lang['summary'] = 'Edit summary';
$lang['unknowndate'] = 'Unknown date';
$lang['noflash'] = 'The <a href="http://get.adobe.com/flashplayer">Adobe Flash Plugin</a> is needed to display this content.';
$lang['download'] = 'Download Snippet';
$lang['tools'] = 'Tools';

View File

@ -199,7 +199,7 @@ $lang['current'] = '現在';
$lang['yours'] = 'あなたのバージョン';
$lang['diff'] = '現在のリビジョンとの差分を表示';
$lang['diff2'] = '選択したリビジョン間の差分を表示';
$lang['difflink'] = 'この比較画面にリンクする';
$lang['difflink'] = 'この比較画面へのリンク';
$lang['diff_type'] = '差分の表示方法:';
$lang['diff_inline'] = 'インライン';
$lang['diff_side'] = '横に並べる';

View File

@ -567,11 +567,11 @@ function media_upload_finish($fn_tmp, $fn, $id, $imime, $overwrite, $move = 'mov
* @param string $id
* @return int - revision date
*/
function media_saveOldRevision($id){
function media_saveOldRevision($id) {
global $conf, $lang;
$oldf = mediaFN($id);
if(!file_exists($oldf)) return '';
if (!file_exists($oldf)) return '';
$date = filemtime($oldf);
if (!$conf['mediarevisions']) return $date;
@ -592,9 +592,9 @@ function media_saveOldRevision($id){
}
}
$newf = mediaFN($id,$date);
$newf = mediaFN($id, $date);
io_makeFileDir($newf);
if(copy($oldf, $newf)) {
if (copy($oldf, $newf)) {
// Set the correct permission here.
// Always chmod media because they may be saved with different permissions than expected from the php umask.
// (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.)
@ -997,10 +997,10 @@ function media_tab_history($image, $ns, $auth=null) {
if ($auth >= AUTH_READ && $image) {
if ($do == 'diff'){
media_diff($image, $ns, $auth);
(new dokuwiki\Ui\MediaDiff($image))->show(); //media_diff($image, $ns, $auth);
} else {
$first = $INPUT->int('first');
html_revisions($first, $image);
(new dokuwiki\Ui\MediaRevisions($image))->show($first);
}
} else {
echo '<div class="nothing">'.$lang['media_perm_read'].'</div>'.NL;
@ -1017,7 +1017,7 @@ function media_tab_history($image, $ns, $auth=null) {
*
* @author Kate Arzamastseva <pshns@ukr.net>
*/
function media_preview($image, $auth, $rev='', $meta=false) {
function media_preview($image, $auth, $rev = '', $meta = false) {
$size = media_image_preview_size($image, $rev, $meta);
@ -1041,7 +1041,7 @@ function media_preview($image, $auth, $rev='', $meta=false) {
echo '<img src="'.$src.'" alt="" style="max-width: '.$size[0].'px;" />';
echo '</a>';
echo '</div>'.NL;
echo '</div>';
}
}
@ -1052,12 +1052,12 @@ function media_preview($image, $auth, $rev='', $meta=false) {
*
* @param string $image media id
* @param int $auth permission level
* @param string|int $rev revision timestamp, or empty string
* @param int|string $rev revision timestamp, or empty string
*/
function media_preview_buttons($image, $auth, $rev = '') {
global $lang, $conf;
echo '<ul class="actions">'.DOKU_LF;
echo '<ul class="actions">';
if ($auth >= AUTH_DELETE && !$rev && file_exists(mediaFN($image))) {
@ -1071,7 +1071,7 @@ function media_preview_buttons($image, $auth, $rev = '') {
$form->addTagClose('div');
echo '<li>';
echo $form->toHTML();
echo '</li>'.DOKU_LF;
echo '</li>';
}
$auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
@ -1087,7 +1087,7 @@ function media_preview_buttons($image, $auth, $rev = '') {
$form->addTagClose('div');
echo '<li>';
echo $form->toHTML();
echo '</li>'.DOKU_LF;
echo '</li>';
}
if ($auth >= AUTH_UPLOAD && $rev && $conf['mediarevisions'] && file_exists(mediaFN($image, $rev))) {
@ -1104,10 +1104,10 @@ function media_preview_buttons($image, $auth, $rev = '') {
$form->addTagClose('div');
echo '<li>';
echo $form->toHTML();
echo '</li>'.DOKU_LF;
echo '</li>';
}
echo '</ul>'.DOKU_LF;
echo '</ul>';
}
/**
@ -1118,16 +1118,18 @@ function media_preview_buttons($image, $auth, $rev = '') {
* @param int|string $rev
* @param JpegMeta|bool $meta
* @param int $size
* @return array|false
* @return array
*/
function media_image_preview_size($image, $rev, $meta, $size = 500) {
if (!preg_match("/\.(jpe?g|gif|png)$/", $image) || !file_exists(mediaFN($image, $rev))) return false;
function media_image_preview_size($image, $rev, $meta = false, $size = 500) {
if (!preg_match("/\.(jpe?g|gif|png)$/", $image)
|| !file_exists($filename = mediaFN($image, $rev))
) return array();
$info = getimagesize(mediaFN($image, $rev));
$info = getimagesize($filename);
$w = (int) $info[0];
$h = (int) $info[1];
if($meta && ($w > $size || $h > $size)){
if ($meta && ($w > $size || $h > $size)) {
$ratio = $meta->getResizeRatio($size, $size);
$w = floor($w * $ratio);
$h = floor($h * $ratio);
@ -1145,10 +1147,10 @@ function media_image_preview_size($image, $rev, $meta, $size = 500) {
* @param string $alt alternative value
* @return string
*/
function media_getTag($tags,$meta,$alt=''){
if($meta === false) return $alt;
function media_getTag($tags, $meta = false, $alt = '') {
if (!$meta) return $alt;
$info = $meta->getField($tags);
if($info == false) return $alt;
if (!$info) return $alt;
return $info;
}
@ -1163,19 +1165,19 @@ function media_getTag($tags,$meta,$alt=''){
function media_file_tags($meta) {
// load the field descriptions
static $fields = null;
if(is_null($fields)){
if (is_null($fields)) {
$config_files = getConfigFiles('mediameta');
foreach ($config_files as $config_file) {
if(file_exists($config_file)) include($config_file);
if (file_exists($config_file)) include($config_file);
}
}
$tags = array();
foreach($fields as $key => $tag){
foreach ($fields as $key => $tag) {
$t = array();
if (!empty($tag[0])) $t = array($tag[0]);
if(isset($tag[3]) && is_array($tag[3])) $t = array_merge($t,$tag[3]);
if (isset($tag[3]) && is_array($tag[3])) $t = array_merge($t,$tag[3]);
$value = media_getTag($t, $meta);
$tags[] = array('tag' => $tag, 'value' => $value);
}
@ -1234,65 +1236,10 @@ function media_details($image, $auth, $rev='', $meta=false) {
* @param int $auth permission level
* @param bool $fromajax
* @return false|null|string
* @deprecated 2020-12-31
*/
function media_diff($image, $ns, $auth, $fromajax = false) {
global $conf;
global $INPUT;
if ($auth < AUTH_READ || !$image || !$conf['mediarevisions']) return '';
$rev1 = $INPUT->int('rev');
$rev2 = $INPUT->ref('rev2');
if(is_array($rev2)){
$rev1 = (int) $rev2[0];
$rev2 = (int) $rev2[1];
if(!$rev1){
$rev1 = $rev2;
unset($rev2);
}
}else{
$rev2 = $INPUT->int('rev2');
}
if ($rev1 && !file_exists(mediaFN($image, $rev1))) $rev1 = false;
if ($rev2 && !file_exists(mediaFN($image, $rev2))) $rev2 = false;
if($rev1 && $rev2){ // two specific revisions wanted
// make sure order is correct (older on the left)
if($rev1 < $rev2){
$l_rev = $rev1;
$r_rev = $rev2;
}else{
$l_rev = $rev2;
$r_rev = $rev1;
}
}elseif($rev1){ // single revision given, compare to current
$r_rev = '';
$l_rev = $rev1;
}else{ // no revision was given, compare previous to current
$r_rev = '';
$medialog = new MediaChangeLog($image);
$revs = $medialog->getRevisions(0, 1);
if (file_exists(mediaFN($image, $revs[0]))) {
$l_rev = $revs[0];
} else {
$l_rev = '';
}
}
// prepare event data
$data = array();
$data[0] = $image;
$data[1] = $l_rev;
$data[2] = $r_rev;
$data[3] = $ns;
$data[4] = $auth;
$data[5] = $fromajax;
// trigger event
return Event::createAndTrigger('MEDIA_DIFF', $data, '_media_file_diff', true);
dbg_deprecated('see '. \dokuwiki\Ui\MediaDiff::class .'::show()');
}
/**
@ -1300,13 +1247,10 @@ function media_diff($image, $ns, $auth, $fromajax = false) {
*
* @param array $data event data
* @return false|null
* @deprecated 2020-12-31
*/
function _media_file_diff($data) {
if(is_array($data) && count($data)===6) {
media_file_diff($data[0], $data[1], $data[2], $data[3], $data[4], $data[5]);
} else {
return false;
}
dbg_deprecated('see '. \dokuwiki\Ui\MediaDiff::class .'::show()');
}
/**
@ -1320,120 +1264,10 @@ function _media_file_diff($data) {
* @param string $ns
* @param int $auth permission level
* @param bool $fromajax
* @deprecated 2020-12-31
*/
function media_file_diff($image, $l_rev, $r_rev, $ns, $auth, $fromajax) {
global $lang;
global $INPUT;
$l_meta = new JpegMeta(mediaFN($image, $l_rev));
$r_meta = new JpegMeta(mediaFN($image, $r_rev));
$is_img = preg_match('/\.(jpe?g|gif|png)$/', $image);
if ($is_img) {
$l_size = media_image_preview_size($image, $l_rev, $l_meta);
$r_size = media_image_preview_size($image, $r_rev, $r_meta);
$is_img = ($l_size && $r_size && ($l_size[0] >= 30 || $r_size[0] >= 30));
$difftype = $INPUT->str('difftype');
if (!$fromajax) {
$form = new Form([
'id' => 'mediamanager__form_diffview',
'action' => media_managerURL([], '&'),
'method' => 'get',
'class' => 'diffView',
]);
$form->addTagOpen('div')->addClass('no');
$form->setHiddenField('sectok', null);
$form->setHiddenField('mediado', 'diff');
$form->setHiddenField('rev2[0]', $l_rev);
$form->setHiddenField('rev2[1]', $r_rev);
echo $form->toHTML();
echo NL.'<div id="mediamanager__diff" >'.NL;
}
if ($difftype == 'opacity' || $difftype == 'portions') {
media_image_diff($image, $l_rev, $r_rev, $l_size, $r_size, $difftype);
if (!$fromajax) echo '</div>';
return;
}
}
list($l_head, $r_head) = (new dokuwiki\Ui\Diff)->diffHead($l_rev, $r_rev, $image, true);
?>
<div class="table">
<table>
<tr>
<th><?php echo $l_head; ?></th>
<th><?php echo $r_head; ?></th>
</tr>
<?php
echo '<tr class="image">';
echo '<td>';
media_preview($image, $auth, $l_rev, $l_meta);
echo '</td>';
echo '<td>';
media_preview($image, $auth, $r_rev, $r_meta);
echo '</td>';
echo '</tr>'.NL;
echo '<tr class="actions">';
echo '<td>';
media_preview_buttons($image, $auth, $l_rev);
echo '</td>';
echo '<td>';
media_preview_buttons($image, $auth, $r_rev);
echo '</td>';
echo '</tr>'.NL;
$l_tags = media_file_tags($l_meta);
$r_tags = media_file_tags($r_meta);
// FIXME r_tags-only stuff
foreach ($l_tags as $key => $l_tag) {
if ($l_tag['value'] != $r_tags[$key]['value']) {
$r_tags[$key]['highlighted'] = true;
$l_tags[$key]['highlighted'] = true;
} else if (!$l_tag['value'] || !$r_tags[$key]['value']) {
unset($r_tags[$key]);
unset($l_tags[$key]);
}
}
echo '<tr>';
foreach(array($l_tags,$r_tags) as $tags){
echo '<td>'.NL;
echo '<dl class="img_tags">';
foreach($tags as $tag){
$value = cleanText($tag['value']);
if (!$value) $value = '-';
echo '<dt>'.$lang[$tag['tag'][1]].'</dt>';
echo '<dd>';
if ($tag['highlighted']) {
echo '<strong>';
}
if ($tag['tag'][2] == 'date') echo dformat($value);
else echo hsc($value);
if ($tag['highlighted']) {
echo '</strong>';
}
echo '</dd>';
}
echo '</dl>'.NL;
echo '</td>';
}
echo '</tr>'.NL;
echo '</table>'.NL;
echo '</div>'.NL;
if ($is_img && !$fromajax) echo '</div>';
dbg_deprecated('see '. \dokuwiki\Ui\MediaDiff::class .'::showFileDiff()');
}
/**
@ -1448,32 +1282,10 @@ function media_file_diff($image, $l_rev, $r_rev, $ns, $auth, $fromajax) {
* @param array $l_size array with width and height
* @param array $r_size array with width and height
* @param string $type
* @deprecated 2020-12-31
*/
function media_image_diff($image, $l_rev, $r_rev, $l_size, $r_size, $type) {
if ($l_size != $r_size) {
if ($r_size[0] > $l_size[0]) {
$l_size = $r_size;
}
}
$l_more = array('rev' => $l_rev, 'h' => $l_size[1], 'w' => $l_size[0]);
$r_more = array('rev' => $r_rev, 'h' => $l_size[1], 'w' => $l_size[0]);
$l_src = ml($image, $l_more);
$r_src = ml($image, $r_more);
// slider
echo '<div class="slider" style="max-width: '.($l_size[0]-20).'px;" ></div>'.NL;
// two images in divs
echo '<div class="imageDiff ' . $type . '">'.NL;
echo '<div class="image1" style="max-width: '.$l_size[0].'px;">';
echo '<img src="'.$l_src.'" alt="" />';
echo '</div>'.NL;
echo '<div class="image2" style="max-width: '.$l_size[0].'px;">';
echo '<img src="'.$r_src.'" alt="" />';
echo '</div>'.NL;
echo '</div>'.NL;
dbg_deprecated('see '. \dokuwiki\Ui\MediaDiff::class .'::showImageDiff()');
}
/**

View File

@ -266,14 +266,14 @@ function sectionID($title,&$check) {
* @param bool $date_at
* @return bool exists?
*/
function page_exists($id,$rev='',$clean=true, $date_at=false) {
if($rev !== '' && $date_at) {
function page_exists($id, $rev = '', $clean = true, $date_at = false) {
if ($rev !== '' && $date_at) {
$pagelog = new PageChangeLog($id);
$pagelog_rev = $pagelog->getLastRevisionAt($rev);
if($pagelog_rev !== false)
if ($pagelog_rev !== false)
$rev = $pagelog_rev;
}
return file_exists(wikiFN($id,$rev,$clean));
return file_exists(wikiFN($id, $rev, $clean));
}
/**
@ -508,18 +508,17 @@ function resolve_id($ns,$id,$clean=true){
* @param int|string $rev
* @param bool $date_at
*/
function resolve_mediaid($ns,&$page,&$exists,$rev='',$date_at=false){
$page = resolve_id($ns,$page);
if($rev !== '' && $date_at){
function resolve_mediaid($ns, &$page, &$exists, $rev = '', $date_at = false) {
$page = resolve_id($ns, $page);
if ($rev !== '' && $date_at) {
$medialog = new MediaChangeLog($page);
$medialog_rev = $medialog->getLastRevisionAt($rev);
if($medialog_rev !== false) {
if ($medialog_rev !== false) {
$rev = $medialog_rev;
}
}
$file = mediaFN($page,$rev);
$exists = file_exists($file);
$exists = file_exists(mediaFN($page, $rev));
}
/**
@ -533,7 +532,7 @@ function resolve_mediaid($ns,&$page,&$exists,$rev='',$date_at=false){
* @param string $rev
* @param bool $date_at
*/
function resolve_pageid($ns,&$page,&$exists,$rev='',$date_at=false ){
function resolve_pageid($ns, &$page, &$exists, $rev = '', $date_at = false) {
global $conf;
global $ID;
$exists = false;
@ -545,56 +544,57 @@ function resolve_pageid($ns,&$page,&$exists,$rev='',$date_at=false ){
//keep hashlink if exists then clean both parts
if (strpos($page,'#')) {
list($page,$hash) = explode('#',$page,2);
list($page,$hash) = explode('#', $page, 2);
} else {
$hash = '';
}
$hash = cleanID($hash);
$page = resolve_id($ns,$page,false); // resolve but don't clean, yet
$page = resolve_id($ns, $page, false); // resolve but don't clean, yet
// get filename (calls clean itself)
if($rev !== '' && $date_at) {
if ($rev !== '' && $date_at) {
$pagelog = new PageChangeLog($page);
$pagelog_rev = $pagelog->getLastRevisionAt($rev);
if($pagelog_rev !== false)//something found
$rev = $pagelog_rev;
if ($pagelog_rev !== false)//something found
$rev = $pagelog_rev;
}
$file = wikiFN($page,$rev);
// if ends with colon or slash we have a namespace link
if(in_array(substr($page,-1), array(':', ';')) ||
($conf['useslash'] && substr($page,-1) == '/')){
if(page_exists($page.$conf['start'],$rev,true,$date_at)){
if (in_array(substr($page, -1), array(':', ';')) ||
($conf['useslash'] && substr($page,-1) == '/')
) {
if (page_exists($page.$conf['start'], $rev, true, $date_at)) {
// start page inside namespace
$page = $page.$conf['start'];
$exists = true;
}elseif(page_exists($page.noNS(cleanID($page)),$rev,true,$date_at)){
} elseif (page_exists($page.noNS(cleanID($page)), $rev, true, $date_at)) {
// page named like the NS inside the NS
$page = $page.noNS(cleanID($page));
$exists = true;
}elseif(page_exists($page,$rev,true,$date_at)){
} elseif (page_exists($page, $rev, true, $date_at)) {
// page like namespace exists
$page = $page;
$exists = true;
}else{
} else {
// fall back to default
$page = $page.$conf['start'];
}
}else{
} else {
//check alternative plural/nonplural form
if(!file_exists($file)){
if( $conf['autoplural'] ){
if(substr($page,-1) == 's'){
$try = substr($page,0,-1);
}else{
if (!file_exists($file)) {
if ($conf['autoplural']) {
if (substr($page, -1) == 's') {
$try = substr($page, 0, -1);
} else {
$try = $page.'s';
}
if(page_exists($try,$rev,true,$date_at)){
$page = $try;
if (page_exists($try, $rev, true, $date_at)) {
$page = $try;
$exists = true;
}
}
}else{
} else {
$exists = true;
}
}
@ -603,7 +603,7 @@ function resolve_pageid($ns,&$page,&$exists,$rev='',$date_at=false ){
$page = cleanID($page);
//add hash if any
if(!empty($hash)) $page .= '#'.$hash;
if (!empty($hash)) $page .= '#'.$hash;
}
/**

View File

@ -1988,10 +1988,10 @@ class Doku_Renderer_xhtml extends Doku_Renderer {
* @access protected
* @return string revision ('' for current)
*/
protected function _getLastMediaRevisionAt($media_id){
if(!$this->date_at || media_isexternal($media_id)) return '';
$pagelog = new MediaChangeLog($media_id);
return $pagelog->getLastRevisionAt($this->date_at);
protected function _getLastMediaRevisionAt($media_id) {
if (!$this->date_at || media_isexternal($media_id)) return '';
$changelog = new MediaChangeLog($media_id);
return $changelog->getLastRevisionAt($this->date_at);
}
#endregion