xref: /dokuwiki/inc/ChangeLog/RevisionInfo.php (revision 6ebc01d8a66de014ca45b97a9ac3392dc9077e40)
1<?php
2
3namespace dokuwiki\ChangeLog;
4
5/**
6 * Class RevisionInfo
7 *
8 * Provides methods to show Revision Information in DokuWiki Ui components:
9 *  - Ui\Recent
10 *  - Ui\PageRevisions
11 *  - Ui\MediaRevisions
12 *  - Ui\PageDiff
13 *  - Ui\MediaDiff
14 */
15class RevisionInfo
16{
17    public const MODE_PAGE = 'page';
18    public const MODE_MEDIA = 'media';
19
20    /* @var array */
21    protected $info;
22
23    /**
24     * Constructor
25     *
26     * @param array $info Revision Information structure with entries:
27     *      - date:  unix timestamp
28     *      - ip:    IPv4 or IPv6 address
29     *      - type:  change type (log line type)
30     *      - id:    page id
31     *      - user:  user name
32     *      - sum:   edit summary (or action reason)
33     *      - extra: extra data (varies by line type)
34     *      - sizechange: change of filesize
35     *      additionally,
36     *      - current:   (optional) whether current revision or not
37     *      - timestamp: (optional) set only when external edits occurred
38     *      - mode:  (internal use) ether "media" or "page"
39     */
40    public function __construct($info = null)
41    {
42        if (!is_array($info) || !isset($info['id'])) {
43            $info = [
44                'mode' => self::MODE_PAGE,
45                'date' => false,
46            ];
47        }
48        $this->info = $info;
49    }
50
51    /**
52     * Set or return whether this revision is current page or media file
53     *
54     * This method does not check exactly whether the revision is current or not. Instead,
55     * set value of associated "current" key for internal use. Some UI element like diff
56     * link button depend on relation to current page or media file. A changelog line does
57     * not indicate whether it corresponds to current page or media file.
58     *
59     * @param bool $value true if the revision is current, otherwise false
60     * @return bool
61     */
62    public function isCurrent($value = null)
63    {
64        return (bool) $this->val('current', $value);
65    }
66
67    /**
68     * Return or set a value of associated key of revision information
69     * but does not allow to change values of existing keys
70     *
71     * @param string $key
72     * @param mixed $value
73     * @return string|null
74     */
75    public function val($key, $value = null)
76    {
77        if (isset($value) && !array_key_exists($key, $this->info)) {
78            // setter, only for new keys
79            $this->info[$key] = $value;
80        }
81        if (array_key_exists($key, $this->info)) {
82            // getter
83            return $this->info[$key];
84        }
85        return null;
86    }
87
88    /**
89     * Set extra key-value to the revision information
90     * but does not allow to change values of existing keys
91     * @param array $info
92     * @return void
93     */
94    public function append(array $info)
95    {
96        foreach ($info as $key => $value) {
97            $this->val($key, $value);
98        }
99    }
100
101
102    /**
103     * file icon of the page or media file
104     * used in [Ui\recent]
105     *
106     * @return string
107     */
108    public function showFileIcon()
109    {
110        $id = $this->val('id');
111        if ($this->val('mode') == self::MODE_MEDIA) {
112            // media file revision
113            return media_printicon($id);
114        } elseif ($this->val('mode') == self::MODE_PAGE) {
115            // page revision
116            return '<img class="icon" src="' . DOKU_BASE . 'lib/images/fileicons/file.png" alt="' . $id . '" />';
117        }
118    }
119
120    /**
121     * edit date and time of the page or media file
122     * used in [Ui\recent, Ui\Revisions]
123     *
124     * @param bool $checkTimestamp  enable timestamp check, alter formatted string when timestamp is false
125     * @return string
126     */
127    public function showEditDate($checkTimestamp = false)
128    {
129        $formatted = dformat($this->val('date'));
130        if ($checkTimestamp && $this->val('timestamp') === false) {
131            // exact date is unknown for externally deleted file
132            // when unknown, alter formatted string "YYYY-mm-DD HH:MM" to "____-__-__ __:__"
133            $formatted = preg_replace('/[0-9a-zA-Z]/', '_', $formatted);
134        }
135        return '<span class="date">' . $formatted . '</span>';
136    }
137
138    /**
139     * edit summary
140     * used in [Ui\recent, Ui\Revisions]
141     *
142     * @return string
143     */
144    public function showEditSummary()
145    {
146        return '<span class="sum">' . ' – ' . hsc($this->val('sum')) . '</span>';
147    }
148
149    /**
150     * editor of the page or media file
151     * used in [Ui\recent, Ui\Revisions]
152     *
153     * @return string
154     */
155    public function showEditor()
156    {
157        if ($this->val('user')) {
158            $html = '<bdi>' . editorinfo($this->val('user')) . '</bdi>';
159            if (auth_ismanager()) {
160                $html .= ' <bdo dir="ltr">(' . $this->val('ip') . ')</bdo>';
161            }
162        } else {
163            $html = '<bdo dir="ltr">' . $this->val('ip') . '</bdo>';
164        }
165        return '<span class="user">' . $html . '</span>';
166    }
167
168    /**
169     * name of the page or media file
170     * used in [Ui\recent, Ui\Revisions]
171     *
172     * @return string
173     */
174    public function showFileName()
175    {
176        $id = $this->val('id');
177        $rev = $this->isCurrent() ? '' : $this->val('date');
178
179        if ($this->val('mode') == self::MODE_MEDIA) {
180            // media file revision
181            $params = ['tab_details' => 'view', 'ns' => getNS($id), 'image' => $id];
182            if ($rev) $params += ['rev' => $rev];
183            $href = media_managerURL($params, '&');
184            $display_name = $id;
185            $exists = file_exists(mediaFN($id, $rev));
186        } elseif ($this->val('mode') == self::MODE_PAGE) {
187            // page revision
188            $params = $rev ? ['rev' => $rev] : [];
189            $href = wl($id, $params, false, '&');
190            $display_name = useHeading('navigation') ? hsc(p_get_first_heading($id)) : $id;
191            if (!$display_name) $display_name = $id;
192            $exists = page_exists($id, $rev);
193        }
194
195        if ($exists) {
196            $class = 'wikilink1';
197        } elseif ($this->isCurrent()) {
198            //show only not-existing link for current page, which allows for directly create a new page/upload
199            $class = 'wikilink2';
200        } else {
201            //revision is not in attic
202            return $display_name;
203        }
204        if ($this->val('type') == DOKU_CHANGE_TYPE_DELETE) {
205            $class = 'wikilink2';
206        }
207        return '<a href="' . $href . '" class="' . $class . '">' . $display_name . '</a>';
208    }
209
210    /**
211     * Revision Title for PageDiff table headline
212     *
213     * @return string
214     */
215    public function showRevisionTitle()
216    {
217        global $lang;
218
219        if (!$this->val('date')) return '&mdash;';
220
221        $id = $this->val('id');
222        $rev = $this->isCurrent() ? '' : $this->val('date');
223        $params = ($rev) ? ['rev' => $rev] : [];
224
225        // revision info may have timestamp key when external edits occurred
226        $date = ($this->val('timestamp') === false)
227            ? $lang['unknowndate']
228            : dformat($this->val('date'));
229
230
231        if ($this->val('mode') == self::MODE_MEDIA) {
232            // media file revision
233            $href = ml($id, $params, false, '&');
234            $exists = file_exists(mediaFN($id, $rev));
235        } elseif ($this->val('mode') == self::MODE_PAGE) {
236            // page revision
237            $href = wl($id, $params, false, '&');
238            $exists = page_exists($id, $rev);
239        }
240        if ($exists) {
241            $class = 'wikilink1';
242        } elseif ($this->isCurrent()) {
243            //show only not-existing link for current page, which allows for directly create a new page/upload
244            $class = 'wikilink2';
245        } else {
246            //revision is not in attic
247            return $id . ' [' . $date . ']';
248        }
249        if ($this->val('type') == DOKU_CHANGE_TYPE_DELETE) {
250            $class = 'wikilink2';
251        }
252        return '<bdi><a class="' . $class . '" href="' . $href . '">' . $id . ' [' . $date . ']' . '</a></bdi>';
253    }
254
255    /**
256     * diff link icon in recent changes list, to compare (this) current revision with previous one
257     * all items in "recent changes" are current revision of the page or media
258     *
259     * @return string
260     */
261    public function showIconCompareWithPrevious()
262    {
263        global $lang;
264
265        if (!actionOK('diff')) {
266            return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />';
267        }
268
269        $id = $this->val('id');
270
271        $href = '';
272        if ($this->val('mode') == self::MODE_MEDIA) {
273            // media file revision
274            // unlike page, media file does not copied to media_attic when uploaded.
275            // diff icon will not be shown when external edit occurred
276            // because no attic file to be compared with current.
277            $revs = (new MediaChangeLog($id))->getRevisions(0, 1);
278            $showLink = (count($revs) && file_exists(mediaFN($id, $revs[0])) && file_exists(mediaFN($id)));
279            if ($showLink) {
280                $param = ['tab_details' => 'history', 'mediado' => 'diff', 'ns' => getNS($id), 'image' => $id];
281                $href = media_managerURL($param, '&');
282            }
283        } elseif ($this->val('mode') == self::MODE_PAGE) {
284            // page revision
285            // when a page just created anyway, it is natural to expect no older revisions
286            // even if it had once existed but deleted before. Simply ignore to check changelog.
287            if ($this->val('type') !== DOKU_CHANGE_TYPE_CREATE) {
288                $href = wl($id, ['do' => 'diff'], false, '&');
289            }
290        }
291
292        if ($href) {
293            return '<a href="' . $href . '" class="diff_link">'
294                  . '<img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"'
295                  . ' title="' . $lang['diff'] . '" alt="' . $lang['diff'] . '" />'
296                  . '</a>';
297        } else {
298            return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />';
299        }
300    }
301
302    /**
303     * diff link icon in revisions list, compare this revision with current one
304     * the icon does not displayed for the current revision
305     *
306     * @return string
307     */
308    public function showIconCompareWithCurrent()
309    {
310        global $lang;
311
312        if (!actionOK('diff')) {
313            return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />';
314        }
315
316        $id = $this->val('id');
317        $rev = $this->isCurrent() ? '' : $this->val('date');
318
319        $href = '';
320        if ($this->val('mode') == self::MODE_MEDIA) {
321            // media file revision
322            if (!$this->isCurrent() && file_exists(mediaFN($id, $rev))) {
323                $param = ['mediado' => 'diff', 'image' => $id, 'rev' => $rev];
324                $href = media_managerURL($param, '&');
325            }
326        } elseif ($this->val('mode') == self::MODE_PAGE) {
327            // page revision
328            if (!$this->isCurrent()) {
329                $href = wl($id, ['rev' => $rev, 'do' => 'diff'], false, '&');
330            }
331        }
332
333        if ($href) {
334            return '<a href="' . $href . '" class="diff_link">'
335                  . '<img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"'
336                  . ' title="' . $lang['diff'] . '" alt="' . $lang['diff'] . '" />'
337                  . '</a>';
338        } else {
339            return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />';
340        }
341    }
342
343    /**
344     * icon for revision action
345     * used in [Ui\recent]
346     *
347     * @return string
348     */
349    public function showIconRevisions()
350    {
351        global $lang;
352
353        if (!actionOK('revisions')) {
354            return '';
355        }
356
357        $id = $this->val('id');
358        if ($this->val('mode') == self::MODE_MEDIA) {
359            // media file revision
360            $param  = ['tab_details' => 'history', 'ns' => getNS($id), 'image' => $id];
361            $href = media_managerURL($param, '&');
362        } elseif ($this->val('mode') == self::MODE_PAGE) {
363            // page revision
364            $href = wl($id, ['do' => 'revisions'], false, '&');
365        }
366        return '<a href="' . $href . '" class="revisions_link">'
367              . '<img src="' . DOKU_BASE . 'lib/images/history.png" width="12" height="14"'
368              . ' title="' . $lang['btn_revs'] . '" alt="' . $lang['btn_revs'] . '" />'
369              . '</a>';
370    }
371
372    /**
373     * size change
374     * used in [Ui\recent, Ui\Revisions]
375     *
376     * @return string
377     */
378    public function showSizeChange()
379    {
380        $class = 'sizechange';
381        $value = filesize_h(abs($this->val('sizechange')));
382        if ($this->val('sizechange') > 0) {
383            $class .= ' positive';
384            $value = '+' . $value;
385        } elseif ($this->val('sizechange') < 0) {
386            $class .= ' negative';
387            $value = '-' . $value;
388        } else {
389            $value = '±' . $value;
390        }
391        return '<span class="' . $class . '">' . $value . '</span>';
392    }
393
394    /**
395     * current indicator, used in revision list
396     * not used in Ui\Recent because recent files are always current one
397     *
398     * @return string
399     */
400    public function showCurrentIndicator()
401    {
402        global $lang;
403        return $this->isCurrent() ? '(' . $lang['current'] . ')' : '';
404    }
405}
406