Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
SlotRoleRegistry
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
10 / 10
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 defineRole
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 defineRoleWithModel
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getRoleHandler
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 getAllowedRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequiredRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefinedRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getKnownRoles
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isDefinedRole
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isKnownRole
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This file is part of MediaWiki.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23namespace MediaWiki\Revision;
24
25use InvalidArgumentException;
26use LogicException;
27use MediaWiki\Page\PageIdentity;
28use MediaWiki\Storage\NameTableStore;
29use Wikimedia\Assert\Assert;
30
31/**
32 * A registry service for SlotRoleHandlers, used to define which slot roles are available on
33 * which page.
34 *
35 * Extensions may use the SlotRoleRegistry to register the slots they define.
36 *
37 * In the context of the SlotRoleRegistry, it is useful to distinguish between "defined" and "known"
38 * slot roles: A slot role is "defined" if defineRole() or defineRoleWithModel() was called for
39 * that role. A slot role is "known" if the NameTableStore provided to the constructor as the
40 * $roleNamesStore parameter has an ID associated with that role, which essentially means that
41 * the role at some point has been used on the wiki. Roles that are not "defined" but are
42 * "known" typically belong to extensions that used to be installed on the wiki, but no longer are.
43 * Such slots should be considered ok for display and administrative operations, but only "defined"
44 * slots should be supported for editing.
45 *
46 * @since 1.33
47 */
48class SlotRoleRegistry {
49
50    private NameTableStore $roleNamesStore;
51    /** @var array<string,callable> */
52    private array $instantiators = [];
53    /** @var array<string,SlotRoleHandler> */
54    private array $handlers = [];
55
56    public function __construct( NameTableStore $roleNamesStore ) {
57        $this->roleNamesStore = $roleNamesStore;
58    }
59
60    /**
61     * Defines a slot role.
62     *
63     * For use by extensions that wish to define roles beyond the main slot role.
64     *
65     * @see defineRoleWithModel()
66     *
67     * @param string $role The role name of the slot to define. This should follow the
68     *        same convention as message keys:
69     * @param callable $instantiator called with $role as a parameter;
70     *        Signature: function ( string $role ): SlotRoleHandler
71     */
72    public function defineRole( string $role, callable $instantiator ): void {
73        $role = strtolower( $role );
74
75        if ( isset( $this->instantiators[$role] ) ) {
76            throw new LogicException( "Role $role is already defined" );
77        }
78
79        $this->instantiators[$role] = $instantiator;
80    }
81
82    /**
83     * Defines a slot role that allows only the given content model, and has no special
84     * behavior.
85     *
86     * For use by extensions that wish to define roles beyond the main slot role, but have
87     * no need to implement any special behavior for that slot.
88     *
89     * @see defineRole()
90     *
91     * @param string $role The role name of the slot to define, see defineRole()
92     *        for more information.
93     * @param string $model A content model name, see ContentHandler
94     * @param array $layout See SlotRoleHandler getOutputLayoutHints
95     * @param bool $derived see SlotRoleHandler constructor
96     * @since 1.36 optional $derived parameter added
97     */
98    public function defineRoleWithModel(
99        string $role,
100        string $model,
101        array $layout = [],
102        bool $derived = false
103    ): void {
104        $this->defineRole(
105            $role,
106            static function ( $role ) use ( $model, $layout, $derived ) {
107                return new SlotRoleHandler( $role, $model, $layout, $derived );
108            }
109        );
110    }
111
112    /**
113     * Gets the SlotRoleHandler that should be used when processing content of the given role.
114     *
115     * @param string $role
116     *
117     * @throws InvalidArgumentException If $role is not a known slot role.
118     * @return SlotRoleHandler The handler to be used for $role. This may be a
119     *         FallbackSlotRoleHandler if the slot is "known" but not "defined".
120     */
121    public function getRoleHandler( string $role ): SlotRoleHandler {
122        $role = strtolower( $role );
123
124        if ( !isset( $this->handlers[$role] ) ) {
125            if ( !isset( $this->instantiators[$role] ) ) {
126                if ( $this->isKnownRole( $role ) ) {
127                    // The role has no handler defined, but is represented in the database.
128                    // This may happen e.g. when the extension that defined the role was uninstalled.
129                    wfWarn( __METHOD__ . ": known but undefined slot role $role" );
130                    $this->handlers[$role] = new FallbackSlotRoleHandler( $role );
131                } else {
132                    // The role doesn't have a handler defined, and is not represented in
133                    // the database. Something must be quite wrong.
134                    throw new InvalidArgumentException( "Unknown role $role" );
135                }
136            } else {
137                $handler = call_user_func( $this->instantiators[$role], $role );
138
139                Assert::postcondition(
140                    $handler instanceof SlotRoleHandler,
141                    "Instantiator for $role role must return a SlotRoleHandler"
142                );
143
144                $this->handlers[$role] = $handler;
145            }
146        }
147
148        return $this->handlers[$role];
149    }
150
151    /**
152     * Returns the list of roles allowed when creating a new revision on the given page.
153     * The choice should not depend on external state, such as the page content.
154     * Note that existing revisions of that page are not guaranteed to comply with this list.
155     *
156     * All implementations of this method are required to return at least all "required" roles.
157     *
158     * @param PageIdentity $page
159     *
160     * @return string[]
161     */
162    public function getAllowedRoles( PageIdentity $page ): array {
163        // TODO: allow this to be overwritten per namespace (or page type)
164        // TODO: decide how to control which slots are offered for editing per default (T209927)
165        return $this->getDefinedRoles();
166    }
167
168    /**
169     * Returns the list of roles required when creating a new revision on the given page.
170     * The should not depend on external state, such as the page content.
171     * Note that existing revisions of that page are not guaranteed to comply with this list.
172     *
173     * All required roles are implicitly considered "allowed", so any roles
174     * returned by this method will also be returned by getAllowedRoles().
175     *
176     * @param PageIdentity $page
177     *
178     * @return string[]
179     */
180    public function getRequiredRoles( PageIdentity $page ): array {
181        // TODO: allow this to be overwritten per namespace (or page type)
182        return [ SlotRecord::MAIN ];
183    }
184
185    /**
186     * Returns the list of roles defined by calling defineRole().
187     *
188     * This list should be used when enumerating slot roles that can be used for editing.
189     *
190     * @return string[]
191     */
192    public function getDefinedRoles(): array {
193        return array_keys( $this->instantiators );
194    }
195
196    /**
197     * Returns the list of known roles, including the ones returned by getDefinedRoles(),
198     * and roles that exist according to the NameTableStore provided to the constructor.
199     *
200     * This list should be used when enumerating slot roles that can be used in queries or
201     * for display.
202     *
203     * @return string[]
204     */
205    public function getKnownRoles(): array {
206        return array_unique( array_merge(
207            $this->getDefinedRoles(),
208            $this->roleNamesStore->getMap()
209        ) );
210    }
211
212    /**
213     * Whether the given role is defined, that is, it was defined by calling defineRole().
214     */
215    public function isDefinedRole( string $role ): bool {
216        $role = strtolower( $role );
217        return isset( $this->instantiators[$role] );
218    }
219
220    /**
221     * Whether the given role is known, that is, it's either defined or exist according to
222     * the NameTableStore provided to the constructor.
223     */
224    public function isKnownRole( string $role ): bool {
225        $role = strtolower( $role );
226        return in_array( $role, $this->getKnownRoles(), true );
227    }
228
229}