1<?php
2
3use MediaWiki\Widget\TitleInputWidget;
4
5/**
6 * Implements a text input field for page titles.
7 * Automatically does validation that the title is valid,
8 * as well as autocompletion if using the OOUI display format.
9 *
10 * Optional parameters:
11 * 'namespace' - Namespace the page must be in (use namespace constant; one of the NS_* constants may be used)
12 * 'relative' - If true and 'namespace' given, strip/add the namespace from/to the title as needed
13 * 'creatable' - Whether to validate the title is creatable (not a special page)
14 * 'exists' - Whether to validate that the title already exists
15 * 'interwiki' – Tolerate interwiki links (other conditions such as 'namespace' or 'exists' will be
16 *   ignored if the title is an interwiki title). Cannot be used together with 'relative'.
17 *
18 * @stable to extend
19 * @since 1.26
20 */
21class HTMLTitleTextField extends HTMLTextField {
22	/**
23	 * @stable to call
24	 * @inheritDoc
25	 */
26	public function __construct( $params ) {
27		$params += [
28			'namespace' => false,
29			'relative' => false,
30			'creatable' => false,
31			'exists' => false,
32			// Default to null during the deprecation process so we can differentiate between
33			// callers who intentionally want to disallow interwiki titles and legacy callers
34			// who aren't aware of the setting.
35			'interwiki' => null,
36			// This overrides the default from HTMLFormField
37			'required' => true,
38		];
39
40		parent::__construct( $params );
41	}
42
43	public function validate( $value, $alldata ) {
44		if ( $this->mParams['interwiki'] && $this->mParams['relative'] ) {
45			// relative and interwiki cannot be used together, because we don't have a way to know about
46			// namespaces used by the other wiki (and it might actually be a non-wiki link, too).
47			throw new InvalidArgumentException( 'relative and interwiki may not be used together' );
48		}
49		// Default value (from getDefault()) is null, which breaks Title::newFromTextThrow() below
50		if ( $value === null ) {
51			$value = '';
52		}
53
54		if ( !$this->mParams['required'] && $value === '' ) {
55			// If this field is not required and the value is empty, that's okay, skip validation
56			return parent::validate( $value, $alldata );
57		}
58
59		try {
60			if ( !$this->mParams['relative'] ) {
61				$title = Title::newFromTextThrow( $value );
62			} else {
63				// Can't use Title::makeTitleSafe(), because it doesn't throw useful exceptions
64				$title = Title::newFromTextThrow( Title::makeName( $this->mParams['namespace'], $value ) );
65			}
66		} catch ( MalformedTitleException $e ) {
67			return $this->msg( $e->getErrorMessage(), $e->getErrorMessageParameters() );
68		}
69
70		if ( $title->isExternal() ) {
71			if ( $this->mParams['interwiki'] ) {
72				// We cannot validate external titles, skip the rest of the validation
73				return parent::validate( $value, $alldata );
74			} elseif ( $this->mParams['interwiki'] === null ) {
75				// Legacy caller, they probably don't need/want interwiki titles as those don't
76				// make sense in most use cases. To avoid a B/C break without deprecation, though,
77				// we let the title through and raise a warning. That way, code that needs to allow
78				// interwiki titles will get deprecation warnings as long as users actually submit
79				// interwiki titles to the form. That's not ideal but a less conditional warning
80				// would be impractical - having to update every title field in the world to avoid
81				// warnings would be too much of a burden.
82				wfDeprecated(
83					__METHOD__ . ' will reject external titles in 1.38 when interwiki is false '
84						. "(field: $this->mName)",
85					'1.37'
86				);
87				return parent::validate( $value, $alldata );
88			} else {
89				return $this->msg( 'htmlform-title-interwiki', $title->getPrefixedText() );
90			}
91		}
92
93		$text = $title->getPrefixedText();
94		if ( $this->mParams['namespace'] !== false &&
95			!$title->inNamespace( $this->mParams['namespace'] )
96		) {
97			return $this->msg( 'htmlform-title-badnamespace', $text, $this->mParams['namespace'] );
98		}
99
100		if ( $this->mParams['creatable'] && !$title->canExist() ) {
101			return $this->msg( 'htmlform-title-not-creatable', $text );
102		}
103
104		if ( $this->mParams['exists'] && !$title->exists() ) {
105			return $this->msg( 'htmlform-title-not-exists', $text );
106		}
107
108		return parent::validate( $value, $alldata );
109	}
110
111	protected function getInputWidget( $params ) {
112		if ( $this->mParams['namespace'] !== false ) {
113			$params['namespace'] = $this->mParams['namespace'];
114		}
115		$params['relative'] = $this->mParams['relative'];
116		return new TitleInputWidget( $params );
117	}
118
119	protected function shouldInfuseOOUI() {
120		return true;
121	}
122
123	protected function getOOUIModules() {
124		// FIXME: TitleInputWidget should be in its own module
125		return [ 'mediawiki.widgets' ];
126	}
127
128	public function getInputHtml( $value ) {
129		// add mw-searchInput class to enable search suggestions for non-OOUI, too
130		$this->mClass .= 'mw-searchInput';
131
132		// return the HTMLTextField html
133		return parent::getInputHTML( $value );
134	}
135
136	protected function getDataAttribs() {
137		return [
138			'data-mw-searchsuggest' => FormatJson::encode( [
139				'wrapAsLink' => false,
140			] ),
141		];
142	}
143}
144