| Current Path : /home/w/u/e/wuectly/www/03cbe/ |
| Current File : /home/w/u/e/wuectly/www/03cbe/table.tar |
behavior.php 0000604 00000014277 15245540356 0007074 0 ustar 00 <?php
/**
* @package FrameworkOnFramework
* @subpackage table
* @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Protect from unauthorized access
defined('F0F_INCLUDED') or die;
/**
* FrameworkOnFramework table behavior class. It defines the events which are
* called by a Table.
*
* @codeCoverageIgnore
* @package FrameworkOnFramework
* @since 2.1
*/
abstract class F0FTableBehavior extends F0FUtilsObservableEvent
{
/**
* This event runs before binding data to the table
*
* @param F0FTable &$table The table which calls this event
* @param array &$data The data to bind
*
* @return boolean True on success
*/
public function onBeforeBind(&$table, &$data)
{
return true;
}
/**
* The event which runs after binding data to the table
*
* @param F0FTable &$table The table which calls this event
* @param object|array &$src The data to bind
*
* @return boolean True on success
*/
public function onAfterBind(&$table, &$src)
{
return true;
}
/**
* The event which runs after loading a record from the database
*
* @param F0FTable &$table The table which calls this event
* @param boolean &$result Did the load succeeded?
*
* @return void
*/
public function onAfterLoad(&$table, &$result)
{
}
/**
* The event which runs before storing (saving) data to the database
*
* @param F0FTable &$table The table which calls this event
* @param boolean $updateNulls Should nulls be saved as nulls (true) or just skipped over (false)?
*
* @return boolean True to allow saving
*/
public function onBeforeStore(&$table, $updateNulls)
{
return true;
}
/**
* The event which runs after storing (saving) data to the database
*
* @param F0FTable &$table The table which calls this event
*
* @return boolean True to allow saving without an error
*/
public function onAfterStore(&$table)
{
return true;
}
/**
* The event which runs before moving a record
*
* @param F0FTable &$table The table which calls this event
* @param boolean $updateNulls Should nulls be saved as nulls (true) or just skipped over (false)?
*
* @return boolean True to allow moving
*/
public function onBeforeMove(&$table, $updateNulls)
{
return true;
}
/**
* The event which runs after moving a record
*
* @param F0FTable &$table The table which calls this event
*
* @return boolean True to allow moving without an error
*/
public function onAfterMove(&$table)
{
return true;
}
/**
* The event which runs before reordering a table
*
* @param F0FTable &$table The table which calls this event
* @param string $where The WHERE clause of the SQL query to run on reordering (record filter)
*
* @return boolean True to allow reordering
*/
public function onBeforeReorder(&$table, $where = '')
{
return true;
}
/**
* The event which runs after reordering a table
*
* @param F0FTable &$table The table which calls this event
*
* @return boolean True to allow the reordering to complete without an error
*/
public function onAfterReorder(&$table)
{
return true;
}
/**
* The event which runs before deleting a record
*
* @param F0FTable &$table The table 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(&$table, $oid)
{
return true;
}
/**
* The event which runs after deleting a record
*
* @param F0FTable &$table The table which calls this event
* @param integer $oid The PK value of the record which was deleted
*
* @return boolean True to allow the deletion without errors
*/
public function onAfterDelete(&$table, $oid)
{
return true;
}
/**
* The event which runs before hitting a record
*
* @param F0FTable &$table The table which calls this event
* @param integer $oid The PK value of the record to hit
* @param boolean $log Should we log the hit?
*
* @return boolean True to allow the hit
*/
public function onBeforeHit(&$table, $oid, $log)
{
return true;
}
/**
* The event which runs after hitting a record
*
* @param F0FTable &$table The table which calls this event
* @param integer $oid The PK value of the record which was hit
*
* @return boolean True to allow the hitting without errors
*/
public function onAfterHit(&$table, $oid)
{
return true;
}
/**
* The even which runs before copying a record
*
* @param F0FTable &$table The table which calls this event
* @param integer $oid The PK value of the record being copied
*
* @return boolean True to allow the copy to take place
*/
public function onBeforeCopy(&$table, $oid)
{
return true;
}
/**
* The even which runs after copying a record
*
* @param F0FTable &$table The table which calls this event
* @param integer $oid The PK value of the record which was copied (not the new one)
*
* @return boolean True to allow the copy without errors
*/
public function onAfterCopy(&$table, $oid)
{
return true;
}
/**
* The event which runs before a record is (un)published
*
* @param F0FTable &$table The table which calls this event
* @param integer|array &$cid The PK IDs of the records being (un)published
* @param integer $publish 1 to publish, 0 to unpublish
*
* @return boolean True to allow the (un)publish to proceed
*/
public function onBeforePublish(&$table, &$cid, $publish)
{
return true;
}
/**
* The event which runs after the object is reset to its default values.
*
* @param F0FTable &$table The table which calls this event
*
* @return boolean True to allow the reset to complete without errors
*/
public function onAfterReset(&$table)
{
return true;
}
/**
* The even which runs before the object is reset to its default values.
*
* @param F0FTable &$table The table which calls this event
*
* @return boolean True to allow the reset to complete
*/
public function onBeforeReset(&$table)
{
return true;
}
}
nested.php 0000604 00000164107 15245540356 0006555 0 ustar 00 <?php
/**
* @package FrameworkOnFramework
* @subpackage table
* @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Protect from unauthorized access
defined('F0F_INCLUDED') or die;
/**
* A class to manage tables holding nested sets (hierarchical data)
*
* @property int $lft Left value (for nested set implementation)
* @property int $rgt Right value (for nested set implementation)
* @property string $hash Slug hash (optional; for faster searching)
* @property string $slug Node's slug (optional)
* @property string $title Title of the node (optional)
*/
class F0FTableNested extends F0FTable
{
/** @var int The level (depth) of this node in the tree */
protected $treeDepth = null;
/** @var F0FTableNested The root node in the tree */
protected $treeRoot = null;
/** @var F0FTableNested The parent node of ourselves */
protected $treeParent = null;
/** @var bool Should I perform a nested get (used to query ascendants/descendants) */
protected $treeNestedGet = false;
/** @var array A collection of custom, additional where clauses to apply during buildQuery */
protected $whereClauses = array();
/**
* Public constructor. Overrides the parent constructor, making sure there are lft/rgt columns which make it
* compatible with nested sets.
*
* @param string $table Name of the database table to model.
* @param string $key Name of the primary key field in the table.
* @param F0FDatabaseDriver &$db Database driver
* @param array $config The configuration parameters array
*
* @throws \RuntimeException When lft/rgt columns are not found
*/
public function __construct($table, $key, &$db, $config = array())
{
parent::__construct($table, $key, $db, $config);
if (!$this->hasField('lft') || !$this->hasField('rgt'))
{
throw new \RuntimeException("Table " . $this->getTableName() . " is not compatible with F0FTableNested: it does not have lft/rgt columns");
}
}
/**
* Overrides the automated table checks to handle the 'hash' column for faster searching
*
* @return boolean
*/
public function check()
{
// Create a slug if there is a title and an empty slug
if ($this->hasField('title') && $this->hasField('slug') && empty($this->slug))
{
$this->slug = F0FStringUtils::toSlug($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();
return parent::check();
}
/**
* 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 integer $oid The primary key value of the item to delete
*
* @throws UnexpectedValueException
*
* @return boolean True on success
*/
public function delete($oid = null)
{
// Load the specified record (if necessary)
if (!empty($oid))
{
$this->load($oid);
}
$k = $this->_tbl_key;
$pk = (!$oid) ? $this->$k : $oid;
// If no primary key is given, return false.
if (!$pk)
{
throw new UnexpectedValueException('Null primary key not allowed.');
}
// 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
if (!$this->onBeforeDelete($oid))
{
return false;
}
$result = true;
// Recursively delete all children nodes as long as we are not a leaf node and $recursive is enabled
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 F0FTableNested $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->delete($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->load($pk);
}
}
if($result)
{
// Delete the row by primary key.
$query = $this->_db->getQuery(true);
$query->delete();
$query->from($this->_tbl);
$query->where($this->_tbl_key . ' = ' . $this->_db->q($pk));
$this->_db->setQuery($query)->execute();
$result = $this->onAfterDelete($oid);
}
return $result;
}
protected function onAfterDelete($oid)
{
$db = $this->getDbo();
$myLeft = $this->lft;
$myRight = $this->rgt;
$fldLft = $db->qn($this->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('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 parent::onAfterDelete($oid);
}
/**
* Not supported in nested sets
*
* @param string $where Ignored
*
* @return void
*
* @throws RuntimeException
*/
public function reorder($where = '')
{
throw new RuntimeException('reorder() is not supported by F0FTableNested');
}
/**
* Not supported in nested sets
*
* @param integer $delta Ignored
* @param string $where Ignored
*
* @return void
*
* @throws RuntimeException
*/
public function move($delta, $where = '')
{
throw new RuntimeException('move() is not supported by F0FTableNested');
}
/**
* 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
{
return $newNode->insertAsChildOf($this->getParent());
}
}
/**
* Makes a copy of the record, inserting it as the last child of the given node's parent.
*
* @param integer|array $cid The primary key value (or values) or the record(s) to copy.
* If null, the current record will be copied
*
* @return self|F0FTableNested The last copied node
*/
public function copy($cid = null)
{
//We have to cast the id as array, or the helper function will return an empty set
if($cid)
{
$cid = (array) $cid;
}
F0FUtilsArray::toInteger($cid);
$k = $this->_tbl_key;
if (count($cid) < 1)
{
if ($this->$k)
{
$cid = array($this->$k);
}
else
{
// Even if it's null, let's still create the record
$this->create($this->getData());
return $this;
}
}
foreach ($cid as $item)
{
// Prevent load with id = 0
if (!$item)
{
continue;
}
$this->load($item);
$this->create($this->getData());
}
return $this;
}
/**
* Method to reset class properties to the defaults set in the class
* definition. It will ignore the primary key as well as any private class
* properties.
*
* @return void
*/
public function reset()
{
$this->resetTreeCache();
parent::reset();
}
/**
* 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 self
*/
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 RuntimeException(__METHOD__.' can be only used with new nodes');
}
// 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->getColumnAlias('rgt'));
$query = $db->getQuery(true)
->select('MAX(' . $fldRgt . ')')
->from($db->qn($this->getTableName()));
$maxRgt = $db->setQuery($query, 0, 1)->loadResult();
if (empty($maxRgt))
{
$maxRgt = 0;
}
$this->lft = ++$maxRgt;
$this->rgt = ++$maxRgt;
$this->store();
return $this;
}
/**
* 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 F0FTableNested $parentNode The node which will become our parent
*
* @return $this for chaining
*
* @throws Exception
* @throws RuntimeException
*/
public function insertAsFirstChildOf(F0FTableNested &$parentNode)
{
if($parentNode->lft >= $parentNode->rgt)
{
throw new RuntimeException('Invalid position values for the parent node');
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
$fldLft = $db->qn($this->getColumnAlias('lft'));
// Nullify the PK, so a new record will be created
$pk = $this->getKeyName();
$this->$pk = 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->getTableName()))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' > ' . $db->q($myLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($fldRgt . ' = ' . $fldRgt . '+ 2')
->where($fldRgt . '>' . $db->q($myLeft));
$db->setQuery($query)->execute();
// Insert the new node
$this->store();
// 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 F0FTableNested $parentNode The node which will become our parent
*
* @return $this for chaining
*
* @throws Exception
* @throws RuntimeException
*/
public function insertAsLastChildOf(F0FTableNested &$parentNode)
{
if($parentNode->lft >= $parentNode->rgt)
{
throw new RuntimeException('Invalid position values for the parent node');
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
$fldLft = $db->qn($this->getColumnAlias('lft'));
// Nullify the PK, so a new record will be created
$pk = $this->getKeyName();
$this->$pk = 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->getTableName()))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . '>=' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . '>' . $db->q($myRight));
$db->setQuery($query)->execute();
// Insert the new node
$this->store();
// 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 F0FTableNested $parentNode
*
* @return $this for chaining
*
* @throws Exception
*/
public function insertAsChildOf(F0FTableNested &$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 F0FTableNested $siblingNode We will be inserted before this node
*
* @return $this for chaining
*
* @throws Exception
* @throws RuntimeException
*/
public function insertLeftOf(F0FTableNested &$siblingNode)
{
if($siblingNode->lft >= $siblingNode->rgt)
{
throw new RuntimeException('Invalid position values for the sibling node');
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
$fldLft = $db->qn($this->getColumnAlias('lft'));
// Nullify the PK, so a new record will be created
$pk = $this->getKeyName();
$this->$pk = 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->getTableName()))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' >= ' . $db->q($myLeft))
)->execute();
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . ' > ' . $db->q($myLeft))
)->execute();
$this->store();
// 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 F0FTableNested $siblingNode We will be inserted after this node
*
* @return $this for chaining
* @throws Exception
* @throws RuntimeException
*/
public function insertRightOf(F0FTableNested &$siblingNode)
{
if($siblingNode->lft >= $siblingNode->rgt)
{
throw new RuntimeException('Invalid position values for the sibling node');
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
$fldLft = $db->qn($this->getColumnAlias('lft'));
// Nullify the PK, so a new record will be created
$pk = $this->getKeyName();
$this->$pk = 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->getTableName()))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . ' > ' . $db->q($myRight))
)->execute();
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' > ' . $db->q($myRight))
)->execute();
$this->store();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Alias for insertRightOf
*
* @codeCoverageIgnore
* @param F0FTableNested $siblingNode
*
* @return $this for chaining
*/
public function insertAsSiblingOf(F0FTableNested &$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 RuntimeException('Invalid position values for the current node');
}
// 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();
$table = $this->getClone();
$table->reset();
$leftSibling = $table->whereRaw($db->qn($this->getColumnAlias('rgt')) . ' = ' . $db->q($this->lft - 1))
->get(0, 1)->current();
// Move the node
if (is_object($leftSibling) && ($leftSibling instanceof F0FTableNested))
{
return $this->moveToLeftOf($leftSibling);
}
return false;
}
/**
* 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 RuntimeException('Invalid position values for the current node');
}
// 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();
$table = $this->getClone();
$table->reset();
$rightSibling = $table->whereRaw($db->qn($this->getColumnAlias('lft')) . ' = ' . $db->q($this->rgt + 1))
->get(0, 1)->current();
// Move the node
if (is_object($rightSibling) && ($rightSibling instanceof F0FTableNested))
{
return $this->moveToRightOf($rightSibling);
}
return false;
}
/**
* 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 F0FTableNested $siblingNode
*
* @return $this for chaining
*
* @throws Exception
* @throws RuntimeException
*/
public function moveToLeftOf(F0FTableNested $siblingNode)
{
// Sanity checks on current and sibling node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if($siblingNode->lft >= $siblingNode->rgt)
{
throw new RuntimeException('Invalid position values for the sibling node');
}
$db = $this->getDbo();
$left = $db->qn($this->getColumnAlias('lft'));
$right = $db->qn($this->getColumnAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get sibling metrics
$sibLeft = $siblingNode->lft;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' >= ' . $db->q($newSibLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->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->load();
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 F0FTableNested $siblingNode
*
* @return $this for chaining
*
* @throws Exception
* @throws RuntimeException
*/
public function moveToRightOf(F0FTableNested $siblingNode)
{
// Sanity checks on current and sibling node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if($siblingNode->lft >= $siblingNode->rgt)
{
throw new RuntimeException('Invalid position values for the sibling node');
}
$db = $this->getDbo();
$left = $db->qn($this->getColumnAlias('lft'));
$right = $db->qn($this->getColumnAlias('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->getTableName()))
->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->getTableName()))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($newSibRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->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->load();
return $this;
}
/**
* Alias for moveToRightOf
*
* @codeCoverageIgnore
* @param F0FTableNested $siblingNode
*
* @return $this for chaining
*/
public function makeNextSiblingOf(F0FTableNested $siblingNode)
{
return $this->moveToRightOf($siblingNode);
}
/**
* Alias for makeNextSiblingOf
*
* @codeCoverageIgnore
* @param F0FTableNested $siblingNode
*
* @return $this for chaining
*/
public function makeSiblingOf(F0FTableNested $siblingNode)
{
return $this->makeNextSiblingOf($siblingNode);
}
/**
* Alias for moveToLeftOf
*
* @codeCoverageIgnore
* @param F0FTableNested $siblingNode
*
* @return $this for chaining
*/
public function makePreviousSiblingOf(F0FTableNested $siblingNode)
{
return $this->moveToLeftOf($siblingNode);
}
/**
* Moves a node and its subtree as a the first (leftmost) child of $parentNode
*
* @param F0FTableNested $parentNode
*
* @return $this for chaining
*
* @throws Exception
*/
public function makeFirstChildOf(F0FTableNested $parentNode)
{
// Sanity checks on current and sibling node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if($parentNode->lft >= $parentNode->rgt)
{
throw new RuntimeException('Invalid position values for the parent node');
}
$db = $this->getDbo();
$left = $db->qn($this->getColumnAlias('lft'));
$right = $db->qn($this->getColumnAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$parentLeft = $parentNode->lft;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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;
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' >= ' . $db->q($newParentLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->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->load();
return $this;
}
/**
* Moves a node and its subtree as a the last (rightmost) child of $parentNode
*
* @param F0FTableNested $parentNode
*
* @return $this for chaining
*
* @throws Exception
* @throws RuntimeException
*/
public function makeLastChildOf(F0FTableNested $parentNode)
{
// Sanity checks on current and sibling node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if($parentNode->lft >= $parentNode->rgt)
{
throw new RuntimeException('Invalid position values for the parent node');
}
$db = $this->getDbo();
$left = $db->qn($this->getColumnAlias('lft'));
$right = $db->qn($this->getColumnAlias('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->getTableName()))
->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->getTableName()))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' >= ' . $db->q($newLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->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->getTableName()))
->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->load();
return $this;
}
/**
* Alias for makeLastChildOf
*
* @codeCoverageIgnore
* @param F0FTableNested $parentNode
*
* @return $this for chaining
*/
public function makeChildOf(F0FTableNested $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 retrieval.
*
* @throws RuntimeException
*
* @return int|mixed
*/
public function getLevel()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if (is_null($this->treeDepth))
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
$query = $db->getQuery(true)
->select('(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth'))
->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->getTableName()) . ' 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 F0FTableNested
*/
public function getParent()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if ($this->isRoot())
{
return $this;
}
if (empty($this->treeParent) || !is_object($this->treeParent) || !($this->treeParent instanceof F0FTableNested))
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
$query = $db->getQuery(true)
->select($db->qn('parent') . '.' . $fldLft)
->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->getTableName()) . ' 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, 0, 1)->loadResult();
$table = $this->getClone();
$table->reset();
$this->treeParent = $table
->whereRaw($fldLft . ' = ' . $db->q($targetLft))
->get()->current();
}
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 RuntimeException('Invalid position values for the current node');
}
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 F0FTableNested $otherNode
*
* @throws RuntimeException
*
* @return bool
*/
public function isDescendantOf(F0FTableNested $otherNode)
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if($otherNode->lft >= $otherNode->rgt)
{
throw new RuntimeException('Invalid position values for the other node');
}
return ($otherNode->lft < $this->lft) && ($otherNode->rgt > $this->rgt);
}
/**
* Returns true if $otherNode is ourselves or if we are a descendant of $otherNode
*
* @param F0FTableNested $otherNode
*
* @throws RuntimeException
*
* @return bool
*/
public function isSelfOrDescendantOf(F0FTableNested $otherNode)
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if($otherNode->lft >= $otherNode->rgt)
{
throw new RuntimeException('Invalid position values for the other node');
}
return ($otherNode->lft <= $this->lft) && ($otherNode->rgt >= $this->rgt);
}
/**
* Returns true if we are an ancestor of $otherNode
*
* @codeCoverageIgnore
* @param F0FTableNested $otherNode
*
* @return bool
*/
public function isAncestorOf(F0FTableNested $otherNode)
{
return $otherNode->isDescendantOf($this);
}
/**
* Returns true if $otherNode is ourselves or we are an ancestor of $otherNode
*
* @codeCoverageIgnore
* @param F0FTableNested $otherNode
*
* @return bool
*/
public function isSelfOrAncestorOf(F0FTableNested $otherNode)
{
return $otherNode->isSelfOrDescendantOf($this);
}
/**
* Is $node this very node?
*
* @param F0FTableNested $node
*
* @throws RuntimeException
*
* @return bool
*/
public function equals(F0FTableNested &$node)
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
if($node->lft >= $node->rgt)
{
throw new RuntimeException('Invalid position values for the other node');
}
return (
($this->getId() == $node->getId())
&& ($this->lft == $node->lft)
&& ($this->rgt == $node->rgt)
);
}
/**
* Alias for isDescendantOf
*
* @codeCoverageIgnore
* @param F0FTableNested $otherNode
*
* @return bool
*/
public function insideSubtree(F0FTableNested $otherNode)
{
return $this->isDescendantOf($otherNode);
}
/**
* Returns true if both this node and $otherNode are root, leaf or child (same tree scope)
*
* @param F0FTableNested $otherNode
*
* @return bool
*/
public function inSameScope(F0FTableNested $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->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('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->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('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->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('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->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('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->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('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->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('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
*
* @return void
*/
protected function scopeImmediateDescendants()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
$db = $this->getDbo();
$fldLft = $db->qn($this->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
$subQuery = $db->getQuery(true)
->select(array(
$db->qn('node') . '.' . $fldLft,
'(COUNT(*) - 1) AS ' . $db->qn('depth')
))
->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'))
->from($db->qn($this->getTableName()) . ' 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->getTableName()) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'))
->join('CROSS', $db->qn($this->getTableName()) . ' 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 F0FTableNested $node The node to exclude from the results
*
* @return void
*/
public function withoutNode(F0FTableNested $node)
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getColumnAlias('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 self
*
* @throws \RuntimeException
*/
public function getRoot()
{
// Sanity checks on current node position
if($this->lft >= $this->rgt)
{
throw new RuntimeException('Invalid position values for the current node');
}
// 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 F0FTableNested))
{
$this->treeRoot = null;
// First try to get the record with the minimum ID
$db = $this->getDbo();
$fldLft = $db->qn($this->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
$subQuery = $db->getQuery(true)
->select('MIN(' . $fldLft . ')')
->from($db->qn($this->getTableName()));
try
{
$table = $this->getClone();
$table->reset();
$root = $table
->whereRaw($fldLft . ' = (' . (string)$subQuery . ')')
->get(0, 1)->current();
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 \RuntimeException("No root found for table " . $this->getTableName() . ", node lft=" . $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->getTableName()) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->getTableName()) . ' 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 \RuntimeException("No root found for table " . $this->getTableName() . ", node lft=" . $this->lft);
}
try
{
$table = $this->getClone();
$table->reset();
$this->treeRoot = $table
->whereRaw($fldLft . ' = ' . $db->q($targetLeft))
->get(0, 1)->current();
}
catch (\RuntimeException $e)
{
// If there is no root found throw an exception. Basically: your table is FUBAR.
throw new \RuntimeException("No root found for table " . $this->getTableName() . ", node lft=" . $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 F0FDatabaseIterator
*/
public function getAncestorsAndSelf()
{
$this->scopeAncestorsAndSelf();
return $this->get();
}
/**
* Get all ancestors to this node and the node itself, but not the root node. If you want to
*
* @codeCoverageIgnore
*
* @return F0FDatabaseIterator
*/
public function getAncestorsAndSelfWithoutRoot()
{
$this->scopeAncestorsAndSelf();
$this->scopeWithoutRoot();
return $this->get();
}
/**
* 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 F0FDatabaseIterator
*/
public function getAncestors()
{
$this->scopeAncestorsAndSelf();
$this->scopeWithoutSelf();
return $this->get();
}
/**
* Get all ancestors to this node but not the node itself and its root.
*
* @codeCoverageIgnore
*
* @return F0FDatabaseIterator
*/
public function getAncestorsWithoutRoot()
{
$this->scopeAncestors();
$this->scopeWithoutRoot();
return $this->get();
}
/**
* Get all sibling nodes, including ourselves
*
* @codeCoverageIgnore
*
* @return F0FDatabaseIterator
*/
public function getSiblingsAndSelf()
{
$this->scopeSiblingsAndSelf();
return $this->get();
}
/**
* Get all sibling nodes, except ourselves
*
* @codeCoverageIgnore
*
* @return F0FDatabaseIterator
*/
public function getSiblings()
{
$this->scopeSiblings();
return $this->get();
}
/**
* 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 F0FDatabaseIterator
*/
public function getLeaves()
{
$this->scopeLeaves();
return $this->get();
}
/**
* Get all descendant (children) nodes and ourselves.
*
* Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
*
* @codeCoverageIgnore
*
* @return F0FDatabaseIterator
*/
public function getDescendantsAndSelf()
{
$this->scopeDescendantsAndSelf();
return $this->get();
}
/**
* Get only our descendant (children) nodes, not ourselves.
*
* Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
*
* @codeCoverageIgnore
*
* @return F0FDatabaseIterator
*/
public function getDescendants()
{
$this->scopeDescendants();
return $this->get();
}
/**
* 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 F0FDatabaseIterator
*/
public function getImmediateDescendants()
{
$this->scopeImmediateDescendants();
return $this->get();
}
/**
* 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->getColumnAlias('lft'));
$fldRgt = $db->qn($this->getColumnAlias('rgt'));
if (empty($key) || !$this->hasField($key))
{
$key = $this->getKeyName();
}
if (empty($column))
{
$column = 'title';
}
$fldKey = $db->qn($this->getColumnAlias($key));
$fldColumn = $db->qn($this->getColumnAlias($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->getTableName()) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->getTableName()) . ' 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 F0FTableNested|null The found node or null if nothing is found
*/
public function findByPath($path)
{
// @todo
}
public function isValid()
{
// @todo
}
public function rebuild()
{
// @todo
}
/**
* 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;
}
/**
* 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.
*
* @param string $rawWhereClause The raw WHERE clause to add
*
* @return $this For chaining
*/
public function whereRaw($rawWhereClause)
{
$this->whereClauses[] = $rawWhereClause;
return $this;
}
/**
* Builds the query for the get() method
*
* @return F0FDatabaseQuery
*/
protected function buildQuery()
{
$db = $this->getDbo();
$query = $db->getQuery(true)
->select($db->qn('node') . '.*')
->from($db->qn($this->getTableName()) . ' AS ' . $db->qn('node'));
if ($this->treeNestedGet)
{
$query
->join('CROSS', $db->qn($this->getTableName()) . ' AS ' . $db->qn('parent'));
}
// Apply custom WHERE clauses
if (count($this->whereClauses))
{
foreach ($this->whereClauses as $clause)
{
$query->where($clause);
}
}
return $query;
}
/**
* Returns a database iterator to retrieve records. Use the scope methods and the whereRaw method to define what
* exactly will be returned.
*
* @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 F0FDatabaseIterator The data collection
*/
public function get($limitstart = 0, $limit = 0)
{
$limitstart = max($limitstart, 0);
$limit = max($limit, 0);
$query = $this->buildQuery();
$db = $this->getDbo();
$db->setQuery($query, $limitstart, $limit);
$cursor = $db->execute();
$dataCollection = F0FDatabaseIterator::getIterator($db->name, $cursor, null, $this->config['_table_class']);
return $dataCollection;
}
}
dispatcher/behavior.php 0000604 00000001032 15245540356 0011203 0 ustar 00 <?php
/**
* @package FrameworkOnFramework
* @subpackage table
* @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Protect from unauthorized access
defined('F0F_INCLUDED') or die;
/**
* FrameworkOnFramework table behavior dispatcher class
*
* @codeCoverageIgnore
* @package FrameworkOnFramework
* @since 2.1
*/
class F0FTableDispatcherBehavior extends F0FUtilsObservableDispatcher
{
}
behavior/tags.php 0000604 00000005740 15245540356 0010025 0 ustar 00 <?php
/**
* @package FrameworkOnFramework
* @subpackage table
* @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Protect from unauthorized access
defined('F0F_INCLUDED') or die;
/**
* FrameworkOnFramework table behavior class for tags
*
* @package FrameworkOnFramework
* @since 2.1
*/
class F0FTableBehaviorTags extends F0FTableBehavior
{
/**
* The event which runs after binding data to the table
*
* @param F0FTable &$table The table which calls this event
* @param object|array &$src The data to bind
* @param array $options The options of the table
*
* @return boolean True on success
*/
public function onAfterBind(&$table, &$src, $options = array())
{
// Bind tags
if ($table->hasTags())
{
if ((!empty($src['tags']) && $src['tags'][0] != ''))
{
$table->newTags = $src['tags'];
}
// Check if the content type exists, and create it if it does not
$table->checkContentType();
$tagsTable = clone($table);
$tagsHelper = new JHelperTags();
$tagsHelper->typeAlias = $table->getContentType();
// TODO: This little guy here fails because JHelperTags
// need a JTable object to work, while our is F0FTable
// Need probably to write our own F0FHelperTags
// Thank you com_tags
if (!$tagsHelper->postStoreProcess($tagsTable))
{
$table->setError('Error storing tags');
return false;
}
}
return true;
}
/**
* The event which runs before storing (saving) data to the database
*
* @param F0FTable &$table The table which calls this event
* @param boolean $updateNulls Should nulls be saved as nulls (true) or just skipped over (false)?
*
* @return boolean True to allow saving
*/
public function onBeforeStore(&$table, $updateNulls)
{
if ($table->hasTags())
{
$tagsHelper = new JHelperTags();
$tagsHelper->typeAlias = $table->getContentType();
// TODO: JHelperTags sucks in Joomla! 3.1, it requires that tags are
// stored in the metadata property. Not our case, therefore we need
// to add it in a fake object. We sent a PR to Joomla! CMS to fix
// that. Once it's accepted, we'll have to remove the attrocity
// here...
$tagsTable = clone($table);
$tagsHelper->preStoreProcess($tagsTable);
}
}
/**
* The event which runs after deleting a record
*
* @param F0FTable &$table The table which calls this event
* @param integer $oid The PK value of the record which was deleted
*
* @return boolean True to allow the deletion without errors
*/
public function onAfterDelete(&$table, $oid)
{
// If this resource has tags, delete the tags first
if ($table->hasTags())
{
$tagsHelper = new JHelperTags();
$tagsHelper->typeAlias = $table->getContentType();
if (!$tagsHelper->deleteTagData($table, $oid))
{
$table->setError('Error deleting Tags');
return false;
}
}
}
}
behavior/assets.php 0000604 00000015022 15245540356 0010363 0 ustar 00 <?php
/**
* @package FrameworkOnFramework
* @subpackage table
* @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Protect from unauthorized access
defined('F0F_INCLUDED') or die;
/**
* FrameworkOnFramework table behavior class for assets
*
* @package FrameworkOnFramework
* @since 2.1
*/
class F0FTableBehaviorAssets extends F0FTableBehavior
{
/**
* The event which runs after storing (saving) data to the database
*
* @param F0FTable &$table The table which calls this event
*
* @return boolean True to allow saving
*/
public function onAfterStore(&$table)
{
$result = true;
$asset_id_field = $table->getColumnAlias('asset_id');
if (in_array($asset_id_field, $table->getKnownFields()))
{
if (!empty($table->$asset_id_field))
{
$currentAssetId = $table->$asset_id_field;
}
// The asset id field is managed privately by this class.
if ($table->isAssetsTracked())
{
unset($table->$asset_id_field);
}
}
// Create the object used for inserting/udpating data to the database
$fields = $table->getTableFields();
// Let's remove the asset_id field, since we unset the property above and we would get a PHP notice
if (isset($fields[$asset_id_field]))
{
unset($fields[$asset_id_field]);
}
// Asset Tracking
if (in_array($asset_id_field, $table->getKnownFields()) && $table->isAssetsTracked())
{
$parentId = $table->getAssetParentId();
try{
$name = $table->getAssetName();
}
catch(Exception $e)
{
$table->setError($e->getMessage());
return false;
}
$title = $table->getAssetTitle();
$asset = JTable::getInstance('Asset');
$asset->loadByName($name);
// Re-inject the asset id.
$this->$asset_id_field = $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)
{
$table->setError($error);
return false;
}
// @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($table->$asset_id_field) || $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 ($table->getRules() instanceof JAccessRules)
{
$asset->rules = (string) $table->getRules();
}
// Since we are using JTable, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!$asset->check() || !$asset->store())
{
$table->setError($asset->getError());
return false;
}
// @codeCoverageIgnoreEnd
// Create an asset_id or heal one that is corrupted.
if (empty($table->$asset_id_field) || (($currentAssetId != $table->$asset_id_field) && !empty($table->$asset_id_field)))
{
// Update the asset_id field in this table.
$table->$asset_id_field = (int) $asset->id;
$k = $table->getKeyName();
$db = $table->getDbo();
$query = $db->getQuery(true)
->update($db->qn($table->getTableName()))
->set($db->qn($asset_id_field).' = ' . (int) $table->$asset_id_field)
->where($db->qn($k) . ' = ' . (int) $table->$k);
$db->setQuery($query)->execute();
}
$result = true;
}
return $result;
}
/**
* The event which runs after binding data to the table
*
* @param F0FTable &$table The table which calls this event
* @param object|array &$src The data to bind
*
* @return boolean True on success
*/
public function onAfterBind(&$table, &$src)
{
// Set rules for assets enabled tables
if ($table->isAssetsTracked())
{
// 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.
// @todo Should we move this logic inside the setRules method?
$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;
}
}
}
$table->setRules($rules);
}
}
return true;
}
/**
* The event which runs before deleting a record
*
* @param F0FTable &$table The table 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(&$table, $oid)
{
// If tracking assets, remove the asset first.
if ($table->isAssetsTracked())
{
$k = $table->getKeyName();
// If the table is not loaded, let's try to load it with the id
if(!$table->$k)
{
$table->load($oid);
}
// If I have an invalid assetName I have to stop
try
{
$name = $table->getAssetName();
}
catch(Exception $e)
{
$table->setError($e->getMessage());
return false;
}
// 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))
{
// Since we are using JTable, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!$asset->delete())
{
$table->setError($asset->getError());
return false;
}
// @codeCoverageIgnoreEnd
}
else
{
// I'll simply return true even if I couldn't load the asset. In this way I can still
// delete a broken record
return true;
}
}
return true;
}
}
behavior/contenthistory.php 0000604 00000003146 15245540356 0012161 0 ustar 00 <?php
/**
* @package FrameworkOnFramework
* @subpackage table
* @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Protect from unauthorized access
defined('F0F_INCLUDED') or die;
/**
* FrameworkOnFramework table behavior class for content History
*
* @package FrameworkOnFramework
* @since 2.2.0
*/
class F0FTableBehaviorContenthistory extends F0FTableBehavior
{
/**
* The event which runs after storing (saving) data to the database
*
* @param F0FTable &$table The table which calls this event
*
* @return boolean True to allow saving without an error
*/
public function onAfterStore(&$table)
{
$aliasParts = explode('.', $table->getContentType());
$table->checkContentType();
if (JComponentHelper::getParams($aliasParts[0])->get('save_history', 0))
{
$historyHelper = new JHelperContenthistory($table->getContentType());
$historyHelper->store($table);
}
return true;
}
/**
* The event which runs before deleting a record
*
* @param F0FTable &$table The table 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(&$table, $oid)
{
$aliasParts = explode('.', $table->getContentType());
if (JComponentHelper::getParams($aliasParts[0])->get('save_history', 0))
{
$historyHelper = new JHelperContenthistory($table->getContentType());
$historyHelper->deleteHistory($table);
}
return true;
}
}
relations.php 0000604 00000100644 15245540356 0007267 0 ustar 00 <?php
/**
* @package FrameworkOnFramework
* @subpackage table
* @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Protect from unauthorized access
defined('F0F_INCLUDED') or die;
class F0FTableRelations
{
/**
* Holds all known relation definitions
*
* @var array
*/
protected $relations = array(
'child' => array(),
'parent' => array(),
'children' => array(),
'multiple' => array(),
);
/**
* Holds the default relations' keys
*
* @var array
*/
protected $defaultRelation = array(
'child' => null,
'parent' => null,
'children' => null,
'multiple' => null,
);
/**
* The table these relations are attached to
*
* @var F0FTable
*/
protected $table = null;
/**
* The name of the component used by our attached table
*
* @var string
*/
protected $componentName = 'joomla';
/**
* The type (table name without prefix and component name) of our attached table
*
* @var string
*/
protected $tableType = '';
/**
* Create a relations object based on the provided F0FTable instance
*
* @param F0FTable $table The table instance used to initialise the relations
*/
public function __construct(F0FTable $table)
{
// Store the table
$this->table = $table;
// Get the table's type from its name
$tableName = $table->getTableName();
$tableName = str_replace('#__', '', $tableName);
$type = explode("_", $tableName);
if (count($type) == 1)
{
$this->tableType = array_pop($type);
}
else
{
$this->componentName = array_shift($type);
$this->tableType = array_pop($type);
}
$this->tableType = F0FInflector::singularize($this->tableType);
$tableKey = $table->getKeyName();
unset($type);
// Scan all table keys and look for foo_bar_id fields. These fields are used to populate parent relations.
foreach ($table->getKnownFields() as $field)
{
// Skip the table key name
if ($field == $tableKey)
{
continue;
}
if (substr($field, -3) != '_id')
{
continue;
}
$parts = explode('_', $field);
// If the component type of the field is not set assume 'joomla'
if (count($parts) == 2)
{
array_unshift($parts, 'joomla');
}
// Sanity check
if (count($parts) != 3)
{
continue;
}
// Make sure we skip any references back to ourselves (should be redundant, due to key field check above)
if ($parts[1] == $this->tableType)
{
continue;
}
// Default item name: the name of the table, singular
$itemName = F0FInflector::singularize($parts[1]);
// Prefix the item name with the component name if we refer to a different component
if ($parts[0] != $this->componentName)
{
$itemName = $parts[0] . '_' . $itemName;
}
// Figure out the table class
$tableClass = ucfirst($parts[0]) . 'Table' . ucfirst($parts[1]);
$default = empty($this->relations['parent']);
$this->addParentRelation($itemName, $tableClass, $field, $field, $default);
}
// Get the relations from the configuration provider
$key = $table->getConfigProviderKey() . '.relations';
$configRelations = $table->getConfigProvider()->get($key, array());
if (!empty($configRelations))
{
foreach ($configRelations as $relation)
{
if (empty($relation['type']))
{
continue;
}
if (isset($relation['pivotTable']))
{
$this->addMultipleRelation($relation['itemName'], $relation['tableClass'],
$relation['localKey'], $relation['ourPivotKey'], $relation['theirPivotKey'],
$relation['remoteKey'], $relation['pivotTable'], $relation['default']);
}
else
{
$method = 'add' . ucfirst($relation['type']). 'Relation';
if (!method_exists($this, $method))
{
continue;
}
$this->$method($relation['itemName'], $relation['tableClass'],
$relation['localKey'], $relation['remoteKey'], $relation['default']);
}
}
}
}
/**
* Add a 1:1 forward (child) relation. This adds relations for the getChild() method.
*
* In other words: does a table HAVE ONE child
*
* Parent and child relations works the same way. We have them separated as it makes more sense for us humans to
* read code like $item->getParent() and $item->getChild() than $item->getRelatedObject('someRandomKeyName')
*
* @param string $itemName is how it will be known locally to the getRelatedItem method (singular)
* @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
* @param string $localKey is the column containing our side of the FK relation, default: our primary key
* @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
* @param boolean $default add as the default child relation?
*
* @return void
*/
public function addChildRelation($itemName, $tableClass = null, $localKey = null, $remoteKey = null, $default = true)
{
$itemName = $this->normaliseItemName($itemName, false);
if (empty($localKey))
{
$localKey = $this->table->getKeyName();
}
$this->addBespokeSimpleRelation('child', $itemName, $tableClass, $localKey, $remoteKey, $default);
}
/**
* Defining an inverse 1:1 (parent) relation. You must specify at least the $tableClass or the $localKey.
* This adds relations for the getParent() method.
*
* In other words: does a table BELONG TO ONE parent
*
* Parent and child relations works the same way. We have them separated as it makes more sense for us humans to
* read code like $item->getParent() and $item->getChild() than $item->getRelatedObject('someRandomKeyName')
*
* @param string $itemName is how it will be known locally to the getRelatedItem method (singular)
* @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
* @param string $localKey is the column containing our side of the FK relation, default: componentname_itemname_id
* @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
* @param boolean $default Is this the default parent relationship?
*
* @return void
*/
public function addParentRelation($itemName, $tableClass = null, $localKey = null, $remoteKey = null, $default = true)
{
$itemName = $this->normaliseItemName($itemName, false);
$this->addBespokeSimpleRelation('parent', $itemName, $tableClass, $localKey, $remoteKey, $default);
}
/**
* Defining a forward 1:∞ (children) relation. This adds relations to the getChildren() method.
*
* In other words: does a table HAVE MANY children?
*
* The children relation works very much the same as the parent and child relation. The difference is that the
* parent and child relations return a single table object, whereas the children relation returns an iterator to
* many objects.
*
* @param string $itemName is how it will be known locally to the getRelatedItems method (plural)
* @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
* @param string $localKey is the column containing our side of the FK relation, default: our primary key
* @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
* @param boolean $default is this the default children relationship?
*
* @return void
*/
public function addChildrenRelation($itemName, $tableClass = null, $localKey = null, $remoteKey = null, $default = true)
{
$itemName = $this->normaliseItemName($itemName, true);
if (empty($localKey))
{
$localKey = $this->table->getKeyName();
}
$this->addBespokeSimpleRelation('children', $itemName, $tableClass, $localKey, $remoteKey, $default);
}
/**
* Defining a ∞:∞ (multiple) relation. This adds relations to the getMultiple() method.
*
* In other words: is a table RELATED TO MANY other records?
*
* @param string $itemName is how it will be known locally to the getRelatedItems method (plural)
* @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
* @param string $localKey is the column containing our side of the FK relation, default: our primary key field name
* @param string $ourPivotKey is the column containing our side of the FK relation in the pivot table, default: $localKey
* @param string $theirPivotKey is the column containing the other table's side of the FK relation in the pivot table, default $remoteKey
* @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
* @param string $glueTable is the name of the glue (pivot) table, default: #__componentname_thisclassname_itemname with plural items (e.g. #__foobar_users_roles)
* @param boolean $default is this the default multiple relation?
*/
public function addMultipleRelation($itemName, $tableClass = null, $localKey = null, $ourPivotKey = null, $theirPivotKey = null, $remoteKey = null, $glueTable = null, $default = true)
{
$itemName = $this->normaliseItemName($itemName, true);
if (empty($localKey))
{
$localKey = $this->table->getKeyName();
}
$this->addBespokePivotRelation('multiple', $itemName, $tableClass, $localKey, $remoteKey, $ourPivotKey, $theirPivotKey, $glueTable, $default);
}
/**
* Removes a previously defined relation by name. You can optionally specify the relation type.
*
* @param string $itemName The name of the relation to remove
* @param string $type [optional] The relation type (child, parent, children, ...)
*
* @return void
*/
public function removeRelation($itemName, $type = null)
{
$types = array_keys($this->relations);
if (in_array($type, $types))
{
$types = array($type);
}
foreach ($types as $type)
{
foreach ($this->relations[$type] as $key => $relations)
{
if ($itemName == $key)
{
unset ($this->relations[$type][$itemName]);
// If it's the default one, remove it from the default array, too
if($this->defaultRelation[$type] == $itemName)
{
$this->defaultRelation[$type] = null;
}
return;
}
}
}
}
/**
* Removes all existing relations
*
* @param string $type The type or relations to remove, omit to remove all relation types
*
* @return void
*/
public function clearRelations($type = null)
{
$types = array_keys($this->relations);
if (in_array($type, $types))
{
$types = array($type);
}
foreach ($types as $type)
{
$this->relations[$type] = array();
// Remove the relation from the default stack, too
$this->defaultRelation[$type] = null;
}
}
/**
* Does the named relation exist? You can optionally specify the type.
*
* @param string $itemName The name of the relation to check
* @param string $type [optional] The relation type (child, parent, children, ...)
*
* @return boolean
*/
public function hasRelation($itemName, $type = null)
{
$types = array_keys($this->relations);
if (in_array($type, $types))
{
$types = array($type);
}
foreach ($types as $type)
{
foreach ($this->relations[$type] as $key => $relations)
{
if ($itemName == $key)
{
return true;
}
}
}
return false;
}
/**
* Get the definition of a relation
*
* @param string $itemName The name of the relation to check
* @param string $type [optional] The relation type (child, parent, children, ...)
*
* @return array
*
* @throws RuntimeException When the relation is not found
*/
public function getRelation($itemName, $type)
{
$types = array_keys($this->relations);
if (in_array($type, $types))
{
$types = array($type);
}
foreach ($types as $type)
{
foreach ($this->relations[$type] as $key => $relations)
{
if ($itemName == $key)
{
$temp = $relations;
$temp['type'] = $type;
return $temp;
}
}
}
throw new RuntimeException("Relation $itemName not found in table {$this->tableType}", 500);
}
/**
* Gets the item referenced by a named relation. You can optionally specify the type. Only single item relation
* types will be searched.
*
* @param string $itemName The name of the relation to use
* @param string $type [optional] The relation type (child, parent)
*
* @return F0FTable
*
* @throws RuntimeException If the named relation doesn't exist or isn't supposed to return single items
*/
public function getRelatedItem($itemName, $type = null)
{
if (empty($type))
{
$relation = $this->getRelation($itemName, $type);
$type = $relation['type'];
}
switch ($type)
{
case 'parent':
return $this->getParent($itemName);
break;
case 'child':
return $this->getChild($itemName);
break;
default:
throw new RuntimeException("Invalid relation type $type for returning a single related item", 500);
break;
}
}
/**
* Gets the iterator for the items referenced by a named relation. You can optionally specify the type. Only
* multiple item relation types will be searched.
*
* @param string $itemName The name of the relation to use
* @param string $type [optional] The relation type (children, multiple)
*
* @return F0FDatabaseIterator
*
* @throws RuntimeException If the named relation doesn't exist or isn't supposed to return single items
*/
public function getRelatedItems($itemName, $type = null)
{
if (empty($type))
{
$relation = $this->getRelation($itemName, $type);
$type = $relation['type'];
}
switch ($type)
{
case 'children':
return $this->getChildren($itemName);
break;
case 'multiple':
return $this->getMultiple($itemName);
break;
case 'siblings':
return $this->getSiblings($itemName);
break;
default:
throw new RuntimeException("Invalid relation type $type for returning a collection of related items", 500);
break;
}
}
/**
* Gets a parent item
*
* @param string $itemName [optional] The name of the relation to use, skip to use the default parent relation
*
* @return F0FTable
*
* @throws RuntimeException When the relation is not found
*/
public function getParent($itemName = null)
{
if (empty($itemName))
{
$itemName = $this->defaultRelation['parent'];
}
if (empty($itemName))
{
throw new RuntimeException(sprintf('Default parent relation for %s not found', $this->table->getTableName()), 500);
}
if (!isset($this->relations['parent'][$itemName]))
{
throw new RuntimeException(sprintf('Parent relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
}
return $this->getTableFromRelation($this->relations['parent'][$itemName]);
}
/**
* Gets a child item
*
* @param string $itemName [optional] The name of the relation to use, skip to use the default child relation
*
* @return F0FTable
*
* @throws RuntimeException When the relation is not found
*/
public function getChild($itemName = null)
{
if (empty($itemName))
{
$itemName = $this->defaultRelation['child'];
}
if (empty($itemName))
{
throw new RuntimeException(sprintf('Default child relation for %s not found', $this->table->getTableName()), 500);
}
if (!isset($this->relations['child'][$itemName]))
{
throw new RuntimeException(sprintf('Child relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
}
return $this->getTableFromRelation($this->relations['child'][$itemName]);
}
/**
* Gets an iterator for the children items
*
* @param string $itemName [optional] The name of the relation to use, skip to use the default children relation
*
* @return F0FDatabaseIterator
*
* @throws RuntimeException When the relation is not found
*/
public function getChildren($itemName = null)
{
if (empty($itemName))
{
$itemName = $this->defaultRelation['children'];
}
if (empty($itemName))
{
throw new RuntimeException(sprintf('Default children relation for %s not found', $this->table->getTableName()), 500);
}
if (!isset($this->relations['children'][$itemName]))
{
throw new RuntimeException(sprintf('Children relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
}
return $this->getIteratorFromRelation($this->relations['children'][$itemName]);
}
/**
* Gets an iterator for the sibling items. This relation is inferred from the parent relation. It returns all
* elements on the same table which have the same parent.
*
* @param string $itemName [optional] The name of the relation to use, skip to use the default children relation
*
* @return F0FDatabaseIterator
*
* @throws RuntimeException When the relation is not found
*/
public function getSiblings($itemName = null)
{
if (empty($itemName))
{
$itemName = $this->defaultRelation['parent'];
}
if (empty($itemName))
{
throw new RuntimeException(sprintf('Default siblings relation for %s not found', $this->table->getTableName()), 500);
}
if (!isset($this->relations['parent'][$itemName]))
{
throw new RuntimeException(sprintf('Sibling relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
}
// Get my table class
$tableName = $this->table->getTableName();
$tableName = str_replace('#__', '', $tableName);
$tableNameParts = explode('_', $tableName, 2);
$tableClass = ucfirst($tableNameParts[0]) . 'Table' . ucfirst(F0FInflector::singularize($tableNameParts[1]));
$parentRelation = $this->relations['parent'][$itemName];
$relation = array(
'tableClass' => $tableClass,
'localKey' => $parentRelation['localKey'],
'remoteKey' => $parentRelation['localKey'],
);
return $this->getIteratorFromRelation($relation);
}
/**
* Gets an iterator for the multiple items
*
* @param string $itemName [optional] The name of the relation to use, skip to use the default multiple relation
*
* @return F0FDatabaseIterator
*
* @throws RuntimeException When the relation is not found
*/
public function getMultiple($itemName = null)
{
if (empty($itemName))
{
$itemName = $this->defaultRelation['multiple'];
}
if (empty($itemName))
{
throw new RuntimeException(sprintf('Default multiple relation for %s not found', $this->table->getTableName()), 500);
}
if (!isset($this->relations['multiple'][$itemName]))
{
throw new RuntimeException(sprintf('Multiple relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
}
return $this->getIteratorFromRelation($this->relations['multiple'][$itemName]);
}
/**
* Returns a F0FTable object based on a given relation
*
* @param array $relation Indexed array holding relation definition.
* tableClass => name of the related table class
* localKey => name of the local key
* remoteKey => name of the remote key
*
* @return F0FTable
*
* @throws RuntimeException
* @throws InvalidArgumentException
*/
protected function getTableFromRelation($relation)
{
// Sanity checks
if(
!isset($relation['tableClass']) || !isset($relation['remoteKey']) || !isset($relation['localKey']) ||
!$relation['tableClass'] || !$relation['remoteKey'] || !$relation['localKey']
)
{
throw new InvalidArgumentException('Missing array index for the '.__METHOD__.' method. Please check method signature', 500);
}
// Get a table object from the table class name
$tableClass = $relation['tableClass'];
$tableClassParts = F0FInflector::explode($tableClass);
if(count($tableClassParts) < 3)
{
throw new InvalidArgumentException('Invalid table class named. It should be something like FooTableBar');
}
$table = F0FTable::getInstance($tableClassParts[2], ucfirst($tableClassParts[0]) . ucfirst($tableClassParts[1]));
// Get the table name
$tableName = $table->getTableName();
// Get the remote and local key names
$remoteKey = $relation['remoteKey'];
$localKey = $relation['localKey'];
// Get the local key's value
$value = $this->table->$localKey;
// If there's no value for the primary key, let's stop here
if(!$value)
{
throw new RuntimeException('Missing value for the primary key of the table '.$this->table->getTableName(), 500);
}
// This is required to prevent one relation from killing the db cursor used in a different relation...
$oldDb = $this->table->getDbo();
$oldDb->disconnect(); // YES, WE DO NEED TO DISCONNECT BEFORE WE CLONE THE DB OBJECT. ARGH!
$db = clone $oldDb;
$query = $db->getQuery(true)
->select('*')
->from($db->qn($tableName))
->where($db->qn($remoteKey) . ' = ' . $db->q($value));
$db->setQuery($query, 0, 1);
$data = $db->loadObject();
if (!is_object($data))
{
throw new RuntimeException(sprintf('Cannot load item from relation against table %s column %s', $tableName, $remoteKey), 500);
}
$table->bind($data);
return $table;
}
/**
* Returns a F0FDatabaseIterator based on a given relation
*
* @param array $relation Indexed array holding relation definition.
* tableClass => name of the related table class
* localKey => name of the local key
* remoteKey => name of the remote key
* pivotTable => name of the pivot table (optional)
* theirPivotKey => name of the remote key in the pivot table (mandatory if pivotTable is set)
* ourPivotKey => name of our key in the pivot table (mandatory if pivotTable is set)
*
* @return F0FDatabaseIterator
*
* @throws RuntimeException
* @throws InvalidArgumentException
*/
protected function getIteratorFromRelation($relation)
{
// Sanity checks
if(
!isset($relation['tableClass']) || !isset($relation['remoteKey']) || !isset($relation['localKey']) ||
!$relation['tableClass'] || !$relation['remoteKey'] || !$relation['localKey']
)
{
throw new InvalidArgumentException('Missing array index for the '.__METHOD__.' method. Please check method signature', 500);
}
if(array_key_exists('pivotTable', $relation))
{
if(
!isset($relation['theirPivotKey']) || !isset($relation['ourPivotKey']) ||
!$relation['pivotTable'] || !$relation['theirPivotKey'] || !$relation['ourPivotKey']
)
{
throw new InvalidArgumentException('Missing array index for the '.__METHOD__.' method. Please check method signature', 500);
}
}
// Get a table object from the table class name
$tableClass = $relation['tableClass'];
$tableClassParts = F0FInflector::explode($tableClass);
if(count($tableClassParts) < 3)
{
throw new InvalidArgumentException('Invalid table class named. It should be something like FooTableBar');
}
$table = F0FTable::getInstance($tableClassParts[2], ucfirst($tableClassParts[0]) . ucfirst($tableClassParts[1]));
// Get the table name
$tableName = $table->getTableName();
// Get the remote and local key names
$remoteKey = $relation['remoteKey'];
$localKey = $relation['localKey'];
// Get the local key's value
$value = $this->table->$localKey;
// If there's no value for the primary key, let's stop here
if(!$value)
{
throw new RuntimeException('Missing value for the primary key of the table '.$this->table->getTableName(), 500);
}
// This is required to prevent one relation from killing the db cursor used in a different relation...
$oldDb = $this->table->getDbo();
$oldDb->disconnect(); // YES, WE DO NEED TO DISCONNECT BEFORE WE CLONE THE DB OBJECT. ARGH!
$db = clone $oldDb;
// Begin the query
$query = $db->getQuery(true)
->select('*')
->from($db->qn($tableName));
// Do we have a pivot table?
$hasPivot = array_key_exists('pivotTable', $relation);
// If we don't have pivot it's a straightforward query
if (!$hasPivot)
{
$query->where($db->qn($remoteKey) . ' = ' . $db->q($value));
}
// If we have a pivot table we have to do a subquery
else
{
$subQuery = $db->getQuery(true)
->select($db->qn($relation['theirPivotKey']))
->from($db->qn($relation['pivotTable']))
->where($db->qn($relation['ourPivotKey']) . ' = ' . $db->q($value));
$query->where($db->qn($remoteKey) . ' IN (' . $subQuery . ')');
}
$db->setQuery($query);
$cursor = $db->execute();
$iterator = F0FDatabaseIterator::getIterator($db->name, $cursor, null, $tableClass);
return $iterator;
}
/**
* Add any bespoke relation which doesn't involve a pivot table.
*
* @param string $relationType The type of the relationship (parent, child, children)
* @param string $itemName is how it will be known locally to the getRelatedItems method
* @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
* @param string $localKey is the column containing our side of the FK relation, default: componentname_itemname_id
* @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
* @param boolean $default is this the default children relationship?
*
* @return void
*/
protected function addBespokeSimpleRelation($relationType, $itemName, $tableClass, $localKey, $remoteKey, $default)
{
$ourPivotKey = null;
$theirPivotKey = null;
$pivotTable = null;
$this->normaliseParameters(false, $itemName, $tableClass, $localKey, $remoteKey, $ourPivotKey, $theirPivotKey, $pivotTable);
$this->relations[$relationType][$itemName] = array(
'tableClass' => $tableClass,
'localKey' => $localKey,
'remoteKey' => $remoteKey,
);
if ($default)
{
$this->defaultRelation[$relationType] = $itemName;
}
}
/**
* Add any bespoke relation which involves a pivot table.
*
* @param string $relationType The type of the relationship (multiple)
* @param string $itemName is how it will be known locally to the getRelatedItems method
* @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
* @param string $localKey is the column containing our side of the FK relation, default: componentname_itemname_id
* @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
* @param string $ourPivotKey is the column containing our side of the FK relation in the pivot table, default: $localKey
* @param string $theirPivotKey is the column containing the other table's side of the FK relation in the pivot table, default $remoteKey
* @param string $pivotTable is the name of the glue (pivot) table, default: #__componentname_thisclassname_itemname with plural items (e.g. #__foobar_users_roles)
* @param boolean $default is this the default children relationship?
*
* @return void
*/
protected function addBespokePivotRelation($relationType, $itemName, $tableClass, $localKey, $remoteKey, $ourPivotKey, $theirPivotKey, $pivotTable, $default)
{
$this->normaliseParameters(true, $itemName, $tableClass, $localKey, $remoteKey, $ourPivotKey, $theirPivotKey, $pivotTable);
$this->relations[$relationType][$itemName] = array(
'tableClass' => $tableClass,
'localKey' => $localKey,
'remoteKey' => $remoteKey,
'ourPivotKey' => $ourPivotKey,
'theirPivotKey' => $theirPivotKey,
'pivotTable' => $pivotTable,
);
if ($default)
{
$this->defaultRelation[$relationType] = $itemName;
}
}
/**
* Normalise the parameters of a relation, guessing missing values
*
* @param boolean $pivot Is this a many to many relation involving a pivot table?
* @param string $itemName is how it will be known locally to the getRelatedItems method (plural)
* @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
* @param string $localKey is the column containing our side of the FK relation, default: componentname_itemname_id
* @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
* @param string $ourPivotKey is the column containing our side of the FK relation in the pivot table, default: $localKey
* @param string $theirPivotKey is the column containing the other table's side of the FK relation in the pivot table, default $remoteKey
* @param string $pivotTable is the name of the glue (pivot) table, default: #__componentname_thisclassname_itemname with plural items (e.g. #__foobar_users_roles)
*
* @return void
*/
protected function normaliseParameters($pivot = false, &$itemName, &$tableClass, &$localKey, &$remoteKey, &$ourPivotKey, &$theirPivotKey, &$pivotTable)
{
// Get a default table class if none is provided
if (empty($tableClass))
{
$tableClassParts = explode('_', $itemName, 3);
if (count($tableClassParts) == 1)
{
array_unshift($tableClassParts, $this->componentName);
}
if ($tableClassParts[0] == 'joomla')
{
$tableClassParts[0] = 'J';
}
$tableClass = ucfirst($tableClassParts[0]) . 'Table' . ucfirst(F0FInflector::singularize($tableClassParts[1]));
}
// Make sure we have both a local and remote key
if (empty($localKey) && empty($remoteKey))
{
// WARNING! If we have a pivot table, this behavior is wrong!
// Infact if we have `parts` and `groups` the local key should be foobar_part_id and the remote one foobar_group_id.
// However, this isn't a real issue because:
// 1. we have no way to detect the local key of a multiple relation
// 2. this scenario never happens, since, in this class, if we're adding a multiple relation we always supply the local key
$tableClassParts = F0FInflector::explode($tableClass);
$localKey = $tableClassParts[0] . '_' . $tableClassParts[2] . '_id';
$remoteKey = $localKey;
}
elseif (empty($localKey) && !empty($remoteKey))
{
$localKey = $remoteKey;
}
elseif (!empty($localKey) && empty($remoteKey))
{
if($pivot)
{
$tableClassParts = F0FInflector::explode($tableClass);
$remoteKey = $tableClassParts[0] . '_' . $tableClassParts[2] . '_id';
}
else
{
$remoteKey = $localKey;
}
}
// If we don't have a pivot table nullify the relevant variables and return
if (!$pivot)
{
$ourPivotKey = null;
$theirPivotKey = null;
$pivotTable = null;
return;
}
if (empty($ourPivotKey))
{
$ourPivotKey = $localKey;
}
if (empty($theirPivotKey))
{
$theirPivotKey = $remoteKey;
}
if (empty($pivotTable))
{
$pivotTable = '#__' . strtolower($this->componentName) . '_' .
strtolower(F0FInflector::pluralize($this->tableType)) . '_';
$itemNameParts = explode('_', $itemName);
$lastPart = array_pop($itemNameParts);
$pivotTable .= strtolower($lastPart);
}
}
/**
* Normalises the format of a relation name
*
* @param string $itemName The raw relation name
* @param boolean $pluralise Should I pluralise the name? If not, I will singularise it
*
* @return string The normalised relation key name
*/
protected function normaliseItemName($itemName, $pluralise = false)
{
// Explode the item name
$itemNameParts = explode('_', $itemName);
// If we have multiple parts the first part is considered to be the component name
if (count($itemNameParts) > 1)
{
$prefix = array_shift($itemNameParts);
}
else
{
$prefix = null;
}
// If we still have multiple parts we need to pluralise/singularise the last part and join everything in
// CamelCase format
if (count($itemNameParts) > 1)
{
$name = array_pop($itemNameParts);
$name = $pluralise ? F0FInflector::pluralize($name) : F0FInflector::singularize($name);
$itemNameParts[] = $name;
$itemName = F0FInflector::implode($itemNameParts);
}
// Otherwise we singularise/pluralise the remaining part
else
{
$name = array_pop($itemNameParts);
$itemName = $pluralise ? F0FInflector::pluralize($name) : F0FInflector::singularize($name);
}
if (!empty($prefix))
{
$itemName = $prefix . '_' . $itemName;
}
return $itemName;
}
} table.php 0000604 00000260752 15245540356 0006365 0 ustar 00 <?php
/**
* @package FrameworkOnFramework
* @subpackage table
* @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Protect from unauthorized access
defined('F0F_INCLUDED') or die;
/**
* Normally this shouldn't be required. Some PHP versions, however, seem to
* require this. Why? No idea whatsoever. If I remove it, F0F crashes on some
* hosts. Same PHP version on another host and no problem occurs. Any takers?
*/
if (class_exists('F0FTable', false))
{
return;
}
if (!interface_exists('JTableInterface', true))
{
interface JTableInterface {}
}
/**
* FrameworkOnFramework Table class. The Table is one part controller, one part
* model and one part data adapter. It's supposed to handle operations for single
* records.
*
* @package FrameworkOnFramework
* @since 1.0
*/
class F0FTable extends F0FUtilsObject implements JTableInterface
{
/**
* Cache array for instances
*
* @var array
*/
protected static $instances = array();
/**
* Include paths for searching for F0FTable classes.
*
* @var array
*/
protected static $_includePaths = array();
/**
* The configuration parameters array
*
* @var array
*/
protected $config = array();
/**
* Name of the database table to model.
*
* @var string
*/
protected $_tbl = '';
/**
* Name of the primary key field in the table.
*
* @var string
*/
protected $_tbl_key = '';
/**
* F0FDatabaseDriver object.
*
* @var F0FDatabaseDriver
*/
protected $_db;
/**
* Should rows be tracked as ACL assets?
*
* @var boolean
*/
protected $_trackAssets = false;
/**
* Does the resource support joomla tags?
*
* @var boolean
*/
protected $_has_tags = false;
/**
* The rules associated with this record.
*
* @var JAccessRules A JAccessRules object.
*/
protected $_rules;
/**
* Indicator that the tables have been locked.
*
* @var boolean
*/
protected $_locked = false;
/**
* If this is set to true, it triggers automatically plugin events for
* table actions
*
* @var boolean
*/
protected $_trigger_events = false;
/**
* Table alias used in queries
*
* @var string
*/
protected $_tableAlias = false;
/**
* Array with alias for "special" columns such as ordering, hits etc etc
*
* @var array
*/
protected $_columnAlias = array();
/**
* If set to true, it enabled automatic checks on fields based on columns properties
*
* @var boolean
*/
protected $_autoChecks = false;
/**
* Array with fields that should be skipped by automatic checks
*
* @var array
*/
protected $_skipChecks = array();
/**
* Does the table actually exist? We need that to avoid PHP notices on
* table-less views.
*
* @var boolean
*/
protected $_tableExists = true;
/**
* 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 = '';
/**
* The input data
*
* @var F0FInput
*/
protected $input = null;
/**
* Extended query including joins with other tables
*
* @var F0FDatabaseQuery
*/
protected $_queryJoin = null;
/**
* The prefix for the table class
*
* @var string
*/
protected $_tablePrefix = '';
/**
* The known fields for this table
*
* @var array
*/
protected $knownFields = array();
/**
* A list of table fields, keyed per table
*
* @var array
*/
protected static $tableFieldCache = array();
/**
* A list of tables in the database
*
* @var array
*/
protected static $tableCache = array();
/**
* An instance of F0FConfigProvider to provision configuration overrides
*
* @var F0FConfigProvider
*/
protected $configProvider = null;
/**
* F0FTableDispatcherBehavior for dealing with extra behaviors
*
* @var F0FTableDispatcherBehavior
*/
protected $tableDispatcher = null;
/**
* List of default behaviors to apply to the table
*
* @var array
*/
protected $default_behaviors = array('tags', 'assets');
/**
* The relations object of the table. It's lazy-loaded by getRelations().
*
* @var F0FTableRelations
*/
protected $_relations = null;
/**
* The configuration provider's key for this table, e.g. foobar.tables.bar for the #__foobar_bars table. This is set
* automatically by the constructor
*
* @var string
*/
protected $_configProviderKey = '';
/**
* The content type of the table. Required if using tags or content history behaviour
*
* @var string
*/
protected $contentType = null;
/**
* Returns a static object instance of a particular table type
*
* @param string $type The table name
* @param string $prefix The prefix of the table class
* @param array $config Optional configuration variables
*
* @return F0FTable
*/
public static function getInstance($type, $prefix = 'JTable', $config = array())
{
return self::getAnInstance($type, $prefix, $config);
}
/**
* Returns a static object instance of a particular table type
*
* @param string $type The table name
* @param string $prefix The prefix of the table class
* @param array $config Optional configuration variables
*
* @return F0FTable
*/
public static function &getAnInstance($type = null, $prefix = 'JTable', $config = array())
{
// Make sure $config is an array
if (is_object($config))
{
$config = (array) $config;
}
elseif (!is_array($config))
{
$config = array();
}
// Guess the component name
if (!array_key_exists('input', $config))
{
$config['input'] = new F0FInput;
}
if ($config['input'] instanceof F0FInput)
{
$tmpInput = $config['input'];
}
else
{
$tmpInput = new F0FInput($config['input']);
}
$option = $tmpInput->getCmd('option', '');
$tmpInput->set('option', $option);
$config['input'] = $tmpInput;
if (!in_array($prefix, array('Table', 'JTable')))
{
preg_match('/(.*)Table$/', $prefix, $m);
$option = 'com_' . strtolower($m[1]);
}
if (array_key_exists('option', $config))
{
$option = $config['option'];
}
$config['option'] = $option;
if (!array_key_exists('view', $config))
{
$config['view'] = $config['input']->getCmd('view', 'cpanel');
}
if (is_null($type))
{
if ($prefix == 'JTable')
{
$prefix = 'Table';
}
$type = $config['view'];
}
$type = preg_replace('/[^A-Z0-9_\.-]/i', '', $type);
$tableClass = $prefix . ucfirst($type);
$config['_table_type'] = $type;
$config['_table_class'] = $tableClass;
$configProvider = new F0FConfigProvider;
$configProviderKey = $option . '.views.' . F0FInflector::singularize($type) . '.config.';
if (!array_key_exists($tableClass, self::$instances))
{
if (!class_exists($tableClass))
{
$componentPaths = F0FPlatform::getInstance()->getComponentBaseDirs($config['option']);
$searchPaths = array(
$componentPaths['main'] . '/tables',
$componentPaths['admin'] . '/tables'
);
if (array_key_exists('tablepath', $config))
{
array_unshift($searchPaths, $config['tablepath']);
}
$altPath = $configProvider->get($configProviderKey . 'table_path', null);
if ($altPath)
{
array_unshift($searchPaths, $componentPaths['admin'] . '/' . $altPath);
}
$filesystem = F0FPlatform::getInstance()->getIntegrationObject('filesystem');
$path = $filesystem->pathFind(
$searchPaths, strtolower($type) . '.php'
);
if ($path)
{
require_once $path;
}
}
if (!class_exists($tableClass))
{
$tableClass = 'F0FTable';
}
$component = str_replace('com_', '', $config['option']);
$tbl_common = $component . '_';
if (!array_key_exists('tbl', $config))
{
$config['tbl'] = strtolower('#__' . $tbl_common . strtolower(F0FInflector::pluralize($type)));
}
$altTbl = $configProvider->get($configProviderKey . 'tbl', null);
if ($altTbl)
{
$config['tbl'] = $altTbl;
}
if (!array_key_exists('tbl_key', $config))
{
$keyName = F0FInflector::singularize($type);
$config['tbl_key'] = strtolower($tbl_common . $keyName . '_id');
}
$altTblKey = $configProvider->get($configProviderKey . 'tbl_key', null);
if ($altTblKey)
{
$config['tbl_key'] = $altTblKey;
}
if (!array_key_exists('db', $config))
{
$config['db'] = F0FPlatform::getInstance()->getDbo();
}
// Assign the correct table alias
if (array_key_exists('table_alias', $config))
{
$table_alias = $config['table_alias'];
}
else
{
$configProviderTableAliasKey = $option . '.tables.' . F0FInflector::singularize($type) . '.tablealias';
$table_alias = $configProvider->get($configProviderTableAliasKey, false );
}
// Can we use the F0F cache?
if (!array_key_exists('use_table_cache', $config))
{
$config['use_table_cache'] = F0FPlatform::getInstance()->isGlobalF0FCacheEnabled();
}
$alt_use_table_cache = $configProvider->get($configProviderKey . 'use_table_cache', null);
if (!is_null($alt_use_table_cache))
{
$config['use_table_cache'] = $alt_use_table_cache;
}
// Create a new table instance
$instance = new $tableClass($config['tbl'], $config['tbl_key'], $config['db'], $config);
$instance->setInput($tmpInput);
$instance->setTablePrefix($prefix);
$instance->setTableAlias($table_alias);
// Determine and set the asset key for this table
$assetKey = 'com_' . $component . '.' . strtolower(F0FInflector::singularize($type));
$assetKey = $configProvider->get($configProviderKey . 'asset_key', $assetKey);
$instance->setAssetKey($assetKey);
if (array_key_exists('trigger_events', $config))
{
$instance->setTriggerEvents($config['trigger_events']);
}
if (version_compare(JVERSION, '3.1', 'ge'))
{
if (array_key_exists('has_tags', $config))
{
$instance->setHasTags($config['has_tags']);
}
$altHasTags = $configProvider->get($configProviderKey . 'has_tags', null);
if ($altHasTags)
{
$instance->setHasTags($altHasTags);
}
}
else
{
$instance->setHasTags(false);
}
$configProviderFieldmapKey = $option . '.tables.' . F0FInflector::singularize($type) . '.field';
$aliases = $configProvider->get($configProviderFieldmapKey, $instance->_columnAlias);
$instance->_columnAlias = array_merge($instance->_columnAlias, $aliases);
self::$instances[$tableClass] = $instance;
}
return self::$instances[$tableClass];
}
/**
* Force an instance inside class cache. Setting arguments to null nukes all or part of the cache
*
* @param string|null $key TableClass to replace. Set it to null to nuke the entire cache
* @param F0FTable|null $instance Instance to replace. Set it to null to nuke $key instances
*
* @return bool Did I correctly switch the instance?
*/
public static function forceInstance($key = null, $instance = null)
{
if(is_null($key))
{
self::$instances = array();
return true;
}
elseif($key && isset(self::$instances[$key]))
{
// I'm forcing an instance, but it's not a F0FTable, abort! abort!
if(!$instance || ($instance && $instance instanceof F0FTable))
{
self::$instances[$key] = $instance;
return true;
}
}
return false;
}
/**
* Class Constructor.
*
* @param string $table Name of the database table to model.
* @param string $key Name of the primary key field in the table.
* @param F0FDatabaseDriver &$db Database driver
* @param array $config The configuration parameters array
*/
public function __construct($table, $key, &$db, $config = array())
{
$this->_tbl = $table;
$this->_tbl_key = $key;
$this->_db = $db;
// Make sure the use F0F cache information is in the config
if (!array_key_exists('use_table_cache', $config))
{
$config['use_table_cache'] = F0FPlatform::getInstance()->isGlobalF0FCacheEnabled();
}
$this->config = $config;
// Load the configuration provider
$this->configProvider = new F0FConfigProvider;
// Load the behavior dispatcher
$this->tableDispatcher = new F0FTableDispatcherBehavior;
// Initialise the table properties.
if ($fields = $this->getTableFields())
{
// Do I have anything joined?
$j_fields = $this->getQueryJoinFields();
if ($j_fields)
{
$fields = array_merge($fields, $j_fields);
}
$this->setKnownFields(array_keys($fields), true);
$this->reset();
}
else
{
$this->_tableExists = false;
}
// Get the input
if (array_key_exists('input', $config))
{
if ($config['input'] instanceof F0FInput)
{
$this->input = $config['input'];
}
else
{
$this->input = new F0FInput($config['input']);
}
}
else
{
$this->input = new F0FInput;
}
// Set the $name/$_name variable
$component = $this->input->getCmd('option', 'com_foobar');
if (array_key_exists('option', $config))
{
$component = $config['option'];
}
$this->input->set('option', $component);
// Apply table behaviors
$type = explode("_", $this->_tbl);
$type = $type[count($type) - 1];
$this->_configProviderKey = $component . '.tables.' . F0FInflector::singularize($type);
$configKey = $this->_configProviderKey . '.behaviors';
if (isset($config['behaviors']))
{
$behaviors = (array) $config['behaviors'];
}
elseif ($behaviors = $this->configProvider->get($configKey, null))
{
$behaviors = explode(',', $behaviors);
}
else
{
$behaviors = $this->default_behaviors;
}
if (is_array($behaviors) && count($behaviors))
{
foreach ($behaviors as $behavior)
{
$this->addBehavior($behavior);
}
}
// If we are tracking assets, make sure an access field exists and initially set the default.
$asset_id_field = $this->getColumnAlias('asset_id');
$access_field = $this->getColumnAlias('access');
if (in_array($asset_id_field, $this->getKnownFields()))
{
JLoader::import('joomla.access.rules');
$this->_trackAssets = true;
}
// If the access property exists, set the default.
if (in_array($access_field, $this->getKnownFields()))
{
$this->$access_field = (int) F0FPlatform::getInstance()->getConfig()->get('access');
}
$this->config = $config;
}
/**
* Replace the entire known fields array
*
* @param array $fields A simple array of known field names
* @param boolean $initialise Should we initialise variables to null?
*
* @return void
*/
public function setKnownFields($fields, $initialise = false)
{
$this->knownFields = $fields;
if ($initialise)
{
foreach ($this->knownFields as $field)
{
$this->$field = null;
}
}
}
/**
* Get the known fields array
*
* @return array
*/
public function getKnownFields()
{
return $this->knownFields;
}
/**
* Does the specified field exist?
*
* @param string $fieldName The field name to search (it's OK to use aliases)
*
* @return bool
*/
public function hasField($fieldName)
{
$search = $this->getColumnAlias($fieldName);
return in_array($search, $this->knownFields);
}
/**
* Add a field to the known fields array
*
* @param string $field The name of the field to add
* @param boolean $initialise Should we initialise the variable to null?
*
* @return void
*/
public function addKnownField($field, $initialise = false)
{
if (!in_array($field, $this->knownFields))
{
$this->knownFields[] = $field;
if ($initialise)
{
$this->$field = null;
}
}
}
/**
* Remove a field from the known fields array
*
* @param string $field The name of the field to remove
*
* @return void
*/
public function removeKnownField($field)
{
if (in_array($field, $this->knownFields))
{
$pos = array_search($field, $this->knownFields);
unset($this->knownFields[$pos]);
}
}
/**
* Adds a behavior to the table
*
* @param string $name The name of the behavior
* @param array $config Optional Behavior configuration
*
* @return boolean
*/
public function addBehavior($name, $config = array())
{
// First look for ComponentnameTableViewnameBehaviorName (e.g. FoobarTableItemsBehaviorTags)
if (isset($this->config['option']))
{
$option_name = str_replace('com_', '', $this->config['option']);
$behaviorClass = $this->config['_table_class'] . 'Behavior' . ucfirst(strtolower($name));
if (class_exists($behaviorClass))
{
$behavior = new $behaviorClass($this->tableDispatcher, $config);
return true;
}
// Then look for ComponentnameTableBehaviorName (e.g. FoobarTableBehaviorTags)
$option_name = str_replace('com_', '', $this->config['option']);
$behaviorClass = ucfirst($option_name) . 'TableBehavior' . ucfirst(strtolower($name));
if (class_exists($behaviorClass))
{
$behavior = new $behaviorClass($this->tableDispatcher, $config);
return true;
}
}
// Nothing found? Return false.
$behaviorClass = 'F0FTableBehavior' . ucfirst(strtolower($name));
if (class_exists($behaviorClass) && $this->tableDispatcher)
{
$behavior = new $behaviorClass($this->tableDispatcher, $config);
return true;
}
return false;
}
/**
* Sets the events trigger switch state
*
* @param boolean $newState The new state of the switch (what else could it be?)
*
* @return void
*/
public function setTriggerEvents($newState = false)
{
$this->_trigger_events = $newState ? true : false;
}
/**
* Gets the events trigger switch state
*
* @return boolean
*/
public function getTriggerEvents()
{
return $this->_trigger_events;
}
/**
* 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 = false;
// Tags are available only in 3.1+
if (version_compare(JVERSION, '3.1', 'ge'))
{
$this->_has_tags = $newState ? true : false;
}
}
/**
* Set the class prefix
*
* @param string $prefix The prefix
*/
public function setTablePrefix($prefix)
{
$this->_tablePrefix = $prefix;
}
/**
* Sets fields to be skipped from automatic checks.
*
* @param array/string $skip Fields to be skipped by automatic checks
*
* @return void
*/
public function setSkipChecks($skip)
{
$this->_skipChecks = (array) $skip;
}
/**
* Method to load a row from the database by primary key and bind the fields
* to the F0FTable instance properties.
*
* @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.
*
* @throws RuntimeException
* @throws UnexpectedValueException
*/
public function load($keys = null, $reset = true)
{
if (!$this->_tableExists)
{
$result = false;
return $this->onAfterLoad($result);
}
if (empty($keys))
{
// If empty, use the value of the current key
$keyName = $this->_tbl_key;
if (isset($this->$keyName))
{
$keyValue = $this->$keyName;
}
else
{
$keyValue = null;
}
// If empty primary key there's is no need to load anything
if (empty($keyValue))
{
$result = true;
return $this->onAfterLoad($result);
}
$keys = array($keyName => $keyValue);
}
elseif (!is_array($keys))
{
// Load by primary key.
$keys = array($this->_tbl_key => $keys);
}
if ($reset)
{
$this->reset();
}
// Initialise the query.
$query = $this->_db->getQuery(true);
$query->select($this->_tbl . '.*');
$query->from($this->_tbl);
// Joined fields are ok, since I initialized them in the constructor
$fields = $this->getKnownFields();
foreach ($keys as $field => $value)
{
// Check that $field is in the table.
if (!in_array($field, $fields))
{
throw new UnexpectedValueException(sprintf('Missing field in table %s : %s.', $this->_tbl, $field));
}
// Add the search tuple to the query.
$query->where($this->_db->qn($this->_tbl . '.' . $field) . ' = ' . $this->_db->q($value));
}
// Do I have any joined table?
$j_query = $this->getQueryJoin();
if ($j_query)
{
if ($j_query->select && $j_query->select->getElements())
{
//$query->select($this->normalizeSelectFields($j_query->select->getElements(), true));
$query->select($j_query->select->getElements());
}
if ($j_query->join)
{
foreach ($j_query->join as $join)
{
$t = (string) $join;
// Joomla doesn't provide any access to the "name" variable, so I have to work with strings...
if (stripos($t, 'inner') !== false)
{
$query->innerJoin($join->getElements());
}
elseif (stripos($t, 'left') !== false)
{
$query->leftJoin($join->getElements());
}
elseif (stripos($t, 'right') !== false)
{
$query->rightJoin($join->getElements());
}
elseif (stripos($t, 'outer') !== false)
{
$query->outerJoin($join->getElements());
}
}
}
}
$this->_db->setQuery($query);
$row = $this->_db->loadAssoc();
// Check that we have a result.
if (empty($row))
{
$result = false;
return $this->onAfterLoad($result);
}
// Bind the object with the row and return.
$result = $this->bind($row);
$this->onAfterLoad($result);
return $result;
}
/**
* Based on fields properties (nullable column), checks if the field is required or not
*
* @return boolean
*/
public function check()
{
if (!$this->_autoChecks)
{
return true;
}
$fields = $this->getTableFields();
// No fields? Why in the hell am I here?
if(!$fields)
{
return false;
}
$result = true;
$known = $this->getKnownFields();
$skipFields[] = $this->_tbl_key;
if(in_array($this->getColumnAlias('title'), $known)
&& in_array($this->getColumnAlias('slug'), $known)) $skipFields[] = $this->getColumnAlias('slug');
if(in_array($this->getColumnAlias('hits'), $known)) $skipFields[] = $this->getColumnAlias('hits');
if(in_array($this->getColumnAlias('created_on'), $known)) $skipFields[] = $this->getColumnAlias('created_on');
if(in_array($this->getColumnAlias('created_by'), $known)) $skipFields[] = $this->getColumnAlias('created_by');
if(in_array($this->getColumnAlias('modified_on'), $known)) $skipFields[] = $this->getColumnAlias('modified_on');
if(in_array($this->getColumnAlias('modified_by'), $known)) $skipFields[] = $this->getColumnAlias('modified_by');
if(in_array($this->getColumnAlias('locked_by'), $known)) $skipFields[] = $this->getColumnAlias('locked_by');
if(in_array($this->getColumnAlias('locked_on'), $known)) $skipFields[] = $this->getColumnAlias('locked_on');
// Let's merge it with custom skips
$skipFields = array_merge($skipFields, $this->_skipChecks);
foreach ($fields as $field)
{
$fieldName = $field->Field;
if (empty($fieldName))
{
$fieldName = $field->column_name;
}
// Field is not nullable but it's null, set error
if ($field->Null == 'NO' && $this->$fieldName == '' && !in_array($fieldName, $skipFields))
{
$text = str_replace('#__', 'COM_', $this->getTableName()) . '_ERR_' . $fieldName;
$this->setError(JText::_(strtoupper($text)));
$result = false;
}
}
return $result;
}
/**
* Method to reset class properties to the defaults set in the class
* definition. It will ignore the primary key as well as any private class
* properties.
*
* @return void
*/
public function reset()
{
if (!$this->onBeforeReset())
{
return false;
}
// Get the default values for the class from the table.
$fields = $this->getTableFields();
$j_fields = $this->getQueryJoinFields();
if ($j_fields)
{
$fields = array_merge($fields, $j_fields);
}
if (is_array($fields) && !empty($fields))
{
foreach ($fields as $k => $v)
{
// If the property is not the primary key or private, reset it.
if ($k != $this->_tbl_key && (strpos($k, '_') !== 0))
{
$this->$k = $v->Default;
}
}
if (!$this->onAfterReset())
{
return false;
}
}
}
/**
* Clones the current object, after resetting it
*
* @return static
*/
public function getClone()
{
$clone = clone $this;
$clone->reset();
$key = $this->getKeyName();
$clone->$key = null;
return $clone;
}
/**
* Generic check for whether dependencies exist for this object in the db schema
*
* @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 boolean True if the record can be deleted
*/
public function canDelete($oid = null, $joins = null)
{
$k = $this->_tbl_key;
if ($oid)
{
$this->$k = intval($oid);
}
if (is_array($joins))
{
$db = $this->_db;
$query = $db->getQuery(true)
->select($db->qn('master') . '.' . $db->qn($k))
->from($db->qn($this->_tbl) . ' AS ' . $db->qn('master'));
$tableNo = 0;
foreach ($joins as $table)
{
$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($k)
);
}
$query->where($db->qn('master') . '.' . $db->qn($k) . ' = ' . $db->q($this->$k));
$query->group($db->qn('master') . '.' . $db->qn($k));
$this->_db->setQuery((string) $query);
if (version_compare(JVERSION, '3.0', 'ge'))
{
try
{
$obj = $this->_db->loadObject();
}
catch (Exception $e)
{
$this->setError($e->getMessage());
}
}
else
{
if (!$obj = $this->_db->loadObject())
{
$this->setError($this->_db->getErrorMsg());
return false;
}
}
$msg = array();
$i = 0;
foreach ($joins as $table)
{
$k = $table['idalias'];
if ($obj->$k > 0)
{
$msg[] = JText::_($table['label']);
}
$i++;
}
if (count($msg))
{
$option = $this->input->getCmd('option', 'com_foobar');
$comName = str_replace('com_', '', $option);
$tview = str_replace('#__' . $comName . '_', '', $this->_tbl);
$prefix = $option . '_' . $tview . '_NODELETE_';
foreach ($msg as $key)
{
$this->setError(JText::_($prefix . $key));
}
return false;
}
else
{
return true;
}
}
return true;
}
/**
* Method to bind an associative array or object to the F0FTable instance.This
* method only binds properties that are publicly accessible and optionally
* takes an array of properties to ignore when binding.
*
* @param mixed $src An associative array or object to bind to the F0FTable instance.
* @param mixed $ignore An optional array or space separated list of properties to ignore while binding.
*
* @return boolean True on success.
*
* @throws InvalidArgumentException
*/
public function bind($src, $ignore = array())
{
if (!$this->onBeforeBind($src))
{
return false;
}
// If the source value is not an array or object return false.
if (!is_object($src) && !is_array($src))
{
throw new InvalidArgumentException(sprintf('%s::bind(*%s*)', get_class($this), gettype($src)));
}
// If the source value is an object, get its accessible properties.
if (is_object($src))
{
$src = get_object_vars($src);
}
// 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->getKnownFields() as $k)
{
// Only process fields not in the ignore array.
if (!in_array($k, $ignore))
{
if (isset($src[$k]))
{
$this->$k = $src[$k];
}
}
}
$result = $this->onAfterBind($src);
return $result;
}
/**
* Method to store a row in the database from the F0FTable instance properties.
* If a primary key value is set the row with that primary key value will be
* updated with the instance property values. If no primary key value is set
* a new row will be inserted into the database with the properties from the
* F0FTable instance.
*
* @param boolean $updateNulls True to update fields even if they are null.
*
* @return boolean True on success.
*/
public function store($updateNulls = false)
{
if (!$this->onBeforeStore($updateNulls))
{
return false;
}
$k = $this->_tbl_key;
if ($this->$k == 0)
{
$this->$k = null;
}
// Create the object used for inserting/updating data to the database
$fields = $this->getTableFields();
$properties = $this->getKnownFields();
$keys = array();
foreach ($properties as $property)
{
// 'input' property is a reserved name
if (isset($fields[$property]))
{
$keys[] = $property;
}
}
$updateObject = array();
foreach ($keys as $key)
{
$updateObject[$key] = $this->$key;
}
$updateObject = (object)$updateObject;
// If a primary key exists update the object, otherwise insert it.
if ($this->$k)
{
$result = $this->_db->updateObject($this->_tbl, $updateObject, $this->_tbl_key, $updateNulls);
}
else
{
$result = $this->_db->insertObject($this->_tbl, $updateObject, $this->_tbl_key);
}
if ($result !== true)
{
$this->setError($this->_db->getErrorMsg());
return false;
}
$this->bind($updateObject);
if ($this->_locked)
{
$this->_unlock();
}
$result = $this->onAfterStore();
return $result;
}
/**
* 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 mixed Boolean True on success.
*
* @throws UnexpectedValueException
*/
public function move($delta, $where = '')
{
if (!$this->onBeforeMove($delta, $where))
{
return false;
}
// If there is no ordering field set an error and return false.
$ordering_field = $this->getColumnAlias('ordering');
if (!in_array($ordering_field, $this->getKnownFields()))
{
throw new UnexpectedValueException(sprintf('%s does not support ordering.', $this->_tbl));
}
// If the change is none, do nothing.
if (empty($delta))
{
$result = $this->onAfterMove();
return $result;
}
$k = $this->_tbl_key;
$row = null;
$query = $this->_db->getQuery(true);
// If the table is not loaded, return false
if (empty($this->$k))
{
return false;
}
// Select the primary key and ordering values from the table.
$query->select(array($this->_db->qn($this->_tbl_key), $this->_db->qn($ordering_field)));
$query->from($this->_tbl);
// If the movement delta is negative move the row up.
if ($delta < 0)
{
$query->where($this->_db->qn($ordering_field) . ' < ' . $this->_db->q((int) $this->$ordering_field));
$query->order($this->_db->qn($ordering_field) . ' DESC');
}
// If the movement delta is positive move the row down.
elseif ($delta > 0)
{
$query->where($this->_db->qn($ordering_field) . ' > ' . $this->_db->q((int) $this->$ordering_field));
$query->order($this->_db->qn($ordering_field) . ' ASC');
}
// Add the custom WHERE clause if set.
if ($where)
{
$query->where($where);
}
// Select the first row with the criteria.
$this->_db->setQuery($query, 0, 1);
$row = $this->_db->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 = $this->_db->getQuery(true);
$query->update($this->_tbl);
$query->set($this->_db->qn($ordering_field) . ' = ' . $this->_db->q((int) $row->$ordering_field));
$query->where($this->_tbl_key . ' = ' . $this->_db->q($this->$k));
$this->_db->setQuery($query);
$this->_db->execute();
// Update the ordering field for the row to this instance's ordering value.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl);
$query->set($this->_db->qn($ordering_field) . ' = ' . $this->_db->q((int) $this->$ordering_field));
$query->where($this->_tbl_key . ' = ' . $this->_db->q($row->$k));
$this->_db->setQuery($query);
$this->_db->execute();
// Update the instance value.
$this->$ordering_field = $row->$ordering_field;
}
else
{
// Update the ordering field for this instance.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl);
$query->set($this->_db->qn($ordering_field) . ' = ' . $this->_db->q((int) $this->$ordering_field));
$query->where($this->_tbl_key . ' = ' . $this->_db->q($this->$k));
$this->_db->setQuery($query);
$this->_db->execute();
}
$result = $this->onAfterMove();
return $result;
}
/**
* Change the ordering of the records of the table
*
* @param string $where The WHERE clause of the SQL used to fetch the order
*
* @return boolean True is successful
*
* @throws UnexpectedValueException
*/
public function reorder($where = '')
{
if (!$this->onBeforeReorder($where))
{
return false;
}
// If there is no ordering field set an error and return false.
$order_field = $this->getColumnAlias('ordering');
if (!in_array($order_field, $this->getKnownFields()))
{
throw new UnexpectedValueException(sprintf('%s does not support ordering.', $this->_tbl_key));
}
$k = $this->_tbl_key;
// Get the primary keys and ordering values for the selection.
$query = $this->_db->getQuery(true);
$query->select($this->_tbl_key . ', ' . $this->_db->qn($order_field));
$query->from($this->_tbl);
$query->where($this->_db->qn($order_field) . ' >= ' . $this->_db->q(0));
$query->order($this->_db->qn($order_field));
// Setup the extra where and ordering clause data.
if ($where)
{
$query->where($where);
}
$this->_db->setQuery($query);
$rows = $this->_db->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 = $this->_db->getQuery(true);
$query->update($this->_tbl);
$query->set($this->_db->qn($order_field) . ' = ' . $this->_db->q($i + 1));
$query->where($this->_tbl_key . ' = ' . $this->_db->q($row->$k));
$this->_db->setQuery($query);
$this->_db->execute();
}
}
}
$result = $this->onAfterReorder();
return $result;
}
/**
* Check out (lock) a record
*
* @param integer $userId The locking user's ID
* @param integer $oid The primary key value of the record to lock
*
* @return boolean True on success
*/
public function checkout($userId, $oid = null)
{
$fldLockedBy = $this->getColumnAlias('locked_by');
$fldLockedOn = $this->getColumnAlias('locked_on');
if (!(in_array($fldLockedBy, $this->getKnownFields())
|| in_array($fldLockedOn, $this->getKnownFields())))
{
return true;
}
$k = $this->_tbl_key;
if ($oid !== null)
{
$this->$k = $oid;
}
// No primary key defined, stop here
if (!$this->$k)
{
return false;
}
$date = F0FPlatform::getInstance()->getDate();
if (method_exists($date, 'toSql'))
{
$time = $date->toSql();
}
else
{
$time = $date->toMySQL();
}
$query = $this->_db->getQuery(true)
->update($this->_db->qn($this->_tbl))
->set(
array(
$this->_db->qn($fldLockedBy) . ' = ' . $this->_db->q((int) $userId),
$this->_db->qn($fldLockedOn) . ' = ' . $this->_db->q($time)
)
)
->where($this->_db->qn($this->_tbl_key) . ' = ' . $this->_db->q($this->$k));
$this->_db->setQuery((string) $query);
$this->$fldLockedBy = $userId;
$this->$fldLockedOn = $time;
return $this->_db->execute();
}
/**
* Check in (unlock) a record
*
* @param integer $oid The primary key value of the record to unlock
*
* @return boolean True on success
*/
public function checkin($oid = null)
{
$fldLockedBy = $this->getColumnAlias('locked_by');
$fldLockedOn = $this->getColumnAlias('locked_on');
if (!(in_array($fldLockedBy, $this->getKnownFields())
|| in_array($fldLockedOn, $this->getKnownFields())))
{
return true;
}
$k = $this->_tbl_key;
if ($oid !== null)
{
$this->$k = $oid;
}
if ($this->$k == null)
{
return false;
}
$query = $this->_db->getQuery(true)
->update($this->_db->qn($this->_tbl))
->set(
array(
$this->_db->qn($fldLockedBy) . ' = 0',
$this->_db->qn($fldLockedOn) . ' = ' . $this->_db->q($this->_db->getNullDate())
)
)
->where($this->_db->qn($this->_tbl_key) . ' = ' . $this->_db->q($this->$k));
$this->_db->setQuery((string) $query);
$this->$fldLockedBy = 0;
$this->$fldLockedOn = '';
return $this->_db->execute();
}
/**
* Is a record locked?
*
* @param integer $with The userid to preform the match with. If an item is checked
* out by this user the function will return false.
* @param integer $unused_against Junk inherited from JTable; ignore
*
* @throws UnexpectedValueException
*
* @return boolean True if the record is locked by another user
*/
public function isCheckedOut($with = 0, $unused_against = null)
{
$against = null;
$fldLockedBy = $this->getColumnAlias('locked_by');
$k = $this->_tbl_key;
// If no primary key is given, return false.
if ($this->$k === null)
{
throw new UnexpectedValueException('Null primary key not allowed.');
}
if (isset($this) && is_a($this, 'F0FTable') && !$against)
{
$against = $this->get($fldLockedBy);
}
// Item is not checked out, or being checked out by the same user
if (!$against || $against == $with)
{
return false;
}
$session = JTable::getInstance('session');
return $session->exists($against);
}
/**
* Copy (duplicate) one or more records
*
* @param integer|array $cid The primary key value (or values) or the record(s) to copy
*
* @return boolean True on success
*/
public function copy($cid = null)
{
//We have to cast the id as array, or the helper function will return an empty set
if($cid)
{
$cid = (array) $cid;
}
F0FUtilsArray::toInteger($cid);
$k = $this->_tbl_key;
if (count($cid) < 1)
{
if ($this->$k)
{
$cid = array($this->$k);
}
else
{
$this->setError("No items selected.");
return false;
}
}
$created_by = $this->getColumnAlias('created_by');
$created_on = $this->getColumnAlias('created_on');
$modified_by = $this->getColumnAlias('modified_by');
$modified_on = $this->getColumnAlias('modified_on');
$locked_byName = $this->getColumnAlias('locked_by');
$checkin = in_array($locked_byName, $this->getKnownFields());
foreach ($cid as $item)
{
// Prevent load with id = 0
if (!$item)
{
continue;
}
$this->load($item);
if ($checkin)
{
// We're using the checkin and the record is used by someone else
if ($this->isCheckedOut($item))
{
continue;
}
}
if (!$this->onBeforeCopy($item))
{
continue;
}
$this->$k = null;
$this->$created_by = null;
$this->$created_on = null;
$this->$modified_on = null;
$this->$modified_by = null;
// Let's fire the event only if everything is ok
if ($this->store())
{
$this->onAfterCopy($item);
}
$this->reset();
}
return true;
}
/**
* Publish or unpublish records
*
* @param integer|array $cid The primary key value(s) of the item(s) to publish/unpublish
* @param integer $publish 1 to publish an item, 0 to unpublish
* @param integer $user_id The user ID of the user (un)publishing the item.
*
* @return boolean True on success, false on failure (e.g. record is locked)
*/
public function publish($cid = null, $publish = 1, $user_id = 0)
{
$enabledName = $this->getColumnAlias('enabled');
$locked_byName = $this->getColumnAlias('locked_by');
// Mhm... you called the publish method on a table without publish support...
if(!in_array($enabledName, $this->getKnownFields()))
{
return false;
}
//We have to cast the id as array, or the helper function will return an empty set
if($cid)
{
$cid = (array) $cid;
}
F0FUtilsArray::toInteger($cid);
$user_id = (int) $user_id;
$publish = (int) $publish;
$k = $this->_tbl_key;
if (count($cid) < 1)
{
if ($this->$k)
{
$cid = array($this->$k);
}
else
{
$this->setError("No items selected.");
return false;
}
}
if (!$this->onBeforePublish($cid, $publish))
{
return false;
}
$query = $this->_db->getQuery(true)
->update($this->_db->qn($this->_tbl))
->set($this->_db->qn($enabledName) . ' = ' . (int) $publish);
$checkin = in_array($locked_byName, $this->getKnownFields());
if ($checkin)
{
$query->where(
' (' . $this->_db->qn($locked_byName) .
' = 0 OR ' . $this->_db->qn($locked_byName) . ' = ' . (int) $user_id . ')', 'AND'
);
}
// TODO Rewrite this statment using IN. Check if it work in SQLServer and PostgreSQL
$cids = $this->_db->qn($k) . ' = ' . implode(' OR ' . $this->_db->qn($k) . ' = ', $cid);
$query->where('(' . $cids . ')');
$this->_db->setQuery((string) $query);
if (version_compare(JVERSION, '3.0', 'ge'))
{
try
{
$this->_db->execute();
}
catch (Exception $e)
{
$this->setError($e->getMessage());
}
}
else
{
if (!$this->_db->execute())
{
$this->setError($this->_db->getErrorMsg());
return false;
}
}
if (count($cid) == 1 && $checkin)
{
if ($this->_db->getAffectedRows() == 1)
{
$this->checkin($cid[0]);
if ($this->$k == $cid[0])
{
$this->$enabledName = $publish;
}
}
}
$this->setError('');
return true;
}
/**
* Delete a record
*
* @param integer $oid The primary key value of the item to delete
*
* @throws UnexpectedValueException
*
* @return boolean True on success
*/
public function delete($oid = null)
{
if ($oid)
{
$this->load($oid);
}
$k = $this->_tbl_key;
$pk = (!$oid) ? $this->$k : $oid;
// If no primary key is given, return false.
if (!$pk)
{
throw new UnexpectedValueException('Null primary key not allowed.');
}
// Execute the logic only if I have a primary key, otherwise I could have weird results
if (!$this->onBeforeDelete($oid))
{
return false;
}
// Delete the row by primary key.
$query = $this->_db->getQuery(true);
$query->delete();
$query->from($this->_tbl);
$query->where($this->_tbl_key . ' = ' . $this->_db->q($pk));
$this->_db->setQuery($query);
$this->_db->execute();
$result = $this->onAfterDelete($oid);
return $result;
}
/**
* Register a hit on a record
*
* @param integer $oid The primary key value of the record
* @param boolean $log Should I log the hit?
*
* @return boolean True on success
*/
public function hit($oid = null, $log = false)
{
if (!$this->onBeforeHit($oid, $log))
{
return false;
}
// If there is no hits field, just return true.
$hits_field = $this->getColumnAlias('hits');
if (!in_array($hits_field, $this->getKnownFields()))
{
return true;
}
$k = $this->_tbl_key;
$pk = ($oid) ? $oid : $this->$k;
// If no primary key is given, return false.
if (!$pk)
{
$result = false;
}
else
{
// Check the row in by primary key.
$query = $this->_db->getQuery(true)
->update($this->_tbl)
->set($this->_db->qn($hits_field) . ' = (' . $this->_db->qn($hits_field) . ' + 1)')
->where($this->_tbl_key . ' = ' . $this->_db->q($pk));
$this->_db->setQuery($query)->execute();
// In order to update the table object, I have to load the table
if(!$this->$k)
{
$query = $this->_db->getQuery(true)
->select($this->_db->qn($hits_field))
->from($this->_db->qn($this->_tbl))
->where($this->_db->qn($this->_tbl_key) . ' = ' . $this->_db->q($pk));
$this->$hits_field = $this->_db->setQuery($query)->loadResult();
}
else
{
// Set table values in the object.
$this->$hits_field++;
}
$result = true;
}
if ($result)
{
$result = $this->onAfterHit($oid);
}
return $result;
}
/**
* Export the item as a CSV line
*
* @param string $separator CSV separator. Tip: use "\t" to get a TSV file instead.
*
* @return string The CSV line
*/
public function toCSV($separator = ',')
{
$csv = array();
foreach (get_object_vars($this) as $k => $v)
{
if (!in_array($k, $this->getKnownFields()))
{
continue;
}
$csv[] = '"' . str_replace('"', '""', $v) . '"';
}
$csv = implode($separator, $csv);
return $csv;
}
/**
* Exports the table in array format
*
* @return array
*/
public function getData()
{
$ret = array();
foreach (get_object_vars($this) as $k => $v)
{
if (!in_array($k, $this->getKnownFields()))
{
continue;
}
$ret[$k] = $v;
}
return $ret;
}
/**
* Get the header for exporting item list to CSV
*
* @param string $separator CSV separator. Tip: use "\t" to get a TSV file instead.
*
* @return string The CSV file's header
*/
public function getCSVHeader($separator = ',')
{
$csv = array();
foreach (get_object_vars($this) as $k => $v)
{
if (!in_array($k, $this->getKnownFields()))
{
continue;
}
$csv[] = '"' . str_replace('"', '\"', $k) . '"';
}
$csv = implode($separator, $csv);
return $csv;
}
/**
* 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)
{
// Should I load the cached data?
$useCache = array_key_exists('use_table_cache', $this->config) ? $this->config['use_table_cache'] : false;
// Make sure we have a list of tables in this db
if (empty(self::$tableCache))
{
if ($useCache)
{
// Try to load table cache from a cache file
$cacheData = F0FPlatform::getInstance()->getCache('tables', null);
// Unserialise the cached data, or set the table cache to empty
// if the cache data wasn't loaded.
if (!is_null($cacheData))
{
self::$tableCache = json_decode($cacheData, true);
}
else
{
self::$tableCache = array();
}
}
// This check is true if the cache data doesn't exist / is not loaded
if (empty(self::$tableCache))
{
self::$tableCache = $this->_db->getTableList();
if ($useCache)
{
F0FPlatform::getInstance()->setCache('tables', json_encode(self::$tableCache));
}
}
}
// Make sure the cached table fields cache is loaded
if (empty(self::$tableFieldCache))
{
if ($useCache)
{
// Try to load table cache from a cache file
$cacheData = F0FPlatform::getInstance()->getCache('tablefields', null);
// Unserialise the cached data, or set to empty if the cache
// data wasn't loaded.
if (!is_null($cacheData))
{
$decoded = json_decode($cacheData, true);
$tableCache = array();
if (count($decoded))
{
foreach ($decoded as $myTableName => $tableFields)
{
$temp = array();
if (is_array($tableFields))
{
foreach($tableFields as $field => $def)
{
$temp[$field] = (object)$def;
}
$tableCache[$myTableName] = $temp;
}
elseif (is_object($tableFields) || is_bool($tableFields))
{
$tableCache[$myTableName] = $tableFields;
}
}
}
self::$tableFieldCache = $tableCache;
}
else
{
self::$tableFieldCache = array();
}
}
}
if (!$tableName)
{
$tableName = $this->_tbl;
}
// 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, self::$tableFieldCache) ||
(isset(self::$tableFieldCache[$tableName]) && !self::$tableFieldCache[$tableName]))
{
// Lookup the fields for this table only once.
$name = $tableName;
$prefix = $this->_db->getPrefix();
if (substr($name, 0, 3) == '#__')
{
$checkName = $prefix . substr($name, 3);
}
else
{
$checkName = $name;
}
if (!in_array($checkName, self::$tableCache))
{
// The table doesn't exist. Return false.
self::$tableFieldCache[$tableName] = false;
}
elseif (version_compare(JVERSION, '3.0', 'ge'))
{
$fields = $this->_db->getTableColumns($name, false);
if (empty($fields))
{
$fields = false;
}
self::$tableFieldCache[$tableName] = $fields;
}
else
{
$fields = $this->_db->getTableFields($name, false);
if (!isset($fields[$name]))
{
$fields = false;
}
self::$tableFieldCache[$tableName] = $fields[$name];
}
// PostgreSQL date type compatibility
if (($this->_db->name == 'postgresql') && (self::$tableFieldCache[$tableName] != false))
{
foreach (self::$tableFieldCache[$tableName] as $field)
{
if (strtolower($field->type) == 'timestamp without time zone')
{
if (stristr($field->Default, '\'::timestamp without time zone'))
{
list ($date, $junk) = explode('::', $field->Default, 2);
$field->Default = trim($date, "'");
}
}
}
}
// Save the data for this table into the cache
if ($useCache)
{
$cacheData = F0FPlatform::getInstance()->setCache('tablefields', json_encode(self::$tableFieldCache));
}
}
return self::$tableFieldCache[$tableName];
}
public function getTableAlias()
{
return $this->_tableAlias;
}
public function setTableAlias($string)
{
$string = preg_replace('#[^A-Z0-9_]#i', '', $string);
$this->_tableAlias = $string;
}
/**
* Method to return the real name of a "special" column such as ordering, hits, published
* etc etc. In this way you are free to follow your db naming convention and use the
* built in Joomla functions.
*
* @param string $column Name of the "special" column (ie ordering, hits etc etc)
*
* @return string The string that identify the special
*/
public function getColumnAlias($column)
{
if (isset($this->_columnAlias[$column]))
{
$return = $this->_columnAlias[$column];
}
else
{
$return = $column;
}
$return = preg_replace('#[^A-Z0-9_]#i', '', $return);
return $return;
}
/**
* Method to register a column alias for a "special" column.
*
* @param string $column The "special" column (ie ordering)
* @param string $columnAlias The real column name (ie foo_ordering)
*
* @return void
*/
public function setColumnAlias($column, $columnAlias)
{
$column = strtolower($column);
$column = preg_replace('#[^A-Z0-9_]#i', '', $column);
$this->_columnAlias[$column] = $columnAlias;
}
/**
* Get a JOIN query, used to join other tables
*
* @param boolean $asReference Return an object reference instead of a copy
*
* @return F0FDatabaseQuery Query used to join other tables
*/
public function getQueryJoin($asReference = false)
{
if ($asReference)
{
return $this->_queryJoin;
}
else
{
if ($this->_queryJoin)
{
return clone $this->_queryJoin;
}
else
{
return null;
}
}
}
/**
* Sets the query with joins to other tables
*
* @param F0FDatabaseQuery $query The JOIN query to use
*
* @return void
*/
public function setQueryJoin($query)
{
$this->_queryJoin = $query;
}
/**
* Extracts the fields from the join query
*
* @return array Fields contained in the join query
*/
protected function getQueryJoinFields()
{
$query = $this->getQueryJoin();
if (!$query)
{
return array();
}
$tables = array();
$j_tables = array();
$j_fields = array();
// Get joined tables. Ignore FROM clause, since it should not be used (the starting point is the table "table")
$joins = $query->join;
foreach ($joins as $join)
{
$tables = array_merge($tables, $join->getElements());
}
// Clean up table names
foreach($tables as $table)
{
preg_match('#(.*)((\w)*(on|using))(.*)#i', $table, $matches);
if($matches && isset($matches[1]))
{
// I always want the first part, no matter what
$parts = explode(' ', $matches[1]);
$t_table = $parts[0];
if($this->isQuoted($t_table))
{
$t_table = substr($t_table, 1, strlen($t_table) - 2);
}
if(!in_array($t_table, $j_tables))
{
$j_tables[] = $t_table;
}
}
}
// Do I have the current table inside the query join? Remove it (its fields are already ok)
$find = array_search($this->getTableName(), $j_tables);
if($find !== false)
{
unset($j_tables[$find]);
}
// Get table fields
$fields = array();
foreach ($j_tables as $table)
{
$t_fields = $this->getTableFields($table);
if ($t_fields)
{
$fields = array_merge($fields, $t_fields);
}
}
// Remove any fields that aren't in the joined select
$j_select = $query->select;
if ($j_select && $j_select->getElements())
{
$j_fields = $this->normalizeSelectFields($j_select->getElements());
}
// I can intesect the keys
$fields = array_intersect_key($fields, $j_fields);
// Now I walk again the array to change the key of columns that have an alias
foreach ($j_fields as $column => $alias)
{
if ($column != $alias)
{
$fields[$alias] = $fields[$column];
unset($fields[$column]);
}
}
return $fields;
}
/**
* Normalizes the fields, returning an associative array with all the fields.
* Ie array('foobar as foo, bar') becomes array('foobar' => 'foo', 'bar' => 'bar')
*
* @param array $fields Array with column fields
*
* @return array Normalized array
*/
protected function normalizeSelectFields($fields)
{
$db = F0FPlatform::getInstance()->getDbo();
$return = array();
foreach ($fields as $field)
{
$t_fields = explode(',', $field);
foreach ($t_fields as $t_field)
{
// Is there any alias?
$parts = preg_split('#\sas\s#i', $t_field);
// Do I have a table.column situation? Let's get the field name
$tableField = explode('.', $parts[0]);
if(isset($tableField[1]))
{
$column = trim($tableField[1]);
}
else
{
$column = trim($tableField[0]);
}
// Is this field quoted? If so, remove the quotes
if($this->isQuoted($column))
{
$column = substr($column, 1, strlen($column) - 2);
}
if(isset($parts[1]))
{
$alias = trim($parts[1]);
// Is this field quoted? If so, remove the quotes
if($this->isQuoted($alias))
{
$alias = substr($alias, 1, strlen($alias) - 2);
}
}
else
{
$alias = $column;
}
$return[$column] = $alias;
}
}
return $return;
}
/**
* Is the field quoted?
*
* @param string $column Column field
*
* @return bool Is the field quoted?
*/
protected function isQuoted($column)
{
// Empty string, un-quoted by definition
if(!$column)
{
return false;
}
// I need some "magic". If the first char is not a letter, a number
// an underscore or # (needed for table), then most likely the field is quoted
preg_match_all('/^[a-z0-9_#]/i', $column, $matches);
if(!$matches[0])
{
return true;
}
return false;
}
/**
* The event which runs before binding data to the table
*
* NOTE TO 3RD PARTY DEVELOPERS:
*
* When you override the following methods in your child classes,
* be sure to call parent::method *AFTER* your code, otherwise the
* plugin events do NOT get triggered
*
* Example:
* protected function onBeforeBind(){
* // Your code here
* return parent::onBeforeBind() && $your_result;
* }
*
* Do not do it the other way around, e.g. return $your_result && parent::onBeforeBind()
* Due to PHP short-circuit boolean evaluation the parent::onBeforeBind()
* will not be called if $your_result is false.
*
* @param object|array &$from The data to bind
*
* @return boolean True on success
*/
protected function onBeforeBind(&$from)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforeBind', array(&$this, &$from));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforeBind' . ucfirst($name), array(&$this, &$from));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs after loading a record from the database
*
* @param boolean &$result Did the load succeeded?
*
* @return void
*/
protected function onAfterLoad(&$result)
{
// Call the behaviors
$eventResult = $this->tableDispatcher->trigger('onAfterLoad', array(&$this, &$result));
if (in_array(false, $eventResult, true))
{
// Behavior failed, return false
$result = false;
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
F0FPlatform::getInstance()->runPlugins('onAfterLoad' . ucfirst($name), array(&$this, &$result));
}
}
/**
* The event which runs before storing (saving) data to the database
*
* @param boolean $updateNulls Should nulls be saved as nulls (true) or just skipped over (false)?
*
* @return boolean True to allow saving
*/
protected function onBeforeStore($updateNulls)
{
// Do we have a "Created" set of fields?
$created_on = $this->getColumnAlias('created_on');
$created_by = $this->getColumnAlias('created_by');
$modified_on = $this->getColumnAlias('modified_on');
$modified_by = $this->getColumnAlias('modified_by');
$locked_on = $this->getColumnAlias('locked_on');
$locked_by = $this->getColumnAlias('locked_by');
$title = $this->getColumnAlias('title');
$slug = $this->getColumnAlias('slug');
$hasCreatedOn = in_array($created_on, $this->getKnownFields());
$hasCreatedBy = in_array($created_by, $this->getKnownFields());
if ($hasCreatedOn && $hasCreatedBy)
{
$hasModifiedOn = in_array($modified_on, $this->getKnownFields());
$hasModifiedBy = in_array($modified_by, $this->getKnownFields());
$nullDate = $this->_db->getNullDate();
if (empty($this->$created_by) || ($this->$created_on == $nullDate) || empty($this->$created_on))
{
$uid = F0FPlatform::getInstance()->getUser()->id;
if ($uid)
{
$this->$created_by = F0FPlatform::getInstance()->getUser()->id;
}
$date = F0FPlatform::getInstance()->getDate('now', null, false);
$this->$created_on = method_exists($date, 'toSql') ? $date->toSql() : $date->toMySQL();
}
elseif ($hasModifiedOn && $hasModifiedBy)
{
$uid = F0FPlatform::getInstance()->getUser()->id;
if ($uid)
{
$this->$modified_by = F0FPlatform::getInstance()->getUser()->id;
}
$date = F0FPlatform::getInstance()->getDate('now', null, false);
$this->$modified_on = method_exists($date, 'toSql') ? $date->toSql() : $date->toMySQL();
}
}
// Do we have a set of title and slug fields?
$hasTitle = in_array($title, $this->getKnownFields());
$hasSlug = in_array($slug, $this->getKnownFields());
if ($hasTitle && $hasSlug)
{
if (empty($this->$slug))
{
// Create a slug from the title
$this->$slug = F0FStringUtils::toSlug($this->$title);
}
else
{
// Filter the slug for invalid characters
$this->$slug = F0FStringUtils::toSlug($this->$slug);
}
// Make sure we don't have a duplicate slug on this table
$db = $this->getDbo();
$query = $db->getQuery(true)
->select($db->qn($slug))
->from($this->_tbl)
->where($db->qn($slug) . ' = ' . $db->q($this->$slug))
->where('NOT ' . $db->qn($this->_tbl_key) . ' = ' . $db->q($this->{$this->_tbl_key}));
$db->setQuery($query);
$existingItems = $db->loadAssocList();
$count = 0;
$newSlug = $this->$slug;
while (!empty($existingItems))
{
$count++;
$newSlug = $this->$slug . '-' . $count;
$query = $db->getQuery(true)
->select($db->qn($slug))
->from($this->_tbl)
->where($db->qn($slug) . ' = ' . $db->q($newSlug))
->where('NOT '. $db->qn($this->_tbl_key) . ' = ' . $db->q($this->{$this->_tbl_key}));
$db->setQuery($query);
$existingItems = $db->loadAssocList();
}
$this->$slug = $newSlug;
}
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforeStore', array(&$this, $updateNulls));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
// Execute onBeforeStore<tablename> events in loaded plugins
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforeStore' . ucfirst($name), array(&$this, $updateNulls));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs after binding data to the class
*
* @param object|array &$src The data to bind
*
* @return boolean True to allow binding without an error
*/
protected function onAfterBind(&$src)
{
// Call the behaviors
$options = array(
'component' => $this->input->get('option'),
'view' => $this->input->get('view'),
'table_prefix' => $this->_tablePrefix
);
$result = $this->tableDispatcher->trigger('onAfterBind', array(&$this, &$src, $options));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onAfterBind' . ucfirst($name), array(&$this, &$src));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs after storing (saving) data to the database
*
* @return boolean True to allow saving without an error
*/
protected function onAfterStore()
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onAfterStore', array(&$this));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onAfterStore' . ucfirst($name), array(&$this));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs before moving a record
*
* @param boolean $updateNulls Should nulls be saved as nulls (true) or just skipped over (false)?
*
* @return boolean True to allow moving
*/
protected function onBeforeMove($updateNulls)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforeMove', array(&$this, $updateNulls));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforeMove' . ucfirst($name), array(&$this, $updateNulls));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs after moving a record
*
* @return boolean True to allow moving without an error
*/
protected function onAfterMove()
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onAfterMove', array(&$this));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onAfterMove' . ucfirst($name), array(&$this));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs before reordering a table
*
* @param string $where The WHERE clause of the SQL query to run on reordering (record filter)
*
* @return boolean True to allow reordering
*/
protected function onBeforeReorder($where = '')
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforeReorder', array(&$this, $where));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforeReorder' . ucfirst($name), array(&$this, $where));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs after reordering a table
*
* @return boolean True to allow the reordering to complete without an error
*/
protected function onAfterReorder()
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onAfterReorder', array(&$this));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onAfterReorder' . ucfirst($name), array(&$this));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs before deleting a record
*
* @param integer $oid The PK value of the record to delete
*
* @return boolean True to allow the deletion
*/
protected function onBeforeDelete($oid)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforeDelete', array(&$this, $oid));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforeDelete' . ucfirst($name), array(&$this, $oid));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs after deleting a record
*
* @param integer $oid The PK value of the record which was deleted
*
* @return boolean True to allow the deletion without errors
*/
protected function onAfterDelete($oid)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onAfterDelete', array(&$this, $oid));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onAfterDelete' . ucfirst($name), array(&$this, $oid));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs before hitting a record
*
* @param integer $oid The PK value of the record to hit
* @param boolean $log Should we log the hit?
*
* @return boolean True to allow the hit
*/
protected function onBeforeHit($oid, $log)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforeHit', array(&$this, $oid, $log));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforeHit' . ucfirst($name), array(&$this, $oid, $log));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs after hitting a record
*
* @param integer $oid The PK value of the record which was hit
*
* @return boolean True to allow the hitting without errors
*/
protected function onAfterHit($oid)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onAfterHit', array(&$this, $oid));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onAfterHit' . ucfirst($name), array(&$this, $oid));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The even which runs before copying a record
*
* @param integer $oid The PK value of the record being copied
*
* @return boolean True to allow the copy to take place
*/
protected function onBeforeCopy($oid)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforeCopy', array(&$this, $oid));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforeCopy' . ucfirst($name), array(&$this, $oid));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The even which runs after copying a record
*
* @param integer $oid The PK value of the record which was copied (not the new one)
*
* @return boolean True to allow the copy without errors
*/
protected function onAfterCopy($oid)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onAfterCopy', array(&$this, $oid));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onAfterCopy' . ucfirst($name), array(&$this, $oid));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs before a record is (un)published
*
* @param integer|array &$cid The PK IDs of the records being (un)published
* @param integer $publish 1 to publish, 0 to unpublish
*
* @return boolean True to allow the (un)publish to proceed
*/
protected function onBeforePublish(&$cid, $publish)
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforePublish', array(&$this, &$cid, $publish));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforePublish' . ucfirst($name), array(&$this, &$cid, $publish));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The event which runs after the object is reset to its default values.
*
* @return boolean True to allow the reset to complete without errors
*/
protected function onAfterReset()
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onAfterReset', array(&$this));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onAfterReset' . ucfirst($name), array(&$this));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* The even which runs before the object is reset to its default values.
*
* @return boolean True to allow the reset to complete
*/
protected function onBeforeReset()
{
// Call the behaviors
$result = $this->tableDispatcher->trigger('onBeforeReset', array(&$this));
if (in_array(false, $result, true))
{
// Behavior failed, return false
return false;
}
if ($this->_trigger_events)
{
$name = F0FInflector::pluralize($this->getKeyName());
$result = F0FPlatform::getInstance()->runPlugins('onBeforeReset' . ucfirst($name), array(&$this));
if (in_array(false, $result, true))
{
return false;
}
else
{
return true;
}
}
return true;
}
/**
* Replace the input object of this table with the provided F0FInput object
*
* @param F0FInput $input The new input object
*
* @return void
*/
public function setInput(F0FInput $input)
{
$this->input = $input;
}
/**
* Get the columns from database table.
*
* @return mixed An array of the field names, or false if an error occurs.
*
* @deprecated 2.1
*/
public function getFields()
{
return $this->getTableFields();
}
/**
* Add a filesystem path where F0FTable should search for table class files.
* You may either pass a string or an array of paths.
*
* @param mixed $path A filesystem path or array of filesystem paths to add.
*
* @return array An array of filesystem paths to find F0FTable classes in.
*/
public static function addIncludePath($path = null)
{
// If the internal paths have not been initialised, do so with the base table path.
if (empty(self::$_includePaths))
{
self::$_includePaths = array(__DIR__);
}
// Convert the passed path(s) to add to an array.
settype($path, 'array');
// If we have new paths to add, do so.
if (!empty($path) && !in_array($path, self::$_includePaths))
{
// Check and add each individual new path.
foreach ($path as $dir)
{
// Sanitize path.
$dir = trim($dir);
// Add to the front of the list so that custom paths are searched first.
array_unshift(self::$_includePaths, $dir);
}
}
return self::$_includePaths;
}
/**
* 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 UnexpectedValueException
*
* @return string
*/
public function getAssetName()
{
$k = $this->_tbl_key;
// If there is no assetKey defined, stop here, or we'll get a wrong name
if(!$this->_assetKey || !$this->$k)
{
throw new UnexpectedValueException('Table must have an asset key defined and a value for the table id in order to track assets');
}
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.
*
* @throws UnexpectedValueException
*
* @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.
*/
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 F0FTable $table A F0FTable object for the asset parent.
* @param integer $id Id to look up
*
* @return integer
*/
public function getAssetParentId($table = null, $id = null)
{
// 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 get the database table name for the class.
*
* @return string The name of the database table being modeled.
*/
public function getTableName()
{
return $this->_tbl;
}
/**
* Method to get the primary key field name for the table.
*
* @return string The name of the primary key for the table.
*/
public function getKeyName()
{
return $this->_tbl_key;
}
/**
* Returns the identity value of this record
*
* @return mixed
*/
public function getId()
{
$key = $this->getKeyName();
return $this->$key;
}
/**
* Method to get the F0FDatabaseDriver object.
*
* @return F0FDatabaseDriver The internal database driver object.
*/
public function getDbo()
{
return $this->_db;
}
/**
* Method to set the F0FDatabaseDriver object.
*
* @param F0FDatabaseDriver $db A F0FDatabaseDriver object to be used by the table object.
*
* @return boolean True on success.
*/
public function setDBO($db)
{
$this->_db = $db;
return true;
}
/**
* 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;
}
/**
* Method to provide a shortcut to binding, checking and storing a F0FTable
* instance to the database table. The method will check a row in once the
* data has been stored and if an ordering filter is present will attempt to
* reorder the table rows based on the filter. The ordering filter is an instance
* property name. The rows that will be reordered are those whose value matches
* the F0FTable instance for the property specified.
*
* @param mixed $src An associative array or object to bind to the F0FTable instance.
* @param string $orderingFilter Filter for the order updating
* @param mixed $ignore An optional array or space separated list of properties
* to ignore while binding.
*
* @return boolean True on success.
*/
public function save($src, $orderingFilter = '', $ignore = '')
{
// Attempt to bind the source to the instance.
if (!$this->bind($src, $ignore))
{
return false;
}
// Run any sanity checks on the instance and verify that it is ready for storage.
if (!$this->check())
{
return false;
}
// Attempt to store the properties to the database table.
if (!$this->store())
{
return false;
}
// Attempt to check the row in, just in case it was checked out.
if (!$this->checkin())
{
return false;
}
// 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 ? $this->_db->qn($orderingFilter) . ' = ' . $this->_db->q($filterValue) : '');
}
// Set the error to empty and return true.
$this->setError('');
return true;
}
/**
* Method to get the next ordering value for a group of rows defined by an SQL WHERE clause.
* This is useful for placing a new item last in a group of items in the table.
*
* @param string $where WHERE clause to use for selecting the MAX(ordering) for the table.
*
* @return mixed Boolean false an failure or the next ordering value as an integer.
*/
public function getNextOrder($where = '')
{
// If there is no ordering field set an error and return false.
$ordering = $this->getColumnAlias('ordering');
if (!in_array($ordering, $this->getKnownFields()))
{
throw new UnexpectedValueException(sprintf('%s does not support ordering.', get_class($this)));
}
// Get the largest ordering value for a given where clause.
$query = $this->_db->getQuery(true);
$query->select('MAX('.$this->_db->qn($ordering).')');
$query->from($this->_tbl);
if ($where)
{
$query->where($where);
}
$this->_db->setQuery($query);
$max = (int) $this->_db->loadResult();
// Return the largest ordering value + 1.
return ($max + 1);
}
/**
* Method to lock the database table for writing.
*
* @return boolean True on success.
*
* @throws RuntimeException
*/
protected function _lock()
{
$this->_db->lockTable($this->_tbl);
$this->_locked = true;
return true;
}
/**
* Method to unlock the database table for writing.
*
* @return boolean True on success.
*/
protected function _unlock()
{
$this->_db->unlockTables();
$this->_locked = false;
return true;
}
public function setConfig(array $config)
{
$this->config = $config;
}
/**
* Get the content type for ucm
*
* @return string The content type alias
*/
public function getContentType()
{
if ($this->contentType)
{
return $this->contentType;
}
/**
* When tags was first introduced contentType variable didn't exist - so we guess one
* This will fail if content history behvaiour is enabled. This code is deprecated
* and will be removed in F0F 3.0 in favour of the content type class variable
*/
$component = $this->input->get('option');
$view = F0FInflector::singularize($this->input->get('view'));
$alias = $component . '.' . $view;
return $alias;
}
/**
* Returns the table relations object of the current table, lazy-loading it if necessary
*
* @return F0FTableRelations
*/
public function getRelations()
{
if (is_null($this->_relations))
{
$this->_relations = new F0FTableRelations($this);
}
return $this->_relations;
}
/**
* Gets a reference to the configuration parameters provider for this table
*
* @return F0FConfigProvider
*/
public function getConfigProvider()
{
return $this->configProvider;
}
/**
* Returns the configuration parameters provider's key for this table
*
* @return string
*/
public function getConfigProviderKey()
{
return $this->_configProviderKey;
}
/**
* 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->_tablePrefix,
'class' => 'F0FTable',
'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")
{
$alias = $this->getColumnAlias($alias);
if (in_array($alias, $this->getKnownFields()))
{
return $alias;
}
return $null;
}
}
plugin.min.js 0000604 00000134026 15245611277 0007176 0 ustar 00 !function(e,t){"use strict";function n(e,t){for(var n,r=[],a=0;a<e.length;++a){if(n=o[e[a]]||i(e[a]),!n)throw"module definition dependecy not found: "+e[a];r.push(n)}t.apply(null,r)}function r(e,r,i){if("string"!=typeof e)throw"invalid module definition, module id must be defined and be a string";if(r===t)throw"invalid module definition, dependencies must be specified";if(i===t)throw"invalid module definition, definition function must be specified";n(r,function(){o[e]=i.apply(null,arguments)})}function i(t){for(var n=e,r=t.split(/[.\/]/),i=0;i<r.length;++i){if(!n[r[i]])return;n=n[r[i]]}return n}var o={};r("tinymce/tableplugin/Utils",["tinymce/Env"],function(e){function t(t){(!e.ie||e.ie>9)&&(t.hasChildNodes()||(t.innerHTML='<br data-mce-bogus="1" />'))}var n=function(e){return function(t,n){t&&(n=parseInt(n,10),1===n||0===n?t.removeAttribute(e,1):t.setAttribute(e,n,1))}},r=function(e){return function(t){return parseInt(t.getAttribute(e)||1,10)}};return{setColSpan:n("colSpan"),setRowSpan:n("rowspan"),getColSpan:r("colSpan"),getRowSpan:r("rowSpan"),setSpanVal:function(e,t,r){n(t)(e,r)},getSpanVal:function(e,t){return r(t)(e)},paddCell:t}}),r("tinymce/tableplugin/SplitCols",["tinymce/util/Tools","tinymce/tableplugin/Utils"],function(e,t){var n=function(e,t,n){return e[n]?e[n][t]:null},r=function(e,t,r){var i=n(e,t,r);return i?i.elm:null},i=function(e,t,i,o){var a,s,l=0,c=r(e,t,i);for(a=i;(o>0?a<e.length:a>=0)&&(s=n(e,t,a),c===s.elm);a+=o)l++;return l},o=function(e,t,n){for(var r,i=e[n],o=t;o<i.length;o++)if(r=i[o],r.real)return r.elm;return null},a=function(e,n){for(var r,o=[],a=e[n],s=0;s<a.length;s++)r=a[s],o.push({elm:r.elm,above:i(e,s,n,-1)-1,below:i(e,s,n,1)-1}),s+=t.getColSpan(r.elm)-1;return o},s=function(e,n){var r=e.elm.ownerDocument,i=r.createElement("td");return t.setColSpan(i,t.getColSpan(e.elm)),t.setRowSpan(i,n),t.paddCell(i),i},l=function(e,t,n,r){var i=o(e,n+1,r);i?i.parentNode.insertBefore(t,i):(i=o(e,0,r),i.parentNode.appendChild(t))},c=function(e,n,r,i){if(0!==n.above){t.setRowSpan(n.elm,n.above);var o=s(n,n.below+1);return l(e,o,r,i),o}return null},u=function(e,n,r,i){if(0!==n.below){t.setRowSpan(n.elm,n.above+1);var o=s(n,n.below);return l(e,o,r,i+1),o}return null},d=function(t,n,i,o){var s=a(t,i),l=r(t,n,i).parentNode,d=[];return e.each(s,function(e,n){var r=o?c(t,e,n,i):u(t,e,n,i);null!==r&&d.push(d)}),{cells:d,row:l}};return{splitAt:d}}),r("tinymce/tableplugin/TableGrid",["tinymce/util/Tools","tinymce/Env","tinymce/tableplugin/Utils","tinymce/tableplugin/SplitCols"],function(e,n,r,i){var o=e.each,a=r.getSpanVal,s=r.setSpanVal;return function(l,c,u){function d(){l.$("td[data-mce-selected],th[data-mce-selected]").removeAttr("data-mce-selected")}function f(e){return e===l.getBody()}function p(t,n){return t?(n=e.map(n.split(","),function(e){return e.toLowerCase()}),e.grep(t.childNodes,function(t){return e.inArray(n,t.nodeName.toLowerCase())!==-1})):[]}function m(){var e=0;Z=[],Q=0,o(["thead","tbody","tfoot"],function(t){var n=p(c,t)[0],r=p(n,"tr");o(r,function(n,r){r+=e,o(p(n,"td,th"),function(e,n){var i,o,s,l;if(Z[r])for(;Z[r][n];)n++;for(s=a(e,"rowspan"),l=a(e,"colspan"),o=r;o<r+s;o++)for(Z[o]||(Z[o]=[]),i=n;i<n+l;i++)Z[o][i]={part:t,real:o==r&&i==n,elm:e,rowspan:s,colspan:l};Q=Math.max(Q,n+1)})}),e+=r.length})}function g(e){return l.fire("newrow",{node:e}),e}function h(e){return l.fire("newcell",{node:e}),e}function v(e,t){return e=e.cloneNode(t),e.removeAttribute("id"),e}function b(e,t){var n;if(n=Z[t])return n[e]}function y(e,t){return e[t]?e[t]:null}function x(e,t){for(var n=[],r=0;r<e.length;r++)n.push(b(t,r));return n}function C(e){return e&&(!!re.getAttrib(e.elm,"data-mce-selected")||e==u)}function w(){var e=[];return o(c.rows,function(t){o(t.cells,function(n){if(re.getAttrib(n,"data-mce-selected")||u&&n==u.elm)return e.push(t),!1})}),e}function N(){var e=0;return o(Z,function(t){if(o(t,function(t){C(t)&&e++}),e)return!1}),e}function k(){var e=re.createRng();f(c)||(e.setStartAfter(c),e.setEndAfter(c),ne.setRng(e),re.remove(c))}function S(t){var i,a={};return l.settings.table_clone_elements!==!1&&(a=e.makeMap((l.settings.table_clone_elements||"strong em b i span font h1 h2 h3 h4 h5 h6 p div").toUpperCase(),/[ ,]/)),e.walk(t,function(e){var r;if(3==e.nodeType)return o(re.getParents(e.parentNode,null,t).reverse(),function(e){a[e.nodeName]&&(e=v(e,!1),i?r&&r.appendChild(e):i=r=e,r=e)}),r&&(r.innerHTML=n.ie&&n.ie<10?" ":'<br data-mce-bogus="1" />'),!1},"childNodes"),t=v(t,!1),h(t),s(t,"rowSpan",1),s(t,"colSpan",1),i?t.appendChild(i):r.paddCell(t),t}function _(){var e,t=re.createRng();return o(re.select("tr",c),function(e){0===e.cells.length&&re.remove(e)}),0===re.select("tr",c).length?(t.setStartBefore(c),t.setEndBefore(c),ne.setRng(t),void re.remove(c)):(o(re.select("thead,tbody,tfoot",c),function(e){0===e.rows.length&&re.remove(e)}),m(),void(ee&&(e=Z[Math.min(Z.length-1,ee.y)],e&&(ne.select(e[Math.min(e.length-1,ee.x)].elm,!0),ne.collapse(!0)))))}function E(e,t,n,r){var i,o,a,s,l;for(i=Z[t][e].elm.parentNode,a=1;a<=n;a++)if(i=re.getNext(i,"tr")){for(o=e;o>=0;o--)if(l=Z[t+a][o].elm,l.parentNode==i){for(s=1;s<=r;s++)re.insertAfter(S(l),l);break}if(o==-1)for(s=1;s<=r;s++)i.insertBefore(S(i.cells[0]),i.cells[0])}}function T(){o(Z,function(e,t){o(e,function(e,n){var r,i,o;if(C(e)&&(e=e.elm,r=a(e,"colspan"),i=a(e,"rowspan"),r>1||i>1)){for(s(e,"rowSpan",1),s(e,"colSpan",1),o=0;o<r-1;o++)re.insertAfter(S(e),e);E(n,t,i-1,r)}})})}function R(e,t,n){for(var r=[],i=0;i<e.length;i++)(i<t||i>n)&&r.push(e[i]);return r}function A(t){return e.grep(t,function(e){return e.real===!1})}function B(e){for(var t=[],n=0;n<e.length;n++){var r=e[n].elm;t[t.length-1]!==r&&t.push(r)}return t}function P(t,n,i,o,a){var s=0;if(a-i<1)return 0;for(var l=i+1;l<=a;l++){var c=R(y(t,l),n,o),u=A(c);c.length===u.length&&(e.each(B(u),function(e){r.setRowSpan(e,r.getRowSpan(e)-1)}),s++)}return s}function D(t,n,i,o,a){var s=0;if(o-n<1)return 0;for(var l=n+1;l<=o;l++){var c=R(x(t,l),i,a),u=A(c);c.length===u.length&&(e.each(B(u),function(e){r.setColSpan(e,r.getColSpan(e)-1)}),s++)}return s}function M(t,n,r){var i,a,l,c,u,d,f,p,g,h,v,y,x;if(t?(i=q(t),a=i.x,l=i.y,c=a+(n-1),u=l+(r-1)):(ee=te=null,o(Z,function(e,t){o(e,function(e,n){C(e)&&(ee||(ee={x:n,y:t}),te={x:n,y:t})})}),ee&&(a=ee.x,l=ee.y,c=te.x,u=te.y)),p=b(a,l),g=b(c,u),p&&g&&p.part==g.part){T(),m(),y=P(Z,a,l,c,u),x=D(Z,a,l,c,u),p=b(a,l).elm;var w=c-a-x+1,N=u-l-y+1;for(w===Q&&N===Z.length&&(w=1,N=1),w===Q&&N>1&&(N=1),s(p,"colSpan",w),s(p,"rowSpan",N),f=l;f<=u;f++)for(d=a;d<=c;d++)Z[f]&&Z[f][d]&&(t=Z[f][d].elm,t!=p&&(h=e.grep(t.childNodes),o(h,function(e){p.appendChild(e)}),h.length&&(h=e.grep(p.childNodes),v=0,o(h,function(e){"BR"==e.nodeName&&v++<h.length-1&&p.removeChild(e)})),re.remove(t)));_()}}function L(e){var n,r,i,l,c,u,d,f,p,m;if(o(Z,function(r,i){if(o(r,function(t){if(C(t)&&(t=t.elm,c=t.parentNode,u=g(v(c,!1)),n=i,e))return!1}),e)return n===t}),n!==t){for(l=0,m=0;l<Z[0].length;l+=m)if(Z[n][l]&&(r=Z[n][l].elm,m=a(r,"colspan"),r!=i)){if(e){if(n>0&&Z[n-1][l]&&(f=Z[n-1][l].elm,p=a(f,"rowSpan"),p>1)){s(f,"rowSpan",p+1);continue}}else if(p=a(r,"rowspan"),p>1){s(r,"rowSpan",p+1);continue}d=S(r),s(d,"colSpan",r.colSpan),u.appendChild(d),i=r}u.hasChildNodes()&&(e?c.parentNode.insertBefore(u,c):re.insertAfter(u,c))}}function I(e,t){t=t||w().length||1;for(var n=0;n<t;n++)L(e)}function O(e){var n,r;o(Z,function(r){if(o(r,function(t,r){if(C(t)&&(n=r,e))return!1}),e)return n===t}),o(Z,function(t,i){var o,l,c;t[n]&&(o=t[n].elm,o!=r&&(c=a(o,"colspan"),l=a(o,"rowspan"),1==c?e?(o.parentNode.insertBefore(S(o),o),E(n,i,l-1,c)):(re.insertAfter(S(o),o),E(n,i,l-1,c)):s(o,"colSpan",o.colSpan+1),r=o))})}function H(e,t){t=t||N()||1;for(var n=0;n<t;n++)O(e)}function F(t){return e.grep(z(t),C)}function z(e){var t=[];return o(e,function(e){o(e,function(e){t.push(e)})}),t}function W(){var t=[];if(f(c)){if(1==Z[0].length)return;if(F(Z).length==z(Z).length)return}o(Z,function(n){o(n,function(n,r){C(n)&&e.inArray(t,r)===-1&&(o(Z,function(e){var t,n=e[r].elm;t=a(n,"colSpan"),t>1?s(n,"colSpan",t-1):re.remove(n)}),t.push(r))})}),_()}function U(){function e(e){var t,n;o(e.cells,function(e){var n=a(e,"rowSpan");n>1&&(s(e,"rowSpan",n-1),t=q(e),E(t.x,t.y,1,1))}),t=q(e.cells[0]),o(Z[t.y],function(e){var t;e=e.elm,e!=n&&(t=a(e,"rowSpan"),t<=1?re.remove(e):s(e,"rowSpan",t-1),n=e)})}var t;t=w(),f(c)&&t.length==c.rows.length||(o(t.reverse(),function(t){e(t)}),_())}function V(){var e=w();if(!f(c)||e.length!=c.rows.length)return re.remove(e),_(),e}function $(){var e=w();return o(e,function(t,n){e[n]=v(t,!0)}),e}function j(t,n){var r,a,l;t&&(r=i.splitAt(Z,ee.x,ee.y,n),a=r.row,e.each(r.cells,h),l=e.map(t,function(e){return e.cloneNode(!0)}),n||l.reverse(),o(l,function(e){var t,r,i=e.cells.length;for(g(e),t=0;t<i;t++)r=e.cells[t],h(r),s(r,"colSpan",1),s(r,"rowSpan",1);for(t=i;t<Q;t++)e.appendChild(h(S(e.cells[i-1])));for(t=Q;t<i;t++)re.remove(e.cells[t]);n?a.parentNode.insertBefore(e,a):re.insertAfter(e,a)}),d())}function q(e){var t;return o(Z,function(n,r){return o(n,function(n,i){if(n.elm==e)return t={x:i,y:r},!1}),!t}),t}function Y(e){ee=q(e)}function X(){var e,t;return e=t=0,o(Z,function(n,r){o(n,function(n,i){var o,a;C(n)&&(n=Z[r][i],i>e&&(e=i),r>t&&(t=r),n.real&&(o=n.colspan-1,a=n.rowspan-1,o&&i+o>e&&(e=i+o),a&&r+a>t&&(t=r+a)))})}),{x:e,y:t}}function K(e){var t,n,r,i,o,a,s,l,c,u;if(te=q(e),ee&&te){for(t=Math.min(ee.x,te.x),n=Math.min(ee.y,te.y),r=Math.max(ee.x,te.x),i=Math.max(ee.y,te.y),o=r,a=i,u=n;u<=i;u++)for(c=t;c<=r;c++)e=Z[u][c],e.real&&(s=e.colspan-1,l=e.rowspan-1,s&&c+s>o&&(o=c+s),l&&u+l>a&&(a=u+l));for(d(),u=n;u<=a;u++)for(c=t;c<=o;c++)Z[u][c]&&re.setAttrib(Z[u][c].elm,"data-mce-selected","1")}}function G(e,t){var n,r,i;n=q(e),r=n.y*Q+n.x;do{if(r+=t,i=b(r%Q,Math.floor(r/Q)),!i)break;if(i.elm!=e)return ne.select(i.elm,!0),re.isEmpty(i.elm)&&ne.collapse(!0),!0}while(i.elm==e);return!1}function J(t){if(ee){var n=i.splitAt(Z,ee.x,ee.y,t);e.each(n.cells,h)}}var Z,Q,ee,te,ne=l.selection,re=ne.dom;c=c||re.getParent(ne.getStart(!0),"table"),m(),u=u||re.getParent(ne.getStart(!0),"th,td"),u&&(ee=q(u),te=X(),u=b(ee.x,ee.y)),e.extend(this,{deleteTable:k,split:T,merge:M,insertRow:L,insertRows:I,insertCol:O,insertCols:H,splitCols:J,deleteCols:W,deleteRows:U,cutRows:V,copyRows:$,pasteRows:j,getPos:q,setStartCell:Y,setEndCell:K,moveRelIdx:G,refresh:m})}}),r("tinymce/tableplugin/Quirks",["tinymce/util/VK","tinymce/util/Delay","tinymce/Env","tinymce/util/Tools","tinymce/tableplugin/Utils"],function(e,t,n,r,i){var o=r.each,a=i.getSpanVal;return function(s){function l(){function n(n){function r(e,t){var r=e?"previousSibling":"nextSibling",o=s.dom.getParent(t,"tr"),a=o[r];if(a)return v(s,t,a,e),n.preventDefault(),!0;var l=s.dom.getParent(o,"table"),d=o.parentNode,f=d.nodeName.toLowerCase();if("tbody"===f||f===(e?"tfoot":"thead")){var p=i(e,l,d,"tbody");if(null!==p)return c(e,p,t)}return u(e,o,r,l)}function i(e,t,n,r){var i=s.dom.select(">"+r,t),o=i.indexOf(n);if(e&&0===o||!e&&o===i.length-1)return l(e,t);if(o===-1){var a="thead"===n.tagName.toLowerCase()?0:i.length-1;return i[a]}return i[o+(e?-1:1)]}function l(e,t){var n=e?"thead":"tfoot",r=s.dom.select(">"+n,t);return 0!==r.length?r[0]:null}function c(e,t,r){var i=d(t,e);return i&&v(s,r,i,e),n.preventDefault(),!0}function u(e,t,i,o){var a=o[i];if(a)return f(a),!0;var l=s.dom.getParent(o,"td,th");if(l)return r(e,l,n);var c=d(t,!e);return f(c),n.preventDefault(),!1}function d(e,t){var n=e&&e[t?"lastChild":"firstChild"];return n&&"BR"===n.nodeName?s.dom.getParent(n,"td,th"):n}function f(e){s.selection.setCursorLocation(e,0)}function p(){return x==e.UP||x==e.DOWN}function m(e){var t=e.selection.getNode(),n=e.dom.getParent(t,"tr");return null!==n}function g(e){for(var t=0,n=e;n.previousSibling;)n=n.previousSibling,t+=a(n,"colspan");return t}function h(e,t){var n=0,r=0;return o(e.children,function(e,i){if(n+=a(e,"colspan"),r=i,n>t)return!1}),r}function v(e,t,n,r){var i=g(s.dom.getParent(t,"td,th")),o=h(n,i),a=n.childNodes[o],l=d(a,r);f(l||a)}function b(e){var t=s.selection.getNode(),n=s.dom.getParent(t,"td,th"),r=s.dom.getParent(e,"td,th");return n&&n!==r&&y(n,r)}function y(e,t){return s.dom.getParent(e,"TABLE")===s.dom.getParent(t,"TABLE")}var x=n.keyCode;if(p()&&m(s)){var C=s.selection.getNode();t.setEditorTimeout(s,function(){b(C)&&r(!n.shiftKey&&x===e.UP,C,n)},0)}}s.on("KeyDown",function(e){n(e)})}function c(){function e(e,t){var n,r=t.ownerDocument,i=r.createRange();return i.setStartBefore(t),i.setEnd(e.endContainer,e.endOffset),n=r.createElement("body"),n.appendChild(i.cloneContents()),0===n.innerHTML.replace(/<(br|img|object|embed|input|textarea)[^>]*>/gi,"-").replace(/<[^>]+>/g,"").length}s.on("KeyDown",function(t){var n,r,i=s.dom;37!=t.keyCode&&38!=t.keyCode||(n=s.selection.getRng(),r=i.getParent(n.startContainer,"table"),r&&s.getBody().firstChild==r&&e(n,r)&&(n=i.createRng(),n.setStartBefore(r),n.setEndBefore(r),s.selection.setRng(n),t.preventDefault()))})}function u(){s.on("KeyDown SetContent VisualAid",function(){var e;for(e=s.getBody().lastChild;e;e=e.previousSibling)if(3==e.nodeType){if(e.nodeValue.length>0)break}else if(1==e.nodeType&&("BR"==e.tagName||!e.getAttribute("data-mce-bogus")))break;e&&"TABLE"==e.nodeName&&(s.settings.forced_root_block?s.dom.add(s.getBody(),s.settings.forced_root_block,s.settings.forced_root_block_attrs,n.ie&&n.ie<10?" ":'<br data-mce-bogus="1" />'):s.dom.add(s.getBody(),"br",{"data-mce-bogus":"1"}))}),s.on("PreProcess",function(e){var t=e.node.lastChild;t&&("BR"==t.nodeName||1==t.childNodes.length&&("BR"==t.firstChild.nodeName||"\xa0"==t.firstChild.nodeValue))&&t.previousSibling&&"TABLE"==t.previousSibling.nodeName&&s.dom.remove(t)})}function d(){function e(e,t,n,r){var i,o,a,s=3,l=e.dom.getParent(t.startContainer,"TABLE");return l&&(i=l.parentNode),o=t.startContainer.nodeType==s&&0===t.startOffset&&0===t.endOffset&&r&&("TR"==n.nodeName||n==i),a=("TD"==n.nodeName||"TH"==n.nodeName)&&!r,o||a}function t(){var t=s.selection.getRng(),n=s.selection.getNode(),r=s.dom.getParent(t.startContainer,"TD,TH");if(e(s,t,n,r)){r||(r=n);for(var i=r.lastChild;i.lastChild;)i=i.lastChild;3==i.nodeType&&(t.setEnd(i,i.data.length),s.selection.setRng(t))}}s.on("KeyDown",function(){t()}),s.on("MouseDown",function(e){2!=e.button&&t()})}function f(){function t(e){s.selection.select(e,!0),s.selection.collapse(!0)}function n(e){s.$(e).empty(),i.paddCell(e)}s.on("keydown",function(i){if((i.keyCode==e.DELETE||i.keyCode==e.BACKSPACE)&&!i.isDefaultPrevented()){var o,a,l,c;if(o=s.dom.getParent(s.selection.getStart(),"table")){if(a=s.dom.select("td,th",o),l=r.grep(a,function(e){return!!s.dom.getAttrib(e,"data-mce-selected")}),0===l.length)return c=s.dom.getParent(s.selection.getStart(),"td,th"),void(s.selection.isCollapsed()&&c&&s.dom.isEmpty(c)&&(i.preventDefault(),n(c),t(c)));i.preventDefault(),s.undoManager.transact(function(){a.length==l.length?s.execCommand("mceTableDelete"):(r.each(l,n),t(l[0]))})}}})}function p(){var t=function(e){return e&&"CAPTION"==e.nodeName&&"TABLE"==e.parentNode.nodeName},r=function(e,t){var n=s.selection.getRng(),r=e.ownerDocument.createTextNode("\xa0");n.startOffset?e.insertBefore(r,e.firstChild):e.appendChild(r),t&&(s.selection.select(r,!0),s.selection.collapse(!0))},i=function(t){return(t.keyCode==e.DELETE||t.keyCode==e.BACKSPACE)&&!t.isDefaultPrevented()},o=function(e){return e.firstChild===e.lastChild&&e.firstChild},a=function(e){return e&&3===e.nodeType},l=function(e){var t=o(e);return a(t)&&1===t.data.length?t.data:null},c=function(e){var t=o(e),n=l(e);return t&&!a(t)||n&&!d(n)},u=function(e){return s.dom.isEmpty(e)||d(l(e))},d=function(e){return"\xa0"===e};s.on("keydown",function(e){if(i(e)){var o=s.dom.getParent(s.selection.getStart(),"caption");t(o)&&(n.ie&&(s.selection.isCollapsed()?c(o)&&r(o):(s.undoManager.transact(function(){s.execCommand("Delete"),u(o)&&r(o,!0)}),e.preventDefault())),u(o)&&e.preventDefault())}})}f(),p(),n.webkit&&(l(),d()),n.gecko&&(c(),u()),n.ie>9&&(c(),u())}}),r("tinymce/tableplugin/CellSelection",["tinymce/tableplugin/TableGrid","tinymce/dom/TreeWalker","tinymce/util/Tools"],function(e,t,n){return function(r,i){function o(e){r.getBody().style.webkitUserSelect="",(e||g)&&(r.$("td[data-mce-selected],th[data-mce-selected]").removeAttr("data-mce-selected"),g=!1)}function a(e,t){return!(!e||!t)&&e===m.getParent(t,"table")}function s(t){var n,o,s=t.target;if(!f&&!p&&s!==d&&(d=s,u&&c)){if(o=m.getParent(s,"td,th"),a(u,o)||(o=m.getParent(u,"td,th")),c===o&&!g)return;if(i(!0),a(u,o)){t.preventDefault(),l||(l=new e(r,u,c),r.getBody().style.webkitUserSelect="none"),l.setEndCell(o),g=!0,n=r.selection.getSel();try{n.removeAllRanges?n.removeAllRanges():n.empty()}catch(e){}}}}var l,c,u,d,f,p,m=r.dom,g=!0,h=function(){c=l=u=d=null,i(!1)};return r.on("SelectionChange",function(e){g&&e.stopImmediatePropagation()},!0),r.on("MouseDown",function(e){2==e.button||f||p||(o(),c=m.getParent(e.target,"td,th"),u=m.getParent(c,"table"))}),r.on("mouseover",s),r.on("remove",function(){m.unbind(r.getDoc(),"mouseover",s),o()}),r.on("MouseUp",function(){function e(e,r){var o=new t(e,e);do{if(3==e.nodeType&&0!==n.trim(e.nodeValue).length)return void(r?i.setStart(e,0):i.setEnd(e,e.nodeValue.length));if("BR"==e.nodeName)return void(r?i.setStartBefore(e):i.setEndBefore(e))}while(e=r?o.next():o.prev())}var i,o,a,s,u,d=r.selection;if(c){if(l&&(r.getBody().style.webkitUserSelect=""),o=m.select("td[data-mce-selected],th[data-mce-selected]"),o.length>0){i=m.createRng(),s=o[0],i.setStartBefore(s),i.setEndAfter(s),e(s,1),a=new t(s,m.getParent(o[0],"table"));do if("TD"==s.nodeName||"TH"==s.nodeName){if(!m.getAttrib(s,"data-mce-selected"))break;u=s}while(s=a.next());e(u),d.setRng(i)}r.nodeChanged(),h()}}),r.on("KeyUp Drop SetContent",function(e){o("setcontent"==e.type),h(),f=!1}),r.on("ObjectResizeStart ObjectResized",function(e){f="objectresized"!=e.type}),r.on("dragstart",function(){p=!0}),r.on("drop dragend",function(){p=!1}),{clear:o}}}),r("tinymce/tableplugin/Dialogs",["tinymce/util/Tools","tinymce/Env"],function(e,t){var n=e.each;return function(r){function i(){var e=r.settings.color_picker_callback;if(e)return function(){var t=this;e.call(r,function(e){t.value(e).fire("change")},t.value())}}function o(e){return{title:"Advanced",type:"form",defaults:{onchange:function(){d(e,this.parents().reverse()[0],"style"==this.name())}},items:[{label:"Style",name:"style",type:"textbox"},{type:"form",padding:0,formItemDefaults:{layout:"grid",alignH:["start","right"]},defaults:{size:7},items:[{label:"Border color",type:"colorbox",name:"borderColor",onaction:i()},{label:"Background color",type:"colorbox",name:"backgroundColor",onaction:i()}]}]}}function a(e){return e?e.replace(/px$/,""):""}function s(e){return/^[0-9]+$/.test(e)&&(e+="px"),e}function l(e){n("left center right".split(" "),function(t){r.formatter.remove("align"+t,{},e)})}function c(e){n("top middle bottom".split(" "),function(t){r.formatter.remove("valign"+t,{},e)})}function u(t,n,r){function i(t,r){return r=r||[],e.each(t,function(e){var t={text:e.text||e.title};e.menu?t.menu=i(e.menu):(t.value=e.value,n&&n(t)),r.push(t)}),r}return i(t,r||[])}function d(e,t,n){var r=t.toJSON(),i=e.parseStyle(r.style);n?(t.find("#borderColor").value(i["border-color"]||"")[0].fire("change"),t.find("#backgroundColor").value(i["background-color"]||"")[0].fire("change")):(i["border-color"]=r.borderColor,i["background-color"]=r.backgroundColor),t.find("#style").value(e.serializeStyle(e.parseStyle(e.serializeStyle(i))))}function f(e,t,n){var r=e.parseStyle(e.getAttrib(n,"style"));r["border-color"]&&(t.borderColor=r["border-color"]),r["background-color"]&&(t.backgroundColor=r["background-color"]),t.style=e.serializeStyle(r)}function p(e,t,r){var i=e.parseStyle(e.getAttrib(t,"style"));n(r,function(e){i[e.name]=e.value}),e.setAttrib(t,"style",e.serializeStyle(e.parseStyle(e.serializeStyle(i))))}var m=this;m.tableProps=function(){m.table(!0)},m.table=function(i){function c(){function n(e,t,r){if("TD"===e.tagName||"TH"===e.tagName)C.setStyle(e,t,r);else if(e.children)for(var i=0;i<e.children.length;i++)n(e.children[i],t,r)}var i;d(C,this),w=e.extend(w,this.toJSON()),w["class"]===!1&&delete w["class"],r.undoManager.transact(function(){if(g||(g=r.plugins.table.insertTable(w.cols||1,w.rows||1)),r.dom.setAttribs(g,{style:w.style,"class":w["class"]}),r.settings.table_style_by_css){if(x=[],x.push({name:"border",value:w.border}),x.push({name:"border-spacing",value:s(w.cellspacing)}),p(C,g,x),C.setAttribs(g,{"data-mce-border-color":w.borderColor,"data-mce-cell-padding":w.cellpadding,"data-mce-border":w.border}),g.children)for(var e=0;e<g.children.length;e++)n(g.children[e],"border",w.border),n(g.children[e],"padding",s(w.cellpadding))}else r.dom.setAttribs(g,{border:w.border,cellpadding:w.cellpadding,cellspacing:w.cellspacing});C.getAttrib(g,"width")&&!r.settings.table_style_by_css?C.setAttrib(g,"width",a(w.width)):C.setStyle(g,"width",s(w.width)),C.setStyle(g,"height",s(w.height)),i=C.select("caption",g)[0],i&&!w.caption&&C.remove(i),!i&&w.caption&&(i=C.create("caption"),i.innerHTML=t.ie?"\xa0":'<br data-mce-bogus="1"/>',g.insertBefore(i,g.firstChild)),l(g),w.align&&r.formatter.apply("align"+w.align,{},g),r.focus(),r.addVisual()})}function m(e,t){function n(e,n){for(var r=0;r<n.length;r++){var i=C.getStyle(n[r],t);if("undefined"==typeof e&&(e=i),e!=i)return""}return e}var i,o=r.dom.select("td,th",e);return i=n(i,o)}var g,h,v,b,y,x,C=r.dom,w={};i===!0?(g=C.getParent(r.selection.getStart(),"table"),g&&(w={width:a(C.getStyle(g,"width")||C.getAttrib(g,"width")),height:a(C.getStyle(g,"height")||C.getAttrib(g,"height")),cellspacing:a(C.getStyle(g,"border-spacing")||C.getAttrib(g,"cellspacing")),cellpadding:C.getAttrib(g,"data-mce-cell-padding")||C.getAttrib(g,"cellpadding")||m(g,"padding"),border:C.getAttrib(g,"data-mce-border")||C.getAttrib(g,"border")||m(g,"border"),borderColor:C.getAttrib(g,"data-mce-border-color"),caption:!!C.select("caption",g)[0],"class":C.getAttrib(g,"class")},n("left center right".split(" "),function(e){r.formatter.matchNode(g,"align"+e)&&(w.align=e)}))):(h={label:"Cols",name:"cols"},v={label:"Rows",name:"rows"}),r.settings.table_class_list&&(w["class"]&&(w["class"]=w["class"].replace(/\s*mce\-item\-table\s*/g,"")),b={name:"class",type:"listbox",label:"Class",values:u(r.settings.table_class_list,function(e){e.value&&(e.textStyle=function(){return r.formatter.getCssText({block:"table",classes:[e.value]})})})}),y={type:"form",layout:"flex",direction:"column",labelGapCalc:"children",padding:0,items:[{type:"form",labelGapCalc:!1,padding:0,layout:"grid",columns:2,defaults:{type:"textbox",maxWidth:50},items:r.settings.table_appearance_options!==!1?[h,v,{label:"Width",name:"width"},{label:"Height",name:"height"},{label:"Cell spacing",name:"cellspacing"},{label:"Cell padding",name:"cellpadding"},{label:"Border",name:"border"},{label:"Caption",name:"caption",type:"checkbox"}]:[h,v,{label:"Width",name:"width"},{label:"Height",name:"height"}]},{label:"Alignment",name:"align",type:"listbox",text:"None",values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},b]},r.settings.table_advtab!==!1?(f(C,w,g),r.windowManager.open({title:"Table properties",data:w,bodyType:"tabpanel",body:[{title:"General",type:"form",items:y},o(C)],onsubmit:c})):r.windowManager.open({title:"Table properties",data:w,body:y,onsubmit:c})},m.merge=function(e,t){r.windowManager.open({title:"Merge cells",body:[{label:"Cols",name:"cols",type:"textbox",value:"1",size:10},{label:"Rows",name:"rows",type:"textbox",value:"1",size:10}],onsubmit:function(){var n=this.toJSON();r.undoManager.transact(function(){e.merge(t,n.cols,n.rows)})}})},m.cell=function(){function t(e,t,n){(1===b.length||n)&&v.setAttrib(e,t,n)}function i(e,t,n){(1===b.length||n)&&v.setStyle(e,t,n)}function p(){d(v,this),g=e.extend(g,this.toJSON()),r.undoManager.transact(function(){n(b,function(e){t(e,"scope",g.scope),t(e,"style",g.style),t(e,"class",g["class"]),i(e,"width",s(g.width)),i(e,"height",s(g.height)),g.type&&e.nodeName.toLowerCase()!==g.type&&(e=v.rename(e,g.type)),1===b.length&&(l(e),c(e)),g.align&&r.formatter.apply("align"+g.align,{},e),g.valign&&r.formatter.apply("valign"+g.valign,{},e)}),r.focus()})}var m,g,h,v=r.dom,b=[];if(b=r.dom.select("td[data-mce-selected],th[data-mce-selected]"),m=r.dom.getParent(r.selection.getStart(),"td,th"),!b.length&&m&&b.push(m),m=m||b[0]){b.length>1?g={width:"",height:"",scope:"","class":"",align:"",style:"",type:m.nodeName.toLowerCase()}:(g={width:a(v.getStyle(m,"width")||v.getAttrib(m,"width")),height:a(v.getStyle(m,"height")||v.getAttrib(m,"height")),scope:v.getAttrib(m,"scope"),"class":v.getAttrib(m,"class")},g.type=m.nodeName.toLowerCase(),n("left center right".split(" "),function(e){r.formatter.matchNode(m,"align"+e)&&(g.align=e)}),n("top middle bottom".split(" "),function(e){r.formatter.matchNode(m,"valign"+e)&&(g.valign=e)}),f(v,g,m)),r.settings.table_cell_class_list&&(h={name:"class",type:"listbox",label:"Class",values:u(r.settings.table_cell_class_list,function(e){e.value&&(e.textStyle=function(){return r.formatter.getCssText({block:"td",classes:[e.value]})})})});var y={type:"form",layout:"flex",direction:"column",labelGapCalc:"children",padding:0,items:[{type:"form",layout:"grid",columns:2,labelGapCalc:!1,padding:0,defaults:{type:"textbox",maxWidth:50},items:[{label:"Width",name:"width"},{label:"Height",name:"height"},{label:"Cell type",name:"type",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"Cell",value:"td"},{text:"Header cell",value:"th"}]},{label:"Scope",name:"scope",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Row",value:"row"},{text:"Column",value:"col"},{text:"Row group",value:"rowgroup"},{text:"Column group",value:"colgroup"}]},{label:"H Align",name:"align",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},{label:"V Align",name:"valign",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Top",value:"top"},{text:"Middle",value:"middle"},{text:"Bottom",value:"bottom"}]}]},h]};r.settings.table_cell_advtab!==!1?r.windowManager.open({title:"Cell properties",bodyType:"tabpanel",data:g,body:[{title:"General",type:"form",items:y},o(v)],onsubmit:p}):r.windowManager.open({title:"Cell properties",data:g,body:y,onsubmit:p})}},m.row=function(){function t(e,t,n){(1===x.length||n)&&y.setAttrib(e,t,n)}function i(e,t,n){(1===x.length||n)&&y.setStyle(e,t,n)}function c(){var o,a,c;d(y,this),v=e.extend(v,this.toJSON()),r.undoManager.transact(function(){var e=v.type;n(x,function(n){t(n,"scope",v.scope),t(n,"style",v.style),t(n,"class",v["class"]),i(n,"height",s(v.height)),e!==n.parentNode.nodeName.toLowerCase()&&(o=y.getParent(n,"table"),a=n.parentNode,c=y.select(e,o)[0],c||(c=y.create(e),o.firstChild?o.insertBefore(c,o.firstChild):o.appendChild(c)),c.appendChild(n),a.hasChildNodes()||y.remove(a)),1===x.length&&l(n),v.align&&r.formatter.apply("align"+v.align,{},n)}),r.focus()})}var p,m,g,h,v,b,y=r.dom,x=[];p=r.dom.getParent(r.selection.getStart(),"table"),m=r.dom.getParent(r.selection.getStart(),"td,th"),n(p.rows,function(e){n(e.cells,function(t){if(y.getAttrib(t,"data-mce-selected")||t==m)return x.push(e),!1})}),g=x[0],g&&(x.length>1?v={height:"",scope:"","class":"",align:"",type:g.parentNode.nodeName.toLowerCase()}:(v={height:a(y.getStyle(g,"height")||y.getAttrib(g,"height")),scope:y.getAttrib(g,"scope"),"class":y.getAttrib(g,"class")},v.type=g.parentNode.nodeName.toLowerCase(),n("left center right".split(" "),function(e){r.formatter.matchNode(g,"align"+e)&&(v.align=e)}),f(y,v,g)),r.settings.table_row_class_list&&(h={name:"class",type:"listbox",label:"Class",values:u(r.settings.table_row_class_list,function(e){e.value&&(e.textStyle=function(){return r.formatter.getCssText({block:"tr",classes:[e.value]})})})}),b={type:"form",columns:2,padding:0,defaults:{type:"textbox"},items:[{type:"listbox",name:"type",label:"Row type",text:"Header",maxWidth:null,values:[{text:"Header",value:"thead"},{text:"Body",value:"tbody"},{text:"Footer",value:"tfoot"}]},{type:"listbox",name:"align",label:"Alignment",text:"None",maxWidth:null,values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},{label:"Height",name:"height"},h]},r.settings.table_row_advtab!==!1?r.windowManager.open({title:"Row properties",data:v,bodyType:"tabpanel",body:[{title:"General",type:"form",items:b},o(y)],onsubmit:c}):r.windowManager.open({title:"Row properties",data:v,body:b,onsubmit:c}))}}}),r("tinymce/tableplugin/ResizeBars",["tinymce/util/Tools","tinymce/util/VK"],function(e,n){var r;return function(i){function o(e,t){return{index:e,y:i.dom.getPos(t).y}}function a(e,t){return{index:e,y:i.dom.getPos(t).y+t.offsetHeight}}function s(e,t){return{index:e,x:i.dom.getPos(t).x}}function l(e,t){return{index:e,x:i.dom.getPos(t).x+t.offsetWidth}}function c(){var e=i.getBody().dir;return"rtl"===e}function u(){return i.inline}function d(){return u?i.getBody().ownerDocument.body:i.getBody()}function f(e,t){return c()?l(e,t):s(e,t)}function p(e,t){return c()?s(e,t):l(e,t)}function m(e,t){return g(e,"width")/g(t,"width")*100}function g(e,t){var n=i.dom.getStyle(e,t,!0),r=parseInt(n,10);return r}function h(e){var t=g(e,"width"),n=g(e.parentElement,"width");return t/n*100}function v(e,t){var n=g(e,"width");return t/n*100}function b(e,t){var n=g(e.parentElement,"width");return t/n*100}function y(e,t,n){for(var r=[],i=1;i<n.length;i++){var o=n[i].element;r.push(e(i-1,o))}var a=n[n.length-1];return r.push(t(n.length-1,a.element)),r}function x(){var t=i.dom.select("."+fe,d());e.each(t,function(e){i.dom.remove(e)})}function C(e){x(),B(e)}function w(e,t,n,r,i,o,a,s){var l={"data-mce-bogus":"all","class":fe+" "+e,unselectable:"on","data-mce-resize":!1,style:"cursor: "+t+"; margin: 0; padding: 0; position: absolute; left: "+n+"px; top: "+r+"px; height: "+i+"px; width: "+o+"px; "};return l[a]=s,l}function N(t,n,r){e.each(t,function(e){var t=r.x,o=e.y-Ce/2,a=Ce,s=n;i.dom.add(d(),"div",w(pe,me,t,o,a,s,ge,e.index))})}function k(t,n,r){e.each(t,function(e){var t=e.x-Ce/2,o=r.y,a=n,s=Ce;i.dom.add(d(),"div",w(ve,be,t,o,a,s,ye,e.index))})}function S(t){return e.map(t.rows,function(t){var n=e.map(t.cells,function(e){var t=e.hasAttribute("rowspan")?parseInt(e.getAttribute("rowspan"),10):1,n=e.hasAttribute("colspan")?parseInt(e.getAttribute("colspan"),10):1;return{element:e,rowspan:t,colspan:n}});return{element:t,cells:n}})}function _(n){function r(e,t){return e+","+t}function i(e,t){return s[r(e,t)]}function o(){var t=[];return e.each(l,function(e){t=t.concat(e.cells)}),t}function a(){return l}var s={},l=[],c=0,u=0;return e.each(n,function(n,i){var o=[];e.each(n.cells,function(e){for(var n=0;s[r(i,n)]!==t;)n++;for(var a={element:e.element,colspan:e.colspan,rowspan:e.rowspan,rowIndex:i,colIndex:n},l=0;l<e.colspan;l++)for(var d=0;d<e.rowspan;d++){var f=i+d,p=n+l;s[r(f,p)]=a,c=Math.max(c,f+1),u=Math.max(u,p+1)}o.push(a)}),l.push({element:n.element,cells:o})}),{grid:{maxRows:c,maxCols:u},getAt:i,getAllCells:o,getAllRows:a}}function E(e,t){for(var n=[],r=e;r<t;r++)n.push(r);return n}function T(e,t,n){for(var r,i=e(),o=0;o<i.length;o++)t(i[o])&&(r=i[o]);return r?r:n()}function R(t){var n=E(0,t.grid.maxCols),r=E(0,t.grid.maxRows);return e.map(n,function(e){function n(){for(var n=[],i=0;i<r.length;i++){var o=t.getAt(i,e);o&&o.colIndex===e&&n.push(o)}return n}function i(e){return 1===e.colspan}function o(){for(var n,i=0;i<r.length;i++)if(n=t.getAt(i,e))return n;return null}return T(n,i,o)})}function A(t){var n=E(0,t.grid.maxCols),r=E(0,t.grid.maxRows);return e.map(r,function(e){function r(){for(var r=[],i=0;i<n.length;i++){var o=t.getAt(e,i);o&&o.rowIndex===e&&r.push(o)}return r}function i(e){return 1===e.rowspan}function o(){return t.getAt(e,0)}return T(r,i,o);
})}function B(e){var t=S(e),n=_(t),r=A(n),s=R(n),l=i.dom.getPos(e),c=r.length>0?y(o,a,r):[],u=s.length>0?y(f,p,s):[];N(c,e.offsetWidth,l),k(u,e.offsetHeight,l)}function P(e,t,n,r){if(t<0||t>=e.length-1)return"";var i=e[t];if(i)i={value:i,delta:0};else for(var o=e.slice(0,t).reverse(),a=0;a<o.length;a++)o[a]&&(i={value:o[a],delta:a+1});var s=e[t+1];if(s)s={value:s,delta:1};else for(var l=e.slice(t+1),c=0;c<l.length;c++)l[c]&&(s={value:l[c],delta:c+1});var u=s.delta-i.delta,d=Math.abs(s.value-i.value)/u;return n?d/g(r,"width")*100:d}function D(e,t){var n=i.dom.getStyle(e,t);return n||(n=i.dom.getAttrib(e,t)),n||(n=i.dom.getStyle(e,t,!0)),n}function M(e,t,n){var r=D(e,"width"),i=parseInt(r,10),o=t?m(e,n):g(e,"width");return(t&&!V(r)||!t&&!$(r))&&(i=0),!isNaN(i)&&i>0?i:o}function L(t,n,r){for(var i=R(t),o=e.map(i,function(e){return f(e.colIndex,e.element).x}),a=[],s=0;s<i.length;s++){var l=i[s].element.hasAttribute("colspan")?parseInt(i[s].element.getAttribute("colspan"),10):1,c=l>1?P(o,s):M(i[s].element,n,r);c=c?c:we,a.push(c)}return a}function I(e){var t=D(e,"height"),n=parseInt(t,10);return V(t)&&(n=0),!isNaN(n)&&n>0?n:g(e,"height")}function O(t){for(var n=A(t),r=e.map(n,function(e){return o(e.rowIndex,e.element).y}),i=[],a=0;a<n.length;a++){var s=n[a].element.hasAttribute("rowspan")?parseInt(n[a].element.getAttribute("rowspan"),10):1,l=s>1?P(r,a):I(n[a].element);l=l?l:Ne,i.push(l)}return i}function H(t,n,r,i,o){function a(t){return e.map(t,function(){return 0})}function s(){var e;if(o)e=[100-d[0]];else{var t=Math.max(i,d[0]+r);e=[t-d[0]]}return e}function l(e,t){var n,o=a(d.slice(0,e)),s=a(d.slice(t+1));if(r>=0){var l=Math.max(i,d[t]-r);n=o.concat([r,l-d[t]]).concat(s)}else{var c=Math.max(i,d[e]+r),u=d[e]-c;n=o.concat([c-d[e],u]).concat(s)}return n}function c(e,t){var n,o=a(d.slice(0,t));if(r>=0)n=o.concat([r]);else{var s=Math.max(i,d[t]+r);n=o.concat([s-d[t]])}return n}var u,d=t.slice(0);return u=0===t.length?[]:1===t.length?s():0===n?l(0,1):n>0&&n<t.length-1?l(n,n+1):n===t.length-1?c(n-1,n):[]}function F(e,t,n){for(var r=0,i=e;i<t;i++)r+=n[i];return r}function z(t,n){var r=t.getAllCells();return e.map(r,function(e){var t=F(e.colIndex,e.colIndex+e.colspan,n);return{element:e.element,width:t,colspan:e.colspan}})}function W(t,n){var r=t.getAllCells();return e.map(r,function(e){var t=F(e.rowIndex,e.rowIndex+e.rowspan,n);return{element:e.element,height:t,rowspan:e.rowspan}})}function U(t,n){var r=t.getAllRows();return e.map(r,function(e,t){return{element:e.element,height:n[t]}})}function V(e){return Se.test(e)}function $(e){return _e.test(e)}function j(t,n,r){function o(t,n){e.each(t,function(e){i.dom.setStyle(e.element,"width",e.width+n),i.dom.setAttrib(e.element,"width",null)})}function a(){return r<u.grid.maxCols-1?h(t):h(t)+b(t,n)}function s(){return r<u.grid.maxCols-1?g(t,"width"):g(t,"width")+n}function l(e,n,o){r!=u.grid.maxCols-1&&o||(i.dom.setStyle(t,"width",e+n),i.dom.setAttrib(t,"width",null))}for(var c=S(t),u=_(c),d=V(t.width)||V(t.style.width),f=L(u,d,t),p=d?v(t,n):n,m=H(f,r,p,we,d,t),y=[],x=0;x<m.length;x++)y.push(m[x]+f[x]);var C=z(u,y),w=d?"%":"px",N=d?a():s();i.undoManager.transact(function(){o(C,w),l(N,w,d)})}function q(t,n,r){for(var o=S(t),a=_(o),s=O(a),l=[],c=0,u=0;u<s.length;u++)l.push(u===r?n+s[u]:s[u]),c+=c[u];var d=W(a,l),f=U(a,l);i.undoManager.transact(function(){e.each(f,function(e){i.dom.setStyle(e.element,"height",e.height+"px"),i.dom.setAttrib(e.element,"height",null)}),e.each(d,function(e){i.dom.setStyle(e.element,"height",e.height+"px"),i.dom.setAttrib(e.element,"height",null)}),i.dom.setStyle(t,"height",c+"px"),i.dom.setAttrib(t,"height",null)})}function Y(){ae=setTimeout(function(){J()},200)}function X(){clearTimeout(ae)}function K(){var e=document.createElement("div");return e.setAttribute("style","margin: 0; padding: 0; position: fixed; left: 0px; top: 0px; height: 100%; width: 100%;"),e.setAttribute("data-mce-bogus","all"),e}function G(e,t){i.dom.bind(e,"mouseup",function(){J()}),i.dom.bind(e,"mousemove",function(e){X(),se&&t(e)}),i.dom.bind(e,"mouseout",function(){Y()})}function J(){if(i.dom.remove(le),se){i.dom.removeClass(ce,ke),se=!1;var e,t;if(Q(ce)){var n=parseInt(i.dom.getAttrib(ce,xe),10),o=i.dom.getPos(ce).x;e=parseInt(i.dom.getAttrib(ce,ye),10),t=c()?n-o:o-n,Math.abs(t)>=1&&j(r,t,e)}else if(ee(ce)){var a=parseInt(i.dom.getAttrib(ce,he),10),s=i.dom.getPos(ce).y;e=parseInt(i.dom.getAttrib(ce,ge),10),t=s-a,Math.abs(t)>=1&&q(r,t,e)}C(r),i.nodeChanged()}}function Z(e,t){le=le?le:K(),se=!0,i.dom.addClass(e,ke),ce=e,G(le,t),i.dom.add(d(),le)}function Q(e){return i.dom.hasClass(e,ve)}function ee(e){return i.dom.hasClass(e,pe)}function te(e){ue=ue!==t?ue:e.clientX;var n=e.clientX-ue;ue=e.clientX;var r=i.dom.getPos(ce).x;i.dom.setStyle(ce,"left",r+n+"px")}function ne(e){de=de!==t?de:e.clientY;var n=e.clientY-de;de=e.clientY;var r=i.dom.getPos(ce).y;i.dom.setStyle(ce,"top",r+n+"px")}function re(e){ue=t,Z(e,te)}function ie(e){de=t,Z(e,ne)}function oe(e){var t=e.target,n=i.getBody();if(i.$.contains(n,r)||r===n)if(Q(t)){e.preventDefault();var o=i.dom.getPos(t).x;i.dom.setAttrib(t,xe,o),re(t)}else if(ee(t)){e.preventDefault();var a=i.dom.getPos(t).y;i.dom.setAttrib(t,he,a),ie(t)}else x()}var ae,se,le,ce,ue,de,fe="mce-resize-bar",pe="mce-resize-bar-row",me="row-resize",ge="data-row",he="data-initial-top",ve="mce-resize-bar-col",be="col-resize",ye="data-col",xe="data-initial-left",Ce=4,we=10,Ne=10,ke="mce-resize-bar-dragging",Se=new RegExp(/(\d+(\.\d+)?%)/),_e=new RegExp(/px|em/);return i.on("init",function(){i.dom.bind(d(),"mousedown",oe)}),i.on("ObjectResized",function(t){var n=t.target;if("TABLE"===n.nodeName){var r=[];e.each(n.rows,function(t){e.each(t.cells,function(e){var t=i.dom.getStyle(e,"width",!0);r.push({cell:e,width:t})})}),e.each(r,function(e){i.dom.setStyle(e.cell,"width",e.width),i.dom.setAttrib(e.cell,"width",null)})}}),i.on("mouseover",function(e){if(!se){var t=i.dom.getParent(e.target,"table");("TABLE"===e.target.nodeName||t)&&(r=t,C(t))}}),i.on("keydown",function(e){switch(e.keyCode){case n.LEFT:case n.RIGHT:case n.UP:case n.DOWN:x()}}),i.on("remove",function(){x(),i.dom.unbind(d(),"mousedown",oe)}),{adjustWidth:j,adjustHeight:q,clearBars:x,drawBars:B,determineDeltas:H,getTableGrid:_,getTableDetails:S,getWidths:L,getPixelHeights:O,isPercentageBasedSize:V,isPixelBasedSize:$,recalculateWidths:z,recalculateCellHeights:W,recalculateRowHeights:U}}}),r("tinymce/tableplugin/Plugin",["tinymce/tableplugin/TableGrid","tinymce/tableplugin/Quirks","tinymce/tableplugin/CellSelection","tinymce/tableplugin/Dialogs","tinymce/tableplugin/ResizeBars","tinymce/util/Tools","tinymce/dom/TreeWalker","tinymce/Env","tinymce/PluginManager"],function(e,t,n,r,i,o,a,s,l){function c(o){function a(e){return function(){o.execCommand(e)}}function l(e,t){var n,r,i,a;for(i='<table id="__mce"><tbody>',n=0;n<t;n++){for(i+="<tr>",r=0;r<e;r++)i+="<td>"+(s.ie&&s.ie<10?" ":"<br>")+"</td>";i+="</tr>"}return i+="</tbody></table>",o.undoManager.transact(function(){o.insertContent(i),a=o.dom.get("__mce"),o.dom.setAttrib(a,"id",null),o.$("tr",a).each(function(e,t){o.fire("newrow",{node:t}),o.$("th,td",t).each(function(e,t){o.fire("newcell",{node:t})})}),o.dom.setAttribs(a,o.settings.table_default_attributes||{}),o.dom.setStyles(a,o.settings.table_default_styles||{})}),a}function c(e,t,n){function r(){var r,i,a,s={},l=0;i=o.dom.select("td[data-mce-selected],th[data-mce-selected]"),r=i[0],r||(r=o.selection.getStart()),n&&i.length>0?(u(i,function(e){return s[e.parentNode.parentNode.nodeName]=1}),u(s,function(e){l+=e}),a=1!==l):a=!o.dom.getParent(r,t),e.disabled(a),o.selection.selectorChanged(t,function(t){e.disabled(!t)})}o.initialized?r():o.on("init",r)}function d(){c(this,"table")}function f(){c(this,"td,th")}function p(){c(this,"td,th",!0)}function m(){var e="";e='<table role="grid" class="mce-grid mce-grid-border" aria-readonly="true">';for(var t=0;t<10;t++){e+="<tr>";for(var n=0;n<10;n++)e+='<td role="gridcell" tabindex="-1"><a id="mcegrid'+(10*t+n)+'" href="#" data-mce-x="'+n+'" data-mce-y="'+t+'"></a></td>';e+="</tr>"}return e+="</table>",e+='<div class="mce-text-center" role="presentation">1 x 1</div>'}function g(e,t,n){var r,i,a,s,l,c=n.getEl().getElementsByTagName("table")[0],u=n.isRtl()||"tl-tr"==n.parent().rel;for(c.nextSibling.innerHTML=e+1+" x "+(t+1),u&&(e=9-e),i=0;i<10;i++)for(r=0;r<10;r++)s=c.rows[i].childNodes[r].firstChild,l=(u?r>=e:r<=e)&&i<=t,o.dom.toggleClass(s,"mce-active",l),l&&(a=s);return a.parentNode}function h(){o.addButton("tableprops",{title:"Table properties",onclick:k.tableProps,icon:"table"}),o.addButton("tabledelete",{title:"Delete table",onclick:a("mceTableDelete")}),o.addButton("tablecellprops",{title:"Cell properties",onclick:a("mceTableCellProps")}),o.addButton("tablemergecells",{title:"Merge cells",onclick:a("mceTableMergeCells")}),o.addButton("tablesplitcells",{title:"Split cell",onclick:a("mceTableSplitCells")}),o.addButton("tableinsertrowbefore",{title:"Insert row before",onclick:a("mceTableInsertRowBefore")}),o.addButton("tableinsertrowafter",{title:"Insert row after",onclick:a("mceTableInsertRowAfter")}),o.addButton("tabledeleterow",{title:"Delete row",onclick:a("mceTableDeleteRow")}),o.addButton("tablerowprops",{title:"Row properties",onclick:a("mceTableRowProps")}),o.addButton("tablecutrow",{title:"Cut row",onclick:a("mceTableCutRow")}),o.addButton("tablecopyrow",{title:"Copy row",onclick:a("mceTableCopyRow")}),o.addButton("tablepasterowbefore",{title:"Paste row before",onclick:a("mceTablePasteRowBefore")}),o.addButton("tablepasterowafter",{title:"Paste row after",onclick:a("mceTablePasteRowAfter")}),o.addButton("tableinsertcolbefore",{title:"Insert column before",onclick:a("mceTableInsertColBefore")}),o.addButton("tableinsertcolafter",{title:"Insert column after",onclick:a("mceTableInsertColAfter")}),o.addButton("tabledeletecol",{title:"Delete column",onclick:a("mceTableDeleteCol")})}function v(e){var t=o.dom.is(e,"table")&&o.getBody().contains(e);return t}function b(){var e=o.settings.table_toolbar;""!==e&&e!==!1&&(e||(e="tableprops tabledelete | tableinsertrowbefore tableinsertrowafter tabledeleterow | tableinsertcolbefore tableinsertcolafter tabledeletecol"),o.addContextToolbar(v,e))}function y(){return C}function x(e){C=e}var C,w,N=this,k=new r(o);!o.settings.object_resizing||o.settings.table_resize_bars===!1||o.settings.object_resizing!==!0&&"table"!==o.settings.object_resizing||(w=i(o)),o.settings.table_grid===!1?o.addMenuItem("inserttable",{text:"Table",icon:"table",context:"table",onclick:k.table}):o.addMenuItem("inserttable",{text:"Table",icon:"table",context:"table",ariaHideMenu:!0,onclick:function(e){e.aria&&(this.parent().hideAll(),e.stopImmediatePropagation(),k.table())},onshow:function(){g(0,0,this.menu.items()[0])},onhide:function(){var e=this.menu.items()[0].getEl().getElementsByTagName("a");o.dom.removeClass(e,"mce-active"),o.dom.addClass(e[0],"mce-active")},menu:[{type:"container",html:m(),onPostRender:function(){this.lastX=this.lastY=0},onmousemove:function(e){var t,n,r=e.target;"A"==r.tagName.toUpperCase()&&(t=parseInt(r.getAttribute("data-mce-x"),10),n=parseInt(r.getAttribute("data-mce-y"),10),(this.isRtl()||"tl-tr"==this.parent().rel)&&(t=9-t),t===this.lastX&&n===this.lastY||(g(t,n,e.control),this.lastX=t,this.lastY=n))},onclick:function(e){var t=this;"A"==e.target.tagName.toUpperCase()&&(e.preventDefault(),e.stopPropagation(),t.parent().cancel(),o.undoManager.transact(function(){l(t.lastX+1,t.lastY+1)}),o.addVisual())}}]}),o.addMenuItem("tableprops",{text:"Table properties",context:"table",onPostRender:d,onclick:k.tableProps}),o.addMenuItem("deletetable",{text:"Delete table",context:"table",onPostRender:d,cmd:"mceTableDelete"}),o.addMenuItem("cell",{separator:"before",text:"Cell",context:"table",menu:[{text:"Cell properties",onclick:a("mceTableCellProps"),onPostRender:f},{text:"Merge cells",onclick:a("mceTableMergeCells"),onPostRender:p},{text:"Split cell",onclick:a("mceTableSplitCells"),onPostRender:f}]}),o.addMenuItem("row",{text:"Row",context:"table",menu:[{text:"Insert row before",onclick:a("mceTableInsertRowBefore"),onPostRender:f},{text:"Insert row after",onclick:a("mceTableInsertRowAfter"),onPostRender:f},{text:"Delete row",onclick:a("mceTableDeleteRow"),onPostRender:f},{text:"Row properties",onclick:a("mceTableRowProps"),onPostRender:f},{text:"-"},{text:"Cut row",onclick:a("mceTableCutRow"),onPostRender:f},{text:"Copy row",onclick:a("mceTableCopyRow"),onPostRender:f},{text:"Paste row before",onclick:a("mceTablePasteRowBefore"),onPostRender:f},{text:"Paste row after",onclick:a("mceTablePasteRowAfter"),onPostRender:f}]}),o.addMenuItem("column",{text:"Column",context:"table",menu:[{text:"Insert column before",onclick:a("mceTableInsertColBefore"),onPostRender:f},{text:"Insert column after",onclick:a("mceTableInsertColAfter"),onPostRender:f},{text:"Delete column",onclick:a("mceTableDeleteCol"),onPostRender:f}]});var S=[];u("inserttable tableprops deletetable | cell row column".split(" "),function(e){"|"==e?S.push({text:"-"}):S.push(o.menuItems[e])}),o.addButton("table",{type:"menubutton",title:"Table",menu:S}),s.isIE||o.on("click",function(e){e=e.target,"TABLE"===e.nodeName&&(o.selection.select(e),o.nodeChanged())}),N.quirks=new t(o),o.on("Init",function(){N.cellSelection=new n(o,function(e){e&&w&&w.clearBars()}),N.resizeBars=w}),o.on("PreInit",function(){o.serializer.addAttributeFilter("data-mce-cell-padding,data-mce-border,data-mce-border-color",function(e,t){for(var n=e.length;n--;)e[n].attr(t,null)})}),u({mceTableSplitCells:function(e){e.split()},mceTableMergeCells:function(e){var t;t=o.dom.getParent(o.selection.getStart(),"th,td"),o.dom.select("td[data-mce-selected],th[data-mce-selected]").length?e.merge():k.merge(e,t)},mceTableInsertRowBefore:function(e){e.insertRows(!0)},mceTableInsertRowAfter:function(e){e.insertRows()},mceTableInsertColBefore:function(e){e.insertCols(!0)},mceTableInsertColAfter:function(e){e.insertCols()},mceTableDeleteCol:function(e){e.deleteCols()},mceTableDeleteRow:function(e){e.deleteRows()},mceTableCutRow:function(e){C=e.cutRows()},mceTableCopyRow:function(e){C=e.copyRows()},mceTablePasteRowBefore:function(e){e.pasteRows(C,!0)},mceTablePasteRowAfter:function(e){e.pasteRows(C)},mceSplitColsBefore:function(e){e.splitCols(!0)},mceSplitColsAfter:function(e){e.splitCols(!1)},mceTableDelete:function(e){w&&w.clearBars(),e.deleteTable()}},function(t,n){o.addCommand(n,function(){var n=new e(o);n&&(t(n),o.execCommand("mceRepaint"),N.cellSelection.clear())})}),u({mceInsertTable:k.table,mceTableProps:function(){k.table(!0)},mceTableRowProps:k.row,mceTableCellProps:k.cell},function(e,t){o.addCommand(t,function(t,n){e(n)})}),h(),b(),o.settings.table_tab_navigation!==!1&&o.on("keydown",function(t){var n,r,i;9==t.keyCode&&(n=o.dom.getParent(o.selection.getStart(),"th,td"),n&&(t.preventDefault(),r=new e(o),i=t.shiftKey?-1:1,o.undoManager.transact(function(){!r.moveRelIdx(n,i)&&i>0&&(r.insertRow(),r.refresh(),r.moveRelIdx(n,i))})))}),N.insertTable=l,N.setClipboardRows=x,N.getClipboardRows=y}var u=o.each;l.add("table",c)})}(window);