0) {
foreach ($existingnode as $node) {
if ($node["name"] == $name) {
return (int)$node["ref"];
}
}
}
}
// If creating new node establish order_by if necessary
if (is_null($ref) && '' == $order_by) {
$order_by = get_node_order_by($resource_type_field, ($resource_type_field_data['type'] == FIELD_TYPE_CATEGORY_TREE), $parent);
}
$query = "INSERT INTO `node` (`resource_type_field`, `name`, `parent`, `order_by`) VALUES (?, ?, ?, ?)";
$parameters = array
(
"i",$resource_type_field,
"s",$name,
"i",$parent,
"s",$order_by
);
// Check if we only need to save the record
$current_node = array();
if (get_node($ref, $current_node)) {
// If nothing has changed, just return true, otherwise continue and update record
if (
$resource_type_field === $current_node['resource_type_field'] &&
$name === $current_node['name'] &&
$parent === $current_node['parent'] &&
$order_by === $current_node['order_by']
) {
return $ref;
}
// When changing parent we need to make sure order by is changed as well
// to reflect the fact that the node has just been added (ie. at the end of the list)
if ($parent != $current_node['parent']) {
$order_by = get_node_order_by($resource_type_field, true, $parent);
}
// Order by can be changed asynchronously, so when we save a node we can pass null or an empty
// order_by value and this will mean we can use the current order
if (!is_null($ref) && '' == $order_by) {
$order_by = $current_node['order_by'];
}
$query = "
UPDATE node
SET resource_type_field = ?,
`name` = ?,
parent = ?,
order_by = ?
WHERE ref = ?
";
$parameters = array
(
"i",$resource_type_field,
"s",$name,
"i",$parent,
"s",$order_by,
"i",$ref
);
// Handle node indexing for existing nodes
remove_node_keyword_mappings(array('ref' => $current_node['ref'], 'resource_type_field' => $current_node['resource_type_field'], 'name' => $current_node['name']), null);
if ($resource_type_field_data["keywords_index"] == 1) {
$is_date = in_array($resource_type_field_data['type'], [FIELD_TYPE_DATE_AND_OPTIONAL_TIME,FIELD_TYPE_EXPIRY_DATE,FIELD_TYPE_DATE,FIELD_TYPE_DATE_RANGE]);
$is_html = ($resource_type_field_data["type"] == FIELD_TYPE_TEXT_BOX_FORMATTED_AND_TINYMCE);
add_node_keyword_mappings(array('ref' => $ref, 'resource_type_field' => $resource_type_field, 'name' => $name), null, $is_date, $is_html);
}
}
ps_query($query, $parameters);
$new_ref = sql_insert_id();
if ($new_ref == 0 || $new_ref === false) {
if ($ref == null) {
$return = ps_value("SELECT `ref` AS 'value' FROM `node` WHERE `resource_type_field`=? AND `name`=?", array("i",$resource_type_field,"s",$name), 0);
} else {
$return = $ref;
}
} else {
if (in_array($resource_type_field_data['type'], $FIXED_LIST_FIELD_TYPES)) {
log_activity("Set metadata field option for field {$resource_type_field}", LOG_CODE_CREATED, $name, 'node', 'name', $new_ref, null, '');
}
// Handle node indexing for new nodes
if ($resource_type_field_data["keywords_index"] == 1) {
add_node_keyword_mappings(array('ref' => $new_ref, 'resource_type_field' => $resource_type_field, 'name' => $name), null);
}
$return = $new_ref;
}
if (in_array($resource_type_field_data['type'], $FIXED_LIST_FIELD_TYPES)) {
clear_query_cache("schema");
}
return $return;
}
/**
* Delete node. This will fully delete a node and remove any association between the deleted node and resources / keywords.
*
* @param integer $ref ID of the node
*
* @return void
*/
function delete_node($ref)
{
if (is_parent_node($ref)) {
return;
}
$returned_node = array();
get_node($ref, $returned_node, false);
if (empty($returned_node)) {
// Node has already been removed.
return;
}
$resource_type_field = $returned_node['resource_type_field'];
$field_data = get_resource_type_field($resource_type_field);
global $FIXED_LIST_FIELD_TYPES;
if (in_array($field_data['type'], $FIXED_LIST_FIELD_TYPES)) {
log_activity("Delete metadata field option for field {$resource_type_field}", LOG_CODE_DELETED, null, 'node', 'name', $ref, null, $returned_node['name']);
}
ps_query("DELETE FROM node WHERE ref = ?", array("i",$ref));
delete_node_resources($ref);
remove_all_node_keyword_mappings($ref);
}
/**
* Delete all nodes for a resource type field
*
* @param integer $resource_type_field ID of the resource type field
*
* @return void
*/
function delete_nodes_for_resource_type_field($ref)
{
if (is_null($ref) || '' === trim($ref) || 0 === $ref) {
trigger_error('$ref must be an integer greater than 0');
}
ps_query("DELETE FROM node WHERE resource_type_field = ?", array("i",$ref));
}
/**
* Get a specific node by ref
*
* @param integer $ref ID of the node
* @param array $returned_node If a value does exist it will be returned through
* this parameter which is passed by reference
* @param bool $cache By default this function returns cached data. This may not be appropriate if called after
* a value has been changed, for example after editing a node name. Set to false to not use cache.
* @return boolean
*/
function get_node($ref, array &$returned_node, $cache = true)
{
if (is_null($ref) || (trim($ref) == "") || 0 >= $ref) {
return false;
}
$parameters = [];
$sql = columns_in("node");
add_sql_node_language($sql, $parameters);
$parameters[] = "i";
$parameters[] = $ref;
$node = ps_query("SELECT " . $sql . " FROM node WHERE ref = ?", $parameters, $cache ? "schema" : "");
if (count($node) == 0) {
return false;
}
$returned_node = $node[0];
return true;
}
/**
* Get all nodes from database for a specific metadata field or parent.
*
* Use $parent = NULL and recursive = TRUE to get all nodes for a category tree field
*
* Use $offset and $rows only when returning a subset.
*
* @param integer $resource_type_field ID of the metadata field
* @param integer $parent ID of parent node
* @param boolean $recursive Set to true to get children nodes as well.
* IMPORTANT: this is normally used with category trees, but can also be used with
* other fixed field types which allow multiple values eg. dynamic keywords list and checkbox list.
* This allows field type changes to be made between these three types without losing sight of all nodes.
* @param integer $offset Specifies the offset of the first row to return
* @param integer $rows Specifies the maximum number of rows to return.
* IMPORTANT! For non-fixed list fields this is capped at 10000
* to avoid out of memory errors
* @param string $name Filter by name of node
* @param boolean $use_count Show how many resources use a particular node in the node properties
* @param boolean $order_by_translated_name Flag to order by translated names rather then the order_by column
* @param boolean $exact_match When $name is supplied, match the exact name only instead of returning
* matches containing the $name string.
*
* @return array
*/
function get_nodes(
$resource_type_field = null,
$parent = null,
$recursive = false,
$offset = null,
$rows = null,
$name = '',
$use_count = false,
$order_by_translated_name = false,
$exact_match = false
) {
global $FIXED_LIST_FIELD_TYPES;
debug_function_call("get_nodes", func_get_args());
if (!is_int_loose($resource_type_field) && !is_null($resource_type_field)) {
return [];
}
if (!is_null($parent)) {
if ($parent == "") {
$parent = null;
} else {
$parent = (int) $parent;
}
}
if (!is_null($resource_type_field)) {
$fieldinfo = get_resource_type_field($resource_type_field);
if ($fieldinfo === false) {
return false;
}
if (!in_array($fieldinfo["type"], $FIXED_LIST_FIELD_TYPES) && (is_null($rows) || (int)$rows > 10000 )) {
$rows = 10000;
}
}
$return_nodes = array();
$parameters = [];
$sql = "";
add_sql_node_language($sql, $parameters);
// Filter by resource type if required
$filter_by_resource_type_field = "true";
if (!is_null($resource_type_field)) {
$filter_by_resource_type_field = "resource_type_field=?";
$parameters[] = "i";
$parameters[] = $resource_type_field;
}
// Filter by name if required
$filter_by_name = '';
if ('' != $name) {
if ($exact_match) {
$filter_by_name = " AND `name` = ?";
$parameters[] = "s";
$parameters[] = $name;
} else {
$filter_by_name = " AND `name` LIKE ?";
$parameters[] = "s";
$parameters[] = "%" . $name . "%";
}
}
// Option to include a usage count alongside each node
$use_count_sql = "";
if ($use_count) {
$use_count_sql = ",(SELECT count(resource) FROM resource_node WHERE resource_node.resource > 0 AND resource_node.node = node.ref) AS use_count";
}
$parent_sql = is_null($parent) ? ($recursive ? "TRUE" : "parent IS NULL") : ("parent = ?");
if (strpos($parent_sql, "?") !== false) {
$parameters[] = "i";
$parameters[] = $parent;
}
// Order by translated_name or order_by based on flag
$order_by = $order_by_translated_name ? "translated_name" : "order_by";
// Check if limiting is required
$limit = '';
if (!is_null($offset) && is_int($offset)) { # Offset specified
if (!is_null($rows) && is_int($rows)) { # Row limit specified
$limit = "LIMIT ?,?";
$parameters[] = "i";
$parameters[] = $offset;
$parameters[] = "i";
$parameters[] = $rows;
} else # Row limit absent
{
$limit = "LIMIT ?,999999999"; # Use a large arbitrary limit
$parameters[] = "i";
$parameters[] = $offset;
}
} else # Offset not specified
{
if (!is_null($rows) && is_int($rows)) { # Row limit specified
$limit = "LIMIT ?";
$parameters[] = "i";
$parameters[] = $rows;
}
}
$query = "SELECT " . columns_in("node") . $sql . $use_count_sql . "
FROM node
WHERE
" . $filter_by_resource_type_field . $filter_by_name . "
AND " . $parent_sql . "
ORDER BY " . $order_by . ", ref ASC
" . $limit;
$sqlcache = (is_null($resource_type_field) || in_array($fieldinfo["type"], $FIXED_LIST_FIELD_TYPES)) ? "schema" : "";
$nodes = ps_query($query, $parameters, $sqlcache);
// No need to recurse if no parent was specified as we already have all nodes
if ($recursive && (int)$parent > 0) {
foreach ($nodes as $node) {
foreach (get_nodes($resource_type_field, $node['ref'], true) as $sub_node) {
array_push($nodes, $sub_node);
}
}
} else {
$return_nodes = $nodes;
}
if ($recursive) {
// Need to reorder so that parents are ordered by first, with children between (query will have returned them all according to the passed order_by)
$return_nodes = order_tree_nodes($return_nodes, $order_by_translated_name);
}
return $return_nodes;
}
/**
* Find and return node details for a list of node IDs.
*
* @param array $refs List of node IDs
*
* @return array
*/
function get_nodes_by_refs(array $refs)
{
$refs = array_filter($refs, 'is_int_loose');
if (empty($refs)) {
return [];
}
$parameters = [];
$sql = columns_in("node");
add_sql_node_language($sql, $parameters);
$query = "SELECT " . $sql . " FROM node WHERE ref IN (" . ps_param_insert(count($refs)) . ")";
$parameters = array_merge($parameters, ps_param_fill($refs, "i"));
return ps_query($query, $parameters);
}
/**
* Checks whether a node is parent to other nodes or not
*
* @param int $ref Node ref
* @param bool $active_only Check only for active (children) nodes
*/
function is_parent_node($ref, bool $active_only = false): bool
{
if (is_null($ref)) {
return false;
}
$query = $active_only
? 'SELECT DISTINCT parent `value` from node WHERE active = 1'
: 'SELECT DISTINCT parent `value` from node';
$parents = ps_array($query, [], 'schema');
return is_array($parents) && in_array($ref, $parents);
}
/**
* Determine how many level deep a node is. Useful for knowing how much to indent a node
*
* @param integer $ref Node ref
*
* @return integer The depth value of a tree node
*/
function get_tree_node_level($ref)
{
if (!isset($ref)) {
trigger_error('Node ID should be set AND NOT NULL');
}
$parent = $ref;
$depth_level = -1;
do {
$query = "SELECT parent AS value FROM node WHERE ref = ?";
$parameters = array("i",$parent);
$parent = ps_value($query, $parameters, 0);
$depth_level++;
} while ('' != trim((string) $parent) && $parent != 0);
return $depth_level;
}
/**
* Return a row consisting of all ancestor nodes of a given node
* Example:
* 1
* 2
* 2.3
* 2.7
* 2.8.4
* 2.8.5
* 2.8.6
* 2.9
* 3
* Passing in node 5 will return nodes 8,2 in one row
*
* @param integer $ref A tree node
* @param integer $level Node depth level (as returned by get_tree_node_level())
*
* @return array|boolean
*/
function get_all_ancestors_for_node(int $ref, int $level)
{
if (0 >= $level) {
return false;
}
$querycolumns = array();
$query = " FROM node AS n{$level}";
$from_level = $level;
$level--;
while (0 <= $level) {
$query .= " LEFT JOIN node AS n{$level} ON n" . ($level + 1) . ".parent = n{$level}.ref";
$querycolumns[] = "n{$level}.ref n{$level}ref";
if (0 === $level) {
$query .= " WHERE n{$from_level}.ref = ?";
$placeholders = ['i', $ref];
}
$level--;
}
$query = "SELECT " . implode(",", $querycolumns) . $query;
return ps_query($query, $placeholders);
}
/**
* Function used to reorder nodes based on an array with nodes in the new order
*
* @param array $nodes_new_order Array of nodes
*
* @return void
*/
function reorder_node(array $nodes_new_order)
{
if (0 === count($nodes_new_order)) {
trigger_error('$nodes_new_order cannot be an empty array!');
}
$order_by = 10;
$query = 'UPDATE node SET order_by = (CASE ref ';
$parameters = array();
foreach ($nodes_new_order as $node_ref) {
$query .= 'WHEN ? THEN ? ';
$parameters[] = "i";
$parameters[] = $node_ref;
$parameters[] = "i";
$parameters[] = $order_by;
$order_by += 10;
}
$query .= 'ELSE order_by END);';
ps_query($query, $parameters);
clear_query_cache("schema");
}
/**
* Virtually re-order nodes
*
* Temporarily re-order nodes (mostly) for display purposes
*
*
* @param array $unordered_nodes Original nodes array
*
* @return array
*/
function reorder_nodes(array $unordered_nodes)
{
// Put $auto_order_checkbox and $auto_order_checkbox_case_insensitive as global
// to future proof function for when we will drop globals for easier refactoring
global $auto_order_checkbox, $auto_order_checkbox_case_insensitive;
$reordered_options = array();
$use_index_key = array();
foreach ($unordered_nodes as $unordered_node_index => $node) {
$reordered_options[$node['ref']] = normalize_keyword(i18n_get_translated($node['name']), true);
$use_index_key[$node['ref']] = ($unordered_node_index == $node['ref']);
}
if (isset($auto_order_checkbox) && $auto_order_checkbox && $auto_order_checkbox_case_insensitive) {
natcasesort($reordered_options);
} else {
natsort($reordered_options);
}
$reordered_nodes = array();
foreach ($reordered_options as $reordered_node_id => $reordered_node_option) {
if (!$use_index_key[$reordered_node_id]) {
$reordered_nodes[$reordered_node_id] = $unordered_nodes[array_search($reordered_node_id, array_column($unordered_nodes, 'ref'))];
} else {
$reordered_nodes[$reordered_node_id] = $unordered_nodes[array_search($reordered_node_id, array_column($unordered_nodes, 'ref', 'ref'))];
}
}
return $reordered_nodes;
}
/**
* Renders HTML for adding a new node record in the database
*
* @param string $form_action Set the action path of the form
* @param boolean $is_tree Set to TRUE if the field is category tree type
* @param integer $parent ID of the parent of this node
* @param integer $node_depth_level When rendering for trees, we need to know how many levels deep we need to render it
* @param array $parent_node_options Array of node options to be used as parent for new records
*/
function render_new_node_record($form_action, bool $is_tree, $parent = 0, $node_depth_level = 0, array $parent_node_options = array()): void
{
global $baseurl_short, $lang;
if (!is_safe_url($form_action)) {
$form_action = '';
}
if (trim($form_action) == "") {
trigger_error('$form_action param for render_new_node_record() must be set and not be an empty string!');
}
// Render normal fields first then go to tree type
if (!$is_tree) {
?>
$resource_type_field]
),
true,
$parent,
$node_depth_level,
$all_nodes
);
}
return true;
}
/**
* Overrides either a field[options] array structure or a flat option array with values derived from nodes.
* If a field, then will also add field=>nodes[] sub array ready for field rendering.
*
* @param mixed $field Either field array structure or flat options list array
* @param integer $resource_type_field ID of the metadata field, if specified will treat as flat options list
*
* @return boolean
*/
function node_field_options_override(&$field, $resource_type_field = null)
{
global $FIXED_LIST_FIELD_TYPES;
if (isset($field["type"]) && !in_array($field["type"], $FIXED_LIST_FIELD_TYPES)) {
return false;
}
if (!is_null($resource_type_field)) { // we are dealing with a single specified resource type so simply return array of options
$options = get_nodes($resource_type_field);
if (count($options) > 0) { // only override if field options found within nodes
$field = array();
foreach ($options as $option) {
array_push($field, $option['name']);
}
}
return true;
}
if (
!isset($field['ref']) ||
!isset($field['type']) ||
!in_array($field['type'], array(2, 3, 7, 9, 12))
) {
return false; // get out of here if not a node supported field type
}
migrate_resource_type_field_check($field);
$field['node_options'] = [];
$nodes = get_nodes($field['ref'], null, $field['type'] == FIELD_TYPE_CATEGORY_TREE, null, null, null, null, (bool)$field['automatic_nodes_ordering']);
foreach ($nodes as $node) {
$field['node_options'][$node['ref']] = $node;
}
return true;
}
/**
* Adds node keyword for indexing purposes
*
* @param integer $node ID of the node (from node table) the keyword should be linked to
* @param string $keyword Keyword to index
* @param integer $position The position of the keyword in the string that was indexed
* @param boolean $normalized If this keyword is normalized by the time we add it, set as true
*
* @return boolean
*/
function add_node_keyword($node, $keyword, $position, $normalize = true, $stem = true)
{
global $noadd, $stemming;
debug("add_node_keyword: node:" . $node . ", keyword: " . $keyword . ", position: " . $position . ", normalize:" . ($normalize ? "TRUE" : "FALSE") . ", stem:" . ($stem ? "TRUE" : "FALSE"));
$unstemmed = $keyword;
if ($stem && $stemming && function_exists("GetStem")) {
$keyword = GetStem($keyword);
if ($keyword != $unstemmed) {
// $keyword has been changed by stemming, also index the original value
debug("add_node_keyword - adding unstemmed: " . $unstemmed);
add_node_keyword($node, $unstemmed, $position, $normalize, false);
}
}
// $keyword should not be indexed if it can be found in the $noadd array, no need to continue
if (in_array($unstemmed, $noadd)) {
debug('Ignored keyword "' . $keyword . '" as it is in the $noadd array. Triggered in ' . __FUNCTION__ . '() on line ' . __LINE__);
return false;
}
$keyword_ref = resolve_keyword($keyword, true, $normalize, false); // We have already stemmed
ps_query("INSERT INTO node_keyword (node, keyword, position) VALUES (?, ?, ?)", array("i",$node,"i",$keyword_ref,"i",$position));
return true;
}
/**
* Removes node keyword for indexing purposes
*
* @param integer $node ID of the node (from node table) the keyword should be linked to
* @param string $keyword Keyword to index
* @param integer $position The position of the keyword in the string that was indexed
* @param boolean $normalized If this keyword is normalized by the time we add it, set as true
*
* @return void
*/
function remove_node_keyword($node, $keyword, $position, $normalized = false)
{
global $noadd;
if (!$normalized) {
$keyword = normalize_keyword($keyword);
}
$keyword_ref = resolve_keyword($keyword, true);
$parameters = array("i",$node,"i",$keyword_ref);
$position_sql = '';
if ('' != trim($position)) {
$position_sql = " AND position = ?";
$parameters[] = "i";
$parameters[] = $position;
}
ps_query("DELETE FROM node_keyword WHERE node = ? AND keyword = ? $position_sql", $parameters);
ps_query("UPDATE keyword SET hit_count = hit_count - 1 WHERE ref = ?", array("i",$keyword_ref));
}
/**
* Removes all indexed keywords for a specific node ID
*
* @param integer $node Node ID
*
* @return void
*/
function remove_all_node_keyword_mappings($node)
{
ps_query("DELETE FROM node_keyword WHERE node = ?", array("i",$node));
}
/**
* Function used to check if a fields' node needs (re-)indexing
*
* @param array $node Individual node for a field ( as returned by get_nodes() )
* @param boolean $partial_index Partially index flag for node keywords
*
* @return void
*/
function check_node_indexed(array $node, $partial_index = false)
{
if ('' === trim($node['name'])) {
return;
}
$count_indexed_node_keywords = ps_value("SELECT count(node) AS 'value' FROM node_keyword WHERE node = ?", array("i", $node['ref']), 0);
$keywords = split_keywords($node['name'], true, $partial_index);
if ($count_indexed_node_keywords == count($keywords)) {
// node has already been indexed
return;
}
// (re-)index node
remove_all_node_keyword_mappings($node['ref']);
add_node_keyword_mappings($node, $partial_index);
}
/**
* Function used to index node keywords
*
* @param array $node Individual node for a field ( as returned by get_nodes() )
* @param boolean|null $partial_index Partially index flag for node keywords. Use NULL if code doesn't
* have access to the fields' data
*
* @return boolean
*/
function add_node_keyword_mappings(array $node, $partial_index = false, bool $is_date = false, bool $is_html = false)
{
global $node_keyword_index_chars,$index_whole_field;
if ('' == trim($node['ref']) && '' == trim($node['name']) && '' == trim($node['resource_type_field'])) {
return false;
}
$field_data = get_field($node['resource_type_field']);
if (isset($field_data['complete_index']) && $field_data['complete_index']) {
add_node_keyword($node['ref'], $node['name'], 0);
return true;
}
// Client code does not know whether field is partially indexed or not
if (is_null($partial_index)) {
if (isset($field_data['partial_index']) && '' != trim($field_data['partial_index'])) {
$partial_index = $field_data['partial_index'];
}
}
// Check for translations and split as necessary
if (substr($node['name'], 0, 1) == "~") {
$translations = array_filter(i18n_get_translations($node['name']));
} else {
$translations[] = $node['name'];
}
$in_transaction = $GLOBALS['sql_transaction_in_progress'] ?? false;
if (!$in_transaction) {
db_begin_transaction("add_node_keyword_mappings");
}
foreach ($translations as $translation) {
// Only index the first 500 characters
$translation = mb_substr($translation, 0, $node_keyword_index_chars);
$keywords = split_keywords($translation, true, $partial_index, $is_date, $is_html);
add_verbatim_keywords($keywords, $translation, $node['resource_type_field']);
for ($n = 0; $n < count($keywords); $n++) {
unset($keyword_position);
if (is_array($keywords[$n])) {
$keyword_position = $keywords[$n]['position'];
$keywords[$n] = $keywords[$n]['keyword'];
}
if (!isset($keyword_position)) {
$keyword_position = $n;
}
add_node_keyword($node['ref'], $keywords[$n], $keyword_position);
}
}
if (!$in_transaction) {
db_end_transaction("add_node_keyword_mappings");
}
return true;
}
/**
* Function used to un-index node keywords
*
* @param array $node Individual node for a field ( as returned by get_nodes() )
* @param boolean|null $partial_index Partially index flag for node keywords. Use NULL if code doesn't
* have access to the fields' data
*
* @return boolean
*/
function remove_node_keyword_mappings(array $node, $partial_index = false)
{
if ('' == trim($node['ref']) && '' == trim($node['name']) && '' == trim($node['resource_type_field'])) {
return false;
}
// Client code does not know whether field is partially indexed or not
if (is_null($partial_index)) {
$field_data = get_field($node['resource_type_field']);
if (isset($field_data['partial_index']) && '' != trim($field_data['partial_index'])) {
$partial_index = $field_data['partial_index'];
}
}
$keywords = split_keywords($node['name'], true, $partial_index);
add_verbatim_keywords($keywords, $node['name'], $node['resource_type_field']);
for ($n = 0; $n < count($keywords); $n++) {
unset($keyword_position);
if (is_array($keywords[$n])) {
$keyword_position = $keywords[$n]['position'];
$keywords[$n] = $keywords[$n]['keyword'];
}
if (!isset($keyword_position)) {
$keyword_position = $n;
}
remove_node_keyword($node['ref'], $keywords[$n], $keyword_position);
}
return true;
}
/**
* Add nodes in array to resource
*
* @param integer $resourceid Resource ID to add nodes to
* @param array $nodes Array of node IDs to add
* @param boolean $checkperms Check permissions before adding?
* @param boolean $logthis Log this? Log entries are ideally added when more data on all the changes made is available to make reverts easier.
*
* @return boolean
*/
function add_resource_nodes(int $resourceid, $nodes = array(), $checkperms = true, $logthis = true)
{
global $userref;
if (!is_array($nodes) && (string)(int)$nodes != $nodes) {
return false;
}
if (count($nodes) == 0) {
return false;
}
$sql = '';
$sql_params = [];
# check $nodes array values are positive integers and valid for int type node db field
$options_db_int = [ 'options' => [ 'min_range' => 1, 'max_range' => 2147483647] ];
foreach ($nodes as $node) {
if (!filter_var($node, FILTER_VALIDATE_INT, $options_db_int)) {
return false;
}
$sql .= ',(?, ?)';
$sql_params[] = 'i';
$sql_params[] = $resourceid;
$sql_params[] = 'i';
$sql_params[] = $node;
}
$sql = ltrim($sql, ',');
if ($checkperms && (PHP_SAPI != 'cli' || defined("RS_TEST_MODE"))) {
// Need to check user has permissions to add nodes (unless running from any CLI script other than unit tests)
$resourcedata = get_resource_data($resourceid);
if (!$resourcedata) {
return false;
}
$access = get_edit_access($resourceid, $resourcedata["archive"], $resourcedata);
if (!$access) {
return false;
}
if ($resourcedata["lock_user"] > 0 && $resourcedata["lock_user"] != $userref) {
return false;
}
}
if (!is_array($nodes)) {
$nodes = array($nodes);
}
ps_query("INSERT INTO resource_node(resource, node) VALUES {$sql} ON DUPLICATE KEY UPDATE hit_count=hit_count", $sql_params);
if ($logthis) {
$field_nodes_arr = array();
foreach ($nodes as $node) {
$nodedata = array();
get_node($node, $nodedata);
if ($nodedata) {
$field_nodes_arr[$nodedata["resource_type_field"]][] = $nodedata["name"];
}
}
foreach ($field_nodes_arr as $key => $value) {
resource_log($resourceid, "e", $key, "", "", implode(NODE_NAME_STRING_SEPARATOR, $value));
}
}
return true;
}
/**
* Add nodes in array to multiple resources. Changes made using this function will not be logged by default.
*
* @param array $resources Array of resource IDs to add nodes to
* @param array $nodes Array of node IDs to add
* @param boolean $checkperms Check permissions before adding?
* @param boolean $logthis Log this? Log entries are ideally added when more data on all the changes made is available to make reverts easier.
*
* @return boolean
*/
function add_resource_nodes_multi($resources = array(), $nodes = array(), $checkperms = true, bool $logthis = false)
{
global $userref, $lang;
if ((!is_array($resources) && (string)(int)$resources != $resources) || (!is_array($nodes) && (string)(int)$nodes != $nodes)) {
return false;
}
$resources = array_values(array_filter($resources, 'is_int_loose'));
$nodes = array_values(array_filter(is_array($nodes) ? $nodes : [$nodes], 'is_int_loose'));
if ($checkperms) {
// Need to check user has permissions to add nodes
foreach ($resources as $resourceid) {
$resourcedata = get_resource_data($resourceid);
$access = get_edit_access($resourceid, $resourcedata["archive"], $resourcedata);
if (!$access) {
return false;
}
if ($resourcedata["lock_user"] > 0 && $resourcedata["lock_user"] != $userref) {
return false;
}
}
}
$resources_chunks = array_chunk($resources, SYSTEM_DATABASE_IDS_CHUNK_SIZE);
$done = 0;
foreach ($resources_chunks as $resources_chunk) {
if (PHP_SAPI !== "cli") {
set_processing_message(str_replace(["[done]","[total]"], [$done,count($resources)], $lang["processing_updating_resources"]));
}
$done += count($resources_chunk);
$resource_node_values = '';
$sql_params = [];
foreach ($resources_chunk as $resource) {
foreach ($nodes as $node) {
$resource_node_values .= ',(?, ?)';
$sql_params[] = 'i';
$sql_params[] = $resource;
$sql_params[] = 'i';
$sql_params[] = $node;
}
if ($logthis && !empty($nodes)) {
log_node_changes($resource, $nodes, []);
}
}
$resource_node_values = ltrim($resource_node_values, ',');
if ($resource_node_values !== '') {
ps_query("INSERT INTO resource_node (resource, node) VALUES {$resource_node_values} ON DUPLICATE KEY UPDATE hit_count=hit_count", $sql_params);
}
}
return true;
}
/**
* Get nodes associated with a particular resource for all / a specific field (optionally)
*
* @param integer $resource
* @param integer $resource_type_field
* @param boolean $detailed Set to true to return full node details (as get_node() does)
* @param boolean $node_sort Set to SORT_ASC to sort nodes ascending, SORT_DESC sort nodes descending, null means do not sort
*
* @return array
*/
function get_resource_nodes($resource, $resource_type_field = null, $detailed = false, $node_sort = null)
{
$sql_select = 'n.ref AS `value`';
if ($detailed) {
$sql_select = columns_in("node", "n");
// Add code to get translated names
$params = [];
add_sql_node_language($sql_select, $params, "n");
}
$query = "SELECT {$sql_select} FROM node AS n INNER JOIN resource_node AS rn ON n.ref = rn.node WHERE rn.resource = ?";
$params[] = 'i';
$params[] = $resource;
if (!is_null($resource_type_field) && is_numeric($resource_type_field)) {
$query .= " AND n.resource_type_field = ?";
$params[] = 'i';
$params[] = $resource_type_field;
}
if (!is_null($node_sort)) {
if ($node_sort == SORT_ASC) {
$query .= " ORDER BY n.ref ASC";
}
if ($node_sort == SORT_DESC) {
$query .= " ORDER BY n.ref DESC";
}
} else {
$query .= " ORDER BY n.resource_type_field, n.order_by ASC";
}
return $detailed ? ps_query($query, $params) : ps_array($query, $params);
}
/**
* Get all resource nodes associated for a specific resource type field.
*
* @param integer $ref Resource type field ID
*
* @return Generator
*/
function get_resources_nodes_by_rtf(int $ref)
{
$offset = null;
do {
$rows = 1000;
$sql_limit = sql_limit($offset, $rows);
$offset += $rows;
$parameters = [];
$sql = columns_in("node", "n");
add_sql_node_language($sql, $parameters, "n");
$parameters = array_merge($parameters, ['i', $ref]);
$data = ps_query(
"SELECT " . $sql . "
FROM resource_node AS rn
INNER JOIN node AS n ON rn.node = n.ref AND n.resource_type_field = ?
INNER JOIN resource AS r ON rn.resource = r.ref
$sql_limit",
$parameters
);
foreach ($data as $page_data) {
yield $page_data;
}
} while (!empty($data) && count($data) === $rows);
}
/**
* Delete nodes in array from resource
*
* @param integer $resourceid Resource ID to add nodes to
* @param array $nodes Array of node IDs to remove
* @param boolean $logthis Log this? Log entries are ideally added when more data on all changes made is available to make reverts easier.
*
* @return void
*/
function delete_resource_nodes(int $resourceid, $nodes = array(), $logthis = true)
{
if (!is_array($nodes)) {
$nodes = array($nodes);
}
$nodes = array_filter($nodes, 'is_int_loose');
$nodes_count = count($nodes);
if ($nodes_count === 0) {
return;
}
$chunks = array_chunk($nodes, SYSTEM_DATABASE_IDS_CHUNK_SIZE);
foreach ($chunks as $chunk) {
ps_query(
'DELETE FROM resource_node WHERE resource = ? AND node IN (' . ps_param_insert(count($chunk)) . ')',
array_merge(['i', $resourceid], ps_param_fill($chunk, 'i'))
);
}
if ($logthis) {
$field_nodes_arr = array();
foreach ($nodes as $node) {
$nodedata = array();
get_node($node, $nodedata);
if ($nodedata) {
$field_nodes_arr[$nodedata["resource_type_field"]][] = $nodedata["name"];
}
}
foreach ($field_nodes_arr as $key => $value) {
resource_log($resourceid, "e", $key, "", "," . implode(",", $value), '');
}
}
}
/**
* Delete all node relationships matching the passed resource IDs and node IDs.
*
* @param array $resources An array of resource IDs
* @param mixed $nodes An integer or array of single/multiple nodes
* @return void
*/
function delete_resource_nodes_multi($resources = array(), $nodes = array())
{
global $lang;
if (!is_array($nodes)) {
$nodes = array($nodes);
}
$resource_chunks = array_chunk($resources, SYSTEM_DATABASE_IDS_CHUNK_SIZE);
$node_chunks = array_chunk($nodes, SYSTEM_DATABASE_IDS_CHUNK_SIZE);
$done = 0;
foreach ($resource_chunks as $resource_chunk) {
if (PHP_SAPI !== "cli") {
set_processing_message(str_replace(["[done]","[total]"], [$done,count($resources)], $lang["processing_updating_resources"]));
}
$done += count($resource_chunk);
foreach ($node_chunks as $node_chunk) {
$sql = "DELETE FROM resource_node WHERE resource in (" . ps_param_insert(count($resource_chunk)) . ") AND node in (" . ps_param_insert(count($node_chunk)) . ")";
$params = array_merge(ps_param_fill($resource_chunk, "i"), ps_param_fill($node_chunk, "i"));
ps_query($sql, $params);
}
}
}
/**
* Delete all node relationships for the given resource.
*
* @param integer $resourceid The resource ID
* @return void
*/
function delete_all_resource_nodes($resourceid)
{
ps_query("DELETE FROM resource_node WHERE resource = ?", array("i",$resourceid));
}
/**
* Delete all resource node relationships for the given node.
*
* @param integer $node The node ID to remove from all resources.
* @return void
*/
function delete_node_resources(int $node)
{
ps_query("DELETE FROM resource_node WHERE node = ?", array("i", $node));
}
/**
* Copy resource nodes from one resource to another. Only applies for active metadata fields.
*
* @param integer $resourcefrom Resource we are copying data from
* @param integer $resourceto Resource we are copying data to
*/
function copy_resource_nodes($resourcefrom, $resourceto): void
{
$omit_fields_sql = '';
$omit_fields_sql_params = array();
$omitfields = array();
// When copying normal resources from one to another, check for fields that should be excluded
// NOTE: this does not apply to user template resources (negative ID resource)
if ($resourcefrom > 0) {
$omitfields = ps_array("SELECT ref AS `value` FROM resource_type_field WHERE omit_when_copying = 1", array(), "schema");
}
// Exclude fields which user cannot edit "F?" or cannot see "f-?". With config, users permissions maybe overridden for different resource types.
global $userpermissions;
$no_permission_fields = array();
foreach ($userpermissions as $permission_to_check) {
if (substr($permission_to_check, 0, 2) == "f-") {
$no_permission_fields[] = substr($permission_to_check, 2);
} elseif (substr($permission_to_check, 0, 1) == "F") {
$no_permission_fields[] = substr($permission_to_check, 1);
}
}
$omitfields = array_merge($omitfields, array_unique($no_permission_fields));
if (count($omitfields) > 0) {
$omit_fields_sql = " AND n.resource_type_field NOT IN (" . ps_param_insert(count($omitfields)) . ") ";
$omit_fields_sql_params = ps_param_fill($omitfields, "i");
} else {
$omit_fields_sql = "";
}
// This is for logging after the insert statement
$nodes_to_add = ps_array("
SELECT node value
FROM resource_node AS rnold
LEFT JOIN node AS n ON n.ref = rnold.node
WHERE resource = ?
AND n.`active` = 1
{$omit_fields_sql};
", array_merge(array("i", (int) $resourcefrom), $omit_fields_sql_params));
ps_query("
INSERT INTO resource_node(resource, node, hit_count, new_hit_count)
SELECT ?, node, 0, 0
FROM resource_node AS rnold
LEFT JOIN node AS n ON n.ref = rnold.node
LEFT JOIN resource_type_field AS rtf ON n.resource_type_field = rtf.ref
WHERE resource = ?
AND rtf.active = 1
AND n.`active` = 1
{$omit_fields_sql}
ON DUPLICATE KEY UPDATE hit_count = rnold.new_hit_count;
", array_merge(array("i", $resourceto, "i", $resourcefrom), $omit_fields_sql_params));
log_node_changes($resourceto, $nodes_to_add, array());
}
/**
* Copy all nodes from one metadata field to another one.
* Used mostly with copy field functionality
*
* @param integer $from resource_type_field ID FROM which we copy
* @param integer $to resource_type_field ID TO which we copy
*/
function copy_resource_type_field_nodes($from, $to): bool
{
global $FIXED_LIST_FIELD_TYPES;
// Since field has been copied, they are both the same, so we only need to check the from field
$type = ps_value("SELECT `type` AS `value` FROM resource_type_field WHERE ref = ?", array("i", $from), 0, "schema");
if (!in_array($type, $FIXED_LIST_FIELD_TYPES)) {
return false;
}
if (FIELD_TYPE_CATEGORY_TREE == $type) {
$nodes = get_cattree_nodes_ordered($from, null, true);
// Remove the fake "root" node which get_cattree_nodes_ordered() is adding since we won't be
// using get_cattree_node_strings() with it.
array_shift($nodes);
$nodes = array_filter($nodes, 'node_is_active');
// array(from_ref => new_ref)
$processed_nodes = array();
foreach ($nodes as $node) {
if (array_key_exists($node['ref'], $processed_nodes)) {
continue;
}
// Make the parent the expected type of a node parent (i.e. null|int) because get_cattree_nodes_ordered() is
// setting root parent to string zero for some unknown reason.
$parent = $node['parent'] == 0 ? null : $node['parent'];
// Child nodes need to have their parent set to the new parent ID
if ($parent !== null) {
$parent = $processed_nodes[$parent];
}
$new_node_id = set_node(null, $to, $node['name'], $parent, $node['order_by']);
$processed_nodes[$node['ref']] = $new_node_id;
}
return true;
}
// Default handle for types different than category trees
$nodes = array_filter(get_nodes($from), 'node_is_active');
foreach ($nodes as $node) {
set_node(null, $to, $node['name'], $node['parent'], $node['order_by']);
}
return true;
}
/**
* Get all the parent nodes of the given node, all the way back to the top of the node tree.
*
* @param integer $noderef The child node ID
* @param bool $detailed Return all node data? false by default
* @param bool $include_child Include the passed node in the returned array (easier for resolving tree nodes to paths)? false by default
*
* @return array Array of the parent node IDs
*/
function get_parent_nodes(int $noderef, bool $detailed = false, $include_child = false)
{
// Get all parents. Query varies according to MySQL cte support
$mysql_version = ps_query('SELECT LEFT(VERSION(), 3) AS ver');
if (version_compare($mysql_version[0]['ver'], '8.0', '>=')) {
$colsa = $detailed ? "ref, name, parent, resource_type_field, order_by, `active`" : "ref, name, parent";
$colsb = $detailed ? "n.ref, n.name, n.parent, n.resource_type_field, n.order_by, n.`active`" : "n.ref, n.name, n.parent";
$parent_nodes = ps_query(
"
WITH RECURSIVE cte($colsa,level) AS
(
SELECT $colsa,
1 AS level
FROM node
WHERE ref= ?
UNION ALL
SELECT $colsb,
level+1 AS LEVEL
FROM node n
INNER JOIN cte
ON n.ref = cte.parent
)
SELECT $colsa
FROM cte
ORDER BY level ASC;",
['i', $noderef]
);
} else {
$colsa = $detailed ? columns_in("node", "N2") : "ref, name";
$parent_nodes = ps_query(
"
SELECT $colsa
FROM (SELECT @r AS p_ref,
(SELECT @r := parent FROM node WHERE ref = p_ref) AS parent,
@l := @l + 1 AS lvl
FROM (SELECT @r := ?, @l := 0) vars,
node c
WHERE @r <> 0) N1
JOIN node N2
ON N1.p_ref = N2.ref
ORDER BY N1.lvl ASC",
['i', $noderef]
);
}
if (!$include_child) {
$parent_nodes = array_values(array_filter($parent_nodes, function ($node) use ($noderef) {
return $node["ref"] != $noderef;
}));
}
if (!$detailed) {
$parent_nodes = array_column($parent_nodes, "name", "ref");
} else {
for ($n = 0; $n < count($parent_nodes); $n++) {
$parent_nodes[$n]["translated_name"] = i18n_get_translated($parent_nodes[$n]["name"]);
}
}
return $parent_nodes;
}
/**
* Get the total number of nodes for a specific field
*
* @param integer $resource_type_field ID of the metadata field
* @param string $name Filter by name of node
*
* @return integer
*/
function get_nodes_count($resource_type_field, $name = '')
{
$query = "SELECT count(ref) AS `value` FROM node WHERE resource_type_field = ?";
$parameters = array("i",$resource_type_field);
if ('' != $name) {
$query .= " AND `name` LIKE ?";
$parameters[] = "s";
$parameters[] = "%" . $name . "%";
}
return (int) ps_value($query, $parameters, 0);
}
/**
* Extract option names (in raw form if desired) from a nodes array.
*
* @param array $nodes Array of nodes as returned by get_nodes()
* @param boolean $i18n Set to false if you don't need to translate the option name
* @param boolean $index_with_node_id Set to false if you don't want a map between node ID and its name
*
* @return array
*/
function extract_node_options(array $nodes, $i18n = true, $index_with_node_id = true)
{
if (0 == count($nodes)) {
return array();
}
$return = array();
foreach ($nodes as $node) {
$value = $node['name'];
if ($i18n) {
$value = i18n_get_translated($node['name']);
}
if ($index_with_node_id) {
$return[$node['ref']] = $value;
continue;
}
$return[] = $value;
}
return $return;
}
/**
* Search an array of nodes by name
*
* Useful to avoid querying the database multiple times
* if we already have a full detail array of nodes
*
* @uses i18n_get_translated()
*
* @param array $nodes Nodes array as returned by get_nodes()
* @param string $name Filter by name of node
* @param boolean $i18n Use the translated option value?
*
* @return array
*/
function get_node_by_name(array $nodes, $name, $i18n = true)
{
if (0 == count($nodes) || is_null($name) || '' == trim($name)) {
return array();
}
$name = mb_strtolower($name);
foreach ($nodes as $node) {
$option = $node['name'];
if ($i18n) {
$option = i18n_get_translated($node['name']);
}
if ($name === mb_strtolower($option)) {
return $node;
}
}
return array();
}
/**
* Return a node ID for a given string
*
* @param string $value The node name to return
* @param integer $resource_type_field The field to search
*
* @return false|int false = not found
* integer = node ID of matching keyword.
*/
function get_node_id($value, $resource_type_field)
{
// Finding a match MUST distinguish nodes which are different only by diacritics or casing
$node = ps_query(
'SELECT ref FROM node WHERE resource_type_field = ? AND `name` = BINARY(?)',
[
'i',$resource_type_field,
's',$value,
]
);
return count($node) > 0 ? $node[0]['ref'] : false;
}
/**
* Comparator function for uasort to allow sorting of node array by translated name
*
* @param array $n1 Node one to compare
* @param string $n2 Node two to compare
*
* @return 0 means $n1 equals $n2
* <0 means $n1 less than $n2
* >0 means $n1 greater than $n2
*/
function node_translated_name_comparator($n1, $n2)
{
return strcmp(
$n1["translated_name"] ?: i18n_get_translated($n1["name"]),
$n2["translated_name"] ?: i18n_get_translated($n2["name"])
);
}
/**
* Comparator function for uasort to allow sorting of node array by order_by field
*
* @param array $n1 Node one to compare
* @param string $n2 Node two to compare
*
* @return 0 means $n1 equals $n2
* <0 means $n1 less than $n2
* >0 means $n1 greater than $n2
*/
function node_orderby_comparator($n1, $n2)
{
return $n1["order_by"] - $n2["order_by"];
}
/**
*
* This function returns an array containing list of values for a selected field, identified by $field_label, in the multidimensional array $nodes
*
* @param array $nodes - node tree to parse
* @param string $field_label - node field to retrieve value of and add to array $node_values
* @param array $node_values - list of values for a selected field in the node tree
*
* @return array $node_values
*/
function get_node_elements(array $node_values, array $nodes, $field_label)
{
if (isset($nodes[0])) {
foreach ($nodes as $node) {
if (isset($node["name"])) {
array_push($node_values, $node[$field_label]);
}
$node_values = (isset($node["children"])) ? get_node_elements($node_values, $node["children"], $field_label) : get_node_elements($node_values, $node, $field_label);
}
}
return $node_values;
}
/**
* This function returns a multidimensional array with hierarchy that reflects category tree field hierarchy, using parent and order_by fields
*
* @param string $parentId - elements at top of tree do not have a value for "parent" field, so default value is empty string, otherwise it is the value of the parent element in tree
* @param array $nodes - node tree to parse and order
*
* @return array $tree - multidimension array containing nodes in correct hierarchical order
*
*/
function get_node_tree($parentId = "", array $nodes = array())
{
$tree = array();
foreach ($nodes as $node) {
if ($node["parent"] == $parentId) {
$children = get_node_tree($node["ref"], $nodes);
if ($children) {
uasort($children, "node_orderby_comparator");
$node["children"] = $children;
}
$tree[] = $node;
}
}
return $tree;
}
/**
* This function returns an array of category tree nodes in the hierarchical sequence defined in manage options
*
* @param array $treefield - the category tree field to be processed
* @param integer $resource - the resource against which to check for selected nodes - optional
* @param array $allnodes - is true if all nodes in the structure are returned, false if only selected nodes are returned
*
* @return array $flatnodes - the array of nodes returned in correct hierarchical order
*
*/
function get_cattree_nodes_ordered($treefield, $resource = null, $allnodes = false)
{
$sql_query = "SELECT n.ref, n.resource_type_field, n.name, coalesce(n.parent, 0) parent, n.order_by, n.active, rn.resource FROM node n ";
if ($allnodes) {
$sql_query .= " LEFT OUTER ";
}
$sql_query .= "JOIN resource_node rn on rn.resource = ? and rn.node = n.ref WHERE n.resource_type_field=? order by n.parent, n.order_by";
$nodeentries = ps_query($sql_query, array("i", (int) $resource, "i", (int) $treefield));
# Any node that doesn't have a parent in the nodes supplied becomes a parent in this context as its real parent might not have been selected.
# For example, when viewing options set when $category_tree_add_parents=false
# Needed for sorting below to ensure the container "ROOT" has child items to return.
$selected_nodes = array_column($nodeentries, 'ref');
for ($n = 0; $n < count($nodeentries); ++$n) {
if ($nodeentries[$n]['parent'] !== 0 && !in_array($nodeentries[$n]['parent'], $selected_nodes)) {
$nodeentries[$n]['parent'] = 0;
}
}
# Category trees have no container root, so create one to carry all top level category tree nodes which don't have a parent
$rootnode = cattree_node_creator(0, 0, "ROOT", null, 0, null, array(), 1);
$nodeswithpointers = array(0 => &$rootnode);
foreach ($nodeentries as $nodeentry) {
$ref = $nodeentry['ref'];
$resource_type_field = $nodeentry['resource_type_field'];
$name = $nodeentry['name'];
$parent = $nodeentry['parent'];
$order_by = $nodeentry['order_by'];
$resource = $nodeentry['resource'];
$active = $nodeentry['active'] ?? 1;
# Save the current node prior to establishing the pointer which can null the current node
$savednode = null;
if (isset($nodeswithpointers[$ref])) {
$savednode = $nodeswithpointers[$ref];
}
# Establish a pointer so that this node will be a child of its parent node
# This means that the current node entry will be "added" to the children of the parent entry
$nodeswithpointers[$ref] = &$nodeswithpointers[$parent]['children'][];
# Create an entry for the current node with any existing children at this point
$existingchildren = array();
if ($savednode && isset($savednode['children'])) {
$existingchildren = $savednode['children'];
}
$nodeswithpointers[$ref] = cattree_node_creator($ref, $resource_type_field, $name, $parent, $order_by, $resource, $existingchildren, $active);
}
# Flatten the tree starting at the root
$flatnodes = cattree_node_flatten($rootnode);
$returned_nodes = array();
foreach ($flatnodes as $flatnode) {
if ($allnodes || $flatnode['resource'] != '') {
$returned_nodes[$flatnode['ref']] = $flatnode;
}
}
return $returned_nodes;
}
/**
* This function returns an array of category tree node strings in the hierarchical sequence defined in manage options
* The returned strings are i18 translated
*
* @param array $nodesordered - the array of nodes in correct hierarchical order
* @param array $strings_are_paths - governs the format of the name returned
* True (default) strings are paths to nodes; False strings are the individual node names
*
* @return array $strings - the returned array of node paths or node names
*
*/
function get_cattree_node_strings($nodesordered, $strings_are_paths = true)
{
# If names are not to be returned as paths, just return the individual node names
if (!$strings_are_paths) {
$strings_as_names = array();
foreach ($nodesordered as $node) {
$strings_as_names[] = i18n_get_translated($node["name"]);
}
return $strings_as_names;
}
# Build a string consisting of a comma separated list of individual nodes and paths of consecutive child nodes
$strings_as_paths = array();
# Establish a list of parents referenced by the nodes
$parents_referenced = array_column($nodesordered, 'name', 'parent');
# Establish a list of referenced parents which are in the list
$parents_listed = array_intersect_key($nodesordered, $parents_referenced);
# Processing is driven by each leaf node (ie. nodes with no selected children)
foreach ($nodesordered as $node) {
if (!array_key_exists($node['ref'], $parents_listed)) {
# This selected node is effectively a leaf node because it has no selected children
# This leaf node is the first entry in the leafpath
$leafpath = array(i18n_get_translated($node["name"]));
$parenttofind = $node['parent'];
# Append consecutive selected ancestors to the leafpath
while (isset($parenttofind)) {
if ($parenttofind == 0) { # Ignore root node
$parenttofind = null;
continue;
}
# If current node's parent is listed then append it to the leafpath
if (array_key_exists($parenttofind, $parents_listed)) {
$leafpath[] = i18n_get_translated($parents_listed[$parenttofind]['name']);
$parenttofind = $parents_listed[$parenttofind]['parent'];
} else {
# Current node's parent is not listed so this leafpath is complete
$parenttofind = null;
}
}
$leafpathstring = implode("/", array_reverse($leafpath));
$strings_as_paths[] = $leafpathstring;
}
}
return $strings_as_paths;
}
/**
* Helper function for building node entry arrays for ordering
*
* @param int $ref Node id
* @param int $resource_type_field Category tree field id
* @param string $name Node name
* @param int $parent Parent node id
* @param int $order_by Node order by
* @param int $resource Resource id
* @param array $children Array of child node ids
* @param int $active Node active state (0 or 1)
*
* @return array
*/
function cattree_node_creator($ref, $resource_type_field, $name, $parent, $order_by, $resource, $children, int $active): array
{
return [
'ref' => $ref,
'resource_type_field' => $resource_type_field,
'name' => $name,
'parent' => $parent,
'order_by' => $order_by,
'resource' => $resource,
'children' => $children,
'active' => $active,
];
}
/**
* Helper function which adds child nodes after each flattened parent node
*
* @param array $node Array of nodes each with a child node array
*
* @return array Array of nodes with child nodes flattened out after their respective parents
*/
function cattree_node_flatten(array $node): array
{
# Build node being flattened
$flat_element = [
'ref' => (string) $node['ref'],
'resource_type_field' => (string) $node['resource_type_field'],
'name' => (string) $node['name'],
'parent' => (string) $node['parent'],
'order_by' => (string) $node['order_by'],
'resource' => (string) $node['resource'],
'active' => (int) $node['active'],
];
$children = $node['children'] ?? [];
# Append children after flattened node
$cumulative_entries = array($flat_element);
foreach ($children as $child) {
$cumulative_entries = array_merge($cumulative_entries, cattree_node_flatten($child));
}
return $cumulative_entries;
}
/**
* This function returns an array of strings that represent the full paths to each tree node passed
*
* @param array $resource_nodes - node tree to parse
* @param bool $allnodes - include paths to all nodes -if false will just include the paths to the end leaf nodes
* @param bool $translate - translate strings?
*
* @return array $nodestrings - array of strings for all nodes passed in correct hierarchical order
*
*/
function get_node_strings($resource_nodes, $allnodes = false, $translate = true): array
{
// Arrange all passed nodes with parents first so that unnecessary paths can be removed
$orderednodes = order_tree_nodes($resource_nodes);
// Create an array of all branch nodes for each node
$nodestrings = array();
foreach ($orderednodes as $resource_node) {
$path = $translate ? $resource_node["translated_path"] : $resource_node["path"];
if (!$allnodes && isset($nodestrings[$resource_node["parent"]])) {
unset($nodestrings[$resource_node["parent"]]);
}
$nodestrings[$resource_node["ref"]] = $path;
}
return $nodestrings;
}
/**
* Get to the root of the branch starting from a node.
*
* IMPORTANT: the term nodes here is generic, it refers to a tree node structure containing at least ref and parent
*
* @param array $nodes List of nodes to search through (MUST contain elements with at least the "ref" index)
* @param integer $id Node ref we compute the branch path for
*
* @return array Branch path structure starting from root to the searched node (inclusive)
*/
function compute_node_branch_path(array $nodes, int $id)
{
if (empty($nodes)) {
return array();
}
global $NODE_BRANCH_PATHS_CACHE;
$NODE_BRANCH_PATHS_CACHE = (!is_null($NODE_BRANCH_PATHS_CACHE) && is_array($NODE_BRANCH_PATHS_CACHE) ? $NODE_BRANCH_PATHS_CACHE : array());
// create a unique ID for this list of nodes since these can be used for anything
$nodes_list_id = md5(json_encode($nodes));
if (isset($NODE_BRANCH_PATHS_CACHE[$nodes_list_id][$id])) {
return $NODE_BRANCH_PATHS_CACHE[$nodes_list_id][$id];
}
$nodes_ref_list = array_column($nodes, 'ref');
$found_node_index = array_search($id, $nodes_ref_list);
if ($found_node_index === false) {
return array();
}
$node = $nodes[$found_node_index];
$node_parent = (isset($node["parent"]) && $node["parent"] > 0 ? (int) $node["parent"] : null);
$path = array($node);
while (!is_null($node_parent)) {
// Parent node path is already known (cached), use it instead of recalculating it
if (isset($NODE_BRANCH_PATHS_CACHE[$nodes_list_id][$node_parent])) {
$parent_branch_reversed = array_reverse($NODE_BRANCH_PATHS_CACHE[$nodes_list_id][$node_parent]);
$path = array_merge($path, $parent_branch_reversed);
break;
}
$found_node_index = array_search($node_parent, $nodes_ref_list);
if ($found_node_index === false) {
break;
}
$node = $nodes[$found_node_index];
$node_parent = (isset($node["parent"]) && $node["parent"] > 0 ? (int) $node["parent"] : null);
$path[] = $node;
}
$path_reverse = array_reverse($path);
$NODE_BRANCH_PATHS_CACHE[$nodes_list_id][$id] = $path_reverse;
return $path_reverse;
}
/**
* Find all nodes with parent
*
* @param array $nodes List of nodes to search through (MUST contain elements with at least the "parent" index)
* @param integer $id Parent node ref to search by
*
* @return array
*/
function compute_nodes_by_parent(array $nodes, int $id): array
{
$found_nodes_keys = array_keys(array_column($nodes, 'parent'), $id);
$result = array();
foreach ($found_nodes_keys as $nodes_key) {
if (!isset($nodes[$nodes_key])) {
continue;
}
$result[] = $nodes[$nodes_key];
}
return $result;
}
/**
* Get all nodes for given resources and fields. Returns a multidimensional array wth resource IDs as top level indexes and field IDs as second level indexes
*
* @param array $resources
* @param array $resource_type_fields
* @param boolean $detailed Set to true to return full node details (as get_node() does)
* @param boolean $node_sort Set to SORT_ASC to sort nodes ascending, SORT_DESC sort nodes descending, null means do not sort
*
* @return array
*/
function get_resource_nodes_batch(array $resources, array $resource_type_fields = array(), bool $detailed = false, $node_sort = null)
{
$sql_select = "rn.resource, n.ref, n.resource_type_field ";
if ($detailed) {
$sql_select .= ', n.`name`, n.parent, n.order_by, n.`active`';
}
$resources = array_filter($resources, "is_int_loose");
if (empty($resources)) {
return [];
}
$chunks = array_chunk($resources, SYSTEM_DATABASE_IDS_CHUNK_SIZE);
$noderows = [];
foreach ($chunks as $chunk) {
$query = "SELECT {$sql_select} FROM resource_node rn LEFT JOIN node n ON n.ref = rn.node WHERE rn.resource IN (" . ps_param_insert(count($chunk)) . ")";
$query_params = ps_param_fill($chunk, "i");
if (is_array($resource_type_fields) && count($resource_type_fields) > 0) {
$fields = array_filter($resource_type_fields, "is_int_loose");
if (count($fields) > 0) {
$query .= " AND n.resource_type_field IN (" . ps_param_insert(count($fields)) . ")";
$query_params = array_merge($query_params, ps_param_fill($fields, "i"));
}
}
if (!is_null($node_sort)) {
if ($node_sort == SORT_ASC) {
$query .= " ORDER BY n.ref ASC";
}
if ($node_sort == SORT_DESC) {
$query .= " ORDER BY n.ref DESC";
}
}
$newnoderows = ps_query($query, $query_params);
$noderows = array_merge($noderows, $newnoderows);
}
$results = array();
foreach ($noderows as $noderow) {
if (!isset($results[$noderow["resource"]])) {
$results[$noderow["resource"]] = array();
}
if (!isset($results[$noderow["resource"]][$noderow["resource_type_field"]])) {
$results[$noderow["resource"]][$noderow["resource_type_field"]] = array();
}
if ($detailed) {
$results[$noderow["resource"]][$noderow["resource_type_field"]][] = [
"ref" => $noderow["ref"],
"resource_type_field" => $noderow["resource_type_field"],
"name" => $noderow["name"],
"parent" => $noderow["parent"],
"order_by" => $noderow["order_by"],
'active' => $noderow['active'],
];
} else {
$results[$noderow["resource"]][$noderow["resource_type_field"]][] = $noderow["ref"];
}
}
return $results;
}
/**
* Process one of the columns whose value is a search string containing nodes (e.g @@228@229, @@555) and mutate input array
* by adding a new column (named $column + '_node_name') which will hold the nodes found in the search string and their
* translated names
*
* @param array $R Generic type for array (e.g DB results). Each value is a result row.
* @param string $column Record column which needs to be checked and its value converted (if applicable)
*
* @return array
*/
function process_node_search_syntax_to_names(array $R, string $column)
{
$all_nodes = [];
$record_node_buckets = [];
foreach ($R as $idx => $record) {
if (!(is_array($record) && isset($record[$column]))) {
continue;
}
$search = $record[$column];
$node_bucket = $node_bucket_not = [];
resolve_given_nodes($search, $node_bucket, $node_bucket_not);
// Build list of nodes identified (so we can get their details later)
foreach ($node_bucket as $node_refs) {
$all_nodes = array_merge($all_nodes, $node_refs);
}
$all_nodes = array_merge($all_nodes, $node_bucket_not);
// Add node buckets found for this record
$record_node_buckets[$idx] = [
'node_bucket' => $node_bucket,
'node_bucket_not' => $node_bucket_not,
];
}
// Translate nodes
$node_details = get_nodes_by_refs(array_unique($all_nodes));
$i18l_nodes = [];
foreach ($node_details as $node) {
$i18l_nodes[$node['ref']] = i18n_get_translated($node['name']);
}
// Convert the $column value to URL
$new_col_name = "{$column}_node_name";
$syntax_desc_tpl = '%s - "%s" ';
foreach ($R as $idx => $record) {
// mutate array - add a new column for all records
$R[$idx][$new_col_name] = '';
if (!(is_array($record) && isset($record[$column]) && isset($record_node_buckets[$idx]))) {
continue;
}
foreach ($record_node_buckets[$idx] as $bucket_type => $node_buckets) {
$prefix = ($bucket_type === 'node_bucket' ? NODE_TOKEN_PREFIX : NODE_TOKEN_PREFIX . NODE_TOKEN_NOT);
foreach ($node_buckets as $node_ref) {
$nodes = (is_array($node_ref) ? $node_ref : [$node_ref]);
foreach ($nodes as $node) {
if (!isset($i18l_nodes[$node])) {
continue;
}
$R[$idx][$new_col_name] .= sprintf($syntax_desc_tpl, "{$prefix}{$node}", $i18l_nodes[$node]);
}
}
}
}
return $R;
}
/**
* Delete unused non-fixed list field nodes with a 1:1 resource association
*
* @param integer $resource_type_field Resource type field (metadata field) ID
*/
function delete_unused_non_fixed_list_nodes(int $resource_type_field)
{
if ($resource_type_field <= 0) {
return;
}
// Delete nodes that no longer have a resource association
ps_query(
'DELETE n
FROM node AS n
INNER JOIN resource_type_field AS rtf ON n.resource_type_field = rtf.ref
LEFT JOIN resource_node AS rn ON rn.node = n.ref
WHERE n.resource_type_field = ?
AND rtf.`type`IN (' . ps_param_insert(count(NON_FIXED_LIST_SINGULAR_RESOURCE_VALUE_FIELD_TYPES)) . ')
AND rn.node IS NULL',
array_merge(['i', $resource_type_field], ps_param_fill(NON_FIXED_LIST_SINGULAR_RESOURCE_VALUE_FIELD_TYPES, 'i'))
);
remove_invalid_node_keyword_mappings();
}
/**
* Delete invalid node_keyword associations. Note, by invalid, it's meant where the node is missing.
*/
function remove_invalid_node_keyword_mappings()
{
ps_query('DELETE nk FROM node_keyword AS nk LEFT JOIN node AS n ON n.ref = nk.node WHERE n.ref IS NULL');
}
/**
* Delete invalid resource_node associations. Note, by invalid, it's meant where the node is missing.
*/
function remove_invalid_resource_node_mappings()
{
ps_query('DELETE rn FROM resource_node AS rn LEFT JOIN node AS n ON n.ref = rn.node WHERE n.ref IS NULL');
}
/**
* Get a count of how many resources are using the specified nodes
*
* @param array $nodes Array of node refs
*
* @return array Array of node ref as keys and number of resources using them as the values
*/
function get_nodes_use_count(array $nodes)
{
$nodes = array_filter($nodes, 'is_int_loose');
if (empty($nodes)) {
return [];
}
$nodes_use_count = ps_query(
'SELECT node, COUNT(node) AS `use_count` FROM resource_node WHERE node IN (' . ps_param_insert(count($nodes)) . ') GROUP BY node',
ps_param_fill($nodes, 'i')
);
return array_column($nodes_use_count, 'use_count', 'node');
}
/**
* Check array of nodes and delete any that relate to non-fixed list fields and are unused
*
* @param array $nodes Array of node IDs
*/
function check_delete_nodes($nodes)
{
global $FIXED_LIST_FIELD_TYPES;
debug_function_call('check_delete_nodes', func_get_args());
// Check and delete unused nodes
$count = get_nodes_use_count($nodes);
foreach ($nodes as $node) {
$nodeinfo = [];
get_node($node, $nodeinfo);
if (isset($nodeinfo["resource_type_field"])) {
$fieldinfo = get_resource_type_field($nodeinfo["resource_type_field"]);
debug("check_delete_nodes: checking node " . $node . " - (" . $nodeinfo["name"] . ")");
if (
!in_array($fieldinfo["type"], $FIXED_LIST_FIELD_TYPES)
&& (!isset($count[$node]) || $count[$node] == 0)
) {
debug("Deleting unused node #" . $node . " - (" . $nodeinfo["name"] . ")");
delete_node($node);
}
}
}
}
/**
* Delete all keywords for all nodes associated with the specified field
*
* @param integer $field Field ID
*
* @return void
*/
function remove_field_keywords($field)
{
ps_query("DELETE nk FROM node_keyword nk LEFT JOIN node n ON n.ref=nk.node WHERE n.resource_type_field = ?", ["i",$field]);
}
/**
* For the specified $resource, increment the hitcount for each node in array
*
* @param integer $resource
* @param array $nodes
* @return void
*/
function update_resource_node_hitcount($resource, $nodes)
{
if (!is_array($nodes)) {
$nodes = array($nodes);
}
if (count($nodes) > 0) {
ps_query("UPDATE resource_node SET new_hit_count = new_hit_count + 1 WHERE resource = ? AND node IN (" . ps_param_insert(count($nodes)) . ")", array_merge(array("i", $resource), ps_param_fill($nodes, "i")), false, -1, true, 0);
}
}
/**
* Order array of tree nodes into logical order - Each parent followed by its child nodes, all following order_by
*
* @param array $nodes Array of detailed nodes
*
* @return array Full nodes in order
*
*/
function order_tree_nodes($nodes, $order_by_translated_name = false)
{
if (count($nodes) == 0) {
return [];
}
if ($order_by_translated_name) {
$sort = 'node_translated_name_comparator';
} else {
$sort = 'node_orderby_comparator';
}
// Find parent nodes first
$parents = array_column($nodes, "parent");
$toplevels = min($parents) > 0 ? $parents : [0];
$orderednodes = array_values(array_filter($nodes, function ($node) use ($toplevels) {
return in_array((int)$node["parent"], $toplevels);
}));
if (!$order_by_translated_name) {
usort($orderednodes, $sort);
}
for ($n = 0; $n < count($orderednodes); $n++) {
$orderednodes[$n]["path"] = $orderednodes[$n]["name"];
if (!isset($orderednodes[$n]["translated_name"]) || is_i18n_language_string($orderednodes[$n]["translated_name"])) {
// Where translated_path is in i18n format, add_sql_node_language() couldn't find a match with the current language. Translate it to get default language.
$orderednodes[$n]["translated_path"] = i18n_get_translated($orderednodes[$n]["name"]);
} else {
$orderednodes[$n]["translated_path"] = $orderednodes[$n]["translated_name"];
}
}
// Find child nodes
$parents_processed = [];
while (count($nodes) > 0) {
// Loop to find children
for ($n = 0; $n < count($orderednodes); $n++) {
if (!in_array($orderednodes[$n]["ref"], $parents_processed)) {
// Add the children of this node with the the path added (relative to paremnt)
$children = array_filter($nodes, function ($node) use ($orderednodes, $n) {
return (int)$node["parent"] == $orderednodes[$n]["ref"];
});
// Set order
uasort($children, $sort);
$children = array_values($children);
for ($c = 0; $c < count($children); $c++) {
$children[$c]["path"] = $orderednodes[$n]["path"] . "/" . $children[$c]["name"];
if (!isset($children[$c]["translated_name"]) || is_i18n_language_string($children[$c]["translated_name"])) {
// Where translated_path is in i18n format, add_sql_node_language() couldn't find a match with the current language. Translate it to get default language.
$children[$c]["translated_path"] = $orderednodes[$n]["translated_path"] . "/" . i18n_get_translated($children[$c]["name"]);
} else {
$children[$c]["translated_path"] = $orderednodes[$n]["translated_path"] . "/" . ($children[$c]["translated_name"]);
}
// Insert the child after the parent and any nodes with a lower order_by value
array_splice($orderednodes, $n + 1 + $c, 0, [$children[$c]]);
// Remove child from $treenodes
$pos = array_search($children[$c]["ref"], array_column($nodes, "ref"));
unset($nodes[$pos]);
$nodes = array_values($nodes);
}
$parents_processed[] = $orderednodes[$n]["ref"];
} else {
$pos = array_search($orderednodes[$n]["ref"], array_column($nodes, "ref"));
unset($nodes[$pos]);
}
}
$nodes = array_values($nodes);
}
return $orderednodes;
}
/**
* Append SQL to an existing node query to obtain the translated names of the node
*
* @param string $sql_select SQL query
* @param array $sql_params Array of SQL parameters
*
* @return void
*
*/
function add_sql_node_language(&$sql_select, &$sql_params, string $alias = "node")
{
global $language,$defaultlanguage;
// Use language specified, if not use default
isset($language) ? $language_in_use = $language : $language_in_use = $defaultlanguage;
// Get length of language string + 2 (for ~ and :) for usage in SQL below
$language_string_length = (strlen($language_in_use) + 2);
$sql_params = array_merge($sql_params, [
"s","~" . $language_in_use,
"s","~" . $language_in_use . ":",
"i",$language_string_length,
"s","~" . $language_in_use . ":",
"i",$language_string_length,
"s","~" . $language_in_use . ":",
"i",$language_string_length,
]);
$sql_select .= ",
CASE
WHEN
POSITION(? IN " . $alias . ".name) > 0
THEN
TRIM(SUBSTRING(name,
POSITION(? IN " . $alias . ".name) + ?,
CASE
WHEN
POSITION('~' IN SUBSTRING(" . $alias . ".name,
POSITION(? IN " . $alias . ".name) + ?,
LENGTH(" . $alias . ".name) - 1)) > 0
THEN
POSITION('~' IN SUBSTRING(" . $alias . ".name,
POSITION(? IN " . $alias . ".name) + ?,
LENGTH(" . $alias . ".name) - 1)) - 1
ELSE LENGTH(" . $alias . ".name)
END))
ELSE TRIM(" . $alias . ".name)
END AS translated_name";
}
/**
* Migrate fixed list field data to text field data for a given resource reference. Useful when changing resource type field from a data type
* that can contain multiple values such as a dynamic keywords field. This script will concatenate the existing values and leave one remaining
* node for the new text field.
*
* @param mixed $resource_type_field Resource type field id. ** The field type should have been changed to a text type in advance
* - additional checks maybe need before calling this to ensure the fields are / were of the expected type.
* see examples in pages/tools/migrate_fixed_to_text.php **
* @param mixed $resource Resource reference to be processed.
* @param mixed $category_tree Was the field data being migrated previously of type category tree? Specifying true will allow the format
* of category tree branches to be preserved e.g. "level1/value, level2/value"
* @param mixed $separator Default is comma and space e.g. "value1, value2"
*
* @return bool True on success else false.
*/
function migrate_fixed_to_text(int $resource_type_field, int $resource, bool $category_tree, string $separator = ', '): bool
{
$current_nodes = get_resource_nodes($resource, $resource_type_field, true, SORT_ASC); # Ordering will be as displayed on the view page.
if (count($current_nodes) < 2) {
return true; # No need to make changes as no node / only one node present.
}
if ($category_tree) {
$all_treenodes = get_cattree_nodes_ordered($resource_type_field, $resource, false);
$treenodenames = get_cattree_node_strings($all_treenodes, true);
$new_value = implode($separator, $treenodenames);
} else {
$current_nodes_names = array_column($current_nodes, 'name');
$current_nodes_translated = array_map("i18n_get_translated", $current_nodes_names);
$new_value = implode($separator, $current_nodes_translated);
}
delete_resource_nodes($resource, array_column($current_nodes, 'ref'), false);
$savenode = set_node(null, $resource_type_field, $new_value, null, 0);
return add_resource_nodes($resource, [$savenode], true, false);
}
/**
* Remove invalid field data from resources, optionally just for the specified resource types and/or fields
*
* @param array $fields=[] Array of resource_type_field refs
* @param array $restypes=[] Array of resource_type refs
* @param bool $dryrun Don't delete, just return count of rows that will be affected
*
* @return int Count of rows deleted/to delete
*
*/
function cleanup_invalid_nodes(array $fields = [], array $restypes = [], bool $dryrun = false)
{
$allrestypes = get_resource_types('', false, false, true);
$allrestyperefs = array_column($allrestypes, "ref");
$allfields = get_resource_type_fields();
$fieldglobals = array_column($allfields, "global", "ref");
$joined_fields = get_resource_table_joins();
$restypes = array_filter($restypes, function ($val) {
return $val > 0;
});
$fields = array_filter($fields, function ($val) {
return $val > 0;
});
$fields = count($fields) > 0 ? array_intersect($fields, array_column($allfields, "ref")) : array_column($allfields, "ref");
$restypes = count($restypes) > 0 ? array_intersect($restypes, $allrestyperefs) : $allrestyperefs;
$restype_mappings = get_resource_type_field_resource_types();
$deletedrows = 0;
foreach ($restypes as $restype) {
if (!in_array($restype, $allrestyperefs)) {
continue;
}
// Find invalid fields for this resource type
$remove_fields = [];
foreach ($fields as $field) {
if (!in_array($field, array_column($allfields, "ref"))) {
continue;
}
if ((int)$fieldglobals[$field] == 0 && !in_array($restype, $restype_mappings[$field])) {
$remove_fields[] = $field;
}
}
if (count($remove_fields) > 0) {
if ($dryrun) {
$query = "SELECT COUNT(*) AS value FROM resource_node LEFT JOIN resource r ON r.ref=resource_node.resource LEFT JOIN node n ON n.ref=resource_node.node WHERE r.resource_type = ? AND n.resource_type_field IN (" . ps_param_insert(count($remove_fields)) . ");";
$params = array_merge(["i",$restype], ps_param_fill($remove_fields, "i"));
$deletedrows = ps_value($query, $params, 0);
} else {
$query = "DELETE rn.* FROM resource_node rn LEFT JOIN resource r ON r.ref=rn.resource LEFT JOIN node n ON n.ref=rn.node WHERE r.resource_type = ? AND n.resource_type_field IN (" . ps_param_insert(count($remove_fields)) . ");";
$params = array_merge(["i",$restype], ps_param_fill($remove_fields, "i"));
ps_query($query, $params);
$deletedrows += sql_affected_rows();
# Also remove data in joined fields.
foreach ($remove_fields as $check_joined_field) {
if (in_array($check_joined_field, $joined_fields)) {
ps_query("UPDATE resource SET `field" . $check_joined_field . "` = null WHERE resource_type = ?", array("i", $restype));
}
}
}
}
}
return $deletedrows > 0 ? ((!$dryrun ? "Deleted " : "Found ") . $deletedrows . " row(s)") : "No rows found";
}
/**
* Batch update nodes' active state to the database. The same state will apply to all nodes in the list.
*
* For logic on which nodes to toggle {@see toggle_active_state_for_nodes()}
*
* @param list $refs Node IDs
* @param bool $active Should nodes be active or not?
*/
function update_node_active_state(array $refs, bool $active): void
{
if ($refs === []) {
return;
}
$refs_chunked = db_chunk_id_list($refs);
if ($refs_chunked === []) {
return;
}
db_begin_transaction('set_node_active_state');
foreach ($refs_chunked as $refs_chunk) {
ps_query(
sprintf(
'UPDATE node AS n
INNER JOIN resource_type_field AS rtf ON n.resource_type_field = rtf.ref
SET n.`active` = ?
WHERE n.`ref` IN (%s)
AND rtf.`type` IN (%s)',
ps_param_insert(count($refs_chunk)),
ps_param_insert(count($GLOBALS['FIXED_LIST_FIELD_TYPES']))
),
array_merge(
['i', $active],
ps_param_fill($refs_chunk, 'i'),
ps_param_fill($GLOBALS['FIXED_LIST_FIELD_TYPES'], 'i')
)
);
}
db_end_transaction('set_node_active_state');
}
/**
* Toggle nodes' active state
*
* @param list $refs Node IDs
* @return array
*/
function toggle_active_state_for_nodes(array $refs): array
{
$refs_chunked = db_chunk_id_list($refs);
$rtfs_tree = array_column(get_resource_type_fields('', 'ref', 'asc', '', [FIELD_TYPE_CATEGORY_TREE]), 'ref');
$tree_nodes_by_rtf = [];
$nodes_new_state = [];
foreach ($refs_chunked as $refs_chunk) {
db_begin_transaction('toggle_node_active_state');
$nodes = get_nodes_by_refs($refs_chunk);
$nodes_to_toggle = [];
foreach ($nodes as $node) {
if (in_array($node['resource_type_field'], $rtfs_tree)) {
// Build a list of nodes, grouped by resource type fields because changes to a tree need to follow its structure
$tree_nodes_by_rtf[$node['resource_type_field']][] = $node['ref'];
} else {
// Simple fixed list fields get toggled straight away
$nodes_to_toggle[] = $node['ref'];
}
}
if ($nodes_to_toggle !== []) {
ps_query(
sprintf(
'UPDATE node AS n
INNER JOIN resource_type_field AS rtf ON n.resource_type_field = rtf.ref
SET n.`active` = IF(n.`active` = 1, 0, 1)
WHERE n.`ref` IN (%s)
AND rtf.`type` IN (%s)',
ps_param_insert(count($nodes_to_toggle)),
ps_param_insert(count($GLOBALS['FIXED_LIST_FIELD_TYPES']))
),
array_merge(ps_param_fill($nodes_to_toggle, 'i'), ps_param_fill($GLOBALS['FIXED_LIST_FIELD_TYPES'], 'i'))
);
}
db_end_transaction('toggle_node_active_state');
if ($nodes_to_toggle !== []) {
$nodes_new_state += array_column(get_nodes_by_refs($nodes_to_toggle), 'active', 'ref');
}
}
foreach ($tree_nodes_by_rtf as $rtf => $nodes_list) {
$nodes_new_state += toggle_category_tree_nodes_active_state($rtf, $nodes_list);
}
clear_query_cache('schema');
return $nodes_new_state;
}
/**
* Toggle category tree nodes' active state
*
* @param int $rtf Resource type field ID
* @param list $node_refs
* @return array
*/
function toggle_category_tree_nodes_active_state(int $rtf, array $node_refs): array
{
$is_not_active = fn(int $active): bool => $active === 0;
$nodes_to_enable = [];
$nodes_to_disable = [];
$rtf_nodes = get_cattree_nodes_ordered($rtf, null, true);
// Remove the fake "root" node which get_cattree_nodes_ordered() is adding since we won't be
// using get_cattree_node_strings() with it.
array_shift($rtf_nodes);
$rtf_nodes_indexed = array_column($rtf_nodes, null, 'ref');
$nodes_ordered = array_keys(array_intersect_key($rtf_nodes_indexed, array_flip($node_refs)));
foreach ($nodes_ordered as $node_ref) {
if (in_array($node_ref, $nodes_to_disable)) {
// Child node disabled previously by a parent - no need to carry on processing it (already done because of
// a parents' change on its path)
continue;
}
$node = $rtf_nodes_indexed[$node_ref];
$node_is_active = $node['active'] === 1;
// Check that no parent on this branch path is disabled
$node_branch = array_column(
compute_node_branch_path(array_values($rtf_nodes_indexed), $node['ref']),
'active',
'ref'
);
array_pop($node_branch);
if (array_filter($node_branch, $is_not_active) !== []) {
// Node should already be disabled, add it to the list so we can return it
$nodes_to_disable[] = $node['ref'];
continue;
}
if ($node_is_active) {
// Disable - this MUST propagate to its children (if any)
$branch_refs = array_column(
cattree_node_flatten(
array_merge(
$node,
[
'resource' => null, # Fake it for cattree_node_flatten()
'children' => get_node_tree($node['ref'], $rtf_nodes_indexed),
]
)
),
'ref'
);
$nodes_to_disable = array_merge($nodes_to_disable, $branch_refs);
foreach ($branch_refs as $path_node_ref) {
$rtf_nodes_indexed[$path_node_ref]['active'] = 0;
}
} else {
// Activate - this MUST NOT propagate to its children (if any)
$nodes_to_enable[] = $node_ref;
// Pretend this has been done in case there's going to be a child node in the queue waiting to be toggled
$rtf_nodes_indexed[$node_ref]['active'] = 1;
}
}
update_node_active_state($nodes_to_enable, true);
update_node_active_state($nodes_to_disable, false);
return array_column(get_nodes_by_refs(array_merge($nodes_to_enable, $nodes_to_disable)), 'active', 'ref');
}
/** Helper function to check if a node is active
* Note: a node here requires the "active" key.
* @param array{'active': 0|1} $node A node structure with at least the "active" key present
*/
function node_is_active(array $node): bool
{
return $node['active'] === 1;
}
/**
* Suggest dynamic keyword nodes. Used by pages/edit_fields/9_ajax/suggest_keywords.php
*
* @param int $field Metadata field ID
* @param string $keyword String to match
* @param bool $readonly Add an option to add new node if no exact match
*
*/
function suggest_dynamic_keyword_nodes(int $field, string $keyword, bool $readonly): array
{
if (checkperm("bdk" . $field) || !metadata_field_edit_access($field)) {
// Not permitted to add new nodes
$readonly = true;
}
$fielddata = get_resource_type_field($field);
if (
!$fielddata
|| !metadata_field_view_access($field)
) {
return [];
}
$nodes = get_nodes($field);
// Return matches
$exactmatch = false;
$results = [];
$match_is_deactivated = false;
// Set $keywords_remove_diacritics so as to only add versions with diacritics to return array if none are in the submitted string
$GLOBALS['keywords_remove_diacritics'] = mb_strlen($keyword) === strlen($keyword);
$keyword = normalize_keyword($keyword);
foreach ($nodes as $node) {
$trans = i18n_get_translated($node['name'], true);
$compare = normalize_keyword($trans);
$node_is_active = node_is_active($node);
if ($GLOBALS['dynamic_keyword_suggest_contains']) {
if (
'' != $trans
&& (
!isset($GLOBALS['dynamic_keyword_suggest_contains_characters'])
|| $GLOBALS['dynamic_keyword_suggest_contains_characters'] <= mb_strlen($keyword)
)
&& mb_strpos(mb_strtolower($compare), mb_strtolower($keyword)) !== false
) {
if (mb_strtolower($compare) == mb_strtolower($keyword)) {
$exactmatch = true;
$match_is_deactivated = !$node_is_active;
}
if ($node_is_active) {
$results[] = [
'label' => $trans,
'value' => $node['ref']
];
}
}
} else {
if ('' != $compare && mb_substr(mb_strtolower($compare), 0, mb_strlen($keyword)) == mb_strtolower($keyword)) {
if (mb_strtolower($compare) == mb_strtolower($keyword)) {
$exactmatch = true;
$match_is_deactivated = !$node_is_active;
}
if ($node_is_active) {
$results[] = [
'label' => $trans,
'value' => $node['ref']
];
}
}
}
}
$keyword = stripslashes($keyword);
$fielderror = false;
if (!$exactmatch && !$readonly) {
# Ensure regexp filter is honoured if one is present
if (
strlen(trim((string)$fielddata["regexp_filter"])) >= 1
&& preg_match("#^" . str_replace($GLOBALS['regexp_slash_replace'], '\\', $fielddata["regexp_filter"]) . "$#", $keyword, $matches) <= 0
) {
$fielderror = true;
}
if (!$fielderror) {
$results[] = array(
'label' => "{$GLOBALS['lang']['createnewentryfor']} {$keyword}",
'value' => "{$GLOBALS['lang']['createnewentryfor']} {$keyword}"
);
} else {
$results[] = array(
'label' => "{$GLOBALS['lang']['keywordfailedregexfilter']} {$keyword}",
'value' => "{$GLOBALS['lang']['keywordfailedregexfilter']} {$keyword}"
);
}
} elseif ($exactmatch && $match_is_deactivated) {
$text = "{$GLOBALS['lang']['inactive_entry_matched']} {$keyword}";
$results = [
[
'label' => $text,
'value' => $text,
]
];
} elseif ($readonly && empty($results)) {
$results[] = array(
'label' => "{$GLOBALS['lang']['noentryexists']} {$keyword}",
'value' => "{$GLOBALS['lang']['noentryexists']} {$keyword}"
);
}
return $results;
}