1 <?php
2 /**
3 * @package Joomla.Legacy
4 * @subpackage Model
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.txt
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 use Joomla\Utilities\ArrayHelper;
13
14 /**
15 * Base class for a Joomla Model
16 *
17 * Acts as a Factory class for application specific objects and provides many supporting API functions.
18 *
19 * @since 2.5.5
20 */
21 abstract class JModelLegacy extends JObject
22 {
23 /**
24 * Indicates if the internal state has been set
25 *
26 * @var boolean
27 * @since 3.0
28 */
29 protected $__state_set = null;
30
31 /**
32 * Database Connector
33 *
34 * @var JDatabaseDriver
35 * @since 3.0
36 */
37 protected $_db;
38
39 /**
40 * The model (base) name
41 *
42 * @var string
43 * @since 3.0
44 */
45 protected $name;
46
47 /**
48 * The URL option for the component.
49 *
50 * @var string
51 * @since 3.0
52 */
53 protected $option = null;
54
55 /**
56 * A state object
57 *
58 * @var JObject
59 * @since 3.0
60 */
61 protected $state;
62
63 /**
64 * The event to trigger when cleaning cache.
65 *
66 * @var string
67 * @since 3.0
68 */
69 protected $event_clean_cache = null;
70
71 /**
72 * Add a directory where JModelLegacy should search for models. You may
73 * either pass a string or an array of directories.
74 *
75 * @param mixed $path A path or array[sting] of paths to search.
76 * @param string $prefix A prefix for models.
77 *
78 * @return array An array with directory elements. If prefix is equal to '', all directories are returned.
79 *
80 * @since 3.0
81 */
82 public static function addIncludePath($path = '', $prefix = '')
83 {
84 static $paths;
85
86 if (!isset($paths))
87 {
88 $paths = array();
89 }
90
91 if (!isset($paths[$prefix]))
92 {
93 $paths[$prefix] = array();
94 }
95
96 if (!isset($paths['']))
97 {
98 $paths[''] = array();
99 }
100
101 if (!empty($path))
102 {
103 jimport('joomla.filesystem.path');
104
105 foreach ((array) $path as $includePath)
106 {
107 if (!in_array($includePath, $paths[$prefix]))
108 {
109 array_unshift($paths[$prefix], JPath::clean($includePath));
110 }
111
112 if (!in_array($includePath, $paths['']))
113 {
114 array_unshift($paths[''], JPath::clean($includePath));
115 }
116 }
117 }
118
119 return $paths[$prefix];
120 }
121
122 /**
123 * Adds to the stack of model table paths in LIFO order.
124 *
125 * @param mixed $path The directory as a string or directories as an array to add.
126 *
127 * @return void
128 *
129 * @since 3.0
130 */
131 public static function addTablePath($path)
132 {
133 JTable::addIncludePath($path);
134 }
135
136 /**
137 * Create the filename for a resource
138 *
139 * @param string $type The resource type to create the filename for.
140 * @param array $parts An associative array of filename information.
141 *
142 * @return string The filename
143 *
144 * @since 3.0
145 */
146 protected static function _createFileName($type, $parts = array())
147 {
148 $filename = '';
149
150 switch ($type)
151 {
152 case 'model':
153 $filename = strtolower($parts['name']) . '.php';
154 break;
155 }
156
157 return $filename;
158 }
159
160 /**
161 * Returns a Model object, always creating it
162 *
163 * @param string $type The model type to instantiate
164 * @param string $prefix Prefix for the model class name. Optional.
165 * @param array $config Configuration array for model. Optional.
166 *
167 * @return JModelLegacy|boolean A JModelLegacy instance or false on failure
168 *
169 * @since 3.0
170 */
171 public static function getInstance($type, $prefix = '', $config = array())
172 {
173 $type = preg_replace('/[^A-Z0-9_\.-]/i', '', $type);
174 $modelClass = $prefix . ucfirst($type);
175
176 if (!class_exists($modelClass))
177 {
178 jimport('joomla.filesystem.path');
179 $path = JPath::find(self::addIncludePath(null, $prefix), self::_createFileName('model', array('name' => $type)));
180
181 if (!$path)
182 {
183 $path = JPath::find(self::addIncludePath(null, ''), self::_createFileName('model', array('name' => $type)));
184 }
185
186 if (!$path)
187 {
188 return false;
189 }
190
191 require_once $path;
192
193 if (!class_exists($modelClass))
194 {
195 JLog::add(JText::sprintf('JLIB_APPLICATION_ERROR_MODELCLASS_NOT_FOUND', $modelClass), JLog::WARNING, 'jerror');
196
197 return false;
198 }
199 }
200
201 return new $modelClass($config);
202 }
203
204 /**
205 * Constructor
206 *
207 * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request).
208 *
209 * @since 3.0
210 * @throws Exception
211 */
212 public function __construct($config = array())
213 {
214 // Guess the option from the class name (Option)Model(View).
215 if (empty($this->option))
216 {
217 $r = null;
218
219 if (!preg_match('/(.*)Model/i', get_class($this), $r))
220 {
221 throw new Exception(JText::_('JLIB_APPLICATION_ERROR_MODEL_GET_NAME'), 500);
222 }
223
224 $this->option = 'com_' . strtolower($r[1]);
225 }
226
227 // Set the view name
228 if (empty($this->name))
229 {
230 if (array_key_exists('name', $config))
231 {
232 $this->name = $config['name'];
233 }
234 else
235 {
236 $this->name = $this->getName();
237 }
238 }
239
240 // Set the model state
241 if (array_key_exists('state', $config))
242 {
243 $this->state = $config['state'];
244 }
245 else
246 {
247 $this->state = new JObject;
248 }
249
250 // Set the model dbo
251 if (array_key_exists('dbo', $config))
252 {
253 $this->_db = $config['dbo'];
254 }
255 else
256 {
257 $this->_db = JFactory::getDbo();
258 }
259
260 // Set the default view search path
261 if (array_key_exists('table_path', $config))
262 {
263 $this->addTablePath($config['table_path']);
264 }
265 // @codeCoverageIgnoreStart
266 elseif (defined('JPATH_COMPONENT_ADMINISTRATOR'))
267 {
268 $this->addTablePath(JPATH_COMPONENT_ADMINISTRATOR . '/tables');
269 $this->addTablePath(JPATH_COMPONENT_ADMINISTRATOR . '/table');
270 }
271
272 // @codeCoverageIgnoreEnd
273
274 // Set the internal state marker - used to ignore setting state from the request
275 if (!empty($config['ignore_request']))
276 {
277 $this->__state_set = true;
278 }
279
280 // Set the clean cache event
281 if (isset($config['event_clean_cache']))
282 {
283 $this->event_clean_cache = $config['event_clean_cache'];
284 }
285 elseif (empty($this->event_clean_cache))
286 {
287 $this->event_clean_cache = 'onContentCleanCache';
288 }
289 }
290
291 /**
292 * Gets an array of objects from the results of database query.
293 *
294 * @param string $query The query.
295 * @param integer $limitstart Offset.
296 * @param integer $limit The number of records.
297 *
298 * @return object[] An array of results.
299 *
300 * @since 3.0
301 * @throws RuntimeException
302 */
303 protected function _getList($query, $limitstart = 0, $limit = 0)
304 {
305 $this->getDbo()->setQuery($query, $limitstart, $limit);
306
307 return $this->getDbo()->loadObjectList();
308 }
309
310 /**
311 * Returns a record count for the query.
312 *
313 * @param JDatabaseQuery|string $query The query.
314 *
315 * @return integer Number of rows for query.
316 *
317 * @since 3.0
318 */
319 protected function _getListCount($query)
320 {
321 // Use fast COUNT(*) on JDatabaseQuery objects if there is no GROUP BY or HAVING clause:
322 if ($query instanceof JDatabaseQuery
323 && $query->type == 'select'
324 && $query->group === null
325 && $query->union === null
326 && $query->unionAll === null
327 && $query->having === null)
328 {
329 $query = clone $query;
330 $query->clear('select')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)');
331
332 $this->getDbo()->setQuery($query);
333
334 return (int) $this->getDbo()->loadResult();
335 }
336
337 // Otherwise fall back to inefficient way of counting all results.
338
339 // Remove the limit and offset part if it's a JDatabaseQuery object
340 if ($query instanceof JDatabaseQuery)
341 {
342 $query = clone $query;
343 $query->clear('limit')->clear('offset');
344 }
345
346 $this->getDbo()->setQuery($query);
347 $this->getDbo()->execute();
348
349 return (int) $this->getDbo()->getNumRows();
350 }
351
352 /**
353 * Method to load and return a model object.
354 *
355 * @param string $name The name of the view
356 * @param string $prefix The class prefix. Optional.
357 * @param array $config Configuration settings to pass to JTable::getInstance
358 *
359 * @return JTable|boolean Table object or boolean false if failed
360 *
361 * @since 3.0
362 * @see JTable::getInstance()
363 */
364 protected function _createTable($name, $prefix = 'Table', $config = array())
365 {
366 // Clean the model name
367 $name = preg_replace('/[^A-Z0-9_]/i', '', $name);
368 $prefix = preg_replace('/[^A-Z0-9_]/i', '', $prefix);
369
370 // Make sure we are returning a DBO object
371 if (!array_key_exists('dbo', $config))
372 {
373 $config['dbo'] = $this->getDbo();
374 }
375
376 return JTable::getInstance($name, $prefix, $config);
377 }
378
379 /**
380 * Method to get the database driver object
381 *
382 * @return JDatabaseDriver
383 *
384 * @since 3.0
385 */
386 public function getDbo()
387 {
388 return $this->_db;
389 }
390
391 /**
392 * Method to get the model name
393 *
394 * The model name. By default parsed using the classname or it can be set
395 * by passing a $config['name'] in the class constructor
396 *
397 * @return string The name of the model
398 *
399 * @since 3.0
400 * @throws Exception
401 */
402 public function getName()
403 {
404 if (empty($this->name))
405 {
406 $r = null;
407
408 if (!preg_match('/Model(.*)/i', get_class($this), $r))
409 {
410 throw new Exception(JText::_('JLIB_APPLICATION_ERROR_MODEL_GET_NAME'), 500);
411 }
412
413 $this->name = strtolower($r[1]);
414 }
415
416 return $this->name;
417 }
418
419 /**
420 * Method to get model state variables
421 *
422 * @param string $property Optional parameter name
423 * @param mixed $default Optional default value
424 *
425 * @return mixed The property where specified, the state object where omitted
426 *
427 * @since 3.0
428 */
429 public function getState($property = null, $default = null)
430 {
431 if (!$this->__state_set)
432 {
433 // Protected method to auto-populate the model state.
434 $this->populateState();
435
436 // Set the model state set flag to true.
437 $this->__state_set = true;
438 }
439
440 return $property === null ? $this->state : $this->state->get($property, $default);
441 }
442
443 /**
444 * Method to get a table object, load it if necessary.
445 *
446 * @param string $name The table name. Optional.
447 * @param string $prefix The class prefix. Optional.
448 * @param array $options Configuration array for model. Optional.
449 *
450 * @return JTable A JTable object
451 *
452 * @since 3.0
453 * @throws Exception
454 */
455 public function getTable($name = '', $prefix = 'Table', $options = array())
456 {
457 if (empty($name))
458 {
459 $name = $this->getName();
460 }
461
462 if ($table = $this->_createTable($name, $prefix, $options))
463 {
464 return $table;
465 }
466
467 throw new Exception(JText::sprintf('JLIB_APPLICATION_ERROR_TABLE_NAME_NOT_SUPPORTED', $name), 0);
468 }
469
470 /**
471 * Method to load a row for editing from the version history table.
472 *
473 * @param integer $version_id Key to the version history table.
474 * @param JTable &$table Content table object being loaded.
475 *
476 * @return boolean False on failure or error, true otherwise.
477 *
478 * @since 3.2
479 */
480 public function loadHistory($version_id, JTable &$table)
481 {
482 // Only attempt to check the row in if it exists, otherwise do an early exit.
483 if (!$version_id)
484 {
485 return false;
486 }
487
488 // Get an instance of the row to checkout.
489 $historyTable = JTable::getInstance('Contenthistory');
490
491 if (!$historyTable->load($version_id))
492 {
493 $this->setError($historyTable->getError());
494
495 return false;
496 }
497
498 $rowArray = ArrayHelper::fromObject(json_decode($historyTable->version_data));
499 $typeId = JTable::getInstance('Contenttype')->getTypeId($this->typeAlias);
500
501 if ($historyTable->ucm_type_id != $typeId)
502 {
503 $this->setError(JText::_('JLIB_APPLICATION_ERROR_HISTORY_ID_MISMATCH'));
504
505 $key = $table->getKeyName();
506
507 if (isset($rowArray[$key]))
508 {
509 $table->checkIn($rowArray[$key]);
510 }
511
512 return false;
513 }
514
515 $this->setState('save_date', $historyTable->save_date);
516 $this->setState('version_note', $historyTable->version_note);
517
518 return $table->bind($rowArray);
519 }
520
521 /**
522 * Method to auto-populate the model state.
523 *
524 * This method should only be called once per instantiation and is designed
525 * to be called on the first call to the getState() method unless the model
526 * configuration flag to ignore the request is set.
527 *
528 * @return void
529 *
530 * @note Calling getState in this method will result in recursion.
531 * @since 3.0
532 */
533 protected function populateState()
534 {
535 }
536
537 /**
538 * Method to set the database driver object
539 *
540 * @param JDatabaseDriver $db A JDatabaseDriver based object
541 *
542 * @return void
543 *
544 * @since 3.0
545 */
546 public function setDbo($db)
547 {
548 $this->_db = $db;
549 }
550
551 /**
552 * Method to set model state variables
553 *
554 * @param string $property The name of the property.
555 * @param mixed $value The value of the property to set or null.
556 *
557 * @return mixed The previous value of the property or null if not set.
558 *
559 * @since 3.0
560 */
561 public function setState($property, $value = null)
562 {
563 return $this->state->set($property, $value);
564 }
565
566 /**
567 * Clean the cache
568 *
569 * @param string $group The cache group
570 * @param integer $client_id The ID of the client
571 *
572 * @return void
573 *
574 * @since 3.0
575 */
576 protected function cleanCache($group = null, $client_id = 0)
577 {
578 $conf = JFactory::getConfig();
579
580 $options = array(
581 'defaultgroup' => $group ?: (isset($this->option) ? $this->option : JFactory::getApplication()->input->get('option')),
582 'cachebase' => $client_id ? JPATH_ADMINISTRATOR . '/cache' : $conf->get('cache_path', JPATH_SITE . '/cache'),
583 'result' => true,
584 );
585
586 try
587 {
588 /** @var JCacheControllerCallback $cache */
589 $cache = JCache::getInstance('callback', $options);
590 $cache->clean();
591 }
592 catch (JCacheException $exception)
593 {
594 $options['result'] = false;
595 }
596
597 // Trigger the onContentCleanCache event.
598 JEventDispatcher::getInstance()->trigger($this->event_clean_cache, $options);
599 }
600 }
601