<?php
namespace XF\Entity;
use XF\Mvc\Entity\Entity;
use XF\Mvc\Entity\Structure;
use function intval, is_int, strval;
/**
* COLUMNS
* @property int|null $node_id
* @property string $title
* @property string|null $node_name
* @property string $description
* @property string $node_type_id
* @property int $parent_node_id
* @property int $display_order
* @property int $lft
* @property int $rgt
* @property int $depth
* @property int $style_id
* @property int $effective_style_id
* @property bool $display_in_list
* @property array $breadcrumb_data
* @property string $navigation_id
* @property string $effective_navigation_id
*
* GETTERS
* @property AbstractNode|null $Data
* @property array|null $node_type_info
* @property \XF\NodeType\AbstractHandler|null $handler
*
* RELATIONS
* @property \XF\Entity\Node $Parent
* @property \XF\Entity\NodeType $NodeType
* @property \XF\Mvc\Entity\AbstractCollection|\XF\Entity\PermissionCacheContent[] $Permissions
* @property \XF\Mvc\Entity\AbstractCollection|\XF\Entity\ModeratorContent[] $Moderators
* @property \XF\Mvc\Entity\AbstractCollection|\XF\Entity\BookmarkItem[] $Bookmarks
*/
class Node extends Entity implements LinkableInterface
{
use BookmarkTrait;
public function canView(&$error = null)
{
/** @var AbstractNode $data */
$data = $this->Data;
if (!$data)
{
return false;
}
return $data->canView($error);
}
protected function canBookmarkContent(&$error = null)
{
return ($this->node_type_id == 'Page' && $this->canView($error));
}
public function isSearchEngineIndexable(): bool
{
/** @var AbstractNode $data */
$data = $this->Data;
if (!$data)
{
return false;
}
return $data->isSearchEngineIndexable();
}
public function hasChildren()
{
return ($this->rgt - $this->lft) > 1;
}
public function getNodeListExtras()
{
/** @var AbstractNode $data */
$data = $this->Data;
if (!$data)
{
return [];
}
return $data->getNodeListExtras();
}
public function getRoute($linkType = 'public')
{
$nodeType = $this->node_type_info;
if (!$nodeType)
{
return '';
}
return $linkType == 'public' ? $nodeType['public_route'] : $nodeType['admin_route'];
}
public function getBreadcrumbs($includeSelf = true, $linkType = 'public')
{
/** @var \XF\Mvc\Router $router */
$router = $this->app()->container('router.' . $linkType);
$nodeTypes = $this->app()->container('nodeTypes');
$output = [];
if ($this->breadcrumb_data)
{
foreach ($this->breadcrumb_data AS $crumb)
{
if (!isset($nodeTypes[$crumb['node_type_id']]))
{
continue;
}
$nodeType = $nodeTypes[$crumb['node_type_id']];
$route = $linkType == 'public' ? $nodeType['public_route'] : $nodeType['admin_route'];
$output[] = [
'value' => $crumb['title'],
'href' => $router->buildLink($route, $crumb),
'node_id' => $crumb['node_id']
];
}
}
$nodeType = $this->node_type_info;
if ($includeSelf && $nodeType)
{
$route = $linkType == 'public' ? $nodeType['public_route'] : $nodeType['admin_route'];
$output[] = [
'value' => $this->title,
'href' => $router->buildLink($route, $this),
'node_id' => $this->node_id
];
}
return $output;
}
public function getNodeTemplateRenderer($depth)
{
/** @var AbstractNode $data */
$data = $this->Data;
if (!$data)
{
return null;
}
return $data->getNodeTemplateRenderer($depth);
}
/**
* @return AbstractNode|null
*/
public function getData()
{
if (!$this->node_id)
{
return null;
}
$dataEntity = $this->getDataEntityName();
if (!$dataEntity)
{
return null; // no node type record
}
return $this->_em->find($dataEntity, $this->node_id);
}
public function getDataEntityName()
{
$nodeType = $this->node_type_info;
return $nodeType ? $nodeType['entity_identifier'] : null;
}
/**
* @param bool $cascadeSave
*
* @return AbstractNode
*/
public function getDataRelationOrDefault($cascadeSave = true)
{
$data = $this->getData();
if (!$data)
{
$dataEntity = $this->getDataEntityName();
if (!$dataEntity)
{
throw new \LogicException("No node type for '$this->node_type_id' could be found");
}
$data = $this->_em->create($dataEntity);
$data->node_id = $this->_em->getDeferredValue(
function() { return $this->getValue('node_id'); },
'save'
);
$this->_getterCache['Data'] = $data;
}
if ($cascadeSave)
{
$this->addCascadedSave($data);
}
return $data;
}
/**
* @return \XF\NodeType\AbstractHandler|null
*/
public function getHandler()
{
$nodeType = $this->node_type_info;
if (!$nodeType || !$nodeType['handler_class'])
{
return null;
}
$class = \XF::stringToClass($nodeType['handler_class'], '%s\NodeType\%s');
$class = \XF::extendClass($class);
return new $class($this->node_type_id, $nodeType);
}
/**
* @return array|null
*/
public function getNodeTypeInfo()
{
$nodeTypes = $this->app()->container('nodeTypes');
$nodeTypeId = $this->node_type_id;
return $nodeTypes[$nodeTypeId] ?? null;
}
// *********** VERIFIERS *************
protected function verifyNodeName(&$name)
{
if (!$name)
{
if ($this->node_type_id == 'Page')
{
$this->error(\XF::phrase('please_enter_valid_url_portion'));
return false;
}
$name = null;
return true;
}
if ($name === strval(intval($name)) || $name == '-')
{
$this->error(\XF::phrase('node_names_contain_more_numbers_hyphen'), 'node_name');
return false;
}
return true;
}
protected function verifyNodeTypeId(&$nodeTypeId)
{
$nodeTypes = $this->app()->container('nodeTypes');
if (!isset($nodeTypes[$nodeTypeId]))
{
$this->error(\XF::phrase('please_choose_valid_node_type'), 'node_type_id');
return false;
}
return true;
}
// *********** LIFE CYCLE **************
protected function _preSave()
{
if ($this->isUpdate() && $this->isChanged('node_type_id'))
{
throw new \LogicException("Node types can't be changed");
}
}
protected function _preDelete()
{
/** @var \XF\Mvc\Entity\Entity $data */
$data = $this->Data;
if ($data)
{
if (!$data->preDelete())
{
foreach ($data->getErrors() AS $key => $error)
{
$this->error($error, is_int($key) ? null : $key, false);
}
}
}
}
protected function _postDelete()
{
/** @var \XF\Mvc\Entity\Entity $data */
$data = $this->Data;
if ($data)
{
$data->delete();
}
if ($this->Moderators)
{
/** @var \XF\Entity\ModeratorContent $moderator */
foreach ($this->Moderators AS $moderator)
{
$moderator->delete();
}
}
}
/**
* @param \XF\Api\Result\EntityResult $result
* @param int $verbosity
* @param array $options
*
* @api-out array $breadcrumbs A list of breadcrumbs for this node, including the node_id, title, and node_type_id
* @api-out object $type_data Data related to the specific node type this represents. Contents will vary significantly.
* @api-out string $view_url
*/
protected function setupApiResultData(
\XF\Api\Result\EntityResult $result, $verbosity = self::VERBOSITY_NORMAL, array $options = []
)
{
$breadcrumbs = [];
if ($this->breadcrumb_data)
{
foreach ($this->breadcrumb_data AS $breadcrumb)
{
$breadcrumbs[] = [
'node_id' => $breadcrumb['node_id'],
'title' => $breadcrumb['title'],
'node_type_id' => $breadcrumb['node_type_id']
];
}
}
$result->breadcrumbs = $breadcrumbs;
$typeData = $this->Data ? $this->Data->getNodeTypeApiData($verbosity, $options) : (object)[];
$result->type_data = $typeData;
$viewUrl = $this->getContentUrl(true);
if ($viewUrl)
{
$result->view_url = $viewUrl;
}
}
public function getContentUrl(bool $canonical = false, array $extraParams = [], $hash = null)
{
$nodeTypeInfo = $this->getNodeTypeInfo();
if (!$nodeTypeInfo)
{
return '';
}
$route = ($canonical ? 'canonical:' : '') . $nodeTypeInfo['public_route'];
return $this->app()->router('public')->buildLink($route, $this, $extraParams, $hash);
}
public function getContentPublicRoute()
{
$nodeTypeInfo = $this->getNodeTypeInfo();
if (!$nodeTypeInfo)
{
return null;
}
return $nodeTypeInfo['public_route'];
}
public function getContentTitle(string $context = '')
{
return $this->title;
}
public static function getStructure(Structure $structure)
{
$structure->table = 'xf_node';
$structure->shortName = 'XF:Node';
$structure->primaryKey = 'node_id';
$structure->contentType = 'node';
$structure->columns = [
'node_id' => ['type' => self::UINT, 'autoIncrement' => true, 'nullable' => true],
'title' => ['type' => self::STR, 'maxLength' => 50,
'required' => 'please_enter_valid_title', 'api' => true
],
'node_name' => ['type' => self::STR, 'maxLength' => 50, 'nullable' => true, 'default' => null,
'unique' => 'node_names_must_be_unique',
'match' => 'alphanumeric_hyphen',
'api' => true
],
'description' => ['type' => self::STR, 'default' => '', 'api' => true],
'node_type_id' => ['type' => self::BINARY, 'maxLength' => 25, 'required' => true, 'api' => true],
'parent_node_id' => ['type' => self::UINT, 'required' => true, 'default' => 0, 'api' => true],
'display_order' => ['type' => self::UINT, 'default' => 1, 'api' => true],
'lft' => ['type' => self::UINT],
'rgt' => ['type' => self::UINT],
'depth' => ['type' => self::UINT],
'style_id' => ['type' => self::UINT, 'default' => 0],
'effective_style_id' => ['type' => self::UINT, 'default' => 0],
'display_in_list' => ['type' => self::BOOL, 'default' => true, 'api' => true],
'breadcrumb_data' => ['type' => self::JSON_ARRAY, 'default' => []],
'navigation_id' => ['type' => self::STR, 'maxLength' => 50, 'default' => ''],
'effective_navigation_id' => ['type' => self::STR, 'maxLength' => 50, 'default' => ''],
];
$structure->getters = [
'Data' => true,
'node_type_info' => true,
'handler' => true
];
$structure->behaviors = [
'XF:TreeStructured' => [
'parentField' => 'parent_node_id',
'permissionContentType' => 'node',
'rebuildExtraFields' => ['style_id', 'navigation_id', 'node_name'],
'rebuildService' => 'XF:Node\RebuildNestedSet'
]
];
$structure->relations = [
'Parent' => [
'entity' => 'XF:Node',
'type' => self::TO_ONE,
'conditions' => [
['node_id', '=', '$parent_node_id']
],
'primary' => true
],
'NodeType' => [
'entity' => 'XF:NodeType',
'type' => self::TO_ONE,
'conditions' => 'node_type_id',
'primary' => true
],
'Permissions' => [
'entity' => 'XF:PermissionCacheContent',
'type' => self::TO_MANY,
'conditions' => [
['content_type', '=', 'node'],
['content_id', '=', '$node_id']
],
'key' => 'permission_combination_id',
'proxy' => true
],
'Moderators' => [
'entity' => 'XF:ModeratorContent',
'type' => self::TO_MANY,
'conditions' => [
['content_type', '=', 'node'],
['content_id', '=', '$node_id']
]
]
];
$structure->withAliases = [
'api' => [
function()
{
return 'Permissions|' . \XF::visitor()->permission_combination_id;
}
]
];
static::addBookmarkableStructureElements($structure);
return $structure;
}
/**
* @return \XF\Repository\Node
*/
protected function getNodeRepo()
{
return $this->repository('XF:Node');
}
}