Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.86% covered (success)
91.86%
79 / 86
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
DiffParser
91.86% covered (success)
91.86%
79 / 86
40.00% covered (danger)
40.00%
2 / 5
21.24
0.00% covered (danger)
0.00%
0 / 1
 getChangeSet
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getChangeSetFromEmptyLeft
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 usingInternalDiff
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parse
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
3.00
 parseLine
88.37% covered (warning)
88.37%
38 / 43
0.00% covered (danger)
0.00%
0 / 1
14.31
1<?php
2
3namespace MediaWiki\Extension\Notifications;
4
5/**
6 * MediaWiki Extension: Echo
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a copy
9 * of this software and associated documentation files (the "Software"), to deal
10 * in the Software without restriction, including without limitation the rights
11 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 * copies of the Software, and to permit persons to whom the Software is
13 * furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * This program is distributed WITHOUT ANY WARRANTY.
19 */
20
21/**
22 * @file
23 * @ingroup Extensions
24 * @author Erik Bernhardson
25 */
26
27use UnexpectedValueException;
28use Wikimedia\Diff\Diff;
29use Wikimedia\Diff\UnifiedDiffFormatter;
30
31/**
32 * Calculates the individual sets of differences between two pieces of text
33 * as individual groupings of add, subtract, and change actions. Internally
34 * uses 0-indexing for positions.  All results from the class are 1 indexed
35 * to stay consistent with the original diff output and the previous diff
36 * parsing code.
37 */
38class DiffParser {
39
40    /**
41     * @var int The number of characters the diff prefixes a line with
42     */
43    protected $prefixLength = 1;
44
45    /**
46     * @var string[] The text of the left side of the diff operation
47     */
48    protected $left;
49
50    /**
51     * @var int The current position within the left text
52     */
53    protected $leftPos;
54
55    /**
56     * @var string[] The text of the right side of the diff operation
57     */
58    protected $right;
59
60    /**
61     * @var int The current position within the right text
62     */
63    protected $rightPos;
64
65    /**
66     * @var array[] Set of add, subtract, or change operations within the diff
67     */
68    protected $changeSet;
69
70    /**
71     * Get the set of add, subtract, and change operations required to transform leftText into rightText
72     *
73     * @param string $leftText The left, or old, revision of the text
74     * @param string $rightText The right, or new, revision of the text
75     * @return array[] Array of arrays containing changes to individual groups of lines within the text
76     * Each change consists of:
77     * An 'action', one of:
78     * - add
79     * - subtract
80     * - change
81     * 'content' that was added or removed, or in the case
82     *     of a change, 'old_content' and 'new_content'
83     * 'left_pos' and 'right_pos' (in 1-indexed lines) of the change.
84     */
85    public function getChangeSet( $leftText, $rightText ) {
86        $left = trim( $leftText );
87        $right = trim( $rightText );
88
89        if ( $left === '' ) {
90            // fixes T155998
91            return $this->getChangeSetFromEmptyLeft( $right );
92        }
93
94        $diffs = new Diff( explode( "\n", $left ), explode( "\n", $right ) );
95        $format = new UnifiedDiffFormatter();
96        $diff = $format->format( $diffs );
97
98        return $this->parse( $diff, $left, $right );
99    }
100
101    /**
102     * If we add content to an empty page the changeSet can be composed straightaway
103     *
104     * @param string $right
105     * @return array[] See {@see getChangeSet}
106     */
107    private function getChangeSetFromEmptyLeft( $right ) {
108        $rightLines = explode( "\n", $right );
109
110        return [
111            '_info' => [
112                'lhs-length' => 1,
113                'rhs-length' => count( $rightLines ),
114                'lhs' => [ '' ],
115                'rhs' => $rightLines
116            ],
117            [
118                'right-pos' => 1,
119                'left-pos' => 1,
120                'action' => 'add',
121                'content' => $right,
122            ]
123        ];
124    }
125
126    /**
127     * Duplicates the check from the global wfDiff function to determine
128     * if we are using internal or external diff utilities
129     *
130     * @deprecated since 1.29, the internal diff parser is always used
131     * @return bool
132     */
133    protected static function usingInternalDiff() {
134        return true;
135    }
136
137    /**
138     * Parse the unified diff output into an array of changes to individual groups of the text
139     *
140     * @param string $diff The unified diff output
141     * @param string $left The left side of the diff used for sanity checks
142     * @param string $right The right side of the diff used for sanity checks
143     *
144     * @return array[]
145     */
146    protected function parse( $diff, $left, $right ) {
147        $this->left = explode( "\n", $left );
148        $this->right = explode( "\n", $right );
149        $diff = explode( "\n", $diff );
150
151        $this->leftPos = 0;
152        $this->rightPos = 0;
153        $this->changeSet = [
154            '_info' => [
155                'lhs-length' => count( $this->left ),
156                'rhs-length' => count( $this->right ),
157                'lhs' => $this->left,
158                'rhs' => $this->right,
159            ],
160        ];
161
162        $change = null;
163        foreach ( $diff as $line ) {
164            $change = $this->parseLine( $line, $change );
165        }
166        if ( $change === null ) {
167            return $this->changeSet;
168        }
169
170        return array_merge( $this->changeSet, $change->getChangeSet() );
171    }
172
173    /**
174     * Parse the next line of the unified diff output
175     *
176     * @param string $line The next line of the unified diff
177     * @param DiffGroup|null $change Changes the immediately previous lines
178     *
179     * @return DiffGroup|null Changes to this line and any changed lines immediately previous
180     */
181    protected function parseLine( $line, DiffGroup $change = null ) {
182        if ( $line ) {
183            $op = $line[0];
184            if ( strlen( $line ) > $this->prefixLength ) {
185                $line = substr( $line, $this->prefixLength );
186            } else {
187                $line = '';
188            }
189        } else {
190            $op = ' ';
191        }
192
193        switch ( $op ) {
194            case '@':
195                // metadata
196                if ( $change !== null ) {
197                    $this->changeSet = array_merge( $this->changeSet, $change->getChangeSet() );
198                    $change = null;
199                }
200                // @@ -start,numLines +start,numLines @@
201                [ , $left, $right ] = explode( ' ', $line, 3 );
202                [ $this->leftPos ] = explode( ',', substr( $left, 1 ), 2 );
203                [ $this->rightPos ] = explode( ',', substr( $right, 1 ), 2 );
204                $this->leftPos = (int)$this->leftPos;
205                $this->rightPos = (int)$this->rightPos;
206
207                // -1 because diff is 1 indexed, and we are 0 indexed
208                $this->leftPos--;
209                $this->rightPos--;
210                break;
211
212            case ' ':
213                // No changes
214                if ( $change !== null ) {
215                    $this->changeSet = array_merge( $this->changeSet, $change->getChangeSet() );
216                    $change = null;
217                }
218                $this->leftPos++;
219                $this->rightPos++;
220                break;
221
222            case '-':
223                // subtract
224                if ( $this->left[$this->leftPos] !== $line ) {
225                    throw new UnexpectedValueException( 'Positional error: left' );
226                }
227                if ( $change === null ) {
228                    // @phan-suppress-next-line PhanTypeMismatchArgument
229                    $change = new DiffGroup( $this->leftPos, $this->rightPos );
230                }
231                $change->subtract( $line );
232                $this->leftPos++;
233                break;
234
235            case '+':
236                // add
237                if ( $this->right[$this->rightPos] !== $line ) {
238                    throw new UnexpectedValueException( 'Positional error: right' );
239                }
240                if ( $change === null ) {
241                    // @phan-suppress-next-line PhanTypeMismatchArgument
242                    $change = new DiffGroup( $this->leftPos, $this->rightPos );
243                }
244                $change->add( $line );
245                $this->rightPos++;
246                break;
247
248            default:
249                throw new UnexpectedValueException( 'Unknown Diff Operation: ' . $op );
250        }
251
252        return $change;
253    }
254}