Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.61% covered (warning)
83.61%
51 / 61
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContentHandlerFactory
83.61% covered (warning)
83.61%
51 / 61
80.00% covered (warning)
80.00%
8 / 10
19.43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getContentHandler
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 defineContentHandler
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 getContentModels
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getAllContentFormats
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 isDefinedModel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createForModelID
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 validateContentHandler
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
8.12
 createContentHandlerFromHandlerSpec
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 createContentHandlerFromHook
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Content;
8
9use InvalidArgumentException;
10use MediaWiki\Exception\MWUnknownContentModelException;
11use MediaWiki\HookContainer\HookContainer;
12use MediaWiki\HookContainer\HookRunner;
13use Psr\Log\LoggerInterface;
14use Wikimedia\ObjectFactory\ObjectFactory;
15
16/**
17 * @since 1.35
18 * @ingroup Content
19 * @author Art Baltai
20 */
21final class ContentHandlerFactory implements IContentHandlerFactory {
22
23    /**
24     * @var string[]|callable[]
25     */
26    private $handlerSpecs;
27
28    /**
29     * @var ContentHandler[] Registry of ContentHandler instances by model id
30     */
31    private $handlersByModel = [];
32    private ObjectFactory $objectFactory;
33    private HookRunner $hookRunner;
34    private LoggerInterface $logger;
35
36    /**
37     * @since 1.35
38     * @internal Please use MediaWikiServices::getContentHandlerFactory instead
39     *
40     * @param string[]|callable[] $handlerSpecs An associative array mapping each known
41     *   content model to the ObjectFactory spec used to construct its ContentHandler.
42     *   This array typically comes from $wgContentHandlers.
43     * @param ObjectFactory $objectFactory
44     * @param HookContainer $hookContainer
45     * @param LoggerInterface $logger
46     */
47    public function __construct(
48        array $handlerSpecs,
49        ObjectFactory $objectFactory,
50        HookContainer $hookContainer,
51        LoggerInterface $logger
52    ) {
53        $this->handlerSpecs = $handlerSpecs;
54        $this->objectFactory = $objectFactory;
55        $this->hookRunner = new HookRunner( $hookContainer );
56        $this->logger = $logger;
57    }
58
59    /**
60     * @param string $modelID
61     *
62     * @return ContentHandler
63     * @throws MWUnknownContentModelException If no handler is known for the model ID.
64     */
65    public function getContentHandler( string $modelID ): ContentHandler {
66        if ( empty( $this->handlersByModel[$modelID] ) ) {
67            $contentHandler = $this->createForModelID( $modelID );
68
69            $this->logger->info(
70                "Registered handler for {$modelID}" . get_class( $contentHandler )
71            );
72            $this->handlersByModel[$modelID] = $contentHandler;
73        }
74
75        return $this->handlersByModel[$modelID];
76    }
77
78    /**
79     * Define HandlerSpec for ModelID.
80     * @param string $modelID
81     * @param callable|string $handlerSpec
82     *
83     * @internal
84     */
85    public function defineContentHandler( string $modelID, $handlerSpec ): void {
86        if ( !is_callable( $handlerSpec ) && !is_string( $handlerSpec ) ) {
87            throw new InvalidArgumentException(
88                "ContentHandler Spec for modelID '{$modelID}' must be callable or class name"
89            );
90        }
91        unset( $this->handlersByModel[$modelID] );
92        $this->handlerSpecs[$modelID] = $handlerSpec;
93    }
94
95    /**
96     * Get defined ModelIDs
97     *
98     * @return string[]
99     */
100    public function getContentModels(): array {
101        $modelsFromHook = [];
102        $this->hookRunner->onGetContentModels( $modelsFromHook );
103        $models = array_merge( // auto-registered from config and MediaWikiServices or manual
104            array_keys( $this->handlerSpecs ),
105
106            // incorrect registered and called: without HOOK_NAME_GET_CONTENT_MODELS
107            array_keys( $this->handlersByModel ),
108
109            // correct registered: as HOOK_NAME_GET_CONTENT_MODELS
110            $modelsFromHook );
111
112        return array_unique( $models );
113    }
114
115    /**
116     * @return string[]
117     */
118    public function getAllContentFormats(): array {
119        $formats = [];
120        foreach ( $this->handlerSpecs as $model => $class ) {
121            $formats += array_fill_keys(
122                $this->getContentHandler( $model )->getSupportedFormats(),
123                true );
124        }
125
126        return array_keys( $formats );
127    }
128
129    public function isDefinedModel( string $modelID ): bool {
130        return in_array( $modelID, $this->getContentModels(), true );
131    }
132
133    /**
134     * Create ContentHandler for ModelID
135     *
136     * @param string $modelID The ID of the content model for which to get a handler.
137     * Use CONTENT_MODEL_XXX constants.
138     *
139     * @return ContentHandler The ContentHandler singleton for handling the model given by the ID.
140     *
141     * @throws MWUnknownContentModelException If no handler is known for the model ID.
142     */
143    private function createForModelID( string $modelID ): ContentHandler {
144        $handlerSpec = $this->handlerSpecs[$modelID] ?? null;
145        if ( $handlerSpec !== null ) {
146            return $this->createContentHandlerFromHandlerSpec( $modelID, $handlerSpec );
147        }
148
149        return $this->createContentHandlerFromHook( $modelID );
150    }
151
152    /**
153     * @param string $modelID
154     * @param ContentHandler $contentHandler
155     *
156     * @throws MWUnknownContentModelException
157     */
158    private function validateContentHandler( string $modelID, $contentHandler ): void {
159        if ( $contentHandler === null ) {
160            throw new MWUnknownContentModelException( $modelID );
161        }
162
163        if ( !is_object( $contentHandler ) ) {
164            throw new InvalidArgumentException(
165                "ContentHandler for model {$modelID} wrong: non-object given."
166            );
167        }
168
169        if ( !$contentHandler instanceof ContentHandler ) {
170            throw new InvalidArgumentException(
171                "ContentHandler for model {$modelID} must supply a ContentHandler instance, "
172                . get_class( $contentHandler ) . ' given.'
173            );
174        }
175    }
176
177    /**
178     * @param string $modelID
179     * @param callable|string $handlerSpec
180     *
181     * @return ContentHandler
182     * @throws MWUnknownContentModelException
183     */
184    private function createContentHandlerFromHandlerSpec(
185        string $modelID, $handlerSpec
186    ): ContentHandler {
187        /**
188         * @var ContentHandler $contentHandler
189         */
190        $contentHandler = $this->objectFactory->createObject(
191            $handlerSpec,
192            [
193                'assertClass' => ContentHandler::class,
194                'allowCallable' => true,
195                'allowClassName' => true,
196                'extraArgs' => [ $modelID ],
197            ]
198        );
199
200        $this->validateContentHandler( $modelID, $contentHandler );
201
202        return $contentHandler;
203    }
204
205    /**
206     * @param string $modelID
207     *
208     * @return ContentHandler
209     * @throws MWUnknownContentModelException
210     */
211    private function createContentHandlerFromHook( string $modelID ): ContentHandler {
212        $contentHandler = null;
213        // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
214        $this->hookRunner->onContentHandlerForModelID( $modelID, $contentHandler );
215        $this->validateContentHandler( $modelID, $contentHandler );
216
217        '@phan-var ContentHandler $contentHandler';
218
219        return $contentHandler;
220    }
221}