1 <?php
2 /**
3 * @package FrameworkOnFramework
4 * @subpackage form
5 * @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE.txt
7 */
8 // Protect from unauthorized access
9 defined('FOF_INCLUDED') or die;
10
11 if (version_compare(JVERSION, '2.5.0', 'lt'))
12 {
13 jimport('joomla.form.form');
14 jimport('joomla.form.formfield');
15 jimport('joomla.form.formrule');
16 }
17
18 /**
19 * FOFForm is an extension to JForm which support not only edit views but also
20 * browse (record list) and read (single record display) views based on XML
21 * forms.
22 *
23 * @package FrameworkOnFramework
24 * @since 2.0
25 */
26 class FOFForm extends JForm
27 {
28 /**
29 * The model attached to this view
30 *
31 * @var FOFModel
32 */
33 protected $model;
34
35 /**
36 * The view used to render this form
37 *
38 * @var FOFView
39 */
40 protected $view;
41
42 /**
43 * Method to get an instance of a form.
44 *
45 * @param string $name The name of the form.
46 * @param string $data The name of an XML file or string to load as the form definition.
47 * @param array $options An array of form options.
48 * @param bool $replace Flag to toggle whether form fields should be replaced if a field
49 * already exists with the same group/name.
50 * @param bool|string $xpath An optional xpath to search for the fields.
51 *
52 * @return object FOFForm instance.
53 *
54 * @since 2.0
55 * @throws InvalidArgumentException if no data provided.
56 * @throws RuntimeException if the form could not be loaded.
57 */
58 public static function getInstance($name, $data = null, $options = array(), $replace = true, $xpath = false)
59 {
60 // Reference to array with form instances
61 $forms = &self::$forms;
62
63 // Only instantiate the form if it does not already exist.
64 if (!isset($forms[$name]))
65 {
66 $data = trim($data);
67
68 if (empty($data))
69 {
70 throw new InvalidArgumentException(sprintf('FOFForm::getInstance(name, *%s*)', gettype($data)));
71 }
72
73 // Instantiate the form.
74 $forms[$name] = new FOFForm($name, $options);
75
76 // Load the data.
77 if (substr(trim($data), 0, 1) == '<')
78 {
79 if ($forms[$name]->load($data, $replace, $xpath) == false)
80 {
81 throw new RuntimeException('FOFForm::getInstance could not load form');
82 }
83 }
84 else
85 {
86 if ($forms[$name]->loadFile($data, $replace, $xpath) == false)
87 {
88 throw new RuntimeException('FOFForm::getInstance could not load file ' . $data . '.xml');
89 }
90 }
91 }
92
93 return $forms[$name];
94 }
95
96 /**
97 * Returns the value of an attribute of the form itself
98 *
99 * @param string $attribute The name of the attribute
100 * @param mixed $default Optional default value to return
101 *
102 * @return mixed
103 *
104 * @since 2.0
105 */
106 public function getAttribute($attribute, $default = null)
107 {
108 $value = $this->xml->attributes()->$attribute;
109
110 if (is_null($value))
111 {
112 return $default;
113 }
114 else
115 {
116 return (string) $value;
117 }
118 }
119
120 /**
121 * Loads the CSS files defined in the form, based on its cssfiles attribute
122 *
123 * @return void
124 *
125 * @since 2.0
126 */
127 public function loadCSSFiles()
128 {
129 // Support for CSS files
130 $cssfiles = $this->getAttribute('cssfiles');
131
132 if (!empty($cssfiles))
133 {
134 $cssfiles = explode(',', $cssfiles);
135
136 foreach ($cssfiles as $cssfile)
137 {
138 FOFTemplateUtils::addCSS(trim($cssfile));
139 }
140 }
141
142 // Support for LESS files
143 $lessfiles = $this->getAttribute('lessfiles');
144
145 if (!empty($lessfiles))
146 {
147 $lessfiles = explode(',', $lessfiles);
148
149 foreach ($lessfiles as $def)
150 {
151 $parts = explode('||', $def, 2);
152 $lessfile = $parts[0];
153 $alt = (count($parts) > 1) ? trim($parts[1]) : null;
154 FOFTemplateUtils::addLESS(trim($lessfile), $alt);
155 }
156 }
157 }
158
159 /**
160 * Loads the Javascript files defined in the form, based on its jsfiles attribute
161 *
162 * @return void
163 *
164 * @since 2.0
165 */
166 public function loadJSFiles()
167 {
168 $jsfiles = $this->getAttribute('jsfiles');
169
170 if (empty($jsfiles))
171 {
172 return;
173 }
174
175 $jsfiles = explode(',', $jsfiles);
176
177 foreach ($jsfiles as $jsfile)
178 {
179 FOFTemplateUtils::addJS(trim($jsfile));
180 }
181 }
182
183 /**
184 * Returns a reference to the protected $data object, allowing direct
185 * access to and manipulation of the form's data.
186 *
187 * @return JRegistry The form's data registry
188 *
189 * @since 2.0
190 */
191 public function getData()
192 {
193 return $this->data;
194 }
195
196 /**
197 * Attaches a FOFModel to this form
198 *
199 * @param FOFModel &$model The model to attach to the form
200 *
201 * @return void
202 */
203 public function setModel(FOFModel &$model)
204 {
205 $this->model = $model;
206 }
207
208 /**
209 * Returns the FOFModel attached to this form
210 *
211 * @return FOFModel
212 */
213 public function &getModel()
214 {
215 return $this->model;
216 }
217
218 /**
219 * Attaches a FOFView to this form
220 *
221 * @param FOFView &$view The view to attach to the form
222 *
223 * @return void
224 */
225 public function setView(FOFView &$view)
226 {
227 $this->view = $view;
228 }
229
230 /**
231 * Returns the FOFView attached to this form
232 *
233 * @return FOFView
234 */
235 public function &getView()
236 {
237 return $this->view;
238 }
239
240 /**
241 * Method to get an array of FOFFormHeader objects in the headerset.
242 *
243 * @return array The array of FOFFormHeader objects in the headerset.
244 *
245 * @since 2.0
246 */
247 public function getHeaderset()
248 {
249 $fields = array();
250
251 $elements = $this->findHeadersByGroup();
252
253 // If no field elements were found return empty.
254
255 if (empty($elements))
256 {
257 return $fields;
258 }
259
260 // Build the result array from the found field elements.
261
262 foreach ($elements as $element)
263 {
264 // Get the field groups for the element.
265 $attrs = $element->xpath('ancestor::headers[@name]/@name');
266 $groups = array_map('strval', $attrs ? $attrs : array());
267 $group = implode('.', $groups);
268
269 // If the field is successfully loaded add it to the result array.
270 if ($field = $this->loadHeader($element, $group))
271 {
272 $fields[$field->id] = $field;
273 }
274 }
275
276 return $fields;
277 }
278
279 /**
280 * Method to get an array of <header /> elements from the form XML document which are
281 * in a control group by name.
282 *
283 * @param mixed $group The optional dot-separated form group path on which to find the fields.
284 * Null will return all fields. False will return fields not in a group.
285 * @param boolean $nested True to also include fields in nested groups that are inside of the
286 * group for which to find fields.
287 *
288 * @return mixed Boolean false on error or array of SimpleXMLElement objects.
289 *
290 * @since 2.0
291 */
292 protected function &findHeadersByGroup($group = null, $nested = false)
293 {
294 $false = false;
295 $fields = array();
296
297 // Make sure there is a valid JForm XML document.
298 if (!($this->xml instanceof SimpleXMLElement))
299 {
300 return $false;
301 }
302
303 // Get only fields in a specific group?
304 if ($group)
305 {
306 // Get the fields elements for a given group.
307 $elements = &$this->findHeader($group);
308
309 // Get all of the field elements for the fields elements.
310 foreach ($elements as $element)
311 {
312 // If there are field elements add them to the return result.
313 if ($tmp = $element->xpath('descendant::header'))
314 {
315 // If we also want fields in nested groups then just merge the arrays.
316 if ($nested)
317 {
318 $fields = array_merge($fields, $tmp);
319 }
320
321 // If we want to exclude nested groups then we need to check each field.
322 else
323 {
324 $groupNames = explode('.', $group);
325
326 foreach ($tmp as $field)
327 {
328 // Get the names of the groups that the field is in.
329 $attrs = $field->xpath('ancestor::headers[@name]/@name');
330 $names = array_map('strval', $attrs ? $attrs : array());
331
332 // If the field is in the specific group then add it to the return list.
333 if ($names == (array) $groupNames)
334 {
335 $fields = array_merge($fields, array($field));
336 }
337 }
338 }
339 }
340 }
341 }
342 elseif ($group === false)
343 {
344 // Get only field elements not in a group.
345 $fields = $this->xml->xpath('descendant::headers[not(@name)]/header | descendant::headers[not(@name)]/headerset/header ');
346 }
347 else
348 {
349 // Get an array of all the <header /> elements.
350 $fields = $this->xml->xpath('//header');
351 }
352
353 return $fields;
354 }
355
356 /**
357 * Method to get a header field represented as a FOFFormHeader object.
358 *
359 * @param string $name The name of the header field.
360 * @param string $group The optional dot-separated form group path on which to find the field.
361 * @param mixed $value The optional value to use as the default for the field.
362 *
363 * @return mixed The FOFFormHeader object for the field or boolean false on error.
364 *
365 * @since 2.0
366 */
367 public function getHeader($name, $group = null, $value = null)
368 {
369 // Make sure there is a valid FOFForm XML document.
370 if (!($this->xml instanceof SimpleXMLElement))
371 {
372 return false;
373 }
374
375 // Attempt to find the field by name and group.
376 $element = $this->findHeader($name, $group);
377
378 // If the field element was not found return false.
379 if (!$element)
380 {
381 return false;
382 }
383
384 return $this->loadHeader($element, $group, $value);
385 }
386
387 /**
388 * Method to get a header field represented as an XML element object.
389 *
390 * @param string $name The name of the form field.
391 * @param string $group The optional dot-separated form group path on which to find the field.
392 *
393 * @return mixed The XML element object for the field or boolean false on error.
394 *
395 * @since 2.0
396 */
397 protected function findHeader($name, $group = null)
398 {
399 $element = false;
400 $fields = array();
401
402 // Make sure there is a valid JForm XML document.
403 if (!($this->xml instanceof SimpleXMLElement))
404 {
405 return false;
406 }
407
408 // Let's get the appropriate field element based on the method arguments.
409 if ($group)
410 {
411 // Get the fields elements for a given group.
412 $elements = &$this->findGroup($group);
413
414 // Get all of the field elements with the correct name for the fields elements.
415 foreach ($elements as $element)
416 {
417 // If there are matching field elements add them to the fields array.
418 if ($tmp = $element->xpath('descendant::header[@name="' . $name . '"]'))
419 {
420 $fields = array_merge($fields, $tmp);
421 }
422 }
423
424 // Make sure something was found.
425 if (!$fields)
426 {
427 return false;
428 }
429
430 // Use the first correct match in the given group.
431 $groupNames = explode('.', $group);
432
433 foreach ($fields as &$field)
434 {
435 // Get the group names as strings for ancestor fields elements.
436 $attrs = $field->xpath('ancestor::headerfields[@name]/@name');
437 $names = array_map('strval', $attrs ? $attrs : array());
438
439 // If the field is in the exact group use it and break out of the loop.
440 if ($names == (array) $groupNames)
441 {
442 $element = &$field;
443 break;
444 }
445 }
446 }
447 else
448 {
449 // Get an array of fields with the correct name.
450 $fields = $this->xml->xpath('//header[@name="' . $name . '"]');
451
452 // Make sure something was found.
453 if (!$fields)
454 {
455 return false;
456 }
457
458 // Search through the fields for the right one.
459 foreach ($fields as &$field)
460 {
461 // If we find an ancestor fields element with a group name then it isn't what we want.
462 if ($field->xpath('ancestor::headerfields[@name]'))
463 {
464 continue;
465 }
466
467 // Found it!
468 else
469 {
470 $element = &$field;
471 break;
472 }
473 }
474 }
475
476 return $element;
477 }
478
479 /**
480 * Method to load, setup and return a FOFFormHeader object based on field data.
481 *
482 * @param string $element The XML element object representation of the form field.
483 * @param string $group The optional dot-separated form group path on which to find the field.
484 * @param mixed $value The optional value to use as the default for the field.
485 *
486 * @return mixed The FOFFormHeader object for the field or boolean false on error.
487 *
488 * @since 2.0
489 */
490 protected function loadHeader($element, $group = null, $value = null)
491 {
492 // Make sure there is a valid SimpleXMLElement.
493 if (!($element instanceof SimpleXMLElement))
494 {
495 return false;
496 }
497
498 // Get the field type.
499 $type = $element['type'] ? (string) $element['type'] : 'field';
500
501 // Load the JFormField object for the field.
502 $field = $this->loadHeaderType($type);
503
504 // If the object could not be loaded, get a text field object.
505 if ($field === false)
506 {
507 $field = $this->loadHeaderType('field');
508 }
509
510 // Setup the FOFFormHeader object.
511 $field->setForm($this);
512
513 if ($field->setup($element, $value, $group))
514 {
515 return $field;
516 }
517 else
518 {
519 return false;
520 }
521 }
522
523 /**
524 * Method to remove a header from the form definition.
525 *
526 * @param string $name The name of the form field for which remove.
527 * @param string $group The optional dot-separated form group path on which to find the field.
528 *
529 * @return boolean True on success, false otherwise.
530 *
531 * @throws UnexpectedValueException
532 */
533 public function removeHeader($name, $group = null)
534 {
535 // Make sure there is a valid JForm XML document.
536 if (!($this->xml instanceof SimpleXMLElement))
537 {
538 throw new UnexpectedValueException(sprintf('%s::getFieldAttribute `xml` is not an instance of SimpleXMLElement', get_class($this)));
539 }
540
541 // Find the form field element from the definition.
542 $element = $this->findHeader($name, $group);
543
544 // If the element exists remove it from the form definition.
545 if ($element instanceof SimpleXMLElement)
546 {
547 $dom = dom_import_simplexml($element);
548 $dom->parentNode->removeChild($dom);
549
550 return true;
551 }
552
553 return false;
554 }
555
556 /**
557 * Proxy for {@link FOFFormHelper::loadFieldType()}.
558 *
559 * @param string $type The field type.
560 * @param boolean $new Flag to toggle whether we should get a new instance of the object.
561 *
562 * @return mixed FOFFormField object on success, false otherwise.
563 *
564 * @since 2.0
565 */
566 protected function loadFieldType($type, $new = true)
567 {
568 return FOFFormHelper::loadFieldType($type, $new);
569 }
570
571 /**
572 * Proxy for {@link FOFFormHelper::loadHeaderType()}.
573 *
574 * @param string $type The field type.
575 * @param boolean $new Flag to toggle whether we should get a new instance of the object.
576 *
577 * @return mixed FOFFormHeader object on success, false otherwise.
578 *
579 * @since 2.0
580 */
581 protected function loadHeaderType($type, $new = true)
582 {
583 return FOFFormHelper::loadHeaderType($type, $new);
584 }
585
586 /**
587 * Proxy for {@link FOFFormHelper::loadRuleType()}.
588 *
589 * @param string $type The rule type.
590 * @param boolean $new Flag to toggle whether we should get a new instance of the object.
591 *
592 * @return mixed JFormRule object on success, false otherwise.
593 *
594 * @see FOFFormHelper::loadRuleType()
595 * @since 2.0
596 */
597 protected function loadRuleType($type, $new = true)
598 {
599 return FOFFormHelper::loadRuleType($type, $new);
600 }
601
602 /**
603 * Proxy for {@link FOFFormHelper::addFieldPath()}.
604 *
605 * @param mixed $new A path or array of paths to add.
606 *
607 * @return array The list of paths that have been added.
608 *
609 * @since 2.0
610 */
611 public static function addFieldPath($new = null)
612 {
613 return FOFFormHelper::addFieldPath($new);
614 }
615
616 /**
617 * Proxy for {@link FOFFormHelper::addHeaderPath()}.
618 *
619 * @param mixed $new A path or array of paths to add.
620 *
621 * @return array The list of paths that have been added.
622 *
623 * @since 2.0
624 */
625 public static function addHeaderPath($new = null)
626 {
627 return FOFFormHelper::addHeaderPath($new);
628 }
629
630 /**
631 * Proxy for FOFFormHelper::addFormPath().
632 *
633 * @param mixed $new A path or array of paths to add.
634 *
635 * @return array The list of paths that have been added.
636 *
637 * @see FOFFormHelper::addFormPath()
638 * @since 2.0
639 */
640 public static function addFormPath($new = null)
641 {
642 return FOFFormHelper::addFormPath($new);
643 }
644
645 /**
646 * Proxy for FOFFormHelper::addRulePath().
647 *
648 * @param mixed $new A path or array of paths to add.
649 *
650 * @return array The list of paths that have been added.
651 *
652 * @see FOFFormHelper::addRulePath()
653 * @since 2.0
654 */
655 public static function addRulePath($new = null)
656 {
657 return FOFFormHelper::addRulePath($new);
658 }
659 }
660