Failed Conditions
Pull Request — master (#3361)
by
unknown
02:52
created

PageDiff::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace dokuwiki\Ui;
4
5
use dokuwiki\ChangeLog\PageChangeLog;
6
use dokuwiki\Ui\PageRevisions;
7
use dokuwiki\Form\Form;
8
9
/**
10
 * DokuWiki PageDiff Interface
11
 *
12
 * @author Andreas Gohr <[email protected]>
13
 * @author Satoshi Sahara <[email protected]>
14
 * @package dokuwiki\Ui
15
 */
16
class PageDiff extends Diff
17
{
18
    /* @var PageChangeLog */
19
    protected $changelog;
20
21
    /* @var string */
22
    protected $text;
23
24
    /**
25
     * PageDiff Ui constructor
26
     *
27
     * @param string $id  page id
28
     */
29
    public function __construct($id)
30
    {
31
        global $INFO;
32
        if (!isset($id)) $id = $INFO['id'];
33
        $this->item = 'page';
34
35
        // init preference
36
        $this->preference['showIntro'] = true;
37
        $this->preference['difftype'] = 'sidebyside'; // diff view type: inline or sidebyside
38
39
        parent::__construct($id);
40
    }
41
42
    /** @inheritdoc */
43
    protected function setChangeLog()
44
    {
45
        $this->changelog = new PageChangeLog($this->id);
46
    }
47
48
    /** @inheritdoc */
49
    protected function itemFN($id, $rev = '')
50
    {
51
        return wikiFN($id, $rev);
52
    }
53
54
    /**
55
     * Set text to be compared with most current version
56
     * exclusively use of the compare($old, $new) method
57
     *
58
     * @param string $text
59
     * @return $this
60
     */
61
    public function compareWith($text = null)
62
    {
63
        if (isset($text)) {
64
            $this->text = $text;
65
            $this->oldRev = '';
0 ignored issues
show
Documentation Bug introduced by
The property $oldRev was declared of type integer, but '' is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
66
            $this->newRev = null;
67
        }
68
        return $this;
69
    }
70
71
    /** @inheritdoc */
72
    protected function preProcess()
73
    {
74
        parent::preProcess();
75
        if (!isset($this->oldRev, $this->newRev)) {
76
            // no revision was given, compare previous to current
77
            $this->oldRev = $this->changelog->getRevisions(0, 1)[0];
78
            $this->newRev = '';
79
80
            global $INFO, $REV;
81
            if ($this->id == $INFO['id'])
82
               $REV = $this->oldRev; // store revision back in $REV
83
        }
84
    }
85
86
    /**
87
     * Show diff
88
     * between current page version and provided $text
89
     * or between the revisions provided via GET or POST
90
     *
91
     * @author Andreas Gohr <[email protected]>
92
     *
93
     * @return void
94
     */
95
    public function show()
96
    {
97
        global $INFO, $lang;
98
99
       // determine left and right revision
100
        if (!isset($this->oldRev)) $this->preProcess();
101
102
        // create difference engine object
103
        if (isset($this->text)) { // compare text to the most current revision
104
            $oldText = rawWiki($this->id, '');
105
            $newText = cleanText($this->text);
106
        } else {
107
            // when both revisions are empty then the page was created just now
108
            $oldText = (!$this->oldRev && !$this->newRev) ? '' : rawWiki($this->id, $this->oldRev);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->oldRev of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
109
            $newText = rawWiki($this->id, $this->newRev); // empty when removed page
110
        }
111
        $Difference = new \Diff(explode("\n", $oldText), explode("\n", $newText));
112
113
        // revison info of older page (left side)
114
        $oldRevInfo = $this->getExtendedRevisionInfo($this->oldRev);
115
116
        // revison info of newer page (right side)
117
        if (isset($this->text)) {
118
            $newRevInfo = array('date' => null);
119
        } else {
120
            $newRevInfo = $this->getExtendedRevisionInfo($this->newRev);
121
        }
122
123
        // determin exact revision identifiers, even for current page
124
        $oldRev = $oldRevInfo['date'];
125
        $newRev = $newRevInfo['date'];
126
127
        // build paired navigation
128
        $navOlderRevisions = '';
129
        $navNewerRevisions = '';
130
        if (!isset($this->text)) {
131
            list(
132
                $navOlderRevisions,
133
                $navNewerRevisions,
134
            ) = $this->buildRevisionsNavigation($oldRev, $newRev);
135
        }
136
137
        // display intro
138
        if ($this->preference['showIntro']) echo p_locale_xhtml('diff');
139
140
        // print form to choose diff view type, and exact url reference to the view
141
        if (!isset($this->text)) {
142
            $this->showDiffViewSelector($oldRev, $newRev);
143
        }
144
145
        // assign minor edit checker to the variable
146
        $classEditType = function ($info) {
147
            return ($info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) ? ' class="minor"' : '';
148
        };
149
150
        // display diff view table
151
        echo '<div class="table">';
152
        echo '<table class="diff diff_'.$this->preference['difftype'] .'">';
153
154
        //navigation and header
155
        switch ($this->preference['difftype']) {
156
            case 'inline':
157
                if (!isset($this->text)) {
158
                    echo '<tr>'
159
                        .'<td class="diff-lineheader">-</td>'
160
                        .'<td class="diffnav">'. $navOlderRevisions .'</td>'
161
                        .'</tr>';
162
                    echo '<tr>'
163
                        .'<th class="diff-lineheader">-</th>'
164
                        .'<th'.$classEditType($oldRevInfo).'>'.$this->revisionTitle($oldRevInfo).'</th>'
165
                        .'</tr>';
166
                }
167
                echo '<tr>'
168
                    .'<td class="diff-lineheader">+</td>'
169
                    .'<td class="diffnav">'. $navNewerRevisions .'</td>'
170
                    .'</tr>';
171
                echo '<tr>'
172
                    .'<th class="diff-lineheader">+</th>'
173
                    .'<th'.$classEditType($newRevInfo).'>'.$this->revisionTitle($newRevInfo).'</th>'
174
                    .'</tr>';
175
                // create formatter object
176
                $DiffFormatter = new \InlineDiffFormatter();
177
                break;
178
179
            case 'sidebyside':
180
            default:
181
                if (!isset($this->text)) {
182
                    echo '<tr>'
183
                        .'<td colspan="2" class="diffnav">'. $navOlderRevisions .'</td>'
184
                        .'<td colspan="2" class="diffnav">'. $navNewerRevisions .'</td>'
185
                        .'</tr>';
186
                }
187
                echo '<tr>'
188
                    .'<th colspan="2"'.$classEditType($oldRevInfo).'>'.$this->revisionTitle($oldRevInfo).'</th>'
189
                    .'<th colspan="2"'.$classEditType($newRevInfo).'>'.$this->revisionTitle($newRevInfo).'</th>'
190
                    .'</tr>';
191
                // create formatter object
192
                $DiffFormatter = new \TableDiffFormatter();
193
                break;
194
        }
195
196
        // output formatted difference
197
        echo $this->insertSoftbreaks($DiffFormatter->format($Difference));
198
199
        echo '</table>';
200
        echo '</div>';
201
    }
202
203
    /**
204
     * Revision Title for PageDiff table headline
205
     *
206
     * @param array $info  Revision info structure of a page
207
     * @return string
208
     */
209
    protected function revisionTitle(array $info)
210
    {
211
        global $lang, $INFO;
212
213
        // use designated title when compare current page source with given text
214
        if (array_key_exists('date', $info) && is_null($info['date'])) {
215
            return $lang['yours'];
216
        }
217
218
        if (isset($info['date'])) {
219
            $rev = $info['date'];
220
            $title = '<bdi><a class="wikilink1" href="'.wl($this->id, ['rev' => $rev]).'">'
221
                   . $this->id.' ['.dformat($rev).']'.'</a></bdi>';
222
        } else {
223
            $title = '&mdash;';
224
        }
225
        if (isset($info['current']) || ($rev && $rev == $INFO['currentrev'])) {
0 ignored issues
show
Bug introduced by
The variable $rev does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
226
            $title .= '&nbsp;('.$lang['current'].')';
227
        }
228
229
        // append separator
230
        $title .= ($this->preference['difftype'] === 'inline') ? ' ' : '<br />';
231
232
        // supplement
233
        if (isset($info['date'])) {
234
            $objRevInfo = (new PageRevisions($this->id))->getObjRevInfo($info);
235
            $title .= $objRevInfo->editSummary().' '.$objRevInfo->editor();
236
        }
237
        return $title;
238
    }
239
240
    /**
241
     * Print form to choose diff view type, and exact url reference to the view
242
     *
243
     * @param int $oldRev  timestamp of older revision, left side
244
     * @param int $newRev  timestamp of newer revision, right side
245
     */
246
    protected function showDiffViewSelector($oldRev, $newRev)
247
    {
248
        global $lang;
249
250
        echo '<div class="diffoptions group">';
251
252
        // create the form to select difftype
253
        $form = new Form(['action' => wl()]);
254
        $form->setHiddenField('id', $this->id);
255
        $form->setHiddenField('rev2[0]', $this->oldRev ?: 'current');
256
        $form->setHiddenField('rev2[1]', $this->newRev ?: 'current');
257
        $form->setHiddenField('do', 'diff');
258
        $options = array(
259
                     'sidebyside' => $lang['diff_side'],
260
                     'inline' => $lang['diff_inline'],
261
        );
262
        $input = $form->addDropdown('difftype', $options, $lang['diff_type'])
263
            ->val($this->preference['difftype'])
264
            ->addClass('quickselect');
265
        $input->useInput(false); // inhibit prefillInput() during toHTML() process
266
        $form->addButton('do[diff]', 'Go')->attr('type','submit');
267
        echo $form->toHTML();
268
269
        // show exact url reference to the view when it is meaningful
270
        echo '<p>';
271
        if (!isset($this->text) && $oldRev && $newRev) {
272
            // link to exactly this view FS#2835
273
            $viewUrl = $this->diffViewlink('difflink', $oldRev, $newRev);
274
        }
275
        echo $viewUrl ?? '<br />';
276
        echo '</p>';
277
278
        echo '</div>'; // .diffoptions
279
    }
280
281
    /**
282
     * Create html for revision navigation
283
     *
284
     * The navigation consists of older and newer revisions selectors, each
285
     * state mutually depends on the selected revision of opposite side.
286
     *
287
     * @param int $oldRev  timestamp of older revision, older side
288
     * @param int $newRev  timestamp of newer revision, newer side
289
     * @return string[] html of navigation for both older and newer sides
290
     */
291
    protected function buildRevisionsNavigation($oldRev, $newRev)
292
    {
293
        global $INFO;
294
295
        $changelog =& $this->changelog;
296
297
       // determine the last revision, which is usually the timestamp of current page,
298
       // however which might be the last revision if the page had removed.
299
        if (!$newRev) {
300
            if ($this->id == $INFO['id']) {
301
                // note: when page is removed, the metadata timestamp is zero
302
                $lastRev = $INFO['currentrev'] ?? $INFO['meta']['last_change']['date'] ?? 0;
303
            } else {
304
                $lastRevs = $changelog->getRevisions(-1, 1)  // empty array for removed page
0 ignored issues
show
Unused Code introduced by
$lastRevs is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
305
                          ?: $changelog->getRevisions(0, 1); // last entry of changelog
306
                $lastRev = count($last_revs) > 0 ? $last_revs[0] : 0;
0 ignored issues
show
Bug introduced by
The variable $last_revs does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
307
            }
308
            $newRev = $lastRev;
309
        }
310
311
        // retrieve revisions with additional info
312
        list($oldRevs, $newRevs) = $changelog->getRevisionsAround($oldRev, $newRev);
313
314
        // build options for dropdown selector 
315
        $olderRevisions = $this->buildRevisionOptions('older', $oldRevs, $oldRev, $newRev);
316
        $newerRevisions = $this->buildRevisionOptions('newer', $newRevs, $oldRev, $newRev);
317
318
        //determine previous/next revisions
319
        $index = array_search($oldRev, $oldRevs);
320
        $oldPrevRev = $oldRevs[$index + 1];
321
        $oldNextRev = $oldRevs[$index - 1];
322
        if ($newRev) {
323
            $index = array_search($newRev, $newRevs);
324
            $newPrevRev = $newRevs[$index + 1];
325
            $newNextRev = $newRevs[$index - 1];
326
        } else {
327
            //removed page
328
            $newPrevRev = ($oldNextRev) ? $newRevs[0] : null;
329
            $newNextRev = null;
330
        }
331
332
        /*
333
         * navigation UI for older revisions / Left side:
334
         */
335
        $navOlderRevs = '';
336
        //move back
337
        if ($oldPrevRev) {
338
            $navOlderRevs .= $this->diffViewlink('diffbothprevrev', $oldPrevRev, $newPrevRev);
339
            $navOlderRevs .= $this->diffViewlink('diffprevrev', $oldPrevRev, $newRev);
340
        }
341
        //dropdown
342
        $navOlderRevs .= $this->buildDropdownSelector('older', $olderRevisions, $oldRev, $newRev);
343
        //move forward
344
        if ($oldNextRev && ($oldNextRev < $newRev || !$newRev)) {
345
            $navOlderRevs .= $this->diffViewlink('diffnextrev', $oldNextRev, $newRev);
346
        }
347
348
        /*
349
         * navigation UI for newer revisions / Right side:
350
         */
351
        $navNewerRevs = '';
352
        //move back
353
        if ($oldRev < $newPrevRev) {
354
            $navNewerRevs .= $this->diffViewlink('diffprevrev', $oldRev, $newPrevRev);
355
        }
356
        //dropdown
357
        $navNewerRevs .= $this->buildDropdownSelector('newer', $newerRevisions, $oldRev, $newRev);
358
        //move forward
359
        if ($newNextRev) {
360
            if ($changelog->isCurrentRevision($newNextRev)) {
361
                //last revision is diff with current page
362
                $navNewerRevs .= $this->diffViewlink('difflastrev', $oldRev);
363
            } else {
364
                $navNewerRevs .= $this->diffViewlink('diffnextrev', $oldRev, $newNextRev);
365
            }
366
            $navNewerRevs .= $this->diffViewlink('diffbothnextrev', $oldNextRev, $newNextRev);
367
        }
368
        return array($navOlderRevs, $navNewerRevs);
369
    }
370
371
    /**
372
     * prepare options for dropdwon selector
373
     *
374
     * @params string $side  "older" or "newer"
375
     * @params array $revs  list of revsion
376
     * @param int $oldRev  timestamp of older revision, left side
377
     * @param int $newRev  timestamp of newer revision, right side
378
     * @return array
379
     */
380
    protected function buildRevisionOptions($side, $revs, $oldRev, $newRev)
381
    {
382
        $changelog =& $this->changelog;
383
        $revisions = array();
384
385
        if (($side == 'older' && !$oldRev) // NOTE: this case should not happen!
386
          ||($side == 'newer' && (!$newRev || page_exists($this->id) == false))
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
387
        ) {
388
            //no revision given, likely removed page, add dummy entry
389
            $revisions['current'] = array(
390
                'label' => '—', // U+2014 &mdash;
391
                'attrs' => [],
392
            );
393
        }
394
395
        foreach ($revs as $rev) {
396
            $info = $changelog->getRevisionInfo($rev);
397
            $revisions[$rev] = array(
398
                'label' => implode(' ', [
399
                            dformat($info['date']),
400
                            editorinfo($info['user'], true),
401
                            $info['sum'],
402
                           ]),
403
                'attrs' => ['title' => $rev],
404
            );
405
            if (($side == 'older' && ($newRev ? $rev >= $newRev : false))
406
              ||($side == 'newer' && ($rev <= $oldRev))
407
            ) {
408
                $revisions[$rev]['attrs']['disabled'] = 'disabled';
409
            }
410
        }
411
        return $revisions;
412
    }
413
414
    /**
415
     * build Dropdown form for revisions navigation
416
     *
417
     * @params string $side  "older" or "newer"
418
     * @params array $options  dropdown options
419
     * @param int $oldRev  timestamp of older revision, left side
420
     * @param int $newRev  timestamp of newer revision, right side
421
     * @return sting
422
     */
423
    protected function buildDropdownSelector($side, $options, $oldRev, $newRev)
424
    {
425
        $form = new Form(['action' => wl($this->id)]);
426
        $form->setHiddenField('id', $this->id);
427
        $form->setHiddenField('do', 'diff');
428
        $form->setHiddenField('difftype', $this->preference['difftype']);
429
430
        switch ($side) {
431
            case 'older': // left side
432
                $form->setHiddenField('rev2[1]', $newRev ?: 'current');
433
                $input = $form->addDropdown('rev2[0]', $options)
434
                    ->val($oldRev ?: 'current')->addClass('quickselect');
435
                $input->useInput(false); // inhibit prefillInput() during toHTML() process
436
                break;
437
            case 'newer': // right side
438
                $form->setHiddenField('rev2[0]', $oldRev ?: 'current');
439
                $input = $form->addDropdown('rev2[1]', $options)
440
                    ->val($newRev ?: 'current')->addClass('quickselect');
441
                $input->useInput(false); // inhibit prefillInput() during toHTML() process
442
                break;
443
        }
444
        $form->addButton('do[diff]', 'Go')->attr('type','submit');
445
        return $form->toHTML();
446
    }
447
448
    /**
449
     * Create html link to a diff view defined by two revisions
450
     *
451
     * @param string $linktype
452
     * @param int $oldRev older revision
453
     * @param int $newRev newer revision or null for diff with current revision
454
     * @return string html of link to a diff view
455
     */
456
    protected function diffViewlink($linktype, $oldRev, $newRev = null)
457
    {
458
        global $lang;
459
        if ($newRev === null) {
460
            $urlparam = array(
461
                'do' => 'diff',
462
                'rev' => $oldRev,
463
                'difftype' => $this->preference['difftype'],
464
            );
465
        } else {
466
            $urlparam = array(
467
                'do' => 'diff',
468
                'rev2[0]' => $oldRev,
469
                'rev2[1]' => $newRev,
470
                'difftype' => $this->preference['difftype'],
471
            );
472
        }
473
        $attr = array(
474
            'class' => $linktype,
475
            'href'  => wl($this->id, $urlparam, true, '&'),
476
            'title' => $lang[$linktype],
477
        );
478
        return '<a '. buildAttributes($attr) .'><span>'. $lang[$linktype] .'</span></a>';
479
    }
480
481
482
    /**
483
     * Insert soft breaks in diff html
484
     *
485
     * @param string $diffhtml
486
     * @return string
487
     */
488
    public function insertSoftbreaks($diffhtml)
489
    {
490
        // search the diff html string for both:
491
        // - html tags, so these can be ignored
492
        // - long strings of characters without breaking characters
493
        return preg_replace_callback('/<[^>]*>|[^<> ]{12,}/', function ($match) {
494
            // if match is an html tag, return it intact
495
            if ($match[0][0] == '<') return $match[0];
496
            // its a long string without a breaking character,
497
            // make certain characters into breaking characters by inserting a
498
            // word break opportunity (<wbr> tag) in front of them.
499
            $regex = <<< REGEX
500
(?(?=              # start a conditional expression with a positive look ahead ...
501
&\#?\\w{1,6};)     # ... for html entities - we don't want to split them (ok to catch some invalid combinations)
502
&\#?\\w{1,6};      # yes pattern - a quicker match for the html entity, since we know we have one
503
|
504
[?/,&\#;:]         # no pattern - any other group of 'special' characters to insert a breaking character after
505
)+                 # end conditional expression
506
REGEX;
507
            return preg_replace('<'.$regex.'>xu', '\0<wbr>', $match[0]);
508
        }, $diffhtml);
509
    }
510
511
}
512