Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.31% covered (warning)
69.31%
70 / 101
42.86% covered (danger)
42.86%
9 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
ManifoldTextDiffer
69.31% covered (warning)
69.31%
70 / 101
42.86% covered (danger)
42.86%
9 / 21
99.98
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFormats
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasFormat
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 render
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
2.26
 renderBatch
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getFormatContext
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addRowWrapper
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addModules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCacheKeys
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 localize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTablePrefixes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPreferredFormatBatch
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDiffersByFormat
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getDifferForFormat
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
 setEngine
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getEngineForFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDiffers
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
6.07
 injectDeps
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 maybeCreateDiffer
70.83% covered (warning)
70.83%
17 / 24
0.00% covered (danger)
0.00%
0 / 1
11.01
 splitBatchByDiffer
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\Diff\TextDiffer;
4
5use DomainException;
6use InvalidArgumentException;
7use MediaWiki\Language\Language;
8use MediaWiki\Language\MessageLocalizer;
9use MediaWiki\Output\OutputPage;
10use UnexpectedValueException;
11
12/**
13 * A TextDiffer which acts as a container for other TextDiffers, and dispatches
14 * requests to them.
15 *
16 * @since 1.41
17 */
18class ManifoldTextDiffer implements TextDiffer {
19    /** @var MessageLocalizer */
20    private $localizer;
21    /** @var Language|null */
22    private $contentLanguage;
23    /** @var string|null */
24    private $diffEngine;
25    /** @var string|false|null */
26    private $externalPath;
27    /** @var TextDiffer[]|null Differs in order of priority, from highest to lowest */
28    private $differs;
29    /** @var TextDiffer[]|null The differ to use for each format */
30    private $differsByFormat;
31    /** @var array */
32    private $wikidiff2Options;
33
34    /**
35     * @internal For use by DifferenceEngine, ContentHandler
36     *
37     * @param MessageLocalizer $localizer
38     * @param Language|null $contentLanguage
39     * @param string|null $diffEngine The DiffEngine config variable
40     * @param string|false|null $externalPath The ExternalDiffEngine config variable
41     * @param array $wikidiff2Options The Wikidiff2Options config variable
42     */
43    public function __construct(
44        MessageLocalizer $localizer,
45        ?Language $contentLanguage,
46        $diffEngine,
47        $externalPath,
48        $wikidiff2Options
49    ) {
50        $this->localizer = $localizer;
51        $this->contentLanguage = $contentLanguage;
52        $this->diffEngine = $diffEngine;
53        $this->externalPath = $externalPath;
54        $this->wikidiff2Options = $wikidiff2Options;
55    }
56
57    public function getName(): string {
58        return 'manifold';
59    }
60
61    public function getFormats(): array {
62        $differs = $this->getDiffersByFormat();
63        return array_keys( $differs );
64    }
65
66    public function hasFormat( string $format ): bool {
67        $differs = $this->getDiffersByFormat();
68        return isset( $differs[$format] );
69    }
70
71    public function render( string $oldText, string $newText, string $format ): string {
72        if ( !in_array( $format, $this->getFormats(), true ) ) {
73            throw new InvalidArgumentException(
74                'The requested format is not supported by this engine' );
75        }
76        $results = $this->renderBatch( $oldText, $newText, [ $format ] );
77        return reset( $results );
78    }
79
80    public function renderBatch( string $oldText, string $newText, array $formats ): array {
81        $result = [];
82        $differs = $this->splitBatchByDiffer( $formats );
83        /** @var TextDiffer $differ */
84        foreach ( $differs as [ $differ, $formatBatch ] ) {
85            $result += $differ->renderBatch( $oldText, $newText, $formatBatch );
86        }
87        return $result;
88    }
89
90    /** @inheritDoc */
91    public function getFormatContext( string $format ) {
92        return $this->getDifferForFormat( $format )->getFormatContext( $format );
93    }
94
95    public function addRowWrapper( string $format, string $diffText ): string {
96        return $this->getDifferForFormat( $format )->addRowWrapper( $format, $diffText );
97    }
98
99    public function addModules( OutputPage $out, string $format ): void {
100        $this->getDifferForFormat( $format )->addModules( $out, $format );
101    }
102
103    public function getCacheKeys( array $formats ): array {
104        $keys = [];
105        $engines = [];
106        $differs = $this->splitBatchByDiffer( $formats );
107        /** @var TextDiffer $differ */
108        foreach ( $differs as [ $differ, $formatBatch ] ) {
109            $keys += $differ->getCacheKeys( $formatBatch );
110            $engines[] = $differ->getName() . '=' . implode( ',', $formatBatch );
111        }
112        $keys['10-formats-and-engines'] = implode( ';', $engines );
113        return $keys;
114    }
115
116    public function localize( string $format, string $diff, array $options = [] ): string {
117        return $this->getDifferForFormat( $format )->localize( $format, $diff, $options );
118    }
119
120    public function getTablePrefixes( string $format ): array {
121        return $this->getDifferForFormat( $format )->getTablePrefixes( $format );
122    }
123
124    public function getPreferredFormatBatch( string $format ): array {
125        return $this->getDifferForFormat( $format )->getPreferredFormatBatch( $format );
126    }
127
128    /**
129     * @return TextDiffer[]
130     */
131    private function getDiffersByFormat() {
132        if ( $this->differsByFormat === null ) {
133            $differs = [];
134            foreach ( $this->getDiffers() as $differ ) {
135                foreach ( $differ->getFormats() as $format ) {
136                    // getDiffers() is in order of priority -- don't overwrite
137                    $differs[$format] ??= $differ;
138                }
139            }
140            $this->differsByFormat = $differs;
141        }
142        return $this->differsByFormat;
143    }
144
145    /**
146     * @param string $format
147     * @return TextDiffer
148     */
149    private function getDifferForFormat( $format ) {
150        $differs = $this->getDiffersByFormat();
151        if ( !isset( $differs[$format] ) ) {
152            throw new InvalidArgumentException(
153                "Unknown format \"$format\""
154            );
155        }
156        return $differs[$format];
157    }
158
159    /**
160     * Disable text differs apart from the one with the given name.
161     */
162    public function setEngine( string $name ) {
163        $this->diffEngine = $name;
164        $this->differs = null;
165        $this->differsByFormat = null;
166    }
167
168    /**
169     * Get the text differ name which will be used for the specified format
170     *
171     * @param string $format
172     * @return string|null
173     */
174    public function getEngineForFormat( string $format ) {
175        return $this->getDifferForFormat( $format )->getName();
176    }
177
178    /**
179     * Get differs in a numerically indexed array. When a format is requested,
180     * the first TextDiffer in this array which can handle the format will be
181     * used.
182     *
183     * @return TextDiffer[]
184     */
185    private function getDiffers() {
186        if ( $this->differs === null ) {
187            $differs = [];
188            if ( $this->diffEngine === null ) {
189                $differNames = [ 'external', 'wikidiff2', 'php' ];
190            } else {
191                $differNames = [ $this->diffEngine ];
192            }
193            $failureReason = '';
194            foreach ( $differNames as $name ) {
195                $differ = $this->maybeCreateDiffer( $name, $failureReason );
196                if ( $differ ) {
197                    $this->injectDeps( $differ );
198                    $differs[] = $differ;
199                }
200            }
201            if ( !$differs ) {
202                throw new UnexpectedValueException(
203                    "Cannot use diff engine '{$this->diffEngine}': $failureReason" );
204            }
205            // TODO: add a hook here, allowing extensions to add differs
206            $this->differs = $differs;
207        }
208        return $this->differs;
209    }
210
211    /**
212     * Initialize an object which may be a subclass of BaseTextDiffer, passing
213     * down injected dependencies.
214     */
215    public function injectDeps( TextDiffer $differ ) {
216        if ( $differ instanceof BaseTextDiffer ) {
217            $differ->setLocalizer( $this->localizer );
218        }
219    }
220
221    /**
222     * Create a TextDiffer by engine name. If it can't be created due to a
223     * configuration or platform issue, return null and set $failureReason.
224     *
225     * @param string $engine
226     * @param string &$failureReason Out param which will be set to the failure reason
227     * @return TextDiffer|null
228     */
229    private function maybeCreateDiffer( $engine, &$failureReason ) {
230        switch ( $engine ) {
231            case 'external':
232                if ( is_string( $this->externalPath ) ) {
233                    if ( is_executable( $this->externalPath ) ) {
234                        return new ExternalTextDiffer(
235                            $this->externalPath
236                        );
237                    }
238                    $failureReason = 'ExternalDiffEngine config points to a non-executable';
239                } elseif ( $this->externalPath ) {
240                    $failureReason = 'ExternalDiffEngine config is set to a non-string value';
241                } else {
242                    return null;
243                }
244                wfWarn( "$failureReason, ignoring" );
245                return null;
246
247            case 'wikidiff2':
248                if ( Wikidiff2TextDiffer::isInstalled() ) {
249                    return new Wikidiff2TextDiffer(
250                        $this->wikidiff2Options
251                    );
252                }
253                $failureReason = 'wikidiff2 is not available';
254                return null;
255
256            case 'php':
257                // Always available.
258                return new PhpTextDiffer(
259                    $this->contentLanguage
260                );
261
262            default:
263                throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
264        }
265    }
266
267    /**
268     * Given an array of formats, break it down by the TextDiffer object which
269     * will handle each format. Each element of the result array is a list in
270     * which the first element is the TextDiffer object, and the second element
271     * is the list of formats which the TextDiffer will handle.
272     *
273     * @param string[] $formats
274     * @return array<array{0:TextDiffer,1:string[]}>
275     */
276    private function splitBatchByDiffer( $formats ) {
277        $result = [];
278        foreach ( $formats as $format ) {
279            $differ = $this->getDifferForFormat( $format );
280            $name = $differ->getName();
281            if ( isset( $result[$name] ) ) {
282                $result[$name][1][] = $format;
283            } else {
284                $result[$name] = [ $differ, [ $format ] ];
285            }
286        }
287        return array_values( $result );
288    }
289}