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