xref: /dokuwiki/inc/ChangeLog/RevisionInfo.php (revision a835c93a8c9175f580c05a0aeb56b7664f5e8435)
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()) $html .= ' <bdo dir="ltr">(' . $this->val('ip') . ')</bdo>';
160        } else {
161            $html = '<bdo dir="ltr">' . $this->val('ip') . '</bdo>';
162        }
163        return '<span class="user">' . $html . '</span>';
164    }
165
166    /**
167     * name of the page or media file
168     * used in [Ui\recent, Ui\Revisions]
169     *
170     * @return string
171     */
172    public function showFileName()
173    {
174        $id = $this->val('id');
175        $rev = $this->isCurrent() ? '' : $this->val('date');
176
177        if ($this->val('mode') == self::MODE_MEDIA) {
178            // media file revision
179            $params = ['tab_details' => 'view', 'ns' => getNS($id), 'image' => $id];
180            if ($rev) $params += ['rev' => $rev];
181            $href = media_managerURL($params, '&');
182            $display_name = $id;
183            $exists = file_exists(mediaFN($id, $rev));
184        } elseif ($this->val('mode') == self::MODE_PAGE) {
185            // page revision
186            $params = $rev ? ['rev' => $rev] : [];
187            $href = wl($id, $params, false, '&');
188            $display_name = useHeading('navigation') ? hsc(p_get_first_heading($id)) : $id;
189            if (!$display_name) $display_name = $id;
190            $exists = page_exists($id, $rev);
191        }
192
193        if ($exists) {
194            $class = 'wikilink1';
195        } elseif ($this->isCurrent()) {
196            //show only not-existing link for current page, which allows for directly create a new page/upload
197            $class = 'wikilink2';
198        } else {
199            //revision is not in attic
200            return $display_name;
201        }
202        if ($this->val('type') == DOKU_CHANGE_TYPE_DELETE) {
203            $class = 'wikilink2';
204        }
205        return '<a href="' . $href . '" class="' . $class . '">' . $display_name . '</a>';
206    }
207
208    /**
209     * Revision Title for PageDiff table headline
210     *
211     * @return string
212     */
213    public function showRevisionTitle()
214    {
215        global $lang;
216
217        if (!$this->val('date')) return '&mdash;';
218
219        $id = $this->val('id');
220        $rev = $this->isCurrent() ? '' : $this->val('date');
221        $params = ($rev) ? ['rev' => $rev] : [];
222
223        // revision info may have timestamp key when external edits occurred
224        $date = ($this->val('timestamp') === false)
225            ? $lang['unknowndate']
226            : dformat($this->val('date'));
227
228
229        if ($this->val('mode') == self::MODE_MEDIA) {
230            // media file revision
231            $href = ml($id, $params, false, '&');
232            $exists = file_exists(mediaFN($id, $rev));
233        } elseif ($this->val('mode') == self::MODE_PAGE) {
234            // page revision
235            $href = wl($id, $params, false, '&');
236            $exists = page_exists($id, $rev);
237        }
238        if ($exists) {
239            $class = 'wikilink1';
240        } elseif ($this->isCurrent()) {
241            //show only not-existing link for current page, which allows for directly create a new page/upload
242            $class = 'wikilink2';
243        } else {
244            //revision is not in attic
245            return $id . ' [' . $date . ']';
246        }
247        if ($this->val('type') == DOKU_CHANGE_TYPE_DELETE) {
248            $class = 'wikilink2';
249        }
250        return '<bdi><a class="' . $class . '" href="' . $href . '">' . $id . ' [' . $date . ']' . '</a></bdi>';
251    }
252
253    /**
254     * diff link icon in recent changes list, to compare (this) current revision with previous one
255     * all items in "recent changes" are current revision of the page or media
256     *
257     * @return string
258     */
259    public function showIconCompareWithPrevious()
260    {
261        global $lang;
262        $id = $this->val('id');
263
264        $href = '';
265        if ($this->val('mode') == self::MODE_MEDIA) {
266            // media file revision
267            // unlike page, media file does not copied to media_attic when uploaded.
268            // diff icon will not be shown when external edit occurred
269            // because no attic file to be compared with current.
270            $revs = (new MediaChangeLog($id))->getRevisions(0, 1);
271            $showLink = (count($revs) && file_exists(mediaFN($id, $revs[0])) && file_exists(mediaFN($id)));
272            if ($showLink) {
273                $param = ['tab_details' => 'history', 'mediado' => 'diff', 'ns' => getNS($id), 'image' => $id];
274                $href = media_managerURL($param, '&');
275            }
276        } elseif ($this->val('mode') == self::MODE_PAGE) {
277            // page revision
278            // when a page just created anyway, it is natural to expect no older revisions
279            // even if it had once existed but deleted before. Simply ignore to check changelog.
280            if ($this->val('type') !== DOKU_CHANGE_TYPE_CREATE) {
281                $href = wl($id, ['do' => 'diff'], false, '&');
282            }
283        }
284
285        if ($href) {
286            return '<a href="' . $href . '" class="diff_link">'
287                  . '<img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"'
288                  . ' title="' . $lang['diff'] . '" alt="' . $lang['diff'] . '" />'
289                  . '</a>';
290        } else {
291            return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />';
292        }
293    }
294
295    /**
296     * diff link icon in revisions list, compare this revision with current one
297     * the icon does not displayed for the current revision
298     *
299     * @return string
300     */
301    public function showIconCompareWithCurrent()
302    {
303        global $lang;
304        $id = $this->val('id');
305        $rev = $this->isCurrent() ? '' : $this->val('date');
306
307        $href = '';
308        if ($this->val('mode') == self::MODE_MEDIA) {
309            // media file revision
310            if (!$this->isCurrent() && file_exists(mediaFN($id, $rev))) {
311                $param = ['mediado' => 'diff', 'image' => $id, 'rev' => $rev];
312                $href = media_managerURL($param, '&');
313            }
314        } elseif ($this->val('mode') == self::MODE_PAGE) {
315            // page revision
316            if (!$this->isCurrent()) {
317                $href = wl($id, ['rev' => $rev, 'do' => 'diff'], false, '&');
318            }
319        }
320
321        if ($href) {
322            return '<a href="' . $href . '" class="diff_link">'
323                  . '<img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"'
324                  . ' title="' . $lang['diff'] . '" alt="' . $lang['diff'] . '" />'
325                  . '</a>';
326        } else {
327            return '<img src="' . DOKU_BASE . 'lib/images/blank.gif" width="15" height="11" alt="" />';
328        }
329    }
330
331    /**
332     * icon for revision action
333     * used in [Ui\recent]
334     *
335     * @return string
336     */
337    public function showIconRevisions()
338    {
339        global $lang;
340
341        if (!actionOK('revisions')) {
342            return '';
343        }
344
345        $id = $this->val('id');
346        if ($this->val('mode') == self::MODE_MEDIA) {
347            // media file revision
348            $param  = ['tab_details' => 'history', 'ns' => getNS($id), 'image' => $id];
349            $href = media_managerURL($param, '&');
350        } elseif ($this->val('mode') == self::MODE_PAGE) {
351            // page revision
352            $href = wl($id, ['do' => 'revisions'], false, '&');
353        }
354        return '<a href="' . $href . '" class="revisions_link">'
355              . '<img src="' . DOKU_BASE . 'lib/images/history.png" width="12" height="14"'
356              . ' title="' . $lang['btn_revs'] . '" alt="' . $lang['btn_revs'] . '" />'
357              . '</a>';
358    }
359
360    /**
361     * size change
362     * used in [Ui\recent, Ui\Revisions]
363     *
364     * @return string
365     */
366    public function showSizeChange()
367    {
368        $class = 'sizechange';
369        $value = filesize_h(abs($this->val('sizechange')));
370        if ($this->val('sizechange') > 0) {
371            $class .= ' positive';
372            $value = '+' . $value;
373        } elseif ($this->val('sizechange') < 0) {
374            $class .= ' negative';
375            $value = '-' . $value;
376        } else {
377            $value = '±' . $value;
378        }
379        return '<span class="' . $class . '">' . $value . '</span>';
380    }
381
382    /**
383     * current indicator, used in revision list
384     * not used in Ui\Recent because recent files are always current one
385     *
386     * @return string
387     */
388    public function showCurrentIndicator()
389    {
390        global $lang;
391        return $this->isCurrent() ? '(' . $lang['current'] . ')' : '';
392    }
393}
394