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