Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
27 / 27 |
SplitConflictMerger | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
12 | |
100.00% |
27 / 27 |
mergeSplitConflictResults | |
100.00% |
1 / 1 |
7 | |
100.00% |
18 / 18 |
|||
pickBestPossibleValue | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
parseExtraLineFeeds | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
lineFeeds | |
100.00% |
1 / 1 |
3 | |
100.00% |
4 / 4 |
<?php | |
namespace TwoColConflict; | |
/** | |
* @license GPL-2.0-or-later | |
* @author Thiemo Kreuz | |
*/ | |
class SplitConflictMerger { | |
/** | |
* @param array[] $contentRows | |
* @param array[] $extraLineFeeds | |
* @param string[]|string $sideSelection Either an array of side identifiers per row ("copy", | |
* "other", or "your"). Or one side identifier for all rows (either "other" or "your"). | |
* | |
* @return string Wikitext | |
*/ | |
public function mergeSplitConflictResults( | |
array $contentRows, | |
array $extraLineFeeds, | |
$sideSelection | |
): string { | |
$textLines = []; | |
foreach ( $contentRows as $num => $row ) { | |
if ( is_array( $sideSelection ) ) { | |
// There was no selection to be made for "copy" rows in the interface | |
$side = $sideSelection[$num] ?? 'copy'; | |
} else { | |
$side = isset( $row['copy'] ) ? 'copy' : $sideSelection; | |
} | |
$line = $this->pickBestPossibleValue( $row, $side ); | |
// Don't remove all whitespace, because this is not necessarily the end of the article | |
$line = rtrim( $line, "\r\n" ); | |
// *Possibly* emptied by the user, or the line was empty before | |
$emptiedByUser = $line === ''; | |
if ( isset( $extraLineFeeds[$num] ) ) { | |
$value = $this->pickBestPossibleValue( $extraLineFeeds[$num], $side ); | |
[ $before, $after ] = $this->parseExtraLineFeeds( $value ); | |
// We want to understand the difference between a row the user emptied (extra | |
// linefeeds are removed as well then), or a row that was empty before. This is | |
// how HtmlEditableTextComponent marked empty rows. | |
if ( $before === 'was-empty' ) { | |
$emptiedByUser = false; | |
} else { | |
$line = $this->lineFeeds( $before ) . $line; | |
} | |
$line .= $this->lineFeeds( $after ); | |
} | |
// In case a line was emptied, we need to skip the extra linefeeds as well | |
if ( !$emptiedByUser ) { | |
$textLines[] = $line; | |
} | |
} | |
return SplitConflictUtils::mergeTextLines( $textLines ); | |
} | |
/** | |
* @param string[]|mixed $postedValues Typically an array of strings, but not guaranteed | |
* @param string $key Preferred array key to pick from the list of values, if present | |
* | |
* @return string | |
*/ | |
private function pickBestPossibleValue( $postedValues, string $key ): string { | |
// A mismatch here means the request is either incomplete (by design) or broken, and already | |
// detected as such (see ConflictFormValidator). Intentionally return the most recent, most | |
// conflicting value. Fall back to the users unsaved edit, or to *whatever* is there, no | |
// matter how invalid it might be. We *never* want to loose anything. | |
return (string)( | |
$postedValues[$key] ?? | |
$postedValues['your'] ?? | |
current( (array)$postedValues ) | |
); | |
} | |
/** | |
* @param string $postedValue | |
* | |
* @return string[] | |
*/ | |
private function parseExtraLineFeeds( string $postedValue ): array { | |
$counts = explode( ',', $postedValue, 2 ); | |
// "Before" and "after" are intentionally flipped, because "before" is very rare | |
return [ $counts[1] ?? '', $counts[0] ]; | |
} | |
private function lineFeeds( string $count ): string { | |
$count = (int)$count; | |
// Arbitrary limit just to not end with megabytes in case of an attack | |
if ( $count <= 0 || $count > 1000 ) { | |
return ''; | |
} | |
return str_repeat( "\n", $count ); | |
} | |
} |