1 <?php
   2 
   3 /**
   4  * @package redaxo\core\form
   5  */
   6 abstract class rex_form_base
   7 {
   8     /** @var string */
   9     protected $name;
  10 
  11     /** @var string "get" or "post" */
  12     protected $method;
  13 
  14     /** @var string */
  15     protected $fieldset;
  16 
  17     /** @var array */
  18     protected $elements;
  19 
  20     /** @var array */
  21     protected $params;
  22 
  23     /** @var bool */
  24     protected $debug;
  25 
  26     /** @var null|string */
  27     protected $applyUrl;
  28 
  29     /** @var null|string */
  30     protected $message;
  31 
  32     /** @var array */
  33     protected $errorMessages;
  34 
  35     /** @var string */
  36     protected $warning;
  37 
  38     /** @var null|string */
  39     protected $formId;
  40 
  41     /** @var rex_csrf_token */
  42     private $csrfToken;
  43 
  44     /**
  45      * Diese Konstruktor sollte nicht verwendet werden. Instanzen muessen ueber die facotry() Methode erstellt werden!
  46      */
  47     protected function __construct($fieldset, $name, $method = 'post', $debug = false)
  48     {
  49         if (!in_array($method, ['post', 'get'])) {
  50             throw new InvalidArgumentException("rex_form: Method-Parameter darf nur die Werte 'post' oder 'get' annehmen!");
  51         }
  52 
  53         $this->name = $name;
  54         $this->method = $method;
  55         $this->elements = [];
  56         $this->params = [];
  57         $this->addFieldset($fieldset ?: $this->name);
  58         $this->setMessage('');
  59 
  60         $this->debug = $debug;
  61 
  62         $this->csrfToken = rex_csrf_token::factory('rex_form_'.$this->getName());
  63     }
  64 
  65     /**
  66      * Initialisiert das Formular.
  67      */
  68     public function init()
  69     {
  70         // nichts tun
  71     }
  72 
  73     /**
  74      * Laedt die Konfiguration die noetig ist um rex_form im REDAXO Backend zu verwenden.
  75      */
  76     protected function loadBackendConfig()
  77     {
  78         $this->addParam('page', rex_be_controller::getCurrentPage());
  79     }
  80 
  81     public function setFormId($id)
  82     {
  83         $this->formId = $id;
  84     }
  85 
  86     /**
  87      * Gibt eine Formular-Url zurück.
  88      *
  89      * @param array $params
  90      * @param bool  $escape
  91      *
  92      * @return string
  93      */
  94     public function getUrl(array $params = [], $escape = true)
  95     {
  96         $params = array_merge($this->getParams(), $params);
  97         $params['form'] = $this->getName();
  98 
  99         $url = rex::isBackend() ? rex_url::backendController($params, $escape) : rex_url::frontendController($params, $escape);
 100 
 101         return $url;
 102     }
 103 
 104     // --------- Sections
 105 
 106     /**
 107      * Fuegt dem Formular ein Fieldset hinzu.
 108      * Dieses dient dazu ein Formular in mehrere Abschnitte zu gliedern.
 109      */
 110     public function addFieldset($fieldset)
 111     {
 112         $this->fieldset = $fieldset;
 113     }
 114 
 115     // --------- Fields
 116 
 117     /**
 118      * Fuegt dem Formular ein Input-Feld hinzu.
 119      *
 120      * @param string $tag
 121      * @param string $name
 122      * @param mixed  $value
 123      * @param array  $attributes
 124      * @param bool   $addElement
 125      *
 126      * @return rex_form_element
 127      */
 128     public function addField($tag, $name, $value = null, array $attributes = [], $addElement = true)
 129     {
 130         $element = $this->createElement($tag, $name, $value, $attributes);
 131 
 132         if ($addElement) {
 133             $this->addElement($element);
 134             return $element;
 135         }
 136 
 137         return $element;
 138     }
 139 
 140     /**
 141      * Fuegt dem Formular ein Container-Feld hinzu.
 142      *
 143      * Ein Container-Feld wiederrum kann weitere Felder enthalten.
 144      *
 145      * @param string $name
 146      * @param mixed  $value
 147      * @param array  $attributes
 148      *
 149      * @return rex_form_container_element
 150      */
 151     public function addContainerField($name, $value = null, array $attributes = [])
 152     {
 153         if (!isset($attributes['class'])) {
 154             $attributes['class'] = 'rex-form-container';
 155         }
 156         $attributes['internal::fieldClass'] = 'rex_form_container_element';
 157 
 158         $field = $this->addField('', $name, $value, $attributes, true);
 159         return $field;
 160     }
 161 
 162     /**
 163      * Fuegt dem Formular ein Input-Feld mit dem Type $type hinzu.
 164      *
 165      * @param string $type
 166      * @param string $name
 167      * @param mixed  $value
 168      * @param array  $attributes
 169      * @param bool   $addElement
 170      *
 171      * @return rex_form_element
 172      */
 173     public function addInputField($type, $name, $value = null, array $attributes = [], $addElement = true)
 174     {
 175         $attributes['type'] = $type;
 176         $field = $this->addField('input', $name, $value, $attributes, $addElement);
 177         return $field;
 178     }
 179 
 180     /**
 181      * Fuegt dem Formular ein Text-Feld hinzu.
 182      *
 183      * @param string $name
 184      * @param mixed  $value
 185      * @param array  $attributes
 186      *
 187      * @return rex_form_element
 188      */
 189     public function addTextField($name, $value = null, array $attributes = [])
 190     {
 191         if (!isset($attributes['class'])) {
 192             $attributes['class'] = 'form-control';
 193         }
 194         $field = $this->addInputField('text', $name, $value, $attributes);
 195         return $field;
 196     }
 197 
 198     /**
 199      * Fuegt dem Formular ein Read-Only-Text-Feld hinzu.
 200      * Dazu wird ein input-Element verwendet.
 201      *
 202      * @param string $name
 203      * @param mixed  $value
 204      * @param array  $attributes
 205      *
 206      * @return rex_form_element
 207      */
 208     public function addReadOnlyTextField($name, $value = null, array $attributes = [])
 209     {
 210         $attributes['readonly'] = 'readonly';
 211         if (!isset($attributes['class'])) {
 212             $attributes['class'] = 'form-control';
 213         }
 214         $field = $this->addInputField('text', $name, $value, $attributes);
 215         return $field;
 216     }
 217 
 218     /**
 219      * Fuegt dem Formular ein Read-Only-Feld hinzu.
 220      * Dazu wird ein span-Element verwendet.
 221      *
 222      * @param string $name
 223      * @param mixed  $value
 224      * @param array  $attributes
 225      *
 226      * @return rex_form_element
 227      */
 228     public function addReadOnlyField($name, $value = null, array $attributes = [])
 229     {
 230         $attributes['internal::fieldSeparateEnding'] = true;
 231         $attributes['internal::noNameAttribute'] = true;
 232         if (!isset($attributes['class'])) {
 233             // Wenn die class geaendert wird, muss auch
 234             // rex_form_container_element::getSaveValue()
 235             // angepasst werden
 236             $attributes['class'] = 'form-control-static';
 237         }
 238         $field = $this->addField('p', $name, $value, $attributes, true);
 239         return $field;
 240     }
 241 
 242     /**
 243      * Fuegt dem Fomular ein Hidden-Feld hinzu.
 244      *
 245      * @param string $name
 246      * @param mixed  $value
 247      * @param array  $attributes
 248      *
 249      * @return rex_form_element
 250      */
 251     public function addHiddenField($name, $value = null, array $attributes = [])
 252     {
 253         $field = $this->addInputField('hidden', $name, $value, $attributes, true);
 254         return $field;
 255     }
 256 
 257     /**
 258      * Fuegt dem Fomular ein Checkbox-Feld hinzu.
 259      * Dies ermoeglicht die Mehrfach-Selektion aus einer vorgegeben Auswahl an Werten.
 260      *
 261      * @param string $name
 262      * @param mixed  $value
 263      * @param array  $attributes
 264      *
 265      * @return rex_form_checkbox_element
 266      */
 267     public function addCheckboxField($name, $value = null, array $attributes = [])
 268     {
 269         $attributes['internal::fieldClass'] = 'rex_form_checkbox_element';
 270         $field = $this->addField('', $name, $value, $attributes);
 271         return $field;
 272     }
 273 
 274     /**
 275      * Fuegt dem Formular ein Radio-Feld hinzu.
 276      * Dies ermoeglicht eine Einfache-Selektion aus einer vorgegeben Auswahl an Werten.
 277      *
 278      * @param string $name
 279      * @param mixed  $value
 280      * @param array  $attributes
 281      *
 282      * @return rex_form_radio_element
 283      */
 284     public function addRadioField($name, $value = null, array $attributes = [])
 285     {
 286         $attributes['internal::fieldClass'] = 'rex_form_radio_element';
 287         $field = $this->addField('radio', $name, $value, $attributes);
 288         return $field;
 289     }
 290 
 291     /**
 292      * Fuegt dem Formular ein Textarea-Feld hinzu.
 293      *
 294      * @param string $name
 295      * @param mixed  $value
 296      * @param array  $attributes
 297      *
 298      * @return rex_form_element
 299      */
 300     public function addTextAreaField($name, $value = null, array $attributes = [])
 301     {
 302         $attributes['internal::fieldSeparateEnding'] = true;
 303         if (!isset($attributes['class'])) {
 304             $attributes['class'] = 'form-control';
 305         }
 306         /*
 307         if (!isset($attributes['cols'])) {
 308             $attributes['cols'] = 50;
 309         }
 310         */
 311         if (!isset($attributes['rows'])) {
 312             $attributes['rows'] = 6;
 313         }
 314 
 315         $field = $this->addField('textarea', $name, $value, $attributes);
 316         return $field;
 317     }
 318 
 319     /**
 320      * Fuegt dem Formular ein Select/Auswahl-Feld hinzu.
 321      *
 322      * @param string $name
 323      * @param mixed  $value
 324      * @param array  $attributes
 325      *
 326      * @return rex_form_select_element
 327      */
 328     public function addSelectField($name, $value = null, array $attributes = [])
 329     {
 330         $attributes['internal::fieldClass'] = 'rex_form_select_element';
 331         if (!isset($attributes['class'])) {
 332             $attributes['class'] = 'form-control';
 333         }
 334         $field = $this->addField('', $name, $value, $attributes, true);
 335         return $field;
 336     }
 337 
 338     /**
 339      * Fuegt dem Formular ein Feld hinzu mit dem der Medienpool angebunden werden kann.
 340      * Es kann nur ein Element aus dem Medienpool eingefuegt werden.
 341      *
 342      * @param string $name
 343      * @param mixed  $value
 344      * @param array  $attributes
 345      *
 346      * @throws rex_exception
 347      *
 348      * @return rex_form_widget_media_element
 349      */
 350     public function addMediaField($name, $value = null, array $attributes = [])
 351     {
 352         if (!rex_addon::get('mediapool')->isAvailable()) {
 353             throw new rex_exception(__METHOD__ . '() needs "mediapool" addon!');
 354         }
 355         $attributes['internal::fieldClass'] = 'rex_form_widget_media_element';
 356         $field = $this->addField('', $name, $value, $attributes, true);
 357         return $field;
 358     }
 359 
 360     /**
 361      * Fuegt dem Formular ein Feld hinzu mit dem der Medienpool angebunden werden kann.
 362      * Damit koennen mehrere Elemente aus dem Medienpool eingefuegt werden.
 363      *
 364      * @param string $name
 365      * @param mixed  $value
 366      * @param array  $attributes
 367      *
 368      * @throws rex_exception
 369      *
 370      * @return rex_form_widget_medialist_element
 371      */
 372     public function addMedialistField($name, $value = null, array $attributes = [])
 373     {
 374         if (!rex_addon::get('mediapool')->isAvailable()) {
 375             throw new rex_exception(__METHOD__ . '() needs "mediapool" addon!');
 376         }
 377         $attributes['internal::fieldClass'] = 'rex_form_widget_medialist_element';
 378         $field = $this->addField('', $name, $value, $attributes, true);
 379         return $field;
 380     }
 381 
 382     /**
 383      * Fuegt dem Formular ein Feld hinzu mit dem die Struktur-Verwaltung angebunden werden kann.
 384      * Es kann nur ein Element aus der Struktur eingefuegt werden.
 385      *
 386      * @param string $name
 387      * @param mixed  $value
 388      * @param array  $attributes
 389      *
 390      * @throws rex_exception
 391      *
 392      * @return rex_form_widget_linkmap_element
 393      */
 394     public function addLinkmapField($name, $value = null, array $attributes = [])
 395     {
 396         if (!rex_addon::get('structure')->isAvailable()) {
 397             throw new rex_exception(__METHOD__ . '() needs "structure" addon!');
 398         }
 399         $attributes['internal::fieldClass'] = 'rex_form_widget_linkmap_element';
 400         $field = $this->addField('', $name, $value, $attributes, true);
 401         return $field;
 402     }
 403 
 404     /**
 405      * Fuegt dem Formular ein Feld hinzu mit dem die Struktur-Verwaltung angebunden werden kann.
 406      * Damit koennen mehrere Elemente aus der Struktur eingefuegt werden.
 407      *
 408      * @param string $name
 409      * @param mixed  $value
 410      * @param array  $attributes
 411      *
 412      * @throws rex_exception
 413      *
 414      * @return rex_form_widget_linklist_element
 415      */
 416     public function addLinklistField($name, $value = null, array $attributes = [])
 417     {
 418         if (!rex_addon::get('structure')->isAvailable()) {
 419             throw new rex_exception(__METHOD__ . '() needs "structure" addon!');
 420         }
 421         $attributes['internal::fieldClass'] = 'rex_form_widget_linklist_element';
 422         $field = $this->addField('', $name, $value, $attributes, true);
 423         return $field;
 424     }
 425 
 426     /**
 427      * Fuegt dem Fomualar ein Control-Feld hinzu.
 428      * Damit koennen versch. Aktionen mit dem Fomular durchgefuert werden.
 429      *
 430      * @param rex_form_element $saveElement
 431      * @param rex_form_element $applyElement
 432      * @param rex_form_element $deleteElement
 433      * @param rex_form_element $resetElement
 434      * @param rex_form_element $abortElement
 435      *
 436      * @return rex_form_control_element
 437      */
 438     public function addControlField($saveElement = null, $applyElement = null, $deleteElement = null, $resetElement = null, $abortElement = null)
 439     {
 440         $field = $this->addElement(new rex_form_control_element($this, $saveElement, $applyElement, $deleteElement, $resetElement, $abortElement));
 441         return $field;
 442     }
 443 
 444     /**
 445      * Fuegt dem Formular beliebiges HTML zu.
 446      *
 447      * @param string $html HTML code
 448      *
 449      * @return rex_form_raw_element
 450      */
 451     public function addRawField($html)
 452     {
 453         $field = $this->addElement(new rex_form_raw_element($html, $this));
 454         return $field;
 455     }
 456 
 457     /**
 458      * Fuegt dem Formular eine Fehlermeldung hinzu.
 459      */
 460     public function addErrorMessage($errorCode, $errorMessage)
 461     {
 462         $this->errorMessages[$errorCode] = $errorMessage;
 463     }
 464 
 465     /**
 466      * Fuegt dem Formular einen Parameter hinzu.
 467      * Diese an den Stellen eingefuegt, an denen das Fomular neue Requests erzeugt.
 468      */
 469     public function addParam($name, $value)
 470     {
 471         $this->params[$name] = $value;
 472     }
 473 
 474     /**
 475      * Gibt alle Parameter des Fomulars zurueck.
 476      *
 477      * @return array
 478      */
 479     public function getParams()
 480     {
 481         return $this->params;
 482     }
 483 
 484     /**
 485      * Gibt den Wert des Parameters $name zurueck,
 486      * oder $default kein Parameter mit dem Namen exisitiert.
 487      *
 488      * @param string $name
 489      * @param mixed  $default
 490      *
 491      * @return string
 492      */
 493     public function getParam($name, $default = null)
 494     {
 495         if (isset($this->params[$name])) {
 496             return $this->params[$name];
 497         }
 498         return $default;
 499     }
 500 
 501     /**
 502      * Allgemeine Bootleneck-Methode um Elemente in das Formular einzufuegen.
 503      *
 504      * @param rex_form_element $element
 505      *
 506      * @return rex_form_element
 507      */
 508     protected function addElement(rex_form_element $element)
 509     {
 510         $this->elements[$this->fieldset][] = $element;
 511         return $element;
 512     }
 513 
 514     /**
 515      * Erstellt ein Input-Element anhand des Strings $inputType.
 516      *
 517      * @param string $inputType
 518      * @param string $name
 519      * @param mixed  $value
 520      * @param array  $attributes
 521      *
 522      * @return rex_form_element
 523      */
 524     public function createInput($inputType, $name, $value = null, array $attributes = [])
 525     {
 526         $tag = self::getInputTagName($inputType);
 527         $className = self::getInputClassName($inputType);
 528         $attributes = array_merge(self::getInputAttributes($inputType), $attributes);
 529         $attributes['internal::fieldClass'] = $className;
 530 
 531         $element = $this->createElement($tag, $name, $value, $attributes);
 532 
 533         return $element;
 534     }
 535 
 536     /**
 537      * Erstellt ein Input-Element anhand von $tag.
 538      *
 539      * @param string $tag
 540      * @param string $name
 541      * @param mixed  $value
 542      * @param array  $attributes
 543      *
 544      * @return rex_form_element
 545      */
 546     protected function createElement($tag, $name, $value, array $attributes = [])
 547     {
 548         $id = $this->getId($name);
 549 
 550         // Evtl postwerte wieder übernehmen (auch externe Werte überschreiben)
 551         $postValue = $this->elementPostValue($this->getFieldsetName(), $name);
 552         if ($postValue !== null) {
 553             $value = $postValue;
 554         }
 555 
 556         // Wert aus Quelle nehmen, falls keiner extern und keiner im POST angegeben
 557         if ($value === null) {
 558             $value = $this->getValue($name);
 559         }
 560 
 561         if (!isset($attributes['internal::useArraySyntax'])) {
 562             $attributes['internal::useArraySyntax'] = true;
 563         }
 564 
 565         // Eigentlichen Feldnamen nochmals speichern
 566         $fieldName = $name;
 567         if ($attributes['internal::useArraySyntax'] === true) {
 568             $name = $this->fieldset . '[' . $name . ']';
 569         } elseif ($attributes['internal::useArraySyntax'] === false) {
 570             $name = $this->fieldset . '_' . $name;
 571         }
 572         unset($attributes['internal::useArraySyntax']);
 573 
 574         $class = 'rex_form_element';
 575         if (isset($attributes['internal::fieldClass'])) {
 576             $class = $attributes['internal::fieldClass'];
 577             unset($attributes['internal::fieldClass']);
 578         }
 579 
 580         $separateEnding = false;
 581         if (isset($attributes['internal::fieldSeparateEnding'])) {
 582             $separateEnding = $attributes['internal::fieldSeparateEnding'];
 583             unset($attributes['internal::fieldSeparateEnding']);
 584         }
 585 
 586         $internal_attr = ['name' => $name];
 587         if (isset($attributes['internal::noNameAttribute'])) {
 588             $internal_attr = [];
 589             unset($attributes['internal::noNameAttribute']);
 590         }
 591 
 592         // 1. Array: Eigenschaften, die via Parameter Überschrieben werden können/dürfen
 593         // 2. Array: Eigenschaften, via Parameter
 594         // 3. Array: Eigenschaften, die hier fest definiert sind / nicht veränderbar via Parameter
 595         $attributes = array_merge(['id' => $id], $attributes, $internal_attr);
 596         $element = new $class($tag, $this, $attributes, $separateEnding);
 597         $element->setFieldName($fieldName);
 598         $element->setValue($value);
 599         return $element;
 600     }
 601 
 602     protected function getId($name)
 603     {
 604         return $this->fieldset . '_' . $name;
 605     }
 606 
 607     abstract protected function getValue($name);
 608 
 609     /**
 610      * Setzt die Url die bei der apply-action genutzt wird.
 611      */
 612     public function setApplyUrl($url)
 613     {
 614         if (is_array($url)) {
 615             $url = $this->getUrl($url, false);
 616         }
 617 
 618         $this->applyUrl = $url;
 619     }
 620 
 621     // --------- Static Methods
 622 
 623     /**
 624      * @param string $inputType
 625      *
 626      * @throws rex_exception
 627      *
 628      * @return rex_form_element
 629      */
 630     public static function getInputClassName($inputType)
 631     {
 632         // ----- EXTENSION POINT
 633         $className = rex_extension::registerPoint(new rex_extension_point('REX_FORM_INPUT_CLASS', '', ['inputType' => $inputType]));
 634 
 635         if ($className) {
 636             return $className;
 637         }
 638 
 639         switch ($inputType) {
 640             case 'control':
 641                 $className = 'rex_form_control_element';
 642                 break;
 643             case 'checkbox':
 644                 $className = 'rex_form_checkbox_element';
 645                 break;
 646             case 'radio':
 647                 $className = 'rex_form_radio_element';
 648                 break;
 649             case 'select':
 650                 $className = 'rex_form_select_element';
 651                 break;
 652             case 'media':
 653                 $className = 'rex_form_widget_media_element';
 654                 break;
 655             case 'medialist':
 656                 $className = 'rex_form_widget_medialist_element';
 657                 break;
 658             case 'link':
 659                 $className = 'rex_form_widget_linkmap_element';
 660                 break;
 661             case 'linklist':
 662                 $className = 'rex_form_widget_linklist_element';
 663                 break;
 664             case 'hidden':
 665             case 'readonly':
 666             case 'readonlytext':
 667             case 'text':
 668             case 'textarea':
 669                 $className = 'rex_form_element';
 670                 break;
 671             default:
 672                 throw new rex_exception("Unexpected inputType '" . $inputType . "'!");
 673         }
 674 
 675         return $className;
 676     }
 677 
 678     /**
 679      * @param string $inputType
 680      *
 681      * @return string
 682      */
 683     public static function getInputTagName($inputType)
 684     {
 685         // ----- EXTENSION POINT
 686         $inputTag = rex_extension::registerPoint(new rex_extension_point('REX_FORM_INPUT_TAG', '', ['inputType' => $inputType]));
 687 
 688         if ($inputTag) {
 689             return $inputTag;
 690         }
 691 
 692         switch ($inputType) {
 693             case 'checkbox':
 694             case 'hidden':
 695             case 'radio':
 696             case 'readonlytext':
 697             case 'text':
 698                 return 'input';
 699             case 'textarea':
 700                 return $inputType;
 701             case 'readonly':
 702                 return 'p';
 703             default:
 704                 $inputTag = '';
 705         }
 706         return $inputTag;
 707     }
 708 
 709     /**
 710      * @param string $inputType
 711      *
 712      * @return array
 713      */
 714     public static function getInputAttributes($inputType)
 715     {
 716         // ----- EXTENSION POINT
 717         $inputAttr = rex_extension::registerPoint(new rex_extension_point('REX_FORM_INPUT_ATTRIBUTES', [], ['inputType' => $inputType]));
 718 
 719         if ($inputAttr) {
 720             return $inputAttr;
 721         }
 722 
 723         switch ($inputType) {
 724             case 'checkbox':
 725             case 'hidden':
 726             case 'radio':
 727                 return [
 728                     'type' => $inputType,
 729                 ];
 730             case 'select':
 731                 return [
 732                     'class' => 'form-control',
 733                 ];
 734             case 'text':
 735                 return [
 736                     'type' => $inputType,
 737                     'class' => 'form-control',
 738                 ];
 739             case 'textarea':
 740                 return [
 741                     'internal::fieldSeparateEnding' => true,
 742                     'class' => 'form-control',
 743                     //'cols' => 50,
 744                     'rows' => 6,
 745                 ];
 746             case 'readonly':
 747                 return [
 748                     'internal::fieldSeparateEnding' => true,
 749                     'internal::noNameAttribute' => true,
 750                     'class' => 'form-control-static',
 751                 ];
 752             case 'readonlytext':
 753                 return [
 754                     'type' => 'text',
 755                     'readonly' => 'readonly',
 756                     'class' => 'form-control-static',
 757                 ];
 758             default:
 759                 $inputAttr = [];
 760         }
 761 
 762         return $inputAttr;
 763     }
 764 
 765     // --------- Form Methods
 766 
 767     /**
 768      * @param rex_form_element $element
 769      *
 770      * @return bool
 771      */
 772     protected function isHeaderElement(rex_form_element $element)
 773     {
 774         return $element->getTag() == 'input' && $element->getAttribute('type') == 'hidden';
 775     }
 776 
 777     /**
 778      * @param rex_form_element $element
 779      *
 780      * @return bool
 781      */
 782     protected function isFooterElement(rex_form_element $element)
 783     {
 784         return $this->isControlElement($element);
 785     }
 786 
 787     /**
 788      * @param rex_form_element $element
 789      *
 790      * @return bool
 791      */
 792     protected function isControlElement(rex_form_element $element)
 793     {
 794         return is_a($element, 'rex_form_control_element');
 795     }
 796 
 797     /**
 798      * @param rex_form_element $element
 799      *
 800      * @return bool
 801      */
 802     protected function isRawElement(rex_form_element $element)
 803     {
 804         return is_a($element, 'rex_form_raw_element');
 805     }
 806 
 807     /**
 808      * @return rex_form_element[]
 809      */
 810     protected function getHeaderElements()
 811     {
 812         $headerElements = [];
 813         foreach ($this->elements as $fieldsetName => $fieldsetElementsArray) {
 814             foreach ($fieldsetElementsArray as $element) {
 815                 if ($this->isHeaderElement($element)) {
 816                     $headerElements[] = $element;
 817                 }
 818             }
 819         }
 820         return $headerElements;
 821     }
 822 
 823     /**
 824      * @return rex_form_element[]
 825      */
 826     protected function getFooterElements()
 827     {
 828         $footerElements = [];
 829         foreach ($this->elements as $fieldsetName => $fieldsetElementsArray) {
 830             foreach ($fieldsetElementsArray as $element) {
 831                 if ($this->isFooterElement($element)) {
 832                     $footerElements[] = $element;
 833                 }
 834             }
 835         }
 836         return $footerElements;
 837     }
 838 
 839     /**
 840      * @return string
 841      */
 842     protected function getFieldsetName()
 843     {
 844         return $this->fieldset;
 845     }
 846 
 847     /**
 848      * @return array
 849      */
 850     protected function getFieldsets()
 851     {
 852         $fieldsets = [];
 853         foreach ($this->elements as $fieldsetName => $fieldsetElementsArray) {
 854             $fieldsets[] = $fieldsetName;
 855         }
 856         return $fieldsets;
 857     }
 858 
 859     /**
 860      * @return array
 861      */
 862     protected function getFieldsetElements()
 863     {
 864         $fieldsetElements = [];
 865         foreach ($this->elements as $fieldsetName => $fieldsetElementsArray) {
 866             $fieldsetElements[$fieldsetName] = [];
 867 
 868             foreach ($fieldsetElementsArray as $element) {
 869                 if ($this->isHeaderElement($element)) {
 870                     continue;
 871                 }
 872                 if ($this->isFooterElement($element)) {
 873                     continue;
 874                 }
 875 
 876                 $fieldsetElements[$fieldsetName][] = $element;
 877             }
 878         }
 879         return $fieldsetElements;
 880     }
 881 
 882     /**
 883      * @return array
 884      */
 885     protected function getSaveElements()
 886     {
 887         $fieldsetElements = [];
 888         foreach ($this->elements as $fieldsetName => $fieldsetElementsArray) {
 889             $fieldsetElements[$fieldsetName] = [];
 890 
 891             foreach ($fieldsetElementsArray as $key => $element) {
 892                 if ($this->isFooterElement($element)) {
 893                     continue;
 894                 }
 895                 if ($this->isRawElement($element)) {
 896                     continue;
 897                 }
 898 
 899                 $fieldsetElements[$fieldsetName][] = $element;
 900             }
 901         }
 902         return $fieldsetElements;
 903     }
 904 
 905     /**
 906      * @return rex_form_control_element
 907      */
 908     protected function getControlElement()
 909     {
 910         foreach ($this->elements as $fieldsetName => $fieldsetElementsArray) {
 911             foreach ($fieldsetElementsArray as $element) {
 912                 if ($this->isControlElement($element)) {
 913                     return $element;
 914                 }
 915             }
 916         }
 917         $noElement = null;
 918         return $noElement;
 919     }
 920 
 921     /**
 922      * @param string $fieldsetName
 923      * @param string $elementName
 924      *
 925      * @return rex_form_element
 926      */
 927     protected function getElement($fieldsetName, $elementName)
 928     {
 929         $normalizedName = rex_string::normalize($fieldsetName . '[' . $elementName . ']', '_', '[]');
 930         $result = $this->_getElement($fieldsetName, $normalizedName);
 931         return $result;
 932     }
 933 
 934     /**
 935      * @param string $fieldsetName
 936      * @param string $elementName
 937      *
 938      * @return rex_form_element
 939      */
 940     private function _getElement($fieldsetName, $elementName)
 941     {
 942         if (is_array($this->elements[$fieldsetName])) {
 943             for ($i = 0; $i < count($this->elements[$fieldsetName]); ++$i) {
 944                 if ($this->elements[$fieldsetName][$i]->getAttribute('name') == $elementName) {
 945                     return $this->elements[$fieldsetName][$i];
 946                 }
 947             }
 948         }
 949         $result = null;
 950         return $result;
 951     }
 952 
 953     /**
 954      * @return string
 955      */
 956     public function getName()
 957     {
 958         return $this->name;
 959     }
 960 
 961     public function setWarning($warning)
 962     {
 963         $this->warning = $warning;
 964     }
 965 
 966     /**
 967      * @return string
 968      */
 969     public function getWarning()
 970     {
 971         $warning = rex_request($this->getName() . '_warning', 'string');
 972         if ($this->warning != '') {
 973             $warning .= "\n" . $this->warning;
 974         }
 975         return $warning;
 976     }
 977 
 978     public function setMessage($message)
 979     {
 980         $this->message = $message;
 981     }
 982 
 983     /**
 984      * @return string
 985      */
 986     public function getMessage()
 987     {
 988         $message = rex_request($this->getName() . '_msg', 'string');
 989         if ($this->message != '') {
 990             $message .= "\n" . $this->message;
 991         }
 992         return $message;
 993     }
 994 
 995     /**
 996      * Callbackfunktion, damit in subklassen der Value noch beeinflusst werden kann
 997      * wenn das Feld mit Datenbankwerten angezeigt wird.
 998      */
 999     protected function preView($fieldsetName, $fieldName, $fieldValue)
1000     {
1001         return $fieldValue;
1002     }
1003 
1004     /**
1005      * @param string $fieldsetName
1006      *
1007      * @return array
1008      */
1009     public function fieldsetPostValues($fieldsetName)
1010     {
1011         // Name normalisieren, da der gepostete Name auch zuvor normalisiert wurde
1012         $normalizedFieldsetName = rex_string::normalize($fieldsetName, '_', '[]');
1013 
1014         return rex_post($normalizedFieldsetName, 'array');
1015     }
1016 
1017     /**
1018      * @param string $fieldsetName
1019      * @param string $fieldName
1020      * @param mixed  $default
1021      *
1022      * @return string
1023      */
1024     public function elementPostValue($fieldsetName, $fieldName, $default = null)
1025     {
1026         $fields = $this->fieldsetPostValues($fieldsetName);
1027 
1028         // name attributes are normalized
1029         $normalizedFieldName = rex_string::normalize($fieldName, '_', '[]');
1030 
1031         if (isset($fields[$normalizedFieldName])) {
1032             return $fields[$normalizedFieldName];
1033         }
1034 
1035         return $default;
1036     }
1037 
1038     /**
1039      * Validiert die Eingaben.
1040      * Gibt true zurück wenn alles ok war, false bei einem allgemeinen Fehler oder
1041      * einen String mit einer Fehlermeldung.
1042      *
1043      * Eingaben sind via
1044      *   $el    = $this->getElement($fieldSetName, $fieldName);
1045      *   $val   = $el->getValue();
1046      * erreichbar.
1047      *
1048      * @return bool
1049      */
1050     protected function validate()
1051     {
1052         $messages = [];
1053 
1054         if (!$this->csrfToken->isValid()) {
1055             $messages[] = rex_i18n::msg('csrf_token_invalid');
1056         }
1057 
1058         foreach ($this->getSaveElements() as $fieldsetName => $fieldsetElements) {
1059             foreach ($fieldsetElements as $element) {
1060                 /** @var rex_form_element $element */
1061                 // read-only-fields
1062                 if (strpos($element->getAttribute('class'), 'form-control-static') !== false) {
1063                     continue;
1064                 }
1065 
1066                 $validator = $element->getValidator();
1067                 if (!$validator->isValid($element->getSaveValue())) {
1068                     $messages[] = $validator->getMessage();
1069                 }
1070             }
1071         }
1072         return empty($messages) ? true : implode('<br />', $messages);
1073     }
1074 
1075     /**
1076      * Übernimmt die POST-Werte in die FormElemente.
1077      */
1078     protected function processPostValues()
1079     {
1080         $saveElements = $this->getSaveElements();
1081         foreach ($saveElements as $fieldsetName => $fieldsetElements) {
1082             foreach ($fieldsetElements as $key => $element) {
1083                 // read-only-fields nicht speichern
1084                 if (strpos($element->getAttribute('class'), 'form-control-static') !== false) {
1085                     continue;
1086                 }
1087 
1088                 $fieldName = $element->getFieldName();
1089                 $fieldValue = $this->elementPostValue($fieldsetName, $fieldName);
1090 
1091                 $element->setValue($fieldValue);
1092             }
1093         }
1094     }
1095 
1096     /**
1097      * Saves the form.
1098      *
1099      * @return bool|string|int `true` on success, an error message or an error code otherwise
1100      */
1101     abstract protected function save();
1102 
1103     /**
1104      * @return bool
1105      */
1106     protected function delete()
1107     {
1108         throw new BadMethodCallException('delete() is not implemented.');
1109     }
1110 
1111     protected function redirect($listMessage = '', $listWarning = '', array $params = [])
1112     {
1113         if ($listMessage != '') {
1114             $listName = rex_request('list', 'string');
1115             $params[$listName . '_msg'] = $listMessage;
1116         }
1117 
1118         if ($listWarning != '') {
1119             $listName = rex_request('list', 'string');
1120             $params[$listName . '_warning'] = $listWarning;
1121         }
1122 
1123         $paramString = '';
1124         foreach ($params as $name => $value) {
1125             $paramString .= '&' . $name . '=' . $value;
1126         }
1127 
1128         if ($this->debug) {
1129             echo 'redirect to: ' . $this->applyUrl . $paramString;
1130             exit();
1131         }
1132 
1133         header('Location: ' . $this->applyUrl . $paramString);
1134         exit();
1135     }
1136 
1137     /**
1138      * @return string
1139      */
1140     public function get()
1141     {
1142         $this->init();
1143 
1144         rex_extension::registerPoint(new rex_extension_point('REX_FORM_GET', $this, [], true));
1145 
1146         if (!$this->applyUrl) {
1147             $this->setApplyUrl($this->getUrl(['func' => ''], false));
1148         }
1149 
1150         if (($controlElement = $this->getControlElement()) !== null) {
1151             if ($controlElement->saved()) {
1152                 $this->processPostValues();
1153 
1154                 // speichern und umleiten
1155                 // Nachricht in der Liste anzeigen
1156                 if (($result = $this->validate()) === true && ($result = $this->save()) === true) {
1157                     $this->redirect(rex_i18n::msg('form_saved'));
1158                 } elseif (is_int($result) && isset($this->errorMessages[$result])) {
1159                     $this->setWarning($this->errorMessages[$result]);
1160                 } elseif (is_string($result) && $result != '') {
1161                     $this->setWarning($result);
1162                 } else {
1163                     $this->setWarning(rex_i18n::msg('form_save_error'));
1164                 }
1165             } elseif ($controlElement->applied()) {
1166                 $this->processPostValues();
1167 
1168                 // speichern und wiederanzeigen
1169                 // Nachricht im Formular anzeigen
1170                 if (($result = $this->validate()) === true && ($result = $this->save()) === true) {
1171                     $this->setMessage(rex_i18n::msg('form_applied'));
1172                 } elseif (is_int($result) && isset($this->errorMessages[$result])) {
1173                     $this->setWarning($this->errorMessages[$result]);
1174                 } elseif (is_string($result) && $result != '') {
1175                     $this->setWarning($result);
1176                 } else {
1177                     $this->setWarning(rex_i18n::msg('form_save_error'));
1178                 }
1179             } elseif ($controlElement->deleted()) {
1180                 // speichern und wiederanzeigen
1181                 // Nachricht in der Liste anzeigen
1182                 if (($result = $this->delete()) === true) {
1183                     $this->redirect(rex_i18n::msg('form_deleted'));
1184                 } elseif (is_string($result) && $result != '') {
1185                     $this->setWarning($result);
1186                 } else {
1187                     $this->setWarning(rex_i18n::msg('form_delete_error'));
1188                 }
1189             } elseif ($controlElement->resetted()) {
1190                 // verwerfen und wiederanzeigen
1191                 // Nachricht im Formular anzeigen
1192                 $this->setMessage(rex_i18n::msg('form_resetted'));
1193             } elseif ($controlElement->aborted()) {
1194                 // verwerfen und umleiten
1195                 // Nachricht in der Liste anzeigen
1196                 $this->redirect(rex_i18n::msg('form_resetted'));
1197             }
1198         }
1199 
1200         $actionParams = [];
1201         if ('get' == strtolower($this->method)) {
1202             // Parameter dem Formular hinzufügen
1203             foreach ($this->getParams() as $name => $value) {
1204                 $this->addHiddenField($name, $value, ['internal::useArraySyntax' => 'none']);
1205             }
1206         } else {
1207             $actionParams = $this->getParams();
1208         }
1209 
1210         $s = "\n";
1211 
1212         $warning = $this->getWarning();
1213         $message = $this->getMessage();
1214         if ($warning != '') {
1215             $s .= '  ' . rex_view::error($warning) . "\n";
1216         } elseif ($message != '') {
1217             $s .= '  ' . rex_view::success($message) . "\n";
1218         }
1219 
1220         $i = 0;
1221         $addHeaders = true;
1222         $fieldsets = $this->getFieldsetElements();
1223         $last = count($fieldsets);
1224 
1225         $id = '';
1226         if ($this->formId) {
1227             $id = ' id="'.rex_escape($this->formId).'"';
1228         }
1229 
1230         $s .= '<form' . $id . ' action="' . rex_url::backendController($actionParams) . '" method="' . $this->method . '">' . "\n";
1231         foreach ($fieldsets as $fieldsetName => $fieldsetElements) {
1232             $s .= '<fieldset>' . "\n";
1233 
1234             if ($fieldsetName != '' && $fieldsetName != $this->name) {
1235                 $s .= '<legend>' . rex_escape($fieldsetName) . '</legend>' . "\n";
1236             }
1237 
1238             // Die HeaderElemente nur im 1. Fieldset ganz am Anfang einfügen
1239             if ($i == 0 && $addHeaders) {
1240                 foreach ($this->getHeaderElements() as $element) {
1241                     // Callback
1242                     $element->setValue($this->preView($fieldsetName, $element->getFieldName(), $element->getValue()));
1243                     // HeaderElemente immer ohne <p>
1244                     $s .= $element->formatElement();
1245                 }
1246                 $addHeaders = false;
1247             }
1248 
1249             foreach ($fieldsetElements as $element) {
1250                 // Callback
1251                 $element->setValue($this->preView($fieldsetName, $element->getFieldName(), $element->getValue()));
1252                 $s .= $element->get();
1253             }
1254 
1255             // Die FooterElemente nur innerhalb des letzten Fieldsets
1256             if (($i + 1) == $last) {
1257                 foreach ($this->getFooterElements() as $element) {
1258                     // Callback
1259                     $element->setValue($this->preView($fieldsetName, $element->getFieldName(), $element->getValue()));
1260                     $s .= $element->get();
1261                 }
1262             }
1263 
1264             $s .= '</fieldset>' . "\n";
1265 
1266             ++$i;
1267         }
1268 
1269         $s .= $this->csrfToken->getHiddenField() . "\n";
1270         $s .= '</form>' . "\n";
1271 
1272         return $s;
1273     }
1274 
1275     public function show()
1276     {
1277         echo $this->get();
1278     }
1279 }
1280