1 <?php
  2 /**
  3  * @package     Joomla.Platform
  4  * @subpackage  Form
  5  *
  6  * @copyright   Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
  7  * @license     GNU General Public License version 2 or later; see LICENSE
  8  */
  9 
 10 defined('JPATH_PLATFORM') or die;
 11 
 12 use Joomla\String\StringHelper;
 13 
 14 jimport('joomla.filesystem.path');
 15 
 16 /**
 17  * JForm's helper class.
 18  * Provides a storage for filesystem's paths where JForm's entities reside and methods for creating those entities.
 19  * Also stores objects with entities' prototypes for further reusing.
 20  *
 21  * @since  11.1
 22  */
 23 class JFormHelper
 24 {
 25     /**
 26      * Array with paths where entities(field, rule, form) can be found.
 27      *
 28      * Array's structure:
 29      *
 30      * paths:
 31      * {ENTITY_NAME}:
 32      * - /path/1
 33      * - /path/2
 34      *
 35      * @var    array
 36      * @since  11.1
 37      */
 38     protected static $paths;
 39 
 40     /**
 41      * Static array of JForm's entity objects for re-use.
 42      * Prototypes for all fields and rules are here.
 43      *
 44      * Array's structure:
 45      * entities:
 46      * {ENTITY_NAME}:
 47      * {KEY}: {OBJECT}
 48      *
 49      * @var    array
 50      * @since  11.1
 51      */
 52     protected static $entities = array();
 53 
 54     /**
 55      * Method to load a form field object given a type.
 56      *
 57      * @param   string   $type  The field type.
 58      * @param   boolean  $new   Flag to toggle whether we should get a new instance of the object.
 59      *
 60      * @return  JFormField|boolean  JFormField object on success, false otherwise.
 61      *
 62      * @since   11.1
 63      */
 64     public static function loadFieldType($type, $new = true)
 65     {
 66         return self::loadType('field', $type, $new);
 67     }
 68 
 69     /**
 70      * Method to load a form rule object given a type.
 71      *
 72      * @param   string   $type  The rule type.
 73      * @param   boolean  $new   Flag to toggle whether we should get a new instance of the object.
 74      *
 75      * @return  JFormRule|boolean  JFormRule object on success, false otherwise.
 76      *
 77      * @since   11.1
 78      */
 79     public static function loadRuleType($type, $new = true)
 80     {
 81         return self::loadType('rule', $type, $new);
 82     }
 83 
 84     /**
 85      * Method to load a form entity object given a type.
 86      * Each type is loaded only once and then used as a prototype for other objects of same type.
 87      * Please, use this method only with those entities which support types (forms don't support them).
 88      *
 89      * @param   string   $entity  The entity.
 90      * @param   string   $type    The entity type.
 91      * @param   boolean  $new     Flag to toggle whether we should get a new instance of the object.
 92      *
 93      * @return  mixed  Entity object on success, false otherwise.
 94      *
 95      * @since   11.1
 96      */
 97     protected static function loadType($entity, $type, $new = true)
 98     {
 99         // Reference to an array with current entity's type instances
100         $types = &self::$entities[$entity];
101 
102         $key = md5($type);
103 
104         // Return an entity object if it already exists and we don't need a new one.
105         if (isset($types[$key]) && $new === false)
106         {
107             return $types[$key];
108         }
109 
110         $class = self::loadClass($entity, $type);
111 
112         if ($class === false)
113         {
114             return false;
115         }
116 
117         // Instantiate a new type object.
118         $types[$key] = new $class;
119 
120         return $types[$key];
121     }
122 
123     /**
124      * Attempt to import the JFormField class file if it isn't already imported.
125      * You can use this method outside of JForm for loading a field for inheritance or composition.
126      *
127      * @param   string  $type  Type of a field whose class should be loaded.
128      *
129      * @return  string|boolean  Class name on success or false otherwise.
130      *
131      * @since   11.1
132      */
133     public static function loadFieldClass($type)
134     {
135         return self::loadClass('field', $type);
136     }
137 
138     /**
139      * Attempt to import the JFormRule class file if it isn't already imported.
140      * You can use this method outside of JForm for loading a rule for inheritance or composition.
141      *
142      * @param   string  $type  Type of a rule whose class should be loaded.
143      *
144      * @return  string|boolean  Class name on success or false otherwise.
145      *
146      * @since   11.1
147      */
148     public static function loadRuleClass($type)
149     {
150         return self::loadClass('rule', $type);
151     }
152 
153     /**
154      * Load a class for one of the form's entities of a particular type.
155      * Currently, it makes sense to use this method for the "field" and "rule" entities
156      * (but you can support more entities in your subclass).
157      *
158      * @param   string  $entity  One of the form entities (field or rule).
159      * @param   string  $type    Type of an entity.
160      *
161      * @return  string|boolean  Class name on success or false otherwise.
162      *
163      * @since   11.1
164      */
165     protected static function loadClass($entity, $type)
166     {
167         $prefix = 'J';
168 
169         if (strpos($type, '.'))
170         {
171             list($prefix, $type) = explode('.', $type);
172         }
173 
174         $class = StringHelper::ucfirst($prefix, '_') . 'Form' . StringHelper::ucfirst($entity, '_') . StringHelper::ucfirst($type, '_');
175 
176         if (class_exists($class))
177         {
178             return $class;
179         }
180 
181         // Get the field search path array.
182         $paths = self::addPath($entity);
183 
184         // If the type is complex, add the base type to the paths.
185         if ($pos = strpos($type, '_'))
186         {
187             // Add the complex type prefix to the paths.
188             for ($i = 0, $n = count($paths); $i < $n; $i++)
189             {
190                 // Derive the new path.
191                 $path = $paths[$i] . '/' . strtolower(substr($type, 0, $pos));
192 
193                 // If the path does not exist, add it.
194                 if (!in_array($path, $paths))
195                 {
196                     $paths[] = $path;
197                 }
198             }
199             // Break off the end of the complex type.
200             $type = substr($type, $pos + 1);
201         }
202 
203         // Try to find the class file.
204         $type = strtolower($type) . '.php';
205 
206         foreach ($paths as $path)
207         {
208             $file = JPath::find($path, $type);
209 
210             if (!$file)
211             {
212                 continue;
213             }
214 
215             require_once $file;
216 
217             if (class_exists($class))
218             {
219                 break;
220             }
221         }
222 
223         // Check for all if the class exists.
224         return class_exists($class) ? $class : false;
225     }
226 
227     /**
228      * Method to add a path to the list of field include paths.
229      *
230      * @param   mixed  $new  A path or array of paths to add.
231      *
232      * @return  array  The list of paths that have been added.
233      *
234      * @since   11.1
235      */
236     public static function addFieldPath($new = null)
237     {
238         return self::addPath('field', $new);
239     }
240 
241     /**
242      * Method to add a path to the list of form include paths.
243      *
244      * @param   mixed  $new  A path or array of paths to add.
245      *
246      * @return  array  The list of paths that have been added.
247      *
248      * @since   11.1
249      */
250     public static function addFormPath($new = null)
251     {
252         return self::addPath('form', $new);
253     }
254 
255     /**
256      * Method to add a path to the list of rule include paths.
257      *
258      * @param   mixed  $new  A path or array of paths to add.
259      *
260      * @return  array  The list of paths that have been added.
261      *
262      * @since   11.1
263      */
264     public static function addRulePath($new = null)
265     {
266         return self::addPath('rule', $new);
267     }
268 
269     /**
270      * Method to add a path to the list of include paths for one of the form's entities.
271      * Currently supported entities: field, rule and form. You are free to support your own in a subclass.
272      *
273      * @param   string  $entity  Form's entity name for which paths will be added.
274      * @param   mixed   $new     A path or array of paths to add.
275      *
276      * @return  array  The list of paths that have been added.
277      *
278      * @since   11.1
279      */
280     protected static function addPath($entity, $new = null)
281     {
282         // Reference to an array with paths for current entity
283         $paths = &self::$paths[$entity];
284 
285         // Add the default entity's search path if not set.
286         if (empty($paths))
287         {
288             // While we support limited number of entities (form, field and rule)
289             // we can do this simple pluralisation:
290             $entity_plural = $entity . 's';
291 
292             /*
293              * But when someday we would want to support more entities, then we should consider adding
294              * an inflector class to "libraries/joomla/utilities" and use it here (or somebody can use a real inflector in his subclass).
295              * See also: pluralization snippet by Paul Osman in JControllerForm's constructor.
296              */
297             $paths[] = __DIR__ . '/' . $entity_plural;
298         }
299 
300         // Force the new path(s) to an array.
301         settype($new, 'array');
302 
303         // Add the new paths to the stack if not already there.
304         foreach ($new as $path)
305         {
306             if (!in_array($path, $paths))
307             {
308                 array_unshift($paths, trim($path));
309             }
310 
311             if (!is_dir($path))
312             {
313                 array_unshift($paths, trim($path));
314             }
315         }
316 
317         return $paths;
318     }
319 
320     /**
321      * Parse the show on conditions
322      *
323      * @param   string  $showOn       Show on conditions.
324      * @param   string  $formControl  Form name.
325      * @param   string  $group        The dot-separated form group path.
326      *
327      * @return  array   Array with show on conditions.
328      *
329      * @since   3.7.0
330      */
331     public static function parseShowOnConditions($showOn, $formControl = null, $group = null)
332     {
333         // Process the showon data.
334         if (!$showOn)
335         {
336             return array();
337         }
338 
339         $formPath = $formControl ?: '';
340 
341         if ($group)
342         {
343             $groups = explode('.', $group);
344             
345             // An empty formControl leads to invalid shown property
346             // Use the 1st part of the group instead to avoid. 
347             if (empty($formPath) && isset($groups[0]))
348             {
349                 $formPath = $groups[0];
350                 array_shift($groups);
351             }
352 
353             foreach ($groups as $group)
354             {
355                 $formPath .= '[' . $group . ']';
356             }
357         }
358 
359         $showOnData  = array();
360         $showOnParts = preg_split('#(\[AND\]|\[OR\])#', $showOn, -1, PREG_SPLIT_DELIM_CAPTURE);
361         $op          = '';
362 
363         foreach ($showOnParts as $showOnPart)
364         {
365             if (($showOnPart === '[AND]') || $showOnPart === '[OR]')
366             {
367                 $op = trim($showOnPart, '[]');
368                 continue;
369             }
370 
371             $compareEqual     = strpos($showOnPart, '!:') === false;
372             $showOnPartBlocks = explode(($compareEqual ? ':' : '!:'), $showOnPart, 2);
373 
374             $showOnData[] = array(
375                 'field'  => $formPath ? $formPath . '[' . $showOnPartBlocks[0] . ']' : $showOnPartBlocks[0],
376                 'values' => explode(',', $showOnPartBlocks[1]),
377                 'sign'   => $compareEqual === true ? '=' : '!=',
378                 'op'     => $op,
379             );
380 
381             if ($op !== '')
382             {
383                 $op = '';
384             }
385         }
386 
387         return $showOnData;
388     }
389 }
390