1*b24e9c4aSSatoshi Sahara<?php 2*b24e9c4aSSatoshi Sahara 3*b24e9c4aSSatoshi Saharanamespace dokuwiki\File; 4*b24e9c4aSSatoshi Sahara 5*b24e9c4aSSatoshi Saharause dokuwiki\Cache\CacheInstructions; 6*b24e9c4aSSatoshi Saharause dokuwiki\ChangeLog\PageChangeLog; 7*b24e9c4aSSatoshi Saharause dokuwiki\Extension\Event; 8*b24e9c4aSSatoshi Saharause dokuwiki\Logger; 9*b24e9c4aSSatoshi Sahara 10*b24e9c4aSSatoshi Sahara/** 11*b24e9c4aSSatoshi Sahara * Class PageFile : handles wiki text file and its change management for specific page 12*b24e9c4aSSatoshi Sahara */ 13*b24e9c4aSSatoshi Saharaclass PageFile 14*b24e9c4aSSatoshi Sahara{ 15*b24e9c4aSSatoshi Sahara protected $id; 16*b24e9c4aSSatoshi Sahara 17*b24e9c4aSSatoshi Sahara /* @var PageChangeLog $changelog */ 18*b24e9c4aSSatoshi Sahara public $changelog; 19*b24e9c4aSSatoshi Sahara 20*b24e9c4aSSatoshi Sahara /* @var array $data initial data when event COMMON_WIKIPAGE_SAVE triggered */ 21*b24e9c4aSSatoshi Sahara protected $data; 22*b24e9c4aSSatoshi Sahara 23*b24e9c4aSSatoshi Sahara /** 24*b24e9c4aSSatoshi Sahara * PageFile constructor. 25*b24e9c4aSSatoshi Sahara * 26*b24e9c4aSSatoshi Sahara * @param string $id 27*b24e9c4aSSatoshi Sahara */ 28*b24e9c4aSSatoshi Sahara public function __construct($id) 29*b24e9c4aSSatoshi Sahara { 30*b24e9c4aSSatoshi Sahara $this->id = $id; 31*b24e9c4aSSatoshi Sahara $this->changelog = new PageChangeLog($this->id); 32*b24e9c4aSSatoshi Sahara } 33*b24e9c4aSSatoshi Sahara 34*b24e9c4aSSatoshi Sahara /** @return string */ 35*b24e9c4aSSatoshi Sahara public function getId() 36*b24e9c4aSSatoshi Sahara { 37*b24e9c4aSSatoshi Sahara return $this->id; 38*b24e9c4aSSatoshi Sahara } 39*b24e9c4aSSatoshi Sahara 40*b24e9c4aSSatoshi Sahara /** @return string */ 41*b24e9c4aSSatoshi Sahara public function getPath($rev = '') 42*b24e9c4aSSatoshi Sahara { 43*b24e9c4aSSatoshi Sahara return wikiFN($this->id, $rev); 44*b24e9c4aSSatoshi Sahara } 45*b24e9c4aSSatoshi Sahara 46*b24e9c4aSSatoshi Sahara /** 47*b24e9c4aSSatoshi Sahara * Get raw WikiText of the page, considering change type at revision date 48*b24e9c4aSSatoshi Sahara * similar to function rawWiki($id, $rev = '') 49*b24e9c4aSSatoshi Sahara * 50*b24e9c4aSSatoshi Sahara * @param int|false $rev timestamp when a revision of wikitext is desired 51*b24e9c4aSSatoshi Sahara * @return string 52*b24e9c4aSSatoshi Sahara */ 53*b24e9c4aSSatoshi Sahara public function rawWikiText($rev = null) 54*b24e9c4aSSatoshi Sahara { 55*b24e9c4aSSatoshi Sahara if ($rev !== null) { 56*b24e9c4aSSatoshi Sahara $revInfo = $rev ? $this->changelog->getRevisionInfo($rev) : false; 57*b24e9c4aSSatoshi Sahara return (!$revInfo || $revInfo['type'] == DOKU_CHANGE_TYPE_DELETE) 58*b24e9c4aSSatoshi Sahara ? '' // attic stores complete last page version for a deleted page 59*b24e9c4aSSatoshi Sahara : io_readWikiPage($this->getPath($rev), $this->id, $rev); // retrieve from attic 60*b24e9c4aSSatoshi Sahara } else { 61*b24e9c4aSSatoshi Sahara return io_readWikiPage($this->getPath(), $this->id, ''); 62*b24e9c4aSSatoshi Sahara } 63*b24e9c4aSSatoshi Sahara } 64*b24e9c4aSSatoshi Sahara 65*b24e9c4aSSatoshi Sahara /** 66*b24e9c4aSSatoshi Sahara * Saves a wikitext by calling io_writeWikiPage. 67*b24e9c4aSSatoshi Sahara * Also directs changelog and attic updates. 68*b24e9c4aSSatoshi Sahara * 69*b24e9c4aSSatoshi Sahara * @author Andreas Gohr <andi@splitbrain.org> 70*b24e9c4aSSatoshi Sahara * @author Ben Coburn <btcoburn@silicodon.net> 71*b24e9c4aSSatoshi Sahara * 72*b24e9c4aSSatoshi Sahara * @param string $text wikitext being saved 73*b24e9c4aSSatoshi Sahara * @param string $summary summary of text update 74*b24e9c4aSSatoshi Sahara * @param bool $minor mark this saved version as minor update 75*b24e9c4aSSatoshi Sahara */ 76*b24e9c4aSSatoshi Sahara public function saveWikiText($text, $summary, $minor = false) 77*b24e9c4aSSatoshi Sahara { 78*b24e9c4aSSatoshi Sahara /* Note to developers: 79*b24e9c4aSSatoshi Sahara This code is subtle and delicate. Test the behavior of 80*b24e9c4aSSatoshi Sahara the attic and changelog with dokuwiki and external edits 81*b24e9c4aSSatoshi Sahara after any changes. External edits change the wiki page 82*b24e9c4aSSatoshi Sahara directly without using php or dokuwiki. 83*b24e9c4aSSatoshi Sahara */ 84*b24e9c4aSSatoshi Sahara global $conf; 85*b24e9c4aSSatoshi Sahara global $lang; 86*b24e9c4aSSatoshi Sahara global $REV; 87*b24e9c4aSSatoshi Sahara /* @var Input $INPUT */ 88*b24e9c4aSSatoshi Sahara global $INPUT; 89*b24e9c4aSSatoshi Sahara 90*b24e9c4aSSatoshi Sahara // prevent recursive call 91*b24e9c4aSSatoshi Sahara if (isset($this->data)) return; 92*b24e9c4aSSatoshi Sahara 93*b24e9c4aSSatoshi Sahara $pagefile = $this->getPath(); 94*b24e9c4aSSatoshi Sahara $currentRevision = @filemtime($pagefile); // int or false 95*b24e9c4aSSatoshi Sahara $currentContent = $this->rawWikiText(); 96*b24e9c4aSSatoshi Sahara $currentSize = file_exists($pagefile) ? filesize($pagefile) : 0; 97*b24e9c4aSSatoshi Sahara 98*b24e9c4aSSatoshi Sahara // prepare data for event COMMON_WIKIPAGE_SAVE 99*b24e9c4aSSatoshi Sahara $data = array( 100*b24e9c4aSSatoshi Sahara 'id' => $this->id, // should not be altered by any handlers 101*b24e9c4aSSatoshi Sahara 'file' => $pagefile, // same above 102*b24e9c4aSSatoshi Sahara 'changeType' => null, // set prior to event, and confirm later 103*b24e9c4aSSatoshi Sahara 'revertFrom' => $REV, 104*b24e9c4aSSatoshi Sahara 'oldRevision' => $currentRevision, 105*b24e9c4aSSatoshi Sahara 'oldContent' => $currentContent, 106*b24e9c4aSSatoshi Sahara 'newRevision' => 0, // only available in the after hook 107*b24e9c4aSSatoshi Sahara 'newContent' => $text, 108*b24e9c4aSSatoshi Sahara 'summary' => $summary, 109*b24e9c4aSSatoshi Sahara 'contentChanged' => (bool)($text != $currentContent), // confirm later 110*b24e9c4aSSatoshi Sahara 'changeInfo' => '', // automatically determined by revertFrom 111*b24e9c4aSSatoshi Sahara 'sizechange' => strlen($text) - strlen($currentContent), // TBD 112*b24e9c4aSSatoshi Sahara 'page' => $this, // allow handlers to use class methods 113*b24e9c4aSSatoshi Sahara ); 114*b24e9c4aSSatoshi Sahara 115*b24e9c4aSSatoshi Sahara // determine tentatively change type and relevant elements of event data 116*b24e9c4aSSatoshi Sahara if ($data['revertFrom']) { 117*b24e9c4aSSatoshi Sahara // new text may differ from exact revert revision 118*b24e9c4aSSatoshi Sahara $data['changeType'] = DOKU_CHANGE_TYPE_REVERT; 119*b24e9c4aSSatoshi Sahara $data['changeInfo'] = $REV; 120*b24e9c4aSSatoshi Sahara } elseif (trim($data['newContent']) == '') { 121*b24e9c4aSSatoshi Sahara // empty or whitespace only content deletes 122*b24e9c4aSSatoshi Sahara $data['changeType'] = DOKU_CHANGE_TYPE_DELETE; 123*b24e9c4aSSatoshi Sahara } elseif (!file_exists($pagefile)) { 124*b24e9c4aSSatoshi Sahara $data['changeType'] = DOKU_CHANGE_TYPE_CREATE; 125*b24e9c4aSSatoshi Sahara } else { 126*b24e9c4aSSatoshi Sahara // minor edits allowable only for logged in users 127*b24e9c4aSSatoshi Sahara $is_minor_change = ($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')); 128*b24e9c4aSSatoshi Sahara $data['changeType'] = $is_minor_change 129*b24e9c4aSSatoshi Sahara ? DOKU_CHANGE_TYPE_MINOR_EDIT 130*b24e9c4aSSatoshi Sahara : DOKU_CHANGE_TYPE_EDIT; 131*b24e9c4aSSatoshi Sahara } 132*b24e9c4aSSatoshi Sahara 133*b24e9c4aSSatoshi Sahara $this->data = $data; 134*b24e9c4aSSatoshi Sahara $event = new Event('COMMON_WIKIPAGE_SAVE', $data); 135*b24e9c4aSSatoshi Sahara if (!$event->advise_before()) return; 136*b24e9c4aSSatoshi Sahara 137*b24e9c4aSSatoshi Sahara // if the content has not been changed, no save happens (plugins may override this) 138*b24e9c4aSSatoshi Sahara if (!$data['contentChanged']) return; 139*b24e9c4aSSatoshi Sahara 140*b24e9c4aSSatoshi Sahara // Check whether the pagefile has modified during $event->advise_before() 141*b24e9c4aSSatoshi Sahara clearstatcache(); 142*b24e9c4aSSatoshi Sahara $fileRev = @filemtime($pagefile); 143*b24e9c4aSSatoshi Sahara if ($fileRev === $currentRevision) { 144*b24e9c4aSSatoshi Sahara // pagefile has not touched by plugin's event handler 145*b24e9c4aSSatoshi Sahara // add a potential external edit entry to changelog and store it into attic 146*b24e9c4aSSatoshi Sahara $this->detectExternalEdit(); 147*b24e9c4aSSatoshi Sahara $filesize_old = $currentSize; 148*b24e9c4aSSatoshi Sahara } else { 149*b24e9c4aSSatoshi Sahara // pagefile has modified by plugin's event handler, confirm sizechange 150*b24e9c4aSSatoshi Sahara $filesize_old = ( 151*b24e9c4aSSatoshi Sahara $data['changeType'] == DOKU_CHANGE_TYPE_CREATE || ( 152*b24e9c4aSSatoshi Sahara $data['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($pagefile)) 153*b24e9c4aSSatoshi Sahara ) ? 0 : filesize($pagefile); 154*b24e9c4aSSatoshi Sahara } 155*b24e9c4aSSatoshi Sahara 156*b24e9c4aSSatoshi Sahara // make change to the current file 157*b24e9c4aSSatoshi Sahara if ($data['changeType'] == DOKU_CHANGE_TYPE_DELETE) { 158*b24e9c4aSSatoshi Sahara // nothing to do when the file has already deleted 159*b24e9c4aSSatoshi Sahara if (!file_exists($pagefile)) return; 160*b24e9c4aSSatoshi Sahara // autoset summary on deletion 161*b24e9c4aSSatoshi Sahara if (blank($data['summary'])) { 162*b24e9c4aSSatoshi Sahara $data['summary'] = $lang['deleted']; 163*b24e9c4aSSatoshi Sahara } 164*b24e9c4aSSatoshi Sahara // send "update" event with empty data, so plugins can react to page deletion 165*b24e9c4aSSatoshi Sahara $ioData = array([$pagefile, '', false], getNS($id), noNS($id), false); 166*b24e9c4aSSatoshi Sahara Event::createAndTrigger('IO_WIKIPAGE_WRITE', $ioData); 167*b24e9c4aSSatoshi Sahara // pre-save deleted revision 168*b24e9c4aSSatoshi Sahara @touch($pagefile); 169*b24e9c4aSSatoshi Sahara clearstatcache(); 170*b24e9c4aSSatoshi Sahara $data['newRevision'] = $this->saveOldRevision(); 171*b24e9c4aSSatoshi Sahara // remove empty file 172*b24e9c4aSSatoshi Sahara @unlink($pagefile); 173*b24e9c4aSSatoshi Sahara $filesize_new = 0; 174*b24e9c4aSSatoshi Sahara // don't remove old meta info as it should be saved, plugins can use 175*b24e9c4aSSatoshi Sahara // IO_WIKIPAGE_WRITE for removing their metadata... 176*b24e9c4aSSatoshi Sahara // purge non-persistant meta data 177*b24e9c4aSSatoshi Sahara p_purge_metadata($this->id); 178*b24e9c4aSSatoshi Sahara // remove empty namespaces 179*b24e9c4aSSatoshi Sahara io_sweepNS($this->id, 'datadir'); 180*b24e9c4aSSatoshi Sahara io_sweepNS($this->id, 'mediadir'); 181*b24e9c4aSSatoshi Sahara } else { 182*b24e9c4aSSatoshi Sahara // save file (namespace dir is created in io_writeWikiPage) 183*b24e9c4aSSatoshi Sahara io_writeWikiPage($pagefile, $data['newContent'], $this->id); 184*b24e9c4aSSatoshi Sahara // pre-save the revision, to keep the attic in sync 185*b24e9c4aSSatoshi Sahara $data['newRevision'] = $this->saveOldRevision(); 186*b24e9c4aSSatoshi Sahara $filesize_new = filesize($pagefile); 187*b24e9c4aSSatoshi Sahara } 188*b24e9c4aSSatoshi Sahara $data['sizechange'] = $filesize_new - $filesize_old; 189*b24e9c4aSSatoshi Sahara 190*b24e9c4aSSatoshi Sahara // 191*b24e9c4aSSatoshi Sahara $event->advise_after(); 192*b24e9c4aSSatoshi Sahara 193*b24e9c4aSSatoshi Sahara // adds an entry to the changelog and saves the metadata for the page 194*b24e9c4aSSatoshi Sahara addLogEntry( 195*b24e9c4aSSatoshi Sahara $data['newRevision'], 196*b24e9c4aSSatoshi Sahara $this->id, 197*b24e9c4aSSatoshi Sahara $data['changeType'], 198*b24e9c4aSSatoshi Sahara $data['summary'], 199*b24e9c4aSSatoshi Sahara $data['changeInfo'], 200*b24e9c4aSSatoshi Sahara null, 201*b24e9c4aSSatoshi Sahara $data['sizechange'] 202*b24e9c4aSSatoshi Sahara ); 203*b24e9c4aSSatoshi Sahara 204*b24e9c4aSSatoshi Sahara // update the purgefile (timestamp of the last time anything within the wiki was changed) 205*b24e9c4aSSatoshi Sahara io_saveFile($conf['cachedir'].'/purgefile', time()); 206*b24e9c4aSSatoshi Sahara 207*b24e9c4aSSatoshi Sahara return $data; 208*b24e9c4aSSatoshi Sahara } 209*b24e9c4aSSatoshi Sahara 210*b24e9c4aSSatoshi Sahara /** 211*b24e9c4aSSatoshi Sahara * Checks if the current page version is newer than the last entry in the page's changelog. 212*b24e9c4aSSatoshi Sahara * If so, we assume it has been an external edit and we create an attic copy and add a proper 213*b24e9c4aSSatoshi Sahara * changelog line. 214*b24e9c4aSSatoshi Sahara * 215*b24e9c4aSSatoshi Sahara * This check is only executed when the page is about to be saved again from the wiki, 216*b24e9c4aSSatoshi Sahara * triggered in @see saveWikiText() 217*b24e9c4aSSatoshi Sahara */ 218*b24e9c4aSSatoshi Sahara public function detectExternalEdit() 219*b24e9c4aSSatoshi Sahara { 220*b24e9c4aSSatoshi Sahara $revInfo = $this->changelog->getCurrentRevisionInfo(); 221*b24e9c4aSSatoshi Sahara 222*b24e9c4aSSatoshi Sahara // only interested in external revision 223*b24e9c4aSSatoshi Sahara if (empty($revInfo) || !array_key_exists('timestamp', $revInfo)) return; 224*b24e9c4aSSatoshi Sahara 225*b24e9c4aSSatoshi Sahara if ($revInfo['type'] != DOKU_CHANGE_TYPE_DELETE && !$revInfo['timestamp']) { 226*b24e9c4aSSatoshi Sahara // file is older than last revision, that is erroneous/incorrect occurence. 227*b24e9c4aSSatoshi Sahara // try to change file modification time 228*b24e9c4aSSatoshi Sahara $fileLastMod = $this->getPath(); 229*b24e9c4aSSatoshi Sahara $wrong_timestamp = filemtime($fileLastMod); 230*b24e9c4aSSatoshi Sahara if (touch($fileLastMod, $revInfo['date'])) { 231*b24e9c4aSSatoshi Sahara clearstatcache(); 232*b24e9c4aSSatoshi Sahara $msg = "detectExternalEdit($id): timestamp successfully modified"; 233*b24e9c4aSSatoshi Sahara $details = '('.$wrong_timestamp.' -> '.$revInfo['date'].')'; 234*b24e9c4aSSatoshi Sahara Logger::error($msg, $details, $fileLastMod); 235*b24e9c4aSSatoshi Sahara } else { 236*b24e9c4aSSatoshi Sahara // runtime error 237*b24e9c4aSSatoshi Sahara $msg = "detectExternalEdit($id): page file should be newer than last revision " 238*b24e9c4aSSatoshi Sahara .'('.filemtime($fileLastMod).' < '. $this->changelog->lastRevision() .')'; 239*b24e9c4aSSatoshi Sahara throw new \RuntimeException($msg); 240*b24e9c4aSSatoshi Sahara } 241*b24e9c4aSSatoshi Sahara } 242*b24e9c4aSSatoshi Sahara 243*b24e9c4aSSatoshi Sahara // keep at least 1 sec before new page save 244*b24e9c4aSSatoshi Sahara if ($revInfo['date'] == time()) sleep(1); // wait a tick 245*b24e9c4aSSatoshi Sahara 246*b24e9c4aSSatoshi Sahara // store externally edited file to the attic folder 247*b24e9c4aSSatoshi Sahara $this->saveOldRevision(); 248*b24e9c4aSSatoshi Sahara // add a changelog entry for externally edited file 249*b24e9c4aSSatoshi Sahara $revInfo = $this->changelog->addLogEntry($revInfo); 250*b24e9c4aSSatoshi Sahara // remove soon to be stale instructions 251*b24e9c4aSSatoshi Sahara $cache = new CacheInstructions($this->id, $this->getPath()); 252*b24e9c4aSSatoshi Sahara $cache->removeCache(); 253*b24e9c4aSSatoshi Sahara } 254*b24e9c4aSSatoshi Sahara 255*b24e9c4aSSatoshi Sahara /** 256*b24e9c4aSSatoshi Sahara * Moves the current version to the attic and returns its revision date 257*b24e9c4aSSatoshi Sahara * 258*b24e9c4aSSatoshi Sahara * @author Andreas Gohr <andi@splitbrain.org> 259*b24e9c4aSSatoshi Sahara * 260*b24e9c4aSSatoshi Sahara * @return int|string revision timestamp 261*b24e9c4aSSatoshi Sahara */ 262*b24e9c4aSSatoshi Sahara public function saveOldRevision() 263*b24e9c4aSSatoshi Sahara { 264*b24e9c4aSSatoshi Sahara $oldfile = $this->getPath(); 265*b24e9c4aSSatoshi Sahara if (!file_exists($oldfile)) return ''; 266*b24e9c4aSSatoshi Sahara $date = filemtime($oldfile); 267*b24e9c4aSSatoshi Sahara $newfile = $this->getPath($date); 268*b24e9c4aSSatoshi Sahara io_writeWikiPage($newfile, $this->rawWikiText(), $this->id, $date); 269*b24e9c4aSSatoshi Sahara return $date; 270*b24e9c4aSSatoshi Sahara } 271*b24e9c4aSSatoshi Sahara 272*b24e9c4aSSatoshi Sahara} 273