Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
29 / 29 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
SplitConflictMerger | |
100.00% |
29 / 29 |
|
100.00% |
4 / 4 |
12 | |
100.00% |
1 / 1 |
mergeSplitConflictResults | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
7 | |||
pickBestPossibleValue | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
parseExtraLineFeeds | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
lineFeeds | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace TwoColConflict; |
4 | |
5 | /** |
6 | * @license GPL-2.0-or-later |
7 | * @author Thiemo Kreuz |
8 | */ |
9 | class SplitConflictMerger { |
10 | |
11 | /** |
12 | * @param array[] $contentRows |
13 | * @param array[] $extraLineFeeds |
14 | * @param string[]|string $sideSelection Either an array of side identifiers per row ("copy", |
15 | * "other", or "your"). Or one side identifier for all rows (either "other" or "your"). |
16 | * |
17 | * @return string Wikitext |
18 | */ |
19 | public function mergeSplitConflictResults( |
20 | array $contentRows, |
21 | array $extraLineFeeds, |
22 | $sideSelection |
23 | ): string { |
24 | $textLines = []; |
25 | |
26 | foreach ( $contentRows as $num => $row ) { |
27 | if ( is_array( $sideSelection ) ) { |
28 | // There was no selection to be made for "copy" rows in the interface |
29 | $side = $sideSelection[$num] ?? 'copy'; |
30 | } else { |
31 | $side = isset( $row['copy'] ) ? 'copy' : $sideSelection; |
32 | } |
33 | |
34 | $line = $this->pickBestPossibleValue( $row, $side ); |
35 | // Don't remove all whitespace, because this is not necessarily the end of the article |
36 | $line = rtrim( $line, "\r\n" ); |
37 | // *Possibly* emptied by the user, or the line was empty before |
38 | $emptiedByUser = $line === ''; |
39 | |
40 | if ( isset( $extraLineFeeds[$num] ) ) { |
41 | $value = $this->pickBestPossibleValue( $extraLineFeeds[$num], $side ); |
42 | [ $before, $after ] = $this->parseExtraLineFeeds( $value ); |
43 | // We want to understand the difference between a row the user emptied (extra |
44 | // linefeeds are removed as well then), or a row that was empty before. This is |
45 | // how HtmlEditableTextComponent marked empty rows. |
46 | if ( $before === 'was-empty' ) { |
47 | $emptiedByUser = false; |
48 | } else { |
49 | $line = $this->lineFeeds( $before ) . $line; |
50 | } |
51 | $line .= $this->lineFeeds( $after ); |
52 | } |
53 | |
54 | // In case a line was emptied, we need to skip the extra linefeeds as well |
55 | if ( !$emptiedByUser ) { |
56 | $textLines[] = $line; |
57 | } |
58 | } |
59 | return SplitConflictUtils::mergeTextLines( $textLines ); |
60 | } |
61 | |
62 | /** |
63 | * @param string[]|mixed $postedValues Typically an array of strings, but not guaranteed |
64 | * @param string $key Preferred array key to pick from the list of values, if present |
65 | * |
66 | * @return string |
67 | */ |
68 | private function pickBestPossibleValue( $postedValues, string $key ): string { |
69 | // A mismatch here means the request is either incomplete (by design) or broken, and already |
70 | // detected as such (see ConflictFormValidator). Intentionally return the most recent, most |
71 | // conflicting value. Fall back to the users unsaved edit, or to *whatever* is there, no |
72 | // matter how invalid it might be. We *never* want to loose anything. |
73 | return (string)( |
74 | $postedValues[$key] ?? |
75 | $postedValues['your'] ?? |
76 | current( (array)$postedValues ) |
77 | ); |
78 | } |
79 | |
80 | /** |
81 | * @param string $postedValue |
82 | * |
83 | * @return string[] |
84 | */ |
85 | private function parseExtraLineFeeds( string $postedValue ): array { |
86 | $counts = explode( ',', $postedValue, 2 ); |
87 | // "Before" and "after" are intentionally flipped, because "before" is very rare |
88 | return [ $counts[1] ?? '', $counts[0] ]; |
89 | } |
90 | |
91 | private function lineFeeds( string $count ): string { |
92 | $count = (int)$count; |
93 | |
94 | // Arbitrary limit just to not end with megabytes in case of an attack |
95 | if ( $count <= 0 || $count > 1000 ) { |
96 | return ''; |
97 | } |
98 | |
99 | return str_repeat( "\n", $count ); |
100 | } |
101 | |
102 | } |