xref: /dokuwiki/inc/Ui/Diff.php (revision 259dd31fe29dd063ffc240e7629ae48bb354b825)
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            $form->addButton('do[diff]', 'Go')->attr('type','submit');
164            print $form->toHTML();
165
166            print '<p>';
167            // link to exactly this view FS#2835
168            print $this->diffNavigationlink($type, 'difflink', $l_rev, $r_rev ? $r_rev : $INFO['currentrev']);
169            print '</p>';
170
171            print '</div>'; // .diffoptions
172        }
173
174        /*
175         * Display diff view table
176         */
177        print '<div class="table">';
178        print '<table class="diff diff_'. $type .'">';
179
180        //navigation and header
181        if ($type == 'inline') {
182            if (!$text) {
183                print '<tr>'
184                    . '<td class="diff-lineheader">-</td>'
185                    . '<td class="diffnav">'. $l_nav .'</td>'
186                    . '</tr>';
187                print '<tr>'
188                    . '<th class="diff-lineheader">-</th>'
189                    . '<th '. $l_minor .'>'. $l_head .'</th>'
190                    .'</tr>';
191            }
192            print '<tr>'
193                . '<td class="diff-lineheader">+</td>'
194                . '<td class="diffnav">'. $r_nav .'</td>'
195                .'</tr>';
196            print '<tr>'
197                . '<th class="diff-lineheader">+</th>'
198                . '<th '. $r_minor .'>'. $r_head .'</th>'
199                . '</tr>';
200        } else {
201            if (!$text) {
202                print '<tr>'
203                    . '<td colspan="2" class="diffnav">'. $l_nav .'</td>'
204                    . '<td colspan="2" class="diffnav">'. $r_nav .'</td>'
205                    . '</tr>';
206            }
207            print '<tr>'
208                . '<th colspan="2" '. $l_minor .'>'. $l_head .'</th>'
209                . '<th colspan="2" '. $r_minor .'>'. $r_head .'</th>'
210                . '</tr>';
211        }
212
213        //diff view
214        print html_insert_softbreaks($diffformatter->format($diff));
215
216        print '</table>';
217        print '</div>'. DOKU_LF;
218    }
219
220
221    /**
222     * Get header of diff HTML
223     *
224     * @param string $l_rev   Left revisions
225     * @param string $r_rev   Right revision
226     * @param string $id      Page id, if null $ID is used
227     * @param bool   $media   If it is for media files
228     * @param bool   $inline  Return the header on a single line
229     * @return string[] HTML snippets for diff header
230     */
231    protected function diffHead($l_rev, $r_rev, $id = null, $media = false, $inline = false)
232    {
233        global $lang;
234        if ($id === null) {
235            global $ID;
236            $id = $ID;
237        }
238        $head_separator = $inline ? ' ' : '<br />';
239        $media_or_wikiFN = $media ? 'mediaFN' : 'wikiFN';
240        $ml_or_wl = $media ? 'ml' : 'wl';
241        $l_minor = $r_minor = '';
242
243        if ($media) {
244            $changelog = new MediaChangeLog($id);
245        } else {
246            $changelog = new PageChangeLog($id);
247        }
248        if (!$l_rev) {
249            $l_head = '&mdash;';
250        } else {
251            $l_info   = $changelog->getRevisionInfo($l_rev);
252            if ($l_info['user']) {
253                $l_user = '<bdi>'.editorinfo($l_info['user']).'</bdi>';
254                if (auth_ismanager()) $l_user .= ' <bdo dir="ltr">('.$l_info['ip'].')</bdo>';
255            } else {
256                $l_user = '<bdo dir="ltr">'.$l_info['ip'].'</bdo>';
257            }
258            $l_user  = '<span class="user">'.$l_user.'</span>';
259            $l_sum   = ($l_info['sum']) ? '<span class="sum"><bdi>'.hsc($l_info['sum']).'</bdi></span>' : '';
260            if ($l_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $l_minor = 'class="minor"';
261
262            $l_head_title = ($media) ? dformat($l_rev) : $id.' ['.dformat($l_rev).']';
263            $l_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$l_rev").'">'
264                . $l_head_title.'</a></bdi>'.$head_separator.$l_user.' '.$l_sum;
265        }
266
267        if ($r_rev) {
268            $r_info   = $changelog->getRevisionInfo($r_rev);
269            if ($r_info['user']) {
270                $r_user = '<bdi>'.editorinfo($r_info['user']).'</bdi>';
271                if (auth_ismanager()) $r_user .= ' <bdo dir="ltr">('.$r_info['ip'].')</bdo>';
272            } else {
273                $r_user = '<bdo dir="ltr">'.$r_info['ip'].'</bdo>';
274            }
275            $r_user = '<span class="user">'.$r_user.'</span>';
276            $r_sum  = ($r_info['sum']) ? '<span class="sum"><bdi>'.hsc($r_info['sum']).'</bdi></span>' : '';
277            if ($r_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"';
278
279            $r_head_title = ($media) ? dformat($r_rev) : $id.' ['.dformat($r_rev).']';
280            $r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$r_rev").'">'
281                . $r_head_title.'</a></bdi>'.$head_separator.$r_user.' '.$r_sum;
282        } elseif ($_rev = @filemtime($media_or_wikiFN($id))) {
283            $_info   = $changelog->getRevisionInfo($_rev);
284            if ($_info['user']) {
285                $_user = '<bdi>'.editorinfo($_info['user']).'</bdi>';
286                if (auth_ismanager()) $_user .= ' <bdo dir="ltr">('.$_info['ip'].')</bdo>';
287            } else {
288                $_user = '<bdo dir="ltr">'.$_info['ip'].'</bdo>';
289            }
290            $_user = '<span class="user">'.$_user.'</span>';
291            $_sum  = ($_info['sum']) ? '<span class="sum"><bdi>'.hsc($_info['sum']).'</span></bdi>' : '';
292            if ($_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"';
293
294            $r_head_title = ($media) ? dformat($_rev) : $id.' ['.dformat($_rev).']';
295            $r_head  = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id).'">'
296                . $r_head_title.'</a></bdi> '.'('.$lang['current'].')'.$head_separator.$_user.' '.$_sum;
297        }else{
298            $r_head = '&mdash; ('.$lang['current'].')';
299        }
300
301        return array($l_head, $r_head, $l_minor, $r_minor);
302    }
303
304    /**
305     * Create html for revision navigation
306     *
307     * @param PageChangeLog $pagelog changelog object of current page
308     * @param string        $type    inline vs sidebyside
309     * @param int           $l_rev   left revision timestamp
310     * @param int           $r_rev   right revision timestamp
311     * @return string[] html of left and right navigation elements
312     */
313    protected function diffNavigation($pagelog, $type, $l_rev, $r_rev)
314    {
315        global $INFO, $ID;
316
317        // last timestamp is not in changelog, retrieve timestamp from metadata
318        // note: when page is removed, the metadata timestamp is zero
319        if (!$r_rev) {
320            if (isset($INFO['meta']['last_change']['date'])) {
321                $r_rev = $INFO['meta']['last_change']['date'];
322            } else {
323                $r_rev = 0;
324            }
325        }
326
327        //retrieve revisions with additional info
328        list($l_revs, $r_revs) = $pagelog->getRevisionsAround($l_rev, $r_rev);
329        $l_revisions = array();
330        if (!$l_rev) {
331            //no left revision given, add dummy
332            $l_revisions[0]= array('label' => '', 'attrs' => []);
333        }
334        foreach ($l_revs as $rev) {
335            $info = $pagelog->getRevisionInfo($rev);
336            $l_revisions[$rev] = array(
337                'label' => dformat($info['date']) .' '. editorinfo($info['user'], true) .' '. $info['sum'],
338                'attrs' => ['title' => $rev],
339            );
340            if ($r_rev ? $rev >= $r_rev : false) $l_revisions[$rev]['attrs']['disabled'] = 'disabled';
341        }
342        $r_revisions = array();
343        if (!$r_rev) {
344            //no right revision given, add dummy
345            $r_revisions[0] = array('label' => '', 'attrs' => []);
346        }
347        foreach ($r_revs as $rev) {
348            $info = $pagelog->getRevisionInfo($rev);
349            $r_revisions[$rev] = array(
350                'label' => dformat($info['date']) .' '. editorinfo($info['user'], true) .' '. $info['sum'],
351                'attrs' => ['title' => $rev],
352            );
353            if ($rev <= $l_rev) $r_revisions[$rev]['attrs']['disabled'] = 'disabled';
354        }
355
356        //determine previous/next revisions
357        $l_index = array_search($l_rev, $l_revs);
358        $l_prev = $l_revs[$l_index + 1];
359        $l_next = $l_revs[$l_index - 1];
360        if ($r_rev) {
361            $r_index = array_search($r_rev, $r_revs);
362            $r_prev = $r_revs[$r_index + 1];
363            $r_next = $r_revs[$r_index - 1];
364        } else {
365            //removed page
366            if ($l_next) {
367                $r_prev = $r_revs[0];
368            } else {
369                $r_prev = null;
370            }
371            $r_next = null;
372        }
373
374        /*
375         * Left side:
376         */
377        $l_nav = '';
378        //move back
379        if ($l_prev) {
380            $l_nav .= $this->diffNavigationlink($type, 'diffbothprevrev', $l_prev, $r_prev);
381            $l_nav .= $this->diffNavigationlink($type, 'diffprevrev', $l_prev, $r_rev);
382        }
383        //dropdown
384        $form = new Form(['action' => wl()]);
385        $form->setHiddenField('id', $ID);
386        $form->setHiddenField('difftype', $type);
387        $form->setHiddenField('rev2[1]', $r_rev);
388        $form->setHiddenField('do', 'diff');
389        $input = $form->addDropdown('rev2[0]', $l_revisions, /*'LeftSide'*/)->val($l_rev)->addClass('quickselect');
390        $input->useInput(false); // inhibit prefillInput() during toHTML() process
391        $form->addButton('do[diff]', 'Go')->attr('type','submit');
392        $l_nav .= $form->toHTML();
393        //move forward
394        if ($l_next && ($l_next < $r_rev || !$r_rev)) {
395            $l_nav .= $this->diffNavigationlink($type, 'diffnextrev', $l_next, $r_rev);
396        }
397
398        /*
399         * Right side:
400         */
401        $r_nav = '';
402        //move back
403        if ($l_rev < $r_prev) {
404            $r_nav .= $this->diffNavigationlink($type, 'diffprevrev', $l_rev, $r_prev);
405        }
406        //dropdown
407        $form = new Form(['action' => wl()]);
408        $form->setHiddenField('id', $ID);
409        $form->setHiddenField('rev2[0]', $l_rev);
410        $form->setHiddenField('difftype', $type);
411        $form->setHiddenField('do', 'diff');
412        $input = $form->addDropdown('rev2[1]', $r_revisions, /*'RightSide'*/)->val($r_rev)->addClass('quickselect');
413        $input->useInput(false); // inhibit prefillInput() during toHTML() process
414        $form->addButton('do[diff]', 'Go')->attr('type','submit');
415        $r_nav .= $form->toHTML();
416        //move forward
417        if ($r_next) {
418            if ($pagelog->isCurrentRevision($r_next)) {
419                //last revision is diff with current page
420                $r_nav .= $this->diffNavigationlink($type, 'difflastrev', $l_rev);
421            } else {
422                $r_nav .= $this->diffNavigationlink($type, 'diffnextrev', $l_rev, $r_next);
423            }
424        } else {
425            $r_nav .= $this->diffNavigationlink($type, 'diffbothnextrev', $l_next, $r_next);
426        }
427        return array($l_nav, $r_nav);
428    }
429
430    /**
431     * Create html link to a diff defined by two revisions
432     *
433     * @param string $difftype display type
434     * @param string $linktype
435     * @param int $lrev oldest revision
436     * @param int $rrev newest revision or null for diff with current revision
437     * @return string html of link to a diff
438     */
439    protected function diffNavigationlink($difftype, $linktype, $lrev, $rrev = null)
440    {
441        global $ID, $lang;
442        if (!$rrev) {
443            $urlparam = array(
444                'do' => 'diff',
445                'rev' => $lrev,
446                'difftype' => $difftype,
447            );
448        } else {
449            $urlparam = array(
450                'do' => 'diff',
451                'rev2[0]' => $lrev,
452                'rev2[1]' => $rrev,
453                'difftype' => $difftype,
454            );
455        }
456        return  '<a class="'. $linktype .'" href="'. wl($ID, $urlparam) .'" title="'. $lang[$linktype] .'">'
457              . '<span>'. $lang[$linktype] .'</span>'
458              . '</a>'. DOKU_LF;
459    }
460
461}
462