1<?php
2/**
3 * Special page which uses an HTMLForm to handle processing.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup SpecialPage
22 */
23
24/**
25 * Special page which uses an HTMLForm to handle processing.  This is mostly a
26 * clone of FormAction.  More special pages should be built this way; maybe this could be
27 * a new structure for SpecialPages.
28 *
29 * @ingroup SpecialPage
30 */
31abstract class FormSpecialPage extends SpecialPage {
32	/**
33	 * The sub-page of the special page.
34	 * @var string|null
35	 */
36	protected $par = null;
37
38	/**
39	 * @var array|null POST data preserved across re-authentication
40	 * @since 1.32
41	 */
42	protected $reauthPostData = null;
43
44	/**
45	 * Get an HTMLForm descriptor array
46	 * @return array
47	 */
48	abstract protected function getFormFields();
49
50	/**
51	 * Add pre-text to the form
52	 * @return string HTML which will be sent to $form->addPreText()
53	 */
54	protected function preText() {
55		return '';
56	}
57
58	/**
59	 * Add post-text to the form
60	 * @return string HTML which will be sent to $form->addPostText()
61	 */
62	protected function postText() {
63		return '';
64	}
65
66	/**
67	 * Play with the HTMLForm if you need to more substantially
68	 * @param HTMLForm $form
69	 */
70	protected function alterForm( HTMLForm $form ) {
71	}
72
73	/**
74	 * Get message prefix for HTMLForm
75	 *
76	 * @since 1.21
77	 * @return string
78	 */
79	protected function getMessagePrefix() {
80		return strtolower( $this->getName() );
81	}
82
83	/**
84	 * Get display format for the form. See HTMLForm documentation for available values.
85	 *
86	 * @since 1.25
87	 * @return string
88	 */
89	protected function getDisplayFormat() {
90		return 'table';
91	}
92
93	/**
94	 * Get the HTMLForm to control behavior
95	 * @return HTMLForm|null
96	 */
97	protected function getForm() {
98		$context = $this->getContext();
99		$onSubmit = [ $this, 'onSubmit' ];
100
101		if ( $this->reauthPostData ) {
102			// Restore POST data
103			$context = new DerivativeContext( $context );
104			$oldRequest = $this->getRequest();
105			$context->setRequest( new DerivativeRequest(
106				$oldRequest, $this->reauthPostData + $oldRequest->getQueryValues(), true
107			) );
108
109			// But don't treat it as a "real" submission just in case of some
110			// crazy kind of CSRF.
111			$onSubmit = static function () {
112				return false;
113			};
114		}
115
116		$form = HTMLForm::factory(
117			$this->getDisplayFormat(),
118			$this->getFormFields(),
119			$context,
120			$this->getMessagePrefix()
121		);
122		$form->setSubmitCallback( $onSubmit );
123		if ( $this->getDisplayFormat() !== 'ooui' ) {
124			// No legend and wrapper by default in OOUI forms, but can be set manually
125			// from alterForm()
126			$form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' );
127		}
128
129		$headerMsg = $this->msg( $this->getMessagePrefix() . '-text' );
130		if ( !$headerMsg->isDisabled() ) {
131			$form->addHeaderText( $headerMsg->parseAsBlock() );
132		}
133
134		$form->addPreText( $this->preText() );
135		$form->addPostText( $this->postText() );
136		$this->alterForm( $form );
137		if ( $form->getMethod() == 'post' ) {
138			// Retain query parameters (uselang etc) on POST requests
139			$params = array_diff_key(
140				$this->getRequest()->getQueryValues(), [ 'title' => null ] );
141			$form->addHiddenField( 'redirectparams', wfArrayToCgi( $params ) );
142		}
143
144		// Give hooks a chance to alter the form, adding extra fields or text etc
145		$this->getHookRunner()->onSpecialPageBeforeFormDisplay( $this->getName(), $form );
146
147		return $form;
148	}
149
150	/**
151	 * Process the form on POST submission.
152	 * @phpcs:disable MediaWiki.Commenting.FunctionComment.ExtraParamComment
153	 * @param array $data
154	 * @param HTMLForm|null $form
155	 * @suppress PhanCommentParamWithoutRealParam Many implementations don't have $form
156	 * @return bool|string|array|Status As documented for HTMLForm::trySubmit.
157	 * @phpcs:enable MediaWiki.Commenting.FunctionComment.ExtraParamComment
158	 */
159	abstract public function onSubmit( array $data /* HTMLForm $form = null */ );
160
161	/**
162	 * Do something exciting on successful processing of the form, most likely to show a
163	 * confirmation message
164	 * @since 1.22 Default is to do nothing
165	 */
166	public function onSuccess() {
167	}
168
169	/**
170	 * Basic SpecialPage workflow: get a form, send it to the user; get some data back,
171	 *
172	 * @param string|null $par Subpage string if one was specified
173	 */
174	public function execute( $par ) {
175		$this->setParameter( $par );
176		$this->setHeaders();
177
178		// This will throw exceptions if there's a problem
179		$this->checkExecutePermissions( $this->getUser() );
180
181		$securityLevel = $this->getLoginSecurityLevel();
182		if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) {
183			return;
184		}
185
186		$form = $this->getForm();
187		if ( $form->show() ) {
188			$this->onSuccess();
189		}
190	}
191
192	/**
193	 * Maybe do something interesting with the subpage parameter
194	 * @param string|null $par
195	 */
196	protected function setParameter( $par ) {
197		$this->par = $par;
198	}
199
200	/**
201	 * Called from execute() to check if the given user can perform this action.
202	 * Failures here must throw subclasses of ErrorPageError.
203	 * @param User $user
204	 * @throws UserBlockedError
205	 */
206	protected function checkExecutePermissions( User $user ) {
207		$this->checkPermissions();
208
209		if ( $this->requiresUnblock() ) {
210			$block = $user->getBlock();
211			if ( $block && $block->isSitewide() ) {
212				throw new UserBlockedError(
213					$block,
214					$user,
215					$this->getLanguage(),
216					$this->getRequest()->getIP()
217				);
218			}
219		}
220
221		if ( $this->requiresWrite() ) {
222			$this->checkReadOnly();
223		}
224	}
225
226	/**
227	 * Whether this action requires the wiki not to be locked
228	 * @return bool
229	 */
230	public function requiresWrite() {
231		return true;
232	}
233
234	/**
235	 * Whether this action cannot be executed by a blocked user
236	 * @return bool
237	 */
238	public function requiresUnblock() {
239		return true;
240	}
241
242	/**
243	 * Preserve POST data across reauthentication
244	 *
245	 * @since 1.32
246	 * @param array $data
247	 */
248	protected function setReauthPostData( array $data ) {
249		$this->reauthPostData = $data;
250	}
251}
252