1<?php
2
3use MediaWiki\MediaWikiServices;
4
5/**
6 * Represents a single site.
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 * http://www.gnu.org/copyleft/gpl.html
22 *
23 * @since 1.21
24 *
25 * @file
26 * @ingroup Site
27 *
28 * @license GPL-2.0-or-later
29 * @author Jeroen De Dauw < jeroendedauw@gmail.com >
30 */
31class Site implements Serializable {
32	public const TYPE_UNKNOWN = 'unknown';
33	public const TYPE_MEDIAWIKI = 'mediawiki';
34
35	public const GROUP_NONE = 'none';
36
37	public const ID_INTERWIKI = 'interwiki';
38	public const ID_EQUIVALENT = 'equivalent';
39
40	public const SOURCE_LOCAL = 'local';
41
42	public const PATH_LINK = 'link';
43
44	/**
45	 * A version ID that identifies the serialization structure used by getSerializationData()
46	 * and unserialize(). This is useful for constructing cache keys in cases where the cache relies
47	 * on serialization for storing the SiteList.
48	 *
49	 * @var string A string uniquely identifying the version of the serialization structure.
50	 */
51	public const SERIAL_VERSION_ID = '2013-01-23';
52
53	/**
54	 * @since 1.21
55	 *
56	 * @var string|null
57	 */
58	protected $globalId = null;
59
60	/**
61	 * @since 1.21
62	 *
63	 * @var string
64	 */
65	protected $type = self::TYPE_UNKNOWN;
66
67	/**
68	 * @since 1.21
69	 *
70	 * @var string
71	 */
72	protected $group = self::GROUP_NONE;
73
74	/**
75	 * @since 1.21
76	 *
77	 * @var string
78	 */
79	protected $source = self::SOURCE_LOCAL;
80
81	/**
82	 * @since 1.21
83	 *
84	 * @var string|null
85	 */
86	protected $languageCode = null;
87
88	/**
89	 * Holds the local ids for this site.
90	 * local id type => [ ids for this type (strings) ]
91	 *
92	 * @since 1.21
93	 *
94	 * @var array[]|false
95	 */
96	protected $localIds = [];
97
98	/**
99	 * @since 1.21
100	 *
101	 * @var array
102	 */
103	protected $extraData = [];
104
105	/**
106	 * @since 1.21
107	 *
108	 * @var array
109	 */
110	protected $extraConfig = [];
111
112	/**
113	 * @since 1.21
114	 *
115	 * @var bool
116	 */
117	protected $forward = false;
118
119	/**
120	 * @since 1.21
121	 *
122	 * @var int|null
123	 */
124	protected $internalId = null;
125
126	/**
127	 * @since 1.21
128	 *
129	 * @param string $type
130	 */
131	public function __construct( $type = self::TYPE_UNKNOWN ) {
132		$this->type = $type;
133	}
134
135	/**
136	 * Returns the global site identifier (ie enwiktionary).
137	 *
138	 * @since 1.21
139	 *
140	 * @return string|null
141	 */
142	public function getGlobalId() {
143		return $this->globalId;
144	}
145
146	/**
147	 * Sets the global site identifier (ie enwiktionary).
148	 *
149	 * @since 1.21
150	 *
151	 * @param string|null $globalId
152	 *
153	 * @throws MWException
154	 */
155	public function setGlobalId( $globalId ) {
156		if ( $globalId !== null && !is_string( $globalId ) ) {
157			throw new MWException( '$globalId needs to be string or null' );
158		}
159
160		$this->globalId = $globalId;
161	}
162
163	/**
164	 * Returns the type of the site (ie mediawiki).
165	 *
166	 * @since 1.21
167	 *
168	 * @return string
169	 */
170	public function getType() {
171		return $this->type;
172	}
173
174	/**
175	 * Gets the group of the site (ie wikipedia).
176	 *
177	 * @since 1.21
178	 *
179	 * @return string
180	 */
181	public function getGroup() {
182		return $this->group;
183	}
184
185	/**
186	 * Sets the group of the site (ie wikipedia).
187	 *
188	 * @since 1.21
189	 *
190	 * @param string $group
191	 *
192	 * @throws MWException
193	 */
194	public function setGroup( $group ) {
195		if ( !is_string( $group ) ) {
196			throw new MWException( '$group needs to be a string' );
197		}
198
199		$this->group = $group;
200	}
201
202	/**
203	 * Returns the source of the site data (ie 'local', 'wikidata', 'my-magical-repo').
204	 *
205	 * @since 1.21
206	 *
207	 * @return string
208	 */
209	public function getSource() {
210		return $this->source;
211	}
212
213	/**
214	 * Sets the source of the site data (ie 'local', 'wikidata', 'my-magical-repo').
215	 *
216	 * @since 1.21
217	 *
218	 * @param string $source
219	 *
220	 * @throws MWException
221	 */
222	public function setSource( $source ) {
223		if ( !is_string( $source ) ) {
224			throw new MWException( '$source needs to be a string' );
225		}
226
227		$this->source = $source;
228	}
229
230	/**
231	 * Gets if site.tld/path/key:pageTitle should forward users to  the page on
232	 * the actual site, where "key" is the local identifier.
233	 *
234	 * @since 1.21
235	 *
236	 * @return bool
237	 */
238	public function shouldForward() {
239		return $this->forward;
240	}
241
242	/**
243	 * Sets if site.tld/path/key:pageTitle should forward users to  the page on
244	 * the actual site, where "key" is the local identifier.
245	 *
246	 * @since 1.21
247	 *
248	 * @param bool $shouldForward
249	 *
250	 * @throws MWException
251	 */
252	public function setForward( $shouldForward ) {
253		if ( !is_bool( $shouldForward ) ) {
254			throw new MWException( '$shouldForward needs to be a boolean' );
255		}
256
257		$this->forward = $shouldForward;
258	}
259
260	/**
261	 * Returns the domain of the site, ie en.wikipedia.org
262	 * Or false if it's not known.
263	 *
264	 * @since 1.21
265	 *
266	 * @return string|null
267	 */
268	public function getDomain() {
269		$path = $this->getLinkPath();
270
271		if ( $path === null ) {
272			return null;
273		}
274
275		return parse_url( $path, PHP_URL_HOST );
276	}
277
278	/**
279	 * Returns the protocol of the site.
280	 *
281	 * @since 1.21
282	 *
283	 * @throws MWException
284	 * @return string
285	 */
286	public function getProtocol() {
287		$path = $this->getLinkPath();
288
289		if ( $path === null ) {
290			return '';
291		}
292
293		$protocol = parse_url( $path, PHP_URL_SCHEME );
294
295		// Malformed URL
296		if ( $protocol === false ) {
297			throw new MWException( "failed to parse URL '$path'" );
298		}
299
300		// No schema
301		if ( $protocol === null ) {
302			// Used for protocol relative URLs
303			$protocol = '';
304		}
305
306		return $protocol;
307	}
308
309	/**
310	 * Sets the path used to construct links with.
311	 * Shall be equivalent to setPath( getLinkPathType(), $fullUrl ).
312	 *
313	 * @param string $fullUrl
314	 *
315	 * @since 1.21
316	 *
317	 * @throws MWException
318	 */
319	public function setLinkPath( $fullUrl ) {
320		$type = $this->getLinkPathType();
321
322		if ( $type === null ) {
323			throw new MWException( "This Site does not support link paths." );
324		}
325
326		$this->setPath( $type, $fullUrl );
327	}
328
329	/**
330	 * Returns the path used to construct links with or false if there is no such path.
331	 *
332	 * Shall be equivalent to getPath( getLinkPathType() ).
333	 *
334	 * @return string|null
335	 */
336	public function getLinkPath() {
337		$type = $this->getLinkPathType();
338		return $type === null ? null : $this->getPath( $type );
339	}
340
341	/**
342	 * Returns the main path type, that is the type of the path that should
343	 * generally be used to construct links to the target site.
344	 *
345	 * This default implementation returns Site::PATH_LINK as the default path
346	 * type. Subclasses can override this to define a different default path
347	 * type, or return false to disable site links.
348	 *
349	 * @since 1.21
350	 *
351	 * @return string|null
352	 */
353	public function getLinkPathType() {
354		return self::PATH_LINK;
355	}
356
357	/**
358	 * Returns the full URL for the given page on the site.
359	 * Or null if the needed information is not known.
360	 *
361	 * This generated URL is usually based upon the path returned by getLinkPath(),
362	 * but this is not a requirement.
363	 *
364	 * This implementation returns a URL constructed using the path returned by getLinkPath().
365	 *
366	 * @since 1.21
367	 *
368	 * @param bool|string $pageName
369	 *
370	 * @return string|null
371	 */
372	public function getPageUrl( $pageName = false ) {
373		$url = $this->getLinkPath();
374
375		if ( $url === null ) {
376			return null;
377		}
378
379		if ( $pageName !== false ) {
380			$url = str_replace( '$1', rawurlencode( $pageName ), $url );
381		}
382
383		return $url;
384	}
385
386	/**
387	 * Attempt to normalize the page name in some fashion.
388	 * May return false to indicate various kinds of failure.
389	 *
390	 * This implementation returns $pageName without changes.
391	 *
392	 * @see Site::normalizePageName
393	 *
394	 * @since 1.21
395	 *
396	 * @param string $pageName
397	 *
398	 * @return string|false
399	 */
400	public function normalizePageName( $pageName ) {
401		return $pageName;
402	}
403
404	/**
405	 * Returns the type specific fields.
406	 *
407	 * @since 1.21
408	 *
409	 * @return array
410	 */
411	public function getExtraData() {
412		return $this->extraData;
413	}
414
415	/**
416	 * Sets the type specific fields.
417	 *
418	 * @since 1.21
419	 *
420	 * @param array $extraData
421	 */
422	public function setExtraData( array $extraData ) {
423		$this->extraData = $extraData;
424	}
425
426	/**
427	 * Returns the type specific config.
428	 *
429	 * @since 1.21
430	 *
431	 * @return array
432	 */
433	public function getExtraConfig() {
434		return $this->extraConfig;
435	}
436
437	/**
438	 * Sets the type specific config.
439	 *
440	 * @since 1.21
441	 *
442	 * @param array $extraConfig
443	 */
444	public function setExtraConfig( array $extraConfig ) {
445		$this->extraConfig = $extraConfig;
446	}
447
448	/**
449	 * Returns language code of the sites primary language.
450	 * Or null if it's not known.
451	 *
452	 * @since 1.21
453	 *
454	 * @return string|null
455	 */
456	public function getLanguageCode() {
457		return $this->languageCode;
458	}
459
460	/**
461	 * Sets language code of the sites primary language.
462	 *
463	 * @since 1.21
464	 *
465	 * @param string|null $languageCode
466	 */
467	public function setLanguageCode( $languageCode ) {
468		if ( $languageCode !== null
469			&& !MediaWikiServices::getInstance()
470				->getLanguageNameUtils()
471				->isValidCode( $languageCode )
472		) {
473			throw new InvalidArgumentException( "$languageCode is not a valid language code." );
474		}
475		$this->languageCode = $languageCode;
476	}
477
478	/**
479	 * Returns the set internal identifier for the site.
480	 *
481	 * @since 1.21
482	 *
483	 * @return string|null
484	 */
485	public function getInternalId() {
486		return $this->internalId;
487	}
488
489	/**
490	 * Sets the internal identifier for the site.
491	 * This typically is a primary key in a db table.
492	 *
493	 * @since 1.21
494	 *
495	 * @param int|null $internalId
496	 */
497	public function setInternalId( $internalId = null ) {
498		$this->internalId = $internalId;
499	}
500
501	/**
502	 * Adds a local identifier.
503	 *
504	 * @since 1.21
505	 *
506	 * @param string $type
507	 * @param string $identifier
508	 */
509	public function addLocalId( $type, $identifier ) {
510		if ( $this->localIds === false ) {
511			$this->localIds = [];
512		}
513
514		if ( !array_key_exists( $type, $this->localIds ) ) {
515			$this->localIds[$type] = [];
516		}
517
518		if ( !in_array( $identifier, $this->localIds[$type] ) ) {
519			$this->localIds[$type][] = $identifier;
520		}
521	}
522
523	/**
524	 * Adds an interwiki id to the site.
525	 *
526	 * @since 1.21
527	 *
528	 * @param string $identifier
529	 */
530	public function addInterwikiId( $identifier ) {
531		$this->addLocalId( self::ID_INTERWIKI, $identifier );
532	}
533
534	/**
535	 * Adds a navigation id to the site.
536	 *
537	 * @since 1.21
538	 *
539	 * @param string $identifier
540	 */
541	public function addNavigationId( $identifier ) {
542		$this->addLocalId( self::ID_EQUIVALENT, $identifier );
543	}
544
545	/**
546	 * Returns the interwiki link identifiers that can be used for this site.
547	 *
548	 * @since 1.21
549	 *
550	 * @return string[]
551	 */
552	public function getInterwikiIds() {
553		return array_key_exists( self::ID_INTERWIKI, $this->localIds )
554			? $this->localIds[self::ID_INTERWIKI]
555			: [];
556	}
557
558	/**
559	 * Returns the equivalent link identifiers that can be used to make
560	 * the site show up in interfaces such as the "language links" section.
561	 *
562	 * @since 1.21
563	 *
564	 * @return string[]
565	 */
566	public function getNavigationIds() {
567		return array_key_exists( self::ID_EQUIVALENT, $this->localIds )
568			? $this->localIds[self::ID_EQUIVALENT] :
569			[];
570	}
571
572	/**
573	 * Returns all local ids
574	 *
575	 * @since 1.21
576	 *
577	 * @return array[]
578	 */
579	public function getLocalIds() {
580		return $this->localIds;
581	}
582
583	/**
584	 * Sets the path used to construct links with.
585	 * Shall be equivalent to setPath( getLinkPathType(), $fullUrl ).
586	 *
587	 * @since 1.21
588	 *
589	 * @param string $pathType
590	 * @param string $fullUrl
591	 *
592	 * @throws MWException
593	 */
594	public function setPath( $pathType, $fullUrl ) {
595		if ( !is_string( $fullUrl ) ) {
596			throw new MWException( '$fullUrl needs to be a string' );
597		}
598
599		if ( !array_key_exists( 'paths', $this->extraData ) ) {
600			$this->extraData['paths'] = [];
601		}
602
603		$this->extraData['paths'][$pathType] = $fullUrl;
604	}
605
606	/**
607	 * Returns the path of the provided type or false if there is no such path.
608	 *
609	 * @since 1.21
610	 *
611	 * @param string $pathType
612	 *
613	 * @return string|null
614	 */
615	public function getPath( $pathType ) {
616		$paths = $this->getAllPaths();
617		return array_key_exists( $pathType, $paths ) ? $paths[$pathType] : null;
618	}
619
620	/**
621	 * Returns the paths as associative array.
622	 * The keys are path types, the values are the path urls.
623	 *
624	 * @since 1.21
625	 *
626	 * @return string[]
627	 */
628	public function getAllPaths() {
629		return array_key_exists( 'paths', $this->extraData ) ? $this->extraData['paths'] : [];
630	}
631
632	/**
633	 * Removes the path of the provided type if it's set.
634	 *
635	 * @since 1.21
636	 *
637	 * @param string $pathType
638	 */
639	public function removePath( $pathType ) {
640		if ( array_key_exists( 'paths', $this->extraData ) ) {
641			unset( $this->extraData['paths'][$pathType] );
642		}
643	}
644
645	/**
646	 * @since 1.21
647	 *
648	 * @param string $siteType
649	 *
650	 * @return Site
651	 */
652	public static function newForType( $siteType ) {
653		global $wgSiteTypes;
654
655		if ( array_key_exists( $siteType, $wgSiteTypes ) ) {
656			return new $wgSiteTypes[$siteType]();
657		}
658
659		return new Site();
660	}
661
662	/**
663	 * @see Serializable::serialize
664	 *
665	 * @since 1.21
666	 *
667	 * @return string
668	 */
669	public function serialize() {
670		$fields = [
671			'globalid' => $this->globalId,
672			'type' => $this->type,
673			'group' => $this->group,
674			'source' => $this->source,
675			'language' => $this->languageCode,
676			'localids' => $this->localIds,
677			'config' => $this->extraConfig,
678			'data' => $this->extraData,
679			'forward' => $this->forward,
680			'internalid' => $this->internalId,
681
682		];
683
684		return serialize( $fields );
685	}
686
687	/**
688	 * @see Serializable::unserialize
689	 *
690	 * @since 1.21
691	 *
692	 * @param string $serialized
693	 */
694	public function unserialize( $serialized ) {
695		$fields = unserialize( $serialized );
696
697		$this->__construct( $fields['type'] );
698
699		$this->setGlobalId( $fields['globalid'] );
700		$this->setGroup( $fields['group'] );
701		$this->setSource( $fields['source'] );
702		$this->setLanguageCode( $fields['language'] );
703		$this->localIds = $fields['localids'];
704		$this->setExtraConfig( $fields['config'] );
705		$this->setExtraData( $fields['data'] );
706		$this->setForward( $fields['forward'] );
707		$this->setInternalId( $fields['internalid'] );
708	}
709}
710