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/**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @file
20 * @ingroup Content
21 *
22 * @author Art Baltai
23 */
24
25namespace MediaWiki\Content;
26
27use ContentHandler;
28use InvalidArgumentException;
29use MediaWiki\HookContainer\HookContainer;
30use MediaWiki\HookContainer\HookRunner;
31use MWUnknownContentModelException;
32use Psr\Log\LoggerInterface;
33use Wikimedia\ObjectFactory\ObjectFactory;
34
35/**
36 * Class ContentHandlerFactory
37 * @package MediaWiki\Content
38 * @ingroup Content
39 * @since 1.35
40 */
41final class ContentHandlerFactory implements IContentHandlerFactory {
42
43    /**
44     * @var string[]|callable[]
45     */
46    private $handlerSpecs;
47
48    /**
49     * @var ContentHandler[] Registry of ContentHandler instances by model id
50     */
51    private $handlersByModel = [];
52
53    /** @var ObjectFactory */
54    private $objectFactory;
55
56    /** @var HookRunner */
57    private $hookRunner;
58
59    /** @var LoggerInterface */
60    private $logger;
61
62    /**
63     * @since 1.35
64     * @internal Use @see MediaWikiServices::getContentHandlerFactory
65     *
66     * @param string[]|callable[] $handlerSpecs An associative array mapping each known
67     *   content model to the ObjectFactory spec used to construct its ContentHandler.
68     *   This array typically comes from $wgContentHandlers.
69     * @param ObjectFactory $objectFactory
70     * @param HookContainer $hookContainer
71     * @param LoggerInterface $logger
72     */
73    public function __construct(
74        array $handlerSpecs,
75        ObjectFactory $objectFactory,
76        HookContainer $hookContainer,
77        LoggerInterface $logger
78    ) {
79        $this->handlerSpecs = $handlerSpecs;
80        $this->objectFactory = $objectFactory;
81        $this->hookRunner = new HookRunner( $hookContainer );
82        $this->logger = $logger;
83    }
84
85    /**
86     * @param string $modelID
87     *
88     * @return ContentHandler
89     * @throws MWUnknownContentModelException If no handler is known for the model ID.
90     */
91    public function getContentHandler( string $modelID ): ContentHandler {
92        if ( empty( $this->handlersByModel[$modelID] ) ) {
93            $contentHandler = $this->createForModelID( $modelID );
94
95            $this->logger->info(
96                "Registered handler for {$modelID}" . get_class( $contentHandler )
97            );
98            $this->handlersByModel[$modelID] = $contentHandler;
99        }
100
101        return $this->handlersByModel[$modelID];
102    }
103
104    /**
105     * Define HandlerSpec for ModelID.
106     * @param string $modelID
107     * @param callable|string $handlerSpec
108     *
109     * @internal
110     *
111     */
112    public function defineContentHandler( string $modelID, $handlerSpec ): void {
113        if ( !is_callable( $handlerSpec ) && !is_string( $handlerSpec ) ) {
114            throw new InvalidArgumentException(
115                "ContentHandler Spec for modelID '{$modelID}' must be callable or class name"
116            );
117        }
118        unset( $this->handlersByModel[$modelID] );
119        $this->handlerSpecs[$modelID] = $handlerSpec;
120    }
121
122    /**
123     * Get defined ModelIDs
124     *
125     * @return string[]
126     */
127    public function getContentModels(): array {
128        $modelsFromHook = [];
129        $this->hookRunner->onGetContentModels( $modelsFromHook );
130        $models = array_merge( // auto-registered from config and MediaWikiServices or manual
131            array_keys( $this->handlerSpecs ),
132
133            // incorrect registered and called: without HOOK_NAME_GET_CONTENT_MODELS
134            array_keys( $this->handlersByModel ),
135
136            // correct registered: as HOOK_NAME_GET_CONTENT_MODELS
137            $modelsFromHook );
138
139        return array_unique( $models );
140    }
141
142    /**
143     * @return string[]
144     */
145    public function getAllContentFormats(): array {
146        $formats = [];
147        foreach ( $this->handlerSpecs as $model => $class ) {
148            $formats += array_fill_keys(
149                $this->getContentHandler( $model )->getSupportedFormats(),
150                true );
151        }
152
153        return array_keys( $formats );
154    }
155
156    /**
157     * @param string $modelID
158     *
159     * @return bool
160     */
161    public function isDefinedModel( string $modelID ): bool {
162        return in_array( $modelID, $this->getContentModels(), true );
163    }
164
165    /**
166     * Create ContentHandler for ModelID
167     *
168     * @param string $modelID The ID of the content model for which to get a handler.
169     * Use CONTENT_MODEL_XXX constants.
170     *
171     * @return ContentHandler The ContentHandler singleton for handling the model given by the ID.
172     *
173     * @throws MWUnknownContentModelException If no handler is known for the model ID.
174     */
175    private function createForModelID( string $modelID ): ContentHandler {
176        $handlerSpec = $this->handlerSpecs[$modelID] ?? null;
177        if ( $handlerSpec !== null ) {
178            return $this->createContentHandlerFromHandlerSpec( $modelID, $handlerSpec );
179        }
180
181        return $this->createContentHandlerFromHook( $modelID );
182    }
183
184    /**
185     * @param string $modelID
186     * @param ContentHandler $contentHandler
187     *
188     * @throws MWUnknownContentModelException
189     */
190    private function validateContentHandler( string $modelID, $contentHandler ): void {
191        if ( $contentHandler === null ) {
192            throw new MWUnknownContentModelException( $modelID );
193        }
194
195        if ( !is_object( $contentHandler ) ) {
196            throw new InvalidArgumentException(
197                "ContentHandler for model {$modelID} wrong: non-object given."
198            );
199        }
200
201        if ( !$contentHandler instanceof ContentHandler ) {
202            throw new InvalidArgumentException(
203                "ContentHandler for model {$modelID} must supply a ContentHandler instance, "
204                . get_class( $contentHandler ) . ' given.'
205            );
206        }
207    }
208
209    /**
210     * @param string $modelID
211     * @param callable|string $handlerSpec
212     *
213     * @return ContentHandler
214     * @throws MWUnknownContentModelException
215     */
216    private function createContentHandlerFromHandlerSpec(
217        string $modelID, $handlerSpec
218    ): ContentHandler {
219        /**
220         * @var ContentHandler $contentHandler
221         */
222        $contentHandler = $this->objectFactory->createObject(
223            $handlerSpec,
224            [
225                'assertClass' => ContentHandler::class,
226                'allowCallable' => true,
227                'allowClassName' => true,
228                'extraArgs' => [ $modelID ],
229            ]
230        );
231
232        $this->validateContentHandler( $modelID, $contentHandler );
233
234        return $contentHandler;
235    }
236
237    /**
238     * @param string $modelID
239     *
240     * @return ContentHandler
241     * @throws MWUnknownContentModelException
242     */
243    private function createContentHandlerFromHook( string $modelID ): ContentHandler {
244        $contentHandler = null;
245        // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
246        $this->hookRunner->onContentHandlerForModelID( $modelID, $contentHandler );
247        $this->validateContentHandler( $modelID, $contentHandler );
248
249        '@phan-var ContentHandler $contentHandler';
250
251        return $contentHandler;
252    }
253}