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        $id = $this->val('id');
265
266        $href = '';
267        if ($this->val('mode') == self::MODE_MEDIA) {
268            // media file revision
269            // unlike page, media file does not copied to media_attic when uploaded.
270            // diff icon will not be shown when external edit occurred
271            // because no attic file to be compared with current.
272            $revs = (new MediaChangeLog($id))->getRevisions(0, 1);
273            $showLink = (count($revs) && file_exists(mediaFN($id, $revs[0])) && file_exists(mediaFN($id)));
274            if ($showLink) {
275                $param = ['tab_details' => 'history', 'mediado' => 'diff', 'ns' => getNS($id), 'image' => $id];
276                $href = media_managerURL($param, '&');
277            }
278        } elseif ($this->val('mode') == self::MODE_PAGE) {
279            // page revision
280            // when a page just created anyway, it is natural to expect no older revisions
281            // even if it had once existed but deleted before. Simply ignore to check changelog.
282            if ($this->val('type') !== DOKU_CHANGE_TYPE_CREATE) {
283                $href = wl($id, ['do' => 'diff'], false, '&');
284            }
285        }
286
287        if ($href) {
288            return '<a href="' . $href . '" class="diff_link">'
289                  . '<img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"'
290                  . ' title="' . $lang['diff'] . '" alt="' . $lang['diff'] . '" />'
291                  . '</a>';
292        } else {
293            return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />';
294        }
295    }
296
297    /**
298     * diff link icon in revisions list, compare this revision with current one
299     * the icon does not displayed for the current revision
300     *
301     * @return string
302     */
303    public function showIconCompareWithCurrent()
304    {
305        global $lang;
306        $id = $this->val('id');
307        $rev = $this->isCurrent() ? '' : $this->val('date');
308
309        $href = '';
310        if ($this->val('mode') == self::MODE_MEDIA) {
311            // media file revision
312            if (!$this->isCurrent() && file_exists(mediaFN($id, $rev))) {
313                $param = ['mediado' => 'diff', 'image' => $id, 'rev' => $rev];
314                $href = media_managerURL($param, '&');
315            }
316        } elseif ($this->val('mode') == self::MODE_PAGE) {
317            // page revision
318            if (!$this->isCurrent()) {
319                $href = wl($id, ['rev' => $rev, 'do' => 'diff'], false, '&');
320            }
321        }
322
323        if ($href) {
324            return '<a href="' . $href . '" class="diff_link">'
325                  . '<img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"'
326                  . ' title="' . $lang['diff'] . '" alt="' . $lang['diff'] . '" />'
327                  . '</a>';
328        } else {
329            return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />';
330        }
331    }
332
333    /**
334     * icon for revision action
335     * used in [Ui\recent]
336     *
337     * @return string
338     */
339    public function showIconRevisions()
340    {
341        global $lang;
342
343        if (!actionOK('revisions')) {
344            return '';
345        }
346
347        $id = $this->val('id');
348        if ($this->val('mode') == self::MODE_MEDIA) {
349            // media file revision
350            $param  = ['tab_details' => 'history', 'ns' => getNS($id), 'image' => $id];
351            $href = media_managerURL($param, '&');
352        } elseif ($this->val('mode') == self::MODE_PAGE) {
353            // page revision
354            $href = wl($id, ['do' => 'revisions'], false, '&');
355        }
356        return '<a href="' . $href . '" class="revisions_link">'
357              . '<img src="' . DOKU_BASE . 'lib/images/history.png" width="12" height="14"'
358              . ' title="' . $lang['btn_revs'] . '" alt="' . $lang['btn_revs'] . '" />'
359              . '</a>';
360    }
361
362    /**
363     * size change
364     * used in [Ui\recent, Ui\Revisions]
365     *
366     * @return string
367     */
368    public function showSizeChange()
369    {
370        $class = 'sizechange';
371        $value = filesize_h(abs($this->val('sizechange')));
372        if ($this->val('sizechange') > 0) {
373            $class .= ' positive';
374            $value = '+' . $value;
375        } elseif ($this->val('sizechange') < 0) {
376            $class .= ' negative';
377            $value = '-' . $value;
378        } else {
379            $value = '±' . $value;
380        }
381        return '<span class="' . $class . '">' . $value . '</span>';
382    }
383
384    /**
385     * current indicator, used in revision list
386     * not used in Ui\Recent because recent files are always current one
387     *
388     * @return string
389     */
390    public function showCurrentIndicator()
391    {
392        global $lang;
393        return $this->isCurrent() ? '(' . $lang['current'] . ')' : '';
394    }
395}
396