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