335 lines
13 KiB
PHP
335 lines
13 KiB
PHP
<?php
|
|
|
|
namespace dokuwiki\File;
|
|
|
|
use dokuwiki\Cache\CacheInstructions;
|
|
use dokuwiki\ChangeLog\PageChangeLog;
|
|
use dokuwiki\Extension\Event;
|
|
use dokuwiki\Input\Input;
|
|
use dokuwiki\Logger;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* 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|void 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' => ($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 = "PageFile($this->id)::detectExternalEdit(): timestamp successfully modified";
|
|
$details = '('.$wrong_timestamp.' -> '.$revInfo['date'].')';
|
|
Logger::error($msg, $details, $fileLastMod);
|
|
} else {
|
|
// runtime error
|
|
$msg = "PageFile($this->id)::detectExternalEdit(): 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
|
|
$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);
|
|
}
|
|
|
|
}
|