Add field attribute support for `WysiwygFormField`
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / form / builder / field / wysiwyg / WysiwygFormField.class.php
CommitLineData
5158aa21 1<?php
71212588 2namespace wcf\system\form\builder\field\wysiwyg;
0dc68d99
MS
3use wcf\data\IMessageQuoteAction;
4use wcf\data\object\type\ObjectTypeCache;
f197a243 5use wcf\system\bbcode\BBCodeHandler;
5d086ddf 6use wcf\system\form\builder\data\processor\CustomFormDataProcessor;
71212588 7use wcf\system\form\builder\field\AbstractFormField;
0866c354 8use wcf\system\form\builder\field\IAttributeFormField;
71212588
MS
9use wcf\system\form\builder\field\IMaximumLengthFormField;
10use wcf\system\form\builder\field\IMinimumLengthFormField;
0866c354 11use wcf\system\form\builder\field\TInputAttributeFormField;
71212588
MS
12use wcf\system\form\builder\field\TMaximumLengthFormField;
13use wcf\system\form\builder\field\TMinimumLengthFormField;
5158aa21
MS
14use wcf\system\form\builder\field\validation\FormFieldValidationError;
15use wcf\system\form\builder\IFormDocument;
26e502ba
MS
16use wcf\system\form\builder\IObjectTypeFormNode;
17use wcf\system\form\builder\TObjectTypeFormNode;
5158aa21 18use wcf\system\html\input\HtmlInputProcessor;
a1a60aa6 19use wcf\system\message\censorship\Censorship;
0dc68d99 20use wcf\system\message\quote\MessageQuoteManager;
f197a243 21use wcf\system\WCF;
5158aa21
MS
22use wcf\util\StringUtil;
23
24/**
25 * Implementation of a form field for wysiwyg editors.
26 *
27 * @author Matthias Schmidt
7b7b9764 28 * @copyright 2001-2019 WoltLab GmbH
5158aa21
MS
29 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
30 * @package WoltLabSuite\Core\System\Form\Builder\Field
dd2d8c0c 31 * @since 5.2
5158aa21 32 */
0866c354
MS
33class WysiwygFormField extends AbstractFormField implements IAttributeFormField, IMaximumLengthFormField, IMinimumLengthFormField, IObjectTypeFormNode {
34 use TInputAttributeFormField {
35 getReservedFieldAttributes as private inputGetReservedFieldAttributes;
36 }
5158aa21
MS
37 use TMaximumLengthFormField;
38 use TMinimumLengthFormField;
71212588 39 use TObjectTypeFormNode;
5158aa21
MS
40
41 /**
42 * identifier used to autosave the field value; if empty, autosave is disabled
43 * @var string
44 */
9299c8e5 45 protected $autosaveId = '';
5158aa21 46
71212588
MS
47 /**
48 * input processor containing the wysiwyg text
49 * @var HtmlInputProcessor
50 */
51 protected $htmlInputProcessor;
52
5158aa21
MS
53 /**
54 * last time the field has been edited; if `0`, the last edit time is unknown
55 * @var int
56 */
9299c8e5 57 protected $lastEditTime = 0;
5158aa21
MS
58
59 /**
0dc68d99
MS
60 * quote-related data used to create the JavaScript quote manager
61 * @var null|array
62 */
63 protected $quoteData;
64
65 /**
66 * is `true` if this form field supports attachments, otherwise `false`
71212588 67 * @var boolean
5158aa21 68 */
71212588
MS
69 protected $supportAttachments = false;
70
71 /**
0dc68d99 72 * is `true` if this form field supports mentions, otherwise `false`
71212588
MS
73 * @var boolean
74 */
75 protected $supportMentions = false;
5158aa21 76
0dc68d99
MS
77 /**
78 * is `true` if this form field supports quotes, otherwise `false`
79 * @var boolean
80 */
81 protected $supportQuotes = false;
82
59a624e9 83 /**
84 * @inheritDoc
85 */
86 protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value';
87
5158aa21
MS
88 /**
89 * @inheritDoc
90 */
91 protected $templateName = '__wysiwygFormField';
92
93 /**
94 * Sets the identifier used to autosave the field value and returns this field.
95 *
96 * @param string $autosaveId identifier used to autosave field value
26e502ba 97 * @return WysiwygFormField this field
5158aa21 98 */
020e6570 99 public function autosaveId($autosaveId) {
9299c8e5 100 $this->autosaveId = $autosaveId;
5158aa21
MS
101
102 return $this;
103 }
104
0dc68d99
MS
105 /**
106 * @inheritDoc
107 */
108 public function cleanup() {
109 MessageQuoteManager::getInstance()->saved();
110 }
111
5158aa21
MS
112 /**
113 * Returns the identifier used to autosave the field value. If autosave is disabled,
9d68d5b2 114 * an empty string is returned.
5158aa21
MS
115 *
116 * @return string
117 */
7b43decd 118 public function getAutosaveId() {
9299c8e5 119 return $this->autosaveId;
5158aa21
MS
120 }
121
0dc68d99
MS
122 /**
123 * @inheritDoc
124 */
e9154428 125 public function getFieldHtml() {
0dc68d99
MS
126 if ($this->supportsQuotes()) {
127 MessageQuoteManager::getInstance()->assignVariables();
128 }
129
f197a243
MS
130 /** @noinspection PhpUndefinedFieldInspection */
131 $disallowedBBCodesPermission = $this->getObjectType()->disallowedBBCodesPermission;
132 if ($disallowedBBCodesPermission === null) {
133 $disallowedBBCodesPermission = 'user.message.disallowedBBCodes';
134 }
135
136 BBCodeHandler::getInstance()->setDisallowedBBCodes(explode(
137 ',',
138 WCF::getSession()->getPermission($disallowedBBCodesPermission)
139 ));
140
e9154428 141 return parent::getFieldHtml();
0dc68d99
MS
142 }
143
5158aa21
MS
144 /**
145 * @inheritDoc
146 */
7b43decd 147 public function getObjectTypeDefinition() {
5158aa21
MS
148 return 'com.woltlab.wcf.message';
149 }
150
151 /**
152 * Returns the last time the field has been edited. If no last edit time has
153 * been set, `0` is returned.
154 *
155 * @return int
156 */
7b43decd 157 public function getLastEditTime() {
9299c8e5 158 return $this->lastEditTime;
5158aa21
MS
159 }
160
0dc68d99
MS
161 /**
162 * Returns all quote data or specific quote data if an argument is given.
163 *
164 * @param null|string $index quote data index
165 * @return string[]|string
166 *
167 * @throws \BadMethodCallException if quotes are not supported for this field
168 * @throws \InvalidArgumentException if unknown quote data is requested
169 */
170 public function getQuoteData($index = null) {
171 if (!$this->supportQuotes()) {
172 throw new \BadMethodCallException("Quotes are not supported.");
173 }
174
175 if ($index === null) {
176 return $this->quoteData;
177 }
178
179 if (!isset($this->quoteData[$index])) {
180 throw new \InvalidArgumentException("Unknown quote data '{$index}'.");
181 }
182
183 return $this->quoteData[$index];
184 }
185
5158aa21
MS
186 /**
187 * @inheritDoc
188 */
7b43decd 189 public function hasSaveValue() {
071fff89 190 return false;
5158aa21
MS
191 }
192
193 /**
194 * Sets the last time this field has been edited and returns this field.
195 *
196 * @param int $lastEditTime last time field has been edited
26e502ba 197 * @return WysiwygFormField this field
5158aa21 198 */
020e6570 199 public function lastEditTime($lastEditTime) {
9299c8e5 200 $this->lastEditTime = $lastEditTime;
5158aa21
MS
201
202 return $this;
203 }
204
205 /**
206 * @inheritDoc
207 */
7b43decd 208 public function populate() {
5158aa21
MS
209 parent::populate();
210
5d086ddf 211 $this->getDocument()->getDataHandler()->addProcessor(new CustomFormDataProcessor('wysiwyg', function(IFormDocument $document, array $parameters) {
8bdbb251 212 if ($this->checkDependencies()) {
f3a825e9 213 $parameters[$this->getObjectProperty() . '_htmlInputProcessor'] = $this->htmlInputProcessor;
8bdbb251 214 }
5158aa21
MS
215
216 return $parameters;
217 }));
218
219 return $this;
220 }
0dc68d99
MS
221
222 /**
223 * Sets the data required for advanced quote support for when quotable content is present
224 * on the active page and returns this field.
225 *
226 * Calling this method automatically enables quote support for this field.
227 *
228 * @param string $objectType name of the relevant `com.woltlab.wcf.message.quote` object type
229 * @param string $actionClass action class implementing `wcf\data\IMessageQuoteAction`
230 * @param string[] $selectors selectors for the quotable content (required keys: `container`, `messageBody`, and `messageContent`)
231 * @return static
232 *
233 * @throws \InvalidArgumentException if any of the given arguments is invalid
234 */
235 public function quoteData($objectType, $actionClass, array $selectors = []) {
236 if (ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.message.quote', $objectType) === null) {
237 throw new \InvalidArgumentException("Unknown message quote object type '{$objectType}'.");
238 }
239
240 if (!class_exists($actionClass)) {
241 throw new \InvalidArgumentException("Unknown class '{$actionClass}'");
242 }
243 if (!is_subclass_of($actionClass, IMessageQuoteAction::class)) {
244 throw new \InvalidArgumentException("'{$actionClass}' does not implement '" . IMessageQuoteAction::class . "'.");
245 }
246
247 if (!empty($selectors)) {
248 foreach (['container', 'messageBody', 'messageContent'] as $selector) {
249 if (!isset($selectors[$selector])) {
250 throw new \InvalidArgumentException("Missing selector '{$selector}'.");
251 }
252 }
253 }
254
255 $this->supportQuotes();
256
257 $this->quoteData = [
258 'actionClass' => $actionClass,
259 'objectType' => $objectType,
260 'selectors' => $selectors
261 ];
262
263 return $this;
264 }
5158aa21
MS
265
266 /**
267 * @inheritDoc
268 */
7b43decd 269 public function readValue() {
4b683ecd
MS
270 if ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
271 $value = $this->getDocument()->getRequestData($this->getPrefixedId());
272
273 if (is_string($value)) {
9299c8e5 274 $this->value = StringUtil::trim($value);
4b683ecd 275 }
5158aa21
MS
276 }
277
0dc68d99
MS
278 if ($this->supportsQuotes()) {
279 MessageQuoteManager::getInstance()->readFormParameters();
280 }
281
5158aa21
MS
282 return $this;
283 }
284
71212588
MS
285 /**
286 * Sets if the form field supports attachments and returns this field.
287 *
288 * @param boolean $supportAttachments
26e502ba 289 * @return WysiwygFormField this field
71212588
MS
290 */
291 public function supportAttachments($supportAttachments = true) {
292 $this->supportAttachments = $supportAttachments;
293
294 return $this;
295 }
296
297 /**
298 * Sets if the form field supports mentions and returns this field.
299 *
300 * @param boolean $supportMentions
26e502ba 301 * @return WysiwygFormField this field
71212588
MS
302 */
303 public function supportMentions($supportMentions = true) {
304 $this->supportMentions = $supportMentions;
305
306 return $this;
307 }
308
0dc68d99
MS
309 /**
310 * Sets if the form field supports quotes and returns this field.
311 *
312 * @param boolean $supportQuotes
313 * @return WysiwygFormField this field
314 */
315 public function supportQuotes($supportQuotes = true) {
316 $this->supportQuotes = $supportQuotes;
317
318 if (!$this->supportsQuotes()) {
319 // unset previously set quote data
320 $this->quoteData = null;
321 }
322 else {
323 MessageQuoteManager::getInstance()->readParameters();
324 }
325
326 return $this;
327 }
328
71212588
MS
329 /**
330 * Returns `true` if the form field supports attachments and returns `false` otherwise.
331 *
332 * Important: If this method returns `true`, it does not necessarily mean that attachment
333 * support will also work as that is the task of `WysiwygAttachmentFormField`. This method
334 * is primarily relevant to inform the JavaScript API that the field supports attachments
335 * so that the relevant editor plugin is loaded.
336 *
337 * By default, attachments are not supported.
338 *
339 * @return boolean
340 */
341 public function supportsAttachments() {
342 return $this->supportAttachments;
343 }
344
345 /**
346 * Returns `true` if the form field supports mentions and returns `false` otherwise.
347 *
348 * By default, mentions are not supported.
349 *
350 * @return boolean
351 */
352 public function supportsMentions() {
353 return $this->supportMentions;
354 }
355
0dc68d99
MS
356 /**
357 * Returns `true` if the form field supports quotes and returns `false` otherwise.
358 *
a11099b7 359 * By default, quotes are not supported.
0dc68d99
MS
360 *
361 * @return boolean
362 */
363 public function supportsQuotes() {
477f6668 364 return $this->supportQuotes;
0dc68d99
MS
365 }
366
5158aa21
MS
367 /**
368 * @inheritDoc
369 */
370 public function validate() {
f197a243
MS
371 /** @noinspection PhpUndefinedFieldInspection */
372 $disallowedBBCodesPermission = $this->getObjectType()->disallowedBBCodesPermission;
373 if ($disallowedBBCodesPermission === null) {
374 $disallowedBBCodesPermission = 'user.message.disallowedBBCodes';
375 }
376
377 BBCodeHandler::getInstance()->setDisallowedBBCodes(explode(
378 ',',
379 WCF::getSession()->getPermission($disallowedBBCodesPermission)
380 ));
381
6fc348d5
MS
382 $this->htmlInputProcessor = new HtmlInputProcessor();
383 $this->htmlInputProcessor->process($this->getValue(), $this->getObjectType()->objectType);
384
385 if ($this->isRequired() && $this->htmlInputProcessor->appearsToBeEmpty()) {
5158aa21
MS
386 $this->addValidationError(new FormFieldValidationError('empty'));
387 }
388 else {
f197a243
MS
389 $disallowedBBCodes = $this->htmlInputProcessor->validate();
390 if (!empty($disallowedBBCodes)) {
391 $this->addValidationError(new FormFieldValidationError(
392 'disallowedBBCodes',
393 'wcf.message.error.disallowedBBCodes',
394 ['disallowedBBCodes' => $disallowedBBCodes]
395 ));
396 }
397 else {
398 $message = $this->htmlInputProcessor->getTextContent();
399 $this->validateMinimumLength($message);
400 $this->validateMaximumLength($message);
401
402 if (empty($this->getValidationErrors()) && ENABLE_CENSORSHIP) {
403 $result = Censorship::getInstance()->test($message);
404 if ($result) {
405 $this->addValidationError(new FormFieldValidationError(
406 'censoredWords',
407 'wcf.message.error.censoredWordsFound',
408 ['censoredWords' => $result]
409 ));
410 }
a1a60aa6
MS
411 }
412 }
5158aa21
MS
413 }
414
5158aa21
MS
415 parent::validate();
416 }
0866c354
MS
417
418 /**
419 * @inheritDoc
420 * @since 5.4
421 */
422 protected static function getReservedFieldAttributes(): array {
423 return array_merge(
424 static::inputGetReservedFieldAttributes(),
425 [
426 'data-autosave',
427 'data-autosave-last-edit-time',
428 'data-disable-attachments',
429 'data-support-mention',
430 ]
431 );
432 }
5158aa21 433}