| Current Path : /proc/1908984/root/proc/self/root/proc/self/root/proc/2411249/cwd/ |
| Current File : //proc/1908984/root/proc/self/root/proc/self/root/proc/2411249/cwd/Model.tar |
DataModel/Behaviour/Access.php 0000644 00000003365 15234466453 0012257 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
defined('_JEXEC') or die;
/**
* FOF model behavior class to filter access to items based on the viewing access levels.
*
* @since 2.1
*/
class Access extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(&$model, &$query)
{
// Make sure the field actually exists
if (!$model->hasField('access'))
{
return;
}
$model->applyAccessFiltering(null);
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param Array &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(&$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists
if (!$model->hasField('access'))
{
return;
}
// Get the user
$user = $model->getContainer()->platform->getUser();
$recordAccessLevel = $model->getFieldValue('access', null);
// Filter by authorised access levels
if (!in_array($recordAccessLevel, $user->getAuthorisedViewLevels()))
{
$model->reset(true);
}
}
}
DataModel/Behaviour/Assets.php 0000644 00000010575 15234466453 0012321 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
defined('_JEXEC') or die;
/**
* FOF model behavior class to add Joomla! ACL assets support
*
* @since 2.1
*/
class Assets extends Observer
{
public function onAfterSave(DataModel &$model)
{
if (!$model->hasField('asset_id') || !$model->isAssetsTracked())
{
return true;
}
$assetFieldAlias = $model->getFieldAlias('asset_id');
$currentAssetId = $model->getFieldValue('asset_id');
unset($model->$assetFieldAlias);
// Create the object used for inserting/udpating data to the database
$fields = $model->getTableFields();
// Let's remove the asset_id field, since we unset the property above and we would get a PHP notice
if (isset($fields[ $assetFieldAlias ]))
{
unset($fields[ $assetFieldAlias ]);
}
// Asset Tracking
$parentId = $model->getAssetParentId();
$name = $model->getAssetName();
$title = $model->getAssetTitle();
$asset = \JTable::getInstance('Asset');
$asset->loadByName($name);
// Re-inject the asset id.
$this->$assetFieldAlias = $asset->id;
// Check for an error.
$error = $asset->getError();
// Since we are using JTable, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if ($error)
{
throw new \Exception($error);
}
// @codeCoverageIgnoreEnd
// Specify how a new or moved node asset is inserted into the tree.
// Since we're unsetting the table field before, this statement is always true...
if (empty($model->$assetFieldAlias) || $asset->parent_id != $parentId)
{
$asset->setLocation($parentId, 'last-child');
}
// Prepare the asset to be stored.
$asset->parent_id = $parentId;
$asset->name = $name;
$asset->title = $title;
if ($model->getRules() instanceof \JAccessRules)
{
$asset->rules = (string) $model->getRules();
}
// Since we are using JTable, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!$asset->check() || !$asset->store())
{
throw new \Exception($asset->getError());
}
// @codeCoverageIgnoreEnd
// Create an asset_id or heal one that is corrupted.
if (empty($model->$assetFieldAlias) || (($currentAssetId != $model->$assetFieldAlias) && !empty($model->$assetFieldAlias)))
{
// Update the asset_id field in this table.
$model->$assetFieldAlias = (int) $asset->id;
$k = $model->getKeyName();
$db = $model->getDbo();
$query = $db->getQuery(true)
->update($db->qn($model->getTableName()))
->set($db->qn($assetFieldAlias) . ' = ' . (int) $model->$assetFieldAlias)
->where($db->qn($k) . ' = ' . (int) $model->$k);
$db->setQuery($query)->execute();
}
return true;
}
public function onAfterBind(DataModel &$model, &$src)
{
if (!$model->isAssetsTracked())
{
return true;
}
// Bind the rules.
if (isset($src['rules']) && is_array($src['rules']))
{
// We have to manually remove any empty value, since they will be converted to int,
// and "Inherited" values will become "Denied". Joomla is doing this manually, too.
$rules = array();
foreach ($src['rules'] as $action => $ids)
{
// Build the rules array.
$rules[$action] = array();
foreach ($ids as $id => $p)
{
if ($p !== '')
{
$rules[$action][$id] = ($p == '1' || $p == 'true') ? true : false;
}
}
}
$model->setRules($rules);
}
return true;
}
public function onBeforeDelete(DataModel &$model, $oid)
{
if (!$model->isAssetsTracked())
{
return true;
}
$k = $model->getKeyName();
// If the table is not loaded, let's try to load it with the id
if(!$model->$k)
{
$model->load($oid);
}
// If I have an invalid assetName I have to stop
$name = $model->getAssetName();
// Do NOT touch JTable here -- we are loading the core asset table which is a JTable, not a FOF Table
$asset =\ JTable::getInstance('Asset');
if ($asset->loadByName($name))
{
// Since we are using JTable, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!$asset->delete())
{
throw new \Exception($asset->getError());
}
// @codeCoverageIgnoreEnd
}
return true;
}
}
DataModel/Behaviour/ContentHistory.php 0000644 00000004372 15234466453 0014051 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
defined('_JEXEC') or die;
/**
* FOF model behavior class to add Joomla! content history support
*
* @since 2.1
*/
class ContentHistory extends Observer
{
/** @var \JHelperContenthistory */
protected $historyHelper;
/**
* The event which runs after storing (saving) data to the database
*
* @param DataModel &$model The model which calls this event
*
* @return boolean True to allow saving without an error
*/
public function onAfterSave(&$model)
{
$aliasParts = explode('.', $model->getContentType());
$model->checkContentType();
if (\JComponentHelper::getParams($aliasParts[0])->get('save_history', 0))
{
if(!$this->historyHelper)
{
$this->historyHelper = new \JHelperContenthistory($model->getContentType());
}
$this->historyHelper->store($model);
}
return true;
}
/**
* The event which runs before deleting a record
*
* @param DataModel &$model The model which calls this event
* @param integer $oid The PK value of the record to delete
*
* @return boolean True to allow the deletion
*/
public function onBeforeDelete(&$model, $oid)
{
$aliasParts = explode('.', $model->getContentType());
if (\JComponentHelper::getParams($aliasParts[0])->get('save_history', 0))
{
if(!$this->historyHelper)
{
$this->historyHelper = new \JHelperContenthistory($model->getContentType());
}
$this->historyHelper->deleteHistory($model);
}
return true;
}
/**
* This event runs after publishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterPublish(&$model)
{
$model->updateUcmContent();
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterUnpublish(&$model)
{
$model->updateUcmContent();
}
}
DataModel/Behaviour/Created.php 0000644 00000003717 15234466453 0012426 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
defined('_JEXEC') or die;
/**
* FOF model behavior class to updated the created_by and created_on fields on newly created records.
*
* This behaviour is added to DataModel by default. If you want to remove it you need to do
* $this->behavioursDispatcher->removeBehaviour('Created');
*
* @since 3.0
*/
class Created extends Observer
{
/**
* Add the created_on and created_by fields in the fieldsSkipChecks list of the model. We expect them to be empty
* so that we can fill them in through this behaviour.
*
* @param DataModel $model
*/
public function onBeforeCheck(&$model)
{
$model->addSkipCheckField('created_on');
$model->addSkipCheckField('created_by');
}
/**
* @param DataModel $model
* @param \stdClass $dataObject
*/
public function onBeforeCreate(&$model, &$dataObject)
{
// Handle the created_on field
if ($model->hasField('created_on'))
{
$nullDate = $model->getDbo()->getNullDate();
$created_on = $model->getFieldValue('created_on');
if (empty($created_on) || ($created_on == $nullDate))
{
$model->setFieldValue('created_on', $model->getContainer()->platform->getDate()->toSql(false, $model->getDbo()));
$createdOnField = $model->getFieldAlias('created_on');
$dataObject->$createdOnField = $model->getFieldValue('created_on');
}
}
// Handle the created_by field
if ($model->hasField('created_by'))
{
$created_by = $model->getFieldValue('created_by');
if (empty($created_by))
{
$model->setFieldValue('created_by', $model->getContainer()->platform->getUser()->id);
$createdByField = $model->getFieldAlias('created_by');
$dataObject->$createdByField = $model->getFieldValue('created_by');
}
}
}
}
DataModel/Behaviour/EmptyNonZero.php 0000644 00000001565 15234466453 0013467 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
defined('_JEXEC') or die;
/**
* FOF model behavior class to let the Filters behaviour know that zero value is a valid filter value
*
* @since 2.1
*/
class EmptyNonZero extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(&$model, &$query)
{
$model->setBehaviorParam('filterZero', 1);
}
}
DataModel/Behaviour/Enabled.php 0000644 00000003250 15234466453 0012401 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
defined('_JEXEC') or die;
/**
* FOF model behavior class to filter access to items based on the enabled status
*
* @since 2.1
*/
class Enabled extends Observer
{
/**
* This event runs before we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onBeforeBuildQuery(&$model, &$query)
{
// Make sure the field actually exists
if (!$model->hasField('enabled'))
{
return;
}
$fieldName = $model->getFieldAlias('enabled');
$db = $model->getDbo();
$model->whereRaw($db->qn($fieldName) . ' = ' . $db->q(1));
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param Array &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(&$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists
if (!$model->hasField('enabled'))
{
return;
}
// Filter by enabled status
if (!$model->getFieldValue('enabled', 0))
{
$model->reset(true);
}
}
}
DataModel/Behaviour/Filters.php 0000644 00000006723 15234466453 0012467 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
use Joomla\Registry\Registry;
defined('_JEXEC') or die;
class Filters extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(&$model, &$query)
{
$tableKey = $model->getIdFieldName();
$db = $model->getDbo();
$fields = $model->getTableFields();
$blacklist = $model->getBlacklistFilters();
$filterZero = $model->getBehaviorParam('filterZero', null);
$tableAlias = $model->getBehaviorParam('tableAlias', null);
foreach ($fields as $fieldname => $fieldmeta)
{
if (in_array($fieldname, $blacklist))
{
continue;
}
$fieldInfo = (object)array(
'name' => $fieldname,
'type' => $fieldmeta->Type,
'filterZero' => $filterZero,
'tableAlias' => $tableAlias,
);
$filterName = $fieldInfo->name;
$filterState = $model->getState($filterName, null);
// Special primary key handling: if ignore request is set we'll also look for an 'id' state variable if a
// state variable by the same name as the key doesn't exist. If ignore request is not set in the model we
// do not filter by 'id' since this interferes with going from an edit page to a browse page (the list is
// filtered by id without user controls to unset it).
if ($fieldInfo->name == $tableKey)
{
$filterState = $model->getState($filterName, null);
if (!$model->getIgnoreRequest())
{
continue;
}
if (empty($filterState))
{
$filterState = $model->getState('id', null);
}
}
$field = DataModel\Filter\AbstractFilter::getField($fieldInfo, array('dbo' => $db));
if (!is_object($field) || !($field instanceof DataModel\Filter\AbstractFilter))
{
continue;
}
if ((is_array($filterState) && (
array_key_exists('value', $filterState) ||
array_key_exists('from', $filterState) ||
array_key_exists('to', $filterState)
)) || is_object($filterState))
{
$options = class_exists('JRegistry') ? new \JRegistry($filterState) : new Registry($filterState);
}
else
{
$options = class_exists('JRegistry') ? new \JRegistry() : new Registry();
$options->set('value', $filterState);
}
$methods = $field->getSearchMethods();
$method = $options->get('method', $field->getDefaultSearchMethod());
if (!in_array($method, $methods))
{
$method = 'exact';
}
switch ($method)
{
case 'between':
case 'outside':
case 'range' :
$sql = $field->$method($options->get('from', null), $options->get('to', null), $options->get('include', false));
break;
case 'interval':
case 'modulo':
$sql = $field->$method($options->get('value', null), $options->get('interval'));
break;
case 'search':
$sql = $field->$method($options->get('value', null), $options->get('operator', '='));
break;
case 'exact':
case 'partial':
default:
$sql = $field->$method($options->get('value', null));
break;
}
if ($sql)
{
$query->where($sql);
}
}
}
}
DataModel/Behaviour/Language.php 0000644 00000011022 15234466453 0012566 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
use Joomla\Registry\Registry;
defined('_JEXEC') or die;
/**
* FOF model behavior class to filter front-end access to items
* based on the language.
*
* @since 2.1
*/
class Language extends Observer
{
/** @var \PlgSystemLanguageFilter */
protected $lang_filter_plugin;
/**
* This event runs before we have built the query used to fetch a record
* list in a model. It is used to blacklist the language filter
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The model which calls this event
*
* @return void
*/
public function onBeforeBuildQuery(&$model, &$query)
{
if ($model->getContainer()->platform->isFrontend())
{
$model->blacklistFilters('language');
}
// Make sure the field actually exists AND we're not in CLI
if (!$model->hasField('language') || $model->getContainer()->platform->isCli())
{
return;
}
/** @var \JApplicationSite $app */
$app = \JFactory::getApplication();
$hasLanguageFilter = method_exists($app, 'getLanguageFilter');
if ($hasLanguageFilter)
{
$hasLanguageFilter = $app->getLanguageFilter();
}
if (!$hasLanguageFilter)
{
return;
}
// Ask Joomla for the plugin only if we don't already have it. Useful for tests
if(!$this->lang_filter_plugin)
{
$this->lang_filter_plugin = \JPluginHelper::getPlugin('system', 'languagefilter');
}
$lang_filter_params = class_exists('JRegistry') ? new \JRegistry($this->lang_filter_plugin->params) : new Registry($this->lang_filter_plugin->params);
$languages = array('*');
if ($lang_filter_params->get('remove_default_prefix'))
{
// Get default site language
$platform = $model->getContainer()->platform;
$lg = $platform->getLanguage();
$languages[] = $lg->getTag();
}
else
{
// We have to use JInput since the language fragment is not set in the $_REQUEST, thus we won't have it in our model
// TODO Double check the previous assumption
$languages[] = \JFactory::getApplication()->input->getCmd('language', '*');
}
// Filter out double languages
$languages = array_unique($languages);
// And filter the query output by these languages
$db = $model->getDbo();
$languages = array_map(array($db, 'quote'), $languages);
$fieldName = $model->getFieldAlias('language');
$model->whereRaw($db->qn($fieldName) . ' IN(' . implode(', ', $languages) . ')');
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param Array &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(&$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists AND we're not in CLI
if (!$model->hasField('language') || $model->getContainer()->platform->isCli())
{
return;
}
// Make sure it is a multilingual site and get a list of languages
/** @var \JApplicationSite $app */
$app = \JFactory::getApplication();
$hasLanguageFilter = method_exists($app, 'getLanguageFilter');
if ($hasLanguageFilter)
{
$hasLanguageFilter = $app->getLanguageFilter();
}
if (!$hasLanguageFilter)
{
return;
}
// Ask Joomla for the plugin only if we don't already have it. Useful for tests
if(!$this->lang_filter_plugin)
{
$this->lang_filter_plugin = \JPluginHelper::getPlugin('system', 'languagefilter');
}
$lang_filter_params = class_exists('JRegistry') ? new \JRegistry($this->lang_filter_plugin->params) : new Registry($this->lang_filter_plugin->params);
$languages = array('*');
if ($lang_filter_params->get('remove_default_prefix'))
{
// Get default site language
$lg = $model->getContainer()->platform->getLanguage();
$languages[] = $lg->getTag();
}
else
{
$languages[] = \JFactory::getApplication()->input->getCmd('language', '*');
}
// Filter out double languages
$languages = array_unique($languages);
// Filter by language
if (!in_array($model->getFieldValue('language'), $languages))
{
$model->reset();
}
}
}
DataModel/Behaviour/Modified.php 0000644 00000003560 15234466453 0012573 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
defined('_JEXEC') or die;
/**
* FOF model behavior class to updated the modified_by and modified_on fields on newly created records.
*
* This behaviour is added to DataModel by default. If you want to remove it you need to do
* $this->behavioursDispatcher->removeBehaviour('Modified');
*
* @since 3.0
*/
class Modified extends Observer
{
/**
* Add the modified_on and modified_by fields in the fieldsSkipChecks list of the model. We expect them to be empty
* so that we can fill them in through this behaviour.
*
* @param DataModel $model
*/
public function onBeforeCheck(&$model)
{
$model->addSkipCheckField('modified_on');
$model->addSkipCheckField('modified_by');
}
/**
* @param DataModel $model
* @param \stdClass $dataObject
*/
public function onBeforeUpdate(&$model, &$dataObject)
{
// Make sure we're not modifying a locked record
$userId = $model->getContainer()->platform->getUser()->id;
$isLocked = $model->isLocked($userId);
if ($isLocked)
{
return;
}
// Handle the modified_on field
if ($model->hasField('modified_on'))
{
$model->setFieldValue('modified_on', $model->getContainer()->platform->getDate()->toSql(false, $model->getDbo()));
$modifiedOnField = $model->getFieldAlias('modified_on');
$dataObject->$modifiedOnField = $model->getFieldValue('modified_on');
}
// Handle the modified_by field
if ($model->hasField('modified_by'))
{
$model->setFieldValue('modified_by', $userId);
$modifiedByField = $model->getFieldAlias('modified_by');
$dataObject->$modifiedByField = $model->getFieldValue('modified_by');
}
}
}
DataModel/Behaviour/Own.php 0000644 00000003727 15234466453 0011623 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use JDatabaseQuery;
defined('_JEXEC') or die;
/**
* FOF model behavior class to filter access to items owned by the currently logged in user only
*
* @since 2.1
*/
class Own extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(&$model, &$query)
{
// Make sure the field actually exists
if (!$model->hasField('created_by'))
{
return;
}
// Get the current user's id
$user_id = $model->getContainer()->platform->getUser()->id;
// And filter the query output by the user id
$db = $model->getContainer()->platform->getDbo();
$query->where($db->qn($model->getFieldAlias('created_by')) . ' = ' . $db->q($user_id));
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param Array &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(&$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists
if (!$model->hasField('created_by'))
{
return;
}
// Get the user
$user_id = $model->getContainer()->platform->getUser()->id;
$recordUser = $model->getFieldValue('created_by', null);
// Filter by authorised access levels
if ($recordUser != $user_id)
{
$model->reset(true);
}
}
}
DataModel/Behaviour/PageParametersToState.php 0000644 00000002770 15234466453 0015261 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use Joomla\Registry\Registry;
defined('_JEXEC') or die;
/**
* FOF model behavior class to populate the state with the front-end page parameters
*
* @since 2.1
*/
class PageParametersToState extends Observer
{
public function onAfterConstruct(DataModel &$model)
{
// This only applies to the front-end
if (!$model->getContainer()->platform->isFrontend())
{
return;
}
// Get the page parameters
/** @var \JApplicationSite $app */
$app = \JFactory::getApplication();
/** @var \JRegistry|Registry $params */
$params = $app->getParams();
// Extract the page parameter keys
$asArray = $params->toArray();
if (empty($asArray))
{
// There are no keys; no point in going on.
return;
}
$keys = array_keys($asArray);
unset($asArray);
// Loop all page parameter keys
foreach ($keys as $key)
{
// This is the current model state
$currentState = $model->getState($key);
// This is the explicitly requested state in the input
$explicitInput = $model->input->get($key, null, 'raw');
// If the current state is empty and there's no explicit input we'll use the page parameters instead
if (is_null($currentState) && is_null($explicitInput))
{
$model->setState($key, $params->get($key));
}
}
}
}
DataModel/Behaviour/RelationFilters.php 0000644 00000004376 15234466453 0014167 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use Joomla\Registry\Registry;
use JRegistry;
defined('_JEXEC') or die;
class RelationFilters extends Observer
{
/**
* This event runs after we have built the query used to fetch a record list in a model. It is used to apply
* automatic query filters based on model relations.
*
* @param DataModel &$model The model which calls this event
* @param \JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(&$model, &$query)
{
$relationFilters = $model->getRelationFilters();
foreach ($relationFilters as $filterState)
{
$relationName = $filterState['relation'];
$tableAlias = $model->getBehaviorParam('tableAlias', null);
$subQuery = $model->getRelations()->getCountSubquery($relationName, $tableAlias);
// Callback method needs different handling
if (isset($filterState['method']) && ($filterState['method'] == 'callback'))
{
call_user_func_array($filterState['value'], array(&$subQuery));
$filterState['method'] = 'search';
$filterState['operator'] = '>=';
$filterState['value'] = '1';
}
$options = class_exists('JRegistry') ? new JRegistry($filterState) : new Registry($filterState);
$filter = new DataModel\Filter\Relation($model->getDbo(), $relationName, $subQuery);
$methods = $filter->getSearchMethods();
$method = $options->get('method', $filter->getDefaultSearchMethod());
if (!in_array($method, $methods))
{
$method = 'exact';
}
switch ($method)
{
case 'between':
case 'outside':
$sql = $filter->$method($options->get('from', null), $options->get('to'));
break;
case 'interval':
$sql = $filter->$method($options->get('value', null), $options->get('interval'));
break;
case 'search':
$sql = $filter->$method($options->get('value', null), $options->get('operator', '='));
break;
default:
$sql = $filter->$method($options->get('value', null));
break;
}
if ($sql)
{
$query->where($sql);
}
}
}
}
DataModel/Behaviour/Tags.php 0000644 00000007655 15234466453 0011762 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Behaviour;
use FOF30\Event\Observer;
use FOF30\Model\DataModel;
use FOF30\Event\Observable;
defined('_JEXEC') or die;
/**
* FOF model behavior class to add Joomla! Tags support
*
* @since 2.1
*/
class Tags extends Observer
{
/** @var \JHelperTags */
protected $tagsHelper;
public function __construct(Observable &$subject)
{
parent::__construct($subject);
$this->tagsHelper = new \JHelperTags();
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
* @param \stdClass &$dataObject The data to bind to the form
*
* @return void
*/
public function onBeforeCreate(&$model, &$dataObject)
{
$tagField = $model->getBehaviorParam('tagFieldName', 'tags');
unset($dataObject->$tagField);
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
* @param \stdClass &$dataObject The data to bind to the form
*
* @return void
*/
public function onBeforeUpdate(&$model, &$dataObject)
{
$tagField = $model->getBehaviorParam('tagFieldName', 'tags');
unset($dataObject->$tagField);
}
/**
* The event which runs after binding data to the table
*
* @param DataModel &$model The model which calls this event
*
* @return void
*
* @throws \Exception Error message if failed to store tags
*/
public function onAfterSave(&$model)
{
$tagField = $model->getBehaviorParam('tagFieldName', 'tags');
// Avoid to update on other method (e.g. publish, ...)
if (!in_array($model->getContainer()->input->getCmd('task'), array('apply', 'save', 'savenew')))
{
return;
}
$oldTags = $this->tagsHelper->getTagIds($model->getId(), $model->getContentType());
$newTags = $model->$tagField ? implode(',', $model->$tagField) : null;
// If no changes, we stop here
if ($oldTags == $newTags)
{
return;
}
// Check if the content type exists, and create it if it does not
$model->checkContentType();
$this->tagsHelper->typeAlias = $model->getContentType();
if (!$this->tagsHelper->postStoreProcess($model, $model->$tagField))
{
throw new \Exception('Error storing tags');
}
}
/**
* The event which runs after deleting a record
*
* @param DataModel &$model The model which calls this event
* @param integer $oid The PK value of the record which was deleted
*
* @return void
*
* @throws \Exception Error message if failed to detele tags
*/
public function onAfterDelete(&$model, $oid)
{
$this->tagsHelper->typeAlias = $model->getContentType();
if (!$this->tagsHelper->deleteTagData($model, $oid))
{
throw new \Exception('Error deleting Tags');
}
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
* @param mixed $data An associative array or object to bind to the DataModel instance.
*
* @return void
*/
public function onAfterBind(&$model, &$data)
{
$tagField = $model->getBehaviorParam('tagFieldName', 'tags');
if ($model->$tagField)
{
return;
}
$type = $model->getContentType();
$model->addKnownField($tagField);
$model->$tagField = $this->tagsHelper->getTagIds($model->getId(), $type);
}
/**
* This event runs after publishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterPublish(&$model)
{
$model->updateUcmContent();
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterUnpublish(&$model)
{
$model->updateUcmContent();
}
}
DataModel/Exception/BaseException.php 0000644 00000000426 15234466453 0013614 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
defined('_JEXEC') or die;
class BaseException extends \RuntimeException
{
}
DataModel/Exception/CannotLockNotLoadedRecord.php 0000644 00000001050 15234466453 0016041 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class CannotLockNotLoadedRecord extends BaseException
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_CANNOTLOCKNOTLOADEDRECORD');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/InvalidSearchMethod.php 0000644 00000000430 15234466453 0014733 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
defined('_JEXEC') or die;
class InvalidSearchMethod extends BaseException
{
}
DataModel/Exception/NoAssetKey.php 0000644 00000001026 15234466453 0013105 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class NoAssetKey extends \UnexpectedValueException
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_NOASSETKEY');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/NoContentType.php 0000644 00000001013 15234466453 0013625 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class NoContentType extends \UnexpectedValueException
{
public function __construct( $className, $code = 500, Exception $previous = null )
{
$message = \JText::sprintf('LIB_FOF_MODEL_ERR_NOCONTENTTYPE', $className);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/NoItemsFound.php 0000644 00000000775 15234466453 0013444 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class NoItemsFound extends BaseException
{
public function __construct( $className, $code = 404, Exception $previous = null )
{
$message = \JText::sprintf('LIB_FOF_MODEL_ERR_NOITEMSFOUND', $className);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/NoTableColumns.php 0000644 00000000423 15234466453 0013745 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
defined('_JEXEC') or die;
class NoTableColumns extends BaseException
{
}
DataModel/Exception/RecordNotLoaded.php 0000644 00000001021 15234466453 0014063 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class RecordNotLoaded extends BaseException
{
public function __construct( $message = "", $code = 404, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_COULDNOTLOAD');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/SpecialColumnMissing.php 0000644 00000000431 15234466453 0015147 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
defined('_JEXEC') or die;
class SpecialColumnMissing extends BaseException
{
}
DataModel/Exception/TreeIncompatibleTable.php 0000644 00000001034 15234466453 0015255 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeIncompatibleTable extends \UnexpectedValueException
{
public function __construct( $tableName, $code = 500, Exception $previous = null )
{
$message = \JText::sprintf('LIB_FOF_MODEL_ERR_TREE_INCOMPATIBLETABLE', $tableName);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgt.php 0000644 00000000705 15234466453 0014414 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
abstract class TreeInvalidLftRgt extends \RuntimeException
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgtCurrent.php 0000644 00000001054 15234466453 0015755 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeInvalidLftRgtCurrent extends TreeInvalidLftRgt
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_CURRENT');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgtOther.php 0000644 00000001050 15234466453 0015410 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeInvalidLftRgtOther extends TreeInvalidLftRgt
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_OTHER');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgtParent.php 0000644 00000001052 15234466453 0015562 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeInvalidLftRgtParent extends TreeInvalidLftRgt
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_PARENT');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgtSibling.php 0000644 00000001054 15234466453 0015722 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeInvalidLftRgtSibling extends TreeInvalidLftRgt
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_TREE_INVALIDLFTRGT_SIBLING');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeMethodOnlyAllowedInRoot.php 0000644 00000001022 15234466453 0016421 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeMethodOnlyAllowedInRoot extends \RuntimeException
{
public function __construct( $method = '', $code = 500, Exception $previous = null )
{
$message = \JText::sprintf('LIB_FOF_MODEL_ERR_TREE_ONLYINROOT', $method);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeRootNotFound.php 0000644 00000001026 15234466453 0014300 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeRootNotFound extends \RuntimeException
{
public function __construct( $tableName, $lft, $code = 500, Exception $previous = null )
{
$message = \JText::sprintf('LIB_FOF_MODEL_ERR_TREE_ROOTNOTFOUND', $tableName, $lft);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeUnexpectedPrimaryKey.php 0000644 00000001053 15234466453 0016021 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeUnexpectedPrimaryKey extends \UnexpectedValueException
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_TREE_UNEXPECTEDPK');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeUnsupportedMethod.php 0000644 00000001021 15234466453 0015364 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Exception;
use Exception;
defined('_JEXEC') or die;
class TreeUnsupportedMethod extends \LogicException
{
public function __construct( $method = '', $code = 500, Exception $previous = null )
{
$message = \JText::sprintf('LIB_FOF_MODEL_ERR_TREE_UNSUPPORTEDMETHOD', $method);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Filter/Exception/InvalidFieldObject.php 0000644 00000001056 15234466453 0015771 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Filter\Exception;
use Exception;
defined('_JEXEC') or die;
class InvalidFieldObject extends \InvalidArgumentException
{
public function __construct( $message = "", $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_FILTER_INVALIDFIELD');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Filter/Exception/NoDatabaseObject.php 0000644 00000001031 15234466453 0015431 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Filter\Exception;
use Exception;
defined('_JEXEC') or die;
class NoDatabaseObject extends \InvalidArgumentException
{
public function __construct( $fieldType, $code = 500, Exception $previous = null )
{
$message = \JText::sprintf('LIB_FOF_MODEL_ERR_FILTER_NODBOBJECT', $fieldType);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Filter/AbstractFilter.php 0000644 00000023164 15234466453 0013267 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Filter;
use FOF30\Model\DataModel\Filter\Exception\InvalidFieldObject;
use FOF30\Model\DataModel\Filter\Exception\NoDatabaseObject;
defined('_JEXEC') or die;
abstract class AbstractFilter
{
protected $db = null;
/**
* The column name of the table field
*
* @var string
*/
protected $name = '';
/**
* The column type of the table field
*
* @var string
*/
protected $type = '';
/**
* Should I allow filtering against the number 0?
*
* @var bool
*/
protected $filterZero = true;
/**
* Prefix each table name with this table alias. For example, field bar normally creates a WHERE clause:
* `bar` = '1'
* If tableAlias is set to "foo" then the WHERE clause it generates becomes
* `foo`.`bar` = '1'
*
* @var null
*/
protected $tableAlias = null;
/**
* The null value for this type
*
* @var mixed
*/
public $null_value = null;
/**
* Constructor
*
* @param \JDatabaseDriver $db The database object
* @param object $field The field information as taken from the db
*/
public function __construct($db, $field)
{
$this->db = $db;
if(!is_object($field) || !isset($field->name) || !isset($field->type))
{
throw new InvalidFieldObject;
}
$this->name = $field->name;
$this->type = $field->type;
if (isset ($field->filterZero))
{
$this->filterZero = $field->filterZero;
}
if (isset ($field->tableAlias))
{
$this->tableAlias = $field->tableAlias;
}
}
/**
* Is it a null or otherwise empty value?
*
* @param mixed $value The value to test for emptiness
*
* @return boolean
*/
public function isEmpty($value)
{
return (($value === $this->null_value) || empty($value))
&& !($this->filterZero && ($value === "0"));
}
/**
* Returns the default search method for a field. This always returns 'exact'
* and you are supposed to override it in specialised classes. The possible
* values are exact, partial, between and outside, unless something
* different is returned by getSearchMethods().
*
* @see self::getSearchMethods()
*
* @return string
*/
public function getDefaultSearchMethod()
{
return 'exact';
}
/**
* Return the search methods available for this field class,
*
* @return array
*/
public function getSearchMethods()
{
$ignore = array('isEmpty', 'getField', 'getFieldType', '__construct', 'getDefaultSearchMethod', 'getSearchMethods', 'getFieldName');
$class = new \ReflectionClass(__CLASS__);
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
$tmp = array();
foreach ($methods as $method)
{
$tmp[] = $method->name;
}
$methods = $tmp;
if ($methods = array_diff($methods, $ignore))
{
return $methods;
}
return array();
}
/**
* Perform an exact match (equality matching)
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
public function exact($value)
{
if ($this->isEmpty($value))
{
return '';
}
if (is_array($value))
{
$db = $this->db;
$value = array_map(array($db, 'quote'), $value);
return '(' . $this->getFieldName() . ' IN (' . implode(',', $value) . '))';
}
else
{
return $this->search($value, '=');
}
}
/**
* Perform a partial match (usually: search in string)
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
abstract public function partial($value);
/**
* Perform a between limits match (usually: search for a value between
* two numbers or a date between two preset dates). When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The higherst value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
abstract public function between($from, $to, $include = true);
/**
* Perform an outside limits match (usually: search for a value outside an
* area or a date outside a preset period). When $include is true
* the condition tested is:
* (VALUE <= $from) || (VALUE >= $to)
* When $include is false the condition tested is:
* (VALUE < $from) || (VALUE > $to)
*
* @param mixed $from The lowest value of the excluded range
* @param mixed $to The highest value of the excluded range
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
abstract public function outside($from, $to, $include = false);
/**
* Perform an interval search (usually: a date interval check)
*
* @param string $from The value to search
* @param string|array|object $interval The interval
*
* @return string The SQL where clause for this search
*/
abstract public function interval($from, $interval);
/**
* Perform a between limits match (usually: search for a value between
* two numbers or a date between two preset dates). When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The higherst value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
abstract public function range($from, $to, $include = true);
/**
* Perform an modulo search
*
* @param integer|float $from The starting value of the search space
* @param integer|float $interval The interval period of the search space
* @param boolean $include Should I include the boundaries in the search?
*
* @return string The SQL where clause
*/
abstract public function modulo($from, $interval, $include = true);
/**
* Return the SQL where clause for a search
*
* @param mixed $value The value to search for
* @param string $operator The operator to use
*
* @return string The SQL where clause for this search
*/
public function search($value, $operator = '=')
{
if ($this->isEmpty($value))
{
return '';
}
$prefix = '';
if (substr($operator, 0, 1) == '!')
{
$prefix = 'NOT ';
$operator = substr($operator, 1);
}
return $prefix . '(' . $this->getFieldName() . ' ' . $operator . ' ' . $this->db->quote($value) . ')';
}
/**
* Get the field name
*
* @return string The field name
*/
public function getFieldName()
{
$name = $this->db->qn($this->name);
if (!empty($this->tableAlias))
{
$name = $this->db->qn($this->tableAlias) . '.' . $name;
}
return $name;
}
/**
* Creates a field Object based on the field column type
*
* @param object $field The field informations
* @param array $config The field configuration (like the db object to use)
*
* @return AbstractFilter The Filter object
*
* @throws \InvalidArgumentException
*/
public static function getField($field, $config = array())
{
if(!is_object($field) || !isset($field->name) || !isset($field->type))
{
throw new InvalidFieldObject;
}
$type = $field->type;
$classType = self::getFieldType($type);
$className = '\\FOF30\\Model\\DataModel\\Filter\\' . ucfirst($classType);
if (($classType !== false) && class_exists($className, true))
{
if (!isset($config['dbo']))
{
throw new NoDatabaseObject($className);
}
$db = $config['dbo'];
$field = new $className($db, $field);
return $field;
}
return null;
}
/**
* Get the class name based on the field Type
*
* @param string $type The type of the field
*
* @return string the class name suffix
*/
public static function getFieldType($type)
{
// Remove parentheses, indicating field options / size (they don't matter in type detection)
if (!empty($type))
{
list($type, ) = explode('(', $type);
}
$detectedType = null;
switch (trim($type))
{
case 'varchar':
case 'text':
case 'smalltext':
case 'longtext':
case 'char':
case 'mediumtext':
case 'character varying':
case 'nvarchar':
case 'nchar':
$detectedType = 'Text';
break;
case 'date':
case 'datetime':
case 'time':
case 'year':
case 'timestamp':
case 'timestamp without time zone':
case 'timestamp with time zone':
$detectedType = 'Date';
break;
case 'tinyint':
case 'smallint':
$detectedType = 'Boolean';
break;
}
// Sometimes we have character types followed by a space and some cruft. Let's handle them.
if (is_null($detectedType) && !empty($type))
{
list ($type, ) = explode(' ', $type);
switch (trim($type))
{
case 'varchar':
case 'text':
case 'smalltext':
case 'longtext':
case 'char':
case 'mediumtext':
case 'nvarchar':
case 'nchar':
$detectedType = 'Text';
break;
case 'date':
case 'datetime':
case 'time':
case 'year':
case 'timestamp':
$detectedType = 'Date';
break;
case 'tinyint':
case 'smallint':
$detectedType = 'Boolean';
break;
default:
$detectedType = 'Number';
break;
}
}
// If all else fails assume it's a Number and hope for the best
if (empty($detectedType))
{
$detectedType = 'Number';
}
return $detectedType;
}
}
DataModel/Filter/Boolean.php 0000644 00000000744 15234466453 0011734 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Filter;
defined('_JEXEC') or die;
class Boolean extends Number
{
/**
* Is it a null or otherwise empty value?
*
* @param mixed $value The value to test for emptiness
*
* @return boolean
*/
public function isEmpty($value)
{
return is_null($value) || ($value === '');
}
}
DataModel/Filter/Date.php 0000644 00000011730 15234466453 0011227 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Filter;
defined('_JEXEC') or die;
class Date extends Text
{
/**
* Returns the default search method for this field.
*
* @return string
*/
public function getDefaultSearchMethod()
{
return 'exact';
}
/**
* Perform a between limits match. When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The higherst value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function between($from, $to, $include = true)
{
if ($this->isEmpty($from) || $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($from) . ') AND ';
$sql .= '(' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($to) . '))';
return $sql;
}
/**
* Perform an outside limits match. When $include is true
* the condition tested is:
* (VALUE <= $from) || (VALUE >= $to)
* When $include is false the condition tested is:
* (VALUE < $from) || (VALUE > $to)
*
* @param mixed $from The lowest value of the excluded range
* @param mixed $to The higherst value of the excluded range
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function outside($from, $to, $include = false)
{
if ($this->isEmpty($from) || $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = '((' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($from) . ') AND ';
$sql .= '(' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($to) . '))';
return $sql;
}
/**
* Interval date search
*
* @param string $value The value to search
* @param string|array|object $interval The interval. Can be (+1 MONTH or array('value' => 1, 'unit' => 'MONTH', 'sign' => '+'))
* @param boolean $include If the borders should be included
*
* @return string the sql string
*/
public function interval($value, $interval, $include = true)
{
if ($this->isEmpty($value) || $this->isEmpty($interval))
{
return '';
}
$interval = $this->getInterval($interval);
// Sanity check on $interval array
if(!isset($interval['sign']) || !isset($interval['value']) || !isset($interval['unit']))
{
return '';
}
if ($interval['sign'] == '+')
{
$function = 'DATE_ADD';
}
else
{
$function = 'DATE_SUB';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $function;
$sql .= '(' . $this->getFieldName() . ', INTERVAL ' . $interval['value'] . ' ' . $interval['unit'] . '))';
return $sql;
}
/**
* Perform a between limits match. When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The higherst value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function range($from, $to, $include = true)
{
if ($this->isEmpty($from) && $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = array();
if ($from)
{
$sql[] = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($from) . ')';
}
if ($to)
{
$sql[] = '(' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($to) . ')';
}
$sql = '(' . implode(' AND ', $sql) . ')';
return $sql;
}
/**
* Parses an interval –which may be given as a string, array or object– into
* a standardised hash array that can then be used bu the interval() method.
*
* @param string|array|object $interval The interval expression to parse
*
* @return array The parsed, hash array form of the interval
*/
protected function getInterval($interval)
{
if (is_string($interval))
{
if (strlen($interval) > 2)
{
$interval = explode(" ", $interval);
$sign = ($interval[0] == '-') ? '-' : '+';
$value = (int) substr($interval[0], 1);
$interval = array(
'unit' => $interval[1],
'value' => $value,
'sign' => $sign
);
}
else
{
$interval = array(
'unit' => 'MONTH',
'value' => 1,
'sign' => '+'
);
}
}
else
{
$interval = (array) $interval;
}
return $interval;
}
}
DataModel/Filter/Number.php 0000644 00000016031 15234466453 0011601 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Filter;
defined('_JEXEC') or die;
class Number extends AbstractFilter
{
/**
* The partial match is mapped to an exact match
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
public function partial($value)
{
return $this->exact($value);
}
/**
* Perform a between limits match. When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The higherst value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function between($from, $to, $include = true)
{
$from = (float) $from;
$to = (float) $to;
if ($this->isEmpty($from) || $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$from = $this->sanitiseValue($from);
$to = $this->sanitiseValue($to);
$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ') AND ';
$sql .= '(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . '))';
return $sql;
}
/**
* Perform an outside limits match. When $include is true
* the condition tested is:
* (VALUE <= $from) || (VALUE >= $to)
* When $include is false the condition tested is:
* (VALUE < $from) || (VALUE > $to)
*
* @param mixed $from The lowest value of the excluded range
* @param mixed $to The higherst value of the excluded range
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function outside($from, $to, $include = false)
{
$from = (float) $from;
$to = (float) $to;
if ($this->isEmpty($from) || $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$from = $this->sanitiseValue($from);
$to = $this->sanitiseValue($to);
$sql = '((' . $this->getFieldName() . ' <' . $extra . ' ' . $from . ') OR ';
$sql .= '(' . $this->getFieldName() . ' >' . $extra . ' ' . $to . '))';
return $sql;
}
/**
* Perform an interval match. It's similar to a 'between' match, but the
* from and to values are calculated based on $value and $interval:
* $value - $interval < VALUE < $value + $interval
*
* @param integer|float $value The center value of the search space
* @param integer|float $interval The width of the search space
* @param boolean $include Should I include the boundaries in the search?
*
* @return string The SQL where clause
*/
public function interval($value, $interval, $include = true)
{
if ($this->isEmpty($value))
{
return '';
}
// Convert them to float, just to be sure
$value = (float) $value;
$interval = (float) $interval;
$from = $value - $interval;
$to = $value + $interval;
$extra = '';
if ($include)
{
$extra = '=';
}
$from = $this->sanitiseValue($from);
$to = $this->sanitiseValue($to);
$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ') AND ';
$sql .= '(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . '))';
return $sql;
}
/**
* Perform a range limits match. When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The higherst value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function range($from, $to, $include = true)
{
if ($this->isEmpty($from) && $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = array();
if ($from)
{
$sql[] = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ')';
}
if ($to)
{
$sql[] = '(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . ')';
}
$sql = '(' . implode(' AND ', $sql) . ')';
return $sql;
}
/**
* Perform an interval match. It's similar to a 'between' match, but the
* from and to values are calculated based on $value and $interval:
* $value - $interval < VALUE < $value + $interval
*
* @param integer|float $value The starting value of the search space
* @param integer|float $interval The interval period of the search space
* @param boolean $include Should I include the boundaries in the search?
*
* @return string The SQL where clause
*/
public function modulo($value, $interval, $include = true)
{
if ($this->isEmpty($value) || $this->isEmpty($interval))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $value . ' AND ';
$sql .= '(' . $this->getFieldName() . ' - ' . $value . ') % ' . $interval . ' = 0)';
return $sql;
}
/**
* Overrides the parent to handle floats in locales where the decimal separator is a comma instead of a dot
*
* @param mixed $value
* @param string $operator
*
* @return string
*/
public function search($value, $operator = '=')
{
$value = $this->sanitiseValue($value);
return parent::search($value, $operator);
}
/**
* Sanitises float values. Really ugly and desperate workaround. Read below.
*
* Some locales, such as el-GR, use a comma as the decimal separator. This means that $x = 1.23; echo (string) $x;
* will yield 1,23 (with a comma!) instead of 1.23 (with a dot!). This affects the way the SQL WHERE clauses are
* generated. All database servers expect a dot as the decimal separator. If they see a decimal with a comma as the
* separator they throw a SQL error.
*
* This method will try to replace commas with dots. I tried working around this with locale switching and the %F
* (capital F) format option in sprintf to no avail. I'm pretty sure I was doing something wrong, but I ran out of
* time trying to find an academically correct solution. The current implementation of sanitiseValue is a silly
* hack around the problem. If you have a proper –and better performing– solution please send in a PR and I'll put
* it to the test.
*
* @param mixed $value A string representing a number, integer, float or array of them.
*
* @return mixed The sanitised value, or null if the input wasn't numeric.
*/
public function sanitiseValue($value)
{
if (!is_numeric($value) && !is_string($value) && !is_array($value))
{
$value = null;
}
if (!is_array($value))
{
$value = str_replace(',', '.', (string) $value);
}
else
{
$value = array_map(array($this, 'sanitiseValue'), $value);
}
return $value;
}
}
DataModel/Filter/Relation.php 0000644 00000001350 15234466453 0012124 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Filter;
defined('_JEXEC') or die;
class Relation extends Number
{
/** @var \JDatabaseQuery The COUNT subquery to filter by */
protected $subQuery = null;
public function __construct($db, $relationName, $subQuery)
{
$field = (object)array(
'name' => $relationName,
'type' => 'relation',
);
parent::__construct($db, $field);
$this->subQuery = $subQuery;
}
public function callback($value)
{
return call_user_func($value, $this->subQuery);
}
public function getFieldName()
{
return '(' . (string)$this->subQuery . ')';
}
}
DataModel/Filter/Text.php 0000644 00000006172 15234466453 0011302 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Filter;
defined('_JEXEC') or die;
class Text extends AbstractFilter
{
/**
* Constructor
*
* @param \JDatabaseDriver $db The database object
* @param object $field The field informations as taken from the db
*/
public function __construct($db, $field)
{
parent::__construct($db, $field);
$this->null_value = '';
}
/**
* Returns the default search method for this field.
*
* @return string
*/
public function getDefaultSearchMethod()
{
return 'partial';
}
/**
* Perform a partial match (search in string)
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
public function partial($value)
{
if ($this->isEmpty($value))
{
return '';
}
return '(' . $this->getFieldName() . ' LIKE ' . $this->db->quote('%' . $value . '%') . ')';
}
/**
* Perform an exact match (match string)
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
public function exact($value)
{
if ($this->isEmpty($value))
{
return '';
}
if (is_array($value) || is_object($value))
{
settype($value, 'array');
$db = $this->db;
$value = array_map(array($db, 'quote'), $value);
return '(' . $this->getFieldName() . ' IN (' . implode(',', $value) . '))';
}
return '(' . $this->getFieldName() . ' LIKE ' . $this->db->quote($value) . ')';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $from Ignored
* @param mixed $to Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function between($from, $to, $include = true)
{
return '';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $from Ignored
* @param mixed $to Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function outside($from, $to, $include = false)
{
return '';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $value Ignored
* @param mixed $interval Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function interval($value, $interval, $include = true)
{
return '';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $from Ignored
* @param mixed $to Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function range($from, $to, $include = false)
{
return '';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $from Ignored
* @param mixed $interval Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function modulo($from, $interval, $include = false)
{
return '';
}
}
DataModel/Relation/Exception/ForeignModelNotFound.php 0000644 00000000435 15234466453 0016667 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation\Exception;
defined('_JEXEC') or die;
class ForeignModelNotFound extends \Exception {}
DataModel/Relation/Exception/NewNotSupported.php 0000644 00000000431 15234466453 0015754 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation\Exception;
defined('_JEXEC') or die;
class NewNotSupported extends \Exception
{
}
DataModel/Relation/Exception/PivotTableNotFound.php 0000644 00000000433 15234466453 0016364 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation\Exception;
defined('_JEXEC') or die;
class PivotTableNotFound extends \Exception {}
DataModel/Relation/Exception/RelationNotFound.php 0000644 00000000431 15234466453 0016066 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation\Exception;
defined('_JEXEC') or die;
class RelationNotFound extends \Exception {}
DataModel/Relation/Exception/RelationTypeNotFound.php 0000644 00000000435 15234466453 0016734 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation\Exception;
defined('_JEXEC') or die;
class RelationTypeNotFound extends \Exception {}
DataModel/Relation/Exception/SaveNotSupported.php 0000644 00000000431 15234466453 0016121 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation\Exception;
defined('_JEXEC') or die;
class SaveNotSupported extends \Exception {}
DataModel/Relation/BelongsTo.php 0000644 00000004600 15234466453 0012574 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation;
use FOF30\Model\DataModel;
defined('_JEXEC') or die;
/**
* BelongsTo (reverse 1-to-1 or 1-to-many) relation: this model is a child which belongs to the foreign table
*
* For example, parentModel is Articles and foreignModel is Users. Each article belongs to one user. One user can have
* one or more article.
*
* Example #2: parentModel is Phones and foreignModel is Users. Each phone belongs to one user. One user can have zero
* or one phones.
*/
class BelongsTo extends HasOne
{
/**
* Public constructor. Initialises the relation.
*
* @param DataModel $parentModel The data model we are attached to
* @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something"
* @param string $localKey The local table key for this relation, default: parentModel's ID field name
* @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name
* @param string $pivotTable IGNORED
* @param string $pivotLocalKey IGNORED
* @param string $pivotForeignKey IGNORED
*/
public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);
if (empty($localKey))
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$this->localKey = $foreignModel->getIdFieldName();
}
if (empty($foreignKey))
{
if (!isset($foreignModel))
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
}
$this->foreignKey = $foreignModel->getIdFieldName();
}
}
/**
* This is not supported by the belongsTo relation
*
* @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
*/
public function getNew()
{
throw new DataModel\Relation\Exception\NewNotSupported("getNew() is not supported by the belongsTo relation type");
}
}
DataModel/Relation/BelongsToMany.php 0000644 00000026602 15234466453 0013427 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation;
use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Relation;
defined('_JEXEC') or die;
/**
* BelongsToMany (many-to-many) relation: one or more records of this model are related to one or more records in the
* foreign model.
*
* For example, parentModel is Users and foreignModel is Groups. Each user can be assigned to many groups. Each group
* can be assigned to many users.
*/
class BelongsToMany extends Relation
{
/**
* Public constructor. Initialises the relation.
*
* @param DataModel $parentModel The data model we are attached to
* @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something"
* @param string $localKey The local table key for this relation, default: parentModel's ID field name
* @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name
* @param string $pivotTable For many-to-many relations, the pivot (glue) table
* @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local key
* @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign key
*
* @throws DataModel\Relation\Exception\PivotTableNotFound
*/
public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);
if (empty($localKey))
{
$this->localKey = $parentModel->getIdFieldName();
}
if (empty($pivotLocalKey))
{
$this->pivotLocalKey = $this->localKey;
}
if (empty($foreignKey))
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$this->foreignKey = $foreignModel->getIdFieldName();
}
if (empty($pivotForeignKey))
{
$this->pivotForeignKey = $this->foreignKey;
}
if (empty($pivotTable))
{
// Get the local model's name (e.g. "users")
$localName = $parentModel->getName();
$localName = strtolower($localName);
// Get the foreign model's name (e.g. "groups")
if (!isset($foreignModel))
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
}
$foreignName = $foreignModel->getName();
$foreignName = strtolower($foreignName);
// Get the local model's app name
$parentModelBareComponent = $parentModel->getContainer()->bareComponentName;
$foreignModelBareComponent = $foreignModel->getContainer()->bareComponentName;
// There are two possibilities for the table name: #__component_local_foreign or #__component_foreign_local.
// There are also two possibilities for a component name (local or foreign model's)
$db = $parentModel->getDbo();
$prefix = $db->getPrefix();
$tableNames = array(
'#__' . strtolower($parentModelBareComponent) . '_' . $localName . '_' . $foreignName,
'#__' . strtolower($parentModelBareComponent) . '_' . $foreignName . '_' . $localName,
'#__' . strtolower($foreignModelBareComponent) . '_' . $localName . '_' . $foreignName,
'#__' . strtolower($foreignModelBareComponent) . '_' . $foreignName . '_' . $localName,
);
$allTables = $db->getTableList();
$this->pivotTable = null;
foreach ($tableNames as $tableName)
{
$checkName = $prefix . substr($tableName, 3);
if (in_array($checkName, $allTables))
{
$this->pivotTable = $tableName;
}
}
if (empty($this->pivotTable))
{
throw new DataModel\Relation\Exception\PivotTableNotFound("Pivot table for many-to-many relation between '$localName and '$foreignName' not found'");
}
}
}
/**
* Populates the internal $this->data collection from the contents of the provided collection. This is used by
* DataModel to push the eager loaded data into each item's relation.
*
* @param DataModel\Collection $data The relation data to push into this relation
* @param mixed $keyMap Passes around the local to foreign key map
*
* @return void
*/
public function setDataFromCollection(DataModel\Collection &$data, $keyMap = null)
{
$this->data = new DataModel\Collection();
if (!is_array($keyMap))
{
return;
}
if (!empty($data))
{
// Get the local key value
$localKeyValue = $this->parentModel->getFieldValue($this->localKey);
// Make sure this local key exists in the (cached) pivot table
if (!isset($keyMap[$localKeyValue]))
{
return;
}
/** @var DataModel $item */
foreach ($data as $key => $item)
{
// Only accept foreign items whose key is associated in the pivot table with our local key
if (in_array($item->getFieldValue($this->foreignKey), $keyMap[$localKeyValue]))
{
$this->data->add($item);
}
}
}
}
/**
* Applies the relation filters to the foreign model when getData is called
*
* @param DataModel $foreignModel The foreign model you're operating on
* @param DataModel\Collection $dataCollection If it's an eager loaded relation, the collection of loaded parent records
*
* @return boolean Return false to force an empty data collection
*/
protected function filterForeignModel(DataModel $foreignModel, DataModel\Collection $dataCollection = null)
{
$db = $this->parentModel->getDbo();
// Decide how to proceed, based on eager or lazy loading
if (is_object($dataCollection))
{
// Eager loaded relation
if (!empty($dataCollection))
{
// Get a list of local keys from the collection
$values = array();
/** @var $item DataModel */
foreach ($dataCollection as $item)
{
$v = $item->getFieldValue($this->localKey, null);
if (!is_null($v))
{
$values[] = $v;
}
}
// Keep only unique values
$values = array_unique($values);
$values = array_map(function ($x) use (&$db)
{
return $db->q($x);
}, $values);
// Get the foreign keys from the glue table
$query = $db->getQuery(true)
->select(array($db->qn($this->pivotLocalKey), $db->qn($this->pivotForeignKey)))
->from($db->qn($this->pivotTable))
->where($db->qn($this->pivotLocalKey) . ' IN(' . implode(',', $values) . ')');
$db->setQuery($query);
$foreignKeysUnmapped = $db->loadRowList();
$this->foreignKeyMap = array();
$foreignKeys = array();
foreach ($foreignKeysUnmapped as $unmapped)
{
$local = $unmapped[0];
$foreign = $unmapped[1];
if (!isset($this->foreignKeyMap[$local]))
{
$this->foreignKeyMap[$local] = array();
}
$this->foreignKeyMap[$local][] = $foreign;
$foreignKeys[] = $foreign;
}
// Keep only unique values. However, the array keys are all screwed up. See below.
$foreignKeys = array_unique($foreignKeys);
// This looks stupid, but it's required to reset the array keys. Without it where() below fails.
$foreignKeys = array_merge($foreignKeys);
// Apply the filter
if (!empty($foreignKeys))
{
$foreignModel->where($this->foreignKey, 'in', $foreignKeys);
}
else
{
return false;
}
}
else
{
return false;
}
}
else
{
// Lazy loaded relation; get the single local key
$localKey = $this->parentModel->getFieldValue($this->localKey, null);
if (is_null($localKey) || ($localKey === ''))
{
return false;
}
$query = $db->getQuery(true)
->select($db->qn($this->pivotForeignKey))
->from($db->qn($this->pivotTable))
->where($db->qn($this->pivotLocalKey) . ' = ' . $db->q($localKey));
$db->setQuery($query);
$foreignKeys = $db->loadColumn();
$this->foreignKeyMap[$localKey] = $foreignKeys;
// If there are no foreign keys (no foreign items assigned to our item) we return false which then causes
// the relation to return null, marking the lack of data.
if (empty($foreignKeys))
{
return false;
}
$foreignModel->where($this->foreignKey, 'in', $this->foreignKeyMap[$localKey]);
}
return true;
}
/**
* Returns the count subquery for DataModel's has() and whereHas() methods.
*
* @param string $tableAlias The alias of the local table in the query. Leave blank to use the table's name.
*
* @return \JDatabaseQuery
*/
public function getCountSubquery($tableAlias = null)
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$db = $foreignModel->getDbo();
if (empty($tableAlias))
{
$tableAlias = $this->parentModel->getTableName();
}
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->qn($foreignModel->getTableName()) . ' AS ' . $db->qn('reltbl'))
->innerJoin(
$db->qn($this->pivotTable) . ' AS ' . $db->qn('pivotTable') . ' ON('
. $db->qn('pivotTable') . '.' . $db->qn($this->pivotForeignKey) . ' = '
. $db->qn('reltbl') . '.' . $db->qn($foreignModel->getFieldAlias($this->foreignKey))
. ')'
)
->where(
$db->qn('pivotTable') . '.' . $db->qn($this->pivotLocalKey) . ' ='
. $db->qn($tableAlias) . '.'
. $db->qn($this->parentModel->getFieldAlias($this->localKey))
);
return $query;
}
/**
* Saves all related items. For many-to-many relations there are two things we have to do:
* 1. Save all related items; and
* 2. Overwrite the pivot table data with the new associations
*/
public function saveAll()
{
// Save all related items
parent::saveAll();
$this->saveRelations();
}
/**
* Overwrite the pivot table data with the new associations
*/
public function saveRelations()
{
// Get all the new keys
$newKeys = array();
if ($this->data instanceof DataModel\Collection)
{
foreach ($this->data as $item)
{
if ($item instanceof DataModel)
{
$newKeys[] = $item->getId();
}
elseif (!is_object($item))
{
$newKeys[] = $item;
}
}
}
$newKeys = array_unique($newKeys);
$db = $this->parentModel->getDbo();
$localKeyValue = $this->parentModel->getFieldValue($this->localKey);
// Kill all existing relations in the pivot table
$query = $db->getQuery(true)
->delete($db->qn($this->pivotTable))
->where($db->qn($this->pivotLocalKey) . ' = ' . $db->q($localKeyValue));
$db->setQuery($query);
$db->execute();
// Write the new relations to the database
$protoQuery = $db->getQuery(true)
->insert($db->qn($this->pivotTable))
->columns(array($db->qn($this->pivotLocalKey), $db->qn($this->pivotForeignKey)));
$i = 0;
$query = null;
foreach ($newKeys as $key)
{
$i++;
if (is_null($query))
{
$query = clone $protoQuery;
}
$query->values($db->q($localKeyValue) . ', ' . $db->q($key));
if (($i % 50) == 0)
{
$db->setQuery($query);
$db->execute();
$query = null;
}
}
if (!is_null($query))
{
$db->setQuery($query);
$db->execute();
}
}
/**
* This is not supported by the belongsTo relation
*
* @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
*/
public function getNew()
{
throw new DataModel\Relation\Exception\NewNotSupported("getNew() is not supported for many-to-may relations. Please add/remove items from the relation data and use push() to effect changes.");
}
}
DataModel/Relation/HasMany.php 0000644 00000011174 15234466453 0012244 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation;
use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Relation;
defined('_JEXEC') or die;
/**
* HasMany (1-to-many) relation: this model is a parent which has zero or more children in the foreign table
*
* For example, parentModel is Users and foreignModel is Articles. Each user has zero or more articles.
*/
class HasMany extends Relation
{
/**
* Public constructor. Initialises the relation.
*
* @param DataModel $parentModel The data model we are attached to
* @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something"
* @param string $localKey The local table key for this relation, default: parentModel's ID field name
* @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name
* @param string $pivotTable IGNORED
* @param string $pivotLocalKey IGNORED
* @param string $pivotForeignKey IGNORED
*/
public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);
if (empty($this->localKey))
{
$this->localKey = $parentModel->getIdFieldName();
}
if (empty($this->foreignKey))
{
$this->foreignKey = $this->localKey;
}
}
/**
* Applies the relation filters to the foreign model when getData is called
*
* @param DataModel $foreignModel The foreign model you're operating on
* @param DataModel\Collection $dataCollection If it's an eager loaded relation, the collection of loaded parent records
*
* @return boolean Return false to force an empty data collection
*/
protected function filterForeignModel(DataModel $foreignModel, DataModel\Collection $dataCollection = null)
{
// Decide how to proceed, based on eager or lazy loading
if (is_object($dataCollection))
{
// Eager loaded relation
if (!empty($dataCollection))
{
// Get a list of local keys from the collection
$values = array();
/** @var $item DataModel */
foreach ($dataCollection as $item)
{
$v = $item->getFieldValue($this->localKey, null);
if (!is_null($v))
{
$values[] = $v;
}
}
// Keep only unique values
$values = array_unique($values);
// Apply the filter
if (!empty($values))
{
$foreignModel->where($this->foreignKey, 'in', $values);
}
else
{
return false;
}
}
else
{
return false;
}
}
else
{
// Lazy loaded relation; get the single local key
$localKey = $this->parentModel->getFieldValue($this->localKey, null);
if (is_null($localKey) || ($localKey === ''))
{
return false;
}
$foreignModel->where($this->foreignKey, '==', $localKey);
}
return true;
}
/**
* Returns the count subquery for DataModel's has() and whereHas() methods.
*
* @param string $tableAlias The alias of the local table in the query. Leave blank to use the table's name.
*
* @return \JDatabaseQuery
*/
public function getCountSubquery($tableAlias = null)
{
// Get a model instance
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$db = $foreignModel->getDbo();
if (empty($tableAlias))
{
$tableAlias = $this->parentModel->getTableName();
}
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->qn($foreignModel->getTableName(), 'reltbl'))
->where($db->qn('reltbl') . '.' . $db->qn($foreignModel->getFieldAlias($this->foreignKey)) . ' = '
. $db->qn($tableAlias) . '.'
. $db->qn($this->parentModel->getFieldAlias($this->localKey)));
return $query;
}
/**
* Returns a new item of the foreignModel type, pre-initialised to fulfil this relation
*
* @return DataModel
*
* @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
*/
public function getNew()
{
// Get a model instance
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
// Prime the model
$foreignModel->setFieldValue($this->foreignKey, $this->parentModel->getFieldValue($this->localKey));
// Make sure we do have a data list
if (!($this->data instanceof DataModel\Collection))
{
$this->getData();
}
// Add the model to the data list
$this->data->add($foreignModel);
return $this->data->last();
}
}
DataModel/Relation/HasOne.php 0000644 00000002532 15234466453 0012057 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel\Relation;
use FOF30\Model\DataModel;
use FOF30\Model\DataModel\Collection;
defined('_JEXEC') or die;
/**
* HasOne (straight 1-to-1) relation: this model is a parent which has exactly one child in the foreign table
*
* For example, parentModel is Users and foreignModel is Phones. Each uses has exactly one Phone.
*/
class HasOne extends HasMany
{
/**
* Get the relation data.
*
* If you want to apply additional filtering to the foreign model, use the $callback. It can be any function,
* static method, public method or closure with an interface of function(DataModel $foreignModel). You are not
* supposed to return anything, just modify $foreignModel's state directly. For example, you may want to do:
* $foreignModel->setState('foo', 'bar')
*
* @param callable $callback The callback to run on the remote model.
* @param Collection $dataCollection
*
* @return Collection|DataModel
*/
public function getData($callback = null, Collection $dataCollection = null)
{
if (is_null($dataCollection))
{
return parent::getData($callback, $dataCollection)->first();
}
else
{
return parent::getData($callback, $dataCollection);
}
}
}
DataModel/Collection.php 0000644 00000015340 15234466453 0011221 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel;
use FOF30\Model\DataModel;
use FOF30\Utils\Collection as BaseCollection;
defined('_JEXEC') or die;
/**
* A collection of data models. You can enumerate it like an array, use it everywhere a collection is expected (e.g. a
* foreach loop) and even implements a countable interface. You can also batch-apply DataModel methods on it thanks to
* its magic __call() method, hence the type-hinting below.
*
* @method void setFieldValue(string $name, mixed $value = '')
* @method void archive()
* @method void save(mixed $data, string $orderingFilter = '', bool $ignore = null)
* @method void push(mixed $data, string $orderingFilter = '', bool $ignore = null, array $relations = null)
* @method void bind(mixed $data, array $ignore = array())
* @method void check()
* @method void reorder(string $where = '')
* @method void delete(mixed $id = null)
* @method void trash(mixed $id)
* @method void forceDelete(mixed $id = null)
* @method void lock(int $userId = null)
* @method void move(int $delta, string $where = '')
* @method void publish()
* @method void restore(mixed $id)
* @method void touch(int $userId = null)
* @method void unlock()
* @method void unpublish()
*/
class Collection extends BaseCollection
{
/**
* Find a model in the collection by key.
*
* @param mixed $key
* @param mixed $default
*
* @return DataModel
*/
public function find($key, $default = null)
{
if ($key instanceof DataModel)
{
$key = $key->getId();
}
return array_first($this->items, function($itemKey, $model) use ($key)
{
/** @var DataModel $model */
return $model->getId() == $key;
}, $default);
}
/**
* Remove an item in the collection by key
*
* @param mixed $key
*
* @return void
*/
public function removeById($key)
{
if ($key instanceof DataModel)
{
$key = $key->getId();
}
$index = array_search($key, $this->modelKeys());
if ($index !== false)
{
unset($this->items[$index]);
}
}
/**
* Add an item to the collection.
*
* @param mixed $item
*
* @return Collection
*/
public function add($item)
{
$this->items[] = $item;
return $this;
}
/**
* Determine if a key exists in the collection.
*
* @param mixed $key
*
* @return bool
*/
public function contains($key)
{
return ! is_null($this->find($key));
}
/**
* Fetch a nested element of the collection.
*
* @param string $key
*
* @return Collection
*/
public function fetch($key)
{
return new static(array_fetch($this->toArray(), $key));
}
/**
* Get the max value of a given key.
*
* @param string $key
*
* @return mixed
*/
public function max($key)
{
return $this->reduce(function($result, $item) use ($key)
{
return (is_null($result) || $item->{$key} > $result) ? $item->{$key} : $result;
});
}
/**
* Get the min value of a given key.
*
* @param string $key
*
* @return mixed
*/
public function min($key)
{
return $this->reduce(function($result, $item) use ($key)
{
return (is_null($result) || $item->{$key} < $result) ? $item->{$key} : $result;
});
}
/**
* Get the array of primary keys
*
* @return array
*/
public function modelKeys()
{
return array_map(
function($m) {
/** @var DataModel $m */
return $m->getId();
},
$this->items);
}
/**
* Merge the collection with the given items.
*
* @param BaseCollection|array $collection
*
* @return BaseCollection
*/
public function merge($collection)
{
$dictionary = $this->getDictionary($this);
foreach ($collection as $item)
{
$dictionary[$item->getId()] = $item;
}
return new static(array_values($dictionary));
}
/**
* Diff the collection with the given items.
*
* @param BaseCollection|array $collection
*
* @return BaseCollection
*/
public function diff($collection)
{
$diff = new static;
$dictionary = $this->getDictionary($collection);
foreach ($this->items as $item)
{
/** @var DataModel $item */
if ( ! isset($dictionary[$item->getId()]))
{
$diff->add($item);
}
}
return $diff;
}
/**
* Intersect the collection with the given items.
*
* @param BaseCollection|array $collection
*
* @return Collection
*/
public function intersect($collection)
{
$intersect = new static;
$dictionary = $this->getDictionary($collection);
foreach ($this->items as $item)
{
/** @var DataModel $item */
if (isset($dictionary[$item->getId()]))
{
$intersect->add($item);
}
}
return $intersect;
}
/**
* Return only unique items from the collection.
*
* @return BaseCollection
*/
public function unique()
{
$dictionary = $this->getDictionary($this);
return new static(array_values($dictionary));
}
/**
* Get a dictionary keyed by primary keys.
*
* @param BaseCollection $collection
*
* @return array
*/
protected function getDictionary($collection)
{
$dictionary = array();
foreach ($collection as $value)
{
$dictionary[$value->getId()] = $value;
}
return $dictionary;
}
/**
* Get a base Support collection instance from this collection.
*
* @return BaseCollection
*/
public function toBase()
{
return new BaseCollection($this->items);
}
/**
* Magic method which allows you to run a DataModel method to all items in the collection.
*
* For example, you can do $collection->save('foobar' => 1) to update the 'foobar' column to 1 across all items in
* the collection.
*
* IMPORTANT: The return value of the method call is not returned back to you!
*
* @param string $name The method to call
* @param array $arguments The arguments to the method
*/
public function __call($name, $arguments)
{
if (!count($this))
{
return;
}
$class = get_class($this->first());
if (method_exists($class, $name))
{
foreach ($this as $item)
{
switch (count($arguments))
{
case 0:
$item->$name();
break;
case 1:
$item->$name($arguments[0]);
break;
case 2:
$item->$name($arguments[0], $arguments[1]);
break;
case 3:
$item->$name($arguments[0], $arguments[1], $arguments[2]);
break;
case 4:
$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
break;
case 5:
$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
break;
case 6:
$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]);
break;
default:
call_user_func_array(array($item, $name), $arguments);
break;
}
}
}
}
}
DataModel/Relation.php 0000644 00000017611 15234466453 0010706 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel;
use FOF30\Container\Container;
use FOF30\Model\DataModel;
defined('_JEXEC') or die;
abstract class Relation
{
/** @var DataModel The data model we are attached to */
protected $parentModel = null;
/** @var string The class name of the foreign key's model */
protected $foreignModelClass = null;
/** @var string The application name of the foreign model */
protected $foreignModelComponent = null;
/** @var string The bade name of the foreign model */
protected $foreignModelName = null;
/** @var string The local table key for this relation */
protected $localKey = null;
/** @var string The foreign table key for this relation */
protected $foreignKey = null;
/** @var null For many-to-many relations, the pivot (glue) table */
protected $pivotTable = null;
/** @var null For many-to-many relations, the pivot table's column storing the local key */
protected $pivotLocalKey = null;
/** @var null For many-to-many relations, the pivot table's column storing the foreign key */
protected $pivotForeignKey = null;
/** @var Collection The data loaded by this relation */
protected $data = null;
/** @var array Maps each local table key to an array of foreign table keys, used in many-to-many relations */
protected $foreignKeyMap = array();
/** @var Container The component container for this relation */
protected $container = null;
/**
* Public constructor. Initialises the relation.
*
* @param DataModel $parentModel The data model we are attached to
* @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something"
* @param string $localKey The local table key for this relation
* @param string $foreignKey The foreign key for this relation
* @param string $pivotTable For many-to-many relations, the pivot (glue) table
* @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local key
* @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign key
*/
public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
$this->parentModel = $parentModel;
$this->foreignModelClass = $foreignModelName;
$this->localKey = $localKey;
$this->foreignKey = $foreignKey;
$this->pivotTable = $pivotTable;
$this->pivotLocalKey = $pivotLocalKey;
$this->pivotForeignKey = $pivotForeignKey;
$this->container = $parentModel->getContainer();
$class = $foreignModelName;
if (strpos($class, '@') === false)
{
$this->foreignModelComponent = null;
$this->foreignModelName = $class;
}
else
{
$foreignParts = explode('@', $class, 2);
$this->foreignModelComponent = $foreignParts[1];
$this->foreignModelName = $foreignParts[0];
}
}
/**
* Reset the relation data
*
* @return $this For chaining
*/
public function reset()
{
$this->data = null;
$this->foreignKeyMap = array();
return $this;
}
/**
* Rebase the relation to a different model
*
* @param DataModel $model
*
* @return $this For chaining
*/
public function rebase(DataModel $model)
{
$this->parentModel = $model;
return $this->reset();
}
/**
* Get the relation data.
*
* If you want to apply additional filtering to the foreign model, use the $callback. It can be any function,
* static method, public method or closure with an interface of function(DataModel $foreignModel). You are not
* supposed to return anything, just modify $foreignModel's state directly. For example, you may want to do:
* $foreignModel->setState('foo', 'bar')
*
* @param callable $callback The callback to run on the remote model.
* @param Collection $dataCollection
*
* @return Collection|DataModel
*/
public function getData($callback = null, Collection $dataCollection = null)
{
if (is_null($this->data))
{
// Initialise
$this->data = new Collection();
// Get a model instance
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$filtered = $this->filterForeignModel($foreignModel, $dataCollection);
if (!$filtered)
{
return $this->data;
}
// Apply the callback, if applicable
if (!is_null($callback) && is_callable($callback))
{
call_user_func($callback, $foreignModel);
}
// Get the list of items from the foreign model and cache in $this->data
$this->data = $foreignModel->get(true);
}
return $this->data;
}
/**
* Populates the internal $this->data collection from the contents of the provided collection. This is used by
* DataModel to push the eager loaded data into each item's relation.
*
* @param Collection $data The relation data to push into this relation
* @param mixed $keyMap Used by many-to-many relations to pass around the local to foreign key map
*
* @return void
*/
public function setDataFromCollection(Collection &$data, $keyMap = null)
{
$this->data = new Collection();
if (!empty($data))
{
$localKeyValue = $this->parentModel->getFieldValue($this->localKey);
/** @var DataModel $item */
foreach ($data as $key => $item)
{
if ($item->getFieldValue($this->foreignKey) == $localKeyValue)
{
$this->data->add($item);
}
}
}
}
/**
* Applies the relation filters to the foreign model when getData is called
*
* @param DataModel $foreignModel The foreign model you're operating on
* @param Collection $dataCollection If it's an eager loaded relation, the collection of loaded parent records
*
* @return boolean Return false to force an empty data collection
*/
abstract protected function filterForeignModel(DataModel $foreignModel, Collection $dataCollection = null);
/**
* Returns the count subquery for DataModel's has() and whereHas() methods.
*
* @return \JDatabaseQuery
*/
abstract public function getCountSubquery();
/**
* Returns a new item of the foreignModel type, pre-initialised to fulfil this relation
*
* @return DataModel
*
* @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
*/
abstract public function getNew();
/**
* Saves all related items. You can use it to touch items as well: every item being saved causes the modified_by and
* modified_on fields to be changed automatically, thanks to the DataModel's magic.
*/
public function saveAll()
{
if ($this->data instanceof Collection)
{
foreach ($this->data as $item)
{
if ($item instanceof DataModel)
{
$item->save();
}
}
}
}
/**
* Returns the foreign key map of a many-to-many relation, used for eager loading many-to-many relations
*
* @return array
*/
public function &getForeignKeyMap()
{
return $this->foreignKeyMap;
}
/**
* Gets an object instance of the foreign model
*
* @param array $config Optional configuration information for the Model
*
* @return DataModel
*/
public function &getForeignModel(array $config = array())
{
// If the model comes from this component go through our Factory
if (is_null($this->foreignModelComponent))
{
$model = $this->container->factory->model($this->foreignModelName, $config)->tmpInstance();
return $model;
}
// The model comes from another component. Create a container and go through its factory.
$foreignContainer = Container::getInstance($this->foreignModelComponent, array('tempInstance' => true));
$model = $foreignContainer->factory->model($this->foreignModelName, $config)->tmpInstance();
return $model;
}
/**
* Returns the name of the local key of the relation
*
* @return string
*/
public function getLocalKey()
{
return $this->localKey;
}
}
DataModel/RelationManager.php 0000644 00000033102 15234466453 0012172 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\DataModel;
use FOF30\Inflector\Inflector;
use FOF30\Model\DataModel;
defined('_JEXEC') or die;
class RelationManager
{
/** @var DataModel The data model we are attached to */
protected $parentModel = null;
/** @var Relation[] The relations known to us */
protected $relations = array();
/** @var array A list of the names of eager loaded relations */
protected $eager = array();
/** @var array The known relation types */
protected static $relationTypes = array();
/**
* Creates a new relation manager for the defined parent model
*
* @param DataModel $parentModel The model we are attached to
*/
public function __construct(DataModel $parentModel)
{
// Set the parent model
$this->parentModel = $parentModel;
// Make sure the relation types are initialised
static::getRelationTypes();
// @todo Maybe set up a few relations automatically?
}
/**
* Implements deep cloning of the relation object
*/
function __clone()
{
$relations = array();
if (!empty($this->relations))
{
/** @var Relation[] $relations */
foreach ($this->relations as $key => $relation)
{
$relations[$key] = clone($relation);
$relations[$key]->reset();
}
}
$this->relations = $relations;
}
/**
* Rebase a relation manager
*
* @param DataModel $parentModel
*/
public function rebase(DataModel $parentModel)
{
$this->parentModel = $parentModel;
if (count($this->relations))
{
foreach ($this->relations as $name => $relation)
{
/** @var Relation $relation */
$relation->rebase($parentModel);
}
}
}
/**
* Populates the internal $this->data collection of a relation from the contents of the provided collection. This is
* used by DataModel to push the eager loaded data into each item's relation.
*
* @param string $name Relation name
* @param Collection $data The relation data to push into this relation
* @param mixed $keyMap Used by many-to-many relations to pass around the local to foreign key map
*
* @return void
*
* @throws Relation\Exception\RelationNotFound
*/
public function setDataFromCollection($name, Collection &$data, $keyMap = null)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
$this->relations[$name]->setDataFromCollection($data, $keyMap);
}
/**
* Populates the static map of relation type methods and relation handling classes
*
* @return array Key = method name, Value = relation handling class
*/
public static function getRelationTypes()
{
if (empty(static::$relationTypes))
{
$relationTypeDirectory = __DIR__ . '/Relation';
$fs = new \DirectoryIterator($relationTypeDirectory);
/** @var $file \DirectoryIterator */
foreach ($fs as $file)
{
if ($file->isDir())
{
continue;
}
if ($file->getExtension() != 'php')
{
continue;
}
$baseName = ucfirst($file->getBasename('.php'));
$methodName = strtolower($baseName[0]) . substr($baseName, 1);
$className = '\\FOF30\\Model\\DataModel\\Relation\\' . $baseName;
if (!class_exists($className, true))
{
continue;
}
static::$relationTypes[$methodName] = $className;
}
}
return static::$relationTypes;
}
/**
* Adds a relation to the relation manager
*
* @param string $name The name of the relation as known to this relation manager, e.g. 'phone'
* @param string $type The relation type, e.g. 'hasOne'
* @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something"
* @param string $localKey The local table key for this relation
* @param string $foreignKey The foreign key for this relation
* @param string $pivotTable For many-to-many relations, the pivot (glue) table
* @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local key
* @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign key
*
* @return DataModel The parent model, for chaining
*
* @throws Relation\Exception\RelationTypeNotFound when $type is not known
* @throws Relation\Exception\ForeignModelNotFound when $foreignModelClass doesn't exist
*/
public function addRelation($name, $type, $foreignModelName = null, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
if (!isset(static::$relationTypes[$type]))
{
throw new DataModel\Relation\Exception\RelationTypeNotFound("Relation type '$type' not found");
}
// Guess the foreign model class if necessary
if (empty($foreignModelName))
{
$foreignModelName = ucfirst($name);
}
$className = static::$relationTypes[$type];
/** @var Relation $relation */
$relation = new $className($this->parentModel, $foreignModelName, $localKey, $foreignKey,
$pivotTable, $pivotLocalKey, $pivotForeignKey);
$this->relations[$name] = $relation;
return $this->parentModel;
}
/**
* Removes a known relation
*
* @param string $name The name of the relation to remove
*
* @return DataModel The parent model, for chaining
*/
public function removeRelation($name)
{
if (isset($this->relations[$name]))
{
unset ($this->relations[$name]);
}
return $this->parentModel;
}
/**
* Removes all known relations
*/
public function resetRelations()
{
$this->relations = array();
}
/**
* Resets the data of all relations in this manager. This doesn't remove relations, just their data so that they
* get loaded again.
*
* @param array $relationsToReset The names of the relations to reset. Pass an empty array (default) to reset
* all relations.
*/
public function resetRelationData(array $relationsToReset = array())
{
/** @var Relation $relation */
foreach ($this->relations as $name => $relation)
{
if (!empty($relationsToReset) && !in_array($name, $relationsToReset))
{
continue;
}
$relation->reset();
}
}
/**
* Returns a list of all known relations' names
*
* @return array
*/
public function getRelationNames()
{
return array_keys($this->relations);
}
/**
* Gets the related items of a relation
*
* @param string $name The name of the relation to return data for
*
* @return Relation
*
* @throws Relation\Exception\RelationNotFound
*/
public function &getRelation($name)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name];
}
/**
* Get a new related item which satisfies relation $name and adds it to this relation's data list.
*
* @param string $name The relation based on which a new item is returned
*
* @return DataModel
*
* @throws Relation\Exception\RelationNotFound
*/
public function getNew($name)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name]->getNew();
}
/**
* Saves all related items belonging to the specified relation or, if $name is null, all known relations which
* support saving.
*
* @param null|string $name The relation to save, or null to save all known relations
*
* @return DataModel The parent model, for chaining
*
* @throws Relation\Exception\RelationNotFound
*/
public function save($name = null)
{
if (is_null($name))
{
foreach ($this->relations as $name => $relation)
{
try
{
$relation->saveAll();
}
catch (DataModel\Relation\Exception\SaveNotSupported $e)
{
// We don't care if a relation doesn't support saving
}
}
}
else
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
$this->relations[$name]->saveAll();
}
return $this->parentModel;
}
/**
* Gets the related items of a relation
*
* @param string $name The name of the relation to return data for
* @param callable $callback A callback to customise the returned data
* @param \FOF30\Utils\Collection $dataCollection Used when fetching the data of an eager loaded relation
*
* @see Relation::getData()
*
* @return Collection|DataModel
*
* @throws Relation\Exception\RelationNotFound
*/
public function getData($name, $callback = null, \FOF30\Utils\Collection $dataCollection = null)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name]->getData($callback, $dataCollection);
}
/**
* Gets the foreign key map of a many-to-many relation
*
* @param string $name The name of the relation to return data for
*
* @return array
*
* @throws Relation\Exception\RelationNotFound
*/
public function &getForeignKeyMap($name)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name]->getForeignKeyMap();
}
/**
* Returns the count sub-query for a relation, used for relation filters (whereHas in the DataModel).
*
* @param string $name The relation to get the sub-query for
* @param string $tableAlias The alias to use for the local table
*
* @return \JDatabaseQuery
* @throws Relation\Exception\RelationNotFound
*/
public function getCountSubquery($name, $tableAlias = null)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name]->getCountSubquery($tableAlias);
}
/**
* A magic method which allows us to define relations using shorthand notation, e.g. $manager->hasOne('phone')
* instead of $manager->addRelation('phone', 'hasOne')
*
* You can also use it to get data of a relation using shorthand notation, e.g. $manager->getPhone($callback)
* instead of $manager->getData('phone', $callback);
*
* @param string $name The magic method to call
* @param array $arguments The arguments to the magic method
*
* @return DataModel The parent model, for chaining
*
* @throws \InvalidArgumentException
* @throws DataModel\Relation\Exception\RelationTypeNotFound
*/
function __call($name, $arguments)
{
$numberOfArguments = count($arguments);
if (isset(static::$relationTypes[$name]))
{
if ($numberOfArguments == 1)
{
return $this->addRelation($arguments[0], $name);
}
elseif ($numberOfArguments == 2)
{
return $this->addRelation($arguments[0], $name, $arguments[1]);
}
elseif ($numberOfArguments == 3)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2]);
}
elseif ($numberOfArguments == 4)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3]);
}
elseif ($numberOfArguments == 5)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
}
elseif ($numberOfArguments == 6)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]);
}
elseif ($numberOfArguments >= 7)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]);
}
else
{
throw new \InvalidArgumentException("You can not create an unnamed '$name' relation");
}
}
elseif (substr($name, 0, 3) == 'get')
{
$relationName = substr($name, 3);
$relationName = strtolower($relationName[0]) . substr($relationName, 1);
if ($numberOfArguments == 0)
{
return $this->getData($relationName);
}
elseif ($numberOfArguments == 1)
{
return $this->getData($relationName, $arguments[0]);
}
elseif ($numberOfArguments == 2)
{
return $this->getData($relationName, $arguments[0], $arguments[1]);
}
else
{
throw new \InvalidArgumentException("Invalid number of arguments getting data for the '$relationName' relation");
}
}
// Throw an exception otherwise
throw new DataModel\Relation\Exception\RelationTypeNotFound("Relation type '$name' not known to relation manager");
}
/**
* Is $name a magic-callable method?
*
* @param string $name The name of a potential magic-callable method
*
* @return bool
*/
public function isMagicMethod($name)
{
if (isset(static::$relationTypes[$name]))
{
return true;
}
elseif (substr($name, 0, 3) == 'get')
{
$relationName = substr($name, 3);
$relationName = strtolower($relationName[0]) . substr($relationName, 1);
if (isset($this->relations[$relationName]))
{
return true;
}
}
return false;
}
/**
* Is $name a magic property? Corollary: returns true if a relation of this name is known to the relation manager.
*
* @param string $name The name of a potential magic property
*
* @return bool
*/
public function isMagicProperty($name)
{
return isset($this->relations[$name]);
}
/**
* Magic method to get the data of a relation using shorthand notation, e.g. $manager->phone instead of
* $manager->getData('phone')
*
* @param $name
*
* @return Collection
*/
function __get($name)
{
return $this->getData($name);
}
}
Exception/CannotGetName.php 0000644 00000001107 15234466453 0011711 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\Exception;
use Exception;
defined('_JEXEC') or die;
/**
* Exception thrown when we can't get a Controller's name
*/
class CannotGetName extends \RuntimeException
{
public function __construct( $message = "", $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = \JText::_('LIB_FOF_MODEL_ERR_GET_NAME');
}
parent::__construct( $message, $code, $previous );
}
}
Mixin/Assertions.php 0000644 00000003107 15234466453 0010510 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\Mixin;
defined('_JEXEC') or die;
use JText;
use RuntimeException;
/**
* Trait for check() method assertions
*/
trait Assertions
{
/**
* Make sure $condition is true or throw a RuntimeException with the $message language string
*
* @param bool $condition The condition which must be true
* @param string $message The language key for the message to throw
*
* @throws RuntimeException
*/
protected function assert($condition, $message)
{
if (!$condition)
{
throw new RuntimeException(JText::_($message));
}
}
/**
* Assert that $value is not empty or throw a RuntimeException with the $message language string
*
* @param mixed $value The value to check
* @param string $message The language key for the message to throw
*
* @throws RuntimeException
*/
protected function assertNotEmpty($value, $message)
{
$this->assert(!empty($value), $message);
}
/**
* Assert that $value is set to one of $validValues or throw a RuntimeException with the $message language string
*
* @param mixed $value The value to check
* @param array $validValues An array of valid values for $value
* @param string $message The language key for the message to throw
*
* @throws RuntimeException
*/
protected function assertInArray($value, array $validValues, $message)
{
$this->assert(in_array($value, $validValues), $message);
}
}
Mixin/DateManipulation.php 0000644 00000006451 15234466453 0011621 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\Mixin;
use FOF30\Date\Date;
use FOF30\Model\DataModel;
defined('_JEXEC') or die;
/**
* Trait for date manipulations commonly used in models
*/
trait DateManipulation
{
/**
* Normalise a date into SQL format
*
* @param string $value The date to normalise
* @param string $default The default date to use if the normalised date is invalid or empty (use 'now' for
* current date/time)
*
* @return string
*/
protected function normaliseDate($value, $default = '2001-01-01')
{
/** @var DataModel $this */
\JLoader::import('joomla.utilities.date');
$db = $this->container->platform->getDbo();
if (empty($value) || ($value == $db->getNullDate()))
{
$value = $default;
}
if (empty($value) || ($value == $db->getNullDate()))
{
return $value;
}
$regex = '/^\d{1,4}(\/|-)\d{1,2}(\/|-)\d{2,4}[[:space:]]{0,}(\d{1,2}:\d{1,2}(:\d{1,2}){0,1}){0,1}$/';
if (!preg_match($regex, $value))
{
$value = $default;
}
if (empty($value) || ($value == $db->getNullDate()))
{
return $value;
}
$date = new Date($value);
$value = $date->toSql();
return $value;
}
/**
* Sort the published up/down times in case they are give out of order. If publish_up equals publish_down the
* foreverDate will be used for publish_down.
*
* @param string $publish_up Publish Up date
* @param string $publish_down Publish Down date
* @param string $foreverDate See above
*
* @return array (publish_up, publish_down)
*/
protected function sortPublishDates($publish_up, $publish_down, $foreverDate = '2038-01-18 00:00:00')
{
\JLoader::import('joomla.utilities.date');
$jUp = new Date($publish_up);
$jDown = new Date($publish_down);
if ($jDown->toUnix() < $jUp->toUnix())
{
$temp = $publish_up;
$publish_up = $publish_down;
$publish_down = $temp;
}
elseif ($jDown->toUnix() == $jUp->toUnix())
{
$jDown = new Date($foreverDate);
$publish_down = $jDown->toSql();
}
return array($publish_up, $publish_down);
}
/**
* Publish or unpublish a DataModel item based on its publish_up / publish_down fields
*
* @param DataModel $row The DataModel to publish/unpublish
*
* @return void
*/
protected function publishByDate(DataModel $row)
{
static $uNow = null;
\JLoader::import('joomla.utilities.date');
if (is_null($uNow))
{
$jNow = new Date();
$uNow = $jNow->toUnix();
}
/** @var \JDatabaseDriver $db */
$db = $this->container->platform->getDbo();
$triggered = false;
if ($row->publish_down && ($row->publish_down != $db->getNullDate()))
{
$publish_down = $this->normaliseDate($row->publish_down, '2038-01-18 00:00:00');
$publish_up = $this->normaliseDate($row->publish_up, '2001-01-01 00:00:00');
$jDown = new Date($publish_down);
$jUp = new Date($publish_up);
if (($uNow >= $jDown->toUnix()) && $row->enabled)
{
$row->enabled = 0;
$triggered = true;
}
elseif (($uNow >= $jUp->toUnix()) && !$row->enabled && ($uNow < $jDown->toUnix()))
{
$row->enabled = 1;
$triggered = true;
}
}
if ($triggered)
{
$row->save();
}
}
}
Mixin/Generators.php 0000644 00000003650 15234466453 0010472 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\Mixin;
defined('_JEXEC') or die;
/**
* Trait for PHP 5.5 Generators
*/
trait Generators
{
/**
* Returns a PHP Generator of DataModel instances based on your currently set Model state. You can foreach() the
* returned generator to walk through each item of the data set.
*
* WARNING! This only works on PHP 5.5 and later.
*
* When the generator is done you might get a PHP warning. This is normal. Joomla! doesn't support multiple db
* cursors being open at once. What we do instead is clone the database object. Of course it cannot close the db
* connection when we dispose of it (since it's already in use by Joomla), hence the warning. Pay no attention.
*
* @param integer $limitstart How many items from the start to skip (0 = do not skip)
* @param integer $limit How many items to return (0 = all)
* @param bool $overrideLimits Set to true to override limitstart, limit and ordering
*
* @return \Generator A PHP generator of DataModel objects
* @since 3.3.2
* @throws \Exception
*/
public function &getGenerator($limitstart = 0, $limit = 0, $overrideLimits = false)
{
$limitstart = max($limitstart, 0);
$limit = max($limit, 0);
$query = $this->buildQuery($overrideLimits);
$db = clone $this->getDbo();
$db->setQuery($query, $limitstart, $limit);
$cursor = $db->execute();
$reflectDB = new \ReflectionObject($db);
$refFetchAssoc = $reflectDB->getMethod('fetchAssoc');
$refFetchAssoc->setAccessible(true);
while ($data = $refFetchAssoc->invoke($db, $cursor))
{
$item = clone $this;
$item->clearState()->reset(true);
$item->bind($data);
$item->relationManager = clone $this->relationManager;
$item->relationManager->rebase($item);
yield $item;
}
}
}
Mixin/ImplodedArrays.php 0000644 00000002152 15234466453 0011274 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\Mixin;
defined('_JEXEC') or die;
/**
* Trait for dealing with imploded arrays, stored as comma-separated values
*/
trait ImplodedArrays
{
/**
* Converts the loaded comma-separated list into an array
*
* @param string $value The comma-separated list
*
* @return array The exploded array
*/
protected function getAttributeForImplodedArray($value)
{
if (is_array($value))
{
return $value;
}
if (empty($value))
{
return array();
}
$value = explode(',', $value);
$value = array_map('trim', $value);
return $value;
}
/**
* Converts an array of values into a comma separated list
*
* @param array $value The array of values
*
* @return string The imploded comma-separated list
*/
protected function setAttributeForImplodedArray($value)
{
if (!is_array($value))
{
return $value;
}
$value = array_map('trim', $value);
$value = implode(',', $value);
return $value;
}
}
Mixin/JsonData.php 0000644 00000001742 15234466453 0010064 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model\Mixin;
defined('_JEXEC') or die;
/**
* Trait for dealing with data stored as JSON-encoded strings
*/
trait JsonData
{
/**
* Converts the loaded JSON string into an array
*
* @param string $value The JSON string
*
* @return array The data
*/
protected function getAttributeForJson($value)
{
if (is_array($value))
{
return $value;
}
if (empty($value))
{
return array();
}
$value = json_decode($value, true);
if (empty($value))
{
return array();
}
return $value;
}
/**
* Converts and array into a JSON string
*
* @param array $value The data
*
* @return string The JSON string
*/
protected function setAttributeForJson($value)
{
if (!is_array($value))
{
return $value;
}
$value = json_encode($value);
return $value;
}
}
DataModel.php 0000644 00000350674 15234466453 0007142 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model;
use FOF30\Container\Container;
use FOF30\Controller\Exception\LockedRecord;
use FOF30\Date\Date;
use FOF30\Event\Dispatcher;
use FOF30\Event\Observer;
use FOF30\Form\Form;
use FOF30\Model\DataModel\Collection as DataCollection;
use FOF30\Model\DataModel\Exception\BaseException;
use FOF30\Model\DataModel\Exception\CannotLockNotLoadedRecord;
use FOF30\Model\DataModel\Exception\InvalidSearchMethod;
use FOF30\Model\DataModel\Exception\NoAssetKey;
use FOF30\Model\DataModel\Exception\NoContentType;
use FOF30\Model\DataModel\Exception\NoItemsFound;
use FOF30\Model\DataModel\Exception\NoTableColumns;
use FOF30\Model\DataModel\Exception\RecordNotLoaded;
use FOF30\Model\DataModel\Exception\SpecialColumnMissing;
use FOF30\Model\DataModel\Relation\Exception\RelationNotFound;
use FOF30\Model\DataModel\RelationManager;
use FOF30\Utils\ArrayHelper;
defined('_JEXEC') or die;
/**
* Data-aware model, implementing a convenient ORM
*
* Type hinting -- start
*
* @method $this hasOne() hasOne(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
* @method $this belongsTo() belongsTo(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
* @method $this hasMany() hasMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
* @method $this belongsToMany() belongsToMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null, string $pivotTable = null, string $pivotLocalKey = null, string $pivotForeignKey = null)
*
* @method $this filter_order() filter_order(string $orderingField)
* @method $this filter_order_Dir() filter_order_Dir(string $direction)
* @method $this limit() limit(int $limit)
* @method $this limitstart() limitstart(int $limitStart)
* @method $this enabled() enabled(int $enabled)
* @method DataModel getNew() getNew(string $relationName)
*
* @property int $enabled Publish status of this record
* @property int $ordering Sort ordering of this record
* @property int $created_by ID of the user who created this record
* @property string $created_on Date/time stamp of record creation
* @property int $modified_by ID of the user who modified this record
* @property string $modified_on Date/time stamp of record modification
* @property int $locked_by ID of the user who locked this record
* @property string $locked_on Date/time stamp of record locking
*
* Type hinting -- end
*/
class DataModel extends Model implements \JTableInterface
{
/** @var array A list of tables in the database */
protected static $tableCache = array();
/** @var array A list of table fields, keyed per table */
protected static $tableFieldCache = array();
/** @var array A list of permutations of the prefix with upper/lowercase letters */
protected static $prefixCasePermutations = array();
/** @var array Table field name aliases, defined as aliasFieldName => actualFieldName */
protected $aliasFields = array();
/** @var boolean Should I run automatic checks on the table data? */
protected $autoChecks = true;
/** @var boolean Should I auto-fill the fields of the model object when constructing it? */
protected $autoFill = false;
/** @var Dispatcher An event dispatcher for model behaviours */
protected $behavioursDispatcher = null;
/** @var \JDatabaseDriver The database driver for this model */
protected $dbo = null;
/** @var array Which fields should be exempt from automatic checks when autoChecks is enabled */
protected $fieldsSkipChecks = array();
/** @var array Which fields should be auto-filled from the model state (by extent, the request)? */
protected $fillable = array();
/** @var array Which fields should never be auto-filled from the model state (by extent, the request)? */
protected $guarded = array();
/** @var string The identity field's name */
protected $idFieldName = '';
/** @var array A hash array with the table fields we know about and their information. Each key is the field name, the value is the field information */
protected $knownFields = array();
/** @var array The data of the current record */
protected $recordData = array();
/** @var boolean What will delete() do? True: trash (enabled set to -2); false: hard delete (remove from database) */
protected $softDelete = false;
/** @var string The name of the database table we connect to */
protected $tableName = '';
/** @var array A collection of custom, additional where clauses to apply during buildQuery */
protected $whereClauses = array();
/** @var RelationManager The relation manager of this model */
protected $relationManager = null;
/** @var array A list of all eager loaded relations and their attached callbacks */
protected $eagerRelations = array();
/** @var array A list of the relation filter definitions for this model */
protected $relationFilters = array();
/** @var array A list of the relations which will be auto-touched by save() and touch() methods */
protected $touches = array();
/** @var bool Should rows be tracked as ACL assets? */
protected $_trackAssets = false;
/** @var bool Does the resource support joomla tags? */
protected $_has_tags = false;
/** @var \JAccessRules The rules associated with this record. */
protected $_rules;
/** @var string The UCM content type (typically: com_something.viewname, e.g. com_foobar.items) */
protected $contentType = null;
/**
* The name of the XML form to load
*
* @var string|null
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
protected $formName = null;
/**
* Array of form objects
*
* @var Form[]
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
protected $_forms = array();
/**
* The data to load into a form
*
* @var array
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
protected $_formData = array();
/** @var array Shared parameters for behaviors */
protected $_behaviorParams = array();
/**
* The asset key for items in this table. It's usually something in the
* com_example.viewname format. They asset name will be this key appended
* with the item's ID, e.g. com_example.viewname.123
*
* @var string
*/
protected $_assetKey = '';
/**
* Public constructor. Overrides the parent constructor, adding support for database-aware models.
*
* You can use the $config array to pass some configuration values to the object:
*
* tableName String The name of the database table to use. Default: #__appName_viewNamePlural (Ruby on Rails convention)
* idFieldName String The table key field name. Default: appName_viewNameSingular_id (Ruby on Rails convention)
* knownFields Array The known fields in the table. Default: read from the table itself
* autoChecks Boolean Should I turn on automatic data validation checks?
* fieldsSkipChecks Array List of fields which should not participate in automatic data validation checks.
* aliasFields Array Associative array of "magic" field aliases.
* behavioursDispatcher EventDispatcher The model behaviours event dispatcher.
* behaviourObservers Array The model behaviour observers to attach to the behavioursDispatcher.
* behaviours Array A list of behaviour names to instantiate and attach to the behavioursDispatcher.
* fillable_fields Array Which fields should be auto-filled from the model state (by extent, the request)?
* guarded_fields Array Which fields should never be auto-filled from the model state (by extent, the request)?
* relations Array (hashed) The relations to autoload on model creation.
* contentType String The UCM content type, e.g. "com_foobar.items"
*
* Setting either fillable_fields or guarded_fields turns on automatic filling of fields in the constructor. If both
* are set only guarded_fields is taken into account. Fields are not filled automatically outside the constructor.
*
* @see Model::__construct()
*
* @param Container $container The configuration variables to this model
* @param array $config Configuration values for this model
*
* @throws \FOF30\Model\DataModel\Exception\NoTableColumns
*/
public function __construct(Container $container, array $config = array())
{
// First call the parent constructor.
parent::__construct($container, $config);
// Should I use a different database object?
$this->dbo = $container->db;
// Do I have a table name?
if (isset($config['tableName']))
{
$this->tableName = $config['tableName'];
}
elseif (empty($this->tableName))
{
// The table name is by default: #__appName_viewNamePlural (Ruby on Rails convention)
$viewPlural = $container->inflector->pluralize($this->getName());
$this->tableName = '#__' . strtolower($this->container->bareComponentName) . '_' . strtolower($viewPlural);
}
// Do I have a table key name?
if (isset($config['idFieldName']))
{
$this->idFieldName = $config['idFieldName'];
}
elseif (empty($this->idFieldName))
{
// The default ID field is: appName_viewNameSingular_id (Ruby on Rails convention)
$viewSingular = $container->inflector->singularize($this->getName());
$this->idFieldName = strtolower($this->container->bareComponentName) . '_' . strtolower($viewSingular) . '_id';
}
// Do I have a list of known fields?
if (isset($config['knownFields']) && !empty($config['knownFields']))
{
if (!is_array($config['knownFields']))
{
$config['knownFields'] = explode(',', $config['knownFields']);
}
$this->knownFields = $config['knownFields'];
}
else
{
// By default the known fields are fetched from the table itself (slow!)
$this->knownFields = $this->getTableFields();
}
if (empty($this->knownFields))
{
throw new NoTableColumns(sprintf('Model %s could not fetch column list for the table %s', $this->getName(), $this->tableName));
}
// Should I turn on autoChecks?
if (isset($config['autoChecks']))
{
if (!is_bool($config['autoChecks']))
{
$config['autoChecks'] = strtolower($config['autoChecks']);
$config['autoChecks'] = in_array($config['autoChecks'], array('yes', 'true', 'on', 1));
}
$this->autoChecks = $config['autoChecks'];
}
// Should I exempt fields from autoChecks?
if (isset($config['fieldsSkipChecks']))
{
if (!is_array($config['fieldsSkipChecks']))
{
$config['fieldsSkipChecks'] = explode(',', $config['fieldsSkipChecks']);
$config['fieldsSkipChecks'] = array_map(function ($x) { return trim($x); }, $config['fieldsSkipChecks']);
}
$this->fieldsSkipChecks = $config['fieldsSkipChecks'];
}
// Do I have alias fields?
if (isset($config['aliasFields']))
{
$this->aliasFields = $config['aliasFields'];
}
// Do I have a behaviours dispatcher?
if (isset($config['behavioursDispatcher']) && ($config['behavioursDispatcher'] instanceof Dispatcher))
{
$this->behavioursDispatcher = $config['behavioursDispatcher'];
}
// Otherwise create the model behaviours dispatcher
else
{
$this->behavioursDispatcher = new Dispatcher($this->container);
}
// Do I have an array of behaviour observers
if (isset($config['behaviourObservers']) && is_array($config['behaviourObservers']))
{
foreach ($config['behaviourObservers'] as $observer)
{
$this->behavioursDispatcher->attach($observer);
}
}
// Do I have a list of behaviours?
if (isset($config['behaviours']) && is_array($config['behaviours']))
{
foreach ($config['behaviours'] as $behaviour)
{
$this->addBehaviour($behaviour);
}
}
// Add extra behaviours
foreach (array('Created', 'Modified') as $behaviour)
{
$this->addBehaviour($behaviour);
}
// Do I have a list of fillable fields?
if (isset($config['fillable_fields']) && !empty($config['fillable_fields']))
{
if (!is_array($config['fillable_fields']))
{
$config['fillable_fields'] = explode(',', $config['fillable_fields']);
$config['fillable_fields'] = array_map(function ($x) { return trim($x); }, $config['fillable_fields']);
}
$this->fillable = array();
$this->autoFill = true;
foreach ($config['fillable_fields'] as $field)
{
if (array_key_exists($field, $this->knownFields))
{
$this->fillable[] = $field;
}
elseif (isset($this->aliasFields[$field]))
{
$this->fillable[] = $this->aliasFields[$field];
}
}
}
// Do I have a list of guarded fields?
if (isset($config['guarded_fields']) && !empty($config['guarded_fields']))
{
if (!is_array($config['guarded_fields']))
{
$config['guarded_fields'] = explode(',', $config['guarded_fields']);
$config['guarded_fields'] = array_map(function ($x) { return trim($x); }, $config['guarded_fields']);
}
$this->guarded = array();
$this->autoFill = true;
foreach ($config['guarded_fields'] as $field)
{
if (array_key_exists($field, $this->knownFields))
{
$this->guarded[] = $field;
}
elseif (isset($this->aliasFields[$field]))
{
$this->guarded[] = $this->aliasFields[$field];
}
}
}
// If we are tracking assets, make sure an access field exists and initially set the default.
$asset_id_field = $this->getFieldAlias('asset_id');
$access_field = $this->getFieldAlias('access');
if (array_key_exists($asset_id_field, $this->knownFields))
{
\JLoader::import('joomla.access.rules');
$this->_trackAssets = true;
}
/**
if ($this->_trackAssets && array_key_exists($access_field, $this->knownFields) && !($this->getState($access_field, null)))
{
$this->$access_field = (int) $this->container->platform->getConfig()->get('access');
}
**/
$assetKey = $this->container->componentName . '.' . strtolower($container->inflector->singularize($this->getName()));
$this->setAssetKey($assetKey);
// Set the UCM content type if applicable
if (isset($config['contentType']))
{
$this->contentType = $config['contentType'];
}
// Do I have to auto-fill the fields?
if ($this->autoFill)
{
// If I have guarded fields, I'll try to fill everything, using such fields as a "blacklist"
if (!empty($this->guarded))
{
$fields = array_keys($this->knownFields);
}
else
{
// Otherwise I'll fill only the fillable ones (act like having a "whitelist")
$fields = $this->fillable;
}
foreach ($fields as $field)
{
if (in_array($field, $this->guarded))
{
// Do not set guarded fields
continue;
}
$stateValue = $this->getState($field, null);
if (!is_null($stateValue))
{
$this->setFieldValue($field, $stateValue);
}
}
}
// Create a relation manager
$this->relationManager = new RelationManager($this);
// Do I have a list of relations?
if (isset($config['relations']) && is_array($config['relations']))
{
foreach ($config['relations'] as $relConfig)
{
if (!is_array($relConfig))
{
continue;
}
$defaultRelConfig = array(
'type' => 'hasOne',
'foreignModelClass' => null,
'localKey' => null,
'foreignKey' => null,
'pivotTable' => null,
'pivotLocalKey' => null,
'pivotForeignKey' => null,
);
$relConfig = array_merge($defaultRelConfig, $relConfig);
$this->relationManager->addRelation($relConfig['itemName'], $relConfig['type'], $relConfig['foreignModelClass'],
$relConfig['localKey'], $relConfig['foreignKey'], $relConfig['pivotTable'],
$relConfig['pivotLocalKey'], $relConfig['pivotForeignKey']);
}
}
// Initialise the data model
foreach ($this->knownFields as $fieldName => $information)
{
// Initialize only the null or not yet set records
if(!isset($this->recordData[$fieldName]))
{
$this->recordData[$fieldName] = $information->Default;
}
}
// Trigger the onAfterConstruct event. This allows you to set up model state etc.
$this->triggerEvent('onAfterConstruct');
}
/**
* Magic caller. It works like the magic setter and returns ourselves for chaining. If no arguments are passed we'll
* only look for a scope filter.
*
* @param string $name
* @param mixed $arguments
*
* @return static
*/
public function __call($name, $arguments)
{
// If no arguments are provided try mapping to the scopeSomething() method
if (empty($arguments))
{
$methodName = 'scope' . ucfirst($name);
if (method_exists($this, $methodName))
{
$this->{$methodName}();
return $this;
}
}
// Implements getNew($relationName)
if (($name == 'getNew') && count($arguments))
{
return $this->relationManager->getNew($arguments[0]);
}
// Magically map relations to methods, e.g. $this->foobar will return the "foobar" relations' contents
if ($this->relationManager->isMagicMethod($name))
{
return call_user_func_array(array($this->relationManager, $name), $arguments);
}
// Otherwise call the parent
return parent::__call($name, $arguments);
}
/**
* Magic checker on a property. It follows the same logic of the __get magic method, however, if nothing is found, it
* won't return the state of a variable (we are checking if a property is set)
*
* @param string $name The name of the field to check
*
* @return bool Is the field set?
*/
public function __isset($name)
{
$value = null;
$isState = false;
if (substr($name, 0, 3) == 'flt')
{
$isState = true;
$name = strtolower(substr($name, 3, 1)) . substr($name, 4);
}
// If $name is a field name, get its value
if (!$isState && array_key_exists($name, $this->recordData))
{
$value = $this->getFieldValue($name);
}
elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
{
$name = $this->aliasFields[$name];
$value = $this->getFieldValue($name);
}
elseif ($this->relationManager->isMagicProperty($name))
{
$value = $this->relationManager->$name;
}
// As the core function isset, the property must exists AND must be NOT null
return ($value !== null);
}
/**
* Magic getter. It will return the value of a field or, if no such field is found, the value of the relevant state
* variable.
*
* Tip: Trying to get fltSomething will always return the value of the state variable "something"
*
* Tip: You can define custom field getter methods as getFieldNameAttribute, where FieldName is your field's name,
* in CamelCase (even if the field name itself is in snake_case).
*
* @param string $name The name of the field / state variable to retrieve
*
* @return static|mixed
*/
public function __get($name)
{
// Handle $this->input
if ($name == 'input')
{
return $this->container->input;
}
$isState = false;
if (substr($name, 0, 3) == 'flt')
{
$isState = true;
$name = strtolower(substr($name, 3, 1)) . substr($name, 4);
}
// If $name is a field name, get its value
if (!$isState && array_key_exists($name, $this->recordData))
{
return $this->getFieldValue($name);
}
elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
{
$name = $this->aliasFields[$name];
return $this->getFieldValue($name);
}
elseif ($this->relationManager->isMagicProperty($name))
{
return $this->relationManager->$name;
}
// If $name is not a field name, get the value of a state variable
else
{
return $this->getState($name);
}
}
/**
* Magic setter. It will set the value of a field or the value of a dynamic scope filter, or the value of the
* relevant state variable.
*
* Tip: Trying to set fltSomething will always return the value of the state variable "something"
*
* Tip: Trying to set scopeSomething will always return the value of the dynamic scope filter "something"
*
* Tip: You can define custom field setter methods as setFieldNameAttribute, where FieldName is your field's name,
* in CamelCase (even if the field name itself is in snake_case).
*
* @param string $name The name of the field / scope / state variable to set
* @param mixed $value The value to set
*
* @return void
*/
public function __set($name, $value)
{
$isState = false;
$isScope = false;
if (substr($name, 0, 3) == 'flt')
{
$isState = true;
$name = strtolower(substr($name, 3, 1)) . substr($name, 4);
}
elseif (substr($name, 0, 5) == 'scope')
{
$isScope = true;
$name = strtolower(substr($name, 5, 1)) . substr($name, 5);
}
// If $name is a field name, set its value
if (!$isState && !$isScope && array_key_exists($name, $this->recordData))
{
$this->setFieldValue($name, $value);
}
elseif (!$isState && !$isScope && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
{
$name = $this->aliasFields[$name];
$this->setFieldValue($name, $value);
}
// If $name is a dynamic scope filter, set its value
elseif ($isScope || method_exists($this, 'scope' . ucfirst($name)))
{
$method = 'scope' . ucfirst($name);
$this->{$method}($value);
}
// If $name is not a field name, set the value of a state variable
else
{
$this->setState($name, $value);
}
}
/**
* Returns a temporary instance of the model. Please note that this returns a _clone_ of the model object, not the
* original object. The new object is set up to not save its stats, ignore the request when getting state variables
* and comes with an empty state. The temporary object instance has its data reset as well.
*
* @return $this
*/
public function tmpInstance()
{
return parent::tmpInstance()->reset(true, true);
}
/**
* Adds a known field to the DataModel. This is only necessary if you are using a custom buildQuery with JOINs or
* field aliases. Please note that you need to make further modifications for bind() and save() to work in this
* case. Please refer to the documentation blocks of these methods for more information. It is generally considered
* a very BAD idea using JOINs instead of relations. It complicates your life and is bound to cause bugs that are
* very hard to track back.
*
* Basically, if you find yourself using this method you are probably doing something very wrong or very advanced.
* If you do not feel confident with debugging FOF code STOP WHATEVER YOU'RE DOING and rethink your Model. Why are
* you using a JOIN? If you want to filter the records by a field found in another table you can still use
* relations and whereHas with a callback.
*
* @param string $fieldName The name of the field
* @param mixed $default Default value, used by reset() (default: null)
* @param string $type Database type for the field. If unsure use 'integer', 'float' or 'text'.
* @param bool $replace Should we replace an existing known field definition?
*
* @return $this Self, for chaining
*/
public function addKnownField($fieldName, $default = null, $type = 'integer', $replace = false)
{
if (array_key_exists($fieldName, $this->knownFields) && !$replace)
{
return $this;
}
$info = (object)array(
'Default' => $default,
'Type' => $type,
'Null' => 'YES',
);
$this->knownFields[$fieldName] = $info;
// Initialize only the null or not yet set records
if(!isset($this->recordData[$fieldName]))
{
$this->recordData[$fieldName] = $default;
}
return $this;
}
/**
* Get the columns from database table. For JTableInterface compatibility.
*
* @return mixed An array of the field names, or false if an error occurs.
*/
public function getFields()
{
return $this->getTableFields();
}
/**
* Get the columns from a database table.
*
* @param string $tableName Table name. If null current table is used
*
* @return mixed An array of the field names, or false if an error occurs.
*/
public function getTableFields($tableName = null)
{
// Make sure we have a list of tables in this db
if (empty(static::$tableCache))
{
static::$tableCache = $this->getDbo()->getTableList();
}
if (!$tableName)
{
$tableName = $this->tableName;
}
// Try to load again column specifications if the table is not loaded OR if it's loaded and
// the previous call returned an error
if (!array_key_exists($tableName, static::$tableFieldCache) ||
(isset(static::$tableFieldCache[$tableName]) && !static::$tableFieldCache[$tableName])
)
{
// Lookup the fields for this table only once.
$name = $tableName;
$prefix = $this->getDbo()->getPrefix();
if (substr($name, 0, 3) == '#__')
{
$checkName = $prefix . substr($name, 3);
}
else
{
$checkName = $name;
}
// Iterate through all lower/uppercase permutations of the prefix if we have a prefix with at least one uppercase letter
if (!in_array($checkName, static::$tableCache) && preg_match('/[A-Z]/', $prefix) && (substr($name, 0, 3) == '#__'))
{
$prefixPermutations = $this->getPrefixCasePermutations();
$partialCheckName = substr($name, 3);
foreach ($prefixPermutations as $permutatedPrefix)
{
$checkName = $permutatedPrefix . $partialCheckName;
if (in_array($checkName, static::$tableCache))
{
break;
}
}
}
if (!in_array($checkName, static::$tableCache))
{
// The table doesn't exist. Return false.
static::$tableFieldCache[$tableName] = false;
}
else
{
$fields = $this->getDbo()->getTableColumns($name, false);
if (empty($fields))
{
$fields = false;
}
static::$tableFieldCache[$tableName] = $fields;
}
// PostgreSQL date type compatibility
if (($this->getDbo()->name == 'postgresql') && (static::$tableFieldCache[$tableName] != false))
{
foreach (static::$tableFieldCache[$tableName] as $field)
{
if (strtolower($field->type) == 'timestamp without time zone')
{
if (stristr($field->Default, '\'::timestamp without time zone'))
{
list ($date,) = explode('::', $field->Default, 2);
$field->Default = trim($date, "'");
}
}
}
}
}
return static::$tableFieldCache[$tableName];
}
/**
* Get the database connection associated with this data Model
*
* @return \JDatabaseDriver
*/
public function getDbo()
{
if (!is_object($this->dbo))
{
$this->dbo = $this->container->db;
}
return $this->dbo;
}
/**
* Returns the data currently bound to the model in an array format. Similar to toArray() but returns a copy instead
* of the internal table itself.
*
* @return array
*/
public function getData()
{
$ret = array();
foreach ($this->knownFields as $field => $info)
{
$ret[$field] = $this->getFieldValue($field);
}
return $ret;
}
/**
* Return the value of the identity column of the currently loaded record
*
* @return mixed
*/
public function getId()
{
return $this->{$this->idFieldName};
}
/**
* Returns the name of the table's id field (primary key) name
*
* @return string
*/
public function getIdFieldName()
{
return $this->idFieldName;
}
/**
* Alias of getIdFieldName. Used for JTableInterface compatibility.
*
* @return string The name of the primary key for the table.
*
* @codeCoverageIgnore
*/
public function getKeyName()
{
return $this->getIdFieldName();
}
/**
* Returns the database table name this model talks to
*
* @return string
*/
public function getTableName()
{
return $this->tableName;
}
/**
* Returns the value of a field. If a field is not set it uses the $default value. Automatically uses magic
* getter variables if required.
*
* @param string $name The name of the field to retrieve
* @param mixed $default Default value, if the field is not set and doesn't have a getter method
*
* @return mixed The value of the field
*/
public function getFieldValue($name, $default = null)
{
if (array_key_exists($name, $this->aliasFields))
{
$name = $this->aliasFields[$name];
}
if (!array_key_exists($name, $this->knownFields))
{
return $default;
}
if (!isset($this->recordData[$name]))
{
$this->recordData[$name] = $default;
}
return $this->recordData[$name];
}
/**
* Sets the value of a field.
*
* @param string $name The name of the field to set
* @param mixed $value The value to set it to
*
* @return void
*/
public function setFieldValue($name, $value = null)
{
if (array_key_exists($name, $this->aliasFields))
{
$name = $this->aliasFields[$name];
}
if (array_key_exists($name, $this->knownFields))
{
$this->recordData[$name] = $value;
}
}
/**
* Applies the getSomethingAttribute methods to $this->recordData, converting the database representation of the
* data to the record representation. $this->recordData is directly modified.
*
* @return void
*/
public function databaseDataToRecordData()
{
foreach ($this->recordData as $name => $value)
{
$method = $this->container->inflector->camelize('get_' . $name . '_attribute');
if (method_exists($this, $method))
{
$this->recordData[$name] = $this->{$method}($value);
}
}
}
/**
* Applies the setSomethingAttribute methods to $this->recordData, converting the record representation to database
* representation. It does not modify $this->recordData, it returns a copy of the data array.
*
* If you are using custom knownFields to cater for table JOINs you need to override this method and _remove_ the
* fields which do not belong to the table you are saving to. It's generally a bad idea using JOINs instead of
* relations. You have been warned!
*
* @return array
*/
public function recordDataToDatabaseData()
{
$copy = array_merge($this->recordData);
foreach ($copy as $name => $value)
{
$method = $this->container->inflector->camelize('set_' . $name . '_attribute');
if (method_exists($this, $method))
{
$copy[$name] = $this->{$method}($value);
}
}
return $copy;
}
/**
* Does this model know about a field called $fieldName? Automatically uses aliases when necessary.
*
* @param string $fieldName Field name to check
*
* @return boolean True if the field exists
*/
public function hasField($fieldName)
{
$realFieldName = $this->getFieldAlias($fieldName);
return array_key_exists($realFieldName, $this->knownFields);
}
/**
* Get the real name of a field name based on its alias. If the field is not aliased $alias is returned
*
* @param string $alias The field to get an alias for
*
* @return string The real name of the field
*/
public function getFieldAlias($alias)
{
if (array_key_exists($alias, $this->aliasFields))
{
return $this->aliasFields[$alias];
}
else
{
return $alias;
}
}
/**
* Returns an array mapping relation names to their local key field names.
*
* For example, given a relation "foobar" with local key name "example_item_id" it will return:
* ["foobar" => "example_item_id"]
*
* @return array Array of [relationName => fieldName] arrays
*
* @throws \FOF30\Model\DataModel\Relation\Exception\RelationNotFound
*/
public function getRelationFields()
{
$fields = array();
$relationNames = $this->relationManager->getRelationNames();
if (empty($relationNames))
{
return $fields;
}
foreach ($relationNames as $name)
{
$fields[$name] = $this->relationManager->getRelation($name)->getLocalKey();
}
return $fields;
}
/**
* Returns the qualified foreign model name, in the format "componentName.modelName", for the specified model
* field. First it checks the relations you have defined. If none is found it will try to parse the field name as
* following the componentName_modelName_id naming convention (FOF best practice and recommendation).
*
* This feature is used by the Blade compiler.
*
* @param string $fieldName The field name for which we'll get a foreign model name
*
* @return string
*/
public function getForeignModelNameFor($fieldName)
{
// First look for a local field mapped in a relationship
try
{
$relationMap = $this->getRelationFields();
$relationName = array_search($fieldName, $relationMap);
if ($relationName !== false)
{
$model = $this->relationManager->getRelation($relationName)->getForeignModel();
$component = $model->getContainer()->componentName;
$modelName = $model->getName();
return "$component.$modelName";
}
}
catch (RelationNotFound $e)
{
// Bummer. The relation cannot be found. I will fall back to parsing the field name.
}
// Do I have a field following the componentName_modelName_id format?
$parts = explode('_', $fieldName);
if ((substr($fieldName, -3) != '_id') || (count($parts) < 3))
{
throw new \RuntimeException("Cannot determine the foreign model for local field '$fieldName'; it does not follow the expected component_model_id convention.");
}
$fieldName = substr($fieldName, 0, -3);
list($component, $modelName) = explode('_', $fieldName, 2);
$modelName = $this->container->inflector->camelize($modelName);
return "$component.$modelName";
}
/**
* Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set
* data, unless you provide a $data array.
*
* Special note if you are using a custom buildQuery with JOINs or field aliases:
* You will need to override the recordDataToDatabaseData method. Make sure that you _remove_ or rename any fields
* which do not exist in the table defined in $this->tableName. Otherwise Joomla! will not know how to insert /
* update the data on the table and will throw an Exception denoting a database error. It is generally a BAD idea
* using JOINs instead of relations. If unsure, use relations.
*
* @param null|array $data [Optional] Data to bind
* @param string $orderingFilter A WHERE clause used to apply table item reordering
* @param array $ignore A list of fields to ignore when binding $data
* @para boolean $resetRelations Should I automatically reset relations if relation-important fields are changed?
*
* @return DataModel Self, for chaining
*/
public function save($data = null, $orderingFilter = '', $ignore = null, $resetRelations = true)
{
// Stash the primary key
$oldPKValue = $this->getId();
// Call the onBeforeSave event
$this->triggerEvent('onBeforeSave', array(&$data));
// Get the relation to local field map and initialise the relationsAffected array
$relationImportantFields = $this->getRelationFields();
$dataBeforeBind = array();
// If we have relations we keep a copy of the data before bind.
if (count($relationImportantFields))
{
$dataBeforeBind = array_merge($this->recordData);
}
// Bind any (optional) data. If no data is provided, the current record data is used
if (!is_null($data))
{
$this->bind($data, $ignore);
}
// Is this a new record?
if (empty($oldPKValue))
{
$isNewRecord = true;
}
else
{
$isNewRecord = $oldPKValue != $this->getId();
}
// Check the validity of the data
$this->check();
// Get the database object
$db = $this->getDbo();
// Insert or update the record. Note that the object we use for insertion / update is the a copy holding
// the transformed data.
$dataObject = $this->recordDataToDatabaseData();
$dataObject = (object)$dataObject;
if ($isNewRecord)
{
$this->triggerEvent('onBeforeCreate', array(&$dataObject));
// Insert the new record
$db->insertObject($this->tableName, $dataObject, $this->idFieldName);
// Update ourselves with the new ID field's value
$this->{$this->idFieldName} = $db->insertid();
// Rebase the relations with the newly created model
if ($resetRelations)
{
$this->relationManager->rebase($this);
}
$this->triggerEvent('onAfterCreate');
}
else
{
$this->triggerEvent('onBeforeUpdate', array(&$dataObject));
$db->updateObject($this->tableName, $dataObject, $this->idFieldName, true);
$this->triggerEvent('onAfterUpdate');
}
// If an ordering filter is set, attempt reorder the rows in the table based on the filter and value.
if ($orderingFilter)
{
$filterValue = $this->$orderingFilter;
$this->reorder($orderingFilter ? $db->qn($orderingFilter) . ' = ' . $db->q($filterValue) : '');
}
// One more thing... Touch all relations in the $touches array
if (!empty($this->touches))
{
foreach ($this->touches as $relation)
{
$records = $this->getRelations()->getData($relation);
if (!empty($records))
{
if ($records instanceof DataModel)
{
$records = array($records);
}
/** @var DataModel $record */
foreach ($records as $record)
{
$record->touch();
}
}
}
}
// If we have relations we compare the data to the copy of the data before bind.
if (count($relationImportantFields) && $resetRelations)
{
// Since array_diff_assoc doesn't work recursively we have to do it the EXCRUCIATINGLY SLOW WAY. Sad panda :(
$keysRecord = (is_array($this->recordData) && !empty($this->recordData)) ? array_keys($this->recordData) : array();
$keysBefore = (is_array($dataBeforeBind) && !empty($dataBeforeBind)) ? array_keys($dataBeforeBind) : array();
$keysAll = array_merge($keysRecord, $keysBefore);
$keysAll = array_unique($keysAll);
$modifiedFields = array();
foreach ($keysAll as $key)
{
if (!isset($dataBeforeBind[$key]) || !isset($this->recordData[$key]))
{
$modifiedFields[] = $key;
}
elseif ($dataBeforeBind[$key] != $this->recordData[$key])
{
$modifiedFields[] = $key;
}
}
unset ($dataBeforeBind);
if (count($modifiedFields))
{
$relationsAffected = array();
unset($modifiedData);
foreach ($relationImportantFields as $relationName => $fieldName)
{
if (in_array($fieldName, $modifiedFields))
{
$relationsAffected[] = $relationName;
}
}
// Reset the relations which are affected by the save. This will force-reload the relations when you try to
// access them again.
$this->relationManager->resetRelationData($relationsAffected);
}
}
// Finally, call the onAfterSave event
$this->triggerEvent('onAfterSave');
return $this;
}
/**
* Alias of save. For JTableInterface compatibility.
*
* @param boolean $updateNulls Blatantly ignored.
*
* @return boolean True on success.
*/
public function store($updateNulls = false)
{
try
{
$this->save();
}
catch (\Exception $e)
{
return false;
}
return true;
}
/**
* Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set
* data, unless you provide a $data array. On top of that, it also saves all specified relations. If $relations is
* null it will save all relations known to this model.
*
* @param null|array $data [Optional] Data to bind
* @param string $orderingFilter A WHERE clause used to apply table item reordering
* @param array $ignore A list of fields to ignore when binding $data
* @param array $relations Which relations to save with the model's record. Leave null for all relations
*
* @return $this Self, for chaining
*/
public function push($data = null, $orderingFilter = '', $ignore = null, array $relations = null)
{
// Store the model's $touches definition
$touches = $this->touches;
// If $relations is non-null, remove $relations from $this->touches. Since $relations will be saved, they are
// implicitly touched. We don't want to double-touch those records, do we?
if (is_array($relations))
{
$this->touches = array_diff($this->touches, $relations);
}
// Otherwise empty $this->touches completely as we'll be pushing all relations
else
{
$this->touches = array();
}
// Save this record
$this->save($data, $orderingFilter, $ignore, false);
// Push all relations specified (or all relations if $relations is null)
$relManager = $this->getRelations();
$allRelations = $relManager->getRelationNames();
if (!empty($allRelations))
{
foreach ($allRelations as $relationName)
{
if (!is_null($relations) && !in_array($relationName, $relations))
{
continue;
}
$relManager->save($relationName);
}
}
// Restore the model's $touches definition
$this->touches = $touches;
// Return self for chaining
return $this;
}
/**
* Method to bind an associative array or object to the DataModel instance. This method optionally takes an array of
* properties to ignore when binding.
*
* Special note if you are using a custom buildQuery with JOINs or field aliases:
* You will need to use addKnownField to let FOF know that the fields from your JOINs and the aliased fields should
* be bound to the record data. If you are using aliased fields you may also want to override the
* databaseDataToRecordData method. Generally, it is a BAD idea using JOINs instead of relations.
*
* @param mixed $data An associative array or object to bind to the DataModel instance.
* @param mixed $ignore An optional array or space separated list of properties to ignore while binding.
*
* @return static Self, for chaining
*
* @throws \InvalidArgumentException
* @throws \Exception
*/
public function bind($data, $ignore = array())
{
$this->triggerEvent('onBeforeBind', array(&$data));
// If the source value is not an array or object return false.
if (!is_object($data) && !is_array($data))
{
throw new \InvalidArgumentException(\JText::sprintf('LIB_FOF_MODEL_ERR_BIND', get_class($this), gettype($data)));
}
// If the ignore value is a string, explode it over spaces.
if (!is_array($ignore))
{
$ignore = explode(' ', $ignore);
}
// Bind the source value, excluding the ignored fields.
foreach ($this->recordData as $k => $currentValue)
{
// Only process fields not in the ignore array.
if (!in_array($k, $ignore))
{
if (is_array($data) && isset($data[$k]))
{
$this->setFieldValue($k, $data[$k]);
}
elseif (is_object($data) && isset($data->$k))
{
$this->setFieldValue($k, $data->$k);
}
}
}
// Perform data transformation
$this->databaseDataToRecordData();
$this->triggerEvent('onAfterBind', array($data));
return $this;
}
/**
* Check the data for validity. By default it only checks for fields declared as NOT NULL
*
* @return static Self, for chaining
*
* @throws \RuntimeException When the data bound to this record is invalid
*/
public function check()
{
if (!$this->autoChecks)
{
return $this;
}
// Run a custom event
$this->triggerEvent('onBeforeCheck');
// Create a slug if there is a title and an empty slug
$slugField = $this->getFieldAlias('slug');
$titleField = $this->getFieldAlias('title');
if ($this->hasField('title') && $this->hasField('slug') && !$this->$slugField)
{
$this->$slugField = \JApplicationHelper::stringURLSafe($this->$titleField);
}
// Special handling of the ordering field
if ($this->hasField('ordering') && is_null($this->getFieldValue('ordering')))
{
$this->setFieldValue('ordering', 0);
}
foreach ($this->knownFields as $fieldName => $field)
{
// Never check the key if it's empty; an empty key is normal for new records
if ($fieldName == $this->idFieldName)
{
continue;
}
$value = $this->$fieldName;
if (isset($field->Null) && ($field->Null == 'NO') && empty($value) && !is_numeric($value) && !in_array($fieldName, $this->fieldsSkipChecks))
{
if (!is_null($field->Default))
{
$this->$fieldName = $field->Default;
continue;
}
$text = $this->container->componentName . '_' . $this->container->inflector->singularize($this->getName()) . '_ERR_'
. $fieldName . '_EMPTY';
throw new \RuntimeException(\JText::_(strtoupper($text)), 500);
}
}
// Server-side form validation
$allData = $this->getData();
$form = $this->getForm($allData, false);
if (is_object($form) && $form instanceof Form)
{
$serverside_validate = strtolower($form->getAttribute('serverside_validate'));
if (in_array($serverside_validate, array('true', 'yes', '1', 'on')))
{
$fieldset = $form->getFieldset();
foreach ($fieldset as $nfield => $fldset)
{
if (!array_key_exists($nfield, $allData))
{
$field = $form->getField($fldset->fieldname, $fldset->group);
$type = strtolower($field->type);
switch ($type)
{
case 'checkbox':
$allData[$nfield] = 0;
break;
default:
$allData[$nfield] = '';
break;
}
}
}
try
{
$this->validateForm($form, $allData);
}
catch (\Exception $e)
{
throw new \RuntimeException($e->getMessage(), $e->getCode());
}
}
}
return $this;
}
/**
* Change the ordering of the records of the table
*
* @param string $where The WHERE clause of the SQL used to fetch the order
*
* @return static Self, for chaining
*
* @throws \UnexpectedValueException
*/
public function reorder($where = '')
{
// If there is no ordering field set an error and return false.
if (!$this->hasField('ordering'))
{
throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName));
}
$this->triggerEvent('onBeforeReorder', array(&$where));
$order_field = $this->getFieldAlias('ordering');
$k = $this->getIdFieldName();
$db = $this->getDbo();
// Get the primary keys and ordering values for the selection.
$query = $db->getQuery(true)
->select($db->qn($k) . ', ' . $db->qn($order_field))
->from($db->qn($this->getTableName()))
->where($db->qn($order_field) . ' >= ' . $db->q(0))
->order($db->qn($order_field) . 'ASC, ' . $db->qn($k) . 'ASC');
// Setup the extra where and ordering clause data.
if ($where)
{
$query->where($where);
}
$rows = $db->setQuery($query)->loadObjectList();
// Compact the ordering values.
foreach ($rows as $i => $row)
{
// Make sure the ordering is a positive integer.
if ($row->$order_field >= 0)
{
// Only update rows that are necessary.
if ($row->$order_field != $i + 1)
{
// Update the row ordering field.
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($db->qn($order_field) . ' = ' . $db->q($i + 1))
->where($db->qn($k) . ' = ' . $db->q($row->$k));
$db->setQuery($query)->execute();
}
}
}
$this->triggerEvent('onAfterReorder');
return $this;
}
/**
* Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
* Negative numbers move the row up in the sequence and positive numbers move it down.
*
* @param integer $delta The direction and magnitude to move the row in the ordering sequence.
* @param string $where WHERE clause to use for limiting the selection of rows to compact the
* ordering values.
*
* @return static Self, for chaining
*
* @throws \UnexpectedValueException If the table does not support reordering
* @throws \RuntimeException If the record is not loaded
*/
public function move($delta, $where = '')
{
if (!$this->hasField('ordering'))
{
throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName));
}
$this->triggerEvent('onBeforeMove', array(&$delta, &$where));
$ordering_field = $this->getFieldAlias('ordering');
// If the change is none, do nothing.
if (empty($delta))
{
$this->triggerEvent('onAfterMove');
return $this;
}
$k = $this->idFieldName;
$row = null;
$db = $this->getDbo();
$query = $db->getQuery(true);
// If the table is not loaded, return false
if (empty($this->$k))
{
throw new RecordNotLoaded(sprintf("Model %s does not have a loaded record", $this->getName()));
}
// Select the primary key and ordering values from the table.
$query->select(array(
$db->qn($this->idFieldName), $db->qn($ordering_field)
)
)->from($db->qn($this->tableName));
// If the movement delta is negative move the row up.
if ($delta < 0)
{
$query->where($db->qn($ordering_field) . ' < ' . $db->q((int)$this->$ordering_field));
$query->order($db->qn($ordering_field) . ' DESC');
}
// If the movement delta is positive move the row down.
elseif ($delta > 0)
{
$query->where($db->qn($ordering_field) . ' > ' . $db->q((int)$this->$ordering_field));
$query->order($db->qn($ordering_field) . ' ASC');
}
// Add the custom WHERE clause if set.
if ($where)
{
$query->where($where);
}
// Select the first row with the criteria.
$row = $db->setQuery($query, 0, 1)->loadObject();
// If a row is found, move the item.
if (!empty($row))
{
// Update the ordering field for this instance to the row's ordering value.
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($db->qn($ordering_field) . ' = ' . $db->q((int)$row->$ordering_field))
->where($db->qn($k) . ' = ' . $db->q($this->$k));
$db->setQuery($query)->execute();
// Update the ordering field for the row to this instance's ordering value.
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($db->qn($ordering_field) . ' = ' . $db->q((int)$this->$ordering_field))
->where($db->qn($k) . ' = ' . $db->q($row->$k));
$db->setQuery($query)->execute();
// Update the instance value.
$this->$ordering_field = $row->$ordering_field;
}
$this->triggerEvent('onAfterMove');
return $this;
}
/**
* Process a large collection of records a few at a time.
*
* @param integer $chunkSize How many records to process at once
* @param callable $callback A callable to process each record
*
* @return $this Self, for chaining
*/
public function chunk($chunkSize, $callback)
{
$totalItems = $this->count();
if (!$totalItems)
{
return $this;
}
$start = 0;
while ($start < ($totalItems - 1))
{
$this->get(true, $start, $chunkSize)->transform($callback);
$start += $chunkSize;
}
return $this;
}
/**
* Get the number of all items
*
* @return integer
*/
public function count()
{
// Get a "count all" query
$db = $this->getDbo();
$query = $this->buildQuery(true);
$query->clear('select')->clear('order')->select('COUNT(*)');
// Run the "before build query" hook and behaviours
$this->triggerEvent('onBuildCountQuery', array(&$query));
$total = $db->setQuery($query)->loadResult();
return $total;
}
/**
* Build the query to fetch data from the database
*
* @param boolean $overrideLimits Should I override limits
*
* @return \JDatabaseQuery The database query to use
*/
public function buildQuery($overrideLimits = false)
{
// Get a "select all" query
$db = $this->getDbo();
$query = $db->getQuery(true)
->select('*')
->from($this->getTableName());
// Run the "before build query" hook and behaviours
$this->triggerEvent('onBeforeBuildQuery', array(&$query, $overrideLimits));
// Apply custom WHERE clauses
if (count($this->whereClauses))
{
foreach ($this->whereClauses as $clause)
{
$query->where($clause);
}
}
$order = $this->getState('filter_order', null, 'cmd');
if (!array_key_exists($order, $this->knownFields))
{
$order = $this->getIdFieldName();
$this->setState('filter_order', $order);
}
$order = $db->qn($order);
$dir = strtoupper($this->getState('filter_order_Dir', null, 'cmd'));
if (!in_array($dir, array('ASC', 'DESC')))
{
$dir = 'ASC';
$this->setState('filter_order_Dir', $dir);
}
$query->order($order . ' ' . $dir);
// Run the "before after query" hook and behaviours
$this->triggerEvent('onAfterBuildQuery', array(&$query, $overrideLimits));
return $query;
}
/**
* Returns a DataCollection iterator based on your currently set Model state
*
* @param boolean $overrideLimits Should I ignore limits set in the Model?
* @param integer $limitstart How many items to skip from the start, only when $overrideLimits = true
* @param integer $limit How many items to return, only when $overrideLimits = true
*
* @return DataCollection The data collection
*/
public function get($overrideLimits = false, $limitstart = 0, $limit = 0)
{
if (!$overrideLimits)
{
$limitstart = $this->getState('limitstart', 0);
$limit = $this->getState('limit', 0);
}
$dataCollection = DataCollection::make($this->getItemsArray($limitstart, $limit, $overrideLimits));
$this->eagerLoad($dataCollection, null);
return $dataCollection;
}
/**
* Returns a raw array of DataModel instances based on your currently set Model state
*
* @param integer $limitstart How many items from the start to skip (0 = do not skip)
* @param integer $limit How many items to return (0 = all)
* @param bool $overrideLimits Set to true to override limitstart, limit and ordering
*
* @return array Array of DataModel objects
*/
public function &getItemsArray($limitstart = 0, $limit = 0, $overrideLimits = false)
{
$itemsTemp = $this->getRawDataArray($limitstart, $limit, $overrideLimits);
$items = array();
while (!empty($itemsTemp))
{
$data = array_shift($itemsTemp);
/** @var DataModel $item */
$item = clone $this;
$item->clearState()->reset(true);
$item->bind($data);
$items[$item->getId()] = $item;
$item->relationManager = clone $this->relationManager;
$item->relationManager->rebase($item);
}
$this->triggerEvent('onAfterGetItemsArray', array(&$items));
return $items;
}
/**
* Returns the raw data array, as fetched from the database, based on your currently set Model state
*
* @param integer $limitstart How many items from the start to skip (0 = do not skip)
* @param integer $limit How many items to return (0 = all)
* @param bool $overrideLimits Set to true to override limitstart, limit and ordering
*
* @return array Array of hashed arrays
*/
public function &getRawDataArray($limitstart = 0, $limit = 0, $overrideLimits = false)
{
$limitstart = max($limitstart, 0);
$limit = max($limit, 0);
$query = $this->buildQuery($overrideLimits);
$db = $this->getDbo();
$db->setQuery($query, $limitstart, $limit);
$rawData = $db->loadAssocList();
return $rawData;
}
/**
* Eager loads the provided relations and assigns their data to a data collection
*
* @param DataCollection $dataCollection The data collection on which the eager loaded relations will be applied
* @param array|null $relations The relations to eager load. Leave empty to use the already defined relations
*
* @return $this for chaining
*/
public function eagerLoad(DataCollection &$dataCollection, array $relations = null)
{
if (empty($relations))
{
$relations = $this->eagerRelations;
}
// Apply eager loaded relations
if ($dataCollection->count() && !empty($relations))
{
$relationManager = $this->getRelations();
foreach ($relations as $relation => $callback)
{
// Did they give us a relation name without a callback?
if (!is_callable($callback) && is_string($callback) && !empty($callback))
{
$relation = $callback;
$callback = null;
}
$relationData = $relationManager->getData($relation, $callback, $dataCollection);
$foreignKeyMap = $relationManager->getForeignKeyMap($relation);
/** @var DataModel $item */
foreach ($dataCollection as $item)
{
$item->getRelations()->setDataFromCollection($relation, $relationData, $foreignKeyMap);
}
}
}
return $this;
}
/**
* Archive the record, i.e. set enabled to 2
*
* @return $this For chaining
*/
public function archive()
{
if(!$this->getId())
{
throw new RecordNotLoaded("Can't archive a not loaded DataModel");
}
if (!$this->hasField('enabled'))
{
return $this;
}
$this->triggerEvent('onBeforeArchive', array());
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = 2;
$this->save();
$this->triggerEvent('onAfterArchive');
return $this;
}
/**
* Trashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that record
* is loaded before trying to trash it. Unlike a hard delete, trashing is a "soft delete", only setting the enabled
* field to -2.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
*/
public function trash($id = null)
{
if (!empty($id))
{
$this->findOrFail($id);
}
$id = $this->getId();
if(!$id)
{
throw new RecordNotLoaded("Can't trash a not loaded DataModel");
}
if (!$this->hasField('enabled'))
{
throw new SpecialColumnMissing("DataModel::trash method needs an 'enabled' field");
}
$this->triggerEvent('onBeforeTrash', array(&$id));
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = -2;
$this->save();
$this->triggerEvent('onAfterTrash', array(&$id));
return $this;
}
/**
* Change the publish state of a record. By default it will set it to 1 (published) unless you specify a different
* value.
*
* @param int $state The publish state. Default: 1 (published).
*
* @return $this For chaining
*/
public function publish($state = 1)
{
if(!$this->getId())
{
throw new RecordNotLoaded("Can't change the state of a not loaded DataModel");
}
if (!$this->hasField('enabled'))
{
return $this;
}
$this->triggerEvent('onBeforePublish', array());
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = $state;
$this->save();
$this->triggerEvent('onAfterPublish');
return $this;
}
/**
* Unpublish the record, i.e. set enabled to 0
*
* @return $this For chaining
*/
public function unpublish()
{
if(!$this->getId())
{
throw new RecordNotLoaded("Can't unpublish a not loaded DataModel");
}
if (!$this->hasField('enabled'))
{
return $this;
}
$this->triggerEvent('onBeforeUnpublish', array());
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = 0;
$this->save();
$this->triggerEvent('onAfterUnpublish');
return $this;
}
/**
* Untrashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that
* record is loaded before trying to untrash it. Please note that enabled is set to 0 (unpublished) when you untrash
* an item.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
*/
public function restore($id = null)
{
if (!$this->hasField('enabled'))
{
return $this;
}
if (!empty($id))
{
$this->findOrFail($id);
}
$id = $this->getId();
if(!$id)
{
throw new RecordNotLoaded("Can't change the state of a not loaded DataModel");
}
$this->triggerEvent('onBeforeRestore', array(&$id));
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = 0;
$this->save();
$this->triggerEvent('onAfterRestore', array(&$id));
return $this;
}
/**
* Creates a copy of the current record. After the copy is performed, the data model contains the data of the new
* record.
*
* @param array|DataModel An associative array or object to bind to the DataModel instance. Allows you to override values on the copied object.
*
* @return DataModel
*/
public function copy($data = null)
{
$this->triggerEvent('onBeforeCopy');
$this->{$this->idFieldName} = null;
if ($this->hasField('created_by'))
{
$this->setFieldValue('created_by', null);
}
if ($this->hasField('modified_by'))
{
$this->setFieldValue('modified_by', null);
}
if ($this->hasField('locked_by'))
{
$this->setFieldValue('locked_by', null);
}
if ($this->hasField('created_on'))
{
$this->setFieldValue('created_on', null);
}
if ($this->hasField('modified_on'))
{
$this->setFieldValue('modified_on', null);
}
if ($this->hasField('locked_on'))
{
$this->setFieldValue('locked_on', null);
}
$result = $this->save($data);
$this->triggerEvent('onAfterCopy', array(&$result));
return $result;
}
/**
* Check-in an item. This works similar to unlock() but performs additional checks. If the item is locked by another
* user you need to have adequate ACL privileges to unlock it, i.e. core.admin or core.manage component-wide
* privileges; core.edit.state privileges component-wide or per asset; or be the creator of the item and have
* core.edit.own privileges component-wide or per asset.
*
* @return $this
*
* @throws LockedRecord If you don't have the privilege to check in this item
*/
public function checkIn($userId = null)
{
// If there is no loaded record we can't do much, I'm afraid
if (!$this->getId())
{
throw new RecordNotLoaded("Can't checkin a not loaded DataModel");
}
// If the lock fields are missing we have nothing to do
if (!$this->hasField('locked_by') && !$this->hasField('locked_on'))
{
return $this;
}
// If there's no locked_by field we just unlock and return
if (!$this->hasField('locked_by'))
{
return $this->unlock();
}
// If the current user and the user who locked the record are the same, unlock it.
if (empty($userId))
{
$userId = $this->container->platform->getUser()->id;
}
$lockedBy = $this->getFieldValue('locked_by');
if (empty($lockedBy) || ($lockedBy == $userId))
{
return $this->unlock();
}
// Get the component privileges
$platform = $this->container->platform;
$component = $this->container->componentName;
$privileges = array
(
'editown' => $platform->authorise('core.edit.own' , $component),
'editstate' => $platform->authorise('core.edit.state', $component),
'admin' => $platform->authorise('core.admin' , $component),
'manage' => $platform->authorise('core.manage' , $component),
);
// If we are trackign assets get the item's privileges
if ($this->isAssetsTracked())
{
$assetKey = $this->getAssetKey();
$assetPrivileges = array
(
'editown' => $platform->authorise('core.edit.own' , $assetKey),
'editstate' => $platform->authorise('core.edit.state', $assetKey),
);
foreach ($assetPrivileges as $k => $v)
{
$privileges[$k] = $privileges[$k] || $v;
}
}
// If you are a Super User, component manager or allowed to edit the state of records we unlock it
if ($privileges['admin'] || $privileges['manage'] || $privileges['editstate'])
{
return $this->unlock();
}
// If you are the owner of the record and have core.edit.own privilege we will unlock it.
$owner = 0;
if ($this->hasField('created_by'))
{
$owner = $this->getFieldValue('created_by');
}
if ($privileges['editown'] && ($owner == $userId))
{
return $this->unlock();
}
// All else failed, you don't have the privilege to unlock this item.
throw new LockedRecord;
}
/**
* Reset the record data
*
* @param boolean $useDefaults Should I use the default values? Default: yes
* @param boolean $resetRelations Should I reset the relations too? Default: no
*
* @return static Self, for chaining
*/
public function reset($useDefaults = true, $resetRelations = false)
{
$this->recordData = array();
$this->whereClauses = array();
foreach ($this->knownFields as $fieldName => $information)
{
if ($useDefaults)
{
$this->recordData[$fieldName] = $information->Default;
}
else
{
$this->recordData[$fieldName] = null;
}
}
if ($resetRelations)
{
$this->relationManager->resetRelationData();
$this->eagerRelations = array();
}
$this->relationFilters = array();
$this->triggerEvent('onAfterReset', array($useDefaults, $resetRelations));
return $this;
}
/**
* Automatically performs a hard or soft delete, based on the value of $this->softDelete. A soft delete simply sets
* enabled to -2 whereas a hard delete removes the data from the database. If you want to force a specific behaviour
* directly call trash() for a soft delete or forceDelete() for a hard delete.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
*/
public function delete($id = null)
{
if ($this->softDelete)
{
return $this->trash($id);
}
else
{
return $this->forceDelete($id);
}
}
/**
* Delete a record, either the currently loaded one or the one specified in $id. If an $id is specified that record
* is loaded before trying to delete it. In the end the data model is reset.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
*/
public function forceDelete($id = null)
{
if (!empty($id))
{
$this->findOrFail($id);
}
$id = $this->getId();
if(!$id)
{
throw new RecordNotLoaded("Can't delete a not loaded DataModel object");
}
$this->triggerEvent('onBeforeDelete', array(&$id));
$db = $this->getDbo();
$query = $db->getQuery(true)
->delete()
->from($this->tableName)
->where($db->qn($this->idFieldName) . ' = ' . $db->q($id));
$db->setQuery($query)->execute();
$this->triggerEvent('onAfterDelete', array(&$id));
$this->reset();
return $this;
}
/**
* Generic check for whether dependencies exist for this object in the db schema. This method is NOT used by
* default. If you want to use it you will have to override your delete(), trash() or forceDelete() method,
* or create an onBeforeDelete and/or onBeforeTrash event handler.
*
* @param integer $oid The primary key of the record to delete
* @param array $joins Any joins to foreign table, used to determine if dependent records exist
*
* @return void
*
* @throws \RuntimeException If you should not delete the record (the message tells you why)
*/
public function canDelete($oid = null, $joins = null)
{
$pkField = $this->getKeyName();
if ($oid)
{
$this->$pkField = intval($oid);
}
if(!$this->$pkField)
{
throw new \InvalidArgumentException('Master table should be loaded or an ID should be passed');
}
if (is_array($joins))
{
$db = $this->getDbo();
$query = $db->getQuery(true)
->select($db->qn('master') . '.' . $db->qn($pkField))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('master'));
$tableNo = 0;
foreach ($joins as $table)
{
// Sanity check on passed array
$check = array('idfield', 'idalias', 'name', 'joinfield', 'label');
$result = array_intersect($check, array_keys($table));
if(count($result) != count($check))
{
throw new \InvalidArgumentException('Join array missing some keys, please check the documentation');
}
$tableNo++;
$query->select(
array(
'COUNT(DISTINCT ' . $db->qn('t' . $tableNo) .
'.' . $db->qn($table['idfield']) . ') AS ' . $db->qn($table['idalias'])
)
);
$query->join('LEFT', $db->qn($table['name']) .
' AS ' . $db->qn('t' . $tableNo) .
' ON ' . $db->qn('t' . $tableNo) . '.' . $db->qn($table['joinfield']) .
' = ' . $db->qn('master') . '.' . $db->qn($pkField)
);
}
$query->where($db->qn('master') . '.' . $db->qn($pkField) . ' = ' . $db->q($this->$pkField));
$query->group($db->qn('master') . '.' . $db->qn($pkField));
$this->getDbo()->setQuery((string) $query);
$obj = $this->getDbo()->loadObject();
$msg = array();
$i = 0;
foreach ($joins as $table)
{
$pkField = $table['idalias'];
if ($obj->$pkField > 0)
{
$msg[] = \JText::_($table['label']);
}
$i++;
}
if (count($msg))
{
$option = $this->container->componentName;
$comName = $this->container->bareComponentName;
$tbl = $this->getTableName();
$tview = str_replace('#__' . $comName . '_', '', $tbl);
$prefix = $option . '_' . $tview . '_NODELETE_';
$message = '<ul>';
foreach ($msg as $key)
{
$message .= '<li>'.\JText::_(strtoupper($prefix . $key)).'</li>';
}
$message .= '</ul>';
throw new \RuntimeException($message);
}
}
}
/**
* Find and load a single record based on the provided key values. If the record is not found an exception is thrown
*
* @param array|mixed $keys An optional primary key value to load the row by, or an array of fields to match.
* If not set the "id" state variable or, if empty, the identity column's value is used
*
* @return static Self, for chaining
*
* @throws \RuntimeException When the row is not found
*/
public function findOrFail($keys = null)
{
$this->find($keys);
// We have to assign the value, since empty() is not triggering the __get magic method
// http://stackoverflow.com/questions/2045791/php-empty-on-get-accessor
$value = $this->getId();
if (empty($value))
{
throw new RecordNotLoaded;
}
return $this;
}
/**
* Method to load a row from the database by primary key. Used for JTableInterface compatibility.
*
* @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. If not
* set the instance property value is used.
* @param boolean $reset True to reset the default values before loading the new row.
*
* @return boolean True if successful. False if row not found.
*
* @link http://docs.joomla.org/JTable/load
* @since 3.2
* @throws \RuntimeException
* @throws \UnexpectedValueException
*/
public function load($keys = null, $reset = true)
{
if ($reset)
{
$this->reset();
}
try
{
$this->findOrFail($keys);
}
catch (\Exception $e)
{
return false;
}
return true;
}
/**
* Find and load a single record based on the provided key values
*
* @param array|mixed $keys An optional primary key value to load the row by, or an array of fields to match.
* If not set the "id" state variable or, if empty, the identity column's value is used
*
* @return static Self, for chaining
*/
public function find($keys = null)
{
// Execute the onBeforeLoad event
$this->triggerEvent('onBeforeLoad', array(&$keys));
// If we are not given any keys, try to get the ID from the state or the table data
if (empty($keys))
{
$id = $this->getState('id', 0);
if (empty($id))
{
$id = $this->getId();
}
if (empty($id))
{
$this->triggerEvent('onAfterLoad', array(false, &$keys));
$this->reset();
return $this;
}
$keys = array($this->idFieldName => $id);
}
elseif (!is_array($keys))
{
if (empty($keys))
{
$this->triggerEvent('onAfterLoad', array(false, &$keys));
$this->reset();
return $this;
}
$keys = array($this->idFieldName => $keys);
}
// Reset the table
$this->reset();
// Get the query
$db = $this->getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->qn($this->tableName));
// Apply key filters
foreach ($keys as $filterKey => $filterValue)
{
if ($filterKey == 'id')
{
$filterKey = $this->getIdFieldName();
}
if (array_key_exists($filterKey, $this->recordData))
{
$query->where($db->qn($filterKey) . ' = ' . $db->q($filterValue));
}
}
// Get the row
$db->setQuery($query);
try
{
$row = $db->loadAssoc();
}
catch (\Exception $e)
{
$row = null;
}
if (empty($row))
{
$this->triggerEvent('onAfterLoad', array(false, &$keys));
return $this;
}
// Bind the data
$this->bind($row);
$this->relationManager->rebase($this);
// Execute the onAfterLoad event
$this->triggerEvent('onAfterLoad', array(true, &$keys));
return $this;
}
/**
* Create a new record with the provided data
*
* @param array $data The data to use in the new record
*
* @return static Self, for chaining
*/
public function create($data)
{
return $this->reset()->bind($data)->save();
}
/**
* Return the first item found or create a new one based on the provided $data
*
* @param array $data Data for the newly created item
*
* @return static
*/
public function firstOrCreate($data)
{
$item = $this->get(true, 0, 1)->first();
if (is_null($item))
{
$item = clone $this;
$item->create($data);
}
return $item;
}
/**
* Return the first item found or throw a \RuntimeException
*
* @return static
*
* @throws \RuntimeException
*/
public function firstOrFail()
{
$item = $this->get(true, 0, 1)->first();
if (is_null($item))
{
throw new NoItemsFound(get_class($this));
}
return $item;
}
/**
* Return the first item found or create a new, blank one
*
* @return static
*/
public function firstOrNew()
{
$item = $this->get(true, 0, 1)->first();
if (is_null($item))
{
$item = clone $this;
$item->reset();
}
return $item;
}
/**
* Adds a behaviour by its name. It will search the following classes, in this order:
* \component_namespace\Model\modelName\Behaviour\behaviourName
* \component_namespace\Model\Behaviour\behaviourName
* \FOF30\Model\DataModel\Behaviour\behaviourName
* where:
* component_namespace is the namespace of the component as defined in the container
* modelName is the model's name, first character uppercase, e.g. Baz
* behaviourName is the $behaviour parameter, first character uppercase, e.g. Something
*
* @param string $behaviour The behaviour's name
*
* @return $this Self, for chaining
*/
public function addBehaviour($behaviour)
{
$prefixes = array(
$this->container->getNamespacePrefix() . 'Model\\Behaviour\\' . ucfirst($this->getName()),
$this->container->getNamespacePrefix() . 'Model\\Behaviour',
'\\FOF30\\Model\\DataModel\\Behaviour',
);
foreach ($prefixes as $prefix)
{
$className = $prefix . '\\' . ucfirst($behaviour);
if (class_exists($className, true) && !$this->behavioursDispatcher->hasObserverClass($className))
{
/** @var Observer $o */
$observer = new $className($this->behavioursDispatcher);
$this->behavioursDispatcher->attach($observer);
return $this;
}
}
return $this;
}
/**
* Removes a behaviour by its name. It will search the following classes, in this order:
* \component_namespace\Model\modelName\Behaviour\behaviourName
* \component_namespace\Model\DataModel\Behaviour\behaviourName
* \FOF30\Model\DataModel\Behaviour\behaviourName
* where:
* component_namespace is the namespace of the component as defined in the container
* modelName is the model's name, first character uppercase, e.g. Baz
* behaviourName is the $behaviour parameter, first character uppercase, e.g. Something
*
* @param string $behaviour The behaviour's name
*
* @return $this Self, for chaining
*/
public function removeBehaviour($behaviour)
{
$prefixes = array(
$this->container->getNamespacePrefix() . 'Model\\Behaviour\\' . ucfirst($this->getName()),
$this->container->getNamespacePrefix() . 'Model\\Behaviour',
'\\FOF30\\Model\\DataModel\\Behaviour',
);
foreach ($prefixes as $prefix)
{
$className = ltrim($prefix . '\\' . ucfirst($behaviour), '\\');
$observer = $this->behavioursDispatcher->getObserverByClass($className);
if (is_null($observer))
{
continue;
}
$this->behavioursDispatcher->detach($observer);
return $this;
}
return $this;
}
/**
* Gives you access to the behaviours dispatcher, allowing to attach/detach behaviour observers
*
* @return Dispatcher
*/
public function &getBehavioursDispatcher()
{
return $this->behavioursDispatcher;
}
/**
* Set the field and direction of ordering for the query returned by buildQuery.
* Alias of $this->setState('filter_order', $fieldName) and $this->setState('filter_order_Dir', $direction)
*
* @param string $fieldName The field name to order by
* @param string $direction The direction to order by (ASC for ascending or DESC for descending)
*
* @return $this For chaining
*/
public function orderBy($fieldName, $direction = 'ASC')
{
$direction = strtoupper($direction);
if (!in_array($direction, array('ASC', 'DESC')))
{
$direction = 'ASC';
}
$this->setState('filter_order', $fieldName);
$this->setState('filter_order_Dir', $direction);
return $this;
}
/**
* Set the limitStart for the query, i.e. how many records to skip.
* Alias of $this->setState('limitstart', $limitStart);
*
* @param integer $limitStart Records to skip from the start
*
* @return $this For chaining
*/
public function skip($limitStart = null)
{
// Only positive integers are allowed
if(!is_int($limitStart) || $limitStart < 0 || !$limitStart)
{
$limitStart = 0;
}
$this->setState('limitstart', $limitStart);
return $this;
}
/**
* Set the limit for the query, i.e. how many records to return.
* Alias of $this->setState('limit', $limit);
*
* @param integer $limit Maximum number of records to return
*
* @return $this For chaining
*/
public function take($limit = null)
{
// Only positive integers are allowed
if(!is_int($limit) || $limit < 0 || !$limit)
{
$limit = 0;
}
$this->setState('limit', $limit);
return $this;
}
/**
* Return the record's data as an array
*
* @return array
*/
public function toArray()
{
return $this->recordData;
}
/**
* Returns the record's data as a JSON string
*
* @param boolean $prettyPrint Should I format the JSON for pretty printing
*
* @return string
*/
public function toJson($prettyPrint = false)
{
if (defined('JSON_PRETTY_PRINT'))
{
$options = $prettyPrint ? JSON_PRETTY_PRINT : 0;
}
else
{
$options = 0;
}
return json_encode($this->recordData, $options);
}
/**
* Touch a record, updating its modified_on and/or modified_by columns
*
* @param integer $userId Optional user ID of the user touching the record
*
* @return $this Self, for chaining
*/
public function touch($userId = null)
{
if(!$this->getId())
{
throw new RecordNotLoaded("Can't touch a not loaded DataModel");
}
if (!$this->hasField('modified_on') && !$this->hasField('modified_by'))
{
return $this;
}
$db = $this->getDbo();
$date = new Date();
// Update the created_on / modified_on
if ($this->hasField('modified_on'))
{
$modified_on = $this->getFieldAlias('modified_on');
$this->$modified_on = $date->toSql(false, $db);
}
// Update the created_by / modified_by values if necessary
if ($this->hasField('modified_by'))
{
if (empty($userId))
{
$userId = $this->container->platform->getUser()->id;
}
$modified_by = $this->getFieldAlias('modified_by');
$this->$modified_by = $userId;
}
$this->save();
return $this;
}
/**
* Lock a record by setting its locked_on and/or locked_by columns
*
* @param integer $userId
*
* @return $this Self, for chaining
*/
public function lock($userId = null)
{
if(!$this->getId())
{
throw new CannotLockNotLoadedRecord;
}
if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
{
return $this;
}
$this->triggerEvent('onBeforeLock', array());
$db = $this->getDbo();
if ($this->hasField('locked_on'))
{
$date = new Date();
$locked_on = $this->getFieldAlias('locked_on');
$this->$locked_on = $date->toSql(false, $db);
}
if ($this->hasField('locked_by'))
{
if (empty($userId))
{
$userId = $this->container->platform->getUser()->id;
}
$locked_by = $this->getFieldAlias('locked_by');
$this->$locked_by = $userId;
}
$this->save();
$this->triggerEvent('onAfterLock');
return $this;
}
/**
* Unlock a record by resetting its locked_on and/or locked_by columns
*
* @return $this Self, for chaining
*/
public function unlock()
{
if(!$this->getId())
{
throw new RecordNotLoaded("Can't unlock a not loaded DataModel");
}
if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
{
return $this;
}
$this->triggerEvent('onBeforeUnlock', array());
$db = $this->getDbo();
if ($this->hasField('locked_on'))
{
$locked_on = $this->getFieldAlias('locked_on');
$this->$locked_on = $db->getNullDate();
}
if ($this->hasField('locked_by'))
{
$locked_by = $this->getFieldAlias('locked_by');
$this->$locked_by = 0;
}
$this->save();
$this->triggerEvent('onAfterUnlock');
return $this;
}
/**
* Is this record locked by a different user than $userId?
*
* @param integer $userId
*
* @return bool True if the record is locked
*/
public function isLocked($userId = null)
{
if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
{
return false;
}
$nullDate = $this->getDbo()->getNullDate();
// Get the locked_by / locked_on
$locked_on = $nullDate;
$locked_by = 0;
if ($this->hasField('locked_on'))
{
$locked_on = $this->getFieldValue('locked_on', $nullDate);
if (empty($locked_on))
{
$locked_on = $nullDate;
}
}
if ($this->hasField('locked_by'))
{
$locked_by = $this->getFieldValue('locked_by', 0);
if (empty($locked_by))
{
$locked_by = 0;
}
}
$allowedUsers = array(0);
if (!empty($userId))
{
$allowedUsers[] = $userId;
}
if (in_array($locked_by, $allowedUsers))
{
return false;
}
return $locked_on != $nullDate;
}
/**
* Automatically uses the Filters behaviour to filter records in the model based on your criteria.
*
* @param string $fieldName The field name to filter on
* @param string $method The filtering method, e.g. <>, =, != and so on
* @param mixed $values The value you're filtering on. Some filters (e.g. interval or between) require an array of values
*
* @return $this For chaining
*/
public function where($fieldName, $method = '=', $values = null)
{
// Make sure the Filters behaviour is added to the model
if (!$this->behavioursDispatcher->hasObserverClass('FOF30\\Model\\DataModel\\Behaviour\\Filters'))
{
$this->addBehaviour('filters');
}
// If we are dealing with the primary key, let's set the field name to "id". This is a convention and it will
// be used inside the Filters behaviour
// -- Let's not do this. The Filters behaviour works just fine with the regular field name!
/**
if ($fieldName == $this->getIdFieldName())
{
$fieldName = 'id';
}
**/
$options = array(
'method' => $method,
'value' => $values
);
// Handle method aliases
switch ($method)
{
case '<>':
$options['method'] = 'search';
$options['operator'] = '!=';
break;
case 'lt':
$options['method'] = 'search';
$options['operator'] = '<';
break;
case 'le':
$options['method'] = 'search';
$options['operator'] = '<=';
break;
case 'gt':
$options['method'] = 'search';
$options['operator'] = '>';
break;
case 'ge':
$options['method'] = 'search';
$options['operator'] = '>=';
break;
case 'eq':
$options['method'] = 'search';
$options['operator'] = '=';
break;
case 'neq':
case 'ne':
$options['method'] = 'search';
$options['operator'] = '!=';
break;
case '<':
case '!<':
case '<=':
case '!<=':
case '>':
case '!>':
case '>=':
case '!>=':
case '!=':
case '=':
$options['method'] = 'search';
$options['operator'] = $method;
break;
case 'like':
case '~':
case '%':
$options['method'] = 'partial';
break;
case '==':
case '=[]':
case '=()':
case 'in':
$options['method'] = 'exact';
break;
case '()':
case '[]':
case '[)':
case '(]':
$options['method'] = 'between';
break;
case ')(':
case ')[':
case '](':
case '][':
$options['method'] = 'outside';
break;
case '*=':
case 'every':
$options['method'] = 'interval';
break;
case '?=':
$options['method'] = 'search';
break;
default:
throw new InvalidSearchMethod('Method '.$method.' is unsupported');
break;
}
// Handle real methods
switch ($options['method'])
{
case 'between':
case 'outside':
if (is_array($values) && (count($values) > 1))
{
// Get the from and to values from the $values array
if (isset($values['from']) && isset($values['to']))
{
$options['from'] = $values['from'];
$options['to'] = $values['to'];
}
else
{
$options['from'] = array_shift($values);
$options['to'] = array_shift($values);
}
unset($options['value']);
}
else
{
// $values is not a from/to array. Treat as = (between) or != (outside)
if (is_array($values))
{
$values = array_shift($values);
}
$options['operator'] = ($options['method'] == 'between') ? '=' : '!=';
$options['value'] = $values;
$options['method'] = 'search';
}
break;
case 'interval':
if (is_array($values) && (count($values) > 1))
{
// Get the value and interval from the $values array
if (isset($values['value']) && isset($values['interval']))
{
$options['value'] = $values['value'];
$options['interval'] = $values['interval'];
}
else
{
$options['value'] = array_shift($values);
$options['interval'] = array_shift($values);
}
}
else
{
// $values is not a value/interval array. Treat as =
if (is_array($values))
{
$values = array_shift($values);
}
$options['value'] = $values;
$options['method'] = 'search';
$options['operator'] = '=';
}
break;
case 'search':
// We don't have to do anything if the operator is already set
if (isset($options['operator']))
{
break;
}
if (is_array($values) && (count($values) > 1))
{
// Get the operator and value from the $values array
if (isset($values['operator']) && isset($values['value']))
{
$options['operator'] = $values['operator'];
$options['value'] = $values['value'];
}
else
{
$options['operator'] = array_shift($values);
$options['value'] = array_shift($values);
}
}
break;
}
$this->setState($fieldName, $options);
return $this;
}
/**
* Add custom, pre-compiled WHERE clauses for use in buildQuery. The raw WHERE clause you specify is added as is to
* the query generated by buildQuery. You are responsible for quoting and escaping the field names and data found
* inside the WHERE clause.
*
* Using this method is a generally bad idea. You are better off overriding buildQuery and using state variables to
* customise the query build built instead of using this method to push raw SQL to the query builder. Mixing your
* business logic with raw SQL makes your application harder to maintain and refactor as dependencies to your
* database schema creep in areas of your code that should have nothing to do with it.
*
* @param string $rawWhereClause The raw WHERE clause to add
*
* @return $this For chaining
*/
public function whereRaw($rawWhereClause)
{
$this->whereClauses[] = $rawWhereClause;
return $this;
}
/**
* Instructs the model to eager load the specified relations. The $relations array can have the format:
*
* array('relation1', 'relation2')
* Eager load relation1 and relation2 without any callbacks
* array('relation1' => $callable1, 'relation2' => $callable2)
* Eager load relation1 with callback $callable1 etc
* array('relation1', 'relation2' => $callable2)
* Eager load relation1 without a callback, relation2 with callback $callable2
*
* The callback must have the signature function(\JDatabaseQuery $query) and doesn't return a value. It is
* supposed to modify the query directly.
*
* Please note that eager loaded relations produce their queries without going through the respective model. Instead
* they generate a SQL query directly, then map the loaded results into a DataCollection.
*
* @param array $relations The relations to eager load. See above for more information.
*
* @return $this For chaining
*/
public function with(array $relations)
{
if (empty($relations))
{
$this->eagerRelations = array();
return $this;
}
$knownRelations = $this->relationManager->getRelationNames();
foreach ($relations as $k => $v)
{
if (is_callable($v))
{
$relName = $k;
$callback = $v;
}
else
{
$relName = $v;
$callback = null;
}
if (in_array($relName, $knownRelations))
{
$this->eagerRelations[$relName] = $callback;
}
}
return $this;
}
/**
* Filter the model based on the fulfilment of relations. For example:
* $posts->has('comments', '>=', 10)->get();
* will return all posts with at least 10 comments.
*
* @param string $relation The relation to query
* @param string $operator The comparison operator. Same operators as the where() method.
* @param mixed $value The value(s) to compare against.
* @param bool $replace When true (default) any existing relation filters for the same relation will be replaced
*
* @return $this
*/
public function has($relation, $operator = '>=', $value = 1, $replace = true)
{
// Make sure the Filters behaviour is added to the model
if (!$this->behavioursDispatcher->hasObserverClass('FOF30\\Model\\DataModel\\Behaviour\\RelationFilters'))
{
$this->addBehaviour('relationFilters');
}
$filter = array(
'relation' => $relation,
'method' => $operator,
'operator' => $operator,
'value' => $value
);
// Handle method aliases
switch ($operator)
{
case '<>':
$filter['method'] = 'search';
$filter['operator'] = '!=';
break;
case 'lt':
$filter['method'] = 'search';
$filter['operator'] = '<';
break;
case 'le':
$filter['method'] = 'search';
$filter['operator'] = '<=';
break;
case 'gt':
$filter['method'] = 'search';
$filter['operator'] = '>';
break;
case 'ge':
$filter['method'] = 'search';
$filter['operator'] = '>=';
break;
case 'eq':
$filter['method'] = 'search';
$filter['operator'] = '=';
break;
case 'neq':
case 'ne':
$filter['method'] = 'search';
$filter['operator'] = '!=';
break;
case '<':
case '!<':
case '<=':
case '!<=':
case '>':
case '!>':
case '>=':
case '!>=':
case '!=':
case '=':
$filter['method'] = 'search';
$filter['operator'] = $operator;
break;
case 'like':
case '~':
case '%':
$filter['method'] = 'partial';
break;
case '==':
case '=[]':
case '=()':
case 'in':
$filter['method'] = 'exact';
break;
case '()':
case '[]':
case '[)':
case '(]':
$filter['method'] = 'between';
break;
case ')(':
case ')[':
case '](':
case '][':
$filter['method'] = 'outside';
break;
case '*=':
case 'every':
$filter['method'] = 'interval';
break;
case '?=':
$filter['method'] = 'search';
break;
case 'callback':
$filter['method'] = 'callback';
$filter['operator'] = 'callback';
break;
default:
throw new InvalidSearchMethod('Operator '.$operator.' is unsupported');
break;
}
// Handle real methods
switch ($filter['method'])
{
case 'between':
case 'outside':
if (is_array($value) && (count($value) > 1))
{
// Get the from and to values from the $value array
if (isset($value['from']) && isset($value['to']))
{
$filter['from'] = $value['from'];
$filter['to'] = $value['to'];
}
else
{
$filter['from'] = array_shift($value);
$filter['to'] = array_shift($value);
}
unset($filter['value']);
}
else
{
// $value is not a from/to array. Treat as = (between) or != (outside)
if (is_array($value))
{
$value = array_shift($value);
}
$filter['operator'] = ($filter['method'] == 'between') ? '=' : '!=';
$filter['value'] = $value;
$filter['method'] = 'search';
}
break;
case 'interval':
if (is_array($value) && (count($value) > 1))
{
// Get the value and interval from the $value array
if (isset($value['value']) && isset($value['interval']))
{
$filter['value'] = $value['value'];
$filter['interval'] = $value['interval'];
}
else
{
$filter['value'] = array_shift($value);
$filter['interval'] = array_shift($value);
}
}
else
{
// $value is not a value/interval array. Treat as =
if (is_array($value))
{
$value = array_shift($value);
}
$filter['value'] = $value;
$filter['method'] = 'search';
$filter['operator'] = '=';
}
break;
case 'search':
// We don't have to do anything if the operator is already set
if (isset($filter['operator']))
{
break;
}
if (is_array($value) && (count($value) > 1))
{
// Get the operator and value from the $value array
if (isset($value['operator']) && isset($value['value']))
{
$filter['operator'] = $value['operator'];
$filter['value'] = $value['value'];
}
else
{
$filter['operator'] = array_shift($value);
$filter['value'] = array_shift($value);
}
}
break;
case 'callback':
if (!is_callable($filter['value']))
{
$filter['method'] = 'search';
$filter['operator'] = '=';
$filter['value'] = 1;
}
break;
}
if ($replace && !empty($this->relationFilters))
{
foreach ($this->relationFilters as $k => $v)
{
if ($v['relation'] == $relation)
{
unset ($this->relationFilters[$k]);
}
}
}
$this->relationFilters[] = $filter;
return $this;
}
/**
* Advanced model filtering on the fulfilment of relations. Unlike has() you can provide your own callback which
* modifies the COUNT subquery used to compare against the relation. The $callBack has the signature
* function(\JDatabaseQuery $query)
* and MUST return a string. The $query you are passed is the COUNT subquery of the relation, e.g.
* SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id
* You have to return a WHERE clause for the model's query, e.g.
* (SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id) BETWEEN 1 AND 20
*
* @param string $relation The relation to query against
* @param callable $callBack The callback to use for filtering
* @param bool $replace When true (default) any existing relation filters for the same relation will be replaced
*
* @return $this
*/
public function whereHas($relation, $callBack, $replace = true)
{
$this->has($relation, 'callback', $callBack, $replace);
return $this;
}
/**
* Returns the relations manager of the model
*
* @return RelationManager
*/
public function &getRelations()
{
return $this->relationManager;
}
/**
* Gets the relation filter definitions, for use by the RelationFilters behaviour
*
* @return array
*/
public function getRelationFilters()
{
return $this->relationFilters;
}
/**
* Returns the list of relations which are touched by save() and touch()
*
* @return array
*/
public function &getTouches()
{
return $this->touches;
}
/**
* Method to set rules for the record.
*
* @param mixed $input A JAccessRules object, JSON string, or array.
*
* @return void
*/
public function setRules($input)
{
if ($input instanceof \JAccessRules)
{
$this->_rules = $input;
}
else
{
$this->_rules = new \JAccessRules($input);
}
}
/**
* Method to get the rules for the record.
*
* @return \JAccessRules object
*/
public function getRules()
{
return $this->_rules;
}
/**
* Method to check if the record is treated as an ACL asset
*
* @return boolean [description]
*/
public function isAssetsTracked()
{
return $this->_trackAssets;
}
/**
* Method to manually set this record as ACL asset or not.
* We have to do this since the automatic check is made in the constructor, but here we can't set any alias.
* So, even if you have an alias for `asset_id`, it wouldn't be reconized and assets won't be tracked.
*
* @param $state
*/
public function setAssetsTracked($state)
{
$state = (bool) $state;
if ($state)
{
\JLoader::import('joomla.access.rules');
}
$this->_trackAssets = $state;
}
/**
* Gets the has tags switch state
*
* @return bool
*/
public function hasTags()
{
return $this->_has_tags;
}
/**
* Sets the has tags switch state
*
* @param bool $newState
*/
public function setHasTags($newState = false)
{
$this->_has_tags = $newState;
}
/**
* Loads the asset table related to this table.
* This will help tests, too, since we can mock this function.
*
* @return bool|\JTableAsset False on failure, otherwise JTableAsset
*/
protected function getAsset()
{
$name = $this->getAssetName();
// Do NOT touch JTable here -- we are loading the core asset table which is a JTable, not a F0FTable
$asset = \JTable::getInstance('Asset');
if (!$asset->loadByName($name))
{
return false;
}
return $asset;
}
/**
* Method to compute the default name of the asset.
* The default name is in the form table_name.id
* where id is the value of the primary key of the table.
*
* @throws NoAssetKey
*
* @return string
*/
public function getAssetName()
{
$k = $this->getKeyName();
// If there is no assetKey defined, stop here, or we'll get a wrong name
if (!$this->_assetKey || !$this->$k)
{
throw new NoAssetKey;
}
return $this->_assetKey . '.' . (int) $this->$k;
}
/**
* Method to compute the default name of the asset.
* The default name is in the form table_name.id
* where id is the value of the primary key of the table.
*
* @return string
*/
public function getAssetKey()
{
return $this->_assetKey;
}
/**
* Method to return the title to use for the asset table. In
* tracking the assets a title is kept for each asset so that there is some
* context available in a unified access manager. Usually this would just
* return $this->title or $this->name or whatever is being used for the
* primary name of the row. If this method is not overridden, the asset name is used.
*
* @return string The string to use as the title in the asset table.
*
* @codeCoverageIgnore
*/
public function getAssetTitle()
{
return $this->getAssetName();
}
/**
* Method to get the parent asset under which to register this one.
* By default, all assets are registered to the ROOT node with ID,
* which will default to 1 if none exists.
* The extended class can define a table and id to lookup. If the
* asset does not exist it will be created.
*
* @param DataModel $model A model object for the asset parent.
* @param integer $id Id to look up
*
* @return integer
*/
public function getAssetParentId($model = null, $id = null)
{
if ($model) {}; // Prevent phpStorm's inspections from freaking out
if ($id) {}; // Prevent phpStorm's inspections from freaking out
// For simple cases, parent to the asset root.
$assets = \JTable::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo()));
$rootId = $assets->getRootId();
if (!empty($rootId))
{
return $rootId;
}
return 1;
}
/**
* This method sets the asset key for the items of this table. Obviously, it
* is only meant to be used when you have a table with an asset field.
*
* @param string $assetKey The name of the asset key to use
*
* @return void
*/
public function setAssetKey($assetKey)
{
$this->_assetKey = $assetKey;
}
/**
* Method to load a row for editing from the version history table.
*
* @param integer $version_id Key to the version history table.
* @param string $alias The type_alias in #__content_types
*
* @return boolean True on success
*
* @since 2.3
*
* @throws RecordNotLoaded
* @throws BaseException
*/
public function loadhistory($version_id, $alias)
{
// Only attempt to check the row in if it exists.
if (!$version_id)
{
throw new RecordNotLoaded;
}
// Get an instance of the row to checkout.
$historyTable = \JTable::getInstance('Contenthistory');
if (!$historyTable->load($version_id))
{
throw new BaseException($historyTable->getError());
}
$rowArray = ArrayHelper::fromObject(json_decode($historyTable->version_data));
$typeId = \JTable::getInstance('Contenttype')->getTypeId($alias);
if ($historyTable->ucm_type_id != $typeId)
{
$key = $this->getKeyName();
if (isset($rowArray[$key]))
{
$this->{$this->idFieldName} = $rowArray[$key];
$this->unlock();
}
throw new BaseException(\JText::_('JLIB_APPLICATION_ERROR_HISTORY_ID_MISMATCH'));
}
$this->setState('save_date', $historyTable->save_date);
$this->setState('version_note', $historyTable->version_note);
$this->bind($rowArray);
return true;
}
/**
* Applies view access level filtering for the specified user. Useful to
* filter a front-end items listing.
*
* @param integer $userID The user ID to use. Skip it to use the currently logged in user.
*
* @return DataModel Reference to self
*/
public function applyAccessFiltering($userID = null)
{
if (!$this->hasField('access'))
{
return $this;
}
$user = $this->container->platform->getUser($userID);
$accessField = $this->getFieldAlias('access');
$this->setState($accessField, $user->getAuthorisedViewLevels());
return $this;
}
/**
* Get the content type for ucm
*
* @return string The content type alias
*
* @throws \Exception If you have not set the contentType configuration variable
*/
public function getContentType()
{
if ($this->contentType)
{
return $this->contentType;
}
throw new NoContentType(get_class($this));
}
/**
* Check if a UCM content type exists for this resource, and
* create it if it does not
*
* @param string $alias The content type alias (optional)
*
* @return null
*/
public function checkContentType($alias = null)
{
$contentType = new \JTableContenttype($this->getDbo());
if (!$alias)
{
$alias = $this->getContentType();
}
$aliasParts = explode('.', $alias);
// Fetch the extension name
$component = $aliasParts[0];
$component = \JComponentHelper::getComponent($component);
// Fetch the name using the menu item
$query = $this->getDbo()->getQuery(true);
$query->select('title')->from('#__menu')->where('component_id = ' . (int) $component->id);
$this->getDbo()->setQuery($query);
$component_name = \JText::_($this->getDbo()->loadResult());
$name = $component_name . ' ' . ucfirst($aliasParts[1]);
// Create a new content type for our resource
if (!$contentType->load(array('type_alias' => $alias)))
{
$contentType->type_title = $name;
$contentType->type_alias = $alias;
$contentType->table = json_encode(
array(
'special' => array(
'dbtable' => $this->getTableName(),
'key' => $this->getKeyName(),
'type' => $name,
'prefix' => $this->container->getNamespacePrefix() . '\\Model\\',
'class' => $this->getName(),
'config' => 'array()'
),
'common' => array(
'dbtable' => '#__ucm_content',
'key' => 'ucm_id',
'type' => 'CoreContent',
'prefix' => 'JTable',
'config' => 'array()'
)
)
);
$contentType->field_mappings = json_encode(
array(
'common' => array(
0 => array(
"core_content_item_id" => $this->getKeyName(),
"core_title" => $this->getUcmCoreAlias('title'),
"core_state" => $this->getUcmCoreAlias('enabled'),
"core_alias" => $this->getUcmCoreAlias('alias'),
"core_created_time" => $this->getUcmCoreAlias('created_on'),
"core_modified_time" => $this->getUcmCoreAlias('created_by'),
"core_body" => $this->getUcmCoreAlias('body'),
"core_hits" => $this->getUcmCoreAlias('hits'),
"core_publish_up" => $this->getUcmCoreAlias('publish_up'),
"core_publish_down" => $this->getUcmCoreAlias('publish_down'),
"core_access" => $this->getUcmCoreAlias('access'),
"core_params" => $this->getUcmCoreAlias('params'),
"core_featured" => $this->getUcmCoreAlias('featured'),
"core_metadata" => $this->getUcmCoreAlias('metadata'),
"core_language" => $this->getUcmCoreAlias('language'),
"core_images" => $this->getUcmCoreAlias('images'),
"core_urls" => $this->getUcmCoreAlias('urls'),
"core_version" => $this->getUcmCoreAlias('version'),
"core_ordering" => $this->getUcmCoreAlias('ordering'),
"core_metakey" => $this->getUcmCoreAlias('metakey'),
"core_metadesc" => $this->getUcmCoreAlias('metadesc'),
"core_catid" => $this->getUcmCoreAlias('cat_id'),
"core_xreference" => $this->getUcmCoreAlias('xreference'),
"asset_id" => $this->getUcmCoreAlias('asset_id')
)
),
'special' => array(
0 => array(
)
)
)
);
$ignoreFields = array(
$this->getUcmCoreAlias('modified_on', null),
$this->getUcmCoreAlias('modified_by', null),
$this->getUcmCoreAlias('locked_by', null),
$this->getUcmCoreAlias('locked_on', null),
$this->getUcmCoreAlias('hits', null),
$this->getUcmCoreAlias('version', null)
);
$contentType->content_history_options = json_encode(
array(
"ignoreChanges" => array_filter($ignoreFields, 'strlen')
)
);
$contentType->router = '';
$contentType->store();
}
}
/**
* Utility methods that fetches the column name for the field.
* If it does not exists, returns a "null" string
*
* @param string $alias The alias for the column
* @param string $null What to return if no column exists
*
* @return string The column name
*/
protected function getUcmCoreAlias($alias, $null = "null")
{
if (!$this->hasField($alias))
{
return $null;
}
return $this->getFieldAlias($alias);
}
/**
* Sets the abstract XML form file name
*
* @param string $formName The abstract form file name to set, e.g. "form.default"
*
* @return void
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
public function setFormName($formName)
{
$this->formName = $formName;
}
/**
* Gets the abstract XML form file name
*
* @return string The abstract form file name, e.g. "form.default"
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
public function getFormName()
{
return $this->formName;
}
/**
* A method for getting the form from the model.
*
* @param array $data Data for the form.
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
* @param boolean $source The name of the form. If not set we'll try the form_name state variable or fall back to default.
*
* @return Form|bool A Form object on success, false on failure
*
* @since 2.0
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
public function getForm($data = array(), $loadData = true, $source = null)
{
$this->_formData = $data;
if (empty($source))
{
$source = $this->formName;
}
if (empty($source))
{
$source = 'form.' . $this->name;
}
$name = $this->container->componentName . '.' . $this->name . '.' . $source;
$options = array(
'control' => false,
'load_data' => &$loadData,
);
$this->triggerEvent('onBeforeLoadForm', array(&$name, &$source, &$options));
$form = $this->loadForm($name, $source, $options);
if (is_object($form) && ($form instanceof Form))
{
$this->triggerEvent('onAfterLoadForm', array(&$form, &$name, &$source, &$options));
return $form;
}
return false;
}
/**
* Method to get a form object.
*
* @param string $name The name of the form.
* @param string $source The form filename (e.g. form.browse)
* @param array $options Optional array of options for the form creation.
* @param boolean $clear Optional argument to force load a new form.
* @param bool|string $xpath An optional xpath to search for the fields.
*
* @return Form|bool Form object on success, False on error.
*
* @throws \Exception
*
* @see Form
* @since 2.0
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
protected function loadForm($name, $source, $options = array(), $clear = false, $xpath = false)
{
// Handle the optional arguments.
$options['control'] = isset($options['control']) ? $options['control'] : false;
// Create a signature hash.
$hash = md5($source . serialize($options));
if (!isset($this->_forms[$hash]) || $clear)
{
// Get the form.
$form = $this->container->factory->form($name, $source, $this->name, $options, false, $xpath);
if (!is_object($form))
{
$this->_forms[$hash] = false;
return false;
}
$data = array();
if (isset($options['load_data']) && $options['load_data'])
{
// Get the data for the form.
$data = $this->loadFormData();
}
// Allows data and form manipulation before preprocessing the form
$this->triggerEvent('onBeforePreprocessForm', array(&$form, &$data));
// Allow for additional modification of the form, and events to be triggered.
// We pass the data because plugins may require it.
$this->preprocessForm($form, $data);
// Allows data and form manipulation After preprocessing the form
$this->triggerEvent('onAfterPreprocessForm', array(&$form, &$data));
// Load the data into the form after the plugins have operated.
$form->bind($data);
// Store the form for later.
$this->_forms[$hash] = $form;
}
return $this->_forms[$hash];
}
/**
* Method to get the data that should be injected in the form.
*
* @return array The default data is an empty array.
*
* @since 2.0
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
protected function loadFormData()
{
if (empty($this->_formData))
{
return array();
}
else
{
return $this->_formData;
}
}
/**
* Method to allow derived classes to preprocess the form.
*
* @param Form &$form A Form object.
* @param mixed &$data The data expected for the form.
* @param string $group The name of the plugin group to import (defaults to "content").
*
* @return void
*
* @since 2.0
*
* @throws \Exception if there is an error in the form event.
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
protected function preprocessForm(Form &$form, &$data, $group = 'content')
{
// Import the appropriate plugin group.
$this->container->platform->importPlugin($group);
// Trigger the form preparation event.
$this->container->platform->runPlugins('onContentPrepareForm', array(&$form, &$data));
}
/**
* Method to validate the form data.
*
* @param Form $form The form to validate against.
* @param array $data The data to validate.
* @param string $group The name of the field group to validate.
*
* @return mixed Array of filtered data if valid, false otherwise.
*
* @throws BaseException|\Exception On validation error
*
* @see \JFormRule
* @see \JFilterInput
*
* @since 2.0
*
* @deprecated 3.1 Support for XML forms will be removed in FOF 4
*/
public function validateForm($form, $data, $group = null)
{
// Filter and validate the form data.
$data = $form->filter($data);
$return = $form->validate($data, $group);
// Check for an error.
if ($return instanceof \Exception)
{
throw $return;
}
// Check the validation results.
if ($return === false)
{
// Get the validation messages from the form.
foreach ($form->getErrors() as $message)
{
if ($message instanceof \Exception)
{
throw $message;
}
else
{
throw new BaseException($message);
}
}
return false;
}
return $data;
}
/**
* Set a behavior param
*
* @param string $name The name of the param you want to set
* @param mixed $value The value to set
*
* @return $this Self, for chaining
*/
public function setBehaviorParam($name, $value)
{
$this->_behaviorParams[$name] = $value;
return $this;
}
/**
* Get a behavior param
*
* @param string $name The name of the param you want to get
* @param mixed $default The default value returned if not set
*
* @return mixed
*/
public function getBehaviorParam($name, $default = null)
{
return isset($this->_behaviorParams[$name]) ? $this->_behaviorParams[$name] : $default;
}
/**
* Set or get the backlisted filters.
*
* Note: passing a null $list to get the filter blacklist is deprecated as of FOF 3.1. Pleas use getBlacklistFilters
* instead.
*
* @param mixed $list A filter or list of filters to backlist. If null return the list of backlisted filter
* @param boolean $reset Reset the blacklist if true
*
* @return null|array Return an array of value if $list is null
*/
public function blacklistFilters($list = null, $reset = false)
{
if (!isset($list))
{
return $this->getBehaviorParam('blacklistFilters', array());
}
if (is_string($list))
{
$list = (array) $list;
}
if (!$reset)
{
$list = array_unique(array_merge($this->getBehaviorParam('blacklistFilters', array()), $list));
}
$this->setBehaviorParam('blacklistFilters', $list);
return null;
}
/**
* Get the blacklisted filters.
*
* @return array
*/
public function getBlacklistFilters()
{
return $this->getBehaviorParam('blacklistFilters', array());
}
/**
* This method is called by Joomla! itself when it needs to update the UCM content
*
* @return bool
*/
public function updateUcmContent()
{
// Process the tags
$data = $this->getData();
$alias = $this->getContentType();
$ucmContentTable = \JTable::getInstance('Corecontent');
$ucm = new \JUcmContent($this, $alias);
$ucmData = $data ? $ucm->mapData($data) : $ucm->ucmData;
$primaryId = $ucm->getPrimaryKey($ucmData['common']['core_type_id'], $ucmData['common']['core_content_item_id']);
$result = $ucmContentTable->load($primaryId);
$result = $result && $ucmContentTable->bind($ucmData['common']);
$result = $result && $ucmContentTable->check();
$result = $result && $ucmContentTable->store();
$ucmId = $ucmContentTable->core_content_id;
return $result;
}
/**
* Add a field to the list of fields to be ignored by the check() method
*
* @param string $fieldName The field to add (can be a field alias)
*
* @return void
*/
public function addSkipCheckField($fieldName)
{
if (!is_array($this->fieldsSkipChecks))
{
$this->fieldsSkipChecks = array();
}
if (!$this->hasField($fieldName))
{
return;
}
$fieldName = $this->getFieldAlias($fieldName);
if (!in_array($fieldName, $this->fieldsSkipChecks))
{
$this->fieldsSkipChecks[] = $fieldName;
}
}
/**
* Remove a field from the list of fields to be ignored by the check() method
*
* @param string $fieldName The field to remove (can be a field alias)
*
* @return void
*/
public function removeSkipCheckField($fieldName)
{
if (!is_array($this->fieldsSkipChecks))
{
$this->fieldsSkipChecks = array();
return;
}
if (!$this->hasField($fieldName))
{
return;
}
$fieldName = $this->getFieldAlias($fieldName);
if (in_array($fieldName, $this->fieldsSkipChecks))
{
$index = array_search($fieldName, $this->fieldsSkipChecks);
unset($this->fieldsSkipChecks[$index]);
}
}
/**
* Is a field present in the list of fields to be ignored by the check() method?
*
* @param string $fieldName The field to check (can be a field alias)
*
* @return bool True if the field is skipped from checks, false if not or if the field doesn't exist.
*/
public function hasSkipCheckField($fieldName)
{
if (!is_array($this->fieldsSkipChecks))
{
$this->fieldsSkipChecks = array();
return false;
}
if (!$this->hasField($fieldName))
{
return false;
}
$fieldName = $this->getFieldAlias($fieldName);
return in_array($fieldName, $this->fieldsSkipChecks);
}
/**
* Returns all lower and upper case permutations of the database prefix
*
* @return array
*/
protected function getPrefixCasePermutations()
{
if (empty(self::$prefixCasePermutations))
{
$prefix = $this->getDbo()->getPrefix();
$suffix = '';
if (substr($prefix, -1) == '_')
{
$suffix = '_';
$prefix = substr($prefix, 0, -1);
}
$letters = str_split($prefix, 1);
$permutations = array('');
foreach ($letters as $nextLetter)
{
$lower = strtolower($nextLetter);
$upper = strtoupper($nextLetter);
$ret = array();
foreach ($permutations as $perm)
{
$ret[] = $perm . $lower;
if ($lower != $upper)
{
$ret[] = $perm . $upper;
}
$permutations = $ret;
}
}
$permutations = array_merge(array(
strtolower($prefix),
strtoupper($prefix),
), $permutations);
$permutations = array_map(function ($x) use ($suffix)
{
return $x . $suffix;
}, $permutations);
self::$prefixCasePermutations = array_unique($permutations);
}
return self::$prefixCasePermutations;
}
}
Model.php 0000644 00000034266 15234466453 0006344 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model;
use FOF30\Container\Container;
use FOF30\Input\Input;
use FOF30\Model\Exception\CannotGetName;
use FOF30\Utils\StringHelper;
defined('_JEXEC') or die;
/**
* Class Model
*
* A generic MVC model implementation
*
* @property-read \FOF30\Input\Input $input The input object (magic __get returns the Input from the Container)
*/
class Model
{
/**
* Should I save the model's state in the session?
*
* @var boolean
*/
protected $_savestate = true;
/**
* Should we ignore request data when trying to get state data not already set in the Model?
*
* @var bool
*/
protected $_ignoreRequest = false;
/**
* The model (base) name
*
* @var string
*/
protected $name;
/**
* A state object
*
* @var string
*/
protected $state;
/**
* Are the state variables already set?
*
* @var boolean
*/
protected $_state_set = false;
/**
* The container attached to the model
*
* @var Container
*/
protected $container;
/**
* The state key hash returned by getHash(). This is typically something like "com_foobar.example." (note the dot
* at the end). Always use getHash to get it and setHash to set it.
*
* @var null|string
*/
private $stateHash = null;
/**
* Public class constructor
*
* You can use the $config array to pass some configuration values to the object:
*
* state stdClass|array. The state variables of the Model.
* use_populate Boolean. When true the model will set its state from populateState() instead of the request.
* ignore_request Boolean. When true getState will not automatically load state data from the request.
*
* @param Container $container The configuration variables to this model
* @param array $config Configuration values for this model
*/
public function __construct(Container $container, array $config = array())
{
$this->container = $container;
// Set the model's name from $config
if (isset($config['name']))
{
$this->name = $config['name'];
}
// If $config['name'] is not set, auto-detect the model's name
$this->name = $this->getName();
// Do we have a configured state hash? Since 3.1.2.
if (isset($config['hash']) && !empty($config['hash']))
{
$this->setHash($config['hash']);
}
elseif (isset($config['hash_view']) && !empty($config['hash_view']))
{
$this->getHash($config['hash_view']);
}
// Set the model state
if (array_key_exists('state', $config))
{
if (is_object($config['state']))
{
$this->state = $config['state'];
}
elseif (is_array($config['state']))
{
$this->state = (object)$config['state'];
}
// Protect vs malformed state
else
{
$this->state = new \stdClass();
}
}
else
{
$this->state = new \stdClass();
}
// Set the internal state marker
if (!empty($config['use_populate']))
{
$this->_state_set = true;
}
// Set the internal state marker
if (!empty($config['ignore_request']))
{
$this->_ignoreRequest = true;
}
}
/**
* Method to get the model name
*
* The model name. By default parsed using the classname or it can be set
* by passing a $config['name'] in the class constructor
*
* @return string The name of the model
*
* @throws \RuntimeException If it's impossible to get the name
*/
public function getName()
{
if (empty($this->name))
{
$r = null;
if (!preg_match('/(.*)\\\\Model\\\\(.*)/i', get_class($this), $r))
{
throw new CannotGetName;
}
$this->name = $r[2];
}
return $this->name;
}
/**
* Get a filtered state variable
*
* @param string $key The state variable's name
* @param mixed $default The default value to return if it's not already set
* @param string $filter_type The filter type to use
*
* @return mixed The state variable's contents
*/
public function getState($key = null, $default = null, $filter_type = 'raw')
{
if (empty($key))
{
return $this->internal_getState();
}
// Get the savestate status
$value = $this->internal_getState($key);
// Value is not found in the internal state
if (is_null($value))
{
// Can I fetch it from the request?
if (!$this->_ignoreRequest)
{
$value = $this->container->platform->getUserStateFromRequest($this->getHash() . $key, $key, $this->input, $value, 'none', $this->_savestate);
// Did I get any useful value from the request?
if (is_null($value))
{
return $default;
}
}
// Nope! Let's return the default value
else
{
return $default;
}
}
if (strtoupper($filter_type) == 'RAW')
{
return $value;
}
else
{
$filter = new \JFilterInput();
return $filter->clean($value, $filter_type);
}
}
/**
* Returns a unique hash for each view, used to prefix the state variables to allow us to retrieve them from the
* state later on. If it's not already set (with setHash) it will be set in the form com_something.myModel. If you
* pass a non-empty $viewName then if it's not already set it will be instead set in the form of
* com_something.viewName.myModel which is useful when you are reusing models in multiple views and want to avoid
* state bleedover among views.
*
* Also see the hash and hash_view parameters in the constructor's options.
*
* @return string
*/
public function getHash($viewName = null)
{
if (is_null($this->stateHash))
{
$this->stateHash = ucfirst($this->container->componentName) . '.';
if (!empty($viewName))
{
$this->stateHash .= $viewName . '.';
}
$this->stateHash .= $this->getName() . '.';
}
return $this->stateHash;
}
/**
* Sets the unique hash to prefix the state variables. The hash is cleaned according to the 'CMD' input filtering,
* must end in a dot (if not a dot is added automatically) and cannot be empty.
*
* @param string $hash
*
* @return void
*
* @see self::getHash()
*/
public function setHash($hash)
{
// Clean the hash, it has to conform to 'CMD' filtering
$tempInput = new Input(array('hash' => $hash));
$hash = $tempInput->getCmd('hash', null);
if (empty($hash))
{
return;
}
if (substr($hash, -1) == '_')
{
$hash = substr($hash, 0, -1);
}
if (substr($hash, -1) != '.')
{
$hash .= '.';
}
$this->stateHash = $hash;
}
/**
* Method to get model state variables
*
* @param string $property Optional parameter name
* @param mixed $default Optional default value
*
* @return object The property where specified, the state object where omitted
*/
private function internal_getState($property = null, $default = null)
{
if (!$this->_state_set)
{
// Protected method to auto-populate the model state.
$this->populateState();
// Set the model state set flag to true.
$this->_state_set = true;
}
if (is_null($property))
{
return $this->state;
}
else
{
if (property_exists($this->state, $property))
{
return $this->state->$property;
}
else
{
return $default;
}
}
}
/**
* Method to auto-populate the model state.
*
* This method should only be called once per instantiation and is designed
* to be called on the first call to the getState() method unless the model
* configuration flag to ignore the request is set.
*
* @return void
*
* @note Calling getState in this method will result in recursion.
*/
protected function populateState()
{
}
/**
* Method to set model state variables
*
* @param string $property The name of the property.
* @param mixed $value The value of the property to set or null.
*
* @return mixed The previous value of the property or null if not set.
*/
public function setState($property, $value = null)
{
if (is_null($this->state))
{
$this->state = new \stdClass();
}
return $this->state->$property = $value;
}
/**
* Clears the model state, but doesn't touch the internal lists of records,
* record tables or record id variables. To clear these values, please use
* reset().
*
* @return static
*/
public function clearState()
{
$this->state = new \stdClass();
return $this;
}
/**
* Clones the model object and returns the clone
*
* @return $this for chaining
*/
public function getClone()
{
$clone = clone($this);
return $clone;
}
/**
* Returns a reference to the model's container
*
* @return \FOF30\Container\Container
*/
public function getContainer()
{
return $this->container;
}
/**
* Magic getter; allows to use the name of model state keys as properties. Also handles magic properties:
* $this->input mapped to $this->container->input
*
* @param string $name The state variable key
*
* @return static
*/
public function __get($name)
{
// Handle $this->input
if ($name == 'input')
{
return $this->container->input;
}
return $this->getState($name);
}
/**
* Magic setter; allows to use the name of model state keys as properties
*
* @param string $name The state variable key
* @param mixed $value The state variable value
*
* @return static
*/
public function __set($name, $value)
{
return $this->setState($name, $value);
}
/**
* Magic caller; allows to use the name of model state keys as methods to
* set their values.
*
* @param string $name The state variable key
* @param mixed $arguments The state variable contents
*
* @return static
*/
public function __call($name, $arguments)
{
$arg1 = array_shift($arguments);
$this->setState($name, $arg1);
return $this;
}
/**
* Sets the model state auto-save status. By default the model is set up to
* save its state to the session.
*
* @param boolean $newState True to save the state, false to not save it.
*
* @return static
*/
public function savestate($newState)
{
$this->_savestate = $newState ? true : false;
return $this;
}
/**
* Public setter for the _savestate variable. Set it to true to save the state
* of the Model in the session.
*
* @return static
*/
public function populateSavestate()
{
if (is_null($this->_savestate))
{
$savestate = $this->input->getInt('savestate', -999);
if ($savestate == -999)
{
$savestate = true;
}
$this->savestate($savestate);
}
}
/**
* Sets the ignore request flag. When false, getState() will try to populate state variables not already set from
* same-named state variables in the request.
*
* @param boolean $ignoreRequest
*
* @return $this for chaining
*/
public function setIgnoreRequest($ignoreRequest)
{
$this->_ignoreRequest = $ignoreRequest;
return $this;
}
/**
* Gets the ignore request flag. When false, getState() will try to populate state variables not already set from
* same-named state variables in the request.
*
* @return boolean
*/
public function getIgnoreRequest()
{
return $this->_ignoreRequest;
}
/**
* Returns a temporary instance of the model. Please note that this returns a _clone_ of the model object, not the
* original object. The new object is set up to not save its stats, ignore the request when getting state variables
* and comes with an empty state.
*
* @return $this
*/
public function tmpInstance()
{
return $this->getClone()->savestate(false)->setIgnoreRequest(true)->clearState();
}
/**
* Triggers an object-specific event. The event runs both locally –if a suitable method exists– and through the
* object's behaviours dispatcher and Joomla! plugin system. Neither handler is expected to return anything (return
* values are ignored). If you want to mark an error and cancel the event you have to raise an exception.
*
* EXAMPLE
* Component: com_foobar, Object name: item, Event: onBeforeSomething, Arguments: array(123, 456)
* The event calls:
* 1. $this->onBeforeSomething(123, 456)
* 2. $his->behavioursDispatcher->trigger('onBeforeSomething', array(&$this, 123, 456))
* 3. Joomla! plugin event onComFoobarModelItemBeforeSomething($this, 123, 456)
*
* @param string $event The name of the event, typically named onPredicateVerb e.g. onBeforeKick
* @param array $arguments The arguments to pass to the event handlers
*
* @return void
*/
protected function triggerEvent($event, array $arguments = array())
{
// If there is an object method for this event, call it
if (method_exists($this, $event))
{
switch (count($arguments))
{
case 0:
$this->{$event}();
break;
case 1:
$this->{$event}($arguments[0]);
break;
case 2:
$this->{$event}($arguments[0], $arguments[1]);
break;
case 3:
$this->{$event}($arguments[0], $arguments[1], $arguments[2]);
break;
case 4:
$this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
break;
case 5:
$this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
break;
default:
call_user_func_array(array($this, $event), $arguments);
break;
}
}
// All other event handlers live outside this object, therefore they need to be passed a reference to this
// objects as the first argument.
array_unshift($arguments, $this);
// Trigger the object's behaviours dispatcher, if such a thing exists
if (property_exists($this, 'behavioursDispatcher') && method_exists($this->behavioursDispatcher, 'trigger'))
{
$this->behavioursDispatcher->trigger($event, $arguments);
}
// Prepare to run the Joomla! plugins now.
// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
$prefix = '';
if (substr($event, 0, 2) == 'on')
{
$prefix = 'on';
$event = substr($event, 2);
}
// Get the component/model prefix for the event
$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'Model';
$prefix .= ucfirst($this->getName());
// The event name will be something like onComFoobarItemsBeforeSomething
$event = $prefix . $event;
// Call the Joomla! plugins
$this->container->platform->runPlugins($event, $arguments);
}
}
TreeModel.php 0000644 00000163222 15234466453 0007157 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 2 or later
*/
namespace FOF30\Model;
use FOF30\Container\Container;
use FOF30\Model\DataModel\Exception\TreeIncompatibleTable;
use FOF30\Model\DataModel\Exception\TreeInvalidLftRgtCurrent;
use FOF30\Model\DataModel\Exception\TreeInvalidLftRgtOther;
use FOF30\Model\DataModel\Exception\TreeInvalidLftRgtParent;
use FOF30\Model\DataModel\Exception\TreeInvalidLftRgtSibling;
use FOF30\Model\DataModel\Exception\TreeMethodOnlyAllowedInRoot;
use FOF30\Model\DataModel\Exception\TreeRootNotFound;
use FOF30\Model\DataModel\Exception\TreeUnexpectedPrimaryKey;
use FOF30\Model\DataModel\Exception\TreeUnsupportedMethod;
defined('_JEXEC') or die;
/**
* A DataModel which implements nested trees
*
* @property int $lft Left value (for nested set implementation)
* @property int $rgt Right value (for nested set implementation)
* @property string $hash Slug hash (for faster searching)
*/
class TreeModel extends DataModel
{
/** @var int The level (depth) of this node in the tree */
protected $treeDepth = null;
/** @var TreeModel The root node in the tree */
protected $treeRoot = null;
/** @var TreeModel The parent node of ourselves */
protected $treeParent = null;
/** @var bool Should I perform a nested get (used to query ascendants/descendants) */
protected $treeNestedGet = false;
/**
* Public constructor. Overrides the parent constructor, making sure there are lft/rgt columns which make it
* compatible with nested sets.
*
* @see \FOF30\Model\DataModel::__construct()
*
* @param Container $container The configuration variables to this model
* @param array $config Configuration values for this model
*
* @throws \RuntimeException When lft/rgt columns are not found
*/
public function __construct(Container $container = null, array $config = array())
{
parent::__construct($container, $config);
if (!$this->hasField('lft') || !$this->hasField('rgt'))
{
throw new TreeIncompatibleTable($this->tableName);
}
}
/**
* Overrides the automated table checks to handle the 'hash' column for faster searching
*
* @return $this|DataModel
*/
public function check()
{
// Create a slug if there is a title and an empty slug
if ($this->hasField('title') && $this->hasField('slug') && !$this->slug)
{
$this->slug = \JApplicationHelper::stringURLSafe($this->title);
}
// Create the SHA-1 hash of the slug for faster searching (make sure the hash column is CHAR(64) to take
// advantage of MySQL's optimised searching for fixed size CHAR columns)
if ($this->hasField('hash') && $this->hasField('slug'))
{
$this->hash = sha1($this->slug);
}
// Reset cached values
$this->resetTreeCache();
// Run the parent checks
parent::check();
return $this;
}
/**
* Delete a node, either the currently loaded one or the one specified in $id. If an $id is specified that node
* is loaded before trying to delete it. In the end the data model is reset. If the node has any children nodes
* they will be removed before the node itself is deleted.
*
* @param mixed $id Primary key (id field) value
*
* @throws \UnexpectedValueException
*
* @return $this for chaining
*/
public function forceDelete($id = null)
{
// Load the specified record (if necessary)
if (!empty($id))
{
$this->findOrFail($id);
}
$k = $this->getIdFieldName();
$pk = (!$id) ? $this->$k : $id;
// If no primary key is given, return false.
if (!$pk)
{
throw new TreeUnexpectedPrimaryKey;
}
// Execute the logic only if I have a primary key, otherwise I could have weird results
// Perform the checks on the current node *BEFORE* starting to delete the children
try
{
$this->triggerEvent('onBeforeDelete', array(&$pk));
}
catch (\Exception $e)
{
return false;
}
$result = true;
// Recursively delete all children nodes as long as we are not a leaf node
if (!$this->isLeaf())
{
// Get all sub-nodes
$table = $this->getClone();
$table->bind($this->getData());
$subNodes = $table->getDescendants();
// Delete all subnodes (goes through the model to trigger the observers)
if (!empty($subNodes))
{
/** @var TreeModel $item */
foreach ($subNodes as $item)
{
// We have to pass the id, so we are getting it again from the database.
// We have to do in this way, since a previous child could have changed our lft and rgt values
if(!$item->forceDelete($item->$k))
{
// A subnode failed or prevents the delete, continue deleting other nodes,
// but preserve the current node (ie the parent)
$result = false;
}
};
// Load it again, since while deleting a children we could have updated ourselves, too
$this->find($pk);
}
}
if($result)
{
$db = $this->getDbo();
// Delete the row by primary key.
$query = $db->getQuery(true);
$query->delete();
$query->from($this->getTableName());
$query->where($db->qn($this->getIdFieldName()) . ' = ' . $db->q($pk));
$db->setQuery($query)->execute();
$this->triggerEvent('onAfterDelete', array(&$pk));
}
return $this;
}
protected function onAfterDelete($oid)
{
$db = $this->getDbo();
$myLeft = $this->lft;
$myRight = $this->rgt;
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
// Move all siblings to the left
$width = $this->rgt - $this->lft + 1;
// Wrap everything in a transaction
$db->transactionStart();
try
{
// Shrink lft values
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($fldLft . ' = ' . $fldLft . ' - '.$width)
->where($fldLft . ' > ' . $db->q($myLeft));
$db->setQuery($query)->execute();
// Shrink rgt values
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($fldRgt . ' = ' . $fldRgt . ' - '.$width)
->where($fldRgt . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
// Roll back the transaction on error
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Not supported in nested sets
*
* @param string $where Ignored
*
* @return static Self, for chaining
*
* @throws \RuntimeException
*/
public function reorder($where = '')
{
throw new TreeUnsupportedMethod(__METHOD__);
}
/**
* Not supported in nested sets
*
* @param integer $delta Ignored
* @param string $where Ignored
*
* @return static Self, for chaining
*
* @throws \RuntimeException
*/
public function move($delta, $where = '')
{
throw new TreeUnsupportedMethod(__METHOD__);
}
/**
* Create a new record with the provided data. It is inserted as the last child of the current node's parent
*
* @param array $data The data to use in the new record
*
* @return static The new node
*/
public function create($data)
{
$newNode = $this->getClone();
$newNode->reset();
$newNode->bind($data);
if ($this->isRoot())
{
return $newNode->insertAsChildOf($this);
}
else
{
$parentNode = $this->getParent();
return $newNode->insertAsChildOf($parentNode);
}
}
/**
* Makes a copy of the record, inserting it as the last child of the current node's parent.
*
* @return static
*
* @codeCoverageIgnore
*/
public function copy($data = null)
{
$selfData = $this->toArray();
if (!is_array($data))
{
$data = array();
}
$data = array_merge($data, $selfData);
return $this->create($data);
}
/**
* Reset the record data and the tree cache
*
* @param boolean $useDefaults Should I use the default values? Default: yes
* @param boolean $resetRelations Should I reset the relations too? Default: no
*
* @return static Self, for chaining
*
* @codeCoverageIgnore
*/
public function reset($useDefaults = true, $resetRelations = false)
{
$this->resetTreeCache();
return parent::reset($useDefaults, $resetRelations);
}
/**
* Insert the current node as a tree root. It is a good idea to never use this method, instead providing a root node
* in your schema installation and then sticking to only one root.
*
* @return static
*
* @throws \RuntimeException
*/
public function insertAsRoot()
{
// You can't insert a node that is already saved i.e. the table has an id
if($this->getId())
{
throw new TreeMethodOnlyAllowedInRoot(__METHOD__);
}
// First we need to find the right value of the last parent, a.k.a. the max(rgt) of the table
$db = $this->getDbo();
// Get the lft/rgt names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$query = $db->getQuery(true)
->select('MAX(' . $fldRgt . ')')
->from($db->qn($this->tableName));
$maxRgt = $db->setQuery($query, 0, 1)->loadResult();
if (empty($maxRgt))
{
$maxRgt = 0;
}
$this->lft = ++$maxRgt;
$this->rgt = ++$maxRgt;
return $this->save();
}
/**
* Insert the current node as the first (leftmost) child of a parent node.
*
* WARNING: If it's an existing node it will be COPIED, not moved.
*
* @param TreeModel $parentNode The node which will become our parent
*
* @return $this for chaining
* @throws \Exception
* @throws \RuntimeException
*/
public function insertAsFirstChildOf(TreeModel &$parentNode)
{
if($parentNode->lft >= $parentNode->rgt)
{
throw new TreeInvalidLftRgtParent;
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$fldLft = $db->qn($this->getFieldAlias('lft'));
// Nullify the PK, so a new record will be created
$this->{$this->idFieldName} = null;
// Get the value of the parent node's rgt
$myLeft = $parentNode->lft;
// Update my lft/rgt values
$this->lft = $myLeft + 1;
$this->rgt = $myLeft + 2;
// Update parent node's right (we added two elements in there, remember?)
$parentNode->rgt += 2;
// Wrap everything in a transaction
$db->transactionStart();
try
{
// Make a hole (2 queries)
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' > ' . $db->q($myLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldRgt . ' = ' . $fldRgt . '+ 2')
->where($fldRgt . '>' . $db->q($myLeft));
$db->setQuery($query)->execute();
// Insert the new node
$this->save();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
// Roll back the transaction on error
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Insert the current node as the last (rightmost) child of a parent node.
*
* WARNING: If it's an existing node it will be COPIED, not moved.
*
* @param TreeModel $parentNode The node which will become our parent
*
* @return $this for chaining
* @throws \Exception
* @throws \RuntimeException
*/
public function insertAsLastChildOf(TreeModel &$parentNode)
{
if($parentNode->lft >= $parentNode->rgt)
{
throw new TreeInvalidLftRgtParent;
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$fldLft = $db->qn($this->getFieldAlias('lft'));
// Nullify the PK, so a new record will be created
$this->{$this->idFieldName} = null;
// Get the value of the parent node's lft
$myRight = $parentNode->rgt;
// Update my lft/rgt values
$this->lft = $myRight;
$this->rgt = $myRight + 1;
// Update parent node's right (we added two elements in there, remember?)
$parentNode->rgt += 2;
// Wrap everything in a transaction
$db->transactionStart();
try
{
// Make a hole (2 queries)
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . '>=' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . '>' . $db->q($myRight));
$db->setQuery($query)->execute();
// Insert the new node
$this->save();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
// Roll back the transaction on error
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Alias for insertAsLastchildOf
*
* @codeCoverageIgnore
* @param TreeModel $parentNode
*
* @return $this for chaining
*/
public function insertAsChildOf(TreeModel &$parentNode)
{
return $this->insertAsLastChildOf($parentNode);
}
/**
* Insert the current node to the left of (before) a sibling node
*
* WARNING: If it's an existing node it will be COPIED, not moved.
*
* @param TreeModel $siblingNode We will be inserted before this node
*
* @return $this for chaining
* @throws \Exception
* @throws \RuntimeException
*/
public function insertLeftOf(TreeModel &$siblingNode)
{
if($siblingNode->lft >= $siblingNode->rgt)
{
throw new TreeInvalidLftRgtSibling;
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$fldLft = $db->qn($this->getFieldAlias('lft'));
// Nullify the PK, so a new record will be created
$this->{$this->idFieldName} = null;
// Get the value of the parent node's rgt
$myLeft = $siblingNode->lft;
// Update my lft/rgt values
$this->lft = $myLeft;
$this->rgt = $myLeft + 1;
// Update sibling's lft/rgt values
$siblingNode->lft += 2;
$siblingNode->rgt += 2;
$db->transactionStart();
try
{
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' >= ' . $db->q($myLeft))
)->execute();
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . ' > ' . $db->q($myLeft))
)->execute();
$this->save();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Insert the current node to the right of (after) a sibling node
*
* WARNING: If it's an existing node it will be COPIED, not moved.
*
* @param TreeModel $siblingNode We will be inserted after this node
*
* @return $this for chaining
* @throws \Exception
* @throws \RuntimeException
*/
public function insertRightOf(TreeModel &$siblingNode)
{
if($siblingNode->lft >= $siblingNode->rgt)
{
throw new TreeInvalidLftRgtSibling;
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$fldLft = $db->qn($this->getFieldAlias('lft'));
// Nullify the PK, so a new record will be created
$this->{$this->idFieldName} = null;
// Get the value of the parent node's lft
$myRight = $siblingNode->rgt;
// Update my lft/rgt values
$this->lft = $myRight + 1;
$this->rgt = $myRight + 2;
$db->transactionStart();
try
{
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . ' > ' . $db->q($myRight))
)->execute();
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' > ' . $db->q($myRight))
)->execute();
$this->save();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Alias for insertRightOf
*
* @codeCoverageIgnore
* @param TreeModel $siblingNode
*
* @return $this for chaining
*/
public function insertAsSiblingOf(TreeModel &$siblingNode)
{
return $this->insertRightOf($siblingNode);
}
/**
* Move the current node (and its subtree) one position to the left in the tree, i.e. before its left-hand sibling
*
* @throws \RuntimeException
*
* @return $this
*/
public function moveLeft()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
// If it is a root node we will not move the node (roots don't participate in tree ordering)
if ($this->isRoot())
{
return $this;
}
// Are we already the leftmost node?
$parentNode = $this->getParent();
if ($parentNode->lft == ($this->lft - 1))
{
return $this;
}
// Get the sibling to the left
$db = $this->getDbo();
$leftSibling = $this->getClone()->reset()
->whereRaw($db->qn($this->getFieldAlias('rgt')) . ' = ' . $db->q($this->lft - 1))
->firstOrFail();
// Move the node
return $this->moveToLeftOf($leftSibling);
}
/**
* Move the current node (and its subtree) one position to the right in the tree, i.e. after its right-hand sibling
*
* @throws \RuntimeException
*
* @return $this
*/
public function moveRight()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
// If it is a root node we will not move the node (roots don't participate in tree ordering)
if ($this->isRoot())
{
return $this;
}
// Are we already the rightmost node?
$parentNode = $this->getParent();
if ($parentNode->rgt == ($this->rgt + 1))
{
return $this;
}
// Get the sibling to the right
$db = $this->getDbo();
$rightSibling = $this->getClone()->reset()
->whereRaw($db->qn($this->getFieldAlias('lft')) . ' = ' . $db->q($this->rgt + 1))
->firstOrFail();
// Move the node
return $this->moveToRightOf($rightSibling);
}
/**
* Moves the current node (and its subtree) to the left of another node. The other node can be in a different
* position in the tree or even under a different root.
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @throws \Exception
* @throws \RuntimeException
*/
public function moveToLeftOf(TreeModel $siblingNode)
{
// Sanity checks on current and sibling node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if($siblingNode->lft >= $siblingNode->rgt)
{
throw new TreeInvalidLftRgtSibling;
}
$db = $this->getDbo();
$left = $db->qn($this->getFieldAlias('lft'));
$right = $db->qn($this->getFieldAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$sibLeft = $siblingNode->lft;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set("$left = " . $db->q(0) . " - $left")
->set("$right = " . $db->q(0) . " - $right")
->where($left . ' >= ' . $db->q($myLeft))
->where($right . ' <= ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Close hole left behind
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Make a hole for the new items
$newSibLeft = ($sibLeft > $myRight) ? $sibLeft - $myWidth : $sibLeft;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' >= ' . $db->q($newSibLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' >= ' . $db->q($newSibLeft));
$db->setQuery($query)->execute();
// Move node and subnodes
$moveRight = $newSibLeft - $myLeft;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
->where($left . ' <= 0 - ' . $db->q($myLeft))
->where($right . ' >= 0 - ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
// Let's load the record again to fetch the new values for lft and rgt
$this->findOrFail();
return $this;
}
/**
* Moves the current node (and its subtree) to the right of another node. The other node can be in a different
* position in the tree or even under a different root.
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @throws \Exception
* @throws \RuntimeException
*/
public function moveToRightOf(TreeModel $siblingNode)
{
// Sanity checks on current and sibling node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if($siblingNode->lft >= $siblingNode->rgt)
{
throw new TreeInvalidLftRgtSibling;
}
$db = $this->getDbo();
$left = $db->qn($this->getFieldAlias('lft'));
$right = $db->qn($this->getFieldAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$sibRight = $siblingNode->rgt;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set("$left = " . $db->q(0) . " - $left")
->set("$right = " . $db->q(0) . " - $right")
->where($left . ' >= ' . $db->q($myLeft))
->where($right . ' <= ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Close hole left behind
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Make a hole for the new items
$newSibRight = ($sibRight > $myRight) ? $sibRight - $myWidth : $sibRight;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($newSibRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($newSibRight));
$db->setQuery($query)->execute();
// Move node and subnodes
$moveRight = ($sibRight > $myRight) ? $sibRight - $myRight : $sibRight - $myRight + $myWidth;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
->where($left . ' <= 0 - ' . $db->q($myLeft))
->where($right . ' >= 0 - ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
// Let's load the record again to fetch the new values for lft and rgt
$this->findOrFail();
return $this;
}
/**
* Alias for moveToRightOf
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @codeCoverageIgnore
*/
public function makeNextSiblingOf(TreeModel $siblingNode)
{
return $this->moveToRightOf($siblingNode);
}
/**
* Alias for makeNextSiblingOf
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @codeCoverageIgnore
*/
public function makeSiblingOf(TreeModel $siblingNode)
{
return $this->makeNextSiblingOf($siblingNode);
}
/**
* Alias for moveToLeftOf
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @codeCoverageIgnore
*/
public function makePreviousSiblingOf(TreeModel $siblingNode)
{
return $this->moveToLeftOf($siblingNode);
}
/**
* Moves a node and its subtree as a the first (leftmost) child of $parentNode
*
* @param TreeModel $parentNode
*
* @return $this for chaining
*
* @throws \Exception
* @throws \RuntimeException
*/
public function makeFirstChildOf(TreeModel $parentNode)
{
// Sanity checks on current and sibling node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if($parentNode->lft >= $parentNode->rgt)
{
throw new TreeInvalidLftRgtParent;
}
$db = $this->getDbo();
$left = $db->qn($this->getFieldAlias('lft'));
$right = $db->qn($this->getFieldAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$parentRight = $parentNode->rgt;
$parentLeft = $parentNode->lft;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set("$left = " . $db->q(0) . " - $left")
->set("$right = " . $db->q(0) . " - $right")
->where($left . ' >= ' . $db->q($myLeft))
->where($right . ' <= ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Close hole left behind
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Make a hole for the new items
$newParentLeft = ($parentLeft > $myRight) ? $parentLeft - $myWidth : $parentLeft;
$newParentRight = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' >= ' . $db->q($newParentLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($newParentLeft));
$db->setQuery($query)->execute();
// Move node and subnodes
$moveRight = $newParentLeft - $myLeft + 1;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
->where($left . ' <= 0 - ' . $db->q($myLeft))
->where($right . ' >= 0 - ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
// Let's load the record again to fetch the new values for lft and rgt
$this->findOrFail();
return $this;
}
/**
* Moves a node and its subtree as a the last (rightmost) child of $parentNode
*
* @param TreeModel $parentNode
*
* @return $this for chaining
*
* @throws \Exception
* @throws \RuntimeException
*/
public function makeLastChildOf(TreeModel $parentNode)
{
// Sanity checks on current and sibling node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if($parentNode->lft >= $parentNode->rgt)
{
throw new TreeInvalidLftRgtParent;
}
$db = $this->getDbo();
$left = $db->qn($this->getFieldAlias('lft'));
$right = $db->qn($this->getFieldAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$parentRight = $parentNode->rgt;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set("$left = " . $db->q(0) . " - $left")
->set("$right = " . $db->q(0) . " - $right")
->where($left . ' >= ' . $db->q($myLeft))
->where($right . ' <= ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Close hole left behind
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Make a hole for the new items
$newLeft = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' >= ' . $db->q($newLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' >= ' . $db->q($newLeft));
$db->setQuery($query)->execute();
// Move node and subnodes
$moveRight = ($parentRight > $myRight) ? $parentRight - $myRight - 1 : $parentRight - $myRight - 1 + $myWidth;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
->where($left . ' <= 0 - ' . $db->q($myLeft))
->where($right . ' >= 0 - ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
// Let's load the record again to fetch the new values for lft and rgt
$this->findOrFail();
return $this;
}
/**
* Alias for makeLastChildOf
*
* @param TreeModel $parentNode
*
* @return $this for chaining
*
* @codeCoverageIgnore
*/
public function makeChildOf(TreeModel $parentNode)
{
return $this->makeLastChildOf($parentNode);
}
/**
* Makes the current node a root (and moving its entire subtree along the way). This is achieved by moving the node
* to the right of its root node
*
* @return $this for chaining
*/
public function makeRoot()
{
// Make sure we are not a root
if ($this->isRoot())
{
return $this;
}
// Get a reference to my root
$myRoot = $this->getRoot();
// Double check I am not a root
if ($this->equals($myRoot))
{
return $this;
}
// Move myself to the right of my root
$this->moveToRightOf($myRoot);
$this->treeDepth = 0;
return $this;
}
/**
* Gets the level (depth) of this node in the tree. The result is cached in $this->treeDepth for faster fetch.
*
* @throws \RuntimeException
*
* @return int|mixed
*/
public function getLevel()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if (is_null($this->treeDepth))
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$query = $db->getQuery(true)
->select('(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth'))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
->group($db->qn('node') . '.' . $fldLft)
->order($db->qn('node') . '.' . $fldLft . ' ASC');
$this->treeDepth = $db->setQuery($query, 0, 1)->loadResult();
}
return $this->treeDepth;
}
/**
* Returns the immediate parent of the current node
*
* @throws \RuntimeException
*
* @return static
*/
public function getParent()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($this->isRoot())
{
return $this;
}
if (empty($this->treeParent) || !is_object($this->treeParent) || !($this->treeParent instanceof TreeModel))
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$query = $db->getQuery(true)
->select($db->qn('parent') . '.' . $fldLft)
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
->order($db->qn('parent') . '.' . $fldLft . ' DESC');
$targetLft = $db->setQuery($query, 1, 1)->loadResult();
$this->treeParent = $this->getClone()->reset()
->whereRaw($fldLft . ' = ' . $db->q($targetLft))
->firstOrFail();
}
return $this->treeParent;
}
/**
* Is this a top-level root node?
*
* @return bool
*/
public function isRoot()
{
// If lft=1 it is necessarily a root node
if ($this->lft == 1)
{
return true;
}
// Otherwise make sure its level is 0
return $this->getLevel() == 0;
}
/**
* Is this a leaf node (a node without children)?
*
* @throws \RuntimeException
*
* @return bool
*/
public function isLeaf()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
return ($this->rgt - 1) == $this->lft;
}
/**
* Is this a child node (not root)?
*
* @codeCoverageIgnore
*
* @return bool
*/
public function isChild()
{
return !$this->isRoot();
}
/**
* Returns true if we are a descendant of $otherNode
*
* @param TreeModel $otherNode
*
* @throws \RuntimeException
*
* @return bool
*/
public function isDescendantOf(TreeModel $otherNode)
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if($otherNode->lft >= $otherNode->rgt)
{
throw new TreeInvalidLftRgtOther;
}
return ($otherNode->lft < $this->lft) && ($otherNode->rgt > $this->rgt);
}
/**
* Returns true if $otherNode is ourselves or if we are a descendant of $otherNode
*
* @param TreeModel $otherNode
*
* @return bool
*/
public function isSelfOrDescendantOf(TreeModel $otherNode)
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if($otherNode->lft >= $otherNode->rgt)
{
throw new TreeInvalidLftRgtOther;
}
return ($otherNode->lft <= $this->lft) && ($otherNode->rgt >= $this->rgt);
}
/**
* Returns true if we are an ancestor of $otherNode
*
* @codeCoverageIgnore
* @param TreeModel $otherNode
*
* @return bool
*/
public function isAncestorOf(TreeModel $otherNode)
{
return $otherNode->isDescendantOf($this);
}
/**
* Returns true if $otherNode is ourselves or we are an ancestor of $otherNode
*
* @codeCoverageIgnore
* @param TreeModel $otherNode
*
* @return bool
*/
public function isSelfOrAncestorOf(TreeModel $otherNode)
{
return $otherNode->isSelfOrDescendantOf($this);
}
/**
* Is $node this very node?
*
* @param TreeModel $node
*
* @throws \RuntimeException
*
* @return bool
*/
public function equals(TreeModel &$node)
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if($node->lft >= $node->rgt)
{
throw new TreeInvalidLftRgtOther;
}
return (
($this->getId() == $node->getId())
&& ($this->lft == $node->lft)
&& ($this->rgt == $node->rgt)
);
}
/**
* Checks if our node is inside the subtree of $otherNode. This is a fast check as only lft and rgt values have to
* be compared.
*
* @param TreeModel $otherNode
*
* @throws \RuntimeException
*
* @return bool
*/
public function insideSubtree(TreeModel $otherNode)
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if($otherNode->lft >= $otherNode->rgt)
{
throw new TreeInvalidLftRgtOther;
}
return ($this->lft > $otherNode->lft) && ($this->rgt < $otherNode->rgt);
}
/**
* Returns true if both this node and $otherNode are root, leaf or child (same tree scope)
*
* @param TreeModel $otherNode
*
* @return bool
*/
public function inSameScope(TreeModel $otherNode)
{
if ($this->isLeaf())
{
return $otherNode->isLeaf();
}
elseif ($this->isRoot())
{
return $otherNode->isRoot();
}
elseif ($this->isChild())
{
return $otherNode->isChild();
}
else
{
return false;
}
}
/**
* get() will return all ancestor nodes and ourselves
*
* @return void
*/
protected function scopeAncestorsAndSelf()
{
$this->treeNestedGet = true;
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' >= ' . $db->qn('node') . '.' . $fldLft);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' <= ' . $db->qn('node') . '.' . $fldRgt);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
}
/**
* get() will return all ancestor nodes but not ourselves
*
* @return void
*/
protected function scopeAncestors()
{
$this->treeNestedGet = true;
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' > ' . $db->qn('node') . '.' . $fldLft);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' < ' . $db->qn('node') . '.' . $fldRgt);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
}
/**
* get() will return all sibling nodes and ourselves
*
* @return void
*/
protected function scopeSiblingsAndSelf()
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$parent = $this->getParent();
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->q($parent->lft));
$this->whereRaw($db->qn('node') . '.' . $fldRgt . ' < ' . $db->q($parent->rgt));
}
/**
* get() will return all sibling nodes but not ourselves
*
* @codeCoverageIgnore
*
* @return void
*/
protected function scopeSiblings()
{
$this->scopeSiblingsAndSelf();
$this->scopeWithoutSelf();
}
/**
* get() will return only leaf nodes
*
* @return void
*/
protected function scopeLeaves()
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' = ' . $db->qn('node') . '.' .$fldRgt . ' - ' . $db->q(1));
}
/**
* get() will return all descendants (even subtrees of subtrees!) and ourselves
*
* @return void
*/
protected function scopeDescendantsAndSelf()
{
$this->treeNestedGet = true;
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft);
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
}
/**
* get() will return all descendants (even subtrees of subtrees!) but not ourselves
*
* @return void
*/
protected function scopeDescendants()
{
$this->treeNestedGet = true;
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->qn('parent') . '.' . $fldLft);
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' < ' . $db->qn('parent') . '.' . $fldRgt);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
}
/**
* get() will only return immediate descendants (first level children) of the current node
*
* @throws \RuntimeException
*
* @return void
*/
protected function scopeImmediateDescendants()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$subQuery = $db->getQuery(true)
->select(array(
$db->qn('node') . '.' . $fldLft,
'(COUNT(*) - 1) AS ' . $db->qn('depth')
))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
->group($db->qn('node') . '.' . $fldLft)
->order($db->qn('node') . '.' . $fldLft . ' ASC');
$query = $db->getQuery(true)
->select(array(
$db->qn('node') . '.' . $fldLft,
'(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - (' .
$db->qn('sub_tree') . '.' . $db->qn('depth') . ' + 1)) AS ' . $db->qn('depth')
))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('sub_parent'))
->join('CROSS', '(' . $subQuery . ') AS ' . $db->qn('sub_tree'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('sub_parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('sub_parent') . '.' . $fldRgt)
->where($db->qn('sub_parent') . '.' . $fldLft . ' = ' . $db->qn('sub_tree') . '.' . $fldLft)
->group($db->qn('node') . '.' . $fldLft)
->having(array(
$db->qn('depth') . ' > ' . $db->q(0),
$db->qn('depth') . ' <= ' . $db->q(1),
))
->order($db->qn('node') . '.' . $fldLft . ' ASC');
$leftValues = $db->setQuery($query)->loadColumn();
if (empty($leftValues))
{
$leftValues = array(0);
}
array_walk($leftValues, function(&$item, $key) use (&$db) {
$item = $db->q($item);
});
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' IN (' . implode(',', $leftValues) . ')');
}
/**
* get() will not return the selected node if it's part of the query results
*
* @param TreeModel $node The node to exclude from the results
*
* @return void
*/
public function withoutNode(TreeModel $node)
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$this->whereRaw('NOT(' . $db->qn('node') . '.' . $fldLft . ' = ' . $db->q($node->lft) . ')');
}
/**
* get() will not return ourselves if it's part of the query results
*
* @codeCoverageIgnore
*
* @return void
*/
protected function scopeWithoutSelf()
{
$this->withoutNode($this);
}
/**
* get() will not return our root if it's part of the query results
*
* @codeCoverageIgnore
*
* @return void
*/
protected function scopeWithoutRoot()
{
$rootNode = $this->getRoot();
$this->withoutNode($rootNode);
}
/**
* Returns the root node of the tree this node belongs to
*
* @return static
*
* @throws \RuntimeException
*/
public function getRoot()
{
// Empty node, let's try to get the first available root, ie lft=1
if(!$this->getId())
{
$this->load(array('lft' => 1));
}
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
// If this is a root node return itself (there is no such thing as the root of a root node)
if ($this->isRoot())
{
return $this;
}
if (empty($this->treeRoot) || !is_object($this->treeRoot) || !($this->treeRoot instanceof TreeModel))
{
$this->treeRoot = null;
// First try to get the record with the minimum ID
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$subQuery = $db->getQuery(true)
->select('MIN(' . $fldLft . ')')
->from($db->qn($this->tableName));
try
{
$root = $this->getClone()->reset()
->whereRaw($fldLft . ' = (' . (string)$subQuery . ')')
->firstOrFail();
if ($this->isDescendantOf($root))
{
$this->treeRoot = $root;
}
}
catch (\RuntimeException $e)
{
// If there is no root found throw an exception. Basically: your table is FUBAR.
throw new TreeRootNotFound($this->tableName, $this->lft);
}
// If the above method didn't work, get all roots and select the one with the appropriate lft/rgt values
if (is_null($this->treeRoot))
{
// Find the node with depth = 0, lft < our lft and rgt > our right. That's our root node.
$query = $db->getQuery(true)
->select(array(
$db->qn('node') . '.' . $fldLft,
'(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth')
))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' < ' . $db->q($this->lft))
->where($db->qn('node') . '.' . $fldRgt . ' > ' . $db->q($this->rgt))
->having($db->qn('depth') . ' = ' . $db->q(0))
->group($db->qn('node') . '.' . $fldLft);
// Get the lft value
$targetLeft = $db->setQuery($query)->loadResult();
if (empty($targetLeft))
{
// If there is no root found throw an exception. Basically: your table is FUBAR.
throw new TreeRootNotFound($this->tableName, $this->lft);
}
try
{
$this->treeRoot = $this->getClone()->reset()
->whereRaw($fldLft . ' = ' . $db->q($targetLeft))
->firstOrFail();
}
catch (\RuntimeException $e)
{
// If there is no root found throw an exception. Basically: your table is FUBAR.
throw new TreeRootNotFound($this->tableName, $this->lft);
}
}
}
return $this->treeRoot;
}
/**
* Get all ancestors to this node and the node itself. In other words it gets the full path to the node and the node
* itself.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getAncestorsAndSelf()
{
$this->scopeAncestorsAndSelf();
return $this->get(true);
}
/**
* Get all ancestors to this node and the node itself, but not the root node. If you want to
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getAncestorsAndSelfWithoutRoot()
{
$this->scopeAncestorsAndSelf();
$this->scopeWithoutRoot();
return $this->get(true);
}
/**
* Get all ancestors to this node but not the node itself. In other words it gets the path to the node, without the
* node itself.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getAncestors()
{
$this->scopeAncestorsAndSelf();
$this->scopeWithoutSelf();
return $this->get(true);
}
/**
* Get all ancestors to this node but not the node itself and its root.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getAncestorsWithoutRoot()
{
$this->scopeAncestors();
$this->scopeWithoutRoot();
return $this->get(true);
}
/**
* Get all sibling nodes, including ourselves
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getSiblingsAndSelf()
{
$this->scopeSiblingsAndSelf();
return $this->get(true);
}
/**
* Get all sibling nodes, except ourselves
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getSiblings()
{
$this->scopeSiblings();
return $this->get(true);
}
/**
* Get all leaf nodes in the tree. You may want to use the scopes to narrow down the search in a specific subtree or
* path.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getLeaves()
{
$this->scopeLeaves();
return $this->get(true);
}
/**
* Get all descendant (children) nodes and ourselves.
*
* Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getDescendantsAndSelf()
{
$this->scopeDescendantsAndSelf();
return $this->get(true);
}
/**
* Get only our descendant (children) nodes, not ourselves.
*
* Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getDescendants()
{
$this->scopeDescendants();
return $this->get(true);
}
/**
* Get the immediate descendants (children). Unlike getDescendants it only goes one level deep into the tree
* structure. Descendants of descendant nodes will not be returned.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getImmediateDescendants()
{
$this->scopeImmediateDescendants();
return $this->get(true);
}
/**
* Returns a hashed array where each element's key is the value of the $key column (default: the ID column of the
* table) and its value is the value of the $column column (default: title). Each nesting level will have the value
* of the $column column prefixed by a number of $separator strings, as many as its nesting level (depth).
*
* This is useful for creating HTML select elements showing the hierarchy in a human readable format.
*
* @param string $column
* @param null $key
* @param string $seperator
*
* @return array
*/
public function getNestedList($column = 'title', $key = null, $seperator = ' ')
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
if (empty($key) || !$this->hasField($key))
{
$key = $this->getIdFieldName();
}
if (empty($column))
{
$column = 'title';
}
$fldKey = $db->qn($this->getFieldAlias($key));
$fldColumn = $db->qn($this->getFieldAlias($column));
$query = $db->getQuery(true)
->select(array(
$db->qn('node') . '.' . $fldKey,
$db->qn('node') . '.' . $fldColumn,
'(COUNT(' . $db->qn('parent') . '.' . $fldKey . ') - 1) AS ' . $db->qn('depth')
))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->group($db->qn('node') . '.' . $fldLft)
->order($db->qn('node') . '.' . $fldLft . ' ASC');
$tempResults = $db->setQuery($query)->loadAssocList();
$ret = array();
if (!empty($tempResults))
{
foreach ($tempResults as $row)
{
$ret[$row[$key]] = str_repeat($seperator, $row['depth']) . $row[$column];
}
}
return $ret;
}
/**
* Locate a node from a given path, e.g. "/some/other/leaf"
*
* Notes:
* - This will only work when you have a "slug" and a "hash" field in your table.
* - If the path starts with "/" we will use the root with lft=1. Otherwise the first component of the path is
* supposed to be the slug of the root node.
* - If the root node is not found you'll get null as the return value
* - You will also get null if any component of the path is not found
*
* @param string $path The path to locate
*
* @return TreeModel|null The found node or null if nothing is found
*/
public function findByPath($path)
{
// No path? No node.
if (empty($path))
{
return null;
}
// Extract the path parts
$pathParts = explode('/', $path);
$firstElement = array_shift($pathParts);
if (!empty($firstElement))
{
array_unshift($pathParts, $firstElement);
}
// Just a slash? Return the root
if (empty($pathParts[0]))
{
return $this->getRoot();
}
// Get the quoted field names
$db = $this->getDbo();
$fldLeft = $db->qn($this->getFieldAlias('lft'));
$fldRight = $db->qn($this->getFieldAlias('rgt'));
$fldHash = $db->qn($this->getFieldAlias('hash'));
// Get the quoted hashes of the slugs
$pathHashesQuoted = array();
foreach ($pathParts as $part)
{
$pathHashesQuoted[] = $db->q(sha1($part));
}
// Get all nodes with slugs matching our path
$query = $db->getQuery(true)
->select(array(
$db->qn('node') . '.*',
'(COUNT(' . $db->qn('parent') . '.' . $db->qn($this->getFieldAlias('lft')) . ') - 1) AS ' . $db->qn('depth')
))->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLeft . ' >= ' . $db->qn('parent') . '.' . $fldLeft)
->where($db->qn('node') . '.' . $fldLeft . ' <= ' . $db->qn('parent') . '.' . $fldRight)
->where($db->qn('node') . '.' . $fldHash . ' IN (' . implode(',', $pathHashesQuoted) . ')')
->group($db->qn('node') . '.' . $fldLeft)
->order(array(
$db->qn('depth') . ' ASC',
$db->qn('node') . '.' . $fldLeft . ' ASC',
));
$queryResults = $db->setQuery($query)->loadAssocList();
$pathComponents = array();
// Handle paths with (no root slug provided) and without (root slug provided) a leading slash
$currentLevel = (substr($path, 0, 1) == '/') ? 0 : -1;
$maxLevel = count($pathParts) + $currentLevel;
// Initialise the path results array
$i = $currentLevel;
foreach ($pathParts as $part)
{
$i++;
$pathComponents[$i] = array(
'slug' => $part,
'id' => null,
'lft' => null,
'rgt' => null,
);
}
// Search for the best matching nodes
$colSlug = $this->getFieldAlias('slug');
$colLft = $this->getFieldAlias('lft');
$colRgt = $this->getFieldAlias('rgt');
$colId = $this->getIdFieldName();
foreach ($queryResults as $row)
{
if ($row['depth'] == $currentLevel + 1)
{
if ($row[$colSlug] != $pathComponents[$currentLevel + 1]['slug'])
{
continue;
}
if ($currentLevel > 0)
{
if ($row[$colLft] < $pathComponents[$currentLevel]['lft'])
{
continue;
}
if ($row[$colRgt] > $pathComponents[$currentLevel]['rgt'])
{
continue;
}
}
$currentLevel++;
$pathComponents[$currentLevel]['id'] = $row[$colId];
$pathComponents[$currentLevel]['lft'] = $row[$colLft];
$pathComponents[$currentLevel]['rgt'] = $row[$colRgt];
}
if ($currentLevel == $maxLevel)
{
break;
}
}
// Get the last found node
$lastNode = array_pop($pathComponents);
// If the node exists, return it...
if (!empty($lastNode['lft']))
{
return $this->getClone()->reset()->where($colLft, '=', $lastNode['lft'])->firstOrFail();
}
// ...otherwise return null
return null;
}
/**
* Resets cached values used to speed up querying the tree
*
* @return static for chaining
*/
protected function resetTreeCache()
{
$this->treeDepth = null;
$this->treeRoot = null;
$this->treeParent = null;
$this->treeNestedGet = false;
return $this;
}
/**
* Overrides the DataModel's buildQuery to allow nested set searches using the provided scopes
*
* @param bool $overrideLimits
*
* @return \JDatabaseQuery
*/
public function buildQuery($overrideLimits = false)
{
$db = $this->getDbo();
$query = parent::buildQuery($overrideLimits);
// Wipe out select and from sections
$query->clear('select');
$query->clear('from');
$query
->select($db->qn('node') . '.*')
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'));
if ($this->treeNestedGet)
{
$query
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'));
}
return $query;
}
}
Json/Encapsulation/Base.php 0000644 00000010443 15234643677 0011670 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Encapsulation;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Model\Json\EncapsulationInterface;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Util\Encrypt;
abstract class Base implements EncapsulationInterface
{
/**
* The numeric ID of this encapsulation
*
* @var int
*/
protected $id = 0;
/**
* The code of this encapsulation
*
* @var string
*/
protected $code = 'ENCAPSULATION_VOID';
/**
* The description of this encapsulation
*
* @var string
*/
protected $description = 'Invalid encapsulation';
/**
* The encryption object which is set up for use with the JSON API
*
* @var Encrypt
*/
private $encryption;
/**
* Public constructor. Called by children to customise the encapsulation handler object
*
* @param int $id Numeric ID
* @param string $code Code
* @param string $description Human readable description
*/
function __construct($id, $code, $description)
{
$this->id = $id;
$this->code = strtoupper($code);
$this->description = $description;
}
/**
* Returns information about the encapsulation supported by this class. The return array has the following keys:
* id: The numeric ID of the encapsulation, e.g. 3
* code: The short code of the encapsulation, e.g. ENCAPSULATION_AESCTR256
* description: A human readable descriptions, e.g. "Data in AES-256 stream (CTR) mode encrypted JSON"
*
* @return array See above
*/
public function getInformation()
{
return array(
'id' => $this->id,
'code' => $this->code,
'description' => $this->description,
);
}
/**
* Checks if the request body authorises the user to use the API. Each encapsulation can implement its own
* authorisation method. This method is only called after the request body has been successfully decoded, therefore
* encrypted encapsulations can simply return true.
*
* @param string $serverKey The server key we need to check the authorisation
* @param array $body The decoded body (as returned by the decode() method)
*
* @return bool True if authorised
*/
public function isAuthorised($serverKey, $body)
{
return true;
}
/**
* Is the provided encapsulation type supported by this class?
*
* @param int $encapsulation Encapsulation type
*
* @return bool True if supported
*/
public function isSupported($encapsulation)
{
return $encapsulation == $this->id;
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeeba.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data)
{
return $data;
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeeba.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data)
{
return $data;
}
}
Json/Encapsulation/Raw.php 0000644 00000006053 15234643677 0011551 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Encapsulation;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Raw (plain text) encapsulation
*/
class Raw extends Base
{
/**
* Constructs the encapsulation handler object
*/
function __construct()
{
parent::__construct(1, 'ENCAPSULATION_RAW', 'Data in plain-text JSON');
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeeba.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data)
{
return $data;
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeeba.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data)
{
return $data;
}
/**
* Checks if the request body authorises the user to use the API. Each encapsulation can implement its own
* authorisation method. This method is only called after the request body has been successfully decoded, therefore
* encrypted encapsulations can simply return true.
*
* @param string $serverKey The server key we need to check the authorisation
* @param array $body The decoded body (as returned by the decode() method)
*
* @return bool True if authorised
*/
public function isAuthorised($serverKey, $body)
{
$authenticated = false;
if (isset($body['challenge']) && (strpos($body['challenge'], ':') >= 2) && (strlen($body['challenge']) >= 3))
{
[$challengeData, $providedHash] = explode(':', $body['challenge']);
$computedHash = strtolower(md5($challengeData . $serverKey));
$authenticated = ($computedHash == $providedHash);
}
return $authenticated;
}
}
Json/Task/AbstractTask.php 0000644 00000002743 15234643677 0011505 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
// Protect from unauthorized access
use Akeeba\Backup\Site\Model\Json\TaskInterface;
use FOF40\Container\Container;
class AbstractTask implements TaskInterface
{
/**
* The container of the component we belong to
*
* @var Container
*/
protected $container = null;
/**
* The method name
*
* @var string
*/
protected $methodName = '';
/**
* Public constructor
*
* @param Container $container The container of the component we belong to
*/
public function __construct(Container $container)
{
$this->container = $container;
$path = explode('\\', get_class($this));
$shortName = array_pop($path);
$this->methodName = lcfirst($shortName);
}
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return $this->methodName;
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \LogicException(__CLASS__ . ' has not implemented its execute() method yet.');
}
}
Json/Task/Browse.php 0000644 00000001377 15234643677 0010362 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Return folder browser results
*
* @deprecated
*/
class Browse extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/Delete.php 0000644 00000002174 15234643677 0010317 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Model\Statistics;
use Akeeba\Engine\Platform;
/**
* Delete a backup record
*/
class Delete extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
/** @var Statistics $model */
$model = $this->container->factory->model('Statistics')->tmpInstance();
$model->setState('id', $backup_id);
try
{
$model->delete();
}
catch (\Exception $e)
{
throw new \RuntimeException($e->getMessage(), 500);
}
return true;
}
}
Json/Task/DeleteFiles.php 0000644 00000002234 15234643677 0011277 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Model\Statistics;
use Akeeba\Engine\Platform;
/**
* Delete the backup archives of a backup record
*/
class DeleteFiles extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
/** @var Statistics $model */
$model = $this->container->factory->model('Statistics')->tmpInstance();
$model->setState('id', $backup_id);
try
{
$model->deleteFile();
}
catch (\Exception $e)
{
throw new \RuntimeException($e->getMessage(), 500);
}
return true;
}
}
Json/Task/DeleteProfile.php 0000644 00000001337 15234643677 0011640 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Delete a backup profile
*
* @deprecated
*/
class DeleteProfile extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/Download.php 0000644 00000004376 15234643677 0010672 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
/**
* Download a chunk of a backup archive over HTTP
*/
class Download extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
'part_id' => 1,
'segment' => 1,
'chunk_size' => 1
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
$part_id = (int)$defConfig['part_id'];
$segment = (int)$defConfig['segment'];
$chunk_size = (int)$defConfig['chunk_size'];
$backup_stats = Platform::getInstance()->get_statistics($backup_id);
if (empty($backup_stats))
{
// Backup record doesn't exist
throw new \RuntimeException('Invalid backup record identifier', 404);
}
$files = Factory::getStatistics()->get_all_filenames($backup_stats);
if ((($files === null ? 0 : count($files)) < $part_id) || ($part_id <= 0))
{
// Invalid part
throw new \RuntimeException('Invalid backup part', 404);
}
$file = $files[ $part_id - 1 ];
$filesize = @filesize($file);
$seekPos = $chunk_size * 1048576 * ($segment - 1);
if ($seekPos > $filesize)
{
// Trying to seek past end of file
throw new \RuntimeException('Invalid segment', 404);
}
$fp = fopen($file, 'r');
if ($fp === false)
{
// Could not read file
throw new \RuntimeException('Error reading backup archive', 500);
}
rewind($fp);
if (fseek($fp, $seekPos, SEEK_SET) === -1)
{
// Could not seek to position
throw new \RuntimeException('Error reading specified segment', 500);
}
$buffer = fread($fp, 1048576);
if ($buffer === false)
{
throw new \RuntimeException('Error reading specified segment', 500);
}
fclose($fp);
return base64_encode($buffer);
}
}
Json/Task/DownloadDirect.php 0000644 00000007123 15234643677 0012016 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
/**
* Download an entire backup archive directly over HTTP
*/
class DownloadDirect extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
'part_id' => 1,
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
$part_id = (int)$defConfig['part_id'];
$backup_stats = Platform::getInstance()->get_statistics($backup_id);
if (empty($backup_stats))
{
// Backup record doesn't exist
@ob_end_clean();
header('HTTP/1.1 500 Invalid backup record identifier');
flush();
$this->container->platform->closeApplication();
}
$files = Factory::getStatistics()->get_all_filenames($backup_stats);
if ((($files === null ? 0 : count($files)) < $part_id) || ($part_id <= 0))
{
// Invalid part
@ob_end_clean();
header('HTTP/1.1 500 Invalid backup part');
flush();
$this->container->platform->closeApplication();
}
$filename = $files[ $part_id - 1 ];
@clearstatcache();
// For a certain unmentionable browser
if (function_exists('ini_get') && function_exists('ini_set'))
{
if (ini_get('zlib.output_compression'))
{
ini_set('zlib.output_compression', 'Off');
}
}
// Remove php's time limit
if (function_exists('ini_get') && function_exists('set_time_limit'))
{
if (!ini_get('safe_mode'))
{
@set_time_limit(0);
}
}
$basename = @basename($filename);
$fileSize = @filesize($filename);
$extension = strtolower(str_replace(".", "", strrchr($filename, ".")));
while (@ob_end_clean())
{
;
}
@clearstatcache();
// Send MIME headers
header('MIME-Version: 1.0');
header('Content-Disposition: attachment; filename="' . $basename . '"');
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes');
switch ($extension)
{
case 'zip':
// ZIP MIME type
header('Content-Type: application/zip');
break;
default:
// Generic binary data MIME type
header('Content-Type: application/octet-stream');
break;
}
// Notify of file size, if this info is available
if ($fileSize > 0)
{
header('Content-Length: ' . @filesize($filename));
}
// Disable caching
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Expires: 0");
header('Pragma: no-cache');
flush();
if ($fileSize > 0)
{
// If the filesize is reported, use 1M chunks for echoing the data to the browser
$blockSize = 1048576; //1M chunks
$handle = @fopen($filename, "r");
// Now we need to loop through the file and echo out chunks of file data
if ($handle !== false)
{
while (!@feof($handle))
{
echo @fread($handle, $blockSize);
@ob_flush();
flush();
}
}
if ($handle !== false)
{
@fclose($handle);
}
}
else
{
// If the filesize is not reported, hope that readfile works
@readfile($filename);
}
flush();
$this->container->platform->closeApplication();
// Totally ignored, only added to make static analysis happy
return null;
}
}
Json/Task/ExportConfiguration.php 0000644 00000003220 15234643677 0013117 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Model\Profiles;
use Akeeba\Engine\Factory;
/**
* Export the profile's configuration
*/
class ExportConfiguration extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$profile_id = (int)$defConfig['profile'];
if ($profile_id <= 0)
{
$profile_id = 1;
}
/** @var Profiles $profile */
$profile = $this->container->factory->model('Profiles')->tmpInstance();
$data = $profile->findOrFail($profile_id)->getData();
if (substr($data['configuration'], 0, 12) == '###AES128###')
{
// Load the server key file if necessary
if (!defined('AKEEBA_SERVERKEY'))
{
$filename = JPATH_COMPONENT_ADMINISTRATOR . '/BackupEngine/serverkey.php';
include_once $filename;
}
$key = Factory::getSecureSettings()->getKey();
$data['configuration'] = Factory::getSecureSettings()->decryptSettings($data['configuration'], $key);
}
return array(
'description' => $data['description'],
'configuration' => $data['configuration'],
'filters' => $data['filters'],
);
}
}
Json/Task/GetBackupInfo.php 0000644 00000003361 15234643677 0011575 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
/**
* Get information for a given backup record
*/
class GetBackupInfo extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
// Get the basic statistics
$record = Platform::getInstance()->get_statistics($backup_id);
// Backup record doesn't exist
if (empty($record))
{
throw new \RuntimeException('Invalid backup record identifier', 404);
}
// Get a list of filenames
$filenames = Factory::getStatistics()->get_all_filenames($record);
if (empty($filenames))
{
// Archives are not stored on the server or no files produced
$record['filenames'] = array();
}
else
{
$filedata = array();
$i = 0;
// Get file sizes per part
foreach ($filenames as $file)
{
$i++;
$size = @filesize($file);
$size = is_numeric($size) ? $size : 0;
$filedata[] = array(
'part' => $i,
'name' => basename($file),
'size' => $size
);
}
// Add the file info to $record['filenames']
$record['filenames'] = $filedata;
}
return $record;
}
}
Json/Task/GetDBEntities.php 0000644 00000001504 15234643677 0011543 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Get the database entities along with their filtering status (typically for rendering a GUI)
*
* @deprecated
*/
class GetDBEntities extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetDBFilters.php 0000644 00000001403 15234643677 0011365 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Get the database filters
*
* @deprecated
*/
class GetDBFilters extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetDBRoots.php 0000644 00000001362 15234643677 0011067 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Get the database roots (database definitions)
*
* @deprecated
*/
class GetDBRoots extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetFSEntities.php 0000644 00000001506 15234643677 0011570 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Get the filesystem entities along with their filtering status (typically for rendering a GUI)
*
* @deprecated
*/
class GetFSEntities extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetFSFilters.php 0000644 00000001402 15234643677 0011407 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Get the filesystem filters
*
* @deprecated
*/
class GetFSFilters extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetFSRoots.php 0000644 00000001410 15234643677 0011104 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Get the filesystem roots (site root and extra included directories)
*
* @deprecated
*/
class GetFSRoots extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetGUIConfiguration.php 0000644 00000001400 15234643677 0012720 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Get the GUI definitions for the configuration page
*
* @deprecated
*/
class GetGUIConfiguration extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetIncludedDBs.php 0000644 00000001351 15234643677 0011671 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Get the extra included databases
*
* @deprecated
*/
class GetIncludedDBs extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetIncludedDirectories.php 0000644 00000001363 15234643677 0013500 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Get the extra included directories
*
* @deprecated
*/
class GetIncludedDirectories extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetProfiles.php 0000644 00000002031 15234643677 0011330 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Model\Profiles;
/**
* Get a list of known backup profiles
*/
class GetProfiles extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
/** @var Profiles $model */
$model = $this->container->factory->model('Profiles')->tmpInstance();
$profiles = $model->get(true);
$ret = array();
if (count($profiles))
{
foreach ($profiles as $profile)
{
$temp = new \stdClass();
$temp->id = $profile->id;
$temp->name = $profile->description;
$ret[] = $temp;
}
}
return $ret;
}
}
Json/Task/GetRegexDBFilters.php 0000644 00000001413 15234643677 0012361 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Get the regex database filters
*
* @deprecated
*/
class GetRegexDBFilters extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetRegexFSFilters.php 0000644 00000001415 15234643677 0012406 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Get the regex filesystem filters
*
* @deprecated
*/
class GetRegexFSFilters extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/GetVersion.php 0000644 00000002300 15234643677 0011171 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Model\Updates;
/**
* Get the version information of Akeeba Backup
*/
class GetVersion extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
/** @var Updates $model */
$model = $this->container->factory->model('Updates')->tmpInstance();
$updateInformation = $model->getUpdates();
if (is_array($updateInformation) && array_key_exists('releasenotes', $updateInformation))
{
unset ($updateInformation['releasenotes']);
}
$edition = AKEEBA_PRO ? 'pro' : 'core';
return (object)array(
'api' => AKEEBA_JSON_API_VERSION,
'component' => AKEEBA_VERSION,
'date' => AKEEBA_DATE,
'edition' => $edition,
'updateinfo' => $updateInformation,
);
}
}
Json/Task/ImportConfiguration.php 0000644 00000002254 15234643677 0013116 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Model\Profiles;
use Akeeba\Engine\Factory;
/**
* Import the profile's configuration
*/
class ImportConfiguration extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
'data' => null,
);
$defConfig = array_merge($defConfig, $parameters);
$profile_id = (int)$defConfig['profile'];
$data = $defConfig['data'];
if ($profile_id <= 0)
{
$profile_id = 0;
}
/** @var Profiles $profile */
$profile = $this->container->factory->model('Profiles')->tmpInstance();
if ($profile_id)
{
$profile->find($profile_id);
}
$profile->import($data);
return true;
}
}
Json/Task/ListBackups.php 0000644 00000002174 15234643677 0011341 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Model\Statistics;
use Akeeba\Engine\Platform;
/**
* List the backup records
*/
class ListBackups extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'from' => 0,
'limit' => 50
);
$defConfig = array_merge($defConfig, $parameters);
$from = (int)$defConfig['from'];
$limit = (int)$defConfig['limit'];
/** @var Statistics $model */
$model = $this->container->factory->model('Statistics')->tmpInstance();
$model->setState('limitstart', $from);
$model->setState('limit', $limit);
return $model->getStatisticsListWithMeta(false);
}
}
Json/Task/Log.php 0000644 00000001317 15234643677 0007634 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Get the log contents
*
* @deprecated
*/
class Log extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/RemoveIncludedDB.php 0000644 00000001417 15234643677 0012227 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Remove an extra database definition
*
* @deprecated
*/
class RemoveIncludedDB extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/RemoveIncludedDirectory.php 0000644 00000001454 15234643677 0013707 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
use RuntimeException;
/**
* Remove an extra directory definition
*
* @deprecated
*/
class RemoveIncludedDirectory extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/SaveConfiguration.php 0000644 00000001366 15234643677 0012545 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Save the configuration for a given profile
*
* @deprecated
*/
class SaveConfiguration extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/SaveProfile.php 0000644 00000001334 15234643677 0011331 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Saves a backup profile
*
* @deprecated
*/
class SaveProfile extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/SetDBFilter.php 0000644 00000001405 15234643677 0011220 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Set or unset a database filter
*
* @deprecated
*/
class SetDBFilter extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/SetFSFilter.php 0000644 00000001407 15234643677 0011245 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Set or unset a filesystem filter
*
* @deprecated
*/
class SetFSFilter extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/SetIncludedDB.php 0000644 00000001424 15234643677 0011523 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Set up or edit an extra database definition
*
* @deprecated
*/
class SetIncludedDB extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/SetIncludedDirectory.php 0000644 00000001434 15234643677 0013203 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Set up or edit an extra directory definition
*
* @deprecated
*/
class SetIncludedDirectory extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/SetRegexDBFilter.php 0000644 00000001420 15234643677 0012210 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Set or unset a Regex database filter
*
* @deprecated
*/
class SetRegexDBFilter extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/SetRegexFSFilter.php 0000644 00000001422 15234643677 0012235 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Set or unset a Regex filesystem filter
*
* @deprecated
*/
class SetRegexFSFilter extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/StartBackup.php 0000644 00000005603 15234643677 0011340 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Joomla\CMS\Filter\InputFilter;
/**
* Start a backup job
*/
class StartBackup extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
$filter = InputFilter::getInstance();
// Get the passed configuration values
$defConfig = [
'profile' => 1,
'description' => '',
'comment' => '',
'backupid' => null,
'overrides' => [],
];
$defConfig = array_merge($defConfig, $parameters);
$profile = (int) $defConfig['profile'];
$profile = max(1, $profile); // Make sure $profile is a positive integer >= 1
$description = $filter->clean($defConfig['description'], 'string');
$comment = $filter->clean($defConfig['comment'], 'string');
$overrides = $filter->clean($defConfig['overrides'], 'array');
if (empty($description))
{
$description = $this->container->factory->model('Backup')->getDefaultDescription() . ' (JSON API)';
}
$this->container->platform->setSessionVar('profile', $profile, 'akeeba');
define('AKEEBA_PROFILE', $profile);
/**
* DO NOT REMOVE!
*
* The Model will only try to load the configuration after nuking the factory. This causes Profile 1 to be
* loaded first. Then it figures out it needs to load a different profile and it does – but the protected keys
* are NOT replaced, meaning that certain configuration parameters are not replaced. Most notably, the chain.
* This causes backups to behave weirdly. So, DON'T REMOVE THIS UNLESS WE REFACTOR THE MODEL.
*/
Platform::getInstance()->load_configuration($profile);
/** @var \Akeeba\Backup\Site\Model\Backup $model */
$model = $this->container->factory->model('Backup')->tmpInstance();
$model->setState('tag', 'json');
$model->setState('description', $description);
$model->setState('comment', $comment);
$model->setState('profile', $profile);
$array = $model->startBackup($overrides);
if ($array['Error'] != '')
{
throw new \RuntimeException('A backup error has occurred: ' . $array['Error'], 500);
}
// BackupID contains the numeric backup record ID. backupid contains the backup id (usually in the form id123)
$statistics = Factory::getStatistics();
$array['BackupID'] = $statistics->getId();
// Remote clients expect a boolean, not an integer.
$array['HasRun'] = ($array['HasRun'] === 0);
$array['Profile'] = Platform::getInstance()->get_active_profile();
return $array;
}
}
Json/Task/StepBackup.php 0000644 00000004341 15234643677 0011154 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Joomla\CMS\Filter\InputFilter;
/**
* Step through a backup job
*/
class StepBackup extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
$filter = InputFilter::getInstance();
// Get the passed configuration values
$defConfig = [
'tag' => 'json',
'backupid' => null,
];
$defConfig = array_merge($defConfig, $parameters);
$tag = $filter->clean($defConfig['tag'], 'cmd');
$backupid = $filter->clean($defConfig['backupid'], 'cmd');
if (empty($backupid))
{
throw new \RuntimeException("JSON API :: stepBackup -- You have not provided the required backupid parameter. This parameter is MANDATORY since May 2016. Please update your client software to include this parameter.");
}
/** @var \Akeeba\Backup\Site\Model\Backup $model */
$model = $this->container->factory->model('Backup')->tmpInstance();
$profile = max(1, (int) $model->getLastBackupProfile($tag, $backupid));
$this->container->platform->setSessionVar('profile', $profile, 'akeeba');
define('AKEEBA_PROFILE', $profile);
$model->setState('tag', $tag);
$model->setState('backupid', $backupid);
$model->setState('profile', $profile);
$array = $model->stepBackup(true);
if ($array['Error'] != '')
{
throw new \RuntimeException('A backup error has occurred: ' . $array['Error'], 500);
}
// BackupID contains the numeric backup record ID. backupid contains the backup id (usually in the form id123)
$statistics = Factory::getStatistics();
$array['BackupID'] = $statistics->getId();
// Remote clients expect a boolean, not an integer.
$array['HasRun'] = ($array['HasRun'] === 0);
$array['Profile'] = Platform::getInstance()->get_active_profile();
return $array;
}
}
Json/Task/TestDBConnection.php 0000644 00000001415 15234643677 0012257 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Test an extra database definition
*
* @deprecated
*/
class TestDBConnection extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Task/UpdateGetInformation.php 0000644 00000001412 15234643677 0013177 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json\Task;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Joomla\CMS\Filter\InputFilter;
/**
* Get the update information
*
* @deprecated
*/
class UpdateGetInformation extends AbstractTask
{
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = [])
{
throw new \RuntimeException('This method is no longer supported by the Akeeba Remote JSON API', 501);
}
}
Json/Encapsulation.php 0000644 00000015450 15234643677 0011021 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Handles data encapsulation
*/
class Encapsulation
{
/**
* Known encapsulation handlers
*
* @var EncapsulationInterface[]
*/
protected $handlers = array();
/**
* List of encapsulation types
*
* @var array
*/
protected $encapsulations = array();
/**
* The server key used to decrypt / encrypt data and check the authorisation
*
* @var string
*/
protected $serverKey;
/**
* Public constructor
*
* @param string $serverKey The server key used for data encyrption/decryption and authorisation checks
*/
public function __construct($serverKey)
{
$this->serverKey = $serverKey;
// Populate the list of encapsulation handlers
$this->initialiseHandlers();
}
/**
* Returns the encapsulation ID given its code. For example given $code == 'ENCAPSULATION_AESCTR256' it will return
* the ID integer 3.
*
* @param string $code The encapsulation code, e.g. ENCAPSULATION_AESCTR256
*
* @return int The numeric ID, e.g. 3
*/
public function getEncapsulationByCode($code)
{
$info = $this->getEncapsulationInfoByCode($code);
return $info['id'];
}
/**
* Returns the encapsulation information array given its code. For example given $code == 'ENCAPSULATION_AESCTR256'
* it will return the information for the data in AES-256 stream (CTR) mode encrypted JSON type.
*
* @param string $code The encapsulation code, e.g. ENCAPSULATION_AESCTR256
*
* @return array The information of the encapsulation handler
*/
public function getEncapsulationInfoByCode($code)
{
// Normalise the code
$code = strtoupper($code);
// If we have no idea what the encapsulation should be revert to raw (plain text)
if (!isset($this->encapsulations[$code]))
{
return $this->encapsulations['ENCAPSULATION_RAW'];
}
return $this->encapsulations[$code];
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param int $encapsulation The encapsulation type
* @param string $data Encoded data
*
* @return array The decoded data.
*
* @throw \RuntimeException When the server capabilities don't match the requested encapsulation
* @throw \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeeba.com/documentation/json-api/ar01s02.html
*/
public function decode($encapsulation, $data)
{
$body = null;
// Find the suitable handler and encode the data
foreach ($this->handlers as $handler)
{
if ($handler->isSupported($encapsulation))
{
$body = $handler->decode($this->serverKey, $data);
break;
}
}
// If the data cannot be encoded throw an exception
if (!isset($handler) || is_null($body))
{
throw new \RuntimeException('The requested encapsulation type is not supported', 503);
}
$authorised = true;
$body = rtrim($body, chr(0));
// Make sure it looks like a valid JSON string and is at least 12 characters (minimum valid message length)
if ((strlen($body) < 12) || (substr($body, 0, 1) != '{') || (substr($body, -1) != '}'))
{
$authorised = false;
}
// Try to JSON decode the body
if ($authorised)
{
$body = json_decode($body, true);
if (is_null($body))
{
$authorised = false;
}
elseif (!is_array($body))
{
$authorised = false;
}
}
// Make sure there is a requested method
if ($authorised)
{
if (!isset($body['method']) || empty($body['method']))
{
$authorised = false;
}
}
if ($authorised)
{
$authorised = $handler->isAuthorised($this->serverKey, $body);
}
if (!$authorised)
{
throw new \InvalidArgumentException('Authentication failed', 401);
}
return (array)$body;
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param int $encapsulation The encapsulation type
* @param mixed $data The data to encode, typically a string, array or object
* @param string $key Key to use for encoding. If not provided we revert to $this->serverKey
*
* @return string The encapsulated data
*
* @see https://www.akeeba.com/documentation/json-api/ar01s02s02.html
*
* @throw \RuntimeException When the server capabilities don't match the requested encapsulation
* @throw \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($encapsulation, $data, $key = null)
{
// Try to JSON-encode the data
$data = json_encode($data);
// If the data cannot be JSON-encoded throw an exception
if ($data === false)
{
throw new \InvalidArgumentException('Empty data cannot be encapsulated', 500);
}
// Make sure we have a valid key
if (empty($key))
{
$key = $this->serverKey;
}
// Find the suitable handler and encode the data
foreach ($this->handlers as $handler)
{
if ($handler->isSupported($encapsulation))
{
return $handler->encode($key, $data);
}
}
// If the data cannot be encoded throw an exception
$format = print_r($encapsulation, true);
throw new \RuntimeException("Data cannot be encapsulated in the requested format ($format)", 500);
}
/**
* Initialises the encapsulation handlers
*
* @return void
*/
protected function initialiseHandlers()
{
// Reset the arrays
$this->handlers = array();
$this->encapsulations = array();
// Look all files in the Encapsulation handlers' directory
$dh = new \DirectoryIterator(__DIR__ . '/Encapsulation');
/** @var \DirectoryIterator $entry */
foreach ($dh as $entry)
{
$fileName = $entry->getFilename();
// Ignore non-PHP files
if (substr($fileName, -4) != '.php')
{
continue;
}
// Ignore the Base class
if ($fileName == 'Base.php')
{
continue;
}
// Get the class name
$className = '\\Akeeba\\Backup\\Site\\Model\\Json\\Encapsulation\\' . substr($fileName, 0, -4);
// Check if the class really exists
if (!class_exists($className, true))
{
continue;
}
/** @var EncapsulationInterface $o */
$o = new $className;
$info = $o->getInformation();
$this->encapsulations[$info['code']] = $info;
$this->handlers[] = $o;
}
}
}
Json/EncapsulationInterface.php 0000644 00000006176 15234643677 0012647 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json;
// Protect from unauthorized access
defined('_JEXEC') || die();
/**
* Interface for Encapsulation data handlers
*/
interface EncapsulationInterface
{
/**
* Is the provided encapsulation type supported by this class?
*
* @param int $encapsulation Encapsulation type
*
* @return bool True if supported
*/
public function isSupported($encapsulation);
/**
* Returns information about the encapsulation supported by this class. The return array has the following keys:
* id: The numeric ID of the encapsulation, e.g. 3
* code: The short code of the encapsulation, e.g. ENCAPSULATION_AESCTR256
* description: A human readable descriptions, e.g. "Data in AES-256 stream (CTR) mode encrypted JSON"
*
* @return array See above
*/
public function getInformation();
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it but *NOT* JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeeba.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data);
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeeba.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data);
/**
* Checks if the request body authorises the user to use the API. Each encapsulation can implement its own
* authorisation method. This method is only called after the request body has been successfully decoded, therefore
* encrypted encapsulations can simply return true.
*
* @param string $serverKey The server key we need to check the authorisation
* @param array $body The decoded body (as returned by the decode() method)
*
* @return bool True if authorised
*/
public function isAuthorised($serverKey, $body);
}
Json/Task.php 0000644 00000005170 15234643677 0007114 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json;
// Protect from unauthorized access
defined('_JEXEC') || die();
use FOF40\Container\Container;
/**
* Handles task execution
*/
class Task
{
/**
* The component's container
*
* @var Container
*/
protected $container = null;
/** @var TaskInterface[] The task handlers known to us */
protected $handlers = array();
/**
* Public constructor. Populates the list of task handlers.
*/
public function __construct(Container $container)
{
$this->container = $container;
// Populate the list of task handlers
$this->initialiseHandlers();
}
/**
* Do I have a specific task handling method?
*
* @param string $method The method to check for
*
* @return bool
*/
public function hasMethod($method)
{
$method = strtolower($method);
return isset($this->handlers[$method]);
}
/**
* Execute a JSON API method
*
* @param string $method The method's name
* @param array $parameters The parameters to the method (optional)
*
* @return mixed
*
* @throws \RuntimeException When the method requested is not known to us
*/
public function execute($method, $parameters = array())
{
if ((!defined('AKEEBA_PRO') || !AKEEBA_PRO) && (time() >= 1583020800))
{
throw new \RuntimeException('Access denied', 503);
}
if (!$this->hasMethod($method))
{
throw new \RuntimeException("Invalid method $method", 405);
}
$method = strtolower($method);
return $this->handlers[$method]->execute($parameters);
}
/**
* Initialises the encapsulation handlers
*
* @return void
*/
protected function initialiseHandlers()
{
// Reset the array
$this->handlers = array();
// Look all files in the Task handlers' directory
$dh = new \DirectoryIterator(__DIR__ . '/Task');
/** @var \DirectoryIterator $entry */
foreach ($dh as $entry)
{
$fileName = $entry->getFilename();
// Ignore non-PHP files
if (substr($fileName, -4) != '.php')
{
continue;
}
// Ignore the Base class
if ($fileName == 'AbstractTask.php')
{
continue;
}
// Get the class name
$className = '\\Akeeba\\Backup\\Site\\Model\\Json\\Task\\' . substr($fileName, 0, -4);
// Check if the class really exists
if (!class_exists($className, true))
{
continue;
}
/** @var TaskInterface $o */
$o = new $className($this->container);
$name = $o->getMethodName();
$name = strtolower($name);
$this->handlers[$name] = $o;
}
}
}
Json/TaskInterface.php 0000644 00000001674 15234643677 0010742 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model\Json;
// Protect from unauthorized access
defined('_JEXEC') || die();
use FOF40\Container\Container;
/**
* Interface for JSON API tasks
*/
interface TaskInterface
{
/**
* Public constructor
*
* @param Container $container The container of the component we belong to
*/
public function __construct(Container $container);
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName();
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array());
}
Backup.php 0000644 00000000563 15234643677 0006507 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\Backup as AdminModel;
class Backup extends AdminModel
{
}
Browser.php 0000644 00000000564 15234643677 0006726 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\Browser as AdminModel;
class Browser extends AdminModel
{
}
Configuration.php 0000644 00000000600 15234643677 0010101 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\Configuration as AdminModel;
class Configuration extends AdminModel
{
}
ConfigurationWizard.php 0000644 00000000614 15234643677 0011267 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\ConfigurationWizard as AdminModel;
class ConfigurationWizard extends AdminModel
{
}
DatabaseFilters.php 0000644 00000000604 15234643677 0010333 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\DatabaseFilters as AdminModel;
class DatabaseFilters extends AdminModel
{
}
FileFilters.php 0000644 00000000574 15234643677 0007514 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\FileFilters as AdminModel;
class FileFilters extends AdminModel
{
}
IncludeFolders.php 0000644 00000000602 15234643677 0010176 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\IncludeFolders as AdminModel;
class IncludeFolders extends AdminModel
{
}
Json.php 0000644 00000015644 15234643677 0006221 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Site\Controller\Mixin\FrontEndPermissions;
use Akeeba\Backup\Site\Model\Json\Encapsulation;
use Akeeba\Backup\Site\Model\Json\Task;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\Complexify;
use FOF40\Container\Container;
use FOF40\Model\Model;
// JSON API version number
define('AKEEBA_JSON_API_VERSION', '400');
/*
* Short API version history:
* 300 First draft. Basic backup working. Encryption semi-broken.
* 316 Fixed download feature.
* 320 Minor bug fixes
* 330 Introduction of Akeeba Solo
* 335 Configuration overrides in startBackup
* 340 Advanced API allows full configuration
* 350 exportConfiguration, importConfiguration
* 400 API version 2
*
* Notes:
*
* When support for non-Raw encapsulations was removed December 2019 the API level was left at 350 (same
* since May 2016). If you see API level 350 try using only ever using the RAW encapsulation.
*
* If you see API level 400 or greater you SHOULD try using JSON API v2. The legacy JSON API will eventually go away.
*/
if (!defined('AKEEBA_BACKUP_ORIGIN'))
{
define('AKEEBA_BACKUP_ORIGIN', 'json');
}
/**
* JSON API model. Handles remote API calls through our JSON API.
*/
class Json extends Model
{
use FrontEndPermissions;
/** @var int Normal reply */
public const COM_AKEEBA_CPANEL_LBL_STATUS_OK = 200;
/** @var int Invalid credentials */
public const STATUS_NOT_AUTH = 401;
/** @var int Not enough privileges */
public const STATUS_NOT_ALLOWED = 403;
/** @var int Requested resource not found */
public const STATUS_NOT_FOUND = 404;
/** @var int Unknown JSON method */
public const STATUS_INVALID_METHOD = 405;
/** @var int An error occurred */
public const COM_AKEEBA_CPANEL_LBL_STATUS_ERROR = 500;
/** @var int Not implemented feature */
public const STATUS_NOT_IMPLEMENTED = 501;
/** @var int Remote service not activated */
public const STATUS_NOT_AVAILABLE = 503;
/** @var int Data encapsulation format */
private $encapsulationType = 1;
/** @var Encapsulation */
private $encapsulation;
/** @var string A password passed to us by the caller */
private $password = null;
/**
* Overridden constructor
*
* Sets up the encapsulation.
*
* @param Container $container The configuration variables to this model
* @param array $config Configuration values for this model
*/
public function __construct(Container $container, array $config)
{
parent::__construct($container, $config);
$this->encapsulation = new Encapsulation($this->serverKey());
}
/**
* Parses the JSON data sent by the client and executes the appropriate JSON API task
*
* @param string $json The raw JSON data received from the remote client
*
* @return string The JSON-encoded, fully encapsulated response
*/
public function execute($json)
{
// Check if we're activated
$enabled = $this->container->params->get('jsonapi_enabled', 0) == 1;
// Is the Secret Key strong enough?
$validKey = $this->serverKey();
if (!Complexify::isStrongEnough($validKey, false))
{
$enabled = false;
}
$rawEncapsulation = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW');
if (!$this->confirmDates())
{
return $this->getResponse('Your version of Akeeba Backup is too old. Please update it to re-enable the remote backup and administration features.', 402);
}
if (!$enabled)
{
return $this->getResponse('Access denied', 503);
}
// Try to JSON-decode the request's input first
$request = @json_decode($json, true);
if (is_null($request))
{
return $this->getResponse('JSON decoding error', 500);
}
// Transform legacy requests
if (!is_array($request))
{
$request = [
'encapsulation' => $rawEncapsulation,
'body' => $request,
];
}
// Transform partial requests
if (!isset($request['encapsulation']))
{
$request['encapsulation'] = $rawEncapsulation;
}
// Make sure we have a request body
if (!isset($request['body']))
{
$request['body'] = '';
}
try
{
$request['body'] = $this->encapsulation->decode($request['encapsulation'], $request['body']);
}
catch (\Exception $e)
{
return $this->getResponse($e->getMessage(), $e->getCode());
}
// Replicate the encapsulation preferences of the client for our own output
$this->encapsulationType = $request['encapsulation'];
// Store the client-specified key, or use the server key if none specified and the request
// came encrypted.
$this->password = $request['body']['key'] ?? $this->serverKey();
// Run the method
$params = [];
if (isset($request['body']['data']))
{
$params = (array) $request['body']['data'];
}
try
{
if (class_exists('Joomla\CMS\Component\ComponentHelper') && \Joomla\CMS\Component\ComponentHelper::isEnabled('com_akeebabackup'))
{
throw new \RuntimeException(sprintf('Please finish upgrading to Akeeba Backup 9 and uninstall Akeeba Backup 8 per the instructions shown on your site\'s backend, Components, Akeeba Backup'), 400);
}
$taskHandler = new Task($this->container);
$data = $taskHandler->execute($request['body']['method'], $params);
}
catch (\RuntimeException $e)
{
return $this->getResponse($e->getMessage(), $e->getCode());
}
return $this->getResponse($data);
}
/**
* Packages the response to a JSON-encoded object, optionally encrypting the data part with a caller-supplied
* password.
*
* @param mixed $data The response to encapsulate
* @param int $status The status code to return. 200 = Success, anything else is treated as an error.
*
* @return string The JSON-encoded response
*/
private function getResponse($data, $status = 200)
{
// Initialize the response
$response = [
'encapsulation' => $this->encapsulationType,
'body' => [
'status' => $status,
'data' => null,
],
];
if ($status != 200)
{
$response['encapsulation'] = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW');
}
try
{
$response['body']['data'] = $this->encapsulation->encode($response['encapsulation'], $data, $this->password);
}
catch (\Exception $e)
{
$response['encapsulation'] = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW');
$response['body'] = [
'status' => $e->getCode(),
'data' => $e->getMessage(),
];
}
return '###' . json_encode($response) . '###';
}
/**
* Get the server key, i.e. the Secret Word for the front-end backups and JSON API
*
* @return mixed
*/
private function serverKey()
{
static $key = null;
if (is_null($key))
{
$key = Platform::getInstance()->get_platform_configuration_option('frontend_secret_word', '');
}
return $key;
}
}
MultipleDatabases.php 0000644 00000000610 15234643677 0010676 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\MultipleDatabases as AdminModel;
class MultipleDatabases extends AdminModel
{
}
Profiles.php 0000644 00000000566 15234643677 0007070 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\Profiles as AdminModel;
class Profiles extends AdminModel
{
}
RegExDatabaseFilters.php 0000644 00000000616 15234643677 0011271 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\RegExDatabaseFilters as AdminModel;
class RegExDatabaseFilters extends AdminModel
{
}
RegExFileFilters.php 0000644 00000000606 15234643677 0010443 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\RegExFileFilters as AdminModel;
class RegExFileFilters extends AdminModel
{
}
Statistics.php 0000644 00000000572 15234643677 0007434 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\Statistics as AdminModel;
class Statistics extends AdminModel
{
}
Updates.php 0000644 00000000564 15234643677 0006710 0 ustar 00 <?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Backup\Site\Model;
// Protect from unauthorized access
defined('_JEXEC') || die();
use Akeeba\Backup\Admin\Model\Updates as AdminModel;
class Updates extends AdminModel
{
}
index.html 0000644 00000000352 15234643677 0006562 0 ustar 00 <!--~
~ @package akeebabackup
~ @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
~ @license GNU General Public License version 3, or later
-->
<html><head><title></title></head><body></body></html>