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 Language;
7use MediaWiki\Output\OutputPage;
8use MessageLocalizer;
9use UnexpectedValueException;
10
11/**
12 * A TextDiffer which acts as a container for other TextDiffers, and dispatches
13 * requests to them.
14 *
15 * @since 1.41
16 */
17class ManifoldTextDiffer implements TextDiffer {
18    /** @var MessageLocalizer */
19    private $localizer;
20    /** @var Language|null */
21    private $contentLanguage;
22    /** @var string|null */
23    private $diffEngine;
24    /** @var string|false|null */
25    private $externalPath;
26    /** @var TextDiffer[]|null Differs in order of priority, from highest to lowest */
27    private $differs;
28    /** @var TextDiffer[]|null The differ to use for each format */
29    private $differsByFormat;
30    /** @var array */
31    private $wikidiff2Options;
32
33    /**
34     * @internal For use by DifferenceEngine, ContentHandler
35     *
36     * @param MessageLocalizer $localizer
37     * @param Language|null $contentLanguage
38     * @param string|null $diffEngine The DiffEngine config variable
39     * @param string|false|null $externalPath The ExternalDiffEngine config variable
40     * @param array $wikidiff2Options The Wikidiff2Options config variable
41     */
42    public function __construct(
43        MessageLocalizer $localizer,
44        ?Language $contentLanguage,
45        $diffEngine,
46        $externalPath,
47        $wikidiff2Options
48    ) {
49        $this->localizer = $localizer;
50        $this->contentLanguage = $contentLanguage;
51        $this->diffEngine = $diffEngine;
52        $this->externalPath = $externalPath;
53        $this->wikidiff2Options = $wikidiff2Options;
54    }
55
56    public function getName(): string {
57        return 'manifold';
58    }
59
60    public function getFormats(): array {
61        $differs = $this->getDiffersByFormat();
62        return array_keys( $differs );
63    }
64
65    public function hasFormat( string $format ): bool {
66        $differs = $this->getDiffersByFormat();
67        return isset( $differs[$format] );
68    }
69
70    public function render( string $oldText, string $newText, string $format ): string {
71        if ( !in_array( $format, $this->getFormats(), true ) ) {
72            throw new \InvalidArgumentException(
73                'The requested format is not supported by this engine' );
74        }
75        $results = $this->renderBatch( $oldText, $newText, [ $format ] );
76        return reset( $results );
77    }
78
79    public function renderBatch( string $oldText, string $newText, array $formats ): array {
80        $result = [];
81        $differs = $this->splitBatchByDiffer( $formats );
82        /** @var TextDiffer $differ */
83        foreach ( $differs as [ $differ, $formatBatch ] ) {
84            $result += $differ->renderBatch( $oldText, $newText, $formatBatch );
85        }
86        return $result;
87    }
88
89    public function getFormatContext( string $format ) {
90        return $this->getDifferForFormat( $format )->getFormatContext( $format );
91    }
92
93    public function addRowWrapper( string $format, string $diffText ): string {
94        return $this->getDifferForFormat( $format )->addRowWrapper( $format, $diffText );
95    }
96
97    public function addModules( OutputPage $out, string $format ): void {
98        $this->getDifferForFormat( $format )->addModules( $out, $format );
99    }
100
101    public function getCacheKeys( array $formats ): array {
102        $keys = [];
103        $engines = [];
104        $differs = $this->splitBatchByDiffer( $formats );
105        /** @var TextDiffer $differ */
106        foreach ( $differs as [ $differ, $formatBatch ] ) {
107            $keys += $differ->getCacheKeys( $formatBatch );
108            $engines[] = $differ->getName() . '=' . implode( ',', $formatBatch );
109        }
110        $keys['10-formats-and-engines'] = implode( ';', $engines );
111        return $keys;
112    }
113
114    public function localize( string $format, string $diff, array $options = [] ): string {
115        return $this->getDifferForFormat( $format )->localize( $format, $diff, $options );
116    }
117
118    public function getTablePrefixes( string $format ): array {
119        return $this->getDifferForFormat( $format )->getTablePrefixes( $format );
120    }
121
122    public function getPreferredFormatBatch( string $format ): array {
123        return $this->getDifferForFormat( $format )->getPreferredFormatBatch( $format );
124    }
125
126    /**
127     * @return TextDiffer[]
128     */
129    private function getDiffersByFormat() {
130        if ( $this->differsByFormat === null ) {
131            $differs = [];
132            foreach ( $this->getDiffers() as $differ ) {
133                foreach ( $differ->getFormats() as $format ) {
134                    // getDiffers() is in order of priority -- don't overwrite
135                    $differs[$format] ??= $differ;
136                }
137            }
138            $this->differsByFormat = $differs;
139        }
140        return $this->differsByFormat;
141    }
142
143    /**
144     * @param string $format
145     * @return TextDiffer
146     */
147    private function getDifferForFormat( $format ) {
148        $differs = $this->getDiffersByFormat();
149        if ( !isset( $differs[$format] ) ) {
150            throw new \InvalidArgumentException(
151                "Unknown format \"$format\""
152            );
153        }
154        return $differs[$format];
155    }
156
157    /**
158     * Disable text differs apart from the one with the given name.
159     *
160     * @param string $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     * @param TextDiffer $differ
216     */
217    public function injectDeps( TextDiffer $differ ) {
218        if ( $differ instanceof BaseTextDiffer ) {
219            $differ->setLocalizer( $this->localizer );
220        }
221    }
222
223    /**
224     * Create a TextDiffer by engine name. If it can't be created due to a
225     * configuration or platform issue, return null and set $failureReason.
226     *
227     * @param string $engine
228     * @param string &$failureReason Out param which will be set to the failure reason
229     * @return TextDiffer|null
230     */
231    private function maybeCreateDiffer( $engine, &$failureReason ) {
232        switch ( $engine ) {
233            case 'external':
234                if ( is_string( $this->externalPath ) ) {
235                    if ( is_executable( $this->externalPath ) ) {
236                        return new ExternalTextDiffer(
237                            $this->externalPath
238                        );
239                    }
240                    $failureReason = 'ExternalDiffEngine config points to a non-executable';
241                } elseif ( $this->externalPath ) {
242                    $failureReason = 'ExternalDiffEngine config is set to a non-string value';
243                } else {
244                    return null;
245                }
246                wfWarn( "$failureReason, ignoring" );
247                return null;
248
249            case 'wikidiff2':
250                if ( Wikidiff2TextDiffer::isInstalled() ) {
251                    return new Wikidiff2TextDiffer(
252                        $this->wikidiff2Options
253                    );
254                }
255                $failureReason = 'wikidiff2 is not available';
256                return null;
257
258            case 'php':
259                // Always available.
260                return new PhpTextDiffer(
261                    $this->contentLanguage
262                );
263
264            default:
265                throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
266        }
267    }
268
269    /**
270     * Given an array of formats, break it down by the TextDiffer object which
271     * will handle each format. Each element of the result array is a list in
272     * which the first element is the TextDiffer object, and the second element
273     * is the list of formats which the TextDiffer will handle.
274     *
275     * @param array $formats
276     * @return array|array{0:TextDiffer,1:string[]}
277     */
278    private function splitBatchByDiffer( $formats ) {
279        $result = [];
280        foreach ( $formats as $format ) {
281            $differ = $this->getDifferForFormat( $format );
282            $name = $differ->getName();
283            if ( isset( $result[$name] ) ) {
284                $result[$name][1][] = $format;
285            } else {
286                $result[$name] = [ $differ, [ $format ] ];
287            }
288        }
289        return array_values( $result );
290    }
291}