xref: /dokuwiki/inc/Ui/Diff.php (revision 60a48169abe6df5c485f0c9df175a5a50c54ee38)
1<?php
2
3namespace dokuwiki\Ui;
4
5use dokuwiki\ChangeLog\PageChangeLog;
6use dokuwiki\ChangeLog\MediaChangeLog;
7use dokuwiki\Extension\Event;
8use dokuwiki\Form\Form;
9
10/**
11 * DokuWiki Diff Insterface
12 *
13 * @package dokuwiki\Ui
14 */
15class Diff extends Ui
16{
17    /**
18     * Show diff
19     * between current page version and provided $text
20     * or between the revisions provided via GET or POST
21     *
22     * @author Andreas Gohr <andi@splitbrain.org>
23     *
24     * @param  string $text  when non-empty: compare with this text with most current version
25     * @param  bool   $intro display the intro text
26     * @param  string $type  type of the diff (inline or sidebyside)
27     * @return void
28     */
29    public function show($text = '', $intro = true, $type = null)
30    {
31        global $ID;
32        global $REV;
33        global $lang;
34        global $INPUT;
35        global $INFO;
36        $pagelog = new PageChangeLog($ID);
37
38        /*
39         * Determine diff type
40         */
41        if (!$type) {
42            $type = $INPUT->str('difftype');
43            if (empty($type)) {
44                $type = get_doku_pref('difftype', $type);
45                if (empty($type) && $INFO['ismobile']) {
46                    $type = 'inline';
47                }
48            }
49        }
50        if ($type != 'inline') $type = 'sidebyside';
51
52        /*
53         * Determine requested revision(s)
54         */
55        // we're trying to be clever here, revisions to compare can be either
56        // given as rev and rev2 parameters, with rev2 being optional. Or in an
57        // array in rev2.
58        $rev1 = $REV;
59
60        $rev2 = $INPUT->ref('rev2');
61        if (is_array($rev2)) {
62            $rev1 = (int) $rev2[0];
63            $rev2 = (int) $rev2[1];
64
65            if (!$rev1) {
66                $rev1 = $rev2;
67                unset($rev2);
68            }
69        } else {
70            $rev2 = $INPUT->int('rev2');
71        }
72
73        /*
74         * Determine left and right revision, its texts and the header
75         */
76        $r_minor = '';
77        $l_minor = '';
78
79        if ($text) { // compare text to the most current revision
80            $l_rev = '';
81            $l_text = rawWiki($ID, '');
82            $l_head = '<a class="wikilink1" href="'. wl($ID) .'">'
83                . $ID .' '. dformat((int) @filemtime(wikiFN($ID))) .'</a> '
84                . $lang['current'];
85
86            $r_rev = '';
87            $r_text = cleanText($text);
88            $r_head = $lang['yours'];
89        } else {
90            if ($rev1 && isset($rev2) && $rev2) { // two specific revisions wanted
91                // make sure order is correct (older on the left)
92                if ($rev1 < $rev2) {
93                    $l_rev = $rev1;
94                    $r_rev = $rev2;
95                } else {
96                    $l_rev = $rev2;
97                    $r_rev = $rev1;
98                }
99            } elseif ($rev1) { // single revision given, compare to current
100                $r_rev = '';
101                $l_rev = $rev1;
102            } else { // no revision was given, compare previous to current
103                $r_rev = '';
104                $revs = $pagelog->getRevisions(0, 1);
105                $l_rev = $revs[0];
106                $REV = $l_rev; // store revision back in $REV
107            }
108
109            // when both revisions are empty then the page was created just now
110            if (!$l_rev && !$r_rev) {
111                $l_text = '';
112            } else {
113                $l_text = rawWiki($ID, $l_rev);
114            }
115            $r_text = rawWiki($ID, $r_rev);
116
117            list($l_head, $r_head, $l_minor, $r_minor) = $this->diffHead(
118                $l_rev, $r_rev, null, false, $type == 'inline'
119            );
120        }
121
122        /*
123         * Build navigation
124         */
125        $l_nav = '';
126        $r_nav = '';
127        if (!$text) {
128            list($l_nav, $r_nav) = $this->diffNavigation($pagelog, $type, $l_rev, $r_rev);
129        }
130        /*
131         * Create diff object and the formatter
132         */
133        $diff = new \Diff(explode("\n", $l_text), explode("\n", $r_text));
134
135        if ($type == 'inline') {
136            $diffformatter = new \InlineDiffFormatter();
137        } else {
138            $diffformatter = new \TableDiffFormatter();
139        }
140        /*
141         * Display intro
142         */
143        if ($intro) print p_locale_xhtml('diff');
144
145        /*
146         * Display type and exact reference
147         */
148        if (!$text) {
149            print '<div class="diffoptions group">';
150
151            // create the form to select diff view type
152            $form = new Form(['action' => wl()]);
153            $form->setHiddenField('id', $ID);
154            $form->setHiddenField('rev2[0]', $l_rev);
155            $form->setHiddenField('rev2[1]', $r_rev);
156            $form->setHiddenField('do', 'diff');
157            $options = array(
158                         'sidebyside' => $lang['diff_side'],
159                         'inline' => $lang['diff_inline']
160            );
161            $input = $form->addDropdown('difftype', $options, $lang['diff_type'])
162                ->val($type)->addClass('quickselect');
163            $input->useInput(false); // inhibit prefillInput() during toHTML() process
164            $form->addButton('do[diff]', 'Go')->attr('type','submit');
165            print $form->toHTML();
166
167            print '<p>';
168            // link to exactly this view FS#2835
169            print $this->diffViewlink($type, 'difflink', $l_rev, $r_rev ? $r_rev : $INFO['currentrev']);
170            print '</p>';
171
172            print '</div>'; // .diffoptions
173        }
174
175        /*
176         * Display diff view table
177         */
178        print '<div class="table">';
179        print '<table class="diff diff_'. $type .'">';
180
181        //navigation and header
182        if ($type == 'inline') {
183            if (!$text) {
184                print '<tr>'
185                    . '<td class="diff-lineheader">-</td>'
186                    . '<td class="diffnav">'. $l_nav .'</td>'
187                    . '</tr>';
188                print '<tr>'
189                    . '<th class="diff-lineheader">-</th>'
190                    . '<th '. $l_minor .'>'. $l_head .'</th>'
191                    .'</tr>';
192            }
193            print '<tr>'
194                . '<td class="diff-lineheader">+</td>'
195                . '<td class="diffnav">'. $r_nav .'</td>'
196                .'</tr>';
197            print '<tr>'
198                . '<th class="diff-lineheader">+</th>'
199                . '<th '. $r_minor .'>'. $r_head .'</th>'
200                . '</tr>';
201        } else {
202            if (!$text) {
203                print '<tr>'
204                    . '<td colspan="2" class="diffnav">'. $l_nav .'</td>'
205                    . '<td colspan="2" class="diffnav">'. $r_nav .'</td>'
206                    . '</tr>';
207            }
208            print '<tr>'
209                . '<th colspan="2" '. $l_minor .'>'. $l_head .'</th>'
210                . '<th colspan="2" '. $r_minor .'>'. $r_head .'</th>'
211                . '</tr>';
212        }
213
214        //diff view
215        print html_insert_softbreaks($diffformatter->format($diff));
216
217        print '</table>';
218        print '</div>'. DOKU_LF;
219    }
220
221
222    /**
223     * Get header of diff HTML
224     *
225     * @param string $l_rev   Left revisions
226     * @param string $r_rev   Right revision
227     * @param string $id      Page id, if null $ID is used
228     * @param bool   $media   If it is for media files
229     * @param bool   $inline  Return the header on a single line
230     * @return string[] HTML snippets for diff header
231     */
232    protected function diffHead($l_rev, $r_rev, $id = null, $media = false, $inline = false)
233    {
234        global $lang;
235        if ($id === null) {
236            global $ID;
237            $id = $ID;
238        }
239        $head_separator = $inline ? ' ' : '<br />';
240        $media_or_wikiFN = $media ? 'mediaFN' : 'wikiFN';
241        $ml_or_wl = $media ? 'ml' : 'wl';
242        $l_minor = $r_minor = '';
243
244        if ($media) {
245            $changelog = new MediaChangeLog($id);
246        } else {
247            $changelog = new PageChangeLog($id);
248        }
249        if (!$l_rev) {
250            $l_head = '&mdash;';
251        } else {
252            $l_info   = $changelog->getRevisionInfo($l_rev);
253            if ($l_info['user']) {
254                $l_user = '<bdi>'.editorinfo($l_info['user']).'</bdi>';
255                if (auth_ismanager()) $l_user .= ' <bdo dir="ltr">('.$l_info['ip'].')</bdo>';
256            } else {
257                $l_user = '<bdo dir="ltr">'.$l_info['ip'].'</bdo>';
258            }
259            $l_user  = '<span class="user">'.$l_user.'</span>';
260            $l_sum   = ($l_info['sum']) ? '<span class="sum"><bdi>'.hsc($l_info['sum']).'</bdi></span>' : '';
261            if ($l_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $l_minor = 'class="minor"';
262
263            $l_head_title = ($media) ? dformat($l_rev) : $id.' ['.dformat($l_rev).']';
264            $l_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$l_rev").'">'
265                . $l_head_title.'</a></bdi>'.$head_separator.$l_user.' '.$l_sum;
266        }
267
268        if ($r_rev) {
269            $r_info   = $changelog->getRevisionInfo($r_rev);
270            if ($r_info['user']) {
271                $r_user = '<bdi>'.editorinfo($r_info['user']).'</bdi>';
272                if (auth_ismanager()) $r_user .= ' <bdo dir="ltr">('.$r_info['ip'].')</bdo>';
273            } else {
274                $r_user = '<bdo dir="ltr">'.$r_info['ip'].'</bdo>';
275            }
276            $r_user = '<span class="user">'.$r_user.'</span>';
277            $r_sum  = ($r_info['sum']) ? '<span class="sum"><bdi>'.hsc($r_info['sum']).'</bdi></span>' : '';
278            if ($r_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"';
279
280            $r_head_title = ($media) ? dformat($r_rev) : $id.' ['.dformat($r_rev).']';
281            $r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$r_rev").'">'
282                . $r_head_title.'</a></bdi>'.$head_separator.$r_user.' '.$r_sum;
283        } elseif ($_rev = @filemtime($media_or_wikiFN($id))) {
284            $_info   = $changelog->getRevisionInfo($_rev);
285            if ($_info['user']) {
286                $_user = '<bdi>'.editorinfo($_info['user']).'</bdi>';
287                if (auth_ismanager()) $_user .= ' <bdo dir="ltr">('.$_info['ip'].')</bdo>';
288            } else {
289                $_user = '<bdo dir="ltr">'.$_info['ip'].'</bdo>';
290            }
291            $_user = '<span class="user">'.$_user.'</span>';
292            $_sum  = ($_info['sum']) ? '<span class="sum"><bdi>'.hsc($_info['sum']).'</span></bdi>' : '';
293            if ($_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"';
294
295            $r_head_title = ($media) ? dformat($_rev) : $id.' ['.dformat($_rev).']';
296            $r_head  = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id).'">'
297                . $r_head_title.'</a></bdi> '.'('.$lang['current'].')'.$head_separator.$_user.' '.$_sum;
298        }else{
299            $r_head = '&mdash; ('.$lang['current'].')';
300        }
301
302        return array($l_head, $r_head, $l_minor, $r_minor);
303    }
304
305    /**
306     * Create html for revision navigation
307     *
308     * @param PageChangeLog $pagelog changelog object of current page
309     * @param string        $type    inline vs sidebyside
310     * @param int           $l_rev   left revision timestamp
311     * @param int           $r_rev   right revision timestamp
312     * @return string[] html of left and right navigation elements
313     */
314    protected function diffNavigation($pagelog, $type, $l_rev, $r_rev)
315    {
316        global $INFO, $ID;
317
318        // last timestamp is not in changelog, retrieve timestamp from metadata
319        // note: when page is removed, the metadata timestamp is zero
320        if (!$r_rev) {
321            if (isset($INFO['meta']['last_change']['date'])) {
322                $r_rev = $INFO['meta']['last_change']['date'];
323            } else {
324                $r_rev = 0;
325            }
326        }
327
328        //retrieve revisions with additional info
329        list($l_revs, $r_revs) = $pagelog->getRevisionsAround($l_rev, $r_rev);
330        $l_revisions = array();
331        if (!$l_rev) {
332            //no left revision given, add dummy
333            $l_revisions[0]= array('label' => '', 'attrs' => []);
334        }
335        foreach ($l_revs as $rev) {
336            $info = $pagelog->getRevisionInfo($rev);
337            $l_revisions[$rev] = array(
338                'label' => dformat($info['date']) .' '. editorinfo($info['user'], true) .' '. $info['sum'],
339                'attrs' => ['title' => $rev],
340            );
341            if ($r_rev ? $rev >= $r_rev : false) $l_revisions[$rev]['attrs']['disabled'] = 'disabled';
342        }
343        $r_revisions = array();
344        if (!$r_rev) {
345            //no right revision given, add dummy
346            $r_revisions[0] = array('label' => '', 'attrs' => []);
347        }
348        foreach ($r_revs as $rev) {
349            $info = $pagelog->getRevisionInfo($rev);
350            $r_revisions[$rev] = array(
351                'label' => dformat($info['date']) .' '. editorinfo($info['user'], true) .' '. $info['sum'],
352                'attrs' => ['title' => $rev],
353            );
354            if ($rev <= $l_rev) $r_revisions[$rev]['attrs']['disabled'] = 'disabled';
355        }
356
357        //determine previous/next revisions
358        $l_index = array_search($l_rev, $l_revs);
359        $l_prev = $l_revs[$l_index + 1];
360        $l_next = $l_revs[$l_index - 1];
361        if ($r_rev) {
362            $r_index = array_search($r_rev, $r_revs);
363            $r_prev = $r_revs[$r_index + 1];
364            $r_next = $r_revs[$r_index - 1];
365        } else {
366            //removed page
367            if ($l_next) {
368                $r_prev = $r_revs[0];
369            } else {
370                $r_prev = null;
371            }
372            $r_next = null;
373        }
374
375        /*
376         * Left side:
377         */
378        $l_nav = '';
379        //move back
380        if ($l_prev) {
381            $l_nav .= $this->diffViewlink($type, 'diffbothprevrev', $l_prev, $r_prev);
382            $l_nav .= $this->diffViewlink($type, 'diffprevrev', $l_prev, $r_rev);
383        }
384        //dropdown
385        $form = new Form(['action' => wl()]);
386        $form->setHiddenField('id', $ID);
387        $form->setHiddenField('difftype', $type);
388        $form->setHiddenField('rev2[1]', $r_rev);
389        $form->setHiddenField('do', 'diff');
390        $input = $form->addDropdown('rev2[0]', $l_revisions)->val($l_rev)->addClass('quickselect');
391        $input->useInput(false); // inhibit prefillInput() during toHTML() process
392        $form->addButton('do[diff]', 'Go')->attr('type','submit');
393        $l_nav .= $form->toHTML();
394        //move forward
395        if ($l_next && ($l_next < $r_rev || !$r_rev)) {
396            $l_nav .= $this->diffViewlink($type, 'diffnextrev', $l_next, $r_rev);
397        }
398
399        /*
400         * Right side:
401         */
402        $r_nav = '';
403        //move back
404        if ($l_rev < $r_prev) {
405            $r_nav .= $this->diffViewlink($type, 'diffprevrev', $l_rev, $r_prev);
406        }
407        //dropdown
408        $form = new Form(['action' => wl()]);
409        $form->setHiddenField('id', $ID);
410        $form->setHiddenField('rev2[0]', $l_rev);
411        $form->setHiddenField('difftype', $type);
412        $form->setHiddenField('do', 'diff');
413        $input = $form->addDropdown('rev2[1]', $r_revisions)->val($r_rev)->addClass('quickselect');
414        $input->useInput(false); // inhibit prefillInput() during toHTML() process
415        $form->addButton('do[diff]', 'Go')->attr('type','submit');
416        $r_nav .= $form->toHTML();
417        //move forward
418        if ($r_next) {
419            if ($pagelog->isCurrentRevision($r_next)) {
420                //last revision is diff with current page
421                $r_nav .= $this->diffViewlink($type, 'difflastrev', $l_rev);
422            } else {
423                $r_nav .= $this->diffViewlink($type, 'diffnextrev', $l_rev, $r_next);
424            }
425        } else {
426            $r_nav .= $this->diffViewlink($type, 'diffbothnextrev', $l_next, $r_next);
427        }
428        return array($l_nav, $r_nav);
429    }
430
431    /**
432     * Create html link to a diff view defined by two revisions
433     *
434     * @param string $difftype display type
435     * @param string $linktype
436     * @param int $lrev oldest revision
437     * @param int $rrev newest revision or null for diff with current revision
438     * @return string html of link to a diff view
439     */
440    protected function diffViewlink($difftype, $linktype, $lrev, $rrev = null)
441    {
442        global $ID, $lang;
443        if (!$rrev) {
444            $urlparam = array(
445                'do' => 'diff',
446                'rev' => $lrev,
447                'difftype' => $difftype,
448            );
449        } else {
450            $urlparam = array(
451                'do' => 'diff',
452                'rev2[0]' => $lrev,
453                'rev2[1]' => $rrev,
454                'difftype' => $difftype,
455            );
456        }
457        return  '<a class="'. $linktype .'" href="'. wl($ID, $urlparam) .'" title="'. $lang[$linktype] .'">'
458              . '<span>'. $lang[$linktype] .'</span>'
459              . '</a>'. DOKU_LF;
460    }
461
462}
463