Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.98% covered (warning)
82.98%
39 / 47
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ShadowPageLoader
82.98% covered (warning)
82.98%
39 / 47
87.50% covered (warning)
87.50%
7 / 8
34.44
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 existsForLink
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getMessageProvider
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getExtensionProvider
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 maybeCreateShadowPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getProvidersForNamespace
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 getProvider
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\ShadowPage;
4
5use MediaWiki\Page\PageReference;
6use OutOfBoundsException;
7use Wikimedia\ObjectFactory\ObjectFactory;
8use Wikimedia\Parsoid\Core\LinkTarget;
9
10/**
11 * A service which loads shadow content, which is content that is displayed on
12 * a nonexistent page with a valid can-exist title. For example, pages in the
13 * MediaWiki namespace show the default message text.
14 *
15 * A container and factory for ShadowPageProvider instances.
16 *
17 * @since 1.47
18 */
19class ShadowPageLoader {
20    public const CORE_SPECS = [
21        [
22            'namespace' => NS_MEDIAWIKI,
23            'class' => MessageProvider::class,
24            'services' => [
25                'MessageCache',
26                'ContentLanguage',
27                'SlotRoleRegistry',
28                'ContentHandlerFactory',
29            ]
30        ]
31    ];
32
33    private const OBJECT_FACTORY_KEYS = [
34        'class', 'factory', 'args', 'services', 'optional_services'
35    ];
36
37    private const CORE = 0;
38    private const EXTENSION = 1;
39
40    /** @var ShadowPageProvider[][] */
41    private array $providers = [];
42
43    /** @var ShadowPageProvider[][] */
44    private array $providersForNamespace = [];
45
46    private ?PageReference $cachedTitle = null;
47    private ?ShadowPage $cachedShadow = null;
48    private ?MessageProvider $messageProvider = null;
49
50    /**
51     * @param ObjectFactory $objectFactory
52     * @param array $coreSpecs Should be self::CORE_SPECS except while testing
53     * @param array $extensionSpecs Array of associative arrays. The keys
54     *   "class", "factory", "args", "services" and "optional_services"
55     *   are passed down to ObjectFactory. The following additional keys
56     *   are recognised:
57     *     - namespace: If present, only titles with this namespace will
58     *       be given to the provider.
59     */
60    public function __construct(
61        private ObjectFactory $objectFactory,
62        private array $coreSpecs,
63        private array $extensionSpecs
64    ) {
65    }
66
67    /**
68     * Try to get a ShadowPage for the given title.
69     */
70    public function get( PageReference $title ): ?ShadowPage {
71        if ( $this->cachedTitle && $title->isSamePageAs( $this->cachedTitle ) ) {
72            return $this->cachedShadow;
73        }
74        $this->cachedTitle = $title;
75        $this->cachedShadow = $this->maybeCreateShadowPage( $title );
76        return $this->cachedShadow;
77    }
78
79    /**
80     * Check if a link should be shown as existing, due to the existence of
81     * shadow content at that location.
82     */
83    public function existsForLink( LinkTarget $link ): bool {
84        foreach ( $this->getProvidersForNamespace( $link->getNamespace() ) as $provider ) {
85            if ( $provider->existsForLink( $link ) ) {
86                return true;
87            }
88        }
89        return false;
90    }
91
92    /**
93     * Get the MessageProvider instance
94     */
95    public function getMessageProvider(): MessageProvider {
96        if ( $this->messageProvider === null ) {
97            foreach ( $this->getProvidersForNamespace( NS_MEDIAWIKI ) as $provider ) {
98                if ( $provider instanceof MessageProvider ) {
99                    $this->messageProvider = $provider;
100                    break;
101                }
102            }
103        }
104        return $this->messageProvider;
105    }
106
107    /**
108     * Get an extension provider with a given class name, or throw
109     * if no such provider exists.
110     *
111     * To get a core provider, use the relevant accessor such as
112     * getMessageProvider().
113     */
114    public function getExtensionProvider( string $className ): ShadowPageProvider {
115        foreach ( $this->extensionSpecs as $index => $spec ) {
116            if ( !isset( $spec['class'] ) ) {
117                $provider = $this->getProvider( self::EXTENSION, $index, $spec );
118                if ( $provider instanceof $className ) {
119                    return $provider;
120                }
121            } elseif ( $spec['class'] === $className ) {
122                return $this->getProvider( self::EXTENSION, $index, $spec );
123            }
124        }
125        throw new OutOfBoundsException( 'No such provider class' );
126    }
127
128    /**
129     * Uncached implementation of get()
130     */
131    private function maybeCreateShadowPage( PageReference $title ): ?ShadowPage {
132        foreach ( $this->getProvidersForNamespace( $title->getNamespace() ) as $provider ) {
133            $shadowPage = $provider->get( $title );
134            if ( $shadowPage ) {
135                return $shadowPage;
136            }
137        }
138        return null;
139    }
140
141    /**
142     * @param int $namespace
143     * @return ShadowPageProvider[]
144     */
145    private function getProvidersForNamespace( int $namespace ): array {
146        if ( !isset( $this->providersForNamespace[$namespace] ) ) {
147            $providers = [];
148            foreach ( $this->coreSpecs as $index => $spec ) {
149                if ( !isset( $spec['namespace'] ) || $spec['namespace'] === $namespace ) {
150                    $providers[] = $this->getProvider( self::CORE, $index, $spec );
151                }
152            }
153            foreach ( $this->extensionSpecs as $index => $spec ) {
154                if ( !isset( $spec['namespace'] ) || $spec['namespace'] === $namespace ) {
155                    $providers[] = $this->getProvider( self::EXTENSION, $index, $spec );
156                }
157            }
158            $this->providersForNamespace[$namespace] = $providers;
159        }
160        return $this->providersForNamespace[$namespace];
161    }
162
163    /**
164     * Create and cache a provider instance.
165     *
166     * @param int $origin Either self::CORE or self::EXTENSION
167     * @param int $index The cache index
168     * @param array $spec The specification for provider creation
169     * @return ShadowPageProvider
170     */
171    private function getProvider( int $origin, int $index, array $spec ): ShadowPageProvider {
172        if ( !isset( $this->providers[$origin][$index] ) ) {
173            $objSpec = array_intersect_key( $spec,
174                array_fill_keys( self::OBJECT_FACTORY_KEYS, true ) );
175            $provider = $this->objectFactory->createObject( $objSpec );
176            if ( $provider instanceof BaseShadowPageProvider ) {
177                $provider->initBaseDeps( new ParseHelper() );
178            }
179            $this->providers[$origin][$index] = $provider;
180        }
181        return $this->providers[$origin][$index];
182    }
183}