--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\option\Option;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\I18nValue;
+use wcf\system\WCF;
+
+/**
+ * Default implementation for custom options utilizing the option system.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ * @since 3.1
+ */
+abstract class AbstractCustomOptionForm extends AbstractAcpForm {
+ /**
+ * option name
+ * @var string
+ */
+ public $optionTitle = '';
+
+ /**
+ * option description
+ * @var string
+ */
+ public $optionDescription = '';
+
+ /**
+ * option type
+ * @var string
+ */
+ public $optionType = 'text';
+
+ /**
+ * option default value
+ * @var string
+ */
+ public $defaultValue = '';
+
+ /**
+ * validation pattern
+ * @var string
+ */
+ public $validationPattern = '';
+
+ /**
+ * select options
+ * @var string
+ */
+ public $selectOptions = '';
+
+ /**
+ * field is required
+ * @var boolean
+ */
+ public $required = 0;
+
+ /**
+ * show order
+ * @var integer
+ */
+ public $showOrder = 0;
+
+ /**
+ * action class name
+ * @var string
+ */
+ public $actionClass = '';
+
+ /**
+ * base class name
+ * @var string
+ */
+ public $baseClass = '';
+
+ /**
+ * editor class name
+ * @var string
+ */
+ public $editorClass = '';
+
+ /**
+ * object instance
+ * @var Option
+ */
+ public $object;
+
+ /**
+ * object id
+ * @var integer
+ */
+ public $objectID;
+
+ /**
+ * available option types
+ * @var string[]
+ */
+ public static $availableOptionTypes = [
+ 'boolean',
+ 'checkboxes',
+ 'date',
+ 'integer',
+ 'float',
+ 'multiSelect',
+ 'radioButton',
+ 'select',
+ 'text',
+ 'textarea',
+ 'URL'
+ ];
+
+ /**
+ * list of option type that require select options
+ * @var string[]
+ */
+ public static $optionTypesUsingSelectOptions = [
+ 'checkboxes',
+ 'multiSelect',
+ 'radioButton',
+ 'select'
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (empty($this->action)) {
+ throw new \RuntimeException("The 'action' property must equal 'add' or 'edit'.");
+ }
+
+ if ($this->action === 'edit') {
+ if (isset($_REQUEST['id'])) $this->objectID = intval($_REQUEST['id']);
+ $this->object = new $this->baseClass($this->objectID);
+ if (!$this->object->getObjectID()) {
+ throw new IllegalLinkException();
+ }
+ }
+
+ $this->registerI18nValue(new I18nValue('optionTitle'));
+
+ $optionDescription = new I18nValue('optionDescription');
+ $optionDescription->setFlags(I18nValue::ALLOW_EMPTY);
+ $this->registerI18nValue($optionDescription);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readFormParameters() {
+ parent::readFormParameters();
+
+ if (isset($_POST['optionType'])) $this->optionType = $_POST['optionType'];
+ if (isset($_POST['defaultValue'])) $this->defaultValue = $_POST['defaultValue'];
+ if (isset($_POST['validationPattern'])) $this->validationPattern = $_POST['validationPattern'];
+ if (isset($_POST['selectOptions'])) $this->selectOptions = $_POST['selectOptions'];
+ if (isset($_POST['required'])) $this->required = intval($_POST['required']);
+ if (isset($_POST['showOrder'])) $this->showOrder = intval($_POST['showOrder']);
+
+ if ($this->optionType == 'boolean' || $this->optionType == 'integer') {
+ $this->defaultValue = intval($this->defaultValue);
+ }
+ if ($this->optionType == 'float') {
+ $this->defaultValue = floatval($this->defaultValue);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate() {
+ parent::validate();
+
+ // option type
+ if (!in_array($this->optionType, self::$availableOptionTypes)) {
+ throw new UserInputException('optionType');
+ }
+
+ // select options
+ if (in_array($this->optionType, self::$optionTypesUsingSelectOptions) && empty($this->selectOptions)) {
+ throw new UserInputException('selectOptions');
+ }
+ }
+
+ /**
+ * Returns the list of database values including additional fields.
+ *
+ * @return array
+ */
+ protected function getDatabaseValues() {
+ return array_merge($this->additionalFields, [
+ 'optionTitle' => $this->optionTitle,
+ 'optionDescription' => $this->optionDescription,
+ 'optionType' => $this->optionType,
+ 'defaultValue' => $this->defaultValue,
+ 'showOrder' => $this->showOrder,
+ 'validationPattern' => $this->validationPattern,
+ 'selectOptions' => $this->selectOptions,
+ 'required' => $this->required
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ parent::save();
+
+ if ($this->action === 'add') {
+ $this->objectAction = new $this->actionClass([], 'create', ['data' => $this->getDatabaseValues()]);
+
+ $this->saveI18n($this->objectAction->executeAction()['returnValues'], $this->editorClass);
+
+ $this->reset();
+ }
+ else {
+ $this->saved();
+
+ // show success message
+ WCF::getTPL()->assign('success', true);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function reset() {
+ parent::reset();
+
+ // reset values
+ $this->optionTitle = $this->optionDescription = $this->optionType = $this->defaultValue = $this->validationPattern = $this->selectOptions = '';
+ $this->optionType = 'text';
+ $this->required = $this->showOrder = 0;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'defaultValue' => $this->defaultValue,
+ 'validationPattern' => $this->validationPattern,
+ 'optionType' => $this->optionType,
+ 'selectOptions' => $this->selectOptions,
+ 'required' => $this->required,
+ 'showOrder' => $this->showOrder,
+ 'action' => $this->action,
+ 'availableOptionTypes' => self::$availableOptionTypes,
+ 'optionTypesUsingSelectOptions' => self::$optionTypesUsingSelectOptions
+ ]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\custom\option;
+use wcf\data\option\Option;
+use wcf\system\bbcode\MessageParser;
+use wcf\system\bbcode\SimpleMessageParser;
+use wcf\system\exception\NotImplementedException;
+use wcf\system\WCF;
+use wcf\util\DateUtil;
+use wcf\util\OptionUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Default implementation for custom options.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Custom\Option
+ * @since 3.1
+ *
+ * @property-read integer $optionID unique id of the option
+ * @property-read string $optionTitle title of the option or name of language item which contains the title
+ * @property-read string $optionDescription description of the option or name of language item which contains the description
+ * @property-read string $optionType type of the option which determines its input and output
+ * @property-read string $defaultValue default value of the option
+ * @property-read string $validationPattern regular expression used to validate the value of the option
+ * @property-read string $selectOptions possible values of the option separated by newlines
+ * @property-read integer $required is `1` if the option has to be filled out, otherwise `0`
+ * @property-read integer $showOrder position of the option relation tp the other options
+ * @property-read integer $isDisabled is `1` if the option is disabled, otherwise `0`
+ */
+abstract class CustomOption extends Option {
+ /**
+ * option value
+ * @var string
+ */
+ protected $optionValue = '';
+
+ /**
+ * Returns true if the option is visible
+ *
+ * @return boolean
+ */
+ public function isVisible() {
+ return !$this->isDisabled;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getDatabaseTableAlias() {
+ throw new NotImplementedException();
+ }
+
+ /**
+ * Returns the value of this option.
+ *
+ * @return string
+ */
+ public function getOptionValue() {
+ return $this->optionValue;
+ }
+
+ /**
+ * Sets the value of this option.
+ *
+ * @param string $value
+ */
+ public function setOptionValue($value) {
+ $this->optionValue = $value;
+ }
+
+ /**
+ * Returns the formatted value of this option.
+ *
+ * @return string
+ */
+ public function getFormattedOptionValue() {
+ switch ($this->optionType) {
+ case 'boolean':
+ return WCF::getLanguage()->get('wcf.acp.option.optionType.boolean.'.($this->optionValue ? 'yes' : 'no'));
+
+ case 'date':
+ $year = $month = $day = 0;
+ $optionValue = explode('-', $this->optionValue);
+ if (isset($optionValue[0])) $year = intval($optionValue[0]);
+ if (isset($optionValue[1])) $month = intval($optionValue[1]);
+ if (isset($optionValue[2])) $day = intval($optionValue[2]);
+ return DateUtil::format(DateUtil::getDateTimeByTimestamp(gmmktime(12, 1, 1, $month, $day, $year)), DateUtil::DATE_FORMAT);
+
+ case 'float':
+ return StringUtil::formatDouble(intval($this->optionValue));
+
+ case 'integer':
+ return StringUtil::formatInteger(intval($this->optionValue));
+
+ case 'radioButton':
+ case 'select':
+ $selectOptions = OptionUtil::parseSelectOptions($this->selectOptions);
+ if (isset($selectOptions[$this->optionValue])) return WCF::getLanguage()->get($selectOptions[$this->optionValue]);
+ return '';
+
+ case 'multiSelect':
+ case 'checkboxes':
+ $selectOptions = OptionUtil::parseSelectOptions($this->selectOptions);
+ $values = explode("\n", $this->optionValue);
+ $result = '';
+ foreach ($values as $value) {
+ if (isset($selectOptions[$value])) {
+ if (!empty($result)) $result .= "<br>\n";
+ $result .= WCF::getLanguage()->get($selectOptions[$value]);
+ }
+ }
+ return $result;
+
+ case 'textarea':
+ return SimpleMessageParser::getInstance()->parse($this->optionValue);
+
+ case 'message':
+ return MessageParser::getInstance()->parse($this->optionValue);
+
+ case 'URL':
+ return StringUtil::getAnchorTag($this->optionValue);
+
+ default:
+ return StringUtil::encodeHTML($this->optionValue);
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\custom\option;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\IToggleAction;
+
+/**
+ * Executes option-related actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Custom\Option
+ * @since 3.1
+ *
+ * @method CustomOption create()
+ * @method CustomOptionEditor[] getObjects()
+ * @method CustomOptionEditor getSingleObject()
+ */
+abstract class CustomOptionAction extends AbstractDatabaseObjectAction implements IToggleAction {
+ /**
+ * @inheritDoc
+ */
+ protected $className = CustomOptionEditor::class;
+
+ /**
+ * @inheritDoc
+ */
+ public function validateToggle() {
+ $this->validateUpdate();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toggle() {
+ foreach ($this->getObjects() as $optionEditor) {
+ $optionEditor->update([
+ 'isDisabled' => 1 - $optionEditor->isDisabled
+ ]);
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\custom\option;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit file options.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Custom\Option
+ * @since 3.1
+ *
+ * @method static CustomOption create(array $parameters = [])
+ * @method CustomOption getDecoratedObject()
+ * @mixin CustomOption
+ */
+abstract class CustomOptionEditor extends DatabaseObjectEditor {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = CustomOption::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\custom\option;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of options.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Custom\Option
+ * @since 3.1
+ *
+ * @method CustomOption current()
+ * @method CustomOption[] getObjects()
+ * @method CustomOption|null search($objectID)
+ * @property CustomOption[] $objects
+ */
+abstract class CustomOptionList extends DatabaseObjectList {
+ /**
+ * @inheritDoc
+ */
+ public $className = CustomOption::class;
+
+ /**
+ * @inheritDoc
+ */
+ public $sqlOrderBy = 'showOrder';
+
+ /**
+ * @inheritDoc
+ */
+ public function __construct() {
+ parent::__construct();
+
+ $this->sqlSelects = "CONCAT('customOption', CAST({$this->getDatabaseTableAlias()}.optionID AS CHAR)) AS optionName";
+ }
+}