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