xref: /dokuwiki/inc/Ui/Diff.php (revision 0d02bc2f737241397ce87f4c7a7270fb021756da)
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            $l_revisions[0] = array(0, "", false); //no left revision given, add dummy
332        }
333        foreach ($l_revs as $rev) {
334            $info = $pagelog->getRevisionInfo($rev);
335            $l_revisions[$rev] = array(
336                $rev,
337                dformat($info['date']) . ' ' . editorinfo($info['user'], true) . ' ' . $info['sum'],
338                $r_rev ? $rev >= $r_rev : false //disable?
339            );
340        }
341        $r_revisions = array();
342        if (!$r_rev) {
343            $r_revisions[0] = array(0, "", false); //no right revision given, add dummy
344        }
345        foreach ($r_revs as $rev) {
346            $info = $pagelog->getRevisionInfo($rev);
347            $r_revisions[$rev] = array(
348                $rev,
349                dformat($info['date']) . ' ' . editorinfo($info['user'], true) . ' ' . $info['sum'],
350                $rev <= $l_rev //disable?
351            );
352        }
353
354        //determine previous/next revisions
355        $l_index = array_search($l_rev, $l_revs);
356        $l_prev = $l_revs[$l_index + 1];
357        $l_next = $l_revs[$l_index - 1];
358        if ($r_rev) {
359            $r_index = array_search($r_rev, $r_revs);
360            $r_prev = $r_revs[$r_index + 1];
361            $r_next = $r_revs[$r_index - 1];
362        } else {
363            //removed page
364            if ($l_next) {
365                $r_prev = $r_revs[0];
366            } else {
367                $r_prev = null;
368            }
369            $r_next = null;
370        }
371
372        /*
373         * Left side:
374         */
375        $l_nav = '';
376        //move back
377        if ($l_prev) {
378            $l_nav .= $this->diffNavigationlink($type, 'diffbothprevrev', $l_prev, $r_prev);
379            $l_nav .= $this->diffNavigationlink($type, 'diffprevrev', $l_prev, $r_rev);
380        }
381        //dropdown
382        $form = new \Doku_Form(array('action' => wl()));
383        $form->addHidden('id', $ID);
384        $form->addHidden('difftype', $type);
385        $form->addHidden('rev2[1]', $r_rev);
386        $form->addHidden('do', 'diff');
387        $form->addElement(
388             form_makeListboxField(
389                 'rev2[0]',
390                 $l_revisions,
391                 $l_rev,
392                 '', '', '',
393                 array('class' => 'quickselect')
394             )
395        );
396        $form->addElement(form_makeButton('submit', 'diff', 'Go'));
397        $l_nav .= $form->getForm();
398        //move forward
399        if ($l_next && ($l_next < $r_rev || !$r_rev)) {
400            $l_nav .= $this->diffNavigationlink($type, 'diffnextrev', $l_next, $r_rev);
401        }
402
403        /*
404         * Right side:
405         */
406        $r_nav = '';
407        //move back
408        if ($l_rev < $r_prev) {
409            $r_nav .= $this->diffNavigationlink($type, 'diffprevrev', $l_rev, $r_prev);
410        }
411        //dropdown
412        $form = new \Doku_Form(array('action' => wl()));
413        $form->addHidden('id', $ID);
414        $form->addHidden('rev2[0]', $l_rev);
415        $form->addHidden('difftype', $type);
416        $form->addHidden('do', 'diff');
417        $form->addElement(
418             form_makeListboxField(
419                 'rev2[1]',
420                 $r_revisions,
421                 $r_rev,
422                 '', '', '',
423                 array('class' => 'quickselect')
424             )
425        );
426        $form->addElement(form_makeButton('submit', 'diff', 'Go'));
427        $r_nav .= $form->getForm();
428        //move forward
429        if ($r_next) {
430            if ($pagelog->isCurrentRevision($r_next)) {
431                //last revision is diff with current page
432                $r_nav .= $this->diffNavigationlink($type, 'difflastrev', $l_rev);
433            } else {
434                $r_nav .= $this->diffNavigationlink($type, 'diffnextrev', $l_rev, $r_next);
435            }
436        } else {
437            $r_nav .= $this->diffNavigationlink($type, 'diffbothnextrev', $l_next, $r_next);
438        }
439        return array($l_nav, $r_nav);
440    }
441
442    /**
443     * Create html link to a diff defined by two revisions
444     *
445     * @param string $difftype display type
446     * @param string $linktype
447     * @param int $lrev oldest revision
448     * @param int $rrev newest revision or null for diff with current revision
449     * @return string html of link to a diff
450     */
451    protected function diffNavigationlink($difftype, $linktype, $lrev, $rrev = null)
452    {
453        global $ID, $lang;
454        if (!$rrev) {
455            $urlparam = array(
456                'do' => 'diff',
457                'rev' => $lrev,
458                'difftype' => $difftype,
459            );
460        } else {
461            $urlparam = array(
462                'do' => 'diff',
463                'rev2[0]' => $lrev,
464                'rev2[1]' => $rrev,
465                'difftype' => $difftype,
466            );
467        }
468        return  '<a class="'. $linktype .'" href="'. wl($ID, $urlparam) .'" title="'. $lang[$linktype] .'">'
469              . '<span>'. $lang[$linktype] .'</span>'
470              . '</a>'. DOKU_LF;
471    }
472
473}
474