xref: /dokuwiki/inc/File/PageFile.php (revision 01e8d739c8b53aeb1d0a653331d65eb1f8394002)
1b24e9c4aSSatoshi Sahara<?php
2b24e9c4aSSatoshi Sahara
3b24e9c4aSSatoshi Saharanamespace dokuwiki\File;
4b24e9c4aSSatoshi Sahara
5b24e9c4aSSatoshi Saharause dokuwiki\ChangeLog\PageChangeLog;
6b24e9c4aSSatoshi Saharause dokuwiki\Extension\Event;
779a2d784SGerrit Uitslaguse dokuwiki\Input\Input;
8b24e9c4aSSatoshi Sahara
9b24e9c4aSSatoshi Sahara/**
10b24e9c4aSSatoshi Sahara * Class PageFile : handles wiki text file and its change management for specific page
11b24e9c4aSSatoshi Sahara */
12b24e9c4aSSatoshi Saharaclass PageFile
13b24e9c4aSSatoshi Sahara{
14b24e9c4aSSatoshi Sahara    protected $id;
15b24e9c4aSSatoshi Sahara
16b24e9c4aSSatoshi Sahara    /* @var PageChangeLog $changelog */
17b24e9c4aSSatoshi Sahara    public $changelog;
18b24e9c4aSSatoshi Sahara
19b24e9c4aSSatoshi Sahara    /* @var array $data  initial data when event COMMON_WIKIPAGE_SAVE triggered */
20b24e9c4aSSatoshi Sahara    protected $data;
21b24e9c4aSSatoshi Sahara
22b24e9c4aSSatoshi Sahara    /**
23b24e9c4aSSatoshi Sahara     * PageFile constructor.
24b24e9c4aSSatoshi Sahara     *
25b24e9c4aSSatoshi Sahara     * @param string $id
26b24e9c4aSSatoshi Sahara     */
27b24e9c4aSSatoshi Sahara    public function __construct($id)
28b24e9c4aSSatoshi Sahara    {
29b24e9c4aSSatoshi Sahara        $this->id = $id;
30b24e9c4aSSatoshi Sahara        $this->changelog = new PageChangeLog($this->id);
31b24e9c4aSSatoshi Sahara    }
32b24e9c4aSSatoshi Sahara
33b24e9c4aSSatoshi Sahara    /** @return string */
34b24e9c4aSSatoshi Sahara    public function getId()
35b24e9c4aSSatoshi Sahara    {
36b24e9c4aSSatoshi Sahara        return $this->id;
37b24e9c4aSSatoshi Sahara    }
38b24e9c4aSSatoshi Sahara
39b24e9c4aSSatoshi Sahara    /** @return string */
40b24e9c4aSSatoshi Sahara    public function getPath($rev = '')
41b24e9c4aSSatoshi Sahara    {
42b24e9c4aSSatoshi Sahara        return wikiFN($this->id, $rev);
43b24e9c4aSSatoshi Sahara    }
44b24e9c4aSSatoshi Sahara
45b24e9c4aSSatoshi Sahara    /**
46b24e9c4aSSatoshi Sahara     * Get raw WikiText of the page, considering change type at revision date
47b24e9c4aSSatoshi Sahara     * similar to function rawWiki($id, $rev = '')
48b24e9c4aSSatoshi Sahara     *
49b24e9c4aSSatoshi Sahara     * @param int|false $rev  timestamp when a revision of wikitext is desired
50b24e9c4aSSatoshi Sahara     * @return string
51b24e9c4aSSatoshi Sahara     */
52b24e9c4aSSatoshi Sahara    public function rawWikiText($rev = null)
53b24e9c4aSSatoshi Sahara    {
54b24e9c4aSSatoshi Sahara        if ($rev !== null) {
55b24e9c4aSSatoshi Sahara            $revInfo = $rev ? $this->changelog->getRevisionInfo($rev) : false;
56b24e9c4aSSatoshi Sahara            return (!$revInfo || $revInfo['type'] == DOKU_CHANGE_TYPE_DELETE)
57b24e9c4aSSatoshi Sahara                ? '' // attic stores complete last page version for a deleted page
58b24e9c4aSSatoshi Sahara                : io_readWikiPage($this->getPath($rev), $this->id, $rev); // retrieve from attic
59b24e9c4aSSatoshi Sahara        } else {
60b24e9c4aSSatoshi Sahara            return io_readWikiPage($this->getPath(), $this->id, '');
61b24e9c4aSSatoshi Sahara        }
62b24e9c4aSSatoshi Sahara    }
63b24e9c4aSSatoshi Sahara
64b24e9c4aSSatoshi Sahara    /**
65b24e9c4aSSatoshi Sahara     * Saves a wikitext by calling io_writeWikiPage.
66b24e9c4aSSatoshi Sahara     * Also directs changelog and attic updates.
67b24e9c4aSSatoshi Sahara     *
68b24e9c4aSSatoshi Sahara     * @author Andreas Gohr <andi@splitbrain.org>
69b24e9c4aSSatoshi Sahara     * @author Ben Coburn <btcoburn@silicodon.net>
70b24e9c4aSSatoshi Sahara     *
71b24e9c4aSSatoshi Sahara     * @param string $text     wikitext being saved
72b24e9c4aSSatoshi Sahara     * @param string $summary  summary of text update
73b24e9c4aSSatoshi Sahara     * @param bool   $minor    mark this saved version as minor update
7479a2d784SGerrit Uitslag     * @return array|void data of event COMMON_WIKIPAGE_SAVE
75b24e9c4aSSatoshi Sahara     */
76b24e9c4aSSatoshi Sahara    public function saveWikiText($text, $summary, $minor = false)
77b24e9c4aSSatoshi Sahara    {
78b24e9c4aSSatoshi Sahara        /* Note to developers:
79b24e9c4aSSatoshi Sahara           This code is subtle and delicate. Test the behavior of
80b24e9c4aSSatoshi Sahara           the attic and changelog with dokuwiki and external edits
81b24e9c4aSSatoshi Sahara           after any changes. External edits change the wiki page
82b24e9c4aSSatoshi Sahara           directly without using php or dokuwiki.
83b24e9c4aSSatoshi Sahara         */
84b24e9c4aSSatoshi Sahara        global $conf;
85b24e9c4aSSatoshi Sahara        global $lang;
86b24e9c4aSSatoshi Sahara        global $REV;
87b24e9c4aSSatoshi Sahara        /* @var Input $INPUT */
88b24e9c4aSSatoshi Sahara        global $INPUT;
89b24e9c4aSSatoshi Sahara
90b24e9c4aSSatoshi Sahara        // prevent recursive call
91b24e9c4aSSatoshi Sahara        if (isset($this->data)) return;
92b24e9c4aSSatoshi Sahara
93b24e9c4aSSatoshi Sahara        $pagefile = $this->getPath();
94b24e9c4aSSatoshi Sahara        $currentRevision = @filemtime($pagefile);       // int or false
95b24e9c4aSSatoshi Sahara        $currentContent = $this->rawWikiText();
96b24e9c4aSSatoshi Sahara        $currentSize = file_exists($pagefile) ? filesize($pagefile) : 0;
97b24e9c4aSSatoshi Sahara
98b24e9c4aSSatoshi Sahara        // prepare data for event COMMON_WIKIPAGE_SAVE
99445164b2SAndreas Gohr        $data = [
100b24e9c4aSSatoshi Sahara            'id'             => $this->id,// should not be altered by any handlers
101b24e9c4aSSatoshi Sahara            'file'           => $pagefile,// same above
102b24e9c4aSSatoshi Sahara            'changeType'     => null,// set prior to event, and confirm later
103b24e9c4aSSatoshi Sahara            'revertFrom'     => $REV,
104b24e9c4aSSatoshi Sahara            'oldRevision'    => $currentRevision,
105b24e9c4aSSatoshi Sahara            'oldContent'     => $currentContent,
106b24e9c4aSSatoshi Sahara            'newRevision'    => 0,// only available in the after hook
107b24e9c4aSSatoshi Sahara            'newContent'     => $text,
108b24e9c4aSSatoshi Sahara            'summary'        => $summary,
10979a2d784SGerrit Uitslag            'contentChanged' => ($text != $currentContent),// confirm later
110b24e9c4aSSatoshi Sahara            'changeInfo'     => '',// automatically determined by revertFrom
111445164b2SAndreas Gohr            'sizechange'     => strlen($text) - strlen($currentContent),
112445164b2SAndreas Gohr        ];
113b24e9c4aSSatoshi Sahara
114b24e9c4aSSatoshi Sahara        // determine tentatively change type and relevant elements of event data
115b24e9c4aSSatoshi Sahara        if ($data['revertFrom']) {
116b24e9c4aSSatoshi Sahara            // new text may differ from exact revert revision
117b24e9c4aSSatoshi Sahara            $data['changeType'] = DOKU_CHANGE_TYPE_REVERT;
118b24e9c4aSSatoshi Sahara            $data['changeInfo'] = $REV;
119b24e9c4aSSatoshi Sahara        } elseif (trim($data['newContent']) == '') {
120b24e9c4aSSatoshi Sahara            // empty or whitespace only content deletes
121b24e9c4aSSatoshi Sahara            $data['changeType'] = DOKU_CHANGE_TYPE_DELETE;
122b24e9c4aSSatoshi Sahara        } elseif (!file_exists($pagefile)) {
123b24e9c4aSSatoshi Sahara            $data['changeType'] = DOKU_CHANGE_TYPE_CREATE;
124b24e9c4aSSatoshi Sahara        } else {
125b24e9c4aSSatoshi Sahara            // minor edits allowable only for logged in users
126b24e9c4aSSatoshi Sahara            $is_minor_change = ($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER'));
127b24e9c4aSSatoshi Sahara            $data['changeType'] = $is_minor_change
128b24e9c4aSSatoshi Sahara                ? DOKU_CHANGE_TYPE_MINOR_EDIT
129b24e9c4aSSatoshi Sahara                : DOKU_CHANGE_TYPE_EDIT;
130b24e9c4aSSatoshi Sahara        }
131b24e9c4aSSatoshi Sahara
132b24e9c4aSSatoshi Sahara        $this->data = $data;
13336454bb5SSatoshi Sahara        $data['page'] = $this; // allow event handlers to use this class methods
13436454bb5SSatoshi Sahara
135b24e9c4aSSatoshi Sahara        $event = new Event('COMMON_WIKIPAGE_SAVE', $data);
136b24e9c4aSSatoshi Sahara        if (!$event->advise_before()) return;
137b24e9c4aSSatoshi Sahara
138b24e9c4aSSatoshi Sahara        // if the content has not been changed, no save happens (plugins may override this)
139b24e9c4aSSatoshi Sahara        if (!$data['contentChanged']) return;
140b24e9c4aSSatoshi Sahara
141b24e9c4aSSatoshi Sahara        // Check whether the pagefile has modified during $event->advise_before()
142b24e9c4aSSatoshi Sahara        clearstatcache();
143b24e9c4aSSatoshi Sahara        $fileRev = @filemtime($pagefile);
144b24e9c4aSSatoshi Sahara        if ($fileRev === $currentRevision) {
145*01e8d739SAndreas Gohr            // pagefile has not been touched by plugin's event handler — trigger external-edit
146*01e8d739SAndreas Gohr            // detection. ChangeLog persists the synthesized entry (and copies to attic for
147*01e8d739SAndreas Gohr            // non-deletes) on first observation, so this also covers the "external edit
148*01e8d739SAndreas Gohr            // happens just before save" case.
149*01e8d739SAndreas Gohr            $revInfo = $this->changelog->getCurrentRevisionInfo();
150*01e8d739SAndreas Gohr            // ensure the upcoming save timestamp is at least 1 sec after a persisted external entry
151*01e8d739SAndreas Gohr            if (is_array($revInfo) && isset($revInfo['date']) && $revInfo['date'] == time()) {
152*01e8d739SAndreas Gohr                sleep(1);
153*01e8d739SAndreas Gohr            }
154b24e9c4aSSatoshi Sahara            $filesize_old = $currentSize;
155b24e9c4aSSatoshi Sahara        } else {
156b24e9c4aSSatoshi Sahara            // pagefile has modified by plugin's event handler, confirm sizechange
157b24e9c4aSSatoshi Sahara            $filesize_old = (
158b24e9c4aSSatoshi Sahara                $data['changeType'] == DOKU_CHANGE_TYPE_CREATE || (
159b24e9c4aSSatoshi Sahara                $data['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($pagefile))
160b24e9c4aSSatoshi Sahara            ) ? 0 : filesize($pagefile);
161b24e9c4aSSatoshi Sahara        }
162b24e9c4aSSatoshi Sahara
163b24e9c4aSSatoshi Sahara        // make change to the current file
164b24e9c4aSSatoshi Sahara        if ($data['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
165b24e9c4aSSatoshi Sahara            // nothing to do when the file has already deleted
166b24e9c4aSSatoshi Sahara            if (!file_exists($pagefile)) return;
167b24e9c4aSSatoshi Sahara            // autoset summary on deletion
168b24e9c4aSSatoshi Sahara            if (blank($data['summary'])) {
169b24e9c4aSSatoshi Sahara                $data['summary'] = $lang['deleted'];
170b24e9c4aSSatoshi Sahara            }
171b24e9c4aSSatoshi Sahara            // send "update" event with empty data, so plugins can react to page deletion
172445164b2SAndreas Gohr            $ioData = [[$pagefile, '', false], getNS($this->id), noNS($this->id), false];
173b24e9c4aSSatoshi Sahara            Event::createAndTrigger('IO_WIKIPAGE_WRITE', $ioData);
174b24e9c4aSSatoshi Sahara            // pre-save deleted revision
175b24e9c4aSSatoshi Sahara            @touch($pagefile);
176b24e9c4aSSatoshi Sahara            clearstatcache();
177b24e9c4aSSatoshi Sahara            $data['newRevision'] = $this->saveOldRevision();
178b24e9c4aSSatoshi Sahara            // remove empty file
179b24e9c4aSSatoshi Sahara            @unlink($pagefile);
180b24e9c4aSSatoshi Sahara            $filesize_new = 0;
181b24e9c4aSSatoshi Sahara            // don't remove old meta info as it should be saved, plugins can use
182b24e9c4aSSatoshi Sahara            // IO_WIKIPAGE_WRITE for removing their metadata...
183b24e9c4aSSatoshi Sahara            // purge non-persistant meta data
184b24e9c4aSSatoshi Sahara            p_purge_metadata($this->id);
185b24e9c4aSSatoshi Sahara            // remove empty namespaces
186b24e9c4aSSatoshi Sahara            io_sweepNS($this->id, 'datadir');
187b24e9c4aSSatoshi Sahara            io_sweepNS($this->id, 'mediadir');
188b24e9c4aSSatoshi Sahara        } else {
189b24e9c4aSSatoshi Sahara            // save file (namespace dir is created in io_writeWikiPage)
190b24e9c4aSSatoshi Sahara            io_writeWikiPage($pagefile, $data['newContent'], $this->id);
191b24e9c4aSSatoshi Sahara            // pre-save the revision, to keep the attic in sync
192b24e9c4aSSatoshi Sahara            $data['newRevision'] = $this->saveOldRevision();
193b24e9c4aSSatoshi Sahara            $filesize_new = filesize($pagefile);
194b24e9c4aSSatoshi Sahara        }
195b24e9c4aSSatoshi Sahara        $data['sizechange'] = $filesize_new - $filesize_old;
196b24e9c4aSSatoshi Sahara
197b24e9c4aSSatoshi Sahara        $event->advise_after();
198b24e9c4aSSatoshi Sahara
1997fba736bSSatoshi Sahara        unset($data['page']);
2007fba736bSSatoshi Sahara
201b24e9c4aSSatoshi Sahara        // adds an entry to the changelog and saves the metadata for the page
2027fba736bSSatoshi Sahara        $logEntry = $this->changelog->addLogEntry([
2037fba736bSSatoshi Sahara            'date'       => $data['newRevision'],
2047fba736bSSatoshi Sahara            'ip'         => clientIP(true),
2057fba736bSSatoshi Sahara            'type'       => $data['changeType'],
2067fba736bSSatoshi Sahara            'id'         => $this->id,
2077fba736bSSatoshi Sahara            'user'       => $INPUT->server->str('REMOTE_USER'),
2087fba736bSSatoshi Sahara            'sum'        => $data['summary'],
2097fba736bSSatoshi Sahara            'extra'      => $data['changeInfo'],
2107fba736bSSatoshi Sahara            'sizechange' => $data['sizechange'],
2117fba736bSSatoshi Sahara        ]);
2127fba736bSSatoshi Sahara        // update metadata
2137fba736bSSatoshi Sahara        $this->updateMetadata($logEntry);
214b24e9c4aSSatoshi Sahara
215b24e9c4aSSatoshi Sahara        // update the purgefile (timestamp of the last time anything within the wiki was changed)
216b24e9c4aSSatoshi Sahara        io_saveFile($conf['cachedir'] . '/purgefile', time());
217b24e9c4aSSatoshi Sahara
218b24e9c4aSSatoshi Sahara        return $data;
219b24e9c4aSSatoshi Sahara    }
220b24e9c4aSSatoshi Sahara
221b24e9c4aSSatoshi Sahara    /**
222b24e9c4aSSatoshi Sahara     * Moves the current version to the attic and returns its revision date
223b24e9c4aSSatoshi Sahara     *
224b24e9c4aSSatoshi Sahara     * @author Andreas Gohr <andi@splitbrain.org>
225b24e9c4aSSatoshi Sahara     *
226b24e9c4aSSatoshi Sahara     * @return int|string revision timestamp
227b24e9c4aSSatoshi Sahara     */
228b24e9c4aSSatoshi Sahara    public function saveOldRevision()
229b24e9c4aSSatoshi Sahara    {
230b24e9c4aSSatoshi Sahara        $oldfile = $this->getPath();
231b24e9c4aSSatoshi Sahara        if (!file_exists($oldfile)) return '';
232b24e9c4aSSatoshi Sahara        $date = filemtime($oldfile);
233b24e9c4aSSatoshi Sahara        $newfile = $this->getPath($date);
234b24e9c4aSSatoshi Sahara        io_writeWikiPage($newfile, $this->rawWikiText(), $this->id, $date);
235b24e9c4aSSatoshi Sahara        return $date;
236b24e9c4aSSatoshi Sahara    }
237b24e9c4aSSatoshi Sahara
2387fba736bSSatoshi Sahara    /**
2397fba736bSSatoshi Sahara     * Update metadata of changed page
2407fba736bSSatoshi Sahara     *
2417fba736bSSatoshi Sahara     * @param array $logEntry  changelog entry
2427fba736bSSatoshi Sahara     */
2437fba736bSSatoshi Sahara    public function updateMetadata(array $logEntry)
2447fba736bSSatoshi Sahara    {
2457fba736bSSatoshi Sahara        global $INFO;
2467fba736bSSatoshi Sahara
247445164b2SAndreas Gohr        ['date' => $date, 'type' => $changeType, 'user' => $user, ] = $logEntry;
2487fba736bSSatoshi Sahara
2497fba736bSSatoshi Sahara        $wasRemoved   = ($changeType === DOKU_CHANGE_TYPE_DELETE);
2507fba736bSSatoshi Sahara        $wasCreated   = ($changeType === DOKU_CHANGE_TYPE_CREATE);
2517fba736bSSatoshi Sahara        $wasReverted  = ($changeType === DOKU_CHANGE_TYPE_REVERT);
2527fba736bSSatoshi Sahara        $wasMinorEdit = ($changeType === DOKU_CHANGE_TYPE_MINOR_EDIT);
2537fba736bSSatoshi Sahara
25497b27cd4SSatoshi Sahara        $createdDate = @filectime($this->getPath());
2557fba736bSSatoshi Sahara
2567fba736bSSatoshi Sahara        if ($wasRemoved) return;
2577fba736bSSatoshi Sahara
2587fba736bSSatoshi Sahara        $oldmeta = p_read_metadata($this->id)['persistent'];
259445164b2SAndreas Gohr        $meta    = [];
2607fba736bSSatoshi Sahara
2617d34963bSAndreas Gohr        if (
2627d34963bSAndreas Gohr            $wasCreated &&
26397b27cd4SSatoshi Sahara            (empty($oldmeta['date']['created']) || $oldmeta['date']['created'] === $createdDate)
2647fba736bSSatoshi Sahara        ) {
2657fba736bSSatoshi Sahara            // newly created
26697b27cd4SSatoshi Sahara            $meta['date']['created'] = $createdDate;
2677fba736bSSatoshi Sahara            if ($user) {
26897b27cd4SSatoshi Sahara                $meta['creator'] = $INFO['userinfo']['name'] ?? null;
2697fba736bSSatoshi Sahara                $meta['user']    = $user;
2707fba736bSSatoshi Sahara            }
2717fba736bSSatoshi Sahara        } elseif (($wasCreated || $wasReverted) && !empty($oldmeta['date']['created'])) {
2727fba736bSSatoshi Sahara            // re-created / restored
2737fba736bSSatoshi Sahara            $meta['date']['created']  = $oldmeta['date']['created'];
27497b27cd4SSatoshi Sahara            $meta['date']['modified'] = $createdDate; // use the files ctime here
27597b27cd4SSatoshi Sahara            $meta['creator'] = $oldmeta['creator'] ?? null;
2767fba736bSSatoshi Sahara            if ($user) {
27797b27cd4SSatoshi Sahara                $meta['contributor'][$user] = $INFO['userinfo']['name'] ?? null;
2787fba736bSSatoshi Sahara            }
2797fba736bSSatoshi Sahara        } elseif (!$wasMinorEdit) {   // non-minor modification
2807fba736bSSatoshi Sahara            $meta['date']['modified'] = $date;
2817fba736bSSatoshi Sahara            if ($user) {
28297b27cd4SSatoshi Sahara                $meta['contributor'][$user] = $INFO['userinfo']['name'] ?? null;
2837fba736bSSatoshi Sahara            }
2847fba736bSSatoshi Sahara        }
2857fba736bSSatoshi Sahara        $meta['last_change'] = $logEntry;
2867fba736bSSatoshi Sahara        p_set_metadata($this->id, $meta);
2877fba736bSSatoshi Sahara    }
288b24e9c4aSSatoshi Sahara}
289