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