Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.08% covered (success)
96.08%
49 / 51
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
DefaultOutputPipelineFactory
96.08% covered (success)
96.08%
49 / 51
60.00% covered (warning)
60.00%
3 / 5
13
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
 buildDefaultOutputPipeline
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildLanguageConverterPipeline
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildPipeline
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
7
 makeExtraArgs
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\OutputTransform;
5
6use MediaWiki\Config\Config;
7use MediaWiki\Config\ServiceOptions;
8use MediaWiki\MainConfigNames;
9use MediaWiki\OutputTransform\Stages\AddRedirectHeader;
10use MediaWiki\OutputTransform\Stages\AddWrapperDivClass;
11use MediaWiki\OutputTransform\Stages\DeduplicateStylesDOM;
12use MediaWiki\OutputTransform\Stages\DeduplicateStylesText;
13use MediaWiki\OutputTransform\Stages\ExecuteFirstStageTransformHooks;
14use MediaWiki\OutputTransform\Stages\ExecuteLastStageTransformHooks;
15use MediaWiki\OutputTransform\Stages\ExecutePostCacheTransformHooks;
16use MediaWiki\OutputTransform\Stages\ExpandRelativeAttrs;
17use MediaWiki\OutputTransform\Stages\ExpandToAbsoluteUrlsDOM;
18use MediaWiki\OutputTransform\Stages\ExpandToAbsoluteUrlsText;
19use MediaWiki\OutputTransform\Stages\HandleParsoidSectionLinks;
20use MediaWiki\OutputTransform\Stages\HandleSectionLinks;
21use MediaWiki\OutputTransform\Stages\HandleTOCMarkersDOM;
22use MediaWiki\OutputTransform\Stages\HandleTOCMarkersText;
23use MediaWiki\OutputTransform\Stages\HardenNFC;
24use MediaWiki\OutputTransform\Stages\HydrateHeaderPlaceholders;
25use MediaWiki\OutputTransform\Stages\ParsoidLanguageConverter;
26use MediaWiki\OutputTransform\Stages\ParsoidLocalization;
27use MediaWiki\OutputTransform\Stages\RenderDebugInfo;
28use Psr\Log\LoggerInterface;
29use Wikimedia\ObjectFactory\ObjectFactory;
30
31/**
32 * This class contains the default output transformation pipeline factory for wikitext. It is a postprocessor for
33 * ParserOutput objects either directly resulting from a parse or fetched from ParserCache.
34 * @unstable
35 */
36class DefaultOutputPipelineFactory {
37
38    private ServiceOptions $options;
39    private Config $config;
40    private LoggerInterface $logger;
41    private ObjectFactory $objectFactory;
42
43    public const CONSTRUCTOR_OPTIONS = [
44        MainConfigNames::OutputPipelineStages,
45    ];
46
47    private const CORE_LIST = [
48        'ExecuteFirstStageTransformHooks' => [
49            'class' => ExecuteFirstStageTransformHooks::class,
50            'services' => [
51                'HookContainer',
52            ],
53        ],
54        'AddRedirectHeader' => [
55            'class' => AddRedirectHeader::class,
56        ],
57        'RenderDebugInfo' => [
58            'class' => RenderDebugInfo::class,
59            'services' => [
60                'HookContainer',
61            ],
62        ],
63        'ExecutePostCacheTransformHooks' => [
64            'class' => ExecutePostCacheTransformHooks::class,
65            'services' => [
66                'HookContainer',
67            ],
68        ],
69        'AddWrapperDivClass' => [
70            'class' => AddWrapperDivClass::class,
71            'services' => [
72                'LanguageFactory',
73                'ContentLanguage',
74            ],
75        ],
76        // The next five stages are all DOM-based passes. They are adjacent to each
77        // other to be able to skip unnecessary intermediate DOM->text->DOM transformations.
78        'ExpandRelativeAttrs' => [
79            'class' => ExpandRelativeAttrs::class,
80            'services' => [
81                'UrlUtils',
82                'ParsoidSiteConfig',
83                'TitleFormatter',
84            ],
85            'optional_services' => [
86                'MobileFrontend.Context',
87            ],
88        ],
89        // Messages in the user language are already localized to
90        // every variant separately, and don't need to be language
91        // converted.  Therefore, the LanguageConverter pass should be
92        // done before HandleSectionLinks so we don't language-convert
93        // the skin's section edit links (which are in user-interface
94        // language/variant) and before ParsoidLocalization so we
95        // don't try to convert messages which are already in the
96        // user's preferred variant (T416104).  This should also precede
97        // HandleTOCMarkers since we are going to localize the TOC
98        // at the end of this stage, whether or not language conversion
99        // was performed.
100        'ParsoidLanguageConverter' => [
101            'class' => ParsoidLanguageConverter::class,
102            'services' => [
103                'ParsoidSiteConfig',
104                'LanguageFactory',
105                'LanguageConverterFactory',
106                'TitleFactory',
107                'UrlUtils',
108                'LinkBatchFactory',
109            ]
110        ],
111        'HandleSectionLinks' => [
112            'textStage' => [
113                'class' => HandleSectionLinks::class,
114                'services' => [
115                    'TitleFactory',
116                ],
117            ],
118            'domStage' => [
119                'class' => HandleParsoidSectionLinks::class,
120                'services' => [
121                    'TitleFactory',
122                ],
123            ],
124            'exclusive' => true
125        ],
126        // This should be before DeduplicateStyles because some system messages may use TemplateStyles (so we
127        // want to expand them before deduplication).
128        'ParsoidLocalization' => [
129            'class' => ParsoidLocalization::class,
130            'services' => [
131                'LanguageFactory',
132            ]
133        ],
134        'HandleTOCMarkers' => [
135            'textStage' => [
136                'class' => HandleTOCMarkersText::class,
137                'services' => [
138                    'Tidy',
139                ],
140            ],
141            'domStage' => [
142                'class' => HandleTOCMarkersDOM::class,
143            ],
144            'exclusive' => false
145        ],
146        'DeduplicateStyles' => [
147            'textStage' => [
148                'class' => DeduplicateStylesText::class,
149            ],
150            'domStage' => [
151                'class' => DeduplicateStylesDOM::class,
152            ],
153            'exclusive' => false
154        ],
155
156        'ExpandToAbsoluteUrls' => [
157            'textStage' => [
158                'class' => ExpandToAbsoluteUrlsText::class,
159            ],
160            'domStage' => [
161                'class' => ExpandToAbsoluteUrlsDOM::class,
162                'services' => [
163                    'UrlUtils',
164                ],
165            ],
166            'exclusive' => false
167        ],
168
169        'HydrateHeaderPlaceholders' => [
170            'class' => HydrateHeaderPlaceholders::class,
171            'services' => [
172                'SlotRoleRegistry',
173            ]
174        ],
175
176        'ExecuteLastStageTransformHooks' => [
177            'class' => ExecuteLastStageTransformHooks::class,
178            'services' => [
179                'HookContainer',
180            ],
181        ],
182
183        # This should be last, in order to ensure final output is hardened
184        'HardenNFC' => [
185            'class' => HardenNFC::class,
186        ]
187    ];
188
189    public function __construct(
190        ServiceOptions $options,
191        Config $config,
192        LoggerInterface $logger,
193        ObjectFactory $objectFactory
194    ) {
195        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
196        $this->options = $options;
197        $this->config = $config;
198        $this->logger = $logger;
199        $this->objectFactory = $objectFactory;
200    }
201
202    public function buildDefaultOutputPipeline(): OutputTransformPipeline {
203        return $this->buildPipeline();
204    }
205
206    public function buildLanguageConverterPipeline(): OutputTransformPipeline {
207        return $this->buildPipeline( only: [ 'ParsoidLanguageConverter' ] );
208    }
209
210    /**
211     * Creates a pipeline of transformations to transform the content of the ParserOutput object from "parsed HTML"
212     * to "output HTML" and returns it.
213     * @internal
214     * @param ?list<string> $only If provided, create a pipeline consisting
215     *  only of the named stages.
216     * @return OutputTransformPipeline
217     */
218    private function buildPipeline( ?array $only = null ): OutputTransformPipeline {
219        // Add extension stages
220        $list = array_merge(
221            self::CORE_LIST,
222            $this->options->get( MainConfigNames::OutputPipelineStages )
223        );
224
225        $otp = new OutputTransformPipeline();
226        foreach ( $list as $stageName => $spec ) {
227            if ( $only !== null && !in_array( $stageName, $only, true ) ) {
228                continue;
229            }
230            if ( is_array( $spec ) &&
231                array_key_exists( 'domStage', $spec ) &&
232                array_key_exists( 'textStage', $spec )
233            ) {
234                $args = [
235                    $this->objectFactory->createObject( $spec['textStage'],
236                    [
237                        // This stage is also required to implement
238                        // TextTransformStage by the
239                        // ContentHolderTransformStage constructor
240                        'assertClass' => OutputTransformStage::class,
241                        'allowClassName' => true,
242                    ] + $this->makeExtraArgs( $spec['textStage'] ) ),
243                    $this->objectFactory->createObject( $spec['domStage'],
244                        [
245                            // This stage is also required to implement
246                            // DOMTransformStage by the
247                            // ContentHolderTransformStage constructor
248                            'assertClass' => OutputTransformStage::class,
249                            'allowClassName' => true,
250                        ] + $this->makeExtraArgs( $spec['domStage'] ) ),
251                    $spec['exclusive'] ?? false,
252                ];
253                $spec = [
254                    'class' => ContentHolderTransformStage::class,
255                    'args' => $args
256                ];
257            }
258
259            $transform = $this->objectFactory->createObject(
260                $spec,
261                [
262                    'assertClass' => OutputTransformStage::class,
263                    'allowClassName' => true,
264                ] + $this->makeExtraArgs( $spec )
265            );
266            $otp->addStage( $transform );
267        }
268        return $otp;
269    }
270
271    /**
272     * Add appropriate ServiceOptions and a logger to the args array.
273     * @param mixed $spec
274     * @return array{extraArgs:array{0:ServiceOptions,1:LoggerInterface}}
275     */
276    private function makeExtraArgs( $spec ): array {
277        // If the handler is specified as a class, use the CONSTRUCTOR_OPTIONS
278        // for that class.
279        $class = is_string( $spec ) ? $spec : ( $spec['class'] ?? null );
280        $svcOptions = new ServiceOptions(
281            $class ? $class::CONSTRUCTOR_OPTIONS : [],
282            $this->config
283        );
284        $extraArgs = [ $svcOptions, $this->logger ];
285        return [ 'extraArgs' => $extraArgs ];
286    }
287}