[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/File/ -> PageFile.php (source)

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