Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
82.98% |
39 / 47 |
|
87.50% |
7 / 8 |
CRAP | |
0.00% |
0 / 1 |
| ShadowPageLoader | |
82.98% |
39 / 47 |
|
87.50% |
7 / 8 |
34.44 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| existsForLink | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| getMessageProvider | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
| getExtensionProvider | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
| maybeCreateShadowPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| getProvidersForNamespace | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
8 | |||
| getProvider | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\ShadowPage; |
| 4 | |
| 5 | use MediaWiki\Page\PageReference; |
| 6 | use OutOfBoundsException; |
| 7 | use Wikimedia\ObjectFactory\ObjectFactory; |
| 8 | use 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 | */ |
| 19 | class 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 | } |