Your IP : 216.73.216.50


Current Path : /proc/1908984/root/proc/self/root/proc/self/root/proc/2411249/cwd/
Upload File :
Current File : //proc/1908984/root/proc/self/root/proc/self/root/proc/2411249/cwd/Model.tar

DataModel/Behaviour/Access.php000064400000003365152344664530012257 0ustar00<?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.php000064400000010575152344664530012321 0ustar00<?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.php000064400000004372152344664530014051 0ustar00<?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.php000064400000003717152344664530012426 0ustar00<?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.php000064400000001565152344664530013467 0ustar00<?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.php000064400000003250152344664530012401 0ustar00<?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.php000064400000006723152344664530012467 0ustar00<?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.php000064400000011022152344664530012566 0ustar00<?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.php000064400000003560152344664530012573 0ustar00<?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.php000064400000003727152344664530011623 0ustar00<?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.php000064400000002770152344664530015261 0ustar00<?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.php000064400000004376152344664530014167 0ustar00<?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.php000064400000007655152344664530011762 0ustar00<?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.php000064400000000426152344664530013614 0ustar00<?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.php000064400000001050152344664530016041 0ustar00<?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.php000064400000000430152344664530014733 0ustar00<?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.php000064400000001026152344664530013105 0ustar00<?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.php000064400000001013152344664530013625 0ustar00<?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.php000064400000000775152344664530013444 0ustar00<?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.php000064400000000423152344664530013745 0ustar00<?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.php000064400000001021152344664530014063 0ustar00<?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.php000064400000000431152344664530015147 0ustar00<?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.php000064400000001034152344664530015255 0ustar00<?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.php000064400000000705152344664530014414 0ustar00<?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.php000064400000001054152344664530015755 0ustar00<?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.php000064400000001050152344664530015410 0ustar00<?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.php000064400000001052152344664530015562 0ustar00<?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.php000064400000001054152344664530015722 0ustar00<?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.php000064400000001022152344664530016421 0ustar00<?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.php000064400000001026152344664530014300 0ustar00<?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.php000064400000001053152344664530016021 0ustar00<?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.php000064400000001021152344664530015364 0ustar00<?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.php000064400000001056152344664530015771 0ustar00<?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.php000064400000001031152344664530015431 0ustar00<?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.php000064400000023164152344664530013267 0ustar00<?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.php000064400000000744152344664530011734 0ustar00<?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.php000064400000011730152344664530011227 0ustar00<?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.php000064400000016031152344664530011601 0ustar00<?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.php000064400000001350152344664530012124 0ustar00<?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.php000064400000006172152344664530011302 0ustar00<?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.php000064400000000435152344664530016667 0ustar00<?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.php000064400000000431152344664530015754 0ustar00<?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.php000064400000000433152344664530016364 0ustar00<?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.php000064400000000431152344664530016066 0ustar00<?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.php000064400000000435152344664530016734 0ustar00<?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.php000064400000000431152344664530016121 0ustar00<?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.php000064400000004600152344664530012574 0ustar00<?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.php000064400000026602152344664530013427 0ustar00<?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.php000064400000011174152344664530012244 0ustar00<?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.php000064400000002532152344664530012057 0ustar00<?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.php000064400000015340152344664530011221 0ustar00<?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.php000064400000017611152344664530010706 0ustar00<?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.php000064400000033102152344664530012172 0ustar00<?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.php000064400000001107152344664530011711 0ustar00<?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.php000064400000003107152344664530010510 0ustar00<?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.php000064400000006451152344664530011621 0ustar00<?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.php000064400000003650152344664530010472 0ustar00<?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.php000064400000002152152344664530011274 0ustar00<?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.php000064400000001742152344664530010064 0ustar00<?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.php000064400000350674152344664530007142 0ustar00<?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.php000064400000034266152344664530006344 0ustar00<?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.php000064400000163222152344664530007157 0ustar00<?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.php000064400000010443152346436770011670 0ustar00<?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.php000064400000006053152346436770011551 0ustar00<?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.php000064400000002743152346436770011505 0ustar00<?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.php000064400000001377152346436770010362 0ustar00<?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.php000064400000002174152346436770010317 0ustar00<?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.php000064400000002234152346436770011277 0ustar00<?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.php000064400000001337152346436770011640 0ustar00<?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.php000064400000004376152346436770010672 0ustar00<?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.php000064400000007123152346436770012016 0ustar00<?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.php000064400000003220152346436770013117 0ustar00<?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.php000064400000003361152346436770011575 0ustar00<?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.php000064400000001504152346436770011543 0ustar00<?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.php000064400000001403152346436770011365 0ustar00<?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.php000064400000001362152346436770011067 0ustar00<?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.php000064400000001506152346436770011570 0ustar00<?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.php000064400000001402152346436770011407 0ustar00<?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.php000064400000001410152346436770011104 0ustar00<?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.php000064400000001400152346436770012720 0ustar00<?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.php000064400000001351152346436770011671 0ustar00<?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.php000064400000001363152346436770013500 0ustar00<?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.php000064400000002031152346436770011330 0ustar00<?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.php000064400000001413152346436770012361 0ustar00<?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.php000064400000001415152346436770012406 0ustar00<?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.php000064400000002300152346436770011171 0ustar00<?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.php000064400000002254152346436770013116 0ustar00<?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.php000064400000002174152346436770011341 0ustar00<?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.php000064400000001317152346436770007634 0ustar00<?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.php000064400000001417152346436770012227 0ustar00<?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.php000064400000001454152346436770013707 0ustar00<?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.php000064400000001366152346436770012545 0ustar00<?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.php000064400000001334152346436770011331 0ustar00<?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.php000064400000001405152346436770011220 0ustar00<?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.php000064400000001407152346436770011245 0ustar00<?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.php000064400000001424152346436770011523 0ustar00<?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.php000064400000001434152346436770013203 0ustar00<?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.php000064400000001420152346436770012210 0ustar00<?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.php000064400000001422152346436770012235 0ustar00<?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.php000064400000005603152346436770011340 0ustar00<?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.php000064400000004341152346436770011154 0ustar00<?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.php000064400000001415152346436770012257 0ustar00<?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.php000064400000001412152346436770013177 0ustar00<?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.php000064400000015450152346436770011021 0ustar00<?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.php000064400000006176152346436770012647 0ustar00<?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.php000064400000005170152346436770007114 0ustar00<?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.php000064400000001674152346436770010742 0ustar00<?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.php000064400000000563152346436770006507 0ustar00<?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.php000064400000000564152346436770006726 0ustar00<?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.php000064400000000600152346436770010101 0ustar00<?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.php000064400000000614152346436770011267 0ustar00<?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.php000064400000000604152346436770010333 0ustar00<?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.php000064400000000574152346436770007514 0ustar00<?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.php000064400000000602152346436770010176 0ustar00<?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.php000064400000015644152346436770006221 0ustar00<?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.php000064400000000610152346436770010676 0ustar00<?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.php000064400000000566152346436770007070 0ustar00<?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.php000064400000000616152346436770011271 0ustar00<?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.php000064400000000606152346436770010443 0ustar00<?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.php000064400000000572152346436770007434 0ustar00<?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.php000064400000000564152346436770006710 0ustar00<?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.html000064400000000352152346436770006562 0ustar00<!--~
  ~ @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>web.config000064400000001025152346436770006527 0ustar00<?xml version="1.0"?>
<!--
    This only works on IIS 7 or later. See https://www.iis.net/configreference/system.webserver/security/requestfiltering/fileextensions
-->
<configuration>
    <system.webServer>
        <security>
            <requestFiltering>
                <fileExtensions allowUnlisted="false" >
                    <clear />
                    <add fileExtension=".html" allowed="true"/>
                </fileExtensions>
            </requestFiltering>
        </security>
    </system.webServer>
</configuration>FormModel.php000064400000023341152351573400007151 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\MVC\Model;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Utilities\ArrayHelper;

/**
 * Prototype form model.
 *
 * @see    \JForm
 * @see    \JFormField
 * @see    \JFormRule
 * @since  1.6
 */
abstract class FormModel extends BaseDatabaseModel
{
	/**
	 * Array of form objects.
	 *
	 * @var    \JForm[]
	 * @since  1.6
	 */
	protected $_forms = array();

	/**
	 * Maps events to plugin groups.
	 *
	 * @var    array
	 * @since  3.6
	 */
	protected $events_map = null;

	/**
	 * Constructor.
	 *
	 * @param   array                $config   An optional associative array of configuration settings.
	 * @param   MVCFactoryInterface  $factory  The factory.
	 *
	 * @see     \JModelLegacy
	 * @since   3.6
	 */
	public function __construct($config = array(), MVCFactoryInterface $factory = null)
	{
		$config['events_map'] = isset($config['events_map']) ? $config['events_map'] : array();

		$this->events_map = array_merge(
			array(
				'validate' => 'content',
			),
			$config['events_map']
		);

		parent::__construct($config, $factory);
	}

	/**
	 * Method to checkin a row.
	 *
	 * @param   integer  $pk  The numeric id of the primary key.
	 *
	 * @return  boolean  False on failure or error, true otherwise.
	 *
	 * @since   1.6
	 */
	public function checkin($pk = null)
	{
		// Only attempt to check the row in if it exists.
		if ($pk)
		{
			$user = \JFactory::getUser();

			// Get an instance of the row to checkin.
			$table = $this->getTable();

			if (!$table->load($pk))
			{
				$this->setError($table->getError());

				return false;
			}

			$checkedOutField = $table->getColumnAlias('checked_out');
			$checkedOutTimeField = $table->getColumnAlias('checked_out_time');

			// If there is no checked_out or checked_out_time field, just return true.
			if (!property_exists($table, $checkedOutField) || !property_exists($table, $checkedOutTimeField))
			{
				return true;
			}

			// Check if this is the user having previously checked out the row.
			if ($table->{$checkedOutField} > 0 && $table->{$checkedOutField} != $user->get('id') && !$user->authorise('core.manage', 'com_checkin'))
			{
				$this->setError(\JText::_('JLIB_APPLICATION_ERROR_CHECKIN_USER_MISMATCH'));

				return false;
			}

			// Attempt to check the row in.
			if (!$table->checkIn($pk))
			{
				$this->setError($table->getError());

				return false;
			}
		}

		return true;
	}

	/**
	 * Method to check-out a row for editing.
	 *
	 * @param   integer  $pk  The numeric id of the primary key.
	 *
	 * @return  boolean  False on failure or error, true otherwise.
	 *
	 * @since   1.6
	 */
	public function checkout($pk = null)
	{
		// Only attempt to check the row in if it exists.
		if ($pk)
		{
			// Get an instance of the row to checkout.
			$table = $this->getTable();

			if (!$table->load($pk))
			{
				$this->setError($table->getError());

				return false;
			}

			$checkedOutField = $table->getColumnAlias('checked_out');
			$checkedOutTimeField = $table->getColumnAlias('checked_out_time');

			// If there is no checked_out or checked_out_time field, just return true.
			if (!property_exists($table, $checkedOutField) || !property_exists($table, $checkedOutTimeField))
			{
				return true;
			}

			$user = \JFactory::getUser();

			// Check if this is the user having previously checked out the row.
			if ($table->{$checkedOutField} > 0 && $table->{$checkedOutField} != $user->get('id'))
			{
				$this->setError(\JText::_('JLIB_APPLICATION_ERROR_CHECKOUT_USER_MISMATCH'));

				return false;
			}

			// Attempt to check the row out.
			if (!$table->checkOut($user->get('id'), $pk))
			{
				$this->setError($table->getError());

				return false;
			}
		}

		return true;
	}

	/**
	 * Abstract 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.
	 *
	 * @return  \JForm|boolean  A \JForm object on success, false on failure
	 *
	 * @since   1.6
	 */
	abstract public function getForm($data = array(), $loadData = true);

	/**
	 * Method to get a form object.
	 *
	 * @param   string   $name     The name of the form.
	 * @param   string   $source   The form source. Can be XML string if file flag is set to false.
	 * @param   array    $options  Optional array of options for the form creation.
	 * @param   boolean  $clear    Optional argument to force load a new form.
	 * @param   string   $xpath    An optional xpath to search for the fields.
	 *
	 * @return  \JForm|boolean  \JForm object on success, false on error.
	 *
	 * @see     \JForm
	 * @since   1.6
	 */
	protected function loadForm($name, $source = null, $options = array(), $clear = false, $xpath = false)
	{
		// Handle the optional arguments.
		$options['control'] = ArrayHelper::getValue((array) $options, 'control', false);

		// Create a signature hash. But make sure, that loading the data does not create a new instance
		$sigoptions = $options;

		if (isset($sigoptions['load_data']))
		{
			unset($sigoptions['load_data']);
		}

		$hash = md5($source . serialize($sigoptions));

		// Check if we can use a previously loaded form.
		if (!$clear && isset($this->_forms[$hash]))
		{
			return $this->_forms[$hash];
		}

		// Get the form.
		\JForm::addFormPath(JPATH_COMPONENT . '/models/forms');
		\JForm::addFieldPath(JPATH_COMPONENT . '/models/fields');
		\JForm::addFormPath(JPATH_COMPONENT . '/model/form');
		\JForm::addFieldPath(JPATH_COMPONENT . '/model/field');

		try
		{
			$form = \JForm::getInstance($name, $source, $options, false, $xpath);

			if (isset($options['load_data']) && $options['load_data'])
			{
				// Get the data for the form.
				$data = $this->loadFormData();
			}
			else
			{
				$data = array();
			}

			// 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);

			// Load the data into the form after the plugins have operated.
			$form->bind($data);
		}
		catch (\Exception $e)
		{
			$this->setError($e->getMessage());

			return false;
		}

		// Store the form for later.
		$this->_forms[$hash] = $form;

		return $form;
	}

	/**
	 * Method to get the data that should be injected in the form.
	 *
	 * @return  array  The default data is an empty array.
	 *
	 * @since   1.6
	 */
	protected function loadFormData()
	{
		return array();
	}

	/**
	 * Method to allow derived classes to preprocess the data.
	 *
	 * @param   string  $context  The context identifier.
	 * @param   mixed   &$data    The data to be processed. It gets altered directly.
	 * @param   string  $group    The name of the plugin group to import (defaults to "content").
	 *
	 * @return  void
	 *
	 * @since   3.1
	 */
	protected function preprocessData($context, &$data, $group = 'content')
	{
		// Get the dispatcher and load the users plugins.
		$dispatcher = \JEventDispatcher::getInstance();
		\JPluginHelper::importPlugin($group);

		// Trigger the data preparation event.
		$results = $dispatcher->trigger('onContentPrepareData', array($context, &$data));

		// Check for errors encountered while preparing the data.
		if (count($results) > 0 && in_array(false, $results, true))
		{
			$this->setError($dispatcher->getError());
		}
	}

	/**
	 * Method to allow derived classes to preprocess the form.
	 *
	 * @param   \JForm  $form   A \JForm 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
	 *
	 * @see     \JFormField
	 * @since   1.6
	 * @throws  \Exception if there is an error in the form event.
	 */
	protected function preprocessForm(\JForm $form, $data, $group = 'content')
	{
		// Import the appropriate plugin group.
		\JPluginHelper::importPlugin($group);

		// Get the dispatcher.
		$dispatcher = \JEventDispatcher::getInstance();

		// Trigger the form preparation event.
		$results = $dispatcher->trigger('onContentPrepareForm', array($form, $data));

		// Check for errors encountered while preparing the form.
		if (count($results) && in_array(false, $results, true))
		{
			// Get the last error.
			$error = $dispatcher->getError();

			if (!($error instanceof \Exception))
			{
				throw new \Exception($error);
			}
		}
	}

	/**
	 * Method to validate the form data.
	 *
	 * @param   \JForm  $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  array|boolean  Array of filtered data if valid, false otherwise.
	 *
	 * @see     \JFormRule
	 * @see     \JFilterInput
	 * @since   1.6
	 */
	public function validate($form, $data, $group = null)
	{
		// Include the plugins for the delete events.
		\JPluginHelper::importPlugin($this->events_map['validate']);

		$dispatcher = \JEventDispatcher::getInstance();
		$dispatcher->trigger('onUserBeforeDataValidation', array($form, &$data));

		// Filter and validate the form data.
		$data = $form->filter($data);
		$return = $form->validate($data, $group);

		// Check for an error.
		if ($return instanceof \Exception)
		{
			$this->setError($return->getMessage());

			return false;
		}

		// Check the validation results.
		if ($return === false)
		{
			// Get the validation messages from the form.
			foreach ($form->getErrors() as $message)
			{
				$this->setError($message);
			}

			return false;
		}

		// Tags B/C break at 3.1.2
		if (!isset($data['tags']) && isset($data['metadata']['tags']))
		{
			$data['tags'] = $data['metadata']['tags'];
		}

		return $data;
	}
}
BaseDatabaseModel.php000064400000034346152351573400010554 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\MVC\Model;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\MVC\Factory\LegacyFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Utilities\ArrayHelper;

/**
 * Base class for a database aware Joomla Model
 *
 * Acts as a Factory class for application specific objects and provides many supporting API functions.
 *
 * @since  2.5.5
 */
abstract class BaseDatabaseModel extends \JObject
{
	/**
	 * Indicates if the internal state has been set
	 *
	 * @var    boolean
	 * @since  3.0
	 */
	protected $__state_set = null;

	/**
	 * Database Connector
	 *
	 * @var    \JDatabaseDriver
	 * @since  3.0
	 */
	protected $_db;

	/**
	 * The model (base) name
	 *
	 * @var    string
	 * @since  3.0
	 */
	protected $name;

	/**
	 * The URL option for the component.
	 *
	 * @var    string
	 * @since  3.0
	 */
	protected $option = null;

	/**
	 * A state object
	 *
	 * @var    \JObject
	 * @since  3.0
	 */
	protected $state;

	/**
	 * The event to trigger when cleaning cache.
	 *
	 * @var    string
	 * @since  3.0
	 */
	protected $event_clean_cache = null;

	/**
	 * The factory.
	 *
	 * @var    MVCFactoryInterface
	 * @since  3.10.0
	 * @deprecated  4.0  This is a temporary property that will be moved into a trait in Joomla 4
	 */
	protected $factory;

	/**
	 * Add a directory where \JModelLegacy should search for models. You may
	 * either pass a string or an array of directories.
	 *
	 * @param   mixed   $path    A path or array[sting] of paths to search.
	 * @param   string  $prefix  A prefix for models.
	 *
	 * @return  array  An array with directory elements. If prefix is equal to '', all directories are returned.
	 *
	 * @since   3.0
	 */
	public static function addIncludePath($path = '', $prefix = '')
	{
		static $paths;

		if (!isset($paths))
		{
			$paths = array();
		}

		if (!isset($paths[$prefix]))
		{
			$paths[$prefix] = array();
		}

		if (!isset($paths['']))
		{
			$paths[''] = array();
		}

		if (!empty($path))
		{
			jimport('joomla.filesystem.path');

			foreach ((array) $path as $includePath)
			{
				if (!in_array($includePath, $paths[$prefix]))
				{
					array_unshift($paths[$prefix], \JPath::clean($includePath));
				}

				if (!in_array($includePath, $paths['']))
				{
					array_unshift($paths[''], \JPath::clean($includePath));
				}
			}
		}

		return $paths[$prefix];
	}

	/**
	 * Adds to the stack of model table paths in LIFO order.
	 *
	 * @param   mixed  $path  The directory as a string or directories as an array to add.
	 *
	 * @return  void
	 *
	 * @since   3.0
	 */
	public static function addTablePath($path)
	{
		\JTable::addIncludePath($path);
	}

	/**
	 * Create the filename for a resource
	 *
	 * @param   string  $type   The resource type to create the filename for.
	 * @param   array   $parts  An associative array of filename information.
	 *
	 * @return  string  The filename
	 *
	 * @since   3.0
	 */
	protected static function _createFileName($type, $parts = array())
	{
		$filename = '';

		switch ($type)
		{
			case 'model':
				$filename = strtolower($parts['name']) . '.php';
				break;
		}

		return $filename;
	}

	/**
	 * Returns a Model object, always creating it
	 *
	 * @param   string  $type    The model type to instantiate
	 * @param   string  $prefix  Prefix for the model class name. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  \JModelLegacy|boolean   A \JModelLegacy instance or false on failure
	 *
	 * @since   3.0
	 */
	public static function getInstance($type, $prefix = '', $config = array())
	{
		$type = preg_replace('/[^A-Z0-9_\.-]/i', '', $type);
		$modelClass = $prefix . ucfirst($type);

		if (!class_exists($modelClass))
		{
			jimport('joomla.filesystem.path');
			$path = \JPath::find(self::addIncludePath(null, $prefix), self::_createFileName('model', array('name' => $type)));

			if (!$path)
			{
				$path = \JPath::find(self::addIncludePath(null, ''), self::_createFileName('model', array('name' => $type)));
			}

			if (!$path)
			{
				return false;
			}

			require_once $path;

			if (!class_exists($modelClass))
			{
				\JLog::add(\JText::sprintf('JLIB_APPLICATION_ERROR_MODELCLASS_NOT_FOUND', $modelClass), \JLog::WARNING, 'jerror');

				return false;
			}
		}

		return new $modelClass($config);
	}

	/**
	 * Constructor
	 *
	 * @param   array                $config   An array of configuration options (name, state, dbo, table_path, ignore_request).
	 * @param   MVCFactoryInterface  $factory  The factory.
	 *
	 * @since   3.0
	 * @throws  \Exception
	 */
	public function __construct($config = array(), MVCFactoryInterface $factory = null)
	{
		// Guess the option from the class name (Option)Model(View).
		if (empty($this->option))
		{
			$r = null;

			if (!preg_match('/(.*)Model/i', get_class($this), $r))
			{
				throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_MODEL_GET_NAME'), 500);
			}

			$this->option = 'com_' . strtolower($r[1]);
		}

		// Set the view name
		if (empty($this->name))
		{
			if (array_key_exists('name', $config))
			{
				$this->name = $config['name'];
			}
			else
			{
				$this->name = $this->getName();
			}
		}

		// Set the model state
		if (array_key_exists('state', $config))
		{
			$this->state = $config['state'];
		}
		else
		{
			$this->state = new \JObject;
		}

		// Set the model dbo
		if (array_key_exists('dbo', $config))
		{
			$this->_db = $config['dbo'];
		}
		else
		{
			$this->_db = \JFactory::getDbo();
		}

		// Set the default view search path
		if (array_key_exists('table_path', $config))
		{
			$this->addTablePath($config['table_path']);
		}
		// @codeCoverageIgnoreStart
		elseif (defined('JPATH_COMPONENT_ADMINISTRATOR'))
		{
			$this->addTablePath(JPATH_COMPONENT_ADMINISTRATOR . '/tables');
			$this->addTablePath(JPATH_COMPONENT_ADMINISTRATOR . '/table');
		}

		// @codeCoverageIgnoreEnd

		// Set the internal state marker - used to ignore setting state from the request
		if (!empty($config['ignore_request']))
		{
			$this->__state_set = true;
		}

		// Set the clean cache event
		if (isset($config['event_clean_cache']))
		{
			$this->event_clean_cache = $config['event_clean_cache'];
		}
		elseif (empty($this->event_clean_cache))
		{
			$this->event_clean_cache = 'onContentCleanCache';
		}

		$this->factory = $factory ? : new LegacyFactory;
	}

	/**
	 * Gets an array of objects from the results of database query.
	 *
	 * @param   string   $query       The query.
	 * @param   integer  $limitstart  Offset.
	 * @param   integer  $limit       The number of records.
	 *
	 * @return  object[]  An array of results.
	 *
	 * @since   3.0
	 * @throws  \RuntimeException
	 */
	protected function _getList($query, $limitstart = 0, $limit = 0)
	{
		$this->getDbo()->setQuery($query, $limitstart, $limit);

		return $this->getDbo()->loadObjectList();
	}

	/**
	 * Returns a record count for the query.
	 *
	 * Note: Current implementation of this method assumes that getListQuery() returns a set of unique rows,
	 * thus it uses SELECT COUNT(*) to count the rows. In cases that getListQuery() uses DISTINCT
	 * then either this method must be overridden by a custom implementation at the derived Model Class
	 * or a GROUP BY clause should be used to make the set unique.
	 *
	 * @param   \JDatabaseQuery|string  $query  The query.
	 *
	 * @return  integer  Number of rows for query.
	 *
	 * @since   3.0
	 */
	protected function _getListCount($query)
	{
		// Use fast COUNT(*) on \JDatabaseQuery objects if there is no GROUP BY or HAVING clause:
		if ($query instanceof \JDatabaseQuery
			&& $query->type == 'select'
			&& $query->group === null
			&& $query->union === null
			&& $query->unionAll === null
			&& $query->having === null)
		{
			$query = clone $query;
			$query->clear('select')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)');

			$this->getDbo()->setQuery($query);

			return (int) $this->getDbo()->loadResult();
		}

		// Otherwise fall back to inefficient way of counting all results.

		// Remove the limit, offset and order parts if it's a \JDatabaseQuery object
		if ($query instanceof \JDatabaseQuery)
		{
			$query = clone $query;
			$query->clear('limit')->clear('offset')->clear('order');
		}

		$this->getDbo()->setQuery($query);
		$this->getDbo()->execute();

		return (int) $this->getDbo()->getNumRows();
	}

	/**
	 * Method to load and return a model object.
	 *
	 * @param   string  $name    The name of the view
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration settings to pass to \JTable::getInstance
	 *
	 * @return  \JTable|boolean  Table object or boolean false if failed
	 *
	 * @since   3.0
	 * @see     \JTable::getInstance()
	 */
	protected function _createTable($name, $prefix = 'Table', $config = array())
	{
		// Make sure we are returning a DBO object
		if (!array_key_exists('dbo', $config))
		{
			$config['dbo'] = $this->getDbo();
		}

		$table = $this->factory->createTable($name, $prefix, $config);

		if ($table === null)
		{
			return false;
		}

		return $table;
	}

	/**
	 * Method to get the database driver object
	 *
	 * @return  \JDatabaseDriver
	 *
	 * @since   3.0
	 */
	public function getDbo()
	{
		return $this->_db;
	}

	/**
	 * 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
	 *
	 * @since   3.0
	 * @throws  \Exception
	 */
	public function getName()
	{
		if (empty($this->name))
		{
			$r = null;

			if (!preg_match('/Model(.*)/i', get_class($this), $r))
			{
				throw new \Exception(\JText::_('JLIB_APPLICATION_ERROR_MODEL_GET_NAME'), 500);
			}

			$this->name = strtolower($r[1]);
		}

		return $this->name;
	}

	/**
	 * Method to get model state variables
	 *
	 * @param   string  $property  Optional parameter name
	 * @param   mixed   $default   Optional default value
	 *
	 * @return  mixed  The property where specified, the state object where omitted
	 *
	 * @since   3.0
	 */
	public function 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;
		}

		return $property === null ? $this->state : $this->state->get($property, $default);
	}

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $name     The table name. Optional.
	 * @param   string  $prefix   The class prefix. Optional.
	 * @param   array   $options  Configuration array for model. Optional.
	 *
	 * @return  \JTable  A \JTable object
	 *
	 * @since   3.0
	 * @throws  \Exception
	 */
	public function getTable($name = '', $prefix = 'Table', $options = array())
	{
		if (empty($name))
		{
			$name = $this->getName();
		}

		if ($table = $this->_createTable($name, $prefix, $options))
		{
			return $table;
		}

		throw new \Exception(\JText::sprintf('JLIB_APPLICATION_ERROR_TABLE_NAME_NOT_SUPPORTED', $name), 0);
	}

	/**
	 * Method to load a row for editing from the version history table.
	 *
	 * @param   integer  $versionId  Key to the version history table.
	 * @param   \JTable  &$table     Content table object being loaded.
	 *
	 * @return  boolean  False on failure or error, true otherwise.
	 *
	 * @since   3.2
	 */
	public function loadHistory($versionId, \JTable &$table)
	{
		// Only attempt to check the row in if it exists, otherwise do an early exit.
		if (!$versionId)
		{
			return false;
		}

		// Get an instance of the row to checkout.
		$historyTable = \JTable::getInstance('Contenthistory');

		if (!$historyTable->load($versionId))
		{
			$this->setError($historyTable->getError());

			return false;
		}

		$rowArray = ArrayHelper::fromObject(json_decode($historyTable->version_data));
		$typeId   = \JTable::getInstance('Contenttype')->getTypeId($this->typeAlias);

		if ($historyTable->ucm_type_id != $typeId)
		{
			$this->setError(\JText::_('JLIB_APPLICATION_ERROR_HISTORY_ID_MISMATCH'));

			$key = $table->getKeyName();

			if (isset($rowArray[$key]))
			{
				$table->checkIn($rowArray[$key]);
			}

			return false;
		}

		$this->setState('save_date', $historyTable->save_date);
		$this->setState('version_note', $historyTable->version_note);

		return $table->bind($rowArray);
	}

	/**
	 * 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.
	 * @since   3.0
	 */
	protected function populateState()
	{
	}

	/**
	 * Method to set the database driver object
	 *
	 * @param   \JDatabaseDriver  $db  A \JDatabaseDriver based object
	 *
	 * @return  void
	 *
	 * @since   3.0
	 */
	public function setDbo($db)
	{
		$this->_db = $db;
	}

	/**
	 * 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.
	 *
	 * @since   3.0
	 */
	public function setState($property, $value = null)
	{
		return $this->state->set($property, $value);
	}

	/**
	 * Clean the cache
	 *
	 * @param   string   $group     The cache group
	 * @param   integer  $clientId  The ID of the client
	 *
	 * @return  void
	 *
	 * @since   3.0
	 */
	protected function cleanCache($group = null, $clientId = 0)
	{
		$conf = \JFactory::getConfig();

		$options = array(
			'defaultgroup' => $group ?: (isset($this->option) ? $this->option : \JFactory::getApplication()->input->get('option')),
			'cachebase' => $clientId ? JPATH_ADMINISTRATOR . '/cache' : $conf->get('cache_path', JPATH_SITE . '/cache'),
			'result' => true,
		);

		try
		{
			/** @var \JCacheControllerCallback $cache */
			$cache = \JCache::getInstance('callback', $options);
			$cache->clean();
		}
		catch (\JCacheException $exception)
		{
			$options['result'] = false;
		}

		// Trigger the onContentCleanCache event.
		\JEventDispatcher::getInstance()->trigger($this->event_clean_cache, $options);
	}
}
ListModel.php000064400000046410152351573400007163 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\MVC\Model;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Utilities\ArrayHelper;

/**
 * Model class for handling lists of items.
 *
 * @since  1.6
 */
class ListModel extends BaseDatabaseModel
{
	/**
	 * Internal memory based cache array of data.
	 *
	 * @var    array
	 * @since  1.6
	 */
	protected $cache = array();

	/**
	 * Context string for the model type.  This is used to handle uniqueness
	 * when dealing with the getStoreId() method and caching data structures.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $context = null;

	/**
	 * Valid filter fields or ordering.
	 *
	 * @var    array
	 * @since  1.6
	 */
	protected $filter_fields = array();

	/**
	 * An internal cache for the last query used.
	 *
	 * @var    \JDatabaseQuery[]
	 * @since  1.6
	 */
	protected $query = array();

	/**
	 * The cache ID used when last populating $this->query
	 *
	 * @var   null|string
	 * @since 3.10.4
	 */
	protected $lastQueryStoreId = null;

	/**
	 * Name of the filter form to load
	 *
	 * @var    string
	 * @since  3.2
	 */
	protected $filterFormName = null;

	/**
	 * Associated HTML form
	 *
	 * @var    string
	 * @since  3.2
	 */
	protected $htmlFormName = 'adminForm';

	/**
	 * A blacklist of filter variables to not merge into the model's state
	 *
	 * @var    array
	 * @since  3.4.5
	 */
	protected $filterBlacklist = array();

	/**
	 * A blacklist of list variables to not merge into the model's state
	 *
	 * @var    array
	 * @since  3.4.5
	 */
	protected $listBlacklist = array('select');

	/**
	 * Constructor.
	 *
	 * @param   array                $config   An optional associative array of configuration settings.
	 * @param   MVCFactoryInterface  $factory  The factory.
	 *
	 * @see     \JModelLegacy
	 * @since   1.6
	 */
	public function __construct($config = array(), MVCFactoryInterface $factory = null)
	{
		parent::__construct($config, $factory);

		// Add the ordering filtering fields whitelist.
		if (isset($config['filter_fields']))
		{
			$this->filter_fields = $config['filter_fields'];
		}

		// Guess the context as Option.ModelName.
		if (empty($this->context))
		{
			$this->context = strtolower($this->option . '.' . $this->getName());
		}
	}

	/**
	 * Method to cache the last query constructed.
	 *
	 * This method ensures that the query is constructed only once for a given state of the model.
	 *
	 * @return  \JDatabaseQuery  A \JDatabaseQuery object
	 *
	 * @since   1.6
	 */
	protected function _getListQuery()
	{
		// Compute the current store id.
		$currentStoreId = $this->getStoreId();

		// If the last store id is different from the current, refresh the query.
		if ($this->lastQueryStoreId !== $currentStoreId || empty($this->query))
		{
			$this->lastQueryStoreId = $currentStoreId;
			$this->query            = $this->getListQuery();
		}

		return $this->query;
	}

	/**
	 * Function to get the active filters
	 *
	 * @return  array  Associative array in the format: array('filter_published' => 0)
	 *
	 * @since   3.2
	 */
	public function getActiveFilters()
	{
		$activeFilters = array();

		if (!empty($this->filter_fields))
		{
			foreach ($this->filter_fields as $filter)
			{
				$filterName = 'filter.' . $filter;

				if (property_exists($this->state, $filterName) && (!empty($this->state->{$filterName}) || is_numeric($this->state->{$filterName})))
				{
					$activeFilters[$filter] = $this->state->get($filterName);
				}
			}
		}

		return $activeFilters;
	}

	/**
	 * Method to get an array of data items.
	 *
	 * @return  mixed  An array of data items on success, false on failure.
	 *
	 * @since   1.6
	 */
	public function getItems()
	{
		// Get a storage key.
		$store = $this->getStoreId();

		// Try to load the data from internal storage.
		if (isset($this->cache[$store]))
		{
			return $this->cache[$store];
		}

		try
		{
			// Load the list items and add the items to the internal cache.
			$this->cache[$store] = $this->_getList($this->_getListQuery(), $this->getStart(), $this->getState('list.limit'));
		}
		catch (\RuntimeException $e)
		{
			$this->setError($e->getMessage());

			return false;
		}

		return $this->cache[$store];
	}

	/**
	 * Method to get a \JDatabaseQuery object for retrieving the data set from a database.
	 *
	 * @return  \JDatabaseQuery  A \JDatabaseQuery object to retrieve the data set.
	 *
	 * @since   1.6
	 */
	protected function getListQuery()
	{
		return $this->getDbo()->getQuery(true);
	}

	/**
	 * Method to get a \JPagination object for the data set.
	 *
	 * @return  \JPagination  A \JPagination object for the data set.
	 *
	 * @since   1.6
	 */
	public function getPagination()
	{
		// Get a storage key.
		$store = $this->getStoreId('getPagination');

		// Try to load the data from internal storage.
		if (isset($this->cache[$store]))
		{
			return $this->cache[$store];
		}

		$limit = (int) $this->getState('list.limit') - (int) $this->getState('list.links');

		// Create the pagination object and add the object to the internal cache.
		$this->cache[$store] = new \JPagination($this->getTotal(), $this->getStart(), $limit);

		return $this->cache[$store];
	}

	/**
	 * Method to get a store id based on the model configuration state.
	 *
	 * This is necessary because the model is used by the component and
	 * different modules that might need different sets of data or different
	 * ordering requirements.
	 *
	 * @param   string  $id  An identifier string to generate the store id.
	 *
	 * @return  string  A store id.
	 *
	 * @since   1.6
	 */
	protected function getStoreId($id = '')
	{
		// Add the list state to the store id.
		$id .= ':' . $this->getState('list.start');
		$id .= ':' . $this->getState('list.limit');
		$id .= ':' . $this->getState('list.ordering');
		$id .= ':' . $this->getState('list.direction');

		return md5($this->context . ':' . $id);
	}

	/**
	 * Method to get the total number of items for the data set.
	 *
	 * @return  integer  The total number of items available in the data set.
	 *
	 * @since   1.6
	 */
	public function getTotal()
	{
		// Get a storage key.
		$store = $this->getStoreId('getTotal');

		// Try to load the data from internal storage.
		if (isset($this->cache[$store]))
		{
			return $this->cache[$store];
		}

		try
		{
			// Load the total and add the total to the internal cache.
			$this->cache[$store] = (int) $this->_getListCount($this->_getListQuery());
		}
		catch (\RuntimeException $e)
		{
			$this->setError($e->getMessage());

			return false;
		}

		return $this->cache[$store];
	}

	/**
	 * Method to get the starting number of items for the data set.
	 *
	 * @return  integer  The starting number of items available in the data set.
	 *
	 * @since   1.6
	 */
	public function getStart()
	{
		$store = $this->getStoreId('getstart');

		// Try to load the data from internal storage.
		if (isset($this->cache[$store]))
		{
			return $this->cache[$store];
		}

		$start = $this->getState('list.start');

		if ($start > 0)
		{
			$limit = $this->getState('list.limit');
			$total = $this->getTotal();

			if ($start > $total - $limit)
			{
				$start = max(0, (int) (ceil($total / $limit) - 1) * $limit);
			}
		}

		// Add the total to the internal cache.
		$this->cache[$store] = $start;

		return $this->cache[$store];
	}

	/**
	 * Get the filter form
	 *
	 * @param   array    $data      data
	 * @param   boolean  $loadData  load current data
	 *
	 * @return  \JForm|boolean  The \JForm object or false on error
	 *
	 * @since   3.2
	 */
	public function getFilterForm($data = array(), $loadData = true)
	{
		$form = null;

		// Try to locate the filter form automatically. Example: ContentModelArticles => "filter_articles"
		if (empty($this->filterFormName))
		{
			$classNameParts = explode('Model', get_called_class());

			if (count($classNameParts) == 2)
			{
				$this->filterFormName = 'filter_' . strtolower($classNameParts[1]);
			}
		}

		if (!empty($this->filterFormName))
		{
			// Get the form.
			$form = $this->loadForm($this->context . '.filter', $this->filterFormName, array('control' => '', 'load_data' => $loadData));
		}

		return $form;
	}

	/**
	 * Method to get a form object.
	 *
	 * @param   string          $name     The name of the form.
	 * @param   string          $source   The form source. Can be XML string if file flag is set to false.
	 * @param   array           $options  Optional array of options for the form creation.
	 * @param   boolean         $clear    Optional argument to force load a new form.
	 * @param   string|boolean  $xpath    An optional xpath to search for the fields.
	 *
	 * @return  \JForm|boolean  \JForm object on success, False on error.
	 *
	 * @see     \JForm
	 * @since   3.2
	 */
	protected function loadForm($name, $source = null, $options = array(), $clear = false, $xpath = false)
	{
		// Handle the optional arguments.
		$options['control'] = ArrayHelper::getValue((array) $options, 'control', false);

		// Create a signature hash.
		$hash = md5($source . serialize($options));

		// Check if we can use a previously loaded form.
		if (!$clear && isset($this->_forms[$hash]))
		{
			return $this->_forms[$hash];
		}

		// Get the form.
		\JForm::addFormPath(JPATH_COMPONENT . '/models/forms');
		\JForm::addFieldPath(JPATH_COMPONENT . '/models/fields');

		try
		{
			$form = \JForm::getInstance($name, $source, $options, false, $xpath);

			if (isset($options['load_data']) && $options['load_data'])
			{
				// Get the data for the form.
				$data = $this->loadFormData();
			}
			else
			{
				$data = array();
			}

			// 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);

			// Load the data into the form after the plugins have operated.
			$form->bind($data);
		}
		catch (\Exception $e)
		{
			$this->setError($e->getMessage());

			return false;
		}

		// Store the form for later.
		$this->_forms[$hash] = $form;

		return $form;
	}

	/**
	 * Method to get the data that should be injected in the form.
	 *
	 * @return	mixed	The data for the form.
	 *
	 * @since	3.2
	 */
	protected function loadFormData()
	{
		// Check the session for previously entered form data.
		$data = \JFactory::getApplication()->getUserState($this->context, new \stdClass);

		// Pre-fill the list options
		if (!property_exists($data, 'list'))
		{
			$data->list = array(
				'direction' => $this->getState('list.direction'),
				'limit'     => $this->getState('list.limit'),
				'ordering'  => $this->getState('list.ordering'),
				'start'     => $this->getState('list.start'),
			);
		}

		return $data;
	}

	/**
	 * 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.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @param   string  $ordering   An optional ordering field.
	 * @param   string  $direction  An optional direction (asc|desc).
	 *
	 * @return  void
	 *
	 * @since   1.6
	 */
	protected function populateState($ordering = null, $direction = null)
	{
		// If the context is set, assume that stateful lists are used.
		if ($this->context)
		{
			$app         = \JFactory::getApplication();
			$inputFilter = \JFilterInput::getInstance();

			// Receive & set filters
			if ($filters = $app->getUserStateFromRequest($this->context . '.filter', 'filter', array(), 'array'))
			{
				foreach ($filters as $name => $value)
				{
					// Exclude if blacklisted
					if (!in_array($name, $this->filterBlacklist))
					{
						$this->setState('filter.' . $name, $value);
					}
				}
			}

			$limit = 0;

			// Receive & set list options
			if ($list = $app->getUserStateFromRequest($this->context . '.list', 'list', array(), 'array'))
			{
				foreach ($list as $name => $value)
				{
					// Exclude if blacklisted
					if (!in_array($name, $this->listBlacklist))
					{
						// Extra validations
						switch ($name)
						{
							case 'fullordering':
								$orderingParts = explode(' ', $value);

								if (count($orderingParts) >= 2)
								{
									// Latest part will be considered the direction
									$fullDirection = end($orderingParts);

									if (in_array(strtoupper($fullDirection), array('ASC', 'DESC', '')))
									{
										$this->setState('list.direction', $fullDirection);
									}
									else
									{
										$this->setState('list.direction', $direction);

										// Fallback to the default value
										$value = $ordering . ' ' . $direction;
									}

									unset($orderingParts[count($orderingParts) - 1]);

									// The rest will be the ordering
									$fullOrdering = implode(' ', $orderingParts);

									if (in_array($fullOrdering, $this->filter_fields))
									{
										$this->setState('list.ordering', $fullOrdering);
									}
									else
									{
										$this->setState('list.ordering', $ordering);

										// Fallback to the default value
										$value = $ordering . ' ' . $direction;
									}
								}
								else
								{
									$this->setState('list.ordering', $ordering);
									$this->setState('list.direction', $direction);

									// Fallback to the default value
									$value = $ordering . ' ' . $direction;
								}
								break;

							case 'ordering':
								if (!in_array($value, $this->filter_fields))
								{
									$value = $ordering;
								}
								break;

							case 'direction':
								if (!in_array(strtoupper($value), array('ASC', 'DESC', '')))
								{
									$value = $direction;
								}
								break;

							case 'limit':
								$value = $inputFilter->clean($value, 'int');
								$limit = $value;
								break;

							case 'select':
								$explodedValue = explode(',', $value);

								foreach ($explodedValue as &$field)
								{
									$field = $inputFilter->clean($field, 'cmd');
								}

								$value = implode(',', $explodedValue);
								break;
						}

						$this->setState('list.' . $name, $value);
					}
				}
			}
			else
			// Keep B/C for components previous to jform forms for filters
			{
				// Pre-fill the limits
				$limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint');
				$this->setState('list.limit', $limit);

				// Check if the ordering field is in the whitelist, otherwise use the incoming value.
				$value = $app->getUserStateFromRequest($this->context . '.ordercol', 'filter_order', $ordering);

				if (!in_array($value, $this->filter_fields))
				{
					$value = $ordering;
					$app->setUserState($this->context . '.ordercol', $value);
				}

				$this->setState('list.ordering', $value);

				// Check if the ordering direction is valid, otherwise use the incoming value.
				$value = $app->getUserStateFromRequest($this->context . '.orderdirn', 'filter_order_Dir', $direction);

				if (!in_array(strtoupper($value), array('ASC', 'DESC', '')))
				{
					$value = $direction;
					$app->setUserState($this->context . '.orderdirn', $value);
				}

				$this->setState('list.direction', $value);
			}

			// Support old ordering field
			$oldOrdering = $app->input->get('filter_order');

			if (!empty($oldOrdering) && in_array($oldOrdering, $this->filter_fields))
			{
				$this->setState('list.ordering', $oldOrdering);
			}

			// Support old direction field
			$oldDirection = $app->input->get('filter_order_Dir');

			if (!empty($oldDirection) && in_array(strtoupper($oldDirection), array('ASC', 'DESC', '')))
			{
				$this->setState('list.direction', $oldDirection);
			}

			$value = $app->getUserStateFromRequest($this->context . '.limitstart', 'limitstart', 0, 'int');
			$limitstart = ($limit != 0 ? (floor($value / $limit) * $limit) : 0);
			$this->setState('list.start', $limitstart);
		}
		else
		{
			$this->setState('list.start', 0);
			$this->setState('list.limit', 0);
		}
	}

	/**
	 * Method to allow derived classes to preprocess the form.
	 *
	 * @param   \JForm  $form   A \JForm 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   3.2
	 * @throws  \Exception if there is an error in the form event.
	 */
	protected function preprocessForm(\JForm $form, $data, $group = 'content')
	{
		// Import the appropriate plugin group.
		\JPluginHelper::importPlugin($group);

		// Get the dispatcher.
		$dispatcher = \JEventDispatcher::getInstance();

		// Trigger the form preparation event.
		$results = $dispatcher->trigger('onContentPrepareForm', array($form, $data));

		// Check for errors encountered while preparing the form.
		if (count($results) && in_array(false, $results, true))
		{
			// Get the last error.
			$error = $dispatcher->getError();

			if (!($error instanceof \Exception))
			{
				throw new \Exception($error);
			}
		}
	}

	/**
	 * Gets the value of a user state variable and sets it in the session
	 *
	 * This is the same as the method in \JApplication except that this also can optionally
	 * force you back to the first page when a filter has changed
	 *
	 * @param   string   $key        The key of the user state variable.
	 * @param   string   $request    The name of the variable passed in a request.
	 * @param   string   $default    The default value for the variable if not found. Optional.
	 * @param   string   $type       Filter for the variable, for valid values see {@link \JFilterInput::clean()}. Optional.
	 * @param   boolean  $resetPage  If true, the limitstart in request is set to zero
	 *
	 * @return  mixed  The request user state.
	 *
	 * @since   1.6
	 */
	public function getUserStateFromRequest($key, $request, $default = null, $type = 'none', $resetPage = true)
	{
		$app       = \JFactory::getApplication();
		$input     = $app->input;
		$old_state = $app->getUserState($key);
		$cur_state = $old_state !== null ? $old_state : $default;
		$new_state = $input->get($request, null, $type);

		// BC for Search Tools which uses different naming
		if ($new_state === null && strpos($request, 'filter_') === 0)
		{
			$name    = substr($request, 7);
			$filters = $app->input->get('filter', array(), 'array');

			if (isset($filters[$name]))
			{
				$new_state = $filters[$name];
			}
		}

		if ($cur_state != $new_state && $new_state !== null && $resetPage)
		{
			$input->set('limitstart', 0);
		}

		// Save the new value only if it is set in this request.
		if ($new_state !== null)
		{
			$app->setUserState($key, $new_state);
		}
		else
		{
			$new_state = $cur_state;
		}

		return $new_state;
	}

	/**
	 * Parse and transform the search string into a string fit for regex-ing arbitrary strings against
	 *
	 * @param   string  $search          The search string
	 * @param   string  $regexDelimiter  The regex delimiter to use for the quoting
	 *
	 * @return  string  Search string escaped for regex
	 *
	 * @since   3.4
	 */
	protected function refineSearchStringToRegex($search, $regexDelimiter = '/')
	{
		$searchArr = explode('|', trim($search, ' |'));

		foreach ($searchArr as $key => $searchString)
		{
			if (trim($searchString) === '')
			{
				unset($searchArr[$key]);
				continue;
			}

			$searchArr[$key] = str_replace(' ', '.*', preg_quote(trim($searchString), $regexDelimiter));
		}

		return implode('|', $searchArr);
	}
}
ItemModel.php000064400000002020152351573400007133 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\MVC\Model;

defined('JPATH_PLATFORM') or die;

/**
 * Prototype item model.
 *
 * @since  1.6
 */
abstract class ItemModel extends BaseDatabaseModel
{
	/**
	 * An item.
	 *
	 * @var    array
	 * @since  1.6
	 */
	protected $_item = null;

	/**
	 * Model context string.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $_context = 'group.type';

	/**
	 * Method to get a store id based on model configuration state.
	 *
	 * This is necessary because the model is used by the component and
	 * different modules that might need different sets of data or different
	 * ordering requirements.
	 *
	 * @param   string  $id  A prefix for the store id.
	 *
	 * @return  string  A store id.
	 *
	 * @since   1.6
	 */
	protected function getStoreId($id = '')
	{
		// Compile the store id.
		return md5($id);
	}
}
AdminModel.php000064400000116336152351573400007305 0ustar00<?php
/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\MVC\Model;

defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\Utilities\ArrayHelper;

/**
 * Prototype admin model.
 *
 * @since  1.6
 */
abstract class AdminModel extends FormModel
{
	/**
	 * The type alias for this content type (for example, 'com_content.article').
	 *
	 * @var    string
	 * @since  3.8.6
	 */
	public $typeAlias;

	/**
	 * The prefix to use with controller messages.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $text_prefix = null;

	/**
	 * The event to trigger after deleting the data.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $event_after_delete = null;

	/**
	 * The event to trigger after saving the data.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $event_after_save = null;

	/**
	 * The event to trigger before deleting the data.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $event_before_delete = null;

	/**
	 * The event to trigger before saving the data.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $event_before_save = null;

	/**
	 * The event to trigger after changing the published state of the data.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $event_change_state = null;

	/**
	 * Batch copy/move command. If set to false,
	 * the batch copy/move command is not supported
	 *
	 * @var    string
	 * @since  3.4
	 */
	protected $batch_copymove = 'category_id';

	/**
	 * Allowed batch commands
	 *
	 * @var    array
	 * @since  3.4
	 */
	protected $batch_commands = array(
		'assetgroup_id' => 'batchAccess',
		'language_id' => 'batchLanguage',
		'tag' => 'batchTag',
	);

	/**
	 * The context used for the associations table
	 *
	 * @var     string
	 * @since   3.4.4
	 */
	protected $associationsContext = null;

	/**
	 * A flag to indicate if member variables for batch actions (and saveorder) have been initialized
	 *
	 * @var     object
	 * @since   3.8.2
	 */
	protected $batchSet = null;

	/**
	 * The user performing the actions (re-usable in batch methods & saveorder(), initialized via initBatch())
	 *
	 * @var     object
	 * @since   3.8.2
	 */
	protected $user = null;

	/**
	 * A JTable instance (of appropriate type) to manage the DB records (re-usable in batch methods & saveorder(), initialized via initBatch())
	 *
	 * @var     object
	 * @since   3.8.2
	 */
	protected $table = null;

	/**
	 * The class name of the JTable instance managing the DB records (re-usable in batch methods & saveorder(), initialized via initBatch())
	 *
	 * @var     string
	 * @since   3.8.2
	 */
	protected $tableClassName = null;

	/**
	 * UCM Type corresponding to the current model class (re-usable in batch action methods, initialized via initBatch())
	 *
	 * @var     object
	 * @since   3.8.2
	 */
	protected $contentType = null;

	/**
	 * DB data of UCM Type corresponding to the current model class (re-usable in batch action methods, initialized via initBatch())
	 *
	 * @var     object
	 * @since   3.8.2
	 */
	protected $type = null;

	/**
	 * A tags Observer instance to handle assigned tags (re-usable in batch action methods, initialized via initBatch())
	 *
	 * @var     object
	 * @since   3.8.2
	 */
	protected $tagsObserver = null;


	/**
	 * Constructor.
	 *
	 * @param   array                $config   An optional associative array of configuration settings.
	 * @param   MVCFactoryInterface  $factory  The factory.
	 *
	 * @see     \JModelLegacy
	 * @since   1.6
	 */
	public function __construct($config = array(), MVCFactoryInterface $factory = null)
	{
		parent::__construct($config, $factory);

		if (isset($config['event_after_delete']))
		{
			$this->event_after_delete = $config['event_after_delete'];
		}
		elseif (empty($this->event_after_delete))
		{
			$this->event_after_delete = 'onContentAfterDelete';
		}

		if (isset($config['event_after_save']))
		{
			$this->event_after_save = $config['event_after_save'];
		}
		elseif (empty($this->event_after_save))
		{
			$this->event_after_save = 'onContentAfterSave';
		}

		if (isset($config['event_before_delete']))
		{
			$this->event_before_delete = $config['event_before_delete'];
		}
		elseif (empty($this->event_before_delete))
		{
			$this->event_before_delete = 'onContentBeforeDelete';
		}

		if (isset($config['event_before_save']))
		{
			$this->event_before_save = $config['event_before_save'];
		}
		elseif (empty($this->event_before_save))
		{
			$this->event_before_save = 'onContentBeforeSave';
		}

		if (isset($config['event_change_state']))
		{
			$this->event_change_state = $config['event_change_state'];
		}
		elseif (empty($this->event_change_state))
		{
			$this->event_change_state = 'onContentChangeState';
		}

		$config['events_map'] = isset($config['events_map']) ? $config['events_map'] : array();

		$this->events_map = array_merge(
			array(
				'delete'       => 'content',
				'save'         => 'content',
				'change_state' => 'content',
				'validate'     => 'content',
			), $config['events_map']
		);

		// Guess the \JText message prefix. Defaults to the option.
		if (isset($config['text_prefix']))
		{
			$this->text_prefix = strtoupper($config['text_prefix']);
		}
		elseif (empty($this->text_prefix))
		{
			$this->text_prefix = strtoupper($this->option);
		}
	}

	/**
	 * Method to perform batch operations on an item or a set of items.
	 *
	 * @param   array  $commands  An array of commands to perform.
	 * @param   array  $pks       An array of item ids.
	 * @param   array  $contexts  An array of item contexts.
	 *
	 * @return  boolean  Returns true on success, false on failure.
	 *
	 * @since   1.7
	 */
	public function batch($commands, $pks, $contexts)
	{
		// Sanitize ids.
		$pks = array_unique($pks);
		$pks = ArrayHelper::toInteger($pks);

		// Remove any values of zero.
		if (array_search(0, $pks, true))
		{
			unset($pks[array_search(0, $pks, true)]);
		}

		if (empty($pks))
		{
			$this->setError(\JText::_('JGLOBAL_NO_ITEM_SELECTED'));

			return false;
		}

		$done = false;

		// Initialize re-usable member properties
		$this->initBatch();

		if ($this->batch_copymove && !empty($commands[$this->batch_copymove]))
		{
			$cmd = ArrayHelper::getValue($commands, 'move_copy', 'c');

			if ($cmd === 'c')
			{
				$result = $this->batchCopy($commands[$this->batch_copymove], $pks, $contexts);

				if (is_array($result))
				{
					foreach ($result as $old => $new)
					{
						$contexts[$new] = $contexts[$old];
					}

					$pks = array_values($result);
				}
				else
				{
					return false;
				}
			}
			elseif ($cmd === 'm' && !$this->batchMove($commands[$this->batch_copymove], $pks, $contexts))
			{
				return false;
			}

			$done = true;
		}

		foreach ($this->batch_commands as $identifier => $command)
		{
			if (!empty($commands[$identifier]))
			{
				if (!$this->$command($commands[$identifier], $pks, $contexts))
				{
					return false;
				}

				$done = true;
			}
		}

		if (!$done)
		{
			$this->setError(\JText::_('JLIB_APPLICATION_ERROR_INSUFFICIENT_BATCH_INFORMATION'));

			return false;
		}

		// Clear the cache
		$this->cleanCache();

		return true;
	}

	/**
	 * Batch access level changes for a group of rows.
	 *
	 * @param   integer  $value     The new value matching an Asset Group ID.
	 * @param   array    $pks       An array of row IDs.
	 * @param   array    $contexts  An array of item contexts.
	 *
	 * @return  boolean  True if successful, false otherwise and internal error is set.
	 *
	 * @since   1.7
	 */
	protected function batchAccess($value, $pks, $contexts)
	{
		// Initialize re-usable member properties, and re-usable local variables
		$this->initBatch();

		foreach ($pks as $pk)
		{
			if ($this->user->authorise('core.edit', $contexts[$pk]))
			{
				$this->table->reset();
				$this->table->load($pk);
				$this->table->access = (int) $value;

				if (!empty($this->type))
				{
					$this->createTagsHelper($this->tagsObserver, $this->type, $pk, $this->typeAlias, $this->table);
				}

				if (!$this->table->store())
				{
					$this->setError($this->table->getError());

					return false;
				}
			}
			else
			{
				$this->setError(\JText::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT'));

				return false;
			}
		}

		// Clean the cache
		$this->cleanCache();

		return true;
	}

	/**
	 * Batch copy items to a new category or current.
	 *
	 * @param   integer  $value     The new category.
	 * @param   array    $pks       An array of row IDs.
	 * @param   array    $contexts  An array of item contexts.
	 *
	 * @return  array|boolean  An array of new IDs on success, boolean false on failure.
	 *
	 * @since	1.7
	 */
	protected function batchCopy($value, $pks, $contexts)
	{
		// Initialize re-usable member properties, and re-usable local variables
		$this->initBatch();

		$categoryId = $value;

		if (!$this->checkCategoryId($categoryId))
		{
			return false;
		}

		$newIds = array();
		$db     = $this->getDbo();

		// Parent exists so let's proceed
		while (!empty($pks))
		{
			// Pop the first ID off the stack
			$pk = array_shift($pks);

			$this->table->reset();

			// Check that the row actually exists
			if (!$this->table->load($pk))
			{
				if ($error = $this->table->getError())
				{
					// Fatal error
					$this->setError($error);

					return false;
				}
				else
				{
					// Not fatal error
					$this->setError(\JText::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk));
					continue;
				}
			}

			// Check for asset_id
			if ($this->table->hasField($this->table->getColumnAlias('asset_id')))
			{
				$oldAssetId = $this->table->asset_id;
			}

			$this->generateTitle($categoryId, $this->table);

			// Reset the ID because we are making a copy
			$this->table->id = 0;

			// Unpublish because we are making a copy
			if (isset($this->table->published))
			{
				$this->table->published = 0;
			}
			elseif (isset($this->table->state))
			{
				$this->table->state = 0;
			}

			$hitsAlias = $this->table->getColumnAlias('hits');

			if (isset($this->table->$hitsAlias))
			{
				$this->table->$hitsAlias = 0;
			}

			// New category ID
			$this->table->catid = $categoryId;

			// TODO: Deal with ordering?
			// $this->table->ordering = 1;

			// Check the row.
			if (!$this->table->check())
			{
				$this->setError($this->table->getError());

				return false;
			}

			if (!empty($this->type))
			{
				$this->createTagsHelper($this->tagsObserver, $this->type, $pk, $this->typeAlias, $this->table);
			}

			// Store the row.
			if (!$this->table->store())
			{
				$this->setError($this->table->getError());

				return false;
			}

			// Get the new item ID
			$newId = $this->table->get('id');

			if (!empty($oldAssetId))
			{
				// Copy rules
				$query = $db->getQuery(true);
				$query->clear()
					->update($db->quoteName('#__assets', 't'))
					->join('INNER', $db->quoteName('#__assets', 's') .
						' ON ' . $db->quoteName('s.id') . ' = ' . $oldAssetId
					)
					->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules'))
					->where($db->quoteName('t.id') . ' = ' . $this->table->asset_id);

				$db->setQuery($query)->execute();
			}

			$this->cleanupPostBatchCopy($this->table, $newId, $pk);

			// Add the new ID to the array
			$newIds[$pk] = $newId;
		}

		// Clean the cache
		$this->cleanCache();

		return $newIds;
	}

	/**
	 * Function that can be overridden to do any data cleanup after batch copying data
	 *
	 * @param   \JTableInterface  $table  The table object containing the newly created item
	 * @param   integer           $newId  The id of the new item
	 * @param   integer           $oldId  The original item id
	 *
	 * @return  void
	 *
	 * @since  3.8.12
	 */
	protected function cleanupPostBatchCopy(\JTableInterface $table, $newId, $oldId)
	{
	}

	/**
	 * Batch language changes for a group of rows.
	 *
	 * @param   string  $value     The new value matching a language.
	 * @param   array   $pks       An array of row IDs.
	 * @param   array   $contexts  An array of item contexts.
	 *
	 * @return  boolean  True if successful, false otherwise and internal error is set.
	 *
	 * @since   2.5
	 */
	protected function batchLanguage($value, $pks, $contexts)
	{
		// Initialize re-usable member properties, and re-usable local variables
		$this->initBatch();

		foreach ($pks as $pk)
		{
			if ($this->user->authorise('core.edit', $contexts[$pk]))
			{
				$this->table->reset();
				$this->table->load($pk);
				$this->table->language = $value;

				if (!empty($this->type))
				{
					$this->createTagsHelper($this->tagsObserver, $this->type, $pk, $this->typeAlias, $this->table);
				}

				if (!$this->table->store())
				{
					$this->setError($this->table->getError());

					return false;
				}
			}
			else
			{
				$this->setError(\JText::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT'));

				return false;
			}
		}

		// Clean the cache
		$this->cleanCache();

		return true;
	}

	/**
	 * Batch move items to a new category
	 *
	 * @param   integer  $value     The new category ID.
	 * @param   array    $pks       An array of row IDs.
	 * @param   array    $contexts  An array of item contexts.
	 *
	 * @return  boolean  True if successful, false otherwise and internal error is set.
	 *
	 * @since	1.7
	 */
	protected function batchMove($value, $pks, $contexts)
	{
		// Initialize re-usable member properties, and re-usable local variables
		$this->initBatch();

		$categoryId = (int) $value;

		if (!$this->checkCategoryId($categoryId))
		{
			return false;
		}

		// Parent exists so we proceed
		foreach ($pks as $pk)
		{
			if (!$this->user->authorise('core.edit', $contexts[$pk]))
			{
				$this->setError(\JText::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT'));

				return false;
			}

			// Check that the row actually exists
			if (!$this->table->load($pk))
			{
				if ($error = $this->table->getError())
				{
					// Fatal error
					$this->setError($error);

					return false;
				}
				else
				{
					// Not fatal error
					$this->setError(\JText::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk));
					continue;
				}
			}

			// Set the new category ID
			$this->table->catid = $categoryId;

			// Check the row.
			if (!$this->table->check())
			{
				$this->setError($this->table->getError());

				return false;
			}

			if (!empty($this->type))
			{
				$this->createTagsHelper($this->tagsObserver, $this->type, $pk, $this->typeAlias, $this->table);
			}

			// Store the row.
			if (!$this->table->store())
			{
				$this->setError($this->table->getError());

				return false;
			}
		}

		// Clean the cache
		$this->cleanCache();

		return true;
	}

	/**
	 * Batch tag a list of item.
	 *
	 * @param   integer  $value     The value of the new tag.
	 * @param   array    $pks       An array of row IDs.
	 * @param   array    $contexts  An array of item contexts.
	 *
	 * @return  boolean  True if successful, false otherwise and internal error is set.
	 *
	 * @since   3.1
	 */
	protected function batchTag($value, $pks, $contexts)
	{
		// Initialize re-usable member properties, and re-usable local variables
		$this->initBatch();
		$tags = array($value);

		foreach ($pks as $pk)
		{
			if ($this->user->authorise('core.edit', $contexts[$pk]))
			{
				$this->table->reset();
				$this->table->load($pk);

				// Add new tags, keeping existing ones
				$result = $this->tagsObserver->setNewTags($tags, false);

				if (!$result)
				{
					$this->setError($this->table->getError());

					return false;
				}
			}
			else
			{
				$this->setError(\JText::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT'));

				return false;
			}
		}

		// Clean the cache
		$this->cleanCache();

		return true;
	}

	/**
	 * Method to test whether a record can be deleted.
	 *
	 * @param   object  $record  A record object.
	 *
	 * @return  boolean  True if allowed to delete the record. Defaults to the permission for the component.
	 *
	 * @since   1.6
	 */
	protected function canDelete($record)
	{
		return \JFactory::getUser()->authorise('core.delete', $this->option);
	}

	/**
	 * Method to test whether a record can have its state changed.
	 *
	 * @param   object  $record  A record object.
	 *
	 * @return  boolean  True if allowed to change the state of the record. Defaults to the permission for the component.
	 *
	 * @since   1.6
	 */
	protected function canEditState($record)
	{
		return \JFactory::getUser()->authorise('core.edit.state', $this->option);
	}

	/**
	 * Method override to check-in a record or an array of record
	 *
	 * @param   mixed  $pks  The ID of the primary key or an array of IDs
	 *
	 * @return  integer|boolean  Boolean false if there is an error, otherwise the count of records checked in.
	 *
	 * @since   1.6
	 */
	public function checkin($pks = array())
	{
		$pks = (array) $pks;
		$table = $this->getTable();
		$count = 0;

		if (empty($pks))
		{
			$pks = array((int) $this->getState($this->getName() . '.id'));
		}

		$checkedOutField = $table->getColumnAlias('checked_out');

		// Check in all items.
		foreach ($pks as $pk)
		{
			if ($table->load($pk))
			{
				if ($table->{$checkedOutField} > 0)
				{
					if (!parent::checkin($pk))
					{
						return false;
					}

					$count++;
				}
			}
			else
			{
				$this->setError($table->getError());

				return false;
			}
		}

		return $count;
	}

	/**
	 * Method override to check-out a record.
	 *
	 * @param   integer  $pk  The ID of the primary key.
	 *
	 * @return  boolean  True if successful, false if an error occurs.
	 *
	 * @since   1.6
	 */
	public function checkout($pk = null)
	{
		$pk = (!empty($pk)) ? $pk : (int) $this->getState($this->getName() . '.id');

		return parent::checkout($pk);
	}

	/**
	 * Method to delete one or more records.
	 *
	 * @param   array  &$pks  An array of record primary keys.
	 *
	 * @return  boolean  True if successful, false if an error occurs.
	 *
	 * @since   1.6
	 */
	public function delete(&$pks)
	{
		$dispatcher = \JEventDispatcher::getInstance();
		$pks = (array) $pks;
		$table = $this->getTable();

		// Include the plugins for the delete events.
		\JPluginHelper::importPlugin($this->events_map['delete']);

		// Iterate the items to delete each one.
		foreach ($pks as $i => $pk)
		{
			if ($table->load($pk))
			{
				if ($this->canDelete($table))
				{
					$context = $this->option . '.' . $this->name;

					// Trigger the before delete event.
					$result = $dispatcher->trigger($this->event_before_delete, array($context, $table));

					if (in_array(false, $result, true))
					{
						$this->setError($table->getError());

						return false;
					}

					// Multilanguage: if associated, delete the item in the _associations table
					if ($this->associationsContext && \JLanguageAssociations::isEnabled())
					{
						$db = $this->getDbo();
						$query = $db->getQuery(true)
							->select('COUNT(*) as count, ' . $db->quoteName('as1.key'))
							->from($db->quoteName('#__associations') . ' AS as1')
							->join('LEFT', $db->quoteName('#__associations') . ' AS as2 ON ' . $db->quoteName('as1.key') . ' =  ' . $db->quoteName('as2.key'))
							->where($db->quoteName('as1.context') . ' = ' . $db->quote($this->associationsContext))
							->where($db->quoteName('as1.id') . ' = ' . (int) $pk)
							->group($db->quoteName('as1.key'));

						$db->setQuery($query);
						$row = $db->loadAssoc();

						if (!empty($row['count']))
						{
							$query = $db->getQuery(true)
								->delete($db->quoteName('#__associations'))
								->where($db->quoteName('context') . ' = ' . $db->quote($this->associationsContext))
								->where($db->quoteName('key') . ' = ' . $db->quote($row['key']));

							if ($row['count'] > 2)
							{
								$query->where($db->quoteName('id') . ' = ' . (int) $pk);
							}

							$db->setQuery($query);
							$db->execute();
						}
					}

					if (!$table->delete($pk))
					{
						$this->setError($table->getError());

						return false;
					}

					// Trigger the after event.
					$dispatcher->trigger($this->event_after_delete, array($context, $table));
				}
				else
				{
					// Prune items that you can't change.
					unset($pks[$i]);
					$error = $this->getError();

					if ($error)
					{
						\JLog::add($error, \JLog::WARNING, 'jerror');

						return false;
					}
					else
					{
						\JLog::add(\JText::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), \JLog::WARNING, 'jerror');

						return false;
					}
				}
			}
			else
			{
				$this->setError($table->getError());

				return false;
			}
		}

		// Clear the component's cache
		$this->cleanCache();

		return true;
	}

	/**
	 * Method to change the title & alias.
	 *
	 * @param   integer  $categoryId  The id of the category.
	 * @param   string   $alias       The alias.
	 * @param   string   $title       The title.
	 *
	 * @return	array  Contains the modified title and alias.
	 *
	 * @since	1.7
	 */
	protected function generateNewTitle($categoryId, $alias, $title)
	{
		// Alter the title & alias
		$table      = $this->getTable();
		$aliasField = $table->getColumnAlias('alias');
		$catidField = $table->getColumnAlias('catid');
		$titleField = $table->getColumnAlias('title');

		while ($table->load(array($aliasField => $alias, $catidField => $categoryId)))
		{
			if ($title === $table->$titleField)
			{
				$title = StringHelper::increment($title);
			}

			$alias = StringHelper::increment($alias, 'dash');
		}

		return array($title, $alias);
	}

	/**
	 * Method to get a single record.
	 *
	 * @param   integer  $pk  The id of the primary key.
	 *
	 * @return  \JObject|boolean  Object on success, false on failure.
	 *
	 * @since   1.6
	 */
	public function getItem($pk = null)
	{
		$pk = (!empty($pk)) ? $pk : (int) $this->getState($this->getName() . '.id');
		$table = $this->getTable();

		if ($pk > 0)
		{
			// Attempt to load the row.
			$return = $table->load($pk);

			// Check for a table object error.
			if ($return === false && $table->getError())
			{
				$this->setError($table->getError());

				return false;
			}
		}

		// Convert to the \JObject before adding other data.
		$properties = $table->getProperties(1);
		$item = ArrayHelper::toObject($properties, '\JObject');

		if (property_exists($item, 'params'))
		{
			$registry = new Registry($item->params);
			$item->params = $registry->toArray();
		}

		return $item;
	}

	/**
	 * A protected method to get a set of ordering conditions.
	 *
	 * @param   \JTable  $table  A \JTable object.
	 *
	 * @return  array  An array of conditions to add to ordering queries.
	 *
	 * @since   1.6
	 */
	protected function getReorderConditions($table)
	{
		return array();
	}

	/**
	 * Stock method to auto-populate the model state.
	 *
	 * @return  void
	 *
	 * @since   1.6
	 */
	protected function populateState()
	{
		$table = $this->getTable();
		$key = $table->getKeyName();

		// Get the pk of the record from the request.
		$pk = \JFactory::getApplication()->input->getInt($key);
		$this->setState($this->getName() . '.id', $pk);

		// Load the parameters.
		$value = \JComponentHelper::getParams($this->option);
		$this->setState('params', $value);
	}

	/**
	 * Prepare and sanitise the table data prior to saving.
	 *
	 * @param   \JTable  $table  A reference to a \JTable object.
	 *
	 * @return  void
	 *
	 * @since   1.6
	 */
	protected function prepareTable($table)
	{
		// Derived class will provide its own implementation if required.
	}

	/**
	 * Method to change the published state of one or more records.
	 *
	 * @param   array    &$pks   A list of the primary keys to change.
	 * @param   integer  $value  The value of the published state.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.6
	 */
	public function publish(&$pks, $value = 1)
	{
		$dispatcher = \JEventDispatcher::getInstance();
		$user = \JFactory::getUser();
		$table = $this->getTable();
		$pks = (array) $pks;

		// Include the plugins for the change of state event.
		\JPluginHelper::importPlugin($this->events_map['change_state']);

		// Access checks.
		foreach ($pks as $i => $pk)
		{
			$table->reset();

			if ($table->load($pk))
			{
				if (!$this->canEditState($table))
				{
					// Prune items that you can't change.
					unset($pks[$i]);

					\JLog::add(\JText::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), \JLog::WARNING, 'jerror');

					return false;
				}

				// If the table is checked out by another user, drop it and report to the user trying to change its state.
				if (property_exists($table, 'checked_out') && $table->checked_out && ($table->checked_out != $user->id))
				{
					\JLog::add(\JText::_('JLIB_APPLICATION_ERROR_CHECKIN_USER_MISMATCH'), \JLog::WARNING, 'jerror');

					// Prune items that you can't change.
					unset($pks[$i]);

					return false;
				}

				/**
				 * Prune items that are already at the given state.  Note: Only models whose table correctly
				 * sets 'published' column alias (if different than published) will benefit from this
				 */
				$publishedColumnName = $table->getColumnAlias('published');

				if (property_exists($table, $publishedColumnName) && $table->get($publishedColumnName, $value) == $value)
				{
					unset($pks[$i]);

					continue;
				}
			}
		}

		// Check if there are items to change
		if (!count($pks))
		{
			return true;
		}

		// Attempt to change the state of the records.
		if (!$table->publish($pks, $value, $user->get('id')))
		{
			$this->setError($table->getError());

			return false;
		}

		$context = $this->option . '.' . $this->name;

		// Trigger the change state event.
		$result = $dispatcher->trigger($this->event_change_state, array($context, $pks, $value));

		if (in_array(false, $result, true))
		{
			$this->setError($table->getError());

			return false;
		}

		// Clear the component's cache
		$this->cleanCache();

		return true;
	}

	/**
	 * Method to adjust the ordering of a row.
	 *
	 * Returns NULL if the user did not have edit
	 * privileges for any of the selected primary keys.
	 *
	 * @param   integer  $pks    The ID of the primary key to move.
	 * @param   integer  $delta  Increment, usually +1 or -1
	 *
	 * @return  boolean|null  False on failure or error, true on success, null if the $pk is empty (no items selected).
	 *
	 * @since   1.6
	 */
	public function reorder($pks, $delta = 0)
	{
		$table = $this->getTable();
		$pks = (array) $pks;
		$result = true;

		$allowed = true;

		foreach ($pks as $i => $pk)
		{
			$table->reset();

			if ($table->load($pk) && $this->checkout($pk))
			{
				// Access checks.
				if (!$this->canEditState($table))
				{
					// Prune items that you can't change.
					unset($pks[$i]);
					$this->checkin($pk);
					\JLog::add(\JText::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), \JLog::WARNING, 'jerror');
					$allowed = false;
					continue;
				}

				$where = $this->getReorderConditions($table);

				if (!$table->move($delta, $where))
				{
					$this->setError($table->getError());
					unset($pks[$i]);
					$result = false;
				}

				$this->checkin($pk);
			}
			else
			{
				$this->setError($table->getError());
				unset($pks[$i]);
				$result = false;
			}
		}

		if ($allowed === false && empty($pks))
		{
			$result = null;
		}

		// Clear the component's cache
		if ($result == true)
		{
			$this->cleanCache();
		}

		return $result;
	}

	/**
	 * Method to save the form data.
	 *
	 * @param   array  $data  The form data.
	 *
	 * @return  boolean  True on success, False on error.
	 *
	 * @since   1.6
	 */
	public function save($data)
	{
		$dispatcher = \JEventDispatcher::getInstance();
		$table      = $this->getTable();
		$context    = $this->option . '.' . $this->name;
		$app        = \JFactory::getApplication();

		if (!empty($data['tags']) && $data['tags'][0] != '')
		{
			$table->newTags = $data['tags'];
		}

		$key = $table->getKeyName();
		$pk = (!empty($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id');
		$isNew = true;

		// Include the plugins for the save events.
		\JPluginHelper::importPlugin($this->events_map['save']);

		// Allow an exception to be thrown.
		try
		{
			// Load the row if saving an existing record.
			if ($pk > 0)
			{
				$table->load($pk);
				$isNew = false;
			}

			// Bind the data.
			if (!$table->bind($data))
			{
				$this->setError($table->getError());

				return false;
			}

			// Prepare the row for saving
			$this->prepareTable($table);

			// Check the data.
			if (!$table->check())
			{
				$this->setError($table->getError());

				return false;
			}

			// Trigger the before save event.
			$result = $dispatcher->trigger($this->event_before_save, array($context, $table, $isNew, $data));

			if (in_array(false, $result, true))
			{
				$this->setError($table->getError());

				return false;
			}

			// Store the data.
			if (!$table->store())
			{
				$this->setError($table->getError());

				return false;
			}

			// Clean the cache.
			$this->cleanCache();

			// Trigger the after save event.
			$dispatcher->trigger($this->event_after_save, array($context, $table, $isNew, $data));
		}
		catch (\Exception $e)
		{
			$this->setError($e->getMessage());

			return false;
		}

		if (isset($table->$key))
		{
			$this->setState($this->getName() . '.id', $table->$key);
		}

		$this->setState($this->getName() . '.new', $isNew);

		if ($this->associationsContext && \JLanguageAssociations::isEnabled() && !empty($data['associations']))
		{
			$associations = $data['associations'];

			// Unset any invalid associations
			$associations = ArrayHelper::toInteger($associations);

			// Unset any invalid associations
			foreach ($associations as $tag => $id)
			{
				if (!$id)
				{
					unset($associations[$tag]);
				}
			}

			// Show a warning if the item isn't assigned to a language but we have associations.
			if ($associations && $table->language === '*')
			{
				$app->enqueueMessage(
					\JText::_(strtoupper($this->option) . '_ERROR_ALL_LANGUAGE_ASSOCIATED'),
					'warning'
				);
			}

			// Get associationskey for edited item
			$db    = $this->getDbo();
			$query = $db->getQuery(true)
				->select($db->qn('key'))
				->from($db->qn('#__associations'))
				->where($db->qn('context') . ' = ' . $db->quote($this->associationsContext))
				->where($db->qn('id') . ' = ' . (int) $table->$key);
			$db->setQuery($query);
			$old_key = $db->loadResult();

			// Deleting old associations for the associated items
			$query = $db->getQuery(true)
				->delete($db->qn('#__associations'))
				->where($db->qn('context') . ' = ' . $db->quote($this->associationsContext));

			if ($associations)
			{
				$query->where('(' . $db->qn('id') . ' IN (' . implode(',', $associations) . ') OR '
					. $db->qn('key') . ' = ' . $db->q($old_key) . ')');
			}
			else
			{
				$query->where($db->qn('key') . ' = ' . $db->q($old_key));
			}

			$db->setQuery($query);
			$db->execute();

			// Adding self to the association
			if ($table->language !== '*')
			{
				$associations[$table->language] = (int) $table->$key;
			}

			if (count($associations) > 1)
			{
				// Adding new association for these items
				$key   = md5(json_encode($associations));
				$query = $db->getQuery(true)
					->insert('#__associations');

				foreach ($associations as $id)
				{
					$query->values(((int) $id) . ',' . $db->quote($this->associationsContext) . ',' . $db->quote($key));
				}

				$db->setQuery($query);
				$db->execute();
			}
		}

		if ($app->input->get('task') == 'editAssociations')
		{
			return $this->redirectToAssociations($data);
		}

		return true;
	}

	/**
	 * Saves the manually set order of records.
	 *
	 * @param   array    $pks    An array of primary key ids.
	 * @param   integer  $order  +1 or -1
	 *
	 * @return  boolean|\JException  Boolean true on success, false on failure, or \JException if no items are selected
	 *
	 * @since   1.6
	 */
	public function saveorder($pks = array(), $order = null)
	{
		// Initialize re-usable member properties
		$this->initBatch();

		$conditions = array();

		if (empty($pks))
		{
			return \JError::raiseWarning(500, \JText::_($this->text_prefix . '_ERROR_NO_ITEMS_SELECTED'));
		}

		$orderingField = $this->table->getColumnAlias('ordering');

		// Update ordering values
		foreach ($pks as $i => $pk)
		{
			$this->table->load((int) $pk);

			// Access checks.
			if (!$this->canEditState($this->table))
			{
				// Prune items that you can't change.
				unset($pks[$i]);
				\JLog::add(\JText::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), \JLog::WARNING, 'jerror');
			}
			elseif ($this->table->$orderingField != $order[$i])
			{
				$this->table->$orderingField = $order[$i];

				if ($this->type)
				{
					$this->createTagsHelper($this->tagsObserver, $this->type, $pk, $this->typeAlias, $this->table);
				}

				if (!$this->table->store())
				{
					$this->setError($this->table->getError());

					return false;
				}

				// Remember to reorder within position and client_id
				$condition = $this->getReorderConditions($this->table);
				$found = false;

				foreach ($conditions as $cond)
				{
					if ($cond[1] == $condition)
					{
						$found = true;
						break;
					}
				}

				if (!$found)
				{
					$key = $this->table->getKeyName();
					$conditions[] = array($this->table->$key, $condition);
				}
			}
		}

		// Execute reorder for each category.
		foreach ($conditions as $cond)
		{
			$this->table->load($cond[0]);
			$this->table->reorder($cond[1]);
		}

		// Clear the component's cache
		$this->cleanCache();

		return true;
	}

	/**
	 * Method to create a tags helper to ensure proper management of tags
	 *
	 * @param   \JTableObserverTags  $tagsObserver  The tags observer for this table
	 * @param   \JUcmType            $type          The type for the table being processed
	 * @param   integer              $pk            Primary key of the item bing processed
	 * @param   string               $typeAlias     The type alias for this table
	 * @param   \JTable              $table         The \JTable object
	 *
	 * @return  void
	 *
	 * @since   3.2
	 */
	public function createTagsHelper($tagsObserver, $type, $pk, $typeAlias, $table)
	{
		if (!empty($tagsObserver) && !empty($type))
		{
			$table->tagsHelper = new \JHelperTags;
			$table->tagsHelper->typeAlias = $typeAlias;
			$table->tagsHelper->tags = explode(',', $table->tagsHelper->getTagIds($pk, $typeAlias));
		}
	}

	/**
	 * Method to check the validity of the category ID for batch copy and move
	 *
	 * @param   integer  $categoryId  The category ID to check
	 *
	 * @return  boolean
	 *
	 * @since   3.2
	 */
	protected function checkCategoryId($categoryId)
	{
		// Check that the category exists
		if ($categoryId)
		{
			$categoryTable = \JTable::getInstance('Category');

			if (!$categoryTable->load($categoryId))
			{
				if ($error = $categoryTable->getError())
				{
					// Fatal error
					$this->setError($error);

					return false;
				}
				else
				{
					$this->setError(\JText::_('JLIB_APPLICATION_ERROR_BATCH_MOVE_CATEGORY_NOT_FOUND'));

					return false;
				}
			}
		}

		if (empty($categoryId))
		{
			$this->setError(\JText::_('JLIB_APPLICATION_ERROR_BATCH_MOVE_CATEGORY_NOT_FOUND'));

			return false;
		}

		// Check that the user has create permission for the component
		$extension = \JFactory::getApplication()->input->get('option', '');
		$user = \JFactory::getUser();

		if (!$user->authorise('core.create', $extension . '.category.' . $categoryId))
		{
			$this->setError(\JText::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE'));

			return false;
		}

		return true;
	}

	/**
	 * A method to preprocess generating a new title in order to allow tables with alternative names
	 * for alias and title to use the batch move and copy methods
	 *
	 * @param   integer  $categoryId  The target category id
	 * @param   \JTable  $table       The \JTable within which move or copy is taking place
	 *
	 * @return  void
	 *
	 * @since   3.2
	 */
	public function generateTitle($categoryId, $table)
	{
		// Alter the title & alias
		$titleField         = $table->getColumnAlias('title');
		$aliasField         = $table->getColumnAlias('alias');
		$data               = $this->generateNewTitle($categoryId, $table->$aliasField, $table->$titleField);
		$table->$titleField = $data['0'];
		$table->$aliasField = $data['1'];
	}

	/**
	 * Method to initialize member variables used by batch methods and other methods like saveorder()
	 *
	 * @return  void
	 *
	 * @since   3.8.2
	 */
	public function initBatch()
	{
		if ($this->batchSet === null)
		{
			$this->batchSet = true;

			// Get current user
			$this->user = \JFactory::getUser();

			// Get table
			$this->table = $this->getTable();

			// Get table class name
			$tc = explode('\\', get_class($this->table));
			$this->tableClassName = end($tc);

			// Get UCM Type data
			$this->contentType = new \JUcmType;
			$this->type = $this->contentType->getTypeByTable($this->tableClassName)
				?: $this->contentType->getTypeByAlias($this->typeAlias);

			// Get tabs observer
			$this->tagsObserver = $this->table->getObserverOfClass('Joomla\CMS\Table\Observer\Tags');
		}
	}

	/**
	 * Method to load an item in com_associations.
	 *
	 * @param   array  $data  The form data.
	 *
	 * @return  boolean  True if successful, false otherwise.
	 *
	 * @since   3.9.0
	 *
	 * @deprecated 5.0  It is handled by regular save method now.
	 */
	public function editAssociations($data)
	{
		// Save the item
		$this->save($data);
	}

	/**
	 * Method to load an item in com_associations.
	 *
	 * @param   array  $data  The form data.
	 *
	 * @return  boolean  True if successful, false otherwise.
	 *
	 * @throws \Exception
	 * @since   3.9.17
	 */
	protected function redirectToAssociations($data)
	{
		$app = Factory::getApplication();
		$id  = $data['id'];

		// Deal with categories associations
		if ($this->text_prefix === 'COM_CATEGORIES')
		{
			$extension       = $app->input->get('extension', 'com_content');
			$this->typeAlias = $extension . '.category';
			$component       = strtolower($this->text_prefix);
			$view            = 'category';
		}
		else
		{
			$aliasArray = explode('.', $this->typeAlias);
			$component  = $aliasArray[0];
			$view       = $aliasArray[1];
			$extension  = '';
		}

		// Menu item redirect needs admin client
		$client = $component === 'com_menus' ? '&client_id=0' : '';

		if ($id == 0)
		{
			$app->enqueueMessage(\JText::_('JGLOBAL_ASSOCIATIONS_NEW_ITEM_WARNING'), 'error');
			$app->redirect(
				\JRoute::_('index.php?option=' . $component . '&view=' . $view . $client . '&layout=edit&id=' . $id . $extension, false)
			);

			return false;
		}

		if ($data['language'] === '*')
		{
			$app->enqueueMessage(\JText::_('JGLOBAL_ASSOC_NOT_POSSIBLE'), 'notice');
			$app->redirect(
				\JRoute::_('index.php?option=' . $component . '&view=' . $view . $client . '&layout=edit&id=' . $id . $extension, false)
			);

			return false;
		}

		$languages = LanguageHelper::getContentLanguages(array(0, 1));
		$target    = '';

		/* If the site contains only 2 languages and an association exists for the item
		   load directly the associated target item in the side by side view
		   otherwise select already the target language
		*/
		if (count($languages) === 2)
		{
			foreach ($languages as $language)
			{
				$lang_code[] = $language->lang_code;
			}

			$refLang    = array($data['language']);
			$targetLang = array_diff($lang_code, $refLang);
			$targetLang = implode(',', $targetLang);
			$targetId   = $data['associations'][$targetLang];

			if ($targetId)
			{
				$target = '&target=' . $targetLang . '%3A' . $targetId . '%3Aedit';
			}
			else
			{
				$target = '&target=' . $targetLang . '%3A0%3Aadd';
			}
		}

		$app->redirect(
			\JRoute::_(
				'index.php?option=com_associations&view=association&layout=edit&itemtype=' . $this->typeAlias
				. '&task=association.edit&id=' . $id . $target, false
			)
		);

		return true;
	}
}
Builder.php000064400000004575152415775660006700 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Factory\Scaffolding\Model;

use FOF30\Container\Container;
use FOF30\Factory\Magic\ModelFactory;

defined('_JEXEC') or die;

/**
 * Scaffolding Builder
 *
 * @package FOF30\Factory\Scaffolding
 *
 * @deprecated 3.1  Support for XML forms will be removed in FOF 4
 */
class Builder
{
	/** @var  \FOF30\Container\Container  The container we belong to */
	protected $container = null;

    /**
     * Section used to build the namespace prefix. We have to pass it since in CLI scaffolding we need
     * to force the section we're in (ie Site or Admin). {@see \FOF30\Container\Container::getNamespacePrefix() } for valid values
     *
     * @var   string
     */
    protected $section = 'auto';

	/**
	 * Create the scaffolding builder instance
	 *
	 * @param \FOF30\Container\Container $c
	 */
	public function __construct(Container $c)
	{
		$this->container = $c;
	}

	/**
	 * Make a new scaffolding document
	 *
	 * @param   string  $requestedClass     The requested class, with full qualifier ie Myapp\Site\Controller\Foobar
	 * @param   string  $viewName           The name of the view linked to this controller
	 *
	 * @return  bool    True on success, false otherwise
	 */
	public function make($requestedClass, $viewName)
	{
        // Class already exists? Stop here
		if (class_exists($requestedClass))
        {
            return true;
        }

        // I have to magically create the model class
        $magic    = new ModelFactory($this->container);
        $magic->setSection($this->getSection());
        $fofModel = $magic->make($viewName);

		/** @var ErectorInterface $erector */
        $erector = new ModelErector($this, $fofModel, $viewName);
        $erector->setSection($this->getSection());
		$erector->build();

        if(!class_exists($requestedClass))
        {
            return false;
        }

        return true;
	}

	/**
	 * Gets the container this builder belongs to
	 *
	 * @return Container
	 */
	public function getContainer()
	{
		return $this->container;
	}

    /**
     * @return string
     */
    public function getSection()
    {
        return $this->section;
    }

    /**
     * @param string $section
     */
    public function setSection($section)
    {
        $this->section = $section;
    }
}
ErectorInterface.php000064400000002100152415775670010515 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Factory\Scaffolding\Model;

use FOF30\Model\DataModel;

defined('_JEXEC') or die;

/**
 * Interface ErectorInterface
 * @package FOF30\Factory\Scaffolding\Model
 * @deprecated 3.1  Support for XML forms will be removed in FOF 4
 */
interface ErectorInterface
{
	/**
	 * Construct the erector object
	 *
	 * @param   Builder     $parent         The parent builder
	 * @param   DataModel   $model          The model we're erecting a scaffold against
	 * @param   string      $viewName       The view name for this controller
	 */
	public function __construct(Builder $parent, DataModel $model, $viewName);

	/**
	 * Erects a scaffold. It then uses the parent's methods to assign the erected scaffold.
	 *
	 * @return  void
	 */
	public function build();

    /**
     * @return string
     */
    public function getSection();

    /**
     * @param string $section
     */
    public function setSection($section);
}
ModelErector.php000064400000006130152415775720007660 0ustar00<?php
/**
 * @package     FOF
 * @copyright   Copyright (c)2010-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Factory\Scaffolding\Model;

use FOF30\Model\DataModel;
use FOF30\Utils\ModelTypeHints;

defined('_JEXEC') or die;

/**
 * @package FOF30\Factory\Scaffolding
 *
 * @deprecated 3.1  Support for XML forms will be removed in FOF 4
 */
class ModelErector implements ErectorInterface
{
    /**
     * The Builder which called us
     *
     * @var \FOF30\Factory\Scaffolding\Controller\Builder
     */
    protected $builder = null;

    /**
     * The Model attached to the view we're building
     *
     * @var \FOF30\Controller\DataController
     */
    protected $model = null;

    /**
     * The name of our view
     *
     * @var string
     */
    protected $viewName = null;

    /**
     * Section used to build the namespace prefix. We have to pass it since in CLI scaffolding we need
     * to force the section we're in (ie Site or Admin). {@see \FOF30\Container\Container::getNamespacePrefix() } for valid values
     *
     * @var   string
     */
    protected $section = 'auto';

    public function __construct(Builder $parent, DataModel $model, $viewName)
    {
        $this->builder  = $parent;
        $this->model    = $model;
        $this->viewName = $viewName;
    }

	public function build()
	{
        $container = $this->builder->getContainer();
        $fullPath  = $container->getNamespacePrefix($this->getSection()) . 'Model\\' . ucfirst($container->inflector->pluralize($this->viewName));

        // Let's remove the last part and use it to create the class name
        $parts     = explode('\\', trim($fullPath, '\\'));
        $className = array_pop($parts);
        // Now glue everything together
        $namespace = implode('\\', $parts);
        // Let's be sure that the parent class extends with a backslash
        $baseClass = '\\'.trim(get_class($this->model), '\\');

        $code  = '<?php'.PHP_EOL;
        $code .= PHP_EOL;
        $code .= 'namespace '.$namespace.';'.PHP_EOL;
        $code .= PHP_EOL;
        $code .= "defined('_JEXEC') or die;".PHP_EOL;
        $code .= PHP_EOL;

        // Let's create some type-hints for the model class
        $typeHints = new ModelTypeHints($this->model);
        $typeHints->setClassName($fullPath);
        $docBlock  = $typeHints->getHints();

        $code .= $docBlock;
        $code .= 'class '.$className.' extends '.$baseClass.PHP_EOL;
        $code .= '{'.PHP_EOL;
        $code .= PHP_EOL;
        $code .= '}'.PHP_EOL;

        $path = $container->backEndPath;

        if(in_array('Site', $parts))
        {
            $path = $container->frontEndPath;
        }

        $path .= '/Model/'.$className.'.php';

        $filesystem = $container->filesystem;

        $filesystem->fileWrite($path, $code);

        return $path;
	}

    /**
     * @return string
     */
    public function getSection()
    {
        return $this->section;
    }

    /**
     * @param string $section
     */
    public function setSection($section)
    {
        $this->section = $section;
    }
}
AbstractModel.php000064400000000645152422747150010017 0ustar00<?php


namespace Nextend\Framework\Model;


use Nextend\Framework\Pattern\MVCHelperTrait;

abstract class AbstractModel {

    use MVCHelperTrait;

    /**
     * AbstractModel constructor.
     *
     * @param MVCHelperTrait $helper
     */
    public function __construct($helper) {

        $this->setMVCHelper($helper);
        $this->init();
    }

    protected function init() {

    }
}AbstractModelTable.php000064400000001102152422747220010752 0ustar00<?php

namespace Nextend\Framework\Model;


use Nextend\Framework\Database\AbstractPlatformConnectorTable;

abstract class AbstractModelTable extends AbstractModel {

    /**
     * @var AbstractPlatformConnectorTable
     */
    protected $table;

    protected function init() {

        $this->table = $this->createConnectorTable();
    }

    /**
     * @return AbstractPlatformConnectorTable
     */
    protected abstract function createConnectorTable();

    public function getTableName() {
        return $this->table->getTableName();
    }
}ApplicationSection.php000064400000004721152422747270011065 0ustar00<?php

namespace Nextend\Framework\Model;

class ApplicationSection {

    private $application = 'system';

    /**
     * Quick cache implementation to prevent duplicate queries. It might have bugs.
     *
     * @var array
     */
    protected $cache = array();

    public function __construct($application) {
        $this->application = $application;
    }

    public function getById($id, $section) {
        return Section::getById($id, $section);
    }

    public function setById($id, $value) {
        $this->cache = array();

        return Section::setById($id, $value);
    }

    public function get($section, $referenceKey = null, $default = null) {

        if (isset($this->cache[$section . '///' . $referenceKey])) {
            return $this->cache[$section . '///' . $referenceKey];
        }

        $attributes = array(
            "application" => $this->application,
            "section"     => $section
        );

        if ($referenceKey !== null) {
            $attributes['referencekey'] = $referenceKey;
        }
        $result = Section::$tableSectionStorage->findByAttributes($attributes);
        if (is_array($result)) {
            $this->cache[$section . '///' . $referenceKey] = $result['value'];

            return $result['value'];
        }

        return $default;
    }

    public function getAll($section, $referenceKey = null) {
        return Section::getAll($this->application, $section, $referenceKey);
    }

    public function set($section, $referenceKey, $value) {
        if (isset($this->cache[$section . '///' . $referenceKey])) {
            unset($this->cache[$section . '///' . $referenceKey]);
        }

        Section::set($this->application, $section, $referenceKey, $value);
    }

    public function add($section, $referenceKey, $value) {
        if (isset($this->cache[$section . '///' . $referenceKey])) {
            unset($this->cache[$section . '///' . $referenceKey]);
        }

        return Section::add($this->application, $section, $referenceKey, $value);
    }

    public function delete($section, $referenceKey = null) {
        if (isset($this->cache[$section . '///' . $referenceKey])) {
            unset($this->cache[$section . '///' . $referenceKey]);
        }

        return Section::delete($this->application, $section, $referenceKey);
    }

    public function deleteById($id) {
        return Section::deleteById($id);
    }
}Section.php000064400000011462152422747340006677 0ustar00<?php

namespace Nextend\Framework\Model;

use Nextend\Framework\Database\AbstractPlatformConnectorTable;
use Nextend\Framework\Database\Database;
use Nextend\Framework\Plugin;

class Section {

    /** @var AbstractPlatformConnectorTable */
    public static $tableSectionStorage;

    public function __construct() {

        self::$tableSectionStorage = Database::getTable("nextend2_section_storage");
    }

    public static function get($application, $section, $referenceKey = null) {
        $attributes = array(
            "application" => $application,
            "section"     => $section
        );

        if ($referenceKey !== null) {
            $attributes['referencekey'] = $referenceKey;
        }

        return self::$tableSectionStorage->findByAttributes($attributes);
    }

    public static function getById($id, $section = null) {
        static $cache = array();
        if ($id === 0) {
            return null;
        }
        if (!isset($cache[$section])) {
            $cache[$section] = array();
        } else if (isset($cache[$section][$id])) {
            return $cache[$section][$id];
        }

        $cache[$section][$id] = null;
        if ($section) {
            Plugin::doAction($section, array(
                $id,
                &$cache[$section][$id]
            ));
            if ($cache[$section][$id]) {
                return $cache[$section][$id];
            }
        }

        $cache[$section][$id] = self::$tableSectionStorage->findByAttributes(array(
            "id" => $id
        ));
        if ($section && $cache[$section][$id]['section'] != $section) {
            $cache[$section][$id] = null;

            return $cache[$section][$id];
        }

        return $cache[$section][$id];
    }

    public static function getAll($application, $section, $referenceKey = null) {
        $attributes = array(
            "application" => $application,
            "section"     => $section
        );

        if ($referenceKey !== null) {
            $attributes['referencekey'] = $referenceKey;
        }

        $rows = self::$tableSectionStorage->findAllByAttributes($attributes, array(
            "id",
            "referencekey",
            "value",
            "isSystem",
            "editable"
        ));

        Plugin::doAction($application . $section, array(
            $referenceKey,
            &$rows
        ));

        return $rows;
    }

    public static function add($application, $section, $referenceKey, $value, $isSystem = 0, $editable = 1) {
        $row = array(
            "application"  => $application,
            "section"      => $section,
            "referencekey" => '',
            "value"        => $value,
            "isSystem"     => $isSystem,
            "editable"     => $editable
        );

        if ($referenceKey !== null) {
            $row["referencekey"] = $referenceKey;
        }

        self::$tableSectionStorage->insert($row);

        return self::$tableSectionStorage->insertId();
    }


    public static function set($application, $section, $referenceKey, $value, $isSystem = 0, $editable = 1) {

        $result = self::getAll($application, $section, $referenceKey);

        if (empty($result)) {
            return self::add($application, $section, $referenceKey, $value, $isSystem, $editable);
        } else {
            $attributes = array(
                "application" => $application,
                "section"     => $section
            );

            if ($referenceKey !== null) {
                $attributes['referencekey'] = $referenceKey;
            }
            self::$tableSectionStorage->update(array('value' => $value), $attributes);

            return true;
        }
    }

    public static function setById($id, $value) {

        $result = self::getById($id);

        if ($result !== null && $result['editable']) {
            self::$tableSectionStorage->update(array('value' => $value), array(
                "id" => $id
            ));

            return true;
        }

        return false;
    }

    public static function delete($application, $section, $referenceKey = null) {

        $attributes = array(
            "application" => $application,
            "section"     => $section,
            "isSystem"    => 0
        );

        if ($referenceKey !== null) {
            $attributes['referencekey'] = $referenceKey;
        }

        self::$tableSectionStorage->deleteByAttributes($attributes);

        return true;
    }

    public static function deleteById($id) {

        self::$tableSectionStorage->deleteByAttributes(array(
            "id"       => $id,
            "isSystem" => 0
        ));

        return true;
    }
}

new Section();StorageSectionManager.php000064400000000752152422747410011515 0ustar00<?php


namespace Nextend\Framework\Model;


class StorageSectionManager {

    /** @var ApplicationSection[] */
    private static $storageTypes = array();

    /**
     * @param $type
     *
     * @return ApplicationSection
     */
    public static function getStorage($type) {

        if (!isset(self::$storageTypes[$type])) {
            self::$storageTypes[$type] = new ApplicationSection($type);
        }

        return self::$storageTypes[$type];
    }
}