123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- <?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);
- }
- }
|