Files
resourcespace/include/collections_functions.php
2025-07-18 16:20:14 +07:00

6947 lines
282 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
# Collections functions
# Functions to manipulate collections
use Montala\ResourceSpace\CommandPlaceholderArg;
/**
* Return all collections belonging to or shared with $user
*
* @param integer $user
* @param string $find A search string
* @param string $order_by Column to sort by
* @param string $sort ASC or DESC sort order
* @param integer $fetchrows How many rows to fetch
* @param boolean $auto_create Create a standard "Default Collection" if one doesn't exist
* @return array
*/
function get_user_collections($user, $find = "", $order_by = "name", $sort = "ASC", $fetchrows = -1, $auto_create = true)
{
global $usergroup, $themes_in_my_collections, $rs_session;
global $anonymous_login,$username,$anonymous_user_session_collection;
$condsql = "";
$condparams = [];
$keysql = "";
$keyparams = [];
$extrasql = "";
$extraparams = [];
$sort = strtoupper($sort) == "ASC" ? "ASC" : "DESC";
if ($find == "!shared") {
# only return shared collections
$condsql = " WHERE (c.`type` = ? OR c.ref IN (SELECT DISTINCT collection FROM user_collection WHERE user<>? UNION SELECT DISTINCT collection FROM external_access_keys))";
$condparams = array("i",COLLECTION_TYPE_PUBLIC,"i",$user);
} elseif (strlen($find) == 1 && !is_numeric($find)) {
# A-Z search
$condsql = " WHERE c.name LIKE ?";
$condparams = array("s",$find . "%");
} elseif (strlen($find) > 1 || is_numeric($find)) {
$keywords = split_keywords($find);
$keysql = "";
$keyparams = array();
for ($n = 0; $n < count($keywords); $n++) {
$keyref = resolve_keyword($keywords[$n], false);
if ($keyref === false) {
continue;
}
$keysql .= " JOIN collection_keyword k" . $n . " ON k" . $n . ".collection=ref AND (k" . $n . ".keyword=?)";
$keyparams = array_merge($keyparams, ['i', $keyref]);
}
}
$validtypes = [COLLECTION_TYPE_STANDARD, COLLECTION_TYPE_PUBLIC, COLLECTION_TYPE_REQUEST];
if ($themes_in_my_collections) {
$validtypes[] = COLLECTION_TYPE_FEATURED;
}
$condsql .= $condsql == "" ? "WHERE" : " AND";
$condsql .= " c.`type` IN (" . ps_param_insert(count($validtypes)) . ")";
$condparams = array_merge($condparams, ps_param_fill($validtypes, "i"));
if ($themes_in_my_collections) {
// If we show featured collections, remove the categories
$keysql .= " WHERE (clist.`type` IN (?,?,?) OR (clist.`type` = ? AND clist.`count` > 0))";
$keyparams[] = "i";
$keyparams[] = COLLECTION_TYPE_STANDARD;
$keyparams[] = "i";
$keyparams[] = COLLECTION_TYPE_PUBLIC;
$keyparams[] = "i";
$keyparams[] = COLLECTION_TYPE_REQUEST;
$keyparams[] = "i";
$keyparams[] = COLLECTION_TYPE_FEATURED;
}
if (isset($anonymous_login) && ($username == $anonymous_login) && $anonymous_user_session_collection) {
// Anonymous user - only get the user's own collections that are for this session - although we can still join to
// get collections that have been specifically shared with the anonymous user
if ('' == $condsql) {
$extrasql = " WHERE ";
} else {
$extrasql .= " AND ";
}
$extrasql .= " (c.session_id=?)";
$extraparams = array("i",$rs_session);
}
$order_sort = "";
$validsort = array("name","ref","user","created","public","home_page_publish","type","parent");
if ($order_by != "name" && in_array(strtolower($order_by), $validsort)) {
$order_sort = " ORDER BY $order_by $sort";
}
// Control the selected columns. $query_select_columns is for the outer SQL and $collection_select_columns
// is for the inner one. Both have some extra columns from the user & resource table.
$collection_table_columns = [
'ref',
'name',
'user',
'created',
'public',
'allow_changes',
'cant_delete',
'keywords',
'savedsearch',
'home_page_publish',
'home_page_text',
'home_page_image',
'session_id',
'description',
'type',
'parent',
'thumbnail_selection_method',
'bg_img_resource_ref',
'order_by',
];
$query_select_columns = implode(', ', $collection_table_columns) . ', username, fullname, count';
$collection_select_columns = [];
foreach ($collection_table_columns as $column_name) {
$collection_select_columns[] = "c.{$column_name}";
}
$collection_select_columns = implode(', ', $collection_select_columns) . ', u.username, u.fullname, count(r.resource) AS count';
$query = "SELECT {$query_select_columns}
FROM (
SELECT {$collection_select_columns}
FROM user AS u
JOIN collection AS c ON u.ref = c.user AND c.user = ?
LEFT OUTER JOIN collection_resource AS r ON c.ref = r.collection
$condsql
$extrasql
GROUP BY c.ref
UNION
SELECT {$collection_select_columns}
FROM user_collection AS uc
JOIN collection AS c ON uc.collection = c.ref AND uc.user = ? AND c.user <> ?
LEFT OUTER JOIN collection_resource AS r ON c.ref = r.collection
LEFT JOIN user AS u ON c.user = u.ref
$condsql
GROUP BY c.ref
UNION
SELECT {$collection_select_columns}
FROM usergroup_collection AS gc
JOIN collection AS c ON gc.collection = c.ref AND gc.usergroup = ? AND c.user <> ?
LEFT OUTER JOIN collection_resource AS r ON c.ref = r.collection
LEFT JOIN user AS u ON c.user = u.ref
$condsql
GROUP BY c.ref
) AS clist
$keysql
GROUP BY ref $order_sort";
$queryparams = array_merge(
array("i",$user),
$condparams,
$extraparams,
array("i", $user,"i", $user),
$condparams,
array("i", $usergroup,"i",$user),
$condparams,
$keyparams
);
$return = ps_query($query, $queryparams, 'collection_access' . $user);
if ($order_by == "name") {
if ($sort == "ASC") {
usort($return, 'collections_comparator');
} elseif ($sort == "DESC") {
usort($return, 'collections_comparator_desc');
}
}
// To keep Default Collection creation consistent: Check that user has at least one collection of his/her own (not if collection result is empty, which may include shares),
$hasown = false;
for ($n = 0; $n < count($return); $n++) {
if ($return[$n]['user'] == $user) {
$hasown = true;
}
}
if (!$hasown && $auto_create && $find == "") { # User has no collections of their own, and this is not a search. Make a new 'Default Collection'
# No collections of one's own? The user must have at least one Default Collection
global $usercollection;
$usercollection = create_collection($user, "Default Collection", 0, 1); // make not deletable
set_user_collection($user, $usercollection);
# Recurse to send the updated collection list.
return get_user_collections($user, $find, $order_by, $sort, $fetchrows, false);
}
return $return;
}
$GLOBALS['get_collection_cache'] = array();
/**
* Returns all data for collection $ref.
*
* @param int $ref Collection ID
* @param bool $usecache Optionally retrieve from cache
*
* @return array|boolean
*/
function get_collection($ref, $usecache = false)
{
global $lang, $userref,$k;
if (isset($GLOBALS['get_collection_cache'][$ref]) && $usecache) {
return $GLOBALS['get_collection_cache'][$ref];
}
$columns = ", u.fullname, u.username";
$return = ps_query("SELECT " . columns_in('collection', 'c') . $columns . " FROM collection c LEFT OUTER JOIN user u ON u.ref = c.user WHERE c.ref = ?", array("i",$ref));
if (count($return) == 0) {
return false;
} else {
$return = $return[0];
$users = ps_array("SELECT u.username value FROM user u,user_collection c WHERE u.ref=c.user AND c.collection = ? ORDER BY u.username", array("i",$ref));
$return["users"] = join(", ", $users);
$groups = ps_array("SELECT concat('" . $lang["groupsmart"] . ": ',u.name) value FROM usergroup u,usergroup_collection c WHERE u.ref = c.usergroup AND c.collection = ? ORDER BY u.name", array("i",$ref));
$return["groups"] = join(", ", $groups);
$request_feedback = 0;
if ($return["user"] != $userref) {
# If this is not the user's own collection, fetch the user_collection row so that the 'request_feedback' property can be returned.
$request_feedback = ps_value("SELECT request_feedback value FROM user_collection WHERE collection = ? AND user = ?", array("i",$ref,"i",$userref), 0);
if (!$request_feedback && $k == "") {
# try to set via usergroup_collection
global $usergroup;
$request_feedback = ps_value("SELECT request_feedback value FROM usergroup_collection WHERE collection = ? AND usergroup = ?", array("i",$ref,"i",$usergroup), 0);
}
}
if ($k != "") {
# If this is an external user (i.e. access key based) then fetch the 'request_feedback' value from the access keys table
$request_feedback = ps_value("SELECT request_feedback value FROM external_access_keys WHERE access_key = ? AND request_feedback = 1", array("s",$k), 0);
}
$return["request_feedback"] = $request_feedback;
// Legacy property which is now superseded by types. FCs need to be public before they can be put under a category by an admin (perm h)
global $COLLECTION_PUBLIC_TYPES;
$return["public"] = (int) in_array($return["type"], $COLLECTION_PUBLIC_TYPES);
$GLOBALS['get_collection_cache'][$ref] = $return;
return $return;
}
}
/**
* Returns all resources in collection
*
* @param int $collection ID of collection being requested
*
* @return array|boolean
*/
function get_collection_resources($collection)
{
global $userref;
# For many cases (e.g. when displaying a collection for a user) a search is used instead so permissions etc. are honoured.
if (!is_int_loose($collection)) {
return false;
}
# Check if review collection if so delete any resources moved out of users archive status permissions by other users
if ((string)$collection == "-" . $userref) {
collection_cleanup_inaccessible_resources($collection);
}
$plugin_collection_resources = hook('replace_get_collection_resources', "", array($collection));
if (is_array($plugin_collection_resources)) {
return $plugin_collection_resources;
}
return ps_array("SELECT resource value FROM collection_resource WHERE collection = ? ORDER BY sortorder ASC, date_added DESC, resource ASC", array("i",$collection));
}
/**
* Get all resources in a collection without checking permissions or filtering by workflow states.
* This is useful when you want to get all the resources for further subprocessing (@see render_selected_collection_actions()
* as an example)
*
* @param integer $ref Collection ID
*
* @return array
*/
function get_collection_resources_with_data($ref)
{
if (!is_numeric($ref)) {
return array();
}
$result = ps_query(
"
SELECT r.*
FROM collection_resource AS cr
RIGHT JOIN resource AS r ON cr.resource = r.ref
WHERE cr.collection = ?
ORDER BY cr.sortorder ASC , cr.date_added DESC , cr.resource DESC
",
array("i",$ref)
);
if (!is_array($result)) {
return array();
}
return $result;
}
/**
* Add resource $resource to collection $collection
*
* @param integer $resource
* @param integer $collection
* @param boolean $smartadd
* @param string $size
* @param string $addtype
* @param boolean $col_access_control Collection access control. Is user allowed to add to it? You can leave it null
* to allow this function to determine it but it may have performance issues.
* @param array $external_shares List of external share keys. {@see get_external_shares()}. You can leave it null
* to allow this function to determine it but it will affect performance.
* @param string $search Optionsl search string. Used to update resource_node hit count
*
* @param integer $sort_order Sort order of resource in collection
*
* @return boolean | string
*/
function add_resource_to_collection(
$resource,
$collection,
$smartadd = false,
$size = "",
$addtype = "",
?bool $col_access_control = null,
?array $external_shares = null,
string $search = '',
?int $sort_order = null
) {
global $lang;
if (!is_int_loose($collection) || !is_int_loose($resource)) {
return $lang["cantmodifycollection"];
}
global $collection_allow_not_approved_share, $collection_block_restypes;
$addpermitted = $col_access_control ?? (
(collection_writeable($collection) && !is_featured_collection_category_by_children($collection))
|| $smartadd
);
if ($addpermitted && !$smartadd && (count($collection_block_restypes) > 0)) { // Can't always block adding resource types since this may be a single resource managed request
if ($addtype == "") {
$addtype = ps_value("SELECT resource_type value FROM resource WHERE ref = ?", ["i",$resource], 0);
}
if (in_array($addtype, $collection_block_restypes)) {
$addpermitted = false;
}
}
if ($addpermitted) {
$collection_data = get_collection($collection, true);
// If this is a featured collection apply all the external access keys from the categories which make up its
// branch path to prevent breaking existing shares for any of those featured collection categories.
$fc_branch_path_keys = [];
if ($collection_data !== false && $collection_data['type'] === COLLECTION_TYPE_FEATURED) {
$branch_category_ids = array_column(
// determine the branch from the parent because the keys for the collection in question will be done below
get_featured_collection_category_branch_by_leaf((int)$collection_data['parent'], []),
'ref'
);
foreach ($branch_category_ids as $fc_category_id) {
$fc_branch_path_keys = array_merge(
$fc_branch_path_keys,
get_external_shares([
'share_collection' => $fc_category_id,
'share_type' => 0,
'ignore_permissions' => true
])
);
}
}
# Check if this collection has already been shared externally. If it has, we must fail if not permitted or add a further entry
# for this specific resource, and warn the user that this has happened.
$keys = array_merge(
$external_shares ?? get_external_shares(array("share_collection" => $collection,"share_type" => 0,"ignore_permissions" => true)),
$fc_branch_path_keys
);
if (count($keys) > 0) {
$archivestatus = ps_value("SELECT archive AS value FROM resource WHERE ref = ?", ["i",$resource], "");
if ($archivestatus < 0 && !$collection_allow_not_approved_share) {
global $lang;
$lang["cantmodifycollection"] = $lang["notapprovedresources"] . $resource;
return false;
}
// Check if user can share externally and has open access. We shouldn't add this if they can't share externally, have restricted access or only been granted access
if (!can_share_resource($resource)) {
return false;
}
# Set the flag so a warning appears.
global $collection_share_warning;
# Check to see if all shares have expired
$expiry_dates = ps_array("SELECT DISTINCT expires value FROM external_access_keys WHERE collection = ?", ["i",$collection]);
$datetime = time();
$collection_share_warning = true;
foreach ($expiry_dates as $date) {
if ($date != "" && $date < $datetime) {
$collection_share_warning = false;
}
}
for ($n = 0; $n < count($keys); $n++) {
# Insert a new access key entry for this resource/collection.
global $userref;
ps_query(
'INSERT INTO external_access_keys(resource, access_key, user, collection, `date`, expires, access, usergroup, password_hash) VALUES (?, ?, ?, ?, now(), ?, ?, ?, ?)',
[
'i', $resource,
's', $keys[$n]['access_key'],
'i', $userref,
'i', $collection ?: null,
's', $keys[$n]['expires'] ?: null,
'i', $keys[$n]['access'],
'i', $keys[$n]['usergroup'] ?: null,
's', $keys[$n]['password_hash'] ?: null,
]
);
collection_log($collection, LOG_CODE_COLLECTION_SHARED_RESOURCE_WITH, $resource, $keys[$n]["access_key"]);
}
}
ps_query('DELETE FROM collection_resource WHERE collection = ? AND resource = ?', ['i', $collection, 'i', $resource]);
ps_query(
'INSERT INTO collection_resource(collection, resource, sortorder) VALUES (?, ?, ?)',
['i', $collection, 'i', $resource, 'i', $sort_order ?: null]
);
# Update the hitcounts for the search nodes (if search specified)
if (strpos($search, NODE_TOKEN_PREFIX) !== false) {
update_node_hitcount_from_search($resource, $search);
}
if ($collection_data !== false && $collection_data['type'] != COLLECTION_TYPE_SELECTION) {
collection_log($collection, LOG_CODE_COLLECTION_ADDED_RESOURCE, $resource);
}
// Clear theme image cache
clear_query_cache("themeimage");
clear_query_cache('col_total_ref_count_w_perm');
return true;
} else {
return $lang["cantmodifycollection"];
}
}
/**
* Remove resource $resource from collection $collection
*
* @param integer $resource
* @param integer $collection
* @param boolean $smartadd
* @return boolean | string
*/
function remove_resource_from_collection($resource, $collection, $smartadd = false)
{
global $lang;
if ((string)(int)$collection != (string)$collection || (string)(int)$resource != (string)$resource) {
return $lang["cantmodifycollection"];
}
if ($smartadd || collection_writeable($collection)) {
$delparams = ["i",$resource,"i",$collection];
ps_query("DELETE FROM collection_resource WHERE resource = ? AND collection = ?", $delparams);
ps_query("DELETE FROM external_access_keys WHERE resource = ? AND collection = ?", $delparams);
// log this
collection_log($collection, LOG_CODE_COLLECTION_REMOVED_RESOURCE, $resource);
// Clear theme image cache
clear_query_cache("themeimage");
clear_query_cache('col_total_ref_count_w_perm');
return true;
} else {
return $lang["cantmodifycollection"];
}
}
/**
* Add resource(s) $resources to collection $collection
*
* @param mixed $resources
* @param mixed $collection
* @return boolean | string
*/
function collection_add_resources($collection, $resources = '', $search = '', $selected = false)
{
global $USER_SELECTION_COLLECTION,$lang;
if (
!is_int_loose($collection)
|| ($resources == '' && $search == '')
|| !collection_writeable($collection)
|| is_featured_collection_category_by_children($collection)
) {
return $lang["cantmodifycollection"];
}
$access_control = true;
$external_share_keys = get_external_shares([
'share_collection' => $collection,
'share_type' => 0,
'ignore_permissions' => true,
]);
if ($selected) {
$resources = get_collection_resources($USER_SELECTION_COLLECTION);
} elseif ($resources == '') {
$resources = do_search($search);
}
if ($resources === false) {
return $lang["noresourcesfound"];
}
if (!is_array($resources)) {
$resources = explode(",", $resources);
}
if (count($resources) == 0) {
return $lang["noresourcesfound"];
}
$collection_resources = get_collection_resources($collection);
$refs_to_add = array_diff($resources, $collection_resources);
$errors = 0;
foreach ($refs_to_add as $ref) {
if (!add_resource_to_collection($ref, $collection, false, '', '', $access_control, $external_share_keys)) {
$errors++;
}
}
if ($errors == 0) {
return true;
} else {
return $lang["cantaddresourcestocolection"];
}
}
/**
* collection_remove_resources
*
* @param mixed $collection
* @param mixed $resources
* @param mixed $removeall
* @return boolean | string
*/
function collection_remove_resources($collection, $resources = '', $removeall = false, $selected = false)
{
global $USER_SELECTION_COLLECTION,$lang;
if (
(string)(int)$collection != (string)$collection
|| ($resources == '' && !$removeall && !$selected)
|| (!collection_writeable($collection))
|| is_featured_collection_category_by_children($collection)
) {
return $lang["cantmodifycollection"];
}
if ($removeall) {
foreach (get_collection_resources($collection) as $ref) {
remove_resource_from_collection($ref, $collection);
}
return true;
}
if ($selected) {
$resources = get_collection_resources($USER_SELECTION_COLLECTION);
}
if ($resources === false) {
return $lang["noresourcesfound"];
}
$collection_resources = get_collection_resources($collection);
if (!is_array($resources)) {
$resources = explode(",", $resources);
}
$refs_to_remove = array_intersect($collection_resources, $resources);
$errors = 0;
foreach ($refs_to_remove as $ref) {
if (!remove_resource_from_collection($ref, $collection)) {
$errors++;
}
}
if ($errors == 0) {
return true;
} else {
return $lang["cantremoveresourcesfromcollection"];
}
}
/**
* Is the collection $collection writable by the current user?
* Returns true if the current user has write access to the given collection.
*
* @param integer $collection
* @return boolean
*/
function collection_writeable($collection)
{
$collectiondata = get_collection($collection);
if ($collectiondata === false) {
return false;
}
global $userref,$usergroup, $allow_smart_collections;
if (
$allow_smart_collections && !isset($userref)
&& isset($collectiondata['savedsearch']) && $collectiondata['savedsearch'] != null
) {
return false; // so "you cannot modify this collection"
}
if ($collectiondata['type'] == COLLECTION_TYPE_REQUEST && !checkperm('R')) {
return false;
}
# Load a list of attached users
$attached = ps_array("SELECT user value FROM user_collection WHERE collection = ?", ["i",$collection]);
$attached_groups = ps_array("SELECT usergroup value FROM usergroup_collection WHERE collection = ?", ["i",$collection]);
// Can edit if
// - The user owns the collection (if we are anonymous user and are using session collections then this must also have the same session id )
// - The user has system setup access (needs to be able to sort out user issues)
// - Collection changes are allowed and :-
// a) User is attached to the collection or
// b) Collection is public or a theme and the user either has the 'h' permission or the collection is editable
global $usercollection,$username,$anonymous_login,$anonymous_user_session_collection, $rs_session;
debug("collection session : " . $collectiondata["session_id"]);
debug("collection user : " . $collectiondata["user"]);
debug("anonymous_login : " . isset($anonymous_login) && is_string($anonymous_login) ? $anonymous_login : "(no)");
debug("userref : " . $userref);
debug("username : " . $username);
debug("anonymous_user_session_collection : " . (($anonymous_user_session_collection) ? "TRUE" : "FALSE"));
$writable =
// User either owns collection AND is not the anonymous user, or is the anonymous user with a matching/no session
($userref == $collectiondata["user"] && (!isset($anonymous_login) || $username != $anonymous_login || !$anonymous_user_session_collection || $collectiondata["session_id"] == $rs_session))
// Collection is public AND either they have the 'h' permission OR allow_changes has been set
|| ((checkperm("h") || $collectiondata["allow_changes"] == 1) && $collectiondata["public"] == 1)
// Collection has been shared but is not public AND user is either attached or in attached group
|| ($collectiondata["allow_changes"] == 1 && $collectiondata["public"] == 0 && (in_array($userref, $attached) || in_array($usergroup, $attached_groups)))
// System admin
|| checkperm("a")
// Adding to active upload_share
|| upload_share_active() == $collection
// This is a request collection and user is an admin user who can approve requests
|| (checkperm("R") && $collectiondata['type'] == COLLECTION_TYPE_REQUEST && checkperm("t"));
// Check if user has permission to manage research requests. If they do and the collection is research request allow writable.
if ($writable === false && checkperm("r")) {
include_once 'research_functions.php';
$research_requests = get_research_requests();
$collections = array();
foreach ($research_requests as $research_request) {
$collections[] = $research_request["collection"];
}
if (in_array($collection, $collections)) {
$writable = true;
}
}
return $writable;
}
/**
* Returns true if the current user has read access to the given collection.
*
* @param integer $collection
* @return boolean
*/
function collection_readable($collection)
{
global $userref, $usergroup, $ignore_collection_access, $collection_commenting;
$k = getval('k', '');
# Fetch collection details.
if (!is_numeric($collection)) {
return false;
}
$collectiondata = get_collection($collection);
if ($collectiondata === false) {
return false;
}
# Load a list of attached users
$attached = ps_array("SELECT user value FROM user_collection WHERE collection = ?", ["i",$collection]);
$attached_groups = ps_array("SELECT usergroup value FROM usergroup_collection WHERE collection = ?", ["i",$collection]);
# Access if collection_commenting is enabled and request feedback checked
# Access if it's a public collection (or featured collection to which user has access to)
# Access if k is not empty or option to ignore collection access is enabled and k is empty
if (
($collection_commenting && $collectiondata['request_feedback'] == 1)
|| $collectiondata['type'] == COLLECTION_TYPE_PUBLIC
|| ($collectiondata['type'] == COLLECTION_TYPE_FEATURED && featured_collection_check_access_control($collection))
|| $k != ""
|| ($k == "" && $ignore_collection_access)
) {
return true;
}
# Perform these checks only if a user is logged in
# Access if:
# - It's their collection
# - It's a public collection (or featured collection to which user has access to)
# - They have the 'access and edit all collections' admin permission
# - They are attached to this collection
# - Option to ignore collection access is enabled and k is empty
if (
is_numeric($userref)
&& ($userref == $collectiondata["user"]
|| $collectiondata['type'] == COLLECTION_TYPE_PUBLIC
|| ($collectiondata['type'] == COLLECTION_TYPE_FEATURED && featured_collection_check_access_control($collection))
|| checkperm("h")
|| in_array($userref, $attached)
|| in_array($usergroup, $attached_groups)
|| checkperm("R")
|| $k != ""
|| ($k == "" && $ignore_collection_access))
) {
return true;
}
return false;
}
/**
* Sets the current collection of $user to be $collection
*
* @param integer $user
* @param integer $collection
* @return void
*/
function set_user_collection($user, $collection)
{
global $usercollection,$username,$anonymous_login,$anonymous_user_session_collection;
if (!(isset($anonymous_login) && $username == $anonymous_login) || !$anonymous_user_session_collection) {
ps_query("UPDATE user SET current_collection = ? WHERE ref = ?", ["i",$collection,"i",$user]);
}
$usercollection = $collection;
}
/**
* Creates a new collection for user $userid called $name
*
* @param integer $userid
* @param string $name
* @param boolean $allowchanges
* @param boolean $cant_delete
* @param integer $ref
* @param boolean $public
* @return integer
*/
function create_collection($userid, $name, $allowchanges = 0, $cant_delete = 0, $ref = 0, $public = false, $extraparams = array())
{
debug_function_call("create_collection", func_get_args());
global $username,$anonymous_login,$rs_session, $anonymous_user_session_collection;
if (($username == $anonymous_login && $anonymous_user_session_collection) || upload_share_active()) {
// We need to set a collection session_id for the anonymous user. Get session ID to create collection with this set
$rs_session = get_rs_session_id(true);
} else {
$rs_session = "";
}
$setcolumns = array();
$extracolopts = array("type",
"keywords",
"saved_search",
"session_id",
"description",
"savedsearch",
"parent",
"thumbnail_selection_method",
);
foreach ($extracolopts as $coloption) {
if (isset($extraparams[$coloption])) {
$setcolumns[$coloption] = $extraparams[$coloption];
}
}
$setcolumns["name"] = mb_strcut($name, 0, 100);
$setcolumns["user"] = is_numeric($userid) ? $userid : 0;
$setcolumns["allow_changes"] = $allowchanges;
$setcolumns["cant_delete"] = $cant_delete;
$setcolumns["public"] = $public ? COLLECTION_TYPE_PUBLIC : COLLECTION_TYPE_STANDARD;
if ($ref != 0) {
$setcolumns["ref"] = (int)$ref;
}
if (is_int_loose(trim($rs_session))) {
$setcolumns["session_id"] = $rs_session;
}
if ($public) {
$setcolumns["type"] = COLLECTION_TYPE_PUBLIC;
}
$insert_columns = array_keys($setcolumns);
$insert_values = array_values($setcolumns);
$sql = "INSERT INTO collection
(" . implode(",", $insert_columns) . ", created)
VALUES
(" . ps_param_insert(count($insert_values)) . ",NOW())";
ps_query($sql, ps_param_fill($insert_values, 's'));
$ref = sql_insert_id();
index_collection($ref);
clear_query_cache('collection_access' . $userid);
return $ref;
}
/**
* Deletes the collection with reference $ref
*
* @param integer $collection
* @return boolean|void
*/
function delete_collection($collection)
{
global $home_dash, $lang;
if (!is_array($collection)) {
$collection = get_collection($collection);
}
if (!$collection) {
return false;
}
$ref = $collection["ref"];
$type = $collection["type"];
if (!collection_writeable($ref) || is_featured_collection_category_by_children($ref)) {
return false;
}
ps_query("DELETE FROM collection WHERE ref=?", array("i",$ref));
ps_query("DELETE FROM collection_resource WHERE collection=?", array("i",$ref));
ps_query("DELETE FROM collection_keyword WHERE collection=?", array("i",$ref));
ps_query("DELETE FROM external_access_keys WHERE collection=?", array("i",$ref));
if ($home_dash) {
// Delete any dash tiles pointing to this collection
$collection_dash_tiles = ps_array("SELECT ref value FROM dash_tile WHERE link LIKE ?", array("s","%search.php?search=!collection" . $ref . "&%"));
if (count($collection_dash_tiles) > 0) {
ps_query("DELETE FROM dash_tile WHERE ref IN (" . ps_param_insert(count($collection_dash_tiles)) . ")", ps_param_fill($collection_dash_tiles, "i"));
ps_query("DELETE FROM user_dash_tile WHERE dash_tile IN (" . ps_param_insert(count($collection_dash_tiles)) . ")", ps_param_fill($collection_dash_tiles, "i"));
}
}
collection_log($ref, LOG_CODE_COLLECTION_DELETED_COLLECTION, 0, $collection["name"] . " (" . $lang["owner"] . ":" . $collection["username"] . ")");
if ($type === COLLECTION_TYPE_FEATURED) {
clear_query_cache("featured_collections");
} else {
/** {@see create_collection()} */
clear_query_cache("collection_access{$collection['user']}");
}
}
/**
* Adds script to page that refreshes the Collection bar
*
* @param integer $collection Collection id
* @return void
*/
function refresh_collection_frame($collection = "")
{
# Refresh the CollectionDiv
global $baseurl, $headerinsert;
if (getval("ajax", false)) {
echo "<script type=\"text/javascript\">
CollectionDivLoad(\"" . $baseurl . "/pages/collections.php" . ((getval("k", "") != "") ? "?collection=" . urlencode(getval("collection", $collection)) . "&k=" . urlencode(getval("k", "")) . "&" : "?") . "nc=" . time() . "\");
</script>";
} else {
$headerinsert .= "<script type=\"text/javascript\">
CollectionDivLoad(\"" . $baseurl . "/pages/collections.php" . ((getval("k", "") != "") ? "?collection=" . urlencode(getval("collection", $collection)) . "&k=" . urlencode(getval("k", "")) . "&" : "?") . "nc=" . time() . "\");
</script>";
}
}
/**
* Performs a search for featured collections / public collections.
*
* @param string $search
* @param string $order_by
* @param string $sort
* @param boolean $exclude_themes
* @param boolean $include_resources
* @param boolean $override_group_restrict
* @param integer $fetchrows
* @return array
*/
function search_public_collections($search = "", $order_by = "name", $sort = "ASC", $exclude_themes = true, $include_resources = false, $override_group_restrict = false, $fetchrows = -1)
{
global $userref,$public_collections_confine_group,$userref,$usergroup;
$keysql = "";
$sql = "";
$sql_params = [];
$select_extra = "";
debug_function_call("search_public_collections", func_get_args());
// Validate sort & order_by
$sort = validate_sort_value($sort) ? $sort : 'ASC';
$valid_order_bys = array("fullname", "name", "ref", "count", "type", "created");
$order_by = (in_array($order_by, $valid_order_bys) ? $order_by : "name");
if (strpos($search, "collectiontitle:") !== false) {
// This includes a specific title search from the advanced search page.
$searchtitlelength = 0;
$searchtitleval = "";
$origsearch = $search;
// Force quotes around any collectiontitle: search to support old behaviour
// i.e. to allow split_keywords() to work
// collectiontitle:*ser * collection* simpleyear:2022
// - will be changed to -
// "collectiontitle:*ser * collection*" simpleyear:2022
$searchstart = mb_substr($search, 0, strpos($search, "collectiontitle:"));
$titlepos = strpos($search, "collectiontitle:") + 16;
$searchend = mb_substr($search, $titlepos);
if (strpos($searchend, ":") !== false) {
// Remove any other parts of the search with xxxxx: prefix that relate to other search aspects
$searchtitleval = explode(":", $searchend)[0];
$searchtitleparts = explode(" ", $searchtitleval);
if (count($searchtitleparts) > 1) {
// The last string relates to the next searched field name/attribute
array_pop($searchtitleparts);
}
// Build new string for searched value
$searchtitleval = implode(" ", $searchtitleparts);
$searchtitlelength = strlen($searchtitleval);
if (substr($searchtitleval, -1, 1) == ",") {
$searchtitleval = substr($searchtitleval, 0, -1);
}
// Add quotes
$search = $searchstart . ' "' . "collectiontitle:" . $searchtitleval . '"';
// Append the other search strings
$search .= substr($origsearch, $titlepos + $searchtitlelength);
} else {
// nothing to remove
$search = $searchstart . ' "' . "collectiontitle:" . $searchend . '"';
}
debug("New search: " . $search);
}
$keywords = split_keywords($search, false, false, false, false, true);
if (strlen($search) == 1 && !is_numeric($search)) {
# A-Z search
$sql = "AND c.name LIKE ?";
$sql_params[] = "s";
$sql_params[] = $search . "%";
}
if (strlen($search) > 1 || is_numeric($search)) {
$keyrefs = array();
$keyunions = array();
$unionselect = "SELECT kunion.collection";
for ($n = 0; $n < count($keywords); $n++) {
if (substr($keywords[$n], 0, 1) == "\"" && substr($keywords[$n], -1, 1) == "\"") {
$keywords[$n] = substr($keywords[$n], 1, -1);
}
if (substr($keywords[$n], 0, 16) == "collectiontitle:") {
$newsearch = explode(":", $keywords[$n])[1];
$newsearch = strpos($newsearch, '*') === false ? '%' . trim($newsearch) . '%' : str_replace('*', '%', trim($newsearch));
$sql = "AND c.name LIKE ?";
$sql_params[] = "s";
$sql_params[] = $newsearch;
} elseif (substr($keywords[$n], 0, 16) == "collectionowner:") {
$keywords[$n] = substr($keywords[$n], 16);
$keyref = $keywords[$n];
$sql .= " AND (u.username RLIKE ? OR u.fullname RLIKE ?)";
$sql_params[] = "i";
$sql_params[] = $keyref;
$sql_params[] = "i";
$sql_params[] = $keyref;
} elseif (substr($keywords[$n], 0, 19) == "collectionownerref:") {
$keywords[$n] = substr($keywords[$n], 19);
$keyref = $keywords[$n];
$sql .= " AND (c.user=?)";
$sql_params[] = "i";
$sql_params[] = $keyref;
} elseif (substr($keywords[$n], 0, 10) == "basicyear:" || substr($keywords[$n], 0, 11) == "basicmonth:") {
$dateparts = explode(":", $keywords[$n]);
$yearpart = $dateparts[0] == "basicyear" ? $dateparts[1] : "____";
$monthpart = $dateparts[0] == "basicmonth" ? $dateparts[1] : "__";
$sql .= " AND c.created LIKE ?";
$sql_params[] = "s";
$sql_params[] = $yearpart . "-" . $monthpart . "%";
} else {
if (substr($keywords[$n], 0, 19) == "collectionkeywords:") {
$keywords[$n] = substr($keywords[$n], 19);
}
# Support field specific matching - discard the field identifier as not appropriate for collection searches.
if (strpos($keywords[$n], ":") !== false) {
$keywords[$n] = substr($keywords[$n], strpos($keywords[$n], ":") + 1);
}
$keyref = resolve_keyword($keywords[$n], false);
if ($keyref !== false) {
$keyrefs[] = $keyref;
}
}
}
if ($sql == "" && count($keyrefs) == 0) {
// Not a recognised collection search syntax and no matching keywords
return [];
}
for ($n = 0; $n < count($keyrefs); $n++) {
$select_extra .= ", k.key" . $n;
$unionselect .= ", BIT_OR(key" . $n . "_found) AS key" . $n;
$unionsql = "SELECT collection ";
for ($l = 0; $l < count($keyrefs); $l++) {
$unionsql .= $l == $n ? ",TRUE" : ",FALSE";
$unionsql .= " AS key" . $l . "_found";
}
$unionsql .= " FROM collection_keyword WHERE keyword=" . $keyrefs[$n];
$keyunions[] = $unionsql;
$sql .= " AND key" . $n;
}
if (count($keyunions) > 0) {
$keysql .= " LEFT OUTER JOIN (" . $unionselect . " FROM (" . implode(" UNION ", $keyunions) . ") kunion GROUP BY collection) AS k ON c.ref = k.collection";
}
}
# Restrict to parent, child and sibling groups?
if ($public_collections_confine_group && !$override_group_restrict) {
# Form a list of all applicable groups
$groups = array($usergroup); # Start with user's own group
$usergroupparams = ["i",$usergroup];
$groups = array_merge($groups, ps_array("SELECT ref value FROM usergroup WHERE parent=?", $usergroupparams, 'usergroup')); # Children
$groups = array_merge($groups, ps_array("SELECT parent value FROM usergroup WHERE ref=?", $usergroupparams, 'usergroup')); # Parent
$groups = array_merge($groups, ps_array("SELECT ref value FROM usergroup WHERE parent<>0 AND parent=(SELECT parent FROM usergroup WHERE ref=?)", $usergroupparams, 'usergroup')); # Siblings (same parent)
$sql .= " AND u.usergroup IN (" . ps_param_insert(count($groups)) . ")";
$sql_params = array_merge($sql_params, ps_param_fill($groups, "i"));
}
// Add extra elements to the SELECT statement if needed
if ($include_resources) {
$select_extra .= ", COUNT(DISTINCT cr.resource) AS count";
}
// Filter by type (public/featured collections)
$public_type_filter_sql = "c.`type` = ?";
$public_type_filter_sql_params = ["i",COLLECTION_TYPE_PUBLIC];
if ($exclude_themes) {
$featured_type_filter_sql = "";
$featured_type_filter_sql_params = [];
} else {
$featured_type_filter_sql = "(c.`type` = ?)";
$featured_type_filter_sql_params = ["i",COLLECTION_TYPE_FEATURED];
$fcf_sql = featured_collections_permissions_filter_sql("AND", "c.ref");
if (is_array($fcf_sql)) {
// Update with the extra condition
$featured_type_filter_sql = "(c.`type` = ? " . $fcf_sql[0] . ")";
$featured_type_filter_sql_params = array_merge(["i",COLLECTION_TYPE_FEATURED], $fcf_sql[1]);
}
}
if ($public_type_filter_sql != "" && $featured_type_filter_sql != "") {
$type_filter_sql = "(" . $public_type_filter_sql . " OR " . $featured_type_filter_sql . ")";
$type_filter_sql_params = array_merge($public_type_filter_sql_params, $featured_type_filter_sql_params);
} else {
$type_filter_sql = $public_type_filter_sql . $featured_type_filter_sql;
$type_filter_sql_params = array_merge($public_type_filter_sql_params, $featured_type_filter_sql_params);
}
$where_clause_osql = 'col.`type` = ' . COLLECTION_TYPE_PUBLIC;
if ($featured_type_filter_sql !== '') {
$where_clause_osql .= ' OR (col.`type` = ' . COLLECTION_TYPE_FEATURED . ' AND col.is_featured_collection_category = false)';
}
$main_sql = sprintf(
"SELECT *
FROM (
SELECT DISTINCT c.*,
u.username,
u.fullname,
IF(c.`type` = %s AND COUNT(DISTINCT cc.ref)>0, true, false) AS is_featured_collection_category
%s
FROM collection AS c
LEFT OUTER JOIN collection AS cc ON c.ref = cc.parent
LEFT OUTER JOIN collection_resource AS cr ON c.ref = cr.collection
LEFT OUTER JOIN user AS u ON c.user = u.ref
%s # keysql
WHERE %s # type_filter_sql
%s
GROUP BY c.ref
ORDER BY %s
) AS col
WHERE %s",
COLLECTION_TYPE_FEATURED,
$select_extra,
$keysql,
$type_filter_sql,
$sql, # extra filters
"{$order_by} {$sort}",
$where_clause_osql
);
return ps_query($main_sql, array_merge($type_filter_sql_params, $sql_params), '', $fetchrows);
}
/**
* Search within available collections
*
* @param string $search
* @param string $restypes
* @param integer $archive
* @param string $order_by
* @param string $sort
* @param integer $fetchrows
* @return array
*/
function do_collections_search($search, $restypes, $archive = 0, $order_by = '', $sort = "DESC", $fetchrows = -1)
{
global $search_includes_themes, $default_collection_sort;
if ($order_by == '') {
$order_by = $default_collection_sort;
}
$result = array();
# Recognise a quoted search, which is a search for an exact string
if (substr($search, 0, 1) == "\"" && substr($search, -1, 1) == "\"") {
$search = substr($search, 1, -1);
}
$search_includes_themes_now = $search_includes_themes;
if ($restypes != "") {
$restypes_x = explode(",", $restypes);
$search_includes_themes_now = in_array("FeaturedCollections", $restypes_x);
}
if ($search_includes_themes_now) {
# Same search as when searching within public collections.
$result = search_public_collections($search, "name", "ASC", !$search_includes_themes_now, true, false, $fetchrows);
}
return $result;
}
/**
* Add a collection to a user's 'My Collections'
*
* @param integer $user ID of user
* @param integer $collection ID of collection
*
* @return boolean
*/
function add_collection($user, $collection)
{
// Don't add if we are anonymous - we can only have one collection
global $anonymous_login,$username,$anonymous_user_session_collection;
if (isset($anonymous_login) && ($username == $anonymous_login) && $anonymous_user_session_collection) {
return false;
}
remove_collection($user, $collection);
ps_query("insert into user_collection(user,collection) values (?,?)", array("i",$user,"i",$collection));
clear_query_cache('col_total_ref_count_w_perm');
clear_query_cache('collection_access' . $user);
collection_log($collection, LOG_CODE_COLLECTION_SHARED_COLLECTION, 0, ps_value("select username as value from user where ref = ?", array("i",$user), ""));
return true;
}
/**
* Remove someone else's collection from a user's My Collections
*
* @param integer $user
* @param integer $collection
*/
function remove_collection($user, $collection)
{
ps_query("delete from user_collection where user=? and collection=?", array("i",$user,"i",$collection));
clear_query_cache('col_total_ref_count_w_perm');
collection_log($collection, LOG_CODE_COLLECTION_STOPPED_SHARING_COLLECTION, 0, ps_value("select username as value from user where ref = ?", array("i",$user), ""));
}
/**
* Update the keywords index for this collection
*
* @param integer $ref
* @param string $index_string
* @return integer How many keywords were indexed?
*/
function index_collection($ref, $index_string = '')
{
# Remove existing indexed keywords
ps_query("delete from collection_keyword where collection=?", array("i",$ref)); # Remove existing keywords
# Define an indexable string from the name, themes and keywords.
global $index_collection_titles;
if ($index_collection_titles) {
$indexfields = 'c.ref,c.name,c.keywords,c.description';
} else {
$indexfields = 'c.ref,c.keywords';
}
global $index_collection_creator;
if ($index_collection_creator) {
$indexfields .= ',u.fullname';
}
// if an index string wasn't supplied, generate one
if (!strlen($index_string) > 0) {
$indexarray = ps_query("select $indexfields from collection c left join user u on u.ref=c.user where c.ref = ?", array("i",$ref));
for ($i = 0; $i < count($indexarray); $i++) {
$index_string = "," . implode(',', $indexarray[$i]);
}
}
$keywords = split_keywords($index_string, true);
for ($n = 0; $n < count($keywords); $n++) {
if (trim($keywords[$n]) == "") {
continue;
}
$keyref = resolve_keyword($keywords[$n], true);
ps_query("insert into collection_keyword values (?,?)", array("i",$ref,"i",$keyref));
}
// return the number of keywords indexed
return $n;
}
/**
* Process the save action when saving a collection
*
* @param integer $ref
* @param array $coldata
*
* @return false|void
*/
function save_collection($ref, $coldata = array())
{
if (!is_numeric($ref) || !collection_writeable($ref)) {
return false;
}
if (count($coldata) == 0) {
// Old way
$coldata["name"] = getval("name", "");
$coldata["allow_changes"] = getval("allow_changes", "") != "" ? 1 : 0;
$coldata["public"] = getval('public', 0, true);
$coldata["keywords"] = getval("keywords", "");
$coldata["result_limit"] = getval("result_limit", 0, true);
$coldata["relateall"] = getval("relateall", "") != "";
$coldata["removeall"] = getval("removeall", "") != "";
$coldata["users"] = getval("users", "");
if (checkperm("h")) {
$coldata["home_page_publish"] = (getval("home_page_publish", "") != "") ? "1" : "0";
$coldata["home_page_text"] = getval("home_page_text", "");
$home_page_image = getval("home_page_image", 0, true);
if ($home_page_image > 0) {
$coldata["home_page_image"] = $home_page_image;
}
}
}
$oldcoldata = get_collection($ref);
$sqlset = array();
foreach ($coldata as $colopt => $colset) {
// skip data that is not a collection property (e.g result_limit) otherwise the $sqlset will have an
// incorrect SQL query for the update statement.
if (in_array($colopt, ['result_limit', 'relateall', 'removeall', 'users'])) {
continue;
}
// Set type to public unless explicitly passed
if ($colopt == "public" && $colset == 1 && !isset($coldata["type"])) {
$sqlset["type"] = COLLECTION_TYPE_PUBLIC;
}
// "featured_collections_changes" is determined by collection_edit.php page
// This is meant to override the type if collection has a parent. The order of $coldata elements matters!
if ($colopt == "featured_collections_changes" && !empty($colset)) {
$sqlset["type"] = COLLECTION_TYPE_FEATURED;
$sqlset["parent"] = null;
if (isset($colset["update_parent"])) {
$force_featured_collection_type = isset($colset["force_featured_collection_type"]);
// A FC root category is created directly from the collections_featured.php page so not having a parent, means it's just public
if ($colset["update_parent"] == 0 && !$force_featured_collection_type) {
$sqlset["type"] = COLLECTION_TYPE_PUBLIC;
} else {
$sqlset["parent"] = (int) $colset["update_parent"];
}
}
if (isset($colset["thumbnail_selection_method"])) {
$sqlset["thumbnail_selection_method"] = $colset["thumbnail_selection_method"];
}
if (isset($colset["thumbnail_selection_method"]) || isset($colset["name"])) {
// Prevent the parent from being changed if user only modified the thumbnail_selection_method or name
$sqlset["parent"] = (!isset($colset["update_parent"]) ? $oldcoldata["parent"] : $sqlset["parent"]);
}
// Prevent unnecessary changes
foreach (array("type", "parent", "thumbnail_selection_method") as $puc_to_prop) {
if (isset($sqlset[$puc_to_prop]) && $oldcoldata[$puc_to_prop] == $sqlset[$puc_to_prop]) {
unset($sqlset[$puc_to_prop]);
}
}
continue;
}
if (!isset($oldcoldata[$colopt]) || $colset != $oldcoldata[$colopt]) {
$sqlset[$colopt] = $colset;
}
}
// If collection is set as private by caller code, disable incompatible properties used for COLLECTION_TYPE_FEATURED (set by the user or exsting)
if (isset($sqlset["public"]) && $sqlset["public"] == 0) {
$sqlset["type"] = COLLECTION_TYPE_STANDARD;
$sqlset["parent"] = null;
$sqlset["thumbnail_selection_method"] = null;
$sqlset["bg_img_resource_ref"] = null;
}
/*
Order by is applicable only to featured collections.
Determine if we have to reset and, if required, re-order featured collections at the tree level
----------------------------------------------------------------------------------------------------------------
| Old | Set |
|---------------|-------------------|
Use cases | Type | Parent | Type | Parent | Reset order_by? | Re-order?
------------------------------------------------|------|--------|-----------------------------------------------
Move FC to private | 3 | null | 0 | null | yes | no
Move FC to public | 3 | any | 4 | null | yes | no
Move FC to new parent | 3 | null | not set | X | yes | yes
Save FC but dont change type or parent | 3 | null | not set | null | no | no
Save a child FC but dont change type or parent | 3 | X | not set | not set | no | no
Move public to private | 4 | null | 0 | null | no | no
Move public to FC (root) | 4 | null | 3 | not set | yes | yes
Move public to FC (others) | 4 | null | 3 | X | yes | yes
Save public but dont change type or parent | 4 | null | 4 | not set | no | no
Create FC at root | 0 | null | 3 | not set | yes | yes
Create FC at other level | 0 | null | 3 | X | yes | yes
----------------------------------------------------------------------------------------------------------------
*/
// Saving a featured collection without changing its type or parent
$rob_cond_fc_no_change = (
isset($oldcoldata['type']) && $oldcoldata['type'] === COLLECTION_TYPE_FEATURED
&& !isset($sqlset['type'])
&& (!isset($sqlset['parent']) || is_null($sqlset['parent']))
);
// Saving a public collection without changing it into a featured collection
$rob_cond_public_col_no_change = (
isset($oldcoldata['type'], $sqlset['type'])
&& $oldcoldata['type'] === COLLECTION_TYPE_PUBLIC
&& $sqlset["type"] !== COLLECTION_TYPE_FEATURED
);
if (!($rob_cond_fc_no_change || $rob_cond_public_col_no_change)) {
$sqlset['order_by'] = 0;
if (
// Type changed to featured collection
(isset($sqlset['type']) && $sqlset['type'] === COLLECTION_TYPE_FEATURED)
// Featured collection moved in the tree (ie parent changed)
|| ($oldcoldata['type'] === COLLECTION_TYPE_FEATURED && !isset($sqlset['type']) && isset($sqlset['parent']))
) {
$reorder_fcs = true;
}
}
// Update collection record
if (count($sqlset) > 0) {
$sqlupdate = "";
$clear_fc_query_cache = false;
$collection_columns = [
'name',
'user',
'created',
'public',
'allow_changes',
'cant_delete',
'keywords',
'savedsearch',
'home_page_publish',
'home_page_text',
'home_page_image',
'session_id',
'description',
'type',
'parent',
'thumbnail_selection_method',
'bg_img_resource_ref',
'order_by',
];
$params = [];
foreach ($sqlset as $colopt => $colset) {
// Only valid collection columns should be processed
if (!in_array($colopt, $collection_columns)) {
continue;
}
if ($sqlupdate != "") {
$sqlupdate .= ", ";
}
if (in_array($colopt, array("type", "parent", "thumbnail_selection_method", "bg_img_resource_ref"))) {
$clear_fc_query_cache = true;
}
if (in_array($colopt, array("parent", "thumbnail_selection_method", "bg_img_resource_ref"))) {
$sqlupdate .= $colopt . " = ";
if ($colset == 0) {
$sqlupdate .= 'NULL';
} else {
$sqlupdate .= '?';
$params = array_merge($params, ['i', $colset]);
}
continue;
}
if ($colopt == 'allow_changes') {
$colset = (int) $colset;
}
$sqlupdate .= $colopt . " = ? ";
$params = array_merge($params, ['s', $colset]);
}
if ($sqlupdate !== '') {
$sql = "UPDATE collection SET {$sqlupdate} WHERE ref = ?";
ps_query($sql, array_merge($params, ['i', $ref]));
if ($clear_fc_query_cache) {
clear_query_cache("featured_collections");
}
// Log the changes
foreach ($sqlset as $colopt => $colset) {
switch ($colopt) {
case "public";
collection_log($ref, LOG_CODE_COLLECTION_ACCESS_CHANGED, 0, $colset ? 'public' : 'private');
break;
case "allow_changes";
collection_log($ref, LOG_CODE_UNSPECIFIED, 0, $colset ? 'true' : 'false');
break;
default;
collection_log($ref, LOG_CODE_EDITED, 0, $colopt . " = " . $colset);
break;
}
}
}
}
index_collection($ref);
# If 'users' is specified (i.e. access is private) then rebuild users list
if (isset($coldata["users"])) {
$old_attached_users = ps_array("SELECT user value FROM user_collection WHERE collection=?", array("i",$ref));
$new_attached_users = array();
$removed_users = array();
$collection_owner_ref = ps_value(
"SELECT u.ref value FROM collection c LEFT JOIN user u ON c.user=u.ref WHERE c.ref=?",
array("i",$ref),
""
);
global $userref;
$collection_owner = get_user(($collection_owner_ref == '' ? $userref : $collection_owner_ref));
if ($collection_owner_ref != "") {
$old_attached_users[] = $collection_owner["ref"]; # Collection Owner is implied as attached already
}
ps_query("delete from user_collection where collection=?", array("i",$ref));
$old_attached_groups = ps_array("SELECT usergroup value FROM usergroup_collection WHERE collection=?", array("i",$ref));
ps_query("delete from usergroup_collection where collection=?", array("i",$ref));
# Build a new list and insert
$users = resolve_userlist_groups($coldata["users"]);
$ulist = array_unique(trim_array(explode(",", $users)));
$urefs = ps_array("select ref value from user where username in (" . ps_param_insert(count($ulist)) . ")", ps_param_fill($ulist, "s"));
if (count($urefs) > 0) {
$params = [];
foreach ($urefs as $uref) {
$params[] = $ref;
$params[] = $uref;
}
ps_query("insert into user_collection(collection,user) values " . trim(str_repeat('(?, ?),', count($urefs)), ','), ps_param_fill($params, 'i'));
$new_attached_users = array_diff($urefs, $old_attached_users);
$removed_users = array_diff($old_attached_users, $urefs, $collection_owner_ref != "" ? array($collection_owner["ref"]) : array());
}
# log this only if a user is being added
if ($coldata["users"] != "") {
collection_log($ref, LOG_CODE_COLLECTION_SHARED_COLLECTION, 0, join(", ", $ulist));
}
# log the removal of users / smart groups
$was_shared_with = array();
if (count($old_attached_users) > 0) {
$was_shared_with = ps_array("select username value from user where ref in (" . ps_param_insert(count($old_attached_users)) . ")", ps_param_fill($old_attached_users, "i"));
}
if (count($old_attached_groups) > 0) {
foreach ($old_attached_groups as $old_group) {
$was_shared_with[] = "Group (Smart): " . ps_value("SELECT name value FROM usergroup WHERE ref = ?", array("i", $old_group), "");
}
}
if (count($urefs) == 0 && count($was_shared_with) > 0) {
collection_log($ref, LOG_CODE_COLLECTION_STOPPED_SHARING_COLLECTION, 0, join(", ", $was_shared_with));
}
$groups = resolve_userlist_groups_smart($users);
$groupnames = '';
if ($groups != '') {
$groups = explode(",", $groups);
if (count($groups) > 0) {
foreach ($groups as $group) {
ps_query("insert into usergroup_collection(collection,usergroup) values (?,?)", array("i",$ref,"i",$group));
// get the group name
if ($groupnames != '') {
$groupnames .= ", ";
}
$groupnames .= ps_value("select name value from usergroup where ref=?", array("i",$group), "");
}
$new_attached_groups = array_diff($groups, $old_attached_groups);
if (!empty($new_attached_groups)) {
foreach ($new_attached_groups as $newg) {
$group_users = ps_array("SELECT ref value FROM user WHERE usergroup=?", array("i",$newg));
$new_attached_users = array_merge($new_attached_users, $group_users);
}
}
}
#log this
collection_log($ref, LOG_CODE_COLLECTION_SHARED_COLLECTION, 0, $groupnames);
}
# Clear user specific collection cache if user was added or removed.
if (count($new_attached_users) > 0 || count($removed_users) > 0) {
$user_caches = array_unique(array_merge($new_attached_users, $removed_users));
foreach ($user_caches as $user_cache) {
clear_query_cache('collection_access' . $user_cache);
}
}
}
# Send a message to any new attached user
if (!empty($new_attached_users)) {
global $baseurl, $lang;
$new_attached_users = array_unique($new_attached_users);
$message_text = str_replace(
array('%user%', '%colname%'),
array($collection_owner["fullname"] ?? $collection_owner["username"],getval("name", "")),
$lang['collectionprivate_attachedusermessage']
);
$message_url = $baseurl . "/?c=" . $ref;
message_add($new_attached_users, $message_text, $message_url);
}
# Relate all resources?
if (
isset($coldata["relateall"]) && $coldata["relateall"] != ""
&& allow_multi_edit($ref)
) {
relate_all_collection($ref);
}
# Remove all resources?
if (isset($coldata["removeall"]) && $coldata["removeall"] != "") {
remove_all_resources_from_collection($ref);
}
# Update limit count for saved search
if (isset($coldata["result_limit"]) && (int)$coldata["result_limit"] > 0) {
ps_query("update collection_savedsearch set result_limit=? where collection=?", array("i",$coldata["result_limit"],"i",$ref));
}
// Re-order featured collections tree at the level of this collection (if applicable - only for featured collections)
if (isset($reorder_fcs)) {
$new_fcs_order = reorder_all_featured_collections_with_parent($sqlset['parent'] ?? null);
log_activity("via save_collection({$ref})", LOG_CODE_REORDERED, implode(', ', $new_fcs_order), 'collection');
}
// When a collection is now saved as a Featured Collection (must have resources) under an existing branch, apply all
// the external access keys from the categories which make up that path to prevent breaking existing shares.
if (
isset($sqlset['parent']) && $sqlset['parent'] > 0
&& !empty($fc_resources = array_filter((array) get_collection_resources($ref)))
) {
// Delete old branch path external share associations as they are no longer relevant
$old_branch_category_ids = array_column(get_featured_collection_category_branch_by_leaf((int) $oldcoldata['parent'], []), 'ref');
foreach ($old_branch_category_ids as $fc_category_id) {
$old_keys = get_external_shares([
'share_collection' => $fc_category_id,
'share_type' => 0,
'ignore_permissions' => true
]);
foreach ($old_keys as $old_key_data) {
// IMPORTANT: we delete the keys associated with the collection we've just saved. The key may still be valid for the rest of the branch categories.
delete_collection_access_key($ref, $old_key_data['access_key']);
}
}
// Copy associations of all branch parents and apply to this collection and its resources
$all_branch_path_keys = [];
$branch_category_ids = array_column(get_featured_collection_category_branch_by_leaf($sqlset['parent'], []), 'ref');
foreach ($branch_category_ids as $fc_category_id) {
$all_branch_path_keys = array_merge(
$all_branch_path_keys,
get_external_shares([
'share_collection' => $fc_category_id,
'share_type' => 0,
'ignore_permissions' => true
])
);
}
foreach ($all_branch_path_keys as $external_key_data) {
foreach ($fc_resources as $fc_resource_id) {
if (!can_share_resource($fc_resource_id)) {
continue;
}
ps_query(
'INSERT INTO external_access_keys(resource, access_key, collection, `user`, usergroup, email, `date`, access, expires, password_hash) VALUES (?, ?, ?, ?, ?, ?, NOW(), ?, ?, ?)',
[
'i', $fc_resource_id,
's', $external_key_data['access_key'],
'i', $ref,
'i', $GLOBALS['userref'],
'i', $external_key_data['usergroup'],
's', $external_key_data['email'],
'i', $external_key_data['access'],
's', $external_key_data['expires'] ?: null,
's', $external_key_data['password_hash'] ?: null
]
);
collection_log($ref, LOG_CODE_COLLECTION_SHARED_RESOURCE_WITH, $fc_resource_id, $external_key_data['access_key']);
}
}
}
global $userref;
clear_query_cache('collection_access' . $userref);
refresh_collection_frame();
}
/**
* Case insensitive string comparisons using a "natural order" algorithm for collection names
*
* @param string $a
* @param string $b
*
* @return integer < 0 if $a is less than $b > 0 if $a is greater than $b, and 0 if they are equal.
*/
function collections_comparator($a, $b)
{
return strnatcasecmp(i18n_get_collection_name($a), i18n_get_collection_name($b));
}
/**
* Case insensitive string comparisons using a "natural order" algorithm for collection names
*
* @param string $b
* @param string $a
*
* @return integer < 0 if $a is less than $b > 0 if $a is greater than $b, and 0 if they are equal.
*/
function collections_comparator_desc($a, $b)
{
return strnatcasecmp(i18n_get_collection_name($b), i18n_get_collection_name($a));
}
/**
* Returns a list of smart theme headers, which are basically fields with a 'smart theme name' set.
*
* @return array
*/
function get_smart_theme_headers()
{
return ps_query("SELECT ref, name, smart_theme_name, type FROM resource_type_field WHERE length(smart_theme_name) > 0 ORDER BY smart_theme_name", array(), "featured_collections");
}
/**
* get_smart_themes_nodes
*
* @param integer $field
* @param boolean $is_category_tree
* @param integer $parent
* @param array $field_meta - resource type field metadata
* @return array
*/
function get_smart_themes_nodes($field, $is_category_tree, $parent = null, array $field_meta = array())
{
$return = array();
// Determine if this should cascade onto children for category tree type
$recursive = false;
if ($is_category_tree) {
$recursive = true;
}
$nodes = get_nodes($field, ((0 == $parent) ? null : $parent), $recursive);
if (isset($field_meta['automatic_nodes_ordering']) && (bool) $field_meta['automatic_nodes_ordering']) {
$nodes = reorder_nodes($nodes);
$nodes = array_values($nodes); // reindex nodes array
}
if (0 === count($nodes)) {
return $return;
}
/*
Tidy list so it matches the storage format used for keywords
The translated version is fetched as each option will be indexed in the local language version of each option
*/
$options_base = array();
for ($n = 0; $n < count($nodes); $n++) {
$options_base[$n] = trim(mb_convert_case(i18n_get_translated($nodes[$n]['name']), MB_CASE_LOWER, 'UTF-8'));
}
// For each option, if it is in use, add it to the return list
for ($n = 0; $n < count($nodes); $n++) {
$cleaned_option_base = preg_replace('/\W/', ' ', $options_base[$n]); // replace any non-word characters with a space
$cleaned_option_base = trim($cleaned_option_base); // trim (just in case prepended / appended space characters)
$tree_node_depth = 0;
$parent_node_to_use = 0;
$is_parent = false;
if (is_parent_node($nodes[$n]['ref'])) {
$parent_node_to_use = $nodes[$n]['ref'];
$is_parent = true;
$tree_node_depth = get_tree_node_level($nodes[$n]['ref']);
if (!is_null($parent) && is_parent_node($parent)) {
$tree_node_depth--;
}
}
$c = count($return);
$return[$c]['name'] = trim(i18n_get_translated($nodes[$n]['name']));
$return[$c]['indent'] = $tree_node_depth;
$return[$c]['node'] = $parent_node_to_use;
$return[$c]['is_parent'] = $is_parent;
$return[$c]['ref'] = $nodes[$n]['ref'];
}
return $return;
}
/**
* E-mail a collection to users
*
* - Attempt to resolve all users in the string $userlist to user references.
* - Add $collection to these user's 'My Collections' page
* - Send them an e-mail linking to this collection
* - Handle multiple collections (comma separated list)
*
* @param mixed $colrefs
* @param string $collectionname
* @param string $fromusername
* @param string $userlist
* @param string $message
* @param string $feedback
* @param integer $access
* @param string $expires
* @param string $useremail
* @param string $from_name
* @param string $cc
* @param boolean $themeshare
* @param string $themename
* @param string $themeurlsuffix
* @param boolean $list_recipients
* @param boolean $add_internal_access
* @param string $group
* @param string $sharepwd
*/
function email_collection($colrefs, $collectionname, $fromusername, $userlist, $message, $feedback, $access = -1, $expires = "", $useremail = "", $from_name = "", $cc = "", $themeshare = false, $themename = "", $themeurlsuffix = "", $list_recipients = false, $add_internal_access = false, $group = "", $sharepwd = ""): string
{
global $baseurl,$email_from,$applicationname,$lang,$userref,$usergroup;
if ($useremail == "") {
$useremail = $email_from;
}
if ($group == "") {
$group = $usergroup;
}
if (trim($userlist) == "") {
return $lang["mustspecifyoneusername"];
}
$userlist = resolve_userlist_groups($userlist);
if (strpos($userlist, $lang["groupsmart"] . ": ") !== false) {
$groups_users = resolve_userlist_groups_smart($userlist, true);
if ($groups_users != '') {
if ($userlist != "") {
$userlist = remove_groups_smart_from_userlist($userlist);
if ($userlist != "") {
$userlist .= ",";
}
}
$userlist .= $groups_users;
}
}
$ulist = trim_array(explode(",", $userlist));
$emails = array();
$key_required = array();
if ($feedback) {
$feedback = 1;
} else {
$feedback = 0;
}
$reflist = trim_array(explode(",", $colrefs));
// Take out the FC category from the list as this is more of a dummy record rather than a collection we'll be giving
// access to users. See generate_collection_access_key() when collection is a featured collection category.
$fc_category_ref = ($themeshare ? array_shift($reflist) : null);
$emails_keys = resolve_user_emails($ulist);
if (0 === count($emails_keys)) {
return $lang['email_error_user_list_not_valid'];
}
# Make an array of all emails, whether internal or external
$emails = $emails_keys['emails'];
# Make a corresponding array stating whether keys are necessary for the links
$key_required = $emails_keys['key_required'];
# Make an array of internal userids which are unexpired approved with valid emails
$internal_user_ids = $emails_keys['refs'] ?? array();
if (count($internal_user_ids) > 0) {
# Delete any existing collection entries
ps_query("DELETE FROM user_collection WHERE collection IN (" . ps_param_insert(count($reflist)) . ")
AND user IN (" . ps_param_insert(count($internal_user_ids)) . ")", array_merge(ps_param_fill($reflist, "i"), ps_param_fill($internal_user_ids, "i")));
# Insert new user_collection row(s)
#loop through the collections
for ($nx1 = 0; $nx1 < count($reflist); $nx1++) {
#loop through the users
for ($nx2 = 0; $nx2 < count($internal_user_ids); $nx2++) {
ps_query("INSERT INTO user_collection(collection,user,request_feedback) VALUES (?,?,?)", ["i",$reflist[$nx1],"i",$internal_user_ids[$nx2],"i",$feedback ]);
if ($add_internal_access) {
foreach (get_collection_resources($reflist[$nx1]) as $resource) {
if (get_edit_access($resource)) {
open_access_to_user($internal_user_ids[$nx2], $resource, $expires);
}
}
}
#log this
clear_query_cache('collection_access' . $internal_user_ids[$nx2]);
collection_log($reflist[$nx1], LOG_CODE_COLLECTION_SHARED_COLLECTION, 0, ps_value("select username as value from user where ref = ?", array("i", $internal_user_ids[$nx2]), ""));
}
}
}
# Send an e-mail to each resolved email address
# htmlbreak is for composing list
$htmlbreak = "\r\n";
global $use_phpmailer;
if ($use_phpmailer) {
$htmlbreak = "<br/><br/>";
$htmlbreaksingle = "<br/>";
}
if ($fromusername == "") {
$fromusername = $applicationname;
} // fromusername is used for describing the sender's name inside the email
if ($from_name == "") {
$from_name = $applicationname;
} // from_name is for the email headers, and needs to match the email address (app name or user name)
$templatevars['message'] = str_replace(array("\\n","\\r","\\"), array("\n","\r",""), $message);
if (trim($templatevars['message']) == "") {
$templatevars['message'] = $lang['nomessage'];
$message = "lang_nomessage";
}
$templatevars['fromusername'] = $fromusername;
$templatevars['from_name'] = $from_name;
// Create notification message
$notifymessage = new ResourceSpaceUserNotification();
if (count($reflist) > 1) {
$notifymessage->set_subject($applicationname . ": ");
$notifymessage->append_subject("lang_mycollections");
} else {
$notifymessage->set_subject($applicationname . ": " . $collectionname);
}
if ($fromusername == "") {
$fromusername = $applicationname;
}
$externalmessage = str_replace('[applicationname]', $applicationname, $lang["emailcollectionmessageexternal"]);
$internalmessage = "lang_emailcollectionmessage";
$viewlinktext = "lang_clicklinkviewcollection";
if ($themeshare) { // Change the text if sharing a theme category
$externalmessage = str_replace('[applicationname]', $applicationname, $lang["emailthemecollectionmessageexternal"]);
$internalmessage = "lang_emailthememessage";
$viewlinktext = "lang_clicklinkviewcollections";
}
## loop through recipients
for ($nx1 = 0; $nx1 < count($emails); $nx1++) {
## loop through collections
$list = "";
$list2 = "";
$origviewlinktext = $viewlinktext; // Save this text as we may change it for internal theme shares for this user
if ($themeshare && !$key_required[$nx1]) { # don't send a whole list of collections if internal, just send the theme category URL
$notifymessage->set_subject($applicationname . ": " . $themename);
$url = $baseurl . "/pages/collections_featured.php" . $themeurlsuffix;
$viewlinktext = "lang_clicklinkviewthemes";
$notifymessage->url = $url;
$emailcollectionmessageexternal = false;
if ($use_phpmailer) {
$link = '<a href="' . $url . '">' . $themename . '</a>';
$list .= $htmlbreak . $link;
// alternate list style
$list2 .= $htmlbreak . $themename . ' -' . $htmlbreaksingle . $url;
$templatevars['list2'] = $list2;
} else {
$list .= $htmlbreak . $url;
}
for ($nx2 = 0; $nx2 < count($reflist); $nx2++) {
#log this
collection_log($reflist[$nx2], LOG_CODE_COLLECTION_EMAILED_COLLECTION, 0, $emails[$nx1]);
}
} else {
// E-mail external share, generate the access key based on the FC category. Each sub-collection will have the same key.
if ($key_required[$nx1] && $themeshare && !is_null($fc_category_ref)) {
$k = generate_collection_access_key($fc_category_ref, $feedback, $emails[$nx1], $access, $expires, $group, $sharepwd, $reflist);
$fc_key = "&k={$k}";
}
for ($nx2 = 0; $nx2 < count($reflist); $nx2++) {
$key = "";
$emailcollectionmessageexternal = false;
# Do we need to add an external access key for this user (e-mail specified rather than username)?
if ($key_required[$nx1] && !$themeshare) {
$k = generate_collection_access_key($reflist[$nx2], $feedback, $emails[$nx1], $access, $expires, $group, $sharepwd);
$key = "&k=" . $k;
$emailcollectionmessageexternal = true;
}
// If FC category, the key is valid across all sub-featured collections. See generate_collection_access_key()
elseif ($key_required[$nx1] && $themeshare && !is_null($fc_category_ref)) {
$key = $fc_key;
$emailcollectionmessageexternal = true;
}
$url = $baseurl . "/?c=" . $reflist[$nx2] . $key;
$collection = array();
$collection = ps_query("SELECT name,savedsearch FROM collection WHERE ref = ?", ["i",$reflist[$nx2]]);
if ($collection[0]["name"] != "") {
$collection_name = i18n_get_collection_name($collection[0]);
} else {
$collection_name = $reflist[$nx2];
}
if ($use_phpmailer) {
$link = '<a href="' . $url . '">' . escape($collection_name) . '</a>';
$list .= $htmlbreak . $link;
// alternate list style
$list2 .= $htmlbreak . $collection_name . ' -' . $htmlbreaksingle . $url;
$templatevars['list2'] = $list2;
} else {
$list .= $htmlbreak . $collection_name . $htmlbreak . $url . $htmlbreak;
}
#log this
collection_log($reflist[$nx2], LOG_CODE_COLLECTION_EMAILED_COLLECTION, 0, $emails[$nx1]);
}
}
$templatevars['list'] = $list;
$templatevars['from_name'] = $from_name;
if (isset($k)) {
if ($expires == "") {
$templatevars['expires_date'] = $lang["email_link_expires_never"];
$templatevars['expires_days'] = $lang["email_link_expires_never"];
} else {
$day_count = round((strtotime($expires) - strtotime('now')) / (60 * 60 * 24));
$templatevars['expires_date'] = $lang['email_link_expires_date'] . nicedate($expires);
$templatevars['expires_days'] = $lang['email_link_expires_days'] . $day_count;
if ($day_count > 1) {
$templatevars['expires_days'] .= " " . $lang['expire_days'] . ".";
} else {
$templatevars['expires_days'] .= " " . $lang['expire_day'] . ".";
}
}
} else {
# Set empty expiration templatevars
$templatevars['expires_date'] = '';
$templatevars['expires_days'] = '';
}
$body = "";
if ($emailcollectionmessageexternal) {
$template = ($themeshare) ? "emailthemeexternal" : "emailcollectionexternal";
// External - send email
if (is_array($emails) && (count($emails) > 1) && $list_recipients === true) {
$body = $lang["list-recipients"] . "\n" . implode("\n", $emails) . "\n\n";
$templatevars['list-recipients'] = $lang["list-recipients"] . "\n" . implode("\n", $emails) . "\n\n";
}
if (substr($viewlinktext, 0, 5) == "lang_") {
$langkey = substr($viewlinktext, 5);
if (isset($lang[$langkey])) {
$viewlinktext = $lang[$langkey];
}
}
$body .= $templatevars['fromusername'] . " " . $externalmessage . "\n\n" . $templatevars['message'] . "\n\n" . $viewlinktext . "\n\n" . $templatevars['list'];
$emailsubject = $notifymessage->get_subject();
$send_result = send_mail($emails[$nx1], $emailsubject, $body, $fromusername, $useremail, $template, $templatevars, $from_name, $cc);
if ($send_result !== true) {
return $send_result;
}
} else {
$template = ($themeshare) ? "emailtheme" : "emailcollection";
}
$viewlinktext = $origviewlinktext;
}
if (count($internal_user_ids) > 0) {
// Internal share, send notifications
$notifymessage->append_text($templatevars['fromusername'] . "&nbsp;");
$notifymessage->append_text($internalmessage);
$notifymessage->append_text("<br/><br/>" . $templatevars['message'] . "<br/><br/>");
$notifymessage->append_text($viewlinktext);
$notifymessage->url = $url;
send_user_notification($internal_user_ids, $notifymessage);
}
hook("additional_email_collection", "", array($colrefs,$collectionname,$fromusername,$userlist,$message,$feedback,$access,$expires,$useremail,$from_name,$cc,$themeshare,$themename,$themeurlsuffix,$template,$templatevars));
# Identify user accounts which have been skipped
$candidate_users = ps_query("SELECT ref, username FROM user
WHERE username IN (" . ps_param_insert(count($ulist)) . ")", ps_param_fill($ulist, "s"));
$skipped_usernames = array();
if (count($candidate_users) != count($internal_user_ids)) {
foreach ($candidate_users as $candidate_user) {
if (!in_array($candidate_user['ref'], $internal_user_ids)) {
$skipped_usernames[] = $candidate_user['username'];
}
}
}
# Report skipped accounts
if (count($skipped_usernames) > 0) {
return $lang['email_error_user_list_some_skipped'] . ' ' . implode(', ', $skipped_usernames);
}
# Return an empty string (all OK).
return "";
}
/**
* Generate an external access key to allow external people to view the resources in this collection.
*
* @param integer $collection Collection ref -or- collection data structure
* @param integer $feedback
* @param string $email
* @param integer $access
* @param string $expires
* @param string $group
* @param string $sharepwd
* @param array $sub_fcs List of sub-featured collections IDs (collection_email.php page has logic to determine
* this which is carried forward to email_collection())
*
* @return string The generated key used for external sharing
*/
function generate_collection_access_key($collection, $feedback = 0, $email = "", $access = -1, $expires = "", $group = "", $sharepwd = "", array $sub_fcs = array())
{
global $userref, $usergroup, $scramble_key;
// Default to sharing with the permission of the current usergroup if not specified OR no access to alternative group selection.
if ($group == "" || !checkperm("x")) {
$group = $usergroup;
}
if (!is_array($collection)) {
$collection = get_collection($collection);
}
if (!empty($collection) && $collection["type"] == COLLECTION_TYPE_FEATURED && !isset($collection["has_resources"])) {
$collection_resources = get_collection_resources($collection["ref"]);
$collection["has_resources"] = (is_array($collection_resources) && !empty($collection_resources) ? 1 : 0);
}
$is_featured_collection_category = is_featured_collection_category($collection);
// We build a collection list to allow featured collections children that are externally shared as part of a parent,
// to all be shared with the same parameters (e.g key, access, group). When the collection is not COLLECTION_TYPE_FEATURED
// this will hold just that collection
$collections = array($collection["ref"]);
if ($is_featured_collection_category) {
$collections = (!empty($sub_fcs) ? $sub_fcs : get_featured_collection_categ_sub_fcs($collection));
}
// Generate the key based on the original collection. For featured collection category, all sub featured collections
// will share the same key
$k = generate_share_key($collection["ref"]);
if ($expires != '') {
$expires = date_format(date_create($expires), 'Y-m-d') . ' 23:59:59';
}
$main_collection = $collection; // keep record of this info as we need it at the end to record the successful generation of a key for a featured collection category
$created_sub_fc_access_key = false;
foreach ($collections as $collection) {
$r = get_collection_resources($collection);
$shareable_resources = array_filter($r, function ($resource_ref) {
return can_share_resource($resource_ref);
});
foreach ($shareable_resources as $resource_ref) {
$sql = '';
$params = [];
if ($expires == '') {
$sql = 'NULL, ';
} else {
$sql = '?, ';
$params[] = 's';
$params[] = $expires;
}
if (!($sharepwd != "" && $sharepwd != "(unchanged)")) {
$sql .= 'NULL';
} else {
$sql .= '?';
$params[] = 's';
$params[] = hash("sha256", $k . $sharepwd . $scramble_key);
}
ps_query(
"INSERT INTO external_access_keys(resource, access_key, collection, `user`, usergroup, request_feedback, email, `date`, access, expires, password_hash) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?, {$sql})",
array_merge(
[
'i', $resource_ref,
's', $k,
'i', $collection,
'i', $userref,
'i', $group,
's', $feedback,
's', $email,
'i', $access
],
$params
)
);
$created_sub_fc_access_key = true;
}
}
if ($is_featured_collection_category && $created_sub_fc_access_key) {
$sql = '';
$params = [];
if ($expires == '') {
$sql = 'NULL, ';
} else {
$sql = '?, ';
$params[] = 's';
$params[] = $expires;
}
if (!($sharepwd != "" && $sharepwd != "(unchanged)")) {
$sql .= 'NULL';
} else {
$sql .= '?';
$params[] = 's';
$params[] = hash("sha256", $k . $sharepwd . $scramble_key);
}
// add for FC category. No resource. This is a dummy record so we can have a way to edit the external share done
// at the featured collection category level
ps_query(
"INSERT INTO external_access_keys(resource, access_key, collection, `user`, usergroup, request_feedback, email, `date`, access, expires, password_hash) VALUES (NULL, ?, ?, ?, ?, ?, ?, NOW(), ?, {$sql})",
array_merge(
[
's', $k,
'i', $main_collection["ref"],
'i', $userref,
'i', $group,
's', $feedback,
's', $email,
'i', $access
],
$params
)
);
}
return $k;
}
/**
* Returns all saved searches in a collection
*
* @param integer $collection
*/
function get_saved_searches($collection): array
{
return ps_query("select " . columns_in("collection_savedsearch") . " from collection_savedsearch where collection= ? order by created", ['i', $collection]);
}
/**
* Add a saved search to a collection
*
* @param integer $collection
* @return void
*/
function add_saved_search($collection)
{
ps_query("insert into collection_savedsearch(collection,search,restypes,archive) values (?,?,?,?)", array("i",$collection,"s",getval("addsearch", ""),"s",getval("restypes", ""),"s",getval("archive", "")));
}
/**
* Remove a saved search from a collection
*
* @param integer $collection
* @param integer $search
* @return void
*/
function remove_saved_search($collection, $search)
{
ps_query("delete from collection_savedsearch where collection=? and ref=?", array("i",$collection,"i",$search));
}
/**
* Greate a new smart collection using submitted values
*
* @return void
*/
function add_smart_collection()
{
global $userref, $search_all_workflow_states, $lang;
$search = getval("addsmartcollection", "");
$restypes = getval("restypes", "");
if ($restypes == "Global") {
$restypes = "";
}
# archive can be a string of values
$archive = getval('archive', 0, false);
if ($search_all_workflow_states && $archive == "") {
$archive = 'all';
}
if ($archive == "") {
$archive = 0;
}
// more compact search strings should work with get_search_title
$searchstring = array();
if ($search != "") {
$searchstring[] = "search=$search";
}
if ($restypes != "") {
$searchstring[] = "restypes=$restypes";
}
if ($archive !== 0) {
if ($archive === 'all') {
$archive_label = $lang['all_workflow_states'];
} else {
$archive_label = $archive;
}
$searchstring[] = "archive=$archive_label";
}
$searchstring = implode("&", $searchstring);
$newcollection = create_collection($userref, get_search_title($searchstring), 1);
ps_query("insert into collection_savedsearch(collection,search,restypes,archive,starsearch)
values (?,?,?,?,?)", array("i",$newcollection,"s",$search,"s",$restypes,"s",$archive,"i",DEPRECATED_STARSEARCH));
$savedsearch = sql_insert_id();
ps_query("update collection set savedsearch=? where ref=?", array("i",$savedsearch,"i",$newcollection));
set_user_collection($userref, $newcollection);
refresh_collection_frame($newcollection);
}
/**
* Get a display friendly name for the given search string
* Takes a full searchstring of the form 'search=restypes=archive=' and
* uses search_title_processing to autocreate a more informative title
*
* @param string $searchstring Search string
*
* @return string Friendly name for search
*/
function get_search_title($searchstring)
{
$order_by = "";
$sort = "";
$offset = "";
$k = getval("k", "");
$search_titles = true;
$search_titles_searchcrumbs = true;
$use_refine_searchstring = true;
global $lang,$userref,$baseurl,$collectiondata,$result,$display,$pagename,$collection,$userrequestmode;
parse_str($searchstring, $searchvars);
if (isset($searchvars["archive"])) {
$archive = $searchvars["archive"];
} else {
$archive = 0;
}
if (isset($searchvars["search"])) {
$search = $searchvars["search"];
} else {
$search = "";
}
if (isset($searchvars["restypes"])) {
$restypes = $searchvars["restypes"];
} else {
$restypes = "";
}
include __DIR__ . "/search_title_processing.php";
if ($restypes != "") {
$resource_types = get_resource_types($restypes, true, false, true);
foreach ($resource_types as $type) {
$typenames[] = $type['name'];
}
$search_title .= " [" . implode(', ', $typenames) . "]";
}
return str_replace(">", "", strip_tags(htmlspecialchars_decode($search_title)));
}
/**
* Adds all the resources in the provided search to $collection
*
* @param integer $collection
* @param string $search
* @param string $restypes
* @param string $archivesearch
* @param string $order_by
* @param string $sort
* @param string $daylimit
* @param int $res_access The ID of the resource access level
* @param boolean $editable_only If true then only editable resources will be added
* @return boolean
*/
function add_saved_search_items(
$collection,
$search = "",
$restypes = "",
$archivesearch = "",
$order_by = "relevance",
$sort = "desc",
$daylimit = "",
$res_access = "",
$editable_only = false
) {
if ((string)(int)$collection != $collection) {
// Not an integer
return false;
}
global $collection_share_warning, $collection_allow_not_approved_share, $userref, $collection_block_restypes, $search_all_workflow_states;
# Adds resources from a search to the collection.
if ($search_all_workflow_states && trim($archivesearch) !== "" && $archivesearch != 0) {
$search_all_workflow_states = false;
}
$results = do_search($search, $restypes, $order_by, $archivesearch, [0,-1], $sort, false, DEPRECATED_STARSEARCH, false, false, $daylimit, false, true, false, $editable_only, false, $res_access);
if (!is_array($results) || (isset($results["total"]) && $results["total"] == 0)) {
return false;
}
// To maintain current collection order but add the search items in the correct order we must first move the existing collection resources out the way
$searchcount = $results["total"];
if ($searchcount > 0) {
ps_query(
"UPDATE collection_resource SET sortorder = if(isnull(sortorder), ?,sortorder + ?) WHERE collection= ?",
[
'i', $searchcount,
'i', $searchcount,
'i', $collection
]
);
}
// If this is a featured collection apply all the external access keys from the categories which make up its
// branch path to prevent breaking existing shares for any of those featured collection categories.
$fc_branch_path_keys = [];
$collection_data = get_collection($collection, true);
if ($collection_data !== false && $collection_data['type'] === COLLECTION_TYPE_FEATURED) {
$branch_category_ids = array_column(
// determine the branch from the parent because the keys for the collection in question will be done below
get_featured_collection_category_branch_by_leaf((int)$collection_data['parent'], []),
'ref'
);
foreach ($branch_category_ids as $fc_category_id) {
$fc_branch_path_keys = array_merge(
$fc_branch_path_keys,
get_external_shares([
'share_collection' => $fc_category_id,
'share_type' => 0,
'ignore_permissions' => true,
])
);
}
}
# Check if this collection has already been shared externally. If it has, we must add a further entry
# for this specific resource, and warn the user that this has happened.
$keys = array_merge(
get_external_shares([
'share_collection' => $collection,
'share_type' => 0,
'ignore_permissions' => true,
]),
$fc_branch_path_keys
);
$resourcesnotadded = array(); # record the resources that are not added so we can display to the user
$blockedtypes = array();# Record the resource types that are not added
foreach ($results["data"] as $result) {
$resource = $result["ref"];
$archivestatus = $result["archive"];
if (in_array($result["resource_type"], $collection_block_restypes)) {
$blockedtypes[] = $result["resource_type"];
continue;
}
if (count($keys) > 0) {
if (($archivestatus < 0 && !$collection_allow_not_approved_share) || !can_share_resource($resource)) {
$resourcesnotadded[$resource] = $result;
continue;
}
for ($n = 0; $n < count($keys); $n++) {
$sql = '';
$params = [];
if ($keys[$n]["expires"] == '') {
$sql .= 'NULL, ';
} else {
$sql .= '?, ';
$params[] = 's';
$params[] = $keys[$n]["expires"];
}
if ($keys[$n]["usergroup"] == '') {
$sql .= 'NULL';
} else {
$sql .= '?';
$params[] = 'i';
$params[] = $keys[$n]["usergroup"];
}
# Insert a new access key entry for this resource/collection.
ps_query(
"INSERT INTO external_access_keys(resource,access_key,user,collection,date,access,password_hash,expires,usergroup) VALUES (?, ?, ?, ?,NOW(), ?, ?, {$sql})",
array_merge([
'i', $resource,
's', $keys[$n]["access_key"],
'i', $userref,
'i', $collection,
's', $keys[$n]["access"],
's', $keys[$n]["password_hash"]
], $params)
);
#log this
collection_log($collection, LOG_CODE_COLLECTION_SHARED_RESOURCE_WITH, $resource, $keys[$n]["access_key"]);
# Set the flag so a warning appears.
$collection_share_warning = true;
}
}
}
if (is_array($results["data"])) {
$n = 0;
foreach ($results["data"] as $result) {
$resource = $result["ref"];
if (!isset($resourcesnotadded[$resource]) && !in_array($result["resource_type"], $collection_block_restypes)) {
ps_query("DELETE FROM collection_resource WHERE resource=? AND collection=?", array("i",$resource,"i",$collection));
ps_query("INSERT INTO collection_resource(resource,collection,sortorder) VALUES (?,?,?)", array("i",$resource,"i",$collection,"s",$n));
#log this
collection_log($collection, LOG_CODE_COLLECTION_ADDED_RESOURCE, $resource);
$n++;
}
}
}
// Clear theme image cache
clear_query_cache('themeimage');
clear_query_cache('col_total_ref_count_w_perm');
if (!empty($resourcesnotadded) || count($blockedtypes) > 0) {
# Translate to titles only for displaying them to the user
global $view_title_field;
$titles = array();
foreach ($resourcesnotadded as $resource) {
$titles[] = i18n_get_translated($resource['field' . $view_title_field]);
}
if (count($blockedtypes) > 0) {
$blocked_restypes = array_unique($blockedtypes);
// Return a list of blocked resouce types
$titles["blockedtypes"] = $blocked_restypes;
}
return $titles;
}
return array();
}
/**
* Returns true or false, can all resources in this collection be edited by the user?
*
* @param array|int $collection Collection IDs
* @param array $collectionid
*
* @return boolean
*/
function allow_multi_edit($collection, $collectionid = 0)
{
global $resource;
if (is_array($collection) && $collectionid == 0) {
// Do this the hard way by checking every resource for edit access
for ($n = 0; $n < count($collection); $n++) {
$resource = $collection[$n];
if (!get_edit_access($collection[$n]["ref"], $collection[$n]["archive"], $collection[$n])) {
return false;
}
}
# All have edit access
return true;
} else {
// Instead of checking each resource we can do a comparison between a search for all resources in collection and a search for editable resources
$resultcount = 0;
$all_resource_refs = array();
if (!is_array($collection)) {
// Need the collection resources so need to run the search
$collectionid = $collection;
# Editable_only=false (so returns resources whether editable or not)
$collection = do_search("!collection{$collectionid}", '', '', 0, -1, '', false, 0, false, false, '', false, false, true, false);
}
if (is_array($collection)) {
$resultcount = count($collection);
}
$editcount = 0;
# Editable_only=true (so returns editable resources only)
$editresults = do_search("!collection{$collectionid}", '', '', 0, -1, '', false, 0, false, false, '', false, false, true, true);
if (is_array($editresults)) {
$editcount = count($editresults);
}
if ($resultcount == $editcount) {
return true;
}
# Counts differ meaning there are non-editable resources
$all_resource_refs = array_column($collection, "ref");
$editable_resource_refs = array_column($editresults, "ref");
$non_editable_resource_refs = array_diff($all_resource_refs, $editable_resource_refs);
# Is grant edit present for all non-editables?
foreach ($non_editable_resource_refs as $non_editable_ref) {
if (!hook('customediteaccess', '', array($non_editable_ref))) {
return false;
}
}
# All non_editables have grant edit
return true;
}
}
/**
* Get featured collection resources (including from child nodes). For normal FCs this is using the collection_resource table.
* For FC categories, this will check within normal FCs contained by that category. Normally used in combination with
* generate_featured_collection_image_urls() but useful to determine if a FC category is full of empty FCs.
*
* @param array $c Collection data structure similar to the one returned by {@see get_featured_collections()}
* @param array $ctx Extra context used to get FC resources (e.g smart FC?, limit on number of resources returned). Context
* information should take precedence over internal logic (e.g determining the result limit)
*
* @return array
*/
function get_featured_collection_resources(array $c, array $ctx)
{
global $usergroup, $userref, $CACHE_FC_RESOURCES, $themes_simple_images,$collection_allow_not_approved_share;
global $FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS, $theme_images_number;
if (!isset($c["ref"]) || !is_int((int) $c["ref"])) {
return array();
}
$CACHE_FC_RESOURCES = (!is_null($CACHE_FC_RESOURCES) && is_array($CACHE_FC_RESOURCES) ? $CACHE_FC_RESOURCES : array());
// create a unique ID for this result set as the context for the same FC may differ
$cache_id = $c["ref"] . md5(json_encode($ctx));
if (isset($CACHE_FC_RESOURCES[$cache_id])) {
return $CACHE_FC_RESOURCES[$cache_id];
}
$limit = (isset($ctx["limit"]) && (int) $ctx["limit"] > 0 ? (int) $ctx["limit"] : null);
$use_thumbnail_selection_method = (isset($ctx["use_thumbnail_selection_method"]) ? (bool) $ctx["use_thumbnail_selection_method"] : false);
// Smart FCs
if (isset($ctx["smart"]) && $ctx["smart"] === true) {
// Root smart FCs don't have an image (legacy reasons)
if (is_null($c["parent"])) {
return array();
}
$node_search = NODE_TOKEN_PREFIX . $c['ref'];
$limit = (!is_null($limit) ? $limit : 1);
// Access control is still in place (i.e. permissions are honoured)
$smart_fc_resources = do_search($node_search, '', 'hit_count', 0, $limit, 'desc', false, 0, false, false, '', true, false, true);
$smart_fc_resources = (is_array($smart_fc_resources) ? array_column($smart_fc_resources, "ref") : array());
$CACHE_FC_RESOURCES[$cache_id] = $smart_fc_resources;
return $smart_fc_resources;
}
// Access control
$rca_where = '';
$rca_where_params = array();
$rca_joins = array();
$rca_join_params = array();
$fc_permissions_where = '';
$fc_permissions_where_params = [];
$union = "";
$unionparams = [];
if (!checkperm("v")) {
// Add joins for user and group custom access
$rca_joins[] = 'LEFT JOIN resource_custom_access AS rca_u ON r.ref = rca_u.resource AND rca_u.user = ? AND (rca_u.user_expires IS NULL OR rca_u.user_expires > now())';
$rca_join_params [] = "i";
$rca_join_params [] = $userref;
$rca_joins[] = 'LEFT JOIN resource_custom_access AS rca_ug ON r.ref = rca_ug.resource AND rca_ug.usergroup = ?';
$rca_join_params [] = "i";
$rca_join_params [] = $usergroup;
$rca_where = 'AND (r.access < ? OR (r.access IN (?, ?) AND ((rca_ug.access IS NOT NULL AND rca_ug.access < ?) OR (rca_u.access IS NOT NULL AND rca_u.access < ?))))';
$rca_where_params = array("i", RESOURCE_ACCESS_CONFIDENTIAL, "i", RESOURCE_ACCESS_CONFIDENTIAL, "i", RESOURCE_ACCESS_CUSTOM_GROUP, "i", RESOURCE_ACCESS_CONFIDENTIAL, "i", RESOURCE_ACCESS_CONFIDENTIAL);
$fcf_sql = featured_collections_permissions_filter_sql("AND", "c.ref");
if (is_array($fcf_sql)) {
$fc_permissions_where = "AND (c.`type` = ? " . $fcf_sql[0] . ")";
$fc_permissions_where_params = array_merge(["i",COLLECTION_TYPE_FEATURED], $fcf_sql[1]);
}
}
if ($use_thumbnail_selection_method && isset($c["thumbnail_selection_method"])) {
if ($c["thumbnail_selection_method"] == $FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS["no_image"]) {
return array();
} elseif ($c["thumbnail_selection_method"] == $FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS["manual"] && isset($c["bg_img_resource_ref"])) {
$limit = 1;
$union = sprintf(
"
UNION SELECT ref, 1 AS use_as_theme_thumbnail, r.hit_count FROM resource AS r %s WHERE r.ref = ? %s",
implode(" ", $rca_joins),
$rca_where
);
$unionparams = array_merge($rca_join_params, ["i",$c["bg_img_resource_ref"]], $rca_where_params);
}
// For most_popular_image & most_popular_images we change the limit only if it hasn't been provided by the context.
elseif (in_array($c["thumbnail_selection_method"], [$FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS["most_popular_image"],$FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS["most_recent_image"]]) && is_null($limit)) {
$limit = 1;
} elseif ($c["thumbnail_selection_method"] == $FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS["most_popular_images"] && is_null($limit)) {
$limit = $theme_images_number;
}
}
$resource_join = "JOIN resource AS r ON r.ref = cr.resource AND r.ref > 0";
if (!$collection_allow_not_approved_share) {
$resource_join .= " AND r.archive = 0";
}
// A SQL statement. Each array index represents a different SQL clause.
$subquery = array(
"select" => "SELECT r.ref, cr.use_as_theme_thumbnail, r.hit_count",
"from" => "FROM collection AS c",
"join" => array_merge(
array(
"JOIN collection_resource AS cr ON cr.collection = c.ref",
$resource_join,
),
$rca_joins
),
"where" => "WHERE c.ref = ? AND c.`type` = ?",
);
$subquery_params = array_merge($rca_join_params, array("i", $c["ref"], "i", COLLECTION_TYPE_FEATURED), $rca_where_params);
if (is_featured_collection_category($c)) {
$all_fcs = ps_query("SELECT ref, parent FROM collection WHERE `type`=?", array("i",COLLECTION_TYPE_FEATURED), "featured_collections");
$all_fcs_rp = array_column($all_fcs, 'parent', 'ref');
// Array to hold resources
$fcresources = array();
// Create stack of collections to search
// (not a queue as we want to get to the lowest child collections first where the resources are)
$colstack = new SplStack(); //
$children = array_keys($all_fcs_rp, $c["ref"]);
foreach ($children as $child_fc) {
$colstack->push($child_fc);
}
while ((is_null($limit) || count($fcresources) < $limit) && !$colstack->isEmpty()) {
$checkfc = $colstack->pop();
if (!in_array($checkfc, $all_fcs_rp)) {
$subfcimages = get_collection_resources($checkfc);
if (is_array($subfcimages) && count($subfcimages) > 0) {
// The join defined above specifically excludes any resources that are not in the active archive state,
// for the limiting via $ctx to function correctly we'll need to check for each resources state before adding it to fcresources
$resources = get_resource_data_batch($subfcimages);
if (!$collection_allow_not_approved_share) {
$resources = array_filter($resources, function ($r) {
return $r['archive'] == "0";
});
}
$fcresources = array_merge($fcresources, array_column($resources, 'ref'));
}
continue;
}
// Either a parent FC or no results, add sub fcs to stack
$children = array_keys($all_fcs_rp, $checkfc);
foreach ($children as $child_fc) {
$colstack->push($child_fc);
}
}
$fcrescount = count($fcresources);
if ($fcrescount > 0) {
$chunks = [$fcresources];
// Large numbers of query parameters can cause errors so chunking may be required for larger collections.
if ($fcrescount > 20000) {
$chunks = array_chunk($fcresources, 20000);
}
$fc_resources = [];
$subquery["join"] = implode(" ", $subquery["join"]);
foreach ($chunks as $fcresources) {
$subquery["where"] = " WHERE r.ref IN (" . ps_param_insert(count($fcresources)) . ")";
$subquery_params = array_merge($rca_join_params, ps_param_fill($fcresources, "i"), $rca_where_params);
$subquery["where"] .= " {$rca_where} {$fc_permissions_where}";
$subquery_params = array_merge($subquery_params, $fc_permissions_where_params);
$sql = sprintf(
"SELECT DISTINCT ti.ref AS `value`, ti.use_as_theme_thumbnail, ti.hit_count FROM (%s %s) AS ti ORDER BY ti.use_as_theme_thumbnail DESC, ti.hit_count DESC, ti.ref DESC %s",
implode(" ", $subquery),
$union,
sql_limit(null, $limit)
);
$fc_resources = array_merge($fc_resources, ps_array($sql, array_merge($subquery_params, $unionparams), "themeimage"));
}
$CACHE_FC_RESOURCES[$cache_id] = $fc_resources;
return $fc_resources;
}
}
$subquery["join"] = implode(" ", $subquery["join"]);
$subquery["where"] .= " {$rca_where} {$fc_permissions_where}";
$subquery_params = array_merge($subquery_params, $fc_permissions_where_params);
$order_by = "ti.use_as_theme_thumbnail DESC, ti.hit_count DESC, ti.ref DESC";
if ($c["thumbnail_selection_method"] == $FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS["most_recent_image"]) {
$order_by = "ti.ref DESC";
}
$sql = sprintf(
"SELECT DISTINCT ti.ref AS `value`, ti.use_as_theme_thumbnail, ti.hit_count FROM (%s %s) AS ti ORDER BY %s %s",
implode(" ", $subquery),
$union,
$order_by,
sql_limit(null, $limit)
);
$fc_resources = ps_array($sql, array_merge($subquery_params, $unionparams), "themeimage");
$CACHE_FC_RESOURCES[$cache_id] = $fc_resources;
return $fc_resources;
}
/**
* Get a list of featured collections based on a higher level featured collection category. This returns all direct/indirect
* collections under that category.
*
* @param array $c Collection data structure
* @param array $ctx Contextual data (e.g disable access control). This param MUST NOT get exposed over the API
*
* @return array
*/
function get_featured_collection_categ_sub_fcs(array $c, array $ctx = array())
{
global $CACHE_FC_CATEG_SUB_FCS;
$CACHE_FC_CATEG_SUB_FCS = (!is_null($CACHE_FC_CATEG_SUB_FCS) && is_array($CACHE_FC_CATEG_SUB_FCS) ? $CACHE_FC_CATEG_SUB_FCS : array());
if (isset($CACHE_FC_CATEG_SUB_FCS[$c["ref"]])) {
return $CACHE_FC_CATEG_SUB_FCS[$c["ref"]];
}
$access_control = (isset($ctx["access_control"]) && is_bool($ctx["access_control"]) ? $ctx["access_control"] : true);
$all_fcs = (isset($ctx["all_fcs"]) && is_array($ctx["all_fcs"]) && !empty($ctx["all_fcs"]) ? $ctx["all_fcs"] : get_all_featured_collections());
$collections = array();
$allowed_fcs = ($access_control ? compute_featured_collections_access_control() : true);
if ($allowed_fcs === false) {
$CACHE_FC_CATEG_SUB_FCS[$c["ref"]] = $collections;
return $collections;
} elseif (is_array($allowed_fcs)) {
$allowed_fcs_flipped = array_flip($allowed_fcs);
// Collection is not allowed
if (!isset($allowed_fcs_flipped[$c['ref']])) {
$CACHE_FC_CATEG_SUB_FCS[$c["ref"]] = $collections;
return $collections;
}
}
$all_fcs_rp = reshape_array_by_value_keys($all_fcs, 'ref', 'parent');
$all_fcs = array_flip_by_value_key($all_fcs, 'ref');
$queue = new SplQueue();
$queue->setIteratorMode(SplQueue::IT_MODE_DELETE);
$queue->enqueue($c['ref']);
while (!$queue->isEmpty()) {
$fc = $queue->dequeue();
$fc_children = array();
if (
$all_fcs[$fc]['has_resources'] > 0
&& (
$allowed_fcs === true
|| (is_array($allowed_fcs) && isset($allowed_fcs_flipped[$fc]))
)
) {
$collections[] = $fc;
} elseif ($all_fcs[$fc]['has_children'] > 0) {
$fc_children = array_keys($all_fcs_rp, $fc);
}
foreach ($fc_children as $fc_child_ref) {
$queue->enqueue($fc_child_ref);
}
}
$CACHE_FC_CATEG_SUB_FCS[$c["ref"]] = $collections;
debug("get_featured_collection_categ_sub_fcs(ref = {$c["ref"]}): returned collections: " . implode(", ", $collections));
return $collections;
}
/**
* Get preview URLs for a list of resource IDs
*
* @param array $resource_refs List of resources
* @param string $size Preview size
*
* @return array List of resource refs and corresponding images URLs
*/
function generate_featured_collection_image_urls(array $resource_refs, string $size)
{
global $baseurl;
$images = array();
$refs_list = array_filter($resource_refs, 'is_numeric');
if (empty($refs_list)) {
return $images;
}
$refs_rtype = ps_query("SELECT ref, resource_type, file_extension FROM resource WHERE ref IN (" . ps_param_insert(count($refs_list)) . ")", ps_param_fill($refs_list, "i"), 'featured_collections');
foreach ($refs_rtype as $ref_rt) {
$ref = $ref_rt['ref'];
$resource_type = $ref_rt['resource_type'];
if (file_exists(get_resource_path($ref, true, $size, false)) && resource_download_allowed($ref, $size, $resource_type, -1, true)) {
$images[] = ["ref" => $ref, "path" => get_resource_path($ref, false, $size, false)];
}
}
if (count($images) == 0 && count($refs_rtype) != 0) {
$images[] = $baseurl . '/gfx/no_preview/default.png';
}
return $images;
}
/**
* Inserts $resource1 into the position currently occupied by $resource2
*
* @param integer $resource1
* @param integer $resource2
* @param integer $collection
* @return void
*/
function swap_collection_order($resource1, $resource2, $collection)
{
// sanity check -- we should only be getting IDs here
if (!is_numeric($resource1) || !is_numeric($resource2) || !is_numeric($collection)) {
exit("Error: invalid input to swap collection function.");
}
$query = "select resource,date_added,sortorder from collection_resource where collection=? and resource in (?,?) order by sortorder asc, date_added desc";
$existingorder = ps_query($query, array("i",$collection,"i",$resource1,"i",$resource2));
$counter = 1;
foreach ($existingorder as $record) {
$rec[$counter]['resource'] = $record['resource'];
$rec[$counter]['date_added'] = $record['date_added'];
if (strlen($record['sortorder']) == 0) {
$rec[$counter]['sortorder'] = "NULL";
} else {
$rec[$counter]['sortorder'] = "'" . $record['sortorder'] . "'";
}
$counter++;
}
ps_query(
"update collection_resource set date_added = ?, sortorder = ? where collection = ? and resource = ?",
[
's', $rec[1]['date_added'],
'i', $rec[1]['sortorder'],
'i', $collection,
'i', $rec[2]['resource']
]
);
ps_query(
"update collection_resource set date_added = ?, sortorder = ? where collection = ? and resource = ?",
[
's', $rec[2]['date_added'],
'i', $rec[2]['sortorder'],
'i', $collection,
'i', $rec[1]['resource']
]
);
}
/**
* Reorder the items in a collection using $neworder as the order by metric
*
* @param array $neworder Array of columns to order by
* @param integer $collection
* @param integer $offset
* @return void
*/
function update_collection_order($neworder, $collection, $offset = 0)
{
if (!is_array($neworder)) {
exit("Error: invalid input to update collection function.");
}
$neworder = array_filter($neworder, 'is_numeric');
if (count($neworder) > 0) {
$updatesql = "update collection_resource set sortorder=(case resource ";
$counter = 1 + $offset;
$params = [];
foreach ($neworder as $colresource) {
$updatesql .= "when ? then ? ";
$params = array_merge($params, ['i', $colresource, 'i', $counter]);
$counter++;
}
$updatesql .= "else sortorder END) WHERE collection= ?";
ps_query($updatesql, array_merge($params, ['i', $collection]));
}
$updatesql = "update collection_resource set sortorder=99999 WHERE collection= ? and sortorder is NULL";
ps_query($updatesql, ['i', $collection]);
}
/**
* Return comments and other columns stored in the collection_resource join.
*
* @param integer $resource
* @param integer $collection
* @return array|bool Returns found record data, false otherwise
*/
function get_collection_resource_comment($resource, $collection)
{
$data = ps_query("select " . columns_in("collection_resource") . " from collection_resource where collection=? and resource=?", array("i",$collection,"i",$resource), "");
if (!isset($data[0])) {
return false;
}
return $data[0];
}
/**
* Save a comment and/or rating for the instance of a resource in a collection.
*
* @param integer $resource
* @param integer $collection
* @param string $comment
* @param integer $rating
* @return boolean
*/
function save_collection_resource_comment($resource, $collection, $comment, $rating)
{
# get data before update so that changes can be logged.
$data = ps_query(
"select comment,rating from collection_resource where resource= ? and collection= ?",
[
'i', $resource,
'i', $collection
]
);
$params = [];
if ($rating != "") {
$sql = '?';
$params = ['i', $rating];
} else {
$sql = 'null';
}
ps_query(
"update collection_resource set rating= {$sql},comment= ?,use_as_theme_thumbnail= ? where resource= ? and collection= ?",
array_merge(
$params,
[
's', $comment,
'i', (getval("use_as_theme_thumbnail", "") == "" ? 0 : 1),
'i', $resource,
'i', $collection
]
)
);
# log changes
if ($comment != $data[0]['comment']) {
collection_log($collection, LOG_CODE_COLLECTION_ADDED_RESOURCE_COMMENT, $resource);
}
if ($rating != $data[0]['rating']) {
collection_log($collection, LOG_CODE_COLLECTION_ADDED_RESOURCE_RATING, $resource);
}
return true;
}
/**
* Relates every resource in $collection to $ref
*
* @param integer $ref
* @param integer $collection
* @return void
*/
function relate_to_collection($ref, $collection)
{
$colresources = get_collection_resources($collection);
ps_query("delete from resource_related where resource= ? and related in (" . ps_param_insert(count($colresources)) . ")", array_merge(['i', $ref], ps_param_fill($colresources, 'i')));
$params = [];
foreach ($colresources as $colresource) {
$params = array_merge($params, ['i', $ref, 'i', $colresource]);
}
ps_query(
"INSERT INTO resource_related (resource,related)
VALUES " . implode(', ', array_fill(0, count($colresources), '(?, ?)')),
$params
);
}
/**
* Fetch all the comments for a given collection.
*
* @param integer $collection
* @return array
*/
function get_collection_comments($collection)
{
return ps_query("select " . columns_in("collection_resource") . " from collection_resource where collection=? and length(comment)>0 order by date_added", array("i",$collection));
}
/**
* Sends the feedback to the owner of the collection
*
* @param integer $collection Collection ID
* @param string $comment Comment text
* @return array|void
*/
function send_collection_feedback($collection, $comment)
{
global $applicationname,$lang,$userfullname,$userref,$k,$feedback_resource_select,$regex_email;
global $userref;
$cinfo = get_collection($collection);
if ($cinfo === false) {
error_alert($lang["error-collectionnotfound"]);
exit();
}
$user = get_user($cinfo["user"]);
$body = $lang["collectionfeedbackemail"] . "\n\n";
if (isset($userfullname)) {
$body .= $lang["user"] . ": " . $userfullname . "\n";
} else {
# External user.
if (!preg_match("/{$regex_email}/", getval("email", ""))) {
$errors[] = $lang["youremailaddress"] . ": " . $lang["requiredfield"];
return $errors;
}
$body .= $lang["fullname"] . ": " . getval("name", "") . "\n";
$body .= $lang["email"] . ": " . getval("email", "") . "\n";
}
$body .= $lang["message"] . ": " . stripslashes(str_replace("\\r\\n", "\n", trim($comment)));
$f = get_collection_comments($collection);
for ($n = 0; $n < count($f); $n++) {
$body .= "\n\n" . $lang["resourceid"] . ": " . $f[$n]["resource"];
$body .= "\n" . $lang["comment"] . ": " . trim($f[$n]["comment"]);
if (is_numeric($f[$n]["rating"])) {
$body .= "\n" . $lang["rating"] . ": " . substr("**********", 0, $f[$n]["rating"]);
}
}
if ($feedback_resource_select) {
$body .= "\n\n" . $lang["selectedresources"] . ": ";
$file_list = "";
$result = do_search("!collection" . $collection);
for ($n = 0; $n < count($result); $n++) {
$ref = $result[$n]["ref"];
if (getval("select_" . $ref, "") != "") {
global $filename_field;
$filename = get_data_by_field($ref, $filename_field);
$body .= "\n" . $ref . " : " . $filename;
# Append to a file list that is compatible with Adobe Lightroom
if ($file_list != "") {
$file_list .= ", ";
}
$s = explode(".", $filename);
$file_list .= $s[0];
}
}
# Append Lightroom compatible summary.
$body .= "\n\n" . $lang["selectedresourceslightroom"] . "\n" . $file_list;
}
$cc = getval("email", "");
get_config_option(['user' => $user['ref'], 'usergroup' => $user['usergroup']], 'email_user_notifications', $send_email);
// Always send a mail for the feedback whatever the user preference, since the feedback may be very long so can then refer to the CC'd email
if (filter_var($cc, FILTER_VALIDATE_EMAIL)) {
send_mail($user["email"], $applicationname . ": " . $lang["collectionfeedback"] . " - " . $cinfo["name"], $body, "", "", "", null, "", $cc);
} else {
send_mail($user["email"], $applicationname . ": " . $lang["collectionfeedback"] . " - " . $cinfo["name"], $body);
}
// Add a system notification message as well
message_add($user["ref"], $lang["collectionfeedback"] . " - " . $cinfo["name"] . "<br />" . $body, "", (isset($userref)) ? $userref : $user['ref'], MESSAGE_ENUM_NOTIFICATION_TYPE_SCREEN, 60 * 60 * 24 * 30);
}
/**
* Copy a collection contents
*
* @param integer $copied The collection to copy from
* @param integer $current The collection to copy to
* @param boolean $remove_existing Should existing items be removed?
* @return void
*/
function copy_collection($copied, $current, $remove_existing = false)
{
# Get all data from the collection to copy.
$copied_collection = ps_query("select cr.resource, r.resource_type, cr.sortorder from collection_resource cr join resource r on cr.resource=r.ref where collection=?", array("i",$copied), "");
if ($remove_existing) {
#delete all existing data in the current collection
ps_query("delete from collection_resource where collection=?", array("i",$current));
collection_log($current, LOG_CODE_COLLECTION_REMOVED_ALL_RESOURCES, 0);
}
#put all the copied collection records in
foreach ($copied_collection as $col_resource) {
# Use correct function so external sharing is honoured.
add_resource_to_collection($col_resource['resource'], $current, true, "", $col_resource['resource_type'], null, null, '', $col_resource['sortorder']);
}
}
/**
* Returns true if a collection is a research request
*
* @param int $collection Collection ID
*
* @return boolean
*/
function collection_is_research_request($collection)
{
return ps_value("SELECT count(*) value FROM research_request WHERE collection=?", array("i", $collection), 0) > 0;
}
/**
* Generates a HTML link for adding a resource to a collection
*
* @param integer $resource ID of resource
* @param string $extracode Additional code to be run when link is selected
* IMPORTANT: never use untrusted data here!
* @param string $size Resource size if appropriate
* @param string $class Class to be applied to link
* @param string $view_title The title of the field, taken from $view_title_field
*
* @return string
*/
function add_to_collection_link($resource, $extracode = "", $size = "", $class = "", $view_title = ""): string
{
$resource = (int) $resource;
$size = escape($size);
$class = escape($class);
$title = escape($GLOBALS['lang']["addtocurrentcollection"] . (($view_title != "") ? " - " . $view_title : ""));
return "<a class=\"addToCollection {$class}\" href=\"#\" title=\"{$title}\""
. " onClick=\"AddResourceToCollection(event, {draggable: jQuery('div#ResourceShell{$resource}')},'{$resource}','{$size}'); {$extracode} return false;\""
. " data-resource-ref=\"{$resource}\""
. generate_csrf_data_for_api_native_authmode('add_resource_to_collection')
. ">";
}
/**
* Render a "remove from collection" link wherever such a function is shown in the UI
*
* @param integer $resource
* @param string $class
* @param string $onclick Additional onclick code to call before returning false.
* @param bool $notused No longer used
* @param string $view_title The title of the field, taken from $view_title_field
*
*/
function remove_from_collection_link($resource, $class = "", string $onclick = '', $notused = false, $view_title = ""): string
{
# Generates a HTML link for removing a resource from a collection
global $lang, $pagename;
$resource = (int) $resource;
$class = escape($class);
$pagename = escape($pagename);
$title = escape($lang["removefromcurrentcollection"] . (trim($view_title) != "" ? " - " . $view_title : ""));
return "<a class=\"removeFromCollection {$class}\" href=\"#\" title=\"{$title}\" "
. "onClick=\"RemoveResourceFromCollection(event,'{$resource}','{$pagename}'); {$onclick} return false;\""
. "data-resource-ref=\"{$resource}\""
. generate_csrf_data_for_api_native_authmode('remove_resource_from_collection')
. ">";
}
/**
* Generates a HTML link for adding a changing the current collection
*
* @param integer $collection
* @return string
*/
function change_collection_link($collection)
{
global $lang;
return '<a onClick="ChangeCollection(' . $collection . ',\'\');return false;" href="collections.php?collection=' . $collection . '">' . LINK_CARET . $lang["selectcollection"] . '</a>';
}
/**
* Return all external access given to a collection.
* Users, emails and dates could be multiple for a given access key, an in this case they are returned comma-separated.
*
* @param integer $collection
* @return array
*/
function get_collection_external_access($collection)
{
global $userref;
# Restrict to only their shares unless they have the elevated 'v' permission
$condition = "AND upload=0 ";
$params = array("i",$collection);
if (!checkperm("v")) {
$condition .= "AND user=?";
$params[] = "i";
$params[] = $userref;
}
return ps_query("SELECT access_key,GROUP_CONCAT(DISTINCT user ORDER BY user SEPARATOR ', ') users,GROUP_CONCAT(DISTINCT email ORDER BY email SEPARATOR ', ') emails,MAX(date) maxdate,MAX(lastused) lastused,access,expires,usergroup,password_hash,upload from external_access_keys WHERE collection=? $condition group by access_key order by date", $params);
}
/**
* Delete a specific collection access key, withdrawing access via that key to the collection in question
*
* @param integer $collection
* @param string $access_key
* @return void
*/
function delete_collection_access_key($collection, $access_key)
{
# Get details for log
$users = ps_value("SELECT group_concat(DISTINCT email ORDER BY email SEPARATOR ', ') value FROM external_access_keys WHERE collection=? AND access_key = ? group by access_key ", array("i",$collection,"s",$access_key), "");
# Deletes the given access key.
$params = array("s",$access_key);
$sql = "DELETE FROM external_access_keys WHERE access_key=?";
if ($collection != 0) {
$sql .= " AND collection=?";
$params[] = "i";
$params[] = $collection;
}
ps_query($sql, $params);
# log changes
collection_log($collection, LOG_CODE_COLLECTION_STOPPED_RESOURCE_ACCESS, "", $users . " (" . $access_key . ")");
}
/**
* Add a new row to the collection log (e.g. after an action on that collection)
*
* @param integer $collection
* @param string $type Action type
* @param integer $resource
* @param string $notes
* @return void
*/
function collection_log($collection, $type, $resource, $notes = "")
{
global $userref;
if (!is_numeric($collection)) {
return false;
}
$user = ($userref ?: null);
$resource = ($resource ?: null);
$notes = mb_strcut($notes, 0, 255);
ps_query("INSERT INTO collection_log (date, user, collection, type, resource, notes) VALUES (now(), ?, ?, ?, ?, ?)", array("i",$user,"i",$collection,"s",$type,"i",$resource,"s",$notes));
}
/**
* Return the log for $collection
*
* @param integer $collection
* @param integer $fetchrows How many rows to fetch
* @return array
*/
function get_collection_log($collection, $fetchrows = -1)
{
debug_function_call("get_collection_log", func_get_args());
global $view_title_field;
$extra_fields = hook("collection_log_extra_fields");
if (!$extra_fields) {
$extra_fields = "";
}
$log_query = new PreparedStatementQuery(
"SELECT c.ref,
c.date,
u.username,
u.fullname,
c.type,
r.field{$view_title_field} AS title,
c.resource,
c.notes
{$extra_fields}
FROM collection_log AS c
LEFT OUTER JOIN user AS u ON u.ref = c.user
LEFT OUTER JOIN resource AS r ON r.ref = c.resource
WHERE collection = ?
ORDER BY c.ref DESC",
array("i",$collection)
);
return sql_limit_with_total_count($log_query, $fetchrows, 0, false, null);
}
/**
* Returns the maximum access (the most permissive) that the current user has to the resources in $collection.
*
* @param integer $collection
* @return integer
*/
function collection_max_access($collection)
{
$maxaccess = 2;
$result = do_search("!collection" . $collection);
if (!is_array($result)) {
$result = array();
}
for ($n = 0; $n < count($result); $n++) {
# Load access level
$access = get_resource_access($result[$n]);
if ($access < $maxaccess) {
$maxaccess = $access;
}
}
return $maxaccess;
}
/**
* Returns the minimum access (the least permissive) that the current user has to the resources in $collection.
*
* Can be passed a collection ID or the results of a collection search, the result will be the most restrictive
* access that is found.
*
* @param integer|array $collection Collection ID as an integer or the result of a search as an array
*
* @return integer 0 - Open, 1 - restricted, 2 - Confidential
*/
function collection_min_access($collection)
{
global $k, $internal_share_access, $usersearchfilter;
if (is_array($collection)) {
$result = $collection;
} else {
$result = do_search("!collection{$collection}", '', 'relevance', 0, -1, 'desc', false, '', false, '', '', false, false, true);
}
if (!is_array($result) || empty($result)) {
return 2;
}
if (checkperm("v")) {
// Always has open access
return 0;
}
if (isset($result[0]["resultant_access"])) {
$minaccess = max(array_column($result, "resultant_access"));
} else {
# Reset minaccess and allow get_resource_access to determine the min access for the collection
$minaccess = 0;
$usersearchfilter_original = $usersearchfilter;
# Performance improvement - Don't check search filters again in get_resource_access as $result contains only resources allowed by the search filter.
$usersearchfilter = '';
for ($n = 0; $n < count($result); $n++) {
$access = get_resource_access($result[$n]); // Use the access already calculated if available
if ($access > $minaccess) {
$minaccess = $access;
}
}
$usersearchfilter = $usersearchfilter_original;
}
if ($k != "") {
# External access - check how this was shared. If internal share access and share is more open than the user's access return that
$params[] = "s";
$params[] = $k;
// Don't check each resource as an access key only ever has one level of access
$minextaccess = ps_value("SELECT access value FROM external_access_keys WHERE access_key = ? AND (expires IS NULL OR expires > NOW()) LIMIT 1", $params, -1);
if ($minextaccess != -1 && (!$internal_share_access || ($internal_share_access && ($minextaccess < $minaccess)))) {
return $minextaccess;
}
}
return $minaccess;
}
/**
* Set an existing collection to be public
*
* @param integer $collection ID of collection
*
* @return boolean
*/
function collection_set_public($collection)
{
if (is_numeric($collection)) {
$sql = "UPDATE collection SET `type` = " . COLLECTION_TYPE_PUBLIC . " WHERE ref = ?";
ps_query($sql, array("i",$collection));
return true;
} else {
return false;
}
}
/**
* Remove all resources from a collection
*
* @param integer $ref The collection in question
* @return void
*/
function remove_all_resources_from_collection($ref)
{
$collection_type = ps_value("select type value from collection where ref=?", array("i",$ref), "");
if ($collection_type != COLLECTION_TYPE_SELECTION) {
$removed_resources = ps_array("SELECT resource AS value FROM collection_resource WHERE collection = ?", array("i",$ref));
collection_log($ref, LOG_CODE_COLLECTION_REMOVED_ALL_RESOURCES, 0);
foreach ($removed_resources as $removed_resource_id) {
collection_log($ref, LOG_CODE_COLLECTION_REMOVED_RESOURCE, $removed_resource_id, ' - Removed all resources from collection ID ' . $ref);
}
}
ps_query("DELETE FROM collection_resource WHERE collection = ?", array("i",$ref));
ps_query("DELETE FROM external_access_keys WHERE collection = ? AND upload!=1", array("i",$ref));
}
/**
* Retrieve promoted collections to be displayed on the home page.
*
* This function fetches public collections that are marked for publishing to the home page.
* It returns an array of collection data, including metadata and thumbnail information for the
* home page image if one is assigned.
*
* @return array An array of associative arrays representing each promoted collection, with keys:
* - 'ref' (int): The unique identifier for the collection.
* - 'type' (int): The type identifier for the collection.
* - 'name' (string): The name of the collection.
* - 'home_page_publish' (int): Indicates if the collection is published on the home page.
* - 'home_page_text' (string): Display text for the collection for the home page.
* - 'home_page_image' (int): Resource ID for the image displayed on the home page.
* - 'thumb_height' (int): Thumbnail height of the associated image.
* - 'thumb_width' (int): Thumbnail width of the associated image.
* - 'resource_type' (int): The type of the associated resource.
* - 'file_extension' (string): File extension of the associated resource.
*/
function get_home_page_promoted_collections()
{
global $COLLECTION_PUBLIC_TYPES;
$public_types = join(", ", $COLLECTION_PUBLIC_TYPES); // Note this is a constant and not user input - does not need to be a a parameter in the next line.
return ps_query("select collection.ref, collection.`type`,collection.name,collection.home_page_publish,collection.home_page_text,collection.home_page_image,resource.thumb_height,resource.thumb_width, resource.resource_type, resource.file_extension from collection left outer join resource on collection.home_page_image=resource.ref where collection.`type` IN ({$public_types}) and collection.home_page_publish=1 order by collection.ref desc");
}
/**
* Return an array of distinct archive/workflow states for resources in $collection
*
* @param integer $collection
* @return array
*/
function is_collection_approved($collection)
{
if (is_array($collection)) {
$result = $collection;
} else {
$result = do_search("!collection" . $collection, "", "relevance", 0, -1, "desc", false, "", false, "");
}
if (!is_array($result) || count($result) == 0) {
return true;
}
$collectionstates = array();
global $collection_allow_not_approved_share;
for ($n = 0; $n < count($result); $n++) {
$archivestatus = $result[$n]["archive"];
if ($archivestatus < 0 && !$collection_allow_not_approved_share) {
return false;
}
$collectionstates[] = $archivestatus;
}
return array_unique($collectionstates);
}
/**
* Update an existing external access share
*
* @param string $key External access key
* @param int $access Share access level
* @param string $expires Share expiration date
* @param int $group ID of usergroup that share will emulate permissions for
* @param string $sharepwd Share password
* @param array $shareopts Array of additional share options
* "collection" - int collection ID
* "upload" - bool Set to true if share is an upload link (no visibility of existing resources)
*
* @return boolean
*/
function edit_collection_external_access($key, $access = -1, $expires = "", $group = "", $sharepwd = "", $shareopts = array())
{
global $usergroup, $scramble_key, $lang;
if ($key == "") {
return false;
}
if (
(!isset($shareopts['upload']) || !$shareopts['upload'] )
&& ($group == "" || !checkperm("x"))
) {
// Default to sharing with the permission of the current usergroup if not specified OR no access to alternative group selection.
$group = $usergroup;
}
// Ensure these are escaped as required here
$setvals = array(
"access" => (int)$access,
"usergroup" => (int)$group,
);
if (isset($shareopts['upload']) && $shareopts['upload']) {
$setvals['upload'] = 1;
}
if ($expires != "") {
$expires = date_format(date_create($expires), 'Y-m-d') . ' 23:59:59';
$setvals["expires"] = $expires;
} else {
$setvals["expires"] = null;
}
if ($sharepwd != "(unchanged)") {
$setvals["password_hash"] = ($sharepwd == "") ? "" : hash('sha256', $key . $sharepwd . $scramble_key);
}
$setsql = "";
$params = [];
foreach ($setvals as $setkey => $setval) {
$setsql .= $setsql == "" ? "" : ",";
$setsql .= $setkey . "= ?";
$params = array_merge($params, ['s', $setval]);
}
$setsql .= ', date = now()';
$params = array_merge($params, ['s', $key]);
$condition = '';
if (isset($shareopts['collection'])) {
$condition = ' AND collection = ?';
$params = array_merge($params, ['i', $shareopts['collection']]);
}
ps_query(
"UPDATE external_access_keys
SET " . $setsql . "
WHERE access_key= ?" . $condition,
$params
);
hook("edit_collection_external_access", "", array($key,$access,$expires,$group,$sharepwd, $shareopts));
if (isset($shareopts['collection'])) {
$lognotes = array("access_key" => $key);
foreach ($setvals as $column => $value) {
if ($column == "password_hash") {
$lognotes[] = trim($value) != "" ? "password=TRUE" : "";
} else {
$lognotes[] = $column . "=" . $value;
}
}
collection_log($shareopts['collection'], LOG_CODE_COLLECTION_EDIT_UPLOAD_SHARE, null, "(" . implode(",", $lognotes) . ")");
}
return true;
}
/**
* Hide or show a collection from the My Collections area.
*
* @param integer $colref
* @param boolean $show Show or hide?
* @param integer $user
* @return bool
*/
function show_hide_collection($colref, $show = true, $user = "")
{
global $userref;
if ($user == "" || $user == $userref) {
// Working with logged on user, use global variable
$user = $userref;
global $hidden_collections;
} else {
if (!checkperm_user_edit($user)) {
return false;
}
//Get hidden collections for user
$hidden_collections = explode(",", ps_value("SELECT hidden_collections FROM user WHERE ref=?", array("i",$user), ""));
}
if ($show) {
debug("Unhiding collection " . $colref . " from user " . $user);
if (($key = array_search($colref, $hidden_collections)) !== false) {
unset($hidden_collections[$key]);
}
} else {
debug("Hiding collection " . $colref . " from user " . $user);
if (array_search($colref, $hidden_collections) === false) {
$hidden_collections[] = $colref;
}
}
ps_query("UPDATE user SET hidden_collections = ? WHERE ref= ?", ['s', implode(',', $hidden_collections), 'i', $user]);
return true;
}
/**
* Get an array of collection IDs for the specified ResourceSpace session and user
*
* @param string $rs_session Session id - as obtained by get_rs_session_id()
* @param integer $userref User ID
* @param boolean $create Create new collection?
*
* @return array Array of collection IDs for the specified sesssion
*/
function get_session_collections($rs_session, $userref = "", $create = false)
{
$extrasql = "";
$params = array("s",$rs_session);
if ($userref != "") {
$extrasql = "AND user=?";
$params[] = "i";
$params[] = $userref;
} else {
$userref = 'NULL';
}
$collectionrefs = ps_array("SELECT ref value FROM collection WHERE session_id=? AND type IN ('" . COLLECTION_TYPE_STANDARD . "','" . COLLECTION_TYPE_UPLOAD . "','" . COLLECTION_TYPE_SHARE_UPLOAD . "') " . $extrasql, $params, "");
if (count($collectionrefs) < 1 && $create) {
if (upload_share_active()) {
$collectionrefs[0] = create_collection($userref, "New uploads", 0, 1, 0, false, array("type" => 5)); # Do not translate this string!
} else {
$collectionrefs[0] = create_collection($userref, "Default Collection", 0, 1); # Do not translate this string!
}
}
return $collectionrefs;
}
/**
* Update collection to belong to a new user
*
* @param integer $collection Collection ID
* @param integer $newuser User ID to assign collection to
*
* @return boolean success|failure
*/
function update_collection_user($collection, $newuser)
{
if (!collection_writeable($collection)) {
debug("FAILED TO CHANGE COLLECTION USER " . $collection);
return false;
}
ps_query("UPDATE collection SET user=? WHERE ref=?", array("i",$newuser,"i",$collection));
return true;
}
/**
* Helper function for render_actions(). Compiles actions that are normally valid for collections
*
* @param array $collection_data Collection data
* @param boolean $top_actions Set to true if actions are to be rendered in the search filter bar (above results)
* @param array $resource_data Resource data
*
* @return array
*/
function compile_collection_actions(array $collection_data, $top_actions, $resource_data = array())
{
global $baseurl_short, $lang, $k, $userrequestmode, $zipcommand, $collection_download, $archiver_path,
$manage_collections_share_link, $allow_share, $enable_collection_copy,
$manage_collections_remove_link, $userref, $collection_purge, $result,
$order_by, $sort, $archive, $contact_sheet_link_on_collection_bar,
$show_searchitemsdiskusage, $emptycollection, $count_result,
$download_usage, $home_dash, $top_nav_upload_type, $pagename, $offset, $col_order_by, $find, $default_sort,
$default_collection_sort, $restricted_share, $hidden_collections, $internal_share_access, $search,
$usercollection, $disable_geocoding, $collection_download_settings, $contact_sheet, $pagename,$upload_then_edit, $enable_related_resources,$list, $enable_themes,
$system_read_only, $USER_SELECTION_COLLECTION;
$is_selection_collection = isset($collection_data['ref']) && $collection_data['ref'] == $USER_SELECTION_COLLECTION;
#This is to properly render the actions drop down in the themes page
if (isset($collection_data['ref']) && $pagename != "collections") {
if (!is_array($result)) {
$result = get_collection_resources_with_data($collection_data['ref']);
}
if (('' == $k || $internal_share_access) && is_null($list)) {
$list = get_user_collections($userref);
}
$count_result = count($result);
}
if (isset($search) && substr($search, 0, 11) == '!collection' && ($k == '' || $internal_share_access)) {
# Extract the collection number - this bit of code might be useful as a function
$search_collection = explode(' ', $search);
$search_collection = str_replace('!collection', '', $search_collection[0]);
$search_collection = explode(',', $search_collection); // just get the number
$search_collection = $search_collection[0];
}
// Collection bar actions should always be a special search !collection[ID] (exceptions might arise but most of the
// time it should be handled using the special search). If top actions then search may include additional refinement inside the collection
if (isset($collection_data['ref']) && !$top_actions) {
$search = "!collection{$collection_data['ref']}";
}
$urlparams = array(
"search" => $search,
"collection" => (isset($collection_data['ref']) ? $collection_data['ref'] : ""),
"ref" => (isset($collection_data['ref']) ? $collection_data['ref'] : ""),
"restypes" => isset($_COOKIE['restypes']) ? $_COOKIE['restypes'] : "",
"order_by" => $order_by,
"col_order_by" => $col_order_by,
"sort" => $sort,
"offset" => $offset,
"find" => $find,
"k" => $k);
$options = array();
$o = 0;
if (empty($collection_data)) {
return $options;
}
if (empty($order_by)) {
$order_by = $default_collection_sort;
}
// Check minimum access if we have all the data (i.e. not padded for search display), if not then render anyway and access will be checked on target page
$lastresource = end($resource_data);
if ($pagename == 'collection_manage') {
$min_access = collection_min_access($collection_data['ref']);
} elseif (isset($lastresource["ref"])) {
$min_access = collection_min_access($resource_data);
} else {
$min_access = 0;
}
// View all resources
if (
!$top_actions // View all resources makes sense only from collection bar context
&& (
($k == "" || $internal_share_access)
&& (isset($collection_data["c"]) && $collection_data["c"] > 0)
|| (is_array($result) && count($result) > 0)
)
) {
$tempurlparams = array(
'sort' => 'ASC',
'search' => (isset($collection_data['ref']) ? "!collection{$collection_data['ref']}" : $search),
);
$data_attribute['url'] = generateURL($baseurl_short . "pages/search.php", $urlparams, $tempurlparams);
$options[$o]['value'] = 'view_all_resources_in_collection';
$options[$o]['label'] = $lang['view_all_resources'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESOURCE;
$options[$o]['order_by'] = 10;
$o++;
}
// Download option
if ($min_access == 0) {
if ($download_usage && ( isset($zipcommand) || $GLOBALS['use_zip_extension'] || ( isset($archiver_path) && isset($collection_download_settings) ) ) && $collection_download && $count_result > 0) {
$download_url = generateURL($baseurl_short . "pages/download_usage.php", $urlparams);
$data_attribute['url'] = generateURL($baseurl_short . "pages/terms.php", $urlparams, array("url" => $download_url));
$options[$o]['value'] = 'download_collection';
$options[$o]['label'] = $lang['action-download'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESOURCE;
$options[$o]['order_by'] = 20;
$o++;
} elseif ((isset($zipcommand) || $GLOBALS['use_zip_extension'] || ( isset($archiver_path) && isset($collection_download_settings) ) ) && $collection_download && $count_result > 0) {
$download_url = generateURL($baseurl_short . "pages/collection_download.php", $urlparams);
$data_attribute['url'] = generateURL($baseurl_short . "pages/terms.php", $urlparams, array("url" => $download_url));
$options[$o]['value'] = 'download_collection';
$options[$o]['label'] = $lang['action-download'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESOURCE;
$options[$o]['order_by'] = 20;
$o++;
}
}
// Upload to collection
if (allow_upload_to_collection($collection_data)) {
if ($upload_then_edit) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/upload_batch.php", array(), array("collection_add" => $collection_data['ref']));
} else {
$data_attribute['url'] = generateURL($baseurl_short . "pages/edit.php", array(), array("uploader" => $top_nav_upload_type,"ref" => -$userref, "collection_add" => $collection_data['ref']));
}
$options[$o]['value'] = 'upload_collection';
$options[$o]['label'] = $lang['action-upload-to-collection'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESOURCE;
$options[$o]['order_by'] = 30;
$o++;
}
// Remove all resources from collection
if (!checkperm("b") && 0 < $count_result && ($k == "" || $internal_share_access) && isset($emptycollection) && !$system_read_only && collection_writeable($collection_data['ref'])) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/collections.php", $urlparams, array("emptycollection" => $collection_data['ref'],"removeall" => "true","ajax" => "true","submitted" => "removeall"));
$options[$o]['value'] = 'empty_collection';
$options[$o]['label'] = $lang['emptycollection'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESOURCE;
$options[$o]['order_by'] = 50;
$o++;
}
if (!collection_is_research_request($collection_data['ref']) || !checkperm('r')) {
if (
!$top_actions && checkperm('s')
&& $pagename === 'collections'
&& isset($collection_data['request_feedback'])
&& $collection_data['request_feedback']
) {
// Collection feedback
$data_attribute['url'] = sprintf(
'%spages/collection_feedback.php?collection=%s&k=%s',
$baseurl_short,
urlencode($collection_data['ref']),
urlencode($k)
);
$options[$o]['value'] = 'collection_feedback';
$options[$o]['label'] = $lang['sendfeedback'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESOURCE;
$options[$o]['order_by'] = 70;
$o++;
}
} else {
$research = ps_value('SELECT ref value FROM research_request WHERE collection=?', array("i",$collection_data['ref']), 0);
// Manage research requests
$data_attribute['url'] = generateURL($baseurl_short . "pages/team/team_research.php", $urlparams);
$options[$o]['value'] = 'manage_research_requests';
$options[$o]['label'] = $lang['manageresearchrequests'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESEARCH;
$options[$o]['order_by'] = 80;
$o++;
// Edit research requests
$data_attribute['url'] = generateURL($baseurl_short . "pages/team/team_research_edit.php", $urlparams, array("ref" => $research));
$options[$o]['value'] = 'edit_research_requests';
$options[$o]['label'] = $lang['editresearchrequests'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESEARCH;
$options[$o]['order_by'] = 90;
$o++;
}
// Select collection option - not for collection bar
if (
$pagename != 'collections' && ($k == '' || $internal_share_access) && !checkperm('b')
&& ($pagename == 'load_actions' || $pagename == 'themes' || $pagename === 'collection_manage' || $pagename === 'resource_collection_list' || $top_actions)
&& ((isset($search_collection) && isset($usercollection) && $search_collection != $usercollection) || !isset($search_collection))
&& collection_readable($collection_data['ref'])
) {
$options[$o]['value'] = 'select_collection';
$options[$o]['label'] = $lang['selectcollection'];
$options[$o]['category'] = ACTIONGROUP_COLLECTION;
$options[$o]['order_by'] = 100;
$o++;
}
// Copy resources from another collection. Must be in top actions or have more than one collection available if on collections.php
if (
!checkperm('b')
&& ($k == '' || $internal_share_access)
&& collection_readable($collection_data['ref'])
&& ($top_actions || (is_array($list) && count($list) > 1))
&& $enable_collection_copy
) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/collection_copy_resources.php", array("ref" => $collection_data['ref']));
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['value'] = 'copy_collection';
$options[$o]['label'] = $lang['copyfromcollection'];
$options[$o]['category'] = ACTIONGROUP_RESOURCE;
$options[$o]['order_by'] = 105;
$o++;
}
// Edit Collection
if ((($userref == $collection_data['user'] && !in_array($collection_data['type'], [COLLECTION_TYPE_REQUEST,COLLECTION_TYPE_SELECTION])) || (checkperm('h'))) && ($k == '' || $internal_share_access) && !$system_read_only) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/collection_edit.php", $urlparams);
$options[$o]['value'] = 'edit_collection';
$options[$o]['label'] = $lang['editcollection'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_EDIT;
$options[$o]['order_by'] = 110;
$o++;
}
if (isset($lastresource["ref"])) {
// Work this out based on resource data
$allow_multi_edit = allow_multi_edit($resource_data, $collection_data['ref']);
} else {
// Padded result set. It is too expensive to work this out every time for large result sets,
// Show edit actions for logged in users and access will be checked once action has been selected.
$allow_multi_edit = $k == "";
}
// Edit all
# If this collection is (fully) editable, then display an edit all link
if (
( $k == "" || $internal_share_access )
&& $count_result > 0
&& $allow_multi_edit
) {
$extra_params = array(
'editsearchresults' => 'true',
);
$data_attribute['url'] = generateURL($baseurl_short . "pages/edit.php", $urlparams, $extra_params);
$options[$o]['value'] = 'edit_all_in_collection';
$options[$o]['label'] = $lang['edit_all_resources'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_EDIT;
$options[$o]['order_by'] = 120;
$o++;
}
// Edit Previews
if (($k == "" || $internal_share_access) && $count_result > 0 && !(checkperm('F*')) && ($userref == $collection_data['user'] || $collection_data['allow_changes'] == 1 || checkperm('h')) && $allow_multi_edit) {
$main_pages = array('search', 'collection_manage', 'collection_public', 'themes');
$back_to_page = (in_array($pagename, $main_pages) ? escape($pagename) : '');
$data_attribute['url'] = generateURL($baseurl_short . "pages/collection_edit_previews.php", $urlparams, array("backto" => $back_to_page));
$options[$o]['value'] = 'edit_previews';
$options[$o]['label'] = $lang['editcollectionresources'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_EDIT;
$options[$o]['order_by'] = 130;
$o++;
}
// Share
if (allow_collection_share($collection_data)) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/collection_share.php", $urlparams);
$options[$o]['value'] = 'share_collection';
$options[$o]['label'] = $lang['share'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_SHARE;
$options[$o]['order_by'] = 140;
$o++;
}
// Share external link to upload to collection, not permitted if already externally shared for view access
$eakeys = get_external_shares(array("share_collection" => $collection_data['ref'],"share_type" => 0));
if (can_share_upload_link($collection_data) && count($eakeys) == 0) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/share_upload.php", array(), array("share_collection" => $collection_data['ref']));
$options[$o]['value'] = 'share_upload';
$options[$o]['label'] = $lang['action-share-upload-link'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_SHARE;
$options[$o]['order_by'] = 30;
$o++;
}
// Home_dash is on, AND NOT Anonymous use, AND (Dash tile user (NOT with a managed dash) || Dash Tile Admin)
if (!$top_actions && $home_dash && ($k == '' || $internal_share_access) && checkPermission_dashcreate() && !$system_read_only && !in_array($collection_data['type'], [COLLECTION_TYPE_REQUEST,COLLECTION_TYPE_SELECTION])) {
$is_smart_featured_collection = (isset($collection_data["smart"]) ? (bool) $collection_data["smart"] : false);
$is_featured_collection_category = (is_featured_collection_category($collection_data) || is_featured_collection_category_by_children($collection_data["ref"]));
$is_featured_collection = (!$is_featured_collection_category && !$is_smart_featured_collection);
$tileparams = array(
'create' => 'true',
'tltype' => 'srch',
'tlstyle' => 'thmbs',
'promoted_resource' => 'true',
'freetext' => 'true',
'all_users' => '1',
'title' => $collection_data["name"],
);
if ($is_featured_collection) {
$tileparams['tltype'] = 'srch';
$tileparams['link'] = generateURL($baseurl_short . 'pages/search.php', array('search' => '!collection' . $collection_data['ref']));
} else {
$tileparams['tltype'] = 'fcthm';
$tileparams['link'] = generateURL($baseurl_short . 'pages/collections_featured.php', array('parent' => $collection_data['ref']));
}
$data_attribute['url'] = generateURL($baseurl_short . "pages/dash_tile.php", $urlparams, $tileparams);
$options[$o]['value'] = 'save_collection_to_dash';
$options[$o]['label'] = $lang['createnewdashtile'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_SHARE;
$options[$o]['order_by'] = 150;
$o++;
}
// Add option to publish as featured collection
if ($enable_themes && ($k == '' || $internal_share_access) && checkperm("h") && !in_array($collection_data['type'], [COLLECTION_TYPE_REQUEST,COLLECTION_TYPE_SELECTION])) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/collection_set_category.php", $urlparams);
$options[$o]['value'] = 'collection_set_category';
$options[$o]['label'] = $lang['collection_set_theme_category'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_SHARE;
$options[$o]['order_by'] = 160;
$o++;
}
// Request all
if ($count_result > 0 && checkperm("q")) {
# Ability to request a whole collection
# This option should only be rendered if at least one of the resources is not downloadable
$data_attribute['url'] = generateURL($baseurl_short . "pages/collection_request.php", $urlparams);
$options[$o]['value'] = 'request_all';
$options[$o]['label'] = $lang['requestall'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_RESOURCE;
$options[$o]['order_by'] = 170;
$o++;
}
// Contact Sheet
if (0 < $count_result && ($k == "" || $internal_share_access) && $contact_sheet && ($contact_sheet_link_on_collection_bar)) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/contactsheet_settings.php", $urlparams);
$options[$o]['value'] = 'contact_sheet';
$options[$o]['label'] = $lang['contactsheet'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 190;
$o++;
}
// Remove
if (
($k == "" || $internal_share_access)
&& $manage_collections_remove_link
&& $userref != $collection_data['user']
&& !checkperm('b')
&& collection_readable($collection_data['ref'])
) {
$options[$o]['value'] = 'remove_collection';
$options[$o]['label'] = $lang['action-remove'];
$options[$o]['category'] = ACTIONGROUP_COLLECTION;
$options[$o]['order_by'] = 200;
$o++;
}
// Delete
if (($k == "" || $internal_share_access) && (($userref == $collection_data['user']) || checkperm('h')) && ($collection_data['cant_delete'] == 0) && $collection_data['type'] != COLLECTION_TYPE_REQUEST) {
$options[$o]['value'] = 'delete_collection';
$options[$o]['label'] = $lang['action-deletecollection'];
$options[$o]['category'] = ACTIONGROUP_EDIT;
$options[$o]['order_by'] = 210;
$o++;
}
// Collection Purge
if (($k == "" || $internal_share_access) && $collection_purge && isset($collections) && checkperm('e0') && $collection_data['cant_delete'] == 0) {
$options[$o]['value'] = 'purge_collection';
$options[$o]['label'] = $lang['purgeanddelete'];
$options[$o]['category'] = ACTIONGROUP_EDIT;
$options[$o]['order_by'] = 220;
$o++;
}
// Collection log
if (($k == "" || $internal_share_access) && ($userref == $collection_data['user'] || (checkperm('h')))) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/collection_log.php", $urlparams);
$options[$o]['value'] = 'collection_log';
$options[$o]['label'] = $lang['action-log'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 230;
$o++;
}
// Delete all
// Note: functionality moved from edit collection page
if (
($k == "" || $internal_share_access)
&& (!$top_actions || $is_selection_collection)
&& ((is_array($result) && count($result) != 0) || $count_result != 0)
&& collection_writeable($collection_data['ref'])
&& $allow_multi_edit
&& !checkperm('D')
) {
$options[$o]['value'] = 'delete_all_in_collection';
$options[$o]['label'] = $lang['deleteallresourcesfromcollection'];
$options[$o]['category'] = ACTIONGROUP_EDIT;
$options[$o]['order_by'] = 240;
$o++;
}
// Show disk usage
if (($k == "" || $internal_share_access) && (checkperm('a') || checkperm('v')) && !$top_actions && $show_searchitemsdiskusage && 0 < $count_result) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/search_disk_usage.php", $urlparams);
$options[$o]['value'] = 'search_items_disk_usage';
$options[$o]['label'] = $lang['collection_disk_usage'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 250;
$o++;
}
// CSV export of collection metadata
if (
0 < $count_result
&& !$top_actions
&& ($k == '' || $internal_share_access)
&& collection_readable($collection_data['ref'])
) {
$options[$o]['value'] = 'csv_export_results_metadata';
$options[$o]['label'] = $lang['csvExportResultsMetadata'];
$data_attribute['url'] = generateURL($baseurl_short . "pages/csv_export_results_metadata.php", $urlparams);
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 260;
$o++;
if (!checkperm('b') && !$system_read_only) {
// Hide Collection
$user_mycollection = ps_value("select ref value from collection where user=? and name='Default Collection' order by ref limit 1", array("i",$userref), "");
// check that this collection is not hidden. use first in alphabetical order otherwise
if (in_array($user_mycollection, $hidden_collections)) {
$sql = "select ref value from collection where user=?";
$params = array("i",$userref);
if (count($hidden_collections) > 0) {
$sql .= " and ref not in(" . ps_param_insert(count($hidden_collections)) . ")";
$params = array_merge($params, ps_param_fill($hidden_collections, "i"));
}
$user_mycollection = ps_value($sql . " order by ref limit 1", $params, "");
}
$extra_tag_attributes = sprintf(
'
data-mycol="%s"
',
urlencode($user_mycollection)
);
if ($pagename != "load_actions") {
$options[$o]['value'] = 'hide_collection';
$options[$o]['label'] = $lang['hide_collection'];
$options[$o]['extra_tag_attributes'] = $extra_tag_attributes;
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 270;
$o++;
}
}
}
// Relate / Unrelate all resources
if ($enable_related_resources && $allow_multi_edit && 0 < $count_result) {
$options[$o]['value'] = 'relate_all';
$options[$o]['label'] = $lang['relateallresources'];
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 280;
$o++;
$options[$o]['value'] = 'unrelate_all';
$options[$o]['label'] = $lang['unrelateallresources'];
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 290;
$o++;
}
// Add extra collection actions and manipulate existing actions through plugins
$modified_options = hook('render_actions_add_collection_option', '', array($top_actions,$options,$collection_data, $urlparams));
if (is_array($modified_options) && !empty($modified_options)) {
$options = $modified_options;
}
return $options;
}
/**
* Make a filename unique by appending a dupe-string.
*
* @param array $base_values
* @param string $filename
* @param string $dupe_string
* @param string $extension
* @param int $dupe_increment
*
* @return string Unique filename
*/
function makeFilenameUnique($base_values, $filename, $dupe_string, $extension, $dupe_increment = null)
{
// Create filename to check if exist in $base_values
$check_filename = $filename . ($dupe_increment ? $dupe_string . $dupe_increment : '') . '.' . $extension;
if (!in_array($check_filename, $base_values)) {
// Confirmed filename does not exist yet
return $check_filename;
}
// Recursive call this function with incremented value
// Doing $dupe_increment = null, ++$dupe_increment results in $dupe_increment = 1
return makeFilenameUnique($base_values, $filename, $dupe_string, $extension, ++$dupe_increment);
}
/**
* Render the new featured collection form
*
* @param int $parent Featured collection parent. Use zero for root featured collection category
*
* @return void
*/
function new_featured_collection_form(int $parent)
{
global $baseurl_short, $lang;
if (!checkperm('h') || !can_create_collections()) {
http_response_code(401);
exit($lang['error-permissiondenied']);
}
$form_action = "{$baseurl_short}pages/collection_manage.php";
?>
<div class="BasicsBox">
<h1><?php echo escape($lang["createnewcollection"]); ?></h1>
<form name="new_collection_form" id="new_collection_form" class="modalform"
method="POST" action="<?php echo $form_action; ?>" onsubmit="return CentralSpacePost(this, true);">
<?php generateFormToken("new_collection_form"); ?>
<input type="hidden" name="call_to_action_tile" value="true"></input>
<input type="hidden" name="parent" value="<?php echo $parent; ?>"></input>
<div class="Question">
<label for="newcollection" ><?php echo escape($lang["collectionname"]); ?></label>
<input type="text" name="name" id="newcollection" maxlength="100" required="true"></input>
<div class="clearleft"></div>
</div>
<div class="QuestionSubmit" >
<input type="submit" name="create" value="<?php echo escape($lang["create"]); ?>"></input>
<div class="clearleft"></div>
</div>
</form>
</div>
<?php
}
/**
* Get a themes array
*
* @param int $levels Number of levels to parse from request
*
* @return array Array containing names of themes matching the syntax used in the collection table i.e. theme, theme2, theme3
*/
function GetThemesFromRequest($levels)
{
$themes = array();
for ($n = 0; $n <= $levels; $n++) {
$themeindex = ($n == 0 ? "" : $n);
$themename = getval("theme$themeindex", "");
if ($themename != "") {
$themes[] = $themename;
}
// Legacy inconsistency when naming themes params. Sometimes the root theme was also named theme1. We check if theme
// is found, but if not, we just go to theme1 rather than break.
elseif (!($themeindex == 0 && $themename == "")) {
break;
}
}
return $themes;
}
/**
* @param array $dl_data Array of collection download data from process_collection_download()
* (passed by reference so can be added to)
* @param string $filename Filename (passed by reference)
* @param int $ref Resource ID
* @param string $pextension File extension
* @param string $p Path to download file = passed by reference as may be replaced with a copy file
* @param bool $copy Copy the file from filestore rather than renaming?
*
*/
function collection_download_use_original_filenames_when_downloading (
array &$dl_data,
string &$filename,
int $ref,
string $pextension,
string &$p,
bool $copy
): void {
if (trim($filename) === '') {
return;
}
# Only perform the copy if an original filename is set.
# now you've got original filename, but it may have an extension in a different letter case.
# The system needs to replace the extension to change it to jpg if necessary, but if the original file
# is being downloaded, and it originally used a different case, then it should not come from the file_extension,
# but rather from the original filename itself.
# do an extra check to see if the original filename might have uppercase extension that can be preserved.
# also, set extension to "" if the original filename didn't have an extension (exiftool identification of filetypes)
$pathparts = pathinfo($filename);
if (
isset($pathparts['extension'])
&& strtolower($pathparts['extension']) == $pextension
) {
$pextension = $pathparts['extension'];
}
$fs = explode("/", $filename);
$filename = $fs[count($fs) - 1];
set_unique_filename($filename, $dl_data['filenames']);
# Copy to tmp (if exiftool failed) or rename this file
# this is for extra efficiency to reduce copying and disk usage
if (!($dl_data['collection_download_tar'] || $GLOBALS['use_zip_extension'])) {
// The copy or rename to the filename is not necessary using the zip extension since the archived filename can be specified.
$newpath = get_temp_dir(false, $dl_data['id']) . '/' . $filename;
if (!$copy && $GLOBALS['exiftool_write_option']) {
rename($p, $newpath);
} else {
copy($p, $newpath);
}
# Add the temporary file to the post-archiving deletion list.
$dl_data['deletion_array'][] = $newpath;
# Set p so now we are working with this new file
$p = $newpath;
}
}
/**
* Provide resource data/collection_resource data to add to text file during a collection download.
*
* @param array $dl_data Array of collection download data passed from process_collection_download()
* @param integer $ref Resource ID
* @param string $filename
* @param bool $subbed_original Has original file been substituted for unavailable size?
*
* @return string Text to append to file
*/
function collection_download_process_text_file(array $dl_data, int $ref, string $filename, bool $subbed_original): string
{
$sizetext = $dl_data['sizetext'];
if ($subbed_original) {
$sizetext .= ' (' . $GLOBALS['lang']['substituted_original'] . ')';
}
$text = "";
if ($dl_data['includetext'] ?? false) {
if ((string) $dl_data['k'] === '') {
$fields = get_resource_field_data($ref);
} else {
// External shares should take into account fields that are not meant to show in that case
$fields = get_resource_field_data($ref, false, true, null, true);
}
$commentdata = get_collection_resource_comment($ref, $dl_data['collection']);
$fields_count = count($fields);
if ($fields_count > 0) {
$hook_replace_text = hook('replacecollectiontext', '', array($text, $sizetext, $filename, $ref, $fields, $fields_count, $commentdata));
if (!$hook_replace_text) {
$text .= ($sizetext == '' ? '' : $sizetext) . ' ' . $filename . "\r\n-----------------------------------------------------------------\r\n";
$text .= $GLOBALS["lang"]['resourceid'] . ': ' . $ref . "\r\n";
for ($i = 0; $i < $fields_count; $i++) {
$value = $fields[$i]["value"];
$title = str_replace('Keywords - ', '', $fields[$i]["title"]);
if ((trim((string) $value) != "") && (trim((string) $value) != ',')) {
$text .= wordwrap('* ' . $title . ': ' . i18n_get_translated($value) . "\r\n", 65);
}
}
if (trim((string)$commentdata['comment']) != '') {
$text .= wordwrap($GLOBALS["lang"]['comment'] . ': ' . $commentdata['comment'] . "\r\n", 65);
}
if (trim((string)$commentdata['rating']) != '') {
$text .= wordwrap($GLOBALS["lang"]['rating'] . ': ' . $commentdata['rating'] . "\r\n", 65);
}
$text .= "-----------------------------------------------------------------\r\n\r\n";
} else {
$text = $hook_replace_text;
}
}
}
return $text;
}
/**
* Update the resource log to show the download during a collection download.
*
* @param array $dl_data Array of collection download data passed from process_collection_download()
* @param string $tmpfile Temp download file path
* @param integer $ref The resource ID
* @param string $email Email address of downloader
*
* @return void
*/
function collection_download_log_resource_ready(array $dl_data, $tmpfile, $ref, string $email = "")
{
// Build an array of paths so we can clean up any exiftool-modified files.
if ($tmpfile !== false && file_exists($tmpfile)) {
$dl_data['deletion_array'][] = $tmpfile;
}
daily_stat("Resource download", $ref);
$email_add_to_log = ($email != "") ? ' Downloaded by ' . $email : "";
resource_log($ref, LOG_CODE_DOWNLOADED, 0, (string) $dl_data['usagecomment'] . $email_add_to_log, "", "", (int) $dl_data['usage']);
// Udate hit count if tracking downloads only
if ($GLOBALS["resource_hit_count_on_downloads"]) {
# greatest() is used so the value is taken from the hit_count column in the event that new_hit_count is zero to support installations that did not previously have a new_hit_count column (i.e. upgrade compatability).
ps_query("UPDATE resource SET new_hit_count=GREATEST(hit_count,new_hit_count)+1 WHERE ref=?", ["i", $ref]);
}
}
/**
* Add PDFs for "data only" types to a zip file during creation.
*
* @param array $dl_data Array of collection download data
* @param object $zip Collection zip file
* @return void
*/
function collection_download_process_data_only_types(array $dl_data, &$zip)
{
$result = $dl_data['collection_resources'] ?? [];
for ($n = 0; $n < count($result); $n++) {
// Data-only type of resources should be generated and added in the archive
if (in_array($result[$n]['resource_type'], $GLOBALS["data_only_resource_types"])) {
$template_path = get_pdf_template_path($result[$n]['resource_type']);
if ($template_path === false) {
continue;
}
$pdf_filename = 'RS_' . $result[$n]['ref'] . '_data_only.pdf';
$pdf_file_path = get_temp_dir(false, $dl_data['id']) . '/' . $pdf_filename;
// Go through fields and decide which ones we add to the template
$placeholders = array(
'resource_type_name' => get_resource_type_name($result[$n]['resource_type'])
);
$metadata = get_resource_field_data($result[$n]['ref'], false, true, null, '' != $GLOBALS["k"]);
foreach ($metadata as $metadata_field) {
$metadata_field_value = trim(tidylist(i18n_get_translated($metadata_field['value'])));
// Skip if empty
if ('' == $metadata_field_value) {
continue;
}
$placeholders['metadatafield-' . $metadata_field['ref'] . ':title'] = $metadata_field['title'];
$placeholders['metadatafield-' . $metadata_field['ref'] . ':value'] = $metadata_field_value;
}
generate_pdf($template_path, $pdf_file_path, $placeholders, true);
// Go and add file to archive
if ($dl_data['collection_download_tar']) {
// Add a link to the pdf
$usertempdir = get_temp_dir(false, "rs_" . $GLOBALS["userref"] . "_" . $dl_data['id']);
symlink($pdf_file_path, $usertempdir . DIRECTORY_SEPARATOR . $pdf_filename);
} elseif ($GLOBALS["use_zip_extension"]) {
debug("Adding $pdf_file_path to " . $zip->filename);
$zip->addFile($pdf_file_path, $pdf_filename);
} else {
$dl_data['includefiles'][] = $pdf_file_path . "\r\n";
}
$dl_data['deletion_array'][] = $pdf_file_path;
daily_stat('Resource download', $result[$n]['ref']);
resource_log($result[$n]['ref'], 'd', 0, $GLOBALS['lang']['pdffile'] . " - " . $dl_data['usagecomment'], '', '',(int) $dl_data['usage']);
if ($GLOBALS["resource_hit_count_on_downloads"]) {
$resource_ref_escaped = $result[$n]['ref'];
ps_query("UPDATE resource SET new_hit_count = GREATEST(hit_count, new_hit_count) + 1 WHERE ref = ?", array("i",$resource_ref_escaped));
}
}
}
}
/**
* @param array $dl_data Array of collection download data from process_collection_download()
* (passed by reference so can be added to)
* @param string $filename Resource filename
* @param mixed $zip Collection zip file, false if using TAR
*
*/
function collection_download_process_summary_notes(array &$dl_data, string $filename, mixed &$zip)
{
global $p;
$size = $dl_data['size'];
$text = $dl_data['text'];
$subbed_original_resources = $dl_data['subbed_original_resources'];
$used_resources = $dl_data['used_resources'];
$available_sizes = $dl_data['available_sizes'];
if (
!hook('zippedcollectiontextfile', '', array($text))
&& $dl_data['includetext']
) {
$qty_sizes = isset($available_sizes[$size]) ? count($available_sizes[$size]) : 0;
$qty_total = count($dl_data['collection_resources']);
$text .= $GLOBALS["lang"]["status-note"] . ": " . $qty_sizes . " " . $GLOBALS["lang"]["of"] . " " . $qty_total . " ";
switch ($qty_total) {
case 0:
$text .= $GLOBALS["lang"]["resource-0"] . " ";
break;
case 1:
$text .= $GLOBALS["lang"]["resource-1"] . " ";
break;
default:
$text .= $GLOBALS["lang"]["resource-2"] . " ";
break;
}
switch ($qty_sizes) {
case 0:
$text .= $GLOBALS["lang"]["were_available-0"] . " ";
break;
case 1:
$text .= $GLOBALS["lang"]["were_available-1"] . " ";
break;
default:
$text .= $GLOBALS["lang"]["were_available-2"] . " ";
break;
}
$text .= $GLOBALS["lang"]["forthispackage"] . ".\r\n\r\n";
foreach ($dl_data['collection_resources'] as $resource) {
if (in_array($resource['ref'], $subbed_original_resources)) {
$text .= $GLOBALS["lang"]["didnotinclude"] . ": " . $resource['ref'];
$text .= " (" . $GLOBALS["lang"]["substituted_original"] . ")";
$text .= "\r\n";
} elseif (!in_array($resource['ref'], $used_resources)) {
$text .= $GLOBALS["lang"]["didnotinclude"] . ": " . $resource['ref'];
$text .= "\r\n";
}
}
$textfile = get_temp_dir(false, $dl_data['id']) . "/" . (int) $dl_data['collection'] . "-" . safe_file_name(i18n_get_collection_name($dl_data['collectiondata'])) . $dl_data['sizetext'] . ".txt";
$fh = fopen($textfile, 'w') or die("can't open file");
fwrite($fh, $text);
fclose($fh);
if ($dl_data['collection_download_tar']) {
$usertempdir = get_temp_dir(false, "rs_" . $GLOBALS["userref"] . "_" . $dl_data['id']);
debug("collection_download adding symlink: " . $p . " - " . $usertempdir . DIRECTORY_SEPARATOR . $filename);
$GLOBALS["use_error_exception"] = true;
try {
symlink(
$textfile,
$usertempdir
. DIRECTORY_SEPARATOR . $dl_data['collection']
. "-" . safe_file_name(i18n_get_collection_name($dl_data['collectiondata']))
. $dl_data['sizetext'] . '.txt'
);
} catch (Throwable $e) {
debug("collection_download_process_archive_command: Unable to create symlink {$e->getMessage()}");
return false;
}
unset($GLOBALS["use_error_exception"]);
} elseif ($GLOBALS['use_zip_extension']) {
debug("Adding $textfile to " . $zip->filename);
$zip->addFile($textfile, $dl_data['collection'] . "-" . safe_file_name(i18n_get_collection_name($dl_data['collectiondata'])) . $dl_data['sizetext'] . ".txt");
} else {
$dl_data['includefiles'][] = $textfile;
}
$dl_data['deletion_array'][] = $textfile;
}
}
/**
* Add a CSV containing resource metadata to a downloaded zip file during creation of the zip.
*
* @param array $dl_data Array of collection download data from process_collection_download()
* (passed by reference so can be added to)
* @param object $zip Collection zip file
*
* @return void
*/
function collection_download_process_csv_metadata_file(array &$dl_data, &$zip)
{
// Include the CSV file with the metadata of the resources found in this collection
$result = $dl_data['collection_resources'];
$csv_file = get_temp_dir(false, $dl_data['id']) . '/Col-' . $dl_data['collection'] . '-metadata-export.csv';
if (isset($result[0]["ref"])) {
$result = array_column($result, "ref");
}
generateResourcesMetadataCSV($result, false, false, $csv_file);
// Add link to file for use by tar to prevent full paths being included.
if ($dl_data['collection_download_tar']) {
$usertempdir = get_temp_dir(false, "rs_" . $GLOBALS["userref"] . "_" . $dl_data['id']);
debug("collection_download adding symlink: " . $csv_file . " - " . $usertempdir . DIRECTORY_SEPARATOR . 'Col-' . $dl_data['collection'] . '-metadata-export.csv');
$GLOBALS["use_error_exception"] = true;
try {
symlink($csv_file, $usertempdir . DIRECTORY_SEPARATOR . 'Col-' . $dl_data['collection'] . '-metadata-export.csv');
} catch (Throwable $e) {
debug("collection_download_process_csv_metadata_file(): Unable to create symlink for CSV {$e->getMessage()}");
return;
}
unset($GLOBALS["use_error_exception"]);
} elseif ($GLOBALS['use_zip_extension']) {
debug("Adding $csv_file to " . $zip->filename);
$zip->addFile($csv_file, 'Col-' . $dl_data['collection'] . '-metadata-export.csv');
} else {
debug("collection_download_process_csv_metadata_file: ". $csv_file);
$dl_data['includefiles'][] = $csv_file;
}
$dl_data['deletion_array'][] = $csv_file;
}
/**
* Modifies the filename for downloading as part of the specified collection
*
* @param string &$filename Filename (passed by reference)
* @param integer $collection Collection ID
* @param string $size Size code e.g scr,pre
* @param string $suffix String suffix to add (before file extension)
* @param array $collectiondata Collection data obtained by get_collection()
*
* @return void
*/
function collection_download_process_collection_download_name(&$filename, $collection, $size, $suffix, array $collectiondata)
{
global $use_collection_name_in_zip_name;
$filename = hook('changecollectiondownloadname', null, array($collection, $size, $suffix));
if (empty($filename)) {
if ($use_collection_name_in_zip_name) {
# Use collection name (if configured)
$filename = $GLOBALS["lang"]["collectionidprefix"] . $collection . "-"
. safe_file_name(i18n_get_collection_name($collectiondata)) . "-" . $size
. $suffix;
} else {
# Do not include the collection name in the filename (default)
$filename = $GLOBALS["lang"]["collectionidprefix"] . $collection . "-" . $size . $suffix;
}
}
}
/**
* Executes the archiver command when downloading a collection.
*
* @param array $dl_data Array of collection download data from process_collection_download()
* (passed by reference so can be added to)
* @param object $zip Collection zip file
* @param string $filename Download filename
* @param integer $settings_id The index of the selected $collection_download_settings element as defined in config.php
*
* @return bool Will return true if there is no further work to be done as will be the case for a tar file.
* False when further processing needed e.g. when producing a zip file.
*/
function collection_download_process_archive_command(array &$dl_data, &$zip, $filename, &$zipfile)
{
$archiver_settings = $GLOBALS['collection_download_settings'][$dl_data['settings_id']] ?? "";
# Execute the archiver command.
# If $collection_download is true the $collection_download_settings are used if defined
if ($GLOBALS['use_zip_extension'] && !$dl_data['collection_download_tar']) {
set_processing_message($GLOBALS["lang"]["zipping"]);
$GLOBALS["use_error_exception"] = true;
try {
debug("closing " . $zip->filename);
$zip->close();
} catch (Throwable $e) {
debug("collection_download_process_archive_command: Unable to close zip file. Reason {$e->getMessage()}");
}
unset($GLOBALS["use_error_exception"]);
set_processing_message($GLOBALS["lang"]["zipcomplete"]);
} elseif ($dl_data['collection_download_tar']) {
$usertempdir = get_temp_dir(false, "rs_" . $GLOBALS["userref"] . "_" . $dl_data['id']);
header("Content-type: application/tar");
header("Content-disposition: attachment; filename=" . $filename);
debug("collection_download tar command: tar -cv -C " . $usertempdir . " . ");
$cmdtempdir = escapeshellarg($usertempdir);
debug("Calling tar command for filename " . $filename);
passthru("find " . $cmdtempdir . ' -printf "%P\n" | tar -cv --no-recursion --dereference -C ' . $cmdtempdir . " -T -");
return true;
} elseif ($dl_data['archiver']) {
set_processing_message($GLOBALS["lang"]["zipping"]);
// Create a list of files to include
$listfile = get_temp_dir(false, $dl_data['id']) . "/zipcmd" . $dl_data['collection'] . "-" . $dl_data['size'] . ".txt";
// Remove Windows line endings - fixes an issue with using tar command - somehow the file has got Windows line breaks
$filepaths = implode(
($GLOBALS["config_windows"] ? "\n" : "\r\n"),
$dl_data['includefiles']
);
file_put_contents($listfile, $filepaths);
$dl_data['deletion_array'][] = $listfile;
// Set up command line
$command = get_utility_path("archiver") . " [ARGUMENTS] %ZIPFILE %LISTFILEARG%LISTFILE";
$cmdparams = [];
// Likely be more than one argument e.g. 'a -tzip' so will need to be quoted individually
$arguments = explode(" ", $archiver_settings["arguments"]);
$arr_arguments = [];
for($n = 0; $n < count($arguments); $n++) {
$argumentstring = "%ARGUMENT{$n}";
$arr_arguments[] = $argumentstring;
$cmdparams[$argumentstring] = new CommandPlaceholderArg(
$arguments[$n],
"permitted_archiver_arguments"
);
}
$command = str_replace("[ARGUMENTS]", implode(" ", $arr_arguments), $command);
$cmdparams["%ZIPFILE"] = new CommandPlaceholderArg($zipfile, 'is_valid_rs_path');
$cmdparams["%LISTFILEARG"] = new CommandPlaceholderArg(
$GLOBALS["archiver_listfile_argument"],
"permitted_archiver_arguments"
);
$cmdparams["%LISTFILE"] = new CommandPlaceholderArg($listfile, 'is_valid_rs_path');
run_command($command, false, $cmdparams);
set_processing_message($GLOBALS["lang"]["zipcomplete"]);
}
return false;
}
/**
* Remove temporary files created during download by exiftool for adding metadata.
*
* @param array $deletion_array An array of file paths
* @return void
*/
function collection_download_clean_temp_files(array $deletion_array)
{
// Remove temporary files.
foreach ($deletion_array as $tmpfile) {
delete_exif_tmpfile($tmpfile);
}
}
/**
* Delete any resources from collection moved out of users archive status permissions by other users
*
* @param integer $collection ID of collection
*
* @return void
*/
function collection_cleanup_inaccessible_resources($collection)
{
global $userref;
$editable_states = array_column(get_editable_states($userref), 'id');
$count_editable_states = count($editable_states);
if ($count_editable_states === 0) {
return;
}
ps_query("DELETE a
FROM collection_resource AS a
INNER JOIN resource AS b
ON a.resource = b.ref
WHERE a.collection = ?
AND b.archive NOT IN (" . ps_param_insert($count_editable_states) . ")", array_merge(['i', $collection], ps_param_fill($editable_states, 'i')));
}
/**
* Relate all resources in a collection
*
* @param integer $collection ID of collection
*
* @return boolean
*/
function relate_all_collection($collection, $checkperms = true)
{
if (!is_int_loose($collection) || ($checkperms && !allow_multi_edit($collection))) {
return false;
}
$rlist = get_collection_resources($collection);
for ($n = 0; $n < count($rlist); $n++) {
for ($m = 0; $m < count($rlist); $m++) {
if (
$rlist[$n] != $rlist[$m] # Don't relate a resource to itself
&& count(ps_query("SELECT 1 FROM resource_related WHERE resource= ? and related= ? LIMIT 1", ['i', $rlist[$n], 'i', $rlist[$m]])) != 1
) {
ps_query("insert into resource_related (resource,related) values (?, ?)", ['i', $rlist[$n], 'i', $rlist[$m]]);
}
}
}
return true;
}
/**
* Un-relate all resources in a collection
*
* @param integer $collection ID of collection
*
* @return boolean
*/
function unrelate_all_collection($collection, $checkperms = true)
{
if (!is_int_loose($collection) || ($checkperms && !allow_multi_edit($collection))) {
return false;
}
ps_query('DELETE FROM resource_related WHERE `resource` IN (SELECT `resource` FROM collection_resource WHERE collection = ?) AND `related` IN (select `resource` FROM collection_resource WHERE collection = ?)', array('i', $collection, 'i', $collection));
return true;
}
/**
* Update collection type for one collection or batch
*
* @param integer|array $cid Collection ID -or- list of collection IDs
* @param integer $type Collection type. @see include/definitions.php for available options
*
* @return boolean
*/
function update_collection_type($cid, $type, $log = true)
{
debug_function_call("update_collection_type", func_get_args());
if (!is_array($cid)) {
$cid = array($cid);
}
$cid = array_filter($cid, "is_numeric");
if (empty($cid)) {
return false;
}
if (!in_array($type, definitions_get_by_prefix("COLLECTION_TYPE"))) {
return false;
}
if ($log) {
foreach ($cid as $ref) {
collection_log($ref, LOG_CODE_EDITED, "", "Update collection type to '{$type}'");
}
}
ps_query("UPDATE collection SET `type` = ? WHERE ref IN (" . ps_param_insert(count($cid)) . ")", array_merge(['i', $type], ps_param_fill($cid, 'i')));
return true;
}
/**
* Update collection parent for this collection
*
* @param integer @cid The collection ID
* @param integer @parent The featured collection ID that is the parent of this collection
*
* @return boolean
*/
function update_collection_parent(int $cid, int $parent)
{
if ($cid <= 0 || $parent <= 0) {
return false;
}
collection_log($cid, LOG_CODE_EDITED, "", "Update collection parent to '{$parent}'");
ps_query("UPDATE collection SET `parent` = ? WHERE ref = ?", ['i', $parent, 'i', $cid]);
return true;
}
/**
* Get a users' collection of type SELECTION.
*
* There can only be one collection of this type per user. If more, the first one found will be used instead.
*
* @param integer $user User ID
*
* @return null|integer Returns NULL if none found or the collection ID
*/
function get_user_selection_collection($user)
{
if (!is_numeric($user)) {
return null;
}
global $username,$anonymous_login, $rs_session, $anonymous_user_session_collection;
if (($username == $anonymous_login && $anonymous_user_session_collection) || upload_share_active()) {
// We need to set a collection session_id for the anonymous user. Get session ID to create collection with this set
$rs_session = get_rs_session_id(true);
$cache = '';
} else {
$rs_session = "";
$cache = 'user_selection_collection' . $user;
}
$params = [
'i', $user,
'i', COLLECTION_TYPE_SELECTION,
];
$session_id_sql = '';
if (isset($rs_session) && $rs_session !== '') {
$session_id_sql = 'AND session_id = ?';
$params[] = 'i';
$params[] = $rs_session;
}
return ps_value("SELECT ref AS `value` FROM collection WHERE `user` = ? AND `type` = ? {$session_id_sql} ORDER BY ref ASC", $params, null, $cache);
}
/**
* Delete all collections that are not in use e.g. session collections for the anonymous user. Will not affect collections that are public.
*
* @param integer $userref - ID of user to delete collections for
* @param integer $days - minimum age of collections to delete in days
*
* @return integer - number of collections deleted
*/
function delete_old_collections($userref = 0, $days = 30)
{
if ($userref == 0 || !is_numeric($userref)) {
return 0;
}
$deletioncount = 0;
$old_collections = ps_array("SELECT ref value FROM collection WHERE user = ? AND created < DATE_SUB(NOW(), INTERVAL ? DAY) AND `type` = " . COLLECTION_TYPE_STANDARD, array("i",$userref,"i",$days), 0);
foreach ($old_collections as $old_collection) {
delete_collection($old_collection);
$deletioncount++;
}
return $deletioncount;
}
/**
* Get all featured collections
*
* @return array
*/
function get_all_featured_collections()
{
return ps_query(
"SELECT DISTINCT c.ref,
c.`name`,
c.`type`,
c.parent,
c.thumbnail_selection_method,
c.bg_img_resource_ref,
c.created,
count(DISTINCT cr.resource) > 0 AS has_resources,
count(DISTINCT cc.ref) > 0 AS has_children
FROM collection AS c
LEFT JOIN collection_resource AS cr ON c.ref = cr.collection
LEFT JOIN collection AS cc ON c.ref = cc.parent
WHERE c.`type` = ?
GROUP BY c.ref",
array("i",COLLECTION_TYPE_FEATURED),
"featured_collections"
);
}
/**
* Get all featured collections by parent node
*
* @param integer $parent The ref of the parent collection. When a featured collection contains another collection, it is
* then considered a featured collection category and won't have any resources associated with it.
* @param array $ctx Contextual data (e.g disable access control). This param MUST NOT get exposed over the API
*
* @return array List of featured collections (with data)
*/
function get_featured_collections(int $parent, array $ctx)
{
if ($parent < 0) {
return array();
}
$access_control = (isset($ctx["access_control"]) && is_bool($ctx["access_control"]) ? $ctx["access_control"] : true);
$params = array("i",COLLECTION_TYPE_FEATURED);
if ($parent == 0) {
// When searching for parent '0' we're looking for a null value on the parent column denoting the top level of the featured collection tree.
$parentquery = "IS NULL";
} else {
// Numeric parent value.
$parentquery = "=?";
$params[] = "i";
$params[] = $parent;
}
$allfcs = ps_query("SELECT DISTINCT c.ref,
c.`name`,
c.`type`,
c.parent,
c.thumbnail_selection_method,
c.bg_img_resource_ref,
c.order_by,
c.created,
c.savedsearch,
count(DISTINCT cr.resource) > 0 AS has_resources,
count(DISTINCT cc.ref) > 0 AS has_children
FROM collection AS c
LEFT JOIN collection_resource AS cr ON c.ref = cr.collection
LEFT JOIN collection AS cc ON c.ref = cc.parent
WHERE c.`type` = ?
AND c.parent $parentquery
GROUP BY c.ref
ORDER BY c.order_by", $params);
if (!$access_control) {
return $allfcs;
}
$validcollections = array();
foreach ($allfcs as $fc) {
if (featured_collection_check_access_control($fc["ref"])) {
$validcollections[] = $fc;
}
}
return $validcollections;
}
/**
* Build appropriate SQL (for WHERE clause) to filter out featured collections for the user. The function will use either an
* IN or NOT IN depending which list is smaller to increase performance of the search
*
* @param string $prefix SQL WHERE clause element. Mostly should be either WHERE, AND -or- OR depending on the SQL statement
* this is part of.
* @param string $column SQL column on which to apply the filter for
* @param bool $returnstring (temporary) Will return the legacy string version until do_search() and others are migrated to use prepared statements. This can be removed once all functions use prepared statements
*
* @return array|string Returns "" if user should see all featured collections or a SQL filter (e.g AND ref IN("32", "34") ) with the placholders as the first element and the collection IDs as params for the second - for use in e.g. ps_query(), ps_value()
*/
function featured_collections_permissions_filter_sql(string $prefix, string $column, bool $returnstring = false)
{
global $CACHE_FC_PERMS_FILTER_SQL;
$CACHE_FC_PERMS_FILTER_SQL = (!is_null($CACHE_FC_PERMS_FILTER_SQL) && is_array($CACHE_FC_PERMS_FILTER_SQL) ? $CACHE_FC_PERMS_FILTER_SQL : array());
$cache_id = md5("{$prefix}-{$column}");
if (
(isset($CACHE_FC_PERMS_FILTER_SQL[$cache_id])
&& is_string($CACHE_FC_PERMS_FILTER_SQL[$cache_id])
&& $returnstring)
|| (isset($CACHE_FC_PERMS_FILTER_SQL[$cache_id])
&& is_array($CACHE_FC_PERMS_FILTER_SQL[$cache_id]))
) {
return $CACHE_FC_PERMS_FILTER_SQL[$cache_id];
}
// $prefix & $column are used to generate the right SQL (e.g AND ref IN(list of IDs)). If developer/code, passes empty strings,
// that's not this functions' responsibility. We could error here but the code will error anyway because of the bad SQL so
// we might as well fix the problem at its root (ie. where we call this function with bad input arguments).
$prefix = " " . trim($prefix);
$column = trim($column);
$computed_fcs = compute_featured_collections_access_control();
if ($computed_fcs === true) {
$return = ""; # No access control needed! User should see all featured collections
} elseif (is_array($computed_fcs)) {
if ($returnstring) {
$fcs_list = "'" . join("', '", $computed_fcs) . "'";
$return = "{$prefix} {$column} IN ({$fcs_list})";
} else {
$return = array("{$prefix} {$column} IN (" . ps_param_insert(count($computed_fcs)) . ")",ps_param_fill($computed_fcs, "i"));
}
} else {
// User is not allowed to see any of the available FCs if($returnstring)
if ($returnstring) {
$return = "{$prefix} 1 = 0";
} else {
$return = [$prefix . " 1 = 0",[]];
}
}
$CACHE_FC_PERMS_FILTER_SQL[$cache_id] = $return;
return $return;
}
/**
* Access control function used to determine if a featured collection should be accessed by the user
*
* @param integer $c_ref Collection ref to be tested
*
* @return boolean Returns TRUE if user should have access to the featured collection (no parent category prevents this), FALSE otherwise
*/
function featured_collection_check_access_control(int $c_ref)
{
if (checkperm("-j" . $c_ref)) {
return false;
} elseif (checkperm("j*") || checkperm("j" . $c_ref)) {
return true;
} else {
// 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', '>=')) {
$allparents = ps_query(
"
WITH RECURSIVE cte(ref,parent, level) AS
(
SELECT ref,
parent,
1 AS level
FROM collection
WHERE ref= ?
UNION ALL
SELECT c.ref,
c.parent,
level+1 AS LEVEL
FROM collection c
INNER JOIN cte
ON c.ref = cte.parent
)
SELECT ref,
parent,
level
FROM cte
ORDER BY level DESC;",
['i', $c_ref],
"featured_collections",
-1,
true,
0
);
} else {
$allparents = ps_query(
"
SELECT C2.ref, C2.parent
FROM (SELECT @r AS p_ref,
(SELECT @r := parent FROM collection WHERE ref = p_ref) AS parent,
@l := @l + 1 AS lvl
FROM (SELECT @r := ?, @l := 0) vars,
collection c
WHERE @r <> 0) C1
JOIN collection C2
ON C1.p_ref = C2.ref
ORDER BY C1.lvl DESC",
['i', $c_ref],
"featured_collections",
-1,
true,
0
);
}
foreach ($allparents as $parent) {
if (checkperm("-j" . $parent["ref"])) {
// Denied access to parent
return false;
} elseif (checkperm("j" . $parent["ref"])) {
return true;
}
}
return false; // No explicit permission given and user doesn't have f*
}
}
/**
* Helper comparison function for ordering featured collections. It sorts using the order_by property, then based if the
* collection is a category (using the "has_resource" property), then by name (this takes into account the legacy
* use of '*' as a prefix to move to the start).
*
* @param array $a First featured collection data structure to compare
* @param array $b Second featured collection data structure to compare
*
* @return Return an integer less than, equal to, or greater than zero if the first argument is considered to be
* respectively less than, equal to, or greater than the second.
*/
function order_featured_collections(array $a, array $b)
{
global $descthemesorder;
// Sort using the order_by property
if ($a['order_by'] != $b['order_by'] && !($a['order_by'] == 0 || $b['order_by'] == 0)) {
if ($descthemesorder) {
return $a['order_by'] > $b['order_by'] ? -1 : 1;
}
return $a['order_by'] < $b['order_by'] ? -1 : 1;
}
// Order by showing categories first
if ($a['has_resources'] != $b['has_resources']) {
return $a['has_resources'] < $b['has_resources'] ? -1 : 1;
}
// Order by collection name
if ($descthemesorder) {
return strnatcasecmp($b['name'], $a['name']);
}
return strnatcasecmp($a['name'], $b['name']);
}
/**
* Get featured collection categories
*
* @param integer $parent The ref of the parent collection.
* @param array $ctx Extra context for get_featured_collections(). Mostly used for overriding access control (e.g
* on the admin_group_permissions.php where we want to see all available featured collection categories).
*
* @return array
*/
function get_featured_collection_categories(int $parent, array $ctx)
{
return array_values(array_filter(get_featured_collections($parent, $ctx), "is_featured_collection_category"));
}
/**
* Check if a collection is a featured collection category
*
* @param array $fc A featured collection data structure as returned by {@see get_featured_collections()}
*
* @return boolean
*/
function is_featured_collection_category(array $fc)
{
if (!isset($fc["type"]) || !isset($fc["has_resources"])) {
return false;
}
return $fc["type"] == COLLECTION_TYPE_FEATURED && $fc["has_resources"] == 0 && is_null($fc["savedsearch"] ?? null);
}
/**
* Check if a collection is a featured collection category by checking if the collection has been used as a parent. This
* function will make a DB query to find this out, it does not use existing structures.
*
* Normally a featured collection is a category if it has no resources. In some circumstances, when it's impossible to
* determine whether it should be or not, relying on children is another approach.
*
* @param integer $c_ref Collection ID
*
* @return boolean
*/
function is_featured_collection_category_by_children(int $c_ref)
{
$found_ref = ps_value(
"SELECT DISTINCT c.ref AS `value`
FROM collection AS c
LEFT JOIN collection AS cc ON c.ref = cc.parent
WHERE c.`type` = ?
AND c.ref = ?
GROUP BY c.ref
HAVING count(DISTINCT cc.ref) > 0",
array("s",COLLECTION_TYPE_FEATURED,"i",$c_ref),
0
);
return $found_ref > 0;
}
/**
* Validate a collection parent value
*
* @param int|array $c Collection ref -or- collection data as returned by {@see get_collection()}
*
* @return null|integer
*/
function validate_collection_parent($c)
{
if (!is_array($c) && !is_int($c)) {
return null;
}
$collection = $c;
if (!is_array($c) && is_int($c)) {
$collection = get_collection($c);
if ($collection === false) {
return null;
}
}
return is_null($collection["parent"]) ? null : (int) $collection["parent"];
}
/**
* Get to the root of the branch starting from the leaf featured collection
*
* @param integer $ref Collection ref which is considered a leaf of the tree
* @param array $fcs List of all featured collections
*
* @return array Branch path structure starting from root to the leaf
*/
function get_featured_collection_category_branch_by_leaf(int $ref, array $fcs)
{
if (empty($fcs)) {
$fcs = get_all_featured_collections();
}
return compute_node_branch_path($fcs, $ref);
}
/**
* Process POSTed featured collections categories data for a collection
*
* @param integer $depth The depth from which to start from. Usually zero.
* @param array $branch_path A full branch path of the collection. {@see get_featured_collection_category_branch_by_leaf()}
*
* @return array Returns changes done regarding the collection featured collection category structure. This information
* then can be provided to {@see save_collection()} as: $coldata["featured_collections_changes"]
*/
function process_posted_featured_collection_categories(int $depth, array $branch_path)
{
global $enable_themes, $FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS;
if (!($enable_themes && checkperm("h"))) {
return array();
}
if ($depth < 0) {
return array();
}
debug("process_posted_featured_collection_categories: Processing at \$depth = {$depth}");
// For public collections, the branch path doesn't exist (why would it?) in which case only root categories are valid
$current_lvl_parent = (!empty($branch_path) ? (int) $branch_path[$depth]["parent"] : 0);
debug("process_posted_featured_collection_categories: \$current_lvl_parent: " . gettype($current_lvl_parent) . " = " . json_encode($current_lvl_parent));
$selected_fc_category = getval("selected_featured_collection_category_{$depth}", null, true);
debug("process_posted_featured_collection_categories: \$selected_fc_category: " . gettype($selected_fc_category) . " = " . json_encode($selected_fc_category));
$force_featured_collection_type = (getval("force_featured_collection_type", "") == "true");
debug("process_posted_featured_collection_categories: \$force_featured_collection_type: " . gettype($force_featured_collection_type) . " = " . json_encode($force_featured_collection_type));
// Validate the POSTed featured collection category for this depth level
$valid_categories = array_merge(array(0), array_column(get_featured_collection_categories($current_lvl_parent, array()), "ref"));
if (
!is_null($selected_fc_category)
&& isset($branch_path[$depth])
&& !in_array($selected_fc_category, $valid_categories)
) {
return array();
}
$fc_category_at_level = (empty($branch_path) ? null : $branch_path[$depth]["ref"]);
debug("process_posted_featured_collection_categories: \$fc_category_at_level: " . gettype($fc_category_at_level) . " = " . json_encode($fc_category_at_level));
if ($selected_fc_category != $fc_category_at_level || $force_featured_collection_type) {
$new_parent = ($selected_fc_category == 0 ? $current_lvl_parent : $selected_fc_category);
debug("process_posted_featured_collection_categories: \$new_parent: " . gettype($new_parent) . " = " . json_encode($new_parent));
$fc_update = array("update_parent" => $new_parent);
if ($force_featured_collection_type) {
$fc_update["force_featured_collection_type"] = true;
}
// When moving a public collection to featured, default to most popular image
if ($depth == 0 && is_null($fc_category_at_level) && (int) $new_parent > 0) {
$fc_update["thumbnail_selection_method"] = $FEATURED_COLLECTION_BG_IMG_SELECTION_OPTIONS["most_popular_image"];
}
return $fc_update;
}
if (is_null($selected_fc_category)) {
return array();
}
return process_posted_featured_collection_categories(++$depth, $branch_path);
}
/**
* Find existing featured collection ref using its name and parent
*
* @param string $name Featured collection name to search by
* @param null|integer $parent The featured collection parent
*
* @return null|integer
*/
function get_featured_collection_ref_by_name(string $name, $parent)
{
if (!is_null($parent) && !is_int($parent)) {
return null;
}
$sql = "SELECT ref AS `value` FROM collection WHERE `name` = ? AND `type` = ? AND ";
$params = array("s",trim($name),"s",COLLECTION_TYPE_FEATURED);
if (is_null($parent)) {
$sql .= "parent is null";
} else {
$sql .= "parent = ?";
$params[] = "i";
$params[] = $parent;
}
$ref = ps_value($sql, $params, null, "featured_collections");
return is_null($ref) ? null : (int) $ref;
}
/**
* Move a featured collection branch paths' root to the node determined by the global configuration option $featured_collections_root_collection.
*
* This temporarily moves the root of the featured collection branch, removing any nodes on the branch from the real root
* up to the new root.
*
* @see $featured_collections_root_collection configuration option
*
* @param array $branch_path List of branch path nodes as returned by {@see compute_node_branch_path()}
*
* @return array
*/
function move_featured_collection_branch_path_root(array $branch_path)
{
global $featured_collections_root_collection;
if ($featured_collections_root_collection > 0) {
$fc_root_col_position = array_search($featured_collections_root_collection, array_column($branch_path, 'ref'));
if ($fc_root_col_position !== false) {
$branch_path = array_slice($branch_path, ++$fc_root_col_position);
}
}
return $branch_path;
}
/**
* Check if user is allowed to share collection
*
* @param array $c Collection data
*
* @return boolean Return TRUE if user is allowed to share the collection, FALSE otherwise
*/
function allow_collection_share(array $c)
{
global $allow_share, $manage_collections_share_link, $k, $internal_share_access,
$restricted_share, $system_read_only, $system_read_only, $collection_allow_empty_share;
if (!isset($GLOBALS["count_result"])) {
$collection_resources = get_collection_resources($c["ref"]);
$collection_resources = (is_array($collection_resources) ? count($collection_resources) : 0);
} else {
$collection_resources = $GLOBALS["count_result"];
}
$internal_share_access = (!is_null($internal_share_access) && is_bool($internal_share_access) ? $internal_share_access : internal_share_access());
if (!isset($c['type'])) {
$c = get_collection($c['ref']);
}
if (
$allow_share
&& !$system_read_only
&& $manage_collections_share_link
&& ($collection_resources > 0 || $collection_allow_empty_share)
&& ($k == "" || $internal_share_access)
&& !checkperm("b")
&& (checkperm("v")
|| checkperm("g")
|| collection_min_access($c["ref"]) <= RESOURCE_ACCESS_RESTRICTED
|| $restricted_share)
&& !in_array($c['type'], [COLLECTION_TYPE_REQUEST])
) {
return true;
}
return false;
}
/**
* Check if user is allowed to share featured collection. If the featured collection provided is a category, then this
* function will return FALSE if at least one sub featured collection has no share access (this is kept consistent with
* the check for normal collections when checking resources).
*
* @param array $c Collection data. You can add "has_resources" and "sub_fcs" keys if you already have this information
*
* @return boolean Return TRUE if user is allowed to share the featured collection, FALSE otherwise
*/
function allow_featured_collection_share(array $c)
{
if ($c["type"] != COLLECTION_TYPE_FEATURED) {
return allow_collection_share($c);
}
if (!featured_collection_check_access_control($c["ref"])) {
return false;
}
if (!isset($c["has_resources"])) {
$collection_resources = get_collection_resources($c["ref"]);
$c["has_resources"] = (is_array($collection_resources) && !empty($collection_resources) ? 1 : 0);
}
// Not a category, can be treated as a simple collection
if (!is_featured_collection_category($c)) {
return allow_collection_share($c);
}
$sub_fcs = (!isset($c["sub_fcs"]) ? get_featured_collection_categ_sub_fcs($c) : $c["sub_fcs"]);
return array_reduce($sub_fcs, function ($carry, $item) {
// Fake a collection data structure. allow_collection_share() only needs the ref
$c = array("ref" => $item);
$fc_allow_share = allow_collection_share($c);
// FALSE if at least one collection has no share access (consistent with the check for normal collections when checking resources)
return !is_bool($carry) ? $fc_allow_share : $carry && $fc_allow_share;
}, null);
}
/**
* Filter out featured collections that have a different root path. The function builds internally the path to the root from
* the provided featured collection ref and then filters out any featured collections that have a different root path.
*
* @param array $fcs List of featured collections refs to filter out
* @param int $c_ref A root featured collection ref
* @param array $ctx Contextual data
*
* @return array
*/
function filter_featured_collections_by_root(array $fcs, int $c_ref, array $ctx = array())
{
if (empty($fcs)) {
return array();
}
global $CACHE_FCS_BY_ROOT;
$CACHE_FCS_BY_ROOT = (!is_null($CACHE_FCS_BY_ROOT) && is_array($CACHE_FCS_BY_ROOT) ? $CACHE_FCS_BY_ROOT : array());
$cache_id = $c_ref . md5(json_encode($fcs));
if (isset($CACHE_FCS_BY_ROOT[$cache_id][$c_ref])) {
return $CACHE_FCS_BY_ROOT[$cache_id][$c_ref];
}
$all_fcs = (isset($ctx["all_fcs"]) && is_array($ctx["all_fcs"]) ? $ctx["all_fcs"] : array());
$branch_path_fct = function ($carry, $item) {
return "{$carry}/{$item["ref"]}";
};
$category_branch_path = get_featured_collection_category_branch_by_leaf($c_ref, $all_fcs);
$category_branch_path_str = array_reduce($category_branch_path, $branch_path_fct, "");
$collections = array_filter($fcs, function (int $ref) use ($branch_path_fct, $category_branch_path_str, $all_fcs) {
$branch_path = get_featured_collection_category_branch_by_leaf($ref, $all_fcs);
$branch_path_str = array_reduce($branch_path, $branch_path_fct, "");
return substr($branch_path_str, 0, strlen($category_branch_path_str)) == $category_branch_path_str;
});
$CACHE_FCS_BY_ROOT[$cache_id][$c_ref] = $collections;
return array_values($collections);
}
/**
* Get all featured collections branches where the specified resources can be found.
*
* @param array $r_refs List of resource IDs
*
* @return array Returns list of featured collections (categories included) that contain the specified resource(s).
*/
function get_featured_collections_by_resources(array $r_refs)
{
$resources = array_filter($r_refs, "is_numeric");
if (empty($resources)) {
return array();
}
$featured_type_filter_sql = "";
$featured_type_filter_sql_params = [];
$fcf_sql = featured_collections_permissions_filter_sql("AND", "c.ref");
if (is_array($fcf_sql)) {
$featured_type_filter_sql = "(c.`type` = ? " . $fcf_sql[0] . ")";
$featured_type_filter_sql_params = array_merge(["i",COLLECTION_TYPE_FEATURED], $fcf_sql[1]);
}
# Add chunking to avoid exceeding MySQL parameter limits
$fcs = array();
foreach (array_chunk($resources, 10000) as $resource_chunk) {
$sql = sprintf(
"SELECT c.ref, c.`name`, c.`parent`
FROM collection_resource AS cr
JOIN collection AS c ON cr.collection = c.ref AND c.`type` = %s
WHERE cr.resource IN (%s)
%s # access control filter (ok if empty - it means we don't want permission checks or there's nothing to filter out)",
COLLECTION_TYPE_FEATURED,
ps_param_insert(count($resource_chunk)),
$featured_type_filter_sql
);
$fcs_chunk = ps_query($sql, array_merge(ps_param_fill($resource_chunk, 'i'), $featured_type_filter_sql_params));
$fcs = array_merge($fcs, $fcs_chunk);
}
$fcs = array_unique($fcs, SORT_REGULAR);
$results = array();
foreach ($fcs as $fc) {
$results[] = get_featured_collection_category_branch_by_leaf($fc["ref"], array());
}
return $results;
}
/**
* Verify if a featured collection can be deleted. To be deleted, it MUST not have any resources or children (if category).
*
* @param integer $ref Collection ID
*
* @return boolean Returns TRUE if the featured collection can be deleted, FALSE otherwise
*/
function can_delete_featured_collection(int $ref)
{
$sql = "SELECT DISTINCT c.ref AS `value`
FROM collection AS c
LEFT JOIN collection AS cc ON c.ref = cc.parent
LEFT JOIN collection_resource AS cr ON c.ref = cr.collection
WHERE c.`type` = ?
AND c.ref = ?
GROUP BY c.ref
HAVING count(DISTINCT cr.resource) = 0
AND count(DISTINCT cc.ref) = 0";
$params = array("s",COLLECTION_TYPE_FEATURED,"i",$ref);
return ps_value($sql, $params, 0) > 0;
}
/**
* Remove all instances of the specified character from start of string
*
* @param string $string String to update
* @param string $char Character to remove
* @return string
*/
function strip_prefix_chars($string, $char)
{
while (strpos($string, $char) === 0) {
$regmatch = preg_quote($char);
$string = preg_replace("/" . $regmatch . '/', '', $string, 1);
}
return $string;
}
/**
* Check access control if user is allowed to upload to a collection.
*
* @param array $c Collection data structure
*
* @return boolean
*/
function allow_upload_to_collection(array $c)
{
if (empty($c)) {
return false;
}
if (
in_array($c["type"], [COLLECTION_TYPE_SELECTION,COLLECTION_TYPE_REQUEST])
// Featured Collection Categories can't contain resources, only other featured collections (categories or normal)
|| ($c["type"] == COLLECTION_TYPE_FEATURED && is_featured_collection_category_by_children($c["ref"]))
) {
return false;
}
global $userref, $k, $internal_share_access;
$internal_share_access = (!is_null($internal_share_access) && is_bool($internal_share_access) ? $internal_share_access : internal_share_access());
if (
($k == "" || $internal_share_access)
&& ($c["savedsearch"] == "" || $c["savedsearch"] == 0)
&& ($userref == $c["user"] || $c["allow_changes"] == 1 || checkperm("h") || checkperm("a"))
&& (checkperm("c") || checkperm("d"))
) {
return true;
}
return false;
}
/**
* Compute the featured collections allowed based on current access control
*
* @return boolean|array Returns FALSE if user should not see any featured collections (usually means misconfiguration) -or-
* TRUE if user has access to all featured collections. If some access control is in place, then the
* return will be an array with all the allowed featured collections
*/
function compute_featured_collections_access_control()
{
global $CACHE_FC_ACCESS_CONTROL, $userpermissions;
if (!is_null($CACHE_FC_ACCESS_CONTROL)) {
return $CACHE_FC_ACCESS_CONTROL;
}
$all_fcs = ps_query("SELECT ref, parent FROM collection WHERE `type` = ?", ['i', COLLECTION_TYPE_FEATURED], "featured_collections");
$all_fcs_rp = reshape_array_by_value_keys($all_fcs, 'ref', 'parent');
// Set up arrays to store permitted/blocked featured collections
$includerefs = array();
$excluderefs = array();
if (checkperm("j*")) {
// Check for -jX permissions.
foreach ($userpermissions as $userpermission) {
if (substr($userpermission, 0, 2) == "-j") {
$fcid = substr($userpermission, 2);
if (is_int_loose($fcid)) {
// Collection access has been explicitly denied
$excluderefs[] = $fcid;
// Also deny access to child collections.
$excluderefs = array_merge($excluderefs, array_keys($all_fcs_rp, $fcid));
}
}
}
if (count($excluderefs) == 0) {
return true;
}
} else {
// No access to all, check for j{field} permissions that open up access
foreach ($userpermissions as $userpermission) {
if (substr($userpermission, 0, 1) == "j") {
$fcid = substr($userpermission, 1);
if (is_int_loose($fcid)) {
$includerefs[] = $fcid;
// Add children of this collection unless a -j permission has been added below it
$children = array_keys($all_fcs_rp, $fcid);
$queue = new SplQueue();
$queue->setIteratorMode(SplQueue::IT_MODE_DELETE);
foreach ($children as $child_fc) {
$queue->enqueue($child_fc);
}
while (!$queue->isEmpty()) {
$checkfc = $queue->dequeue();
if (!checkperm("-j" . $checkfc)) {
$includerefs[] = $checkfc;
// Also add children of this collection to queue to check
$fcs_sub = array_keys($all_fcs_rp, $checkfc);
foreach ($fcs_sub as $fc_sub) {
$queue->enqueue($fc_sub);
}
}
}
}
}
}
if (count($includerefs) == 0) {
// Misconfiguration - user can only see specific FCs but none have been selected
return false;
}
}
$return = array();
foreach ($all_fcs_rp as $fc => $fcp) {
if ((in_array($fc, $includerefs) || checkperm("j*")) && !in_array($fc, $excluderefs)) {
$return[] = $fc;
}
}
$CACHE_FC_ACCESS_CONTROL = $return;
return $return;
}
/**
* Check if user is allowed to re-order featured collections
* @return boolean
*/
function can_reorder_featured_collections()
{
return checkperm('h') && compute_featured_collections_access_control() === true;
}
/**
* Remove all old anonymous collections
*
* @param int $limit Maximum number of collections to delete - if run from browser this is kept low to avoid delays
* @return void
*/
function cleanup_anonymous_collections(int $limit = 100)
{
global $anonymous_login;
$sql_limit = "";
$params = [];
if ($limit != 0) {
$sql_limit = 'LIMIT ?';
$params = ['i', $limit];
}
if (!is_array($anonymous_login)) {
$anonymous_login = array($anonymous_login);
}
foreach ($anonymous_login as $anonymous_user) {
$user = get_user_by_username($anonymous_user);
if (is_int_loose($user)) {
ps_query("DELETE FROM collection WHERE user = ? AND created < (curdate() - interval '2' DAY) ORDER BY created ASC " . $sql_limit, array_merge(['i', $user], $params));
}
}
}
/**
* Check if user is permitted to create an external upload link for the given collection
*
* @param array $collection_data Array of collection data
* @return boolean
*/
function can_share_upload_link($collection_data)
{
global $usergroup,$upload_link_usergroups;
if (!is_array($collection_data) && is_numeric($collection_data)) {
$collection_data = get_collection($collection_data);
}
return allow_upload_to_collection($collection_data) && (checkperm('a') || checkperm("exup"));
}
/**
* Check if user can edit an existing upload share
*
* @param int $collection Collection ID of share
* @param string $uploadkey External upload key
*
* @return bool
*/
function can_edit_upload_share($collection, $uploadkey)
{
global $userref;
if (checkperm('a')) {
return true;
}
$share_details = get_external_shares(array("share_collection" => $collection,"share_type" => 1, "access_key" => $uploadkey));
$details = isset($share_details[0]) ? $share_details[0] : array();
return
(isset($details["user"]) && $details["user"] == $userref)
|| (checkperm("ex") && array_key_exists("expires", $details) && empty($details["expires"]));
}
/**
* Creates an upload link for a collection that can be shared
*
* @param int $collection Collection ID
* @param array $shareoptions - values to set
* 'usergroup' Usergroup id to share as (must be in $upload_link_usergroups array)
* 'expires' Expiration date in 'YYYY-MM-DD' format
* 'password' Optional password for share access
* 'emails' Optional array of email addresses to generate keys for
*
* @return string Share access key
*/
function create_upload_link($collection, $shareoptions)
{
global $upload_link_usergroups, $lang, $scramble_key, $usergroup, $userref;
global $baseurl, $applicationname;
$stdshareopts = array("user","usergroup","expires");
if (!in_array($shareoptions["usergroup"], $upload_link_usergroups) && $shareoptions["usergroup"] != $usergroup) {
return $lang["error_invalid_usergroup"];
}
if (strtotime($shareoptions["expires"]) < time()) {
return $lang["error_invalid_date"];
}
// Generate as many new keys as required
$newkeys = array();
$numkeys = isset($shareoptions["emails"]) ? count($shareoptions["emails"]) : 1;
for ($n = 0; $n < $numkeys; $n++) {
$newkeys[$n] = generate_share_key($collection);
}
// Create array to store sql insert data
$setcolumns = array(
"collection" => $collection,
"user" => $userref,
"upload" => '1',
"date" => date("Y-m-d H:i", time()),
);
foreach ($stdshareopts as $option) {
if (isset($shareoptions[$option])) {
$setcolumns[$option] = $shareoptions[$option];
}
}
$newshares = array(); // Create array of new share details to return
for ($n = 0; $n < $numkeys; $n++) {
$setcolumns["access_key"] = $newkeys[$n];
if (isset($shareoptions["password"]) && $shareoptions["password"] != "") {
// Only set if it has actually been set to a string
$setcolumns["password_hash"] = hash('sha256', $newkeys[$n] . $shareoptions["password"] . $scramble_key);
}
if (isset($shareoptions["emails"][$n])) {
if (!filter_var($shareoptions["emails"][$n], FILTER_VALIDATE_EMAIL)) {
$newshares[$n] = "";
continue;
}
$setcolumns["email"] = $shareoptions["emails"][$n];
}
$insert_columns = array_keys($setcolumns);
$insert_values = array_values($setcolumns);
$sql = "INSERT INTO external_access_keys
(" . implode(",", $insert_columns) . ")
VALUES (" . ps_param_insert(count($insert_values)) . ")";
ps_query($sql, ps_param_fill($insert_values, 's'));
$newshares[$n] = $newkeys[$n];
if (isset($shareoptions["emails"][$n])) {
// Send email
$url = $baseurl . "/?c=" . $collection . "&k=" . $newkeys[$n];
$coldata = get_collection($collection, true);
$userdetails = get_user($userref);
$collection_name = i18n_get_collection_name($coldata);
$link = "<a href='" . $url . "'>" . $collection_name . "</a>";
$passwordtext = (isset($shareoptions["password"]) && $shareoptions["password"] != "") ? $lang["upload_share_email_password"] . " : '" . $shareoptions["password"] . "'" : "";
$templatevars = array();
$templatevars['link'] = $link;
$templatevars['message'] = trim($shareoptions["message"]) != "" ? $shareoptions["message"] : "";
$templatevars['from_name'] = $userdetails["fullname"] == "" ? $userdetails["username"] : $userdetails["fullname"];
$templatevars['applicationname'] = $applicationname;
$templatevars['passwordtext'] = $passwordtext;
$expires = isset($shareoptions["expires"]) ? $shareoptions["expires"] : "";
if ($expires == "") {
$templatevars['expires_date'] = $lang["email_link_expires_never"];
$templatevars['expires_days'] = $lang["email_link_expires_never"];
} else {
$day_count = round((strtotime($expires) - strtotime('now')) / (60 * 60 * 24));
$templatevars['expires_date'] = $lang['email_link_expires_date'] . nicedate($expires);
$templatevars['expires_days'] = $lang['email_link_expires_days'] . $day_count;
if ($day_count > 1) {
$templatevars['expires_days'] .= " " . $lang['expire_days'] . ".";
} else {
$templatevars['expires_days'] .= " " . $lang['expire_day'] . ".";
}
}
$subject = $lang["upload_share_email_subject"] . $applicationname;
$body = $templatevars['from_name'] . " " . $lang["upload_share_email_text"] . $applicationname;
$body .= "<br/><br/>\n" . ($templatevars['message'] != "" ? $templatevars['message'] : "");
$body .= "<br/><br/>\n" . $templatevars['link'];
if ($passwordtext != "") {
$body .= "<br/><br/>\n" . $passwordtext;
}
$send_result = send_mail($shareoptions["emails"][$n], $subject, $body, $templatevars['from_name'], "", "upload_share_email_template", $templatevars);
if ($send_result !== true) {
return $send_result;
}
}
$lognotes = array();
foreach ($setcolumns as $column => $value) {
if ($column == "password_hash") {
$lognotes[] = trim($value) != "" ? "password=TRUE" : "";
} else {
$lognotes[] = $column . "=" . $value;
}
}
collection_log($collection, LOG_CODE_COLLECTION_SHARED_UPLOAD, null, (isset($shareoptions["emails"][$n]) ? $shareoptions["emails"][$n] : "") . "(" . implode(",", $lognotes) . ")");
}
return $newshares;
}
/**
* Generates an external share key based on provided string
*
* @param string $string
* @return string Generated key
*/
function generate_share_key($string)
{
return substr(md5($string . "," . time() . rand()), 0, 10);
}
/**
* Check if an external upload link is being used
*
* @return mixed false|int ID of upload collection, or false if not active
*/
function upload_share_active()
{
global $upload_share_active;
if (isset($upload_share_active)) {
return $upload_share_active;
} elseif (isset($_COOKIE["upload_share_active"]) && getval("k", "") != "") {
return (int) $_COOKIE["upload_share_active"];
}
return false;
}
/**
* Set up external upload share
*
* @param string $key access key
* @param array $shareopts Array of share options
* "collection" - (int) collection ID
* "user" - (int) user ID of share creator
* "usergroup" - (int) usergroup ID used for share
* @return void
*/
function upload_share_setup(string $key, $shareopts = array())
{
debug_function_call("upload_share_setup", func_get_args());
global $baseurl, $pagename, $upload_share_active, $upload_then_edit;
global $upload_link_workflow_state, $override_status_default,$usergroup;
$rqdopts = array("collection", "usergroup", "user");
foreach ($rqdopts as $rqdopt) {
if (!isset($shareopts[$rqdopt])) {
return false;
}
}
$collection = (int) $shareopts['collection'];
$usergroup = (int) $shareopts['usergroup'];
emulate_user((int) $shareopts['user'], $usergroup);
$upload_share_active = upload_share_active();
$upload_then_edit = true;
if (!$upload_share_active || $upload_share_active != $collection) {
// Create a new session even if one exists to ensure a new temporary collection is created for this share
rs_setcookie("rs_session", '', 7, "", "", substr($baseurl, 0, 5) == "https", true);
rs_setcookie("upload_share_active", $collection, 1, "", "", substr($baseurl, 0, 5) == "https", true);
$upload_share_active = true;
}
// Set default archive state
if (in_array($upload_link_workflow_state, get_workflow_states())) {
$override_status_default = $upload_link_workflow_state;
}
// Upload link key can only work on these pages
$validpages = array(
"upload_batch",
"edit",
"category_tree_lazy_load",
"suggest_keywords",
"add_keyword",
"download", // Required to see newly created thumbnails if $hide_real_filepath=true;
"terms",
);
if (!in_array($pagename, $validpages)) {
$uploadurl = get_upload_url($collection, $key);
redirect($uploadurl);
exit();
}
return true;
}
/**
* Notify the creator of an external upload share that resources have been uploaded
*
* @param int $collection Ref of external shared collection
* @param string $k External upload access key
* @param int $tempcollection Ref of temporay upload collection
* @return void
*/
function external_upload_notify($collection, $k, $tempcollection)
{
global $applicationname,$baseurl,$lang;
$upload_share = get_external_shares(array("share_collection" => $collection,"share_type" => 1, "access_key" => $k));
if (!isset($upload_share[0]["user"])) {
debug("external_upload_notify() - unable to find external share details: " . func_get_args());
}
$user = $upload_share[0]["user"];
$usergroup = $upload_share[0]["usergroup"];
$templatevars = array();
$url = $baseurl . "/?c=" . (int)$collection;
$templatevars['url'] = $url;
$message = $lang["notify_upload_share_new"] . "\n\n" . $lang["clicklinkviewcollection"] . "\n\n" . $url;
$notificationmessage = $lang["notify_upload_share_new"];
// Does the user want an email or notification?
get_config_option(['user' => $user, 'usergroup' => $usergroup], 'email_user_notifications', $send_email);
if ($send_email) {
$notify_email = ps_value("select email value from user where ref=?", array("i",$user), "");
if ($notify_email != '') {
send_mail($notify_email, $applicationname . ": " . $lang["notify_upload_share_new_subject"], $message, "", "", "emailnotifyuploadsharenew", $templatevars);
}
} else {
global $userref;
message_add($user, $notificationmessage, $url, 0);
}
}
/**
* Purge all expired shares/**
* @param array $filteropts Array of options to filter shares purged
* "share_group" - (int) Usergroup ref 'shared as'
* "share_user" - (int) user ID of share creator
* "share_type" - (int) 0=view, 1=upload
* "share_collection" - (int) Collection ID
* @return string|int
*/
function purge_expired_shares($filteropts)
{
global $userref;
$share_group = $filteropts['share_group'] ?? null;
$share_user = $filteropts['share_user'] ?? null;
$share_type = $filteropts['share_type'] ?? null;
$share_collection = $filteropts['share_collection'] ?? null;
$conditions = array();
$params = [];
if ((int)$share_user > 0 && ($share_user == $userref || checkperm_user_edit($share_user))) {
$conditions[] = "user = ?";
$params = array_merge($params, ['i', $share_user]);
} elseif (!checkperm('a') && !checkperm('ex')) {
$conditions[] = "user = ?";
$params = array_merge($params, ['i', $userref]);
}
if (!is_null($share_group) && (int)$share_group > 0 && checkperm('a')) {
$conditions[] = "usergroup = ?";
$params = array_merge($params, ['i', $share_group]);
}
if ($share_type == 0) {
$conditions[] = "(upload=0 OR upload IS NULL)";
} elseif ($share_type == 1) {
$conditions[] = "upload=1";
}
if ((int)$share_collection > 0) {
$conditions[] = "collection = ?";
$params = array_merge($params, ['i', $share_collection]);
}
$conditional_sql = " WHERE expires < now()";
if (count($conditions) > 0) {
$conditional_sql .= " AND " . implode(" AND ", $conditions);
}
$purge_query = "DELETE FROM external_access_keys " . $conditional_sql;
ps_query($purge_query, $params);
return sql_affected_rows();
}
/**
* Check if user has the appropriate access to delete a collection.
*
* @param array $collection_data Array of collection details, typically from get_collection()
* @param int $userref Id of user
* @param int $k External access key value
*
* @return boolean Returns true is the collection can be deleted or false if it cannot.
*/
function can_delete_collection(array $collection_data, $userref, $k = "")
{
return ($k == ''
&& (($userref == $collection_data['user']) || checkperm('h'))
&& $collection_data['cant_delete'] == 0)
&& $collection_data['type'] != COLLECTION_TYPE_REQUEST;
}
/**
* Send collection to administrators - used if $send_collection_to_admin is enabled
*
* @param int $collection Collection ID
* @return boolean
*/
function send_collection_to_admin(int $collection)
{
if (!is_int_loose($collection)) {
return false;
}
global $lang, $userref, $applicationname, $baseurl, $admin_resource_access_notifications;
// Get details about the collection:
$collectiondata = get_collection($collection);
$collection_name = $collectiondata['name'];
$resources_in_collection = count(get_collection_resources($collection));
// Only do this if it is the user's own collection
if ($collectiondata['user'] != $userref) {
return false;
}
$collectionsent = false;
// Create a copy of the collection for admin:
$admin_copy = create_collection(-1, $lang['send_collection_to_admin_emailedcollectionname']);
copy_collection($collection, $admin_copy);
$collection_id = $admin_copy;
// Get the user (or username) of the contributor:
$user = get_user($userref);
if (isset($user) && trim($user['fullname']) != '') {
$user = $user['fullname'];
} else {
$user = $user['username'];
}
// Build mail and send it:
$subject = $applicationname . ': ' . $lang['send_collection_to_admin_emailsubject'] . $user;
$message = $user . $lang['send_collection_to_admin_usercontributedcollection'] . "\n\n";
$message .= $baseurl . '/pages/search.php?search=!collection' . $collection_id . "\n\n";
$message .= $lang['send_collection_to_admin_additionalinformation'] . "\n\n";
$message .= $lang['send_collection_to_admin_collectionname'] . $collection_name . "\n\n";
$message .= $lang['send_collection_to_admin_numberofresources'] . $resources_in_collection . "\n\n";
$notification_message = $lang['send_collection_to_admin_emailsubject'] . " " . $user;
$notification_url = $baseurl . '/?c=' . $collection_id;
$admin_notify_emails = array();
$admin_notify_users = array();
$notify_users = get_notification_users(array("e-1","e0"));
foreach ($notify_users as $notify_user) {
get_config_option(['user' => $notify_user['ref'], 'usergroup' => $notify_user['usergroup']], 'user_pref_resource_notifications', $send_message, $admin_resource_access_notifications);
if (!$send_message) {
continue;
}
get_config_option(['user' => $notify_user['ref'], 'usergroup' => $notify_user['usergroup']], 'email_user_notifications', $send_email);
if ($send_email && $notify_user["email"] != "") {
$admin_notify_emails[] = $notify_user['email'];
} else {
$admin_notify_users[] = $notify_user["ref"];
}
}
foreach ($admin_notify_emails as $admin_notify_email) {
send_mail($admin_notify_email, $subject, $message, '', '');
$collectionsent = true;
}
if (count($admin_notify_users) > 0) {
debug("sending collection to user IDs: " . implode(",", $admin_notify_users));
message_add($admin_notify_users, $notification_message, $notification_url, $userref, MESSAGE_ENUM_NOTIFICATION_TYPE_SCREEN, MESSAGE_DEFAULT_TTL_SECONDS, SUBMITTED_COLLECTION, $collection_id);
$collectionsent = true;
}
return $collectionsent;
}
/**
* Get the user's default collection, creating one if necessary
*
* @param bool $setactive Set the collection as the user's active collection?
* @return int collection ID
*/
function get_default_user_collection($setactive = false)
{
global $userref;
$usercollection = ps_value("SELECT ref value FROM collection WHERE user=? AND name LIKE 'Default Collection%' ORDER BY created ASC LIMIT 1", array("i",$userref), 0);
if ($usercollection == 0) {
# Create a collection for this user
# The collection name is translated when displayed!
$usercollection = create_collection($userref, "Default Collection", 0, 1); # Do not translate this string!
}
if ($setactive) {
# set this to be the user's current collection
ps_query("UPDATE user SET current_collection=? where ref=?", array("i",$usercollection,"i",$userref));
set_user_collection($userref, $usercollection);
}
return $usercollection;
}
/**
* Update a smart collection with or without the $smart_collections_async option.
*
* @param int $smartsearch_ref Id of 'savedsearch'.
*
* @return void
*/
function update_smart_collection(int $smartsearch_ref)
{
if ($smartsearch_ref == 0) {
return;
}
$smartsearch = ps_query("select search, collection, restypes, starsearch, archive, created, result_limit from collection_savedsearch where ref = ?", ['i', $smartsearch_ref]);
global $smart_collections_async;
if (isset($smartsearch[0]['search'])) {
$smartsearch = $smartsearch[0];
$collection = $smartsearch['collection'];
$smartsearch_archives = $smartsearch['archive'];
$search_all_archives = $smartsearch_archives === 'all';
# Option to limit results;
$result_limit = $smartsearch["result_limit"];
if ($result_limit == "" || $result_limit == 0) {
$result_limit = -1;
}
$startTime = microtime(true);
global $smartsearch_accessoverride;
$search_all_workflow_states_original = $GLOBALS['search_all_workflow_states'];
if ($search_all_archives) {
# Search saved for all states when $search_all_workflow_states was true so make sure we always apply it for the search.
$GLOBALS['search_all_workflow_states'] = true;
}
$results = do_search($smartsearch['search'], $smartsearch['restypes'], "relevance", $smartsearch_archives, $result_limit, "desc", $smartsearch_accessoverride, $smartsearch['starsearch'], false, false, "", false, true, false, false, false, null, true);
$GLOBALS['search_all_workflow_states'] = $search_all_workflow_states_original;
# results is a list of the current search without any restrictions
# we need to compare against the current collection contents to minimize inserts and deletions
$current_contents = ps_array("select resource value from collection_resource where collection= ?", ['i', $collection]);
$results_contents = array();
$counter = 0;
if (!empty($results) && is_array($results)) {
foreach ($results as $results_item) {
if (isset($results_item['ref'])) {
$results_contents[] = $results_item['ref'];
$counter++;
if ($counter >= $result_limit && $result_limit != -1) {
break;
}
}
}
}
$results_contents_add = array_values(array_diff($results_contents, $current_contents));
$current_contents_remove = array_values(array_diff($current_contents, $results_contents));
$count_results = count($results_contents_add);
if ($count_results > 0) {
# Add any new resources
debug("smart_collections" . (($smart_collections_async) ? "_async:" : ":") . " Adding $count_results resources to collection...");
if ($smartsearch_archives !== '') {
$smartsearch_archives = explode(",", $smartsearch_archives);
for ($n = 0; $n < $count_results; $n++) {
if ($search_all_archives) {
add_resource_to_collection($results_contents_add[$n], $collection, true);
} else {
# Check the resource archive state
$archivestatus = ps_value("SELECT archive AS value FROM resource WHERE ref = ?", ["i",$results_contents_add[$n]], "");
if (in_array($archivestatus, $smartsearch_archives)) {
add_resource_to_collection($results_contents_add[$n], $collection, true);
}
}
}
}
}
$count_contents = count($current_contents_remove);
if ($count_contents > 0) {
# Remove any resources no longer present.
debug("smart_collections" . (($smart_collections_async) ? "_async:" : ":") . " Removing $count_contents resources...");
for ($n = 0; $n < $count_contents; $n++) {
remove_resource_from_collection($current_contents_remove[$n], $collection, true);
}
}
$endTime = microtime(true);
$elapsed = $endTime - $startTime;
debug("smart_collections" . (($smart_collections_async) ? "_async:" : ":") . " $elapsed seconds for " . $smartsearch['search']);
}
}
/**
* Check if the terms have been accepted for the given upload
* Terms only need to be accepted when uploading through an upload share link
* If uploading through an upload share link then the accepted terms have been stored in $_COOKIE["acceptedterms"]
*
* @param int $collection Collection ref
* @param string $k Share key
*
* @return boolean True if external upload share and terms have also been accepted
* OR if not an external upload
* False if external upload share and terms have NOT been accepted
*/
function check_upload_terms(int $collection, string $k): bool
{
$keyinfo = ps_query(
"SELECT collection,upload
FROM external_access_keys
WHERE access_key = ?
AND (expires IS NULL OR expires > now())",
array("s", $k)
);
$collection = get_collection($collection);
if (
!is_array($collection) // not uploading to collection
|| !in_array($collection["ref"], array_column($keyinfo, "collection")) // share is not for this collection
|| (bool) $keyinfo[0]["upload"] !== true
) { // share type not upload
return true;
} else {
return array_key_exists("acceptedterms", $_COOKIE) && $_COOKIE["acceptedterms"] == 1;
}
}
/**
* Determines whether the current user has permission to create collections.
*
* This function checks for specific conditions that would prevent the user from creating collections.
* It returns `false` if any of these conditions are met:
* - The user has the "b" permission, which restricts collection creation.
* - The user is anonymous and does not have a session collection.
*
* @return bool Returns `true` if the user can create collections; otherwise, `false`.
*/
function can_create_collections()
{
global $anonymous_user_session_collection;
return !( // Return FALSE if any of these conditions are true
checkperm("b")
|| (is_anonymous_user() && !$anonymous_user_session_collection) // User is an anonymous user
);
}
/**
* Re-order all featured collections at a particular tree depth.
*
* @param null|integer parent ID of the featured collections' parent to target
* @return array Featured collection IDs list, in the new order
*/
function reorder_all_featured_collections_with_parent(?int $parent): array
{
$sql_where_parent = is_null($parent)
? new PreparedStatementQuery('IS NULL')
: new PreparedStatementQuery('= ?', ['i', $parent]);
$fcs_at_depth = ps_query(
"SELECT DISTINCT c.ref,
c.`name`,
c.`type`,
c.parent,
c.order_by,
count(DISTINCT cr.resource) > 0 AS has_resources
FROM collection AS c
LEFT JOIN collection_resource AS cr ON c.ref = cr.collection
WHERE c.`type` = ?
AND c.parent {$sql_where_parent->sql}
GROUP BY c.ref",
array_merge(['i', COLLECTION_TYPE_FEATURED], $sql_where_parent->parameters)
);
if (!$GLOBALS['allow_fc_reorder']) {
$fcs_at_depth = array_map('set_order_by_to_zero', $fcs_at_depth);
}
usort($fcs_at_depth, 'order_featured_collections');
$new_fcs_order = array_column($fcs_at_depth, 'ref');
sql_reorder_records('collection', $new_fcs_order);
return $new_fcs_order;
}
/**
* Generate a collection download ZIP file and the download filename
*
* @param array $dl_data Array of collection download options passed from collection_download.php or from the offline job
* This array will be updated and passed to subsidiary functions to keep track of processed file, generate text etc.
* Could be moved to an object later
* [
* "filename" => [the name of download file],
* "collection" => [Collection ID],
* "collection_resources" => [Resources to include in download],
* "collectiondata" => [Collection data - from get_collection()],
* "exiftool_write_option" => [Write exif data?],
* "useoriginal" => [Use original if requested size not available?],
* "size" => [Requested Download size ID],
* "settings_id" => [Index of selected option from $collection_download_settings],
* "deletion_array" => [Array of paths to delete],
* "include_csv_file" => [Include metadata CSV file?],
* "include_alternatives" => [Include alternative files?],
* "includetext" => Include text file?,
* "collection_download_tar" => [Generate a TAR file?],
* "count_data_only_types" => [Count of data only resources],
* "id" => [Optional unique identifier - [used to create a download.php link that is specific to the user],
* "k" => External access key from download request if set
* ];
*
* @return array Array of data about the created file and the download file nam, or the TAR status i.e.
* [
* "filename" => [the name of download file],
* "path" => [path to the zip file],
* "completed" => [Set to true if a tar has been sent],
* ];
*/
function process_collection_download(array $dl_data): array
{
// Set elements that may not have been set e.g. a job created in an earlier version
foreach (['archiver', 'collection_download_tar', 'include_alternatives', 'k'] as $unset_var) {
if (!isset($dl_data[$unset_var])) {
$dl_data[$unset_var] = false;
}
}
$collection = (int) ($dl_data['collection'] ?? 0);
$collectiondata = $dl_data['collectiondata'] ?? [];
// Please note re: collection_resources - the current collection resources are not retrieved here as
// these may have changed since the download was requested. Used to be stored as "result" element
$collection_resources = $dl_data['collection_resources'] ?? ($dl_data['result'] ?? []);
$size = (string) ($dl_data['size'] ?? "");
$useoriginal = (bool) ($dl_data['useoriginal'] ?? false);
$id = (string) ($dl_data['id'] ?? uniqid("Col" . $collection));
$includetext = (bool) ($dl_data['includetext'] ?? false);
$count_data_only_types = (int) ($dl_data['count_data_only_types'] ?? 0);
$settings_id = (string) ($dl_data['settings_id'] ?? "");
$include_csv_file = (bool) ($dl_data['include_csv_file'] ?? false);
$include_alternatives = (bool) ($dl_data['include_alternatives'] ?? false);
$collection_download_tar = (bool) ($dl_data['collection_download_tar'] ?? false);
$archiver = (bool) ($dl_data["archiver"] ?? false);
// Set this as global - required by write_metadata() and hooks
global $exiftool_write_option, $p, $pextension;
$saved_exiftool_write_option = $exiftool_write_option;
$exiftool_write_option = $dl_data['exiftool_write_option'];
if (empty($collectiondata) && $collection > 0) {
$collectiondata = get_collection($collection);
}
if (
empty($collectiondata)
|| empty($collection_resources)
) {
debug("Missing collection data, Unable to proceed with collection download");
return [];
}
$zip = false;
if (!$collection_download_tar) {
// Generate a randomised path for zip file
$extension = $archiver ? $GLOBALS["collection_download_settings"][$settings_id]["extension"] : "zip";
$zipfile = get_temp_dir(false, 'user_downloads') . DIRECTORY_SEPARATOR . $GLOBALS["userref"] . "_" . md5($GLOBALS["username"] . $id . $GLOBALS["scramble_key"]) . "." . $extension;
debug('Collection download : $zipfile =' . $zipfile);
if ($GLOBALS['use_zip_extension']) {
$zip = new ZipArchive();
$zip->open($zipfile, ZIPARCHIVE::CREATE);
}
}
$dl_data['includefiles'] = []; // Store array of files to include in download
$dl_data['deletion_array'] = [];
$dl_data['filenames'] = []; // Set up an array to store the filenames as they are found (to analyze dupes)
$dl_data['used_resources'] = [];
$dl_data['subbed_original_resources'] = [];
$allsizes = get_all_image_sizes(true);
$rescount = count($collection_resources);
if ($includetext) {
// Initiate text file
$dl_data['text'] = i18n_get_collection_name($collectiondata) . "\r\n" .
$GLOBALS["lang"]["downloaded"] . " " . nicedate(date("Y-m-d H:i:s"), true, true) . "\r\n\r\n" .
$GLOBALS["lang"]["contents"] . ":\r\n\r\n";
if ($size == "") {
$dl_data['sizetext'] = "";
} else {
$dl_data['sizetext'] = "-" . $size;
}
}
db_begin_transaction("collection_download"); // Ensure all log updates are committed at once
for ($n = 0; $n < $rescount; $n++) {
// Set a flag to indicate whether file should be included
$skipresource = false;
if (!isset($collection_resources[$n]['resource_type'])) {
// Resource data is not present - e.g. an offline job
$collection_resources[$n] = get_resource_data($collection_resources[$n]["ref"]);
$dl_data['collection_resources'][$n] = $collection_resources[$n]; // Update so will be passed to other functions
}
resource_type_config_override($collection_resources[$n]['resource_type'], false); # False means execute override for every resource
$copy = false;
$ref = $collection_resources[$n]['ref'];
$access = get_resource_access($collection_resources[$n]);
$use_watermark = check_use_watermark();
$subbed_original = false;
// Do not download resources without proper access level
if ($access > 1) {
debug('Collection download : skipping resource ID ' . $ref . ' user ID ' . $GLOBALS["userref"] . ' does not have access to this resource');
continue;
}
// Get all possible sizes for this resource.
// If largest available has been requested then include internal or user could end up with no file depite being able to see the preview
$sizes = array_filter($allsizes, function ($availsize) use ($access, $size) {
return
($availsize["allow_restricted"] || $access === 0)
&& ((int) $availsize["internal"] === 0 || $size == "largest");
});
# Check availability of original file
$p = get_resource_path($ref, true, "", false, $collection_resources[$n]["file_extension"]);
if (
file_exists($p)
&& (($access == 0) || ($access == 1 && $GLOBALS["restricted_full_download"]))
&& resource_download_allowed($ref, '', $collection_resources[$n]['resource_type'], -1, true)
) {
$dl_data['available_sizes']['original'][] = $ref;
}
// Check for the availability of each size and load it to the available_sizes array
foreach ($sizes as $sizeinfo) {
if (in_array($collection_resources[$n]['file_extension'], $GLOBALS["ffmpeg_supported_extensions"])) {
$size_id = $sizeinfo['id'];
// Video files only have a 'pre' sized derivative so flesh out the sizes array using that.
$p = get_resource_path($ref, true, 'pre', false, $collection_resources[$n]['file_extension']);
$size_id = 'pre';
if (
resource_download_allowed($ref, $size_id, $collection_resources[$n]['resource_type'], -1, true)
&&
(
hook('size_is_available', '', array($collection_resources[$n], $p, $size_id))
|| file_exists($p)
)
) {
$dl_data['available_sizes'][$sizeinfo['id']][] = $ref;
}
} elseif (in_array($collection_resources[$n]['file_extension'], array_merge($GLOBALS["ffmpeg_audio_extensions"], ['mp3']))) {
// Audio files are ported to mp3 and do not have different preview sizes
$p = get_resource_path($ref, true, '', false, 'mp3');
if (
resource_download_allowed($ref, '', $collection_resources[$n]['resource_type'], -1, true)
&&
(
hook('size_is_available', '', array($collection_resources[$n], $p, ''))
|| file_exists($p)
)
) {
$dl_data['available_sizes'][$sizeinfo['id']][] = $ref;
}
} else {
$size_id = $sizeinfo['id'];
$size_extension = get_extension($collection_resources[$n], $size_id);
$p = get_resource_path($ref, true, $size_id, false, $size_extension);
if (
resource_download_allowed($ref, $size_id, $collection_resources[$n]['resource_type'], -1, true)
&&
(
hook('size_is_available', '', array($collection_resources[$n], $p, $size_id))
|| file_exists($p)
)
) {
$dl_data['available_sizes'][$size_id][] = $ref;
}
}
}
// Check which size to use
if ($size == "largest") {
foreach ($dl_data['available_sizes'] as $available_size => $resources) {
if (in_array($ref, $resources)) {
$usesize = $available_size;
if ($available_size == 'original') {
$usesize = "";
// Has access to the original so no need to check previews
break;
}
}
}
} else {
$usesize = ($size == 'original') ? "" : $size;
}
if (in_array($collection_resources[$n]['file_extension'], $GLOBALS["ffmpeg_supported_extensions"]) && $usesize !== '') {
// Supported video formats will only have a pre sized derivative
$pextension = $GLOBALS["ffmpeg_preview_extension"];
$p = get_resource_path($ref, true, 'pre', false, $pextension, -1, 1);
$usesize = 'pre';
} elseif (in_array($collection_resources[$n]['file_extension'], array_merge($GLOBALS["ffmpeg_audio_extensions"], ['mp3'])) && $usesize !== '') {
// Supported audio formats are ported to mp3
$pextension = 'mp3';
$p = get_resource_path($ref, true, '', false, 'mp3', -1, 1);
$usesize = '';
} else {
$pextension = get_extension($collection_resources[$n], $usesize);
$p = get_resource_path($ref, true, $usesize, false, $pextension, -1, 1, $use_watermark);
}
$target_exists = file_exists($p);
$replaced_file = false;
$new_file = hook('replacedownloadfile', '', array($collection_resources[$n], $usesize, $pextension, $target_exists));
if (
$new_file != ''
&& $p != $new_file
) {
$p = $new_file;
$dl_data['deletion_array'][] = $p;
$replaced_file = true;
$target_exists = file_exists($p);
} elseif (
!$target_exists
&& $useoriginal
&& resource_download_allowed($ref, '', $collection_resources[$n]['resource_type'], -1, true)
) {
// This size doesn't exist, so we'll try using the original instead
$p = get_resource_path($ref, true, '', false, $collection_resources[$n]['file_extension'], -1, 1, $use_watermark);
$pextension = $collection_resources[$n]['file_extension'];
$subbed_original = true;
$dl_data['subbed_original_resources'][] = $ref;
$target_exists = file_exists($p);
}
if (!isset($pextension) || trim($pextension) == "") {
$pextension = parse_filename_extension($p);
}
// Move to next resource if file doesn't exist or restricted access and user doesn't have access to the requested size
if (
!(
(
($target_exists && $access == 0)
|| (
$target_exists
&& $access == 1
&& (image_size_restricted_access($size) || ($usesize == '' && $GLOBALS["restricted_full_download"]))
)
)
&& resource_download_allowed($ref, $usesize, $collection_resources[$n]['resource_type'], -1, true)
)
) {
debug('Collection download : Skipping resource ID ' . (int) $ref
. ' file inaccessible to user - $target_exists = ' . $target_exists
. ', $access = ' . $access
. ', image_size_restricted_access(' . $size . ') = ' . image_size_restricted_access($size)
. ', $usesize = ' . $usesize
. ', $restricted_full_download = ' . $GLOBALS["restricted_full_download"]
. ', resource_download_allowed() = ' . resource_download_allowed($ref, $usesize, $collection_resources[$n]['resource_type'], -1, true)
);
// Set to skip, although alternative files may still be available
$skipresource = true;
}
$tmpfile = false;
if (!$skipresource) {
$dl_data['used_resources'][] = $ref;
if ($exiftool_write_option && !$collection_download_tar) {
$tmpfile = write_metadata($p, $ref, $id);
if ($tmpfile !== false && file_exists($tmpfile)) {
// File already in tmp, just rename it
$p = $tmpfile;
} elseif (!$replaced_file) {
// Copy the file from filestore rather than renaming
$copy = true;
}
}
// If using original filenames when downloading, copy the file to new location so the name is included.
$filename = get_download_filename($ref, $usesize, 0, $pextension);
collection_download_use_original_filenames_when_downloading(
$dl_data,
$filename,
$ref,
$pextension,
$p,
$copy,
);
if (hook("downloadfilenamealt")) {
$filename = hook("downloadfilenamealt");
}
if ($includetext) {
$addtext = collection_download_process_text_file($dl_data, $ref, $filename, $subbed_original);
$dl_data['text'] .= $addtext;
}
hook('modifydownloadfile', "", [$collection_resources[$n]]);
if ($collection_download_tar) {
$usertempdir = get_temp_dir(false, "rs_" . $GLOBALS["userref"] . "_" . $id);
debug("collection_download adding symlink: " . $p . " - " . $usertempdir . DIRECTORY_SEPARATOR . $filename);
$GLOBALS["use_error_exception"] = true;
try {
symlink($p, $usertempdir . DIRECTORY_SEPARATOR . $filename);
} catch (Throwable $e) {
debug("process_collection_download(): Unable to create symlink for resource $ref {$e->getMessage()}");
return [];
}
unset($GLOBALS["use_error_exception"]);
} elseif ($GLOBALS['use_zip_extension']) {
debug("Adding $p - ($filename) for ref " . $ref . " to " . $zip->filename);
set_processing_message((string) ($n+1 . "/" . $rescount . " " . $GLOBALS["lang"]["filesaddedtozip"]));
$success = $zip->addFile($p, $filename);
debug('Collection download : Added resource ' . $ref . ' to zip archive = ' . ($success ? 'true' : 'false'));
} else {
$dl_data['includefiles'][] = $p;
}
}
if ($include_alternatives) {
debug("Processing alternative files for resource $ref");
// Process alternatives
$alternatives = get_alternative_files($ref);
foreach ($alternatives as $alternative) {
$pextension = get_extension($alternative, $usesize);
debug("Processing alternative file {$alternative['ref']} for resource $ref, extension: $pextension");
$p = get_resource_path($ref, true, $usesize, false, $pextension, true, 1, $use_watermark, '', $alternative["ref"]);
$target_exists = file_exists($p);
if (
!$target_exists
&& ($useoriginal || in_array("format_chooser", $GLOBALS["plugins"]))
&& resource_download_allowed($ref, '', $collection_resources[$n]['resource_type'], $alternative["ref"], true)
) {
debug("Using original alternative file for alternative file {$alternative['ref']}");
// This size doesn't exist, so we'll try using the original instead
// Always use original if using format chooser as the option is not then available and dynamically generating custom sizes is not supported for alternatives
$p = get_resource_path($ref, true, '', false, $alternative['file_extension'], -1, 1, $use_watermark, '', $alternative["ref"]);
$pextension = $alternative['file_extension'];
$target_exists = file_exists($p);
$usesize = "";
}
debug("Using filepath $p for alternative ref " . $alternative["ref"]);
if ($target_exists) {
$download_filename_format_saved = $GLOBALS["download_filename_format"];
if (strpos($GLOBALS["download_filename_format"], "%alternative") === false) {
// To be safe, add in the alternative ID if not present in configured download filename format or
// it may conflict with the primary resource file
$GLOBALS["download_filename_format"] = str_replace(
".%extension",
"%alternative.%extension",
$GLOBALS["download_filename_format"]
);
}
$filename = get_download_filename($ref, $usesize, $alternative["ref"], $pextension);
$GLOBALS["download_filename_format"] = $download_filename_format_saved;
collection_download_use_original_filenames_when_downloading(
$dl_data,
$filename,
$ref,
$pextension,
$p,
$copy,
);
debug("Adding $p ($filename) for alternative ref " . $alternative["ref"]);
set_processing_message((string) ($n+1 . "/" . $rescount . " " . $GLOBALS["lang"]["filesaddedtozip"]));
if ($collection_download_tar) {
debug("collection_download adding symlink: " . $p . " - " . $usertempdir . DIRECTORY_SEPARATOR . $filename);
$GLOBALS["use_error_exception"] = true;
try {
symlink($p, $usertempdir . DIRECTORY_SEPARATOR . $filename);
} catch (Throwable $e) {
debug("process_collection_download(): Unable to create symlink for ref {$ref},
alternative file {$alternative["ref"]}{$e->getMessage()}");
continue;
}
unset($GLOBALS["use_error_exception"]);
} elseif($archiver) {
$dl_data["includefiles"][] = $p;
} else {
$success = $zip->addFile($p, $filename);
debug('Collection download : Added resource ' . $ref . ' to zip archive = ' . ($success ? 'true' : 'false'));
}
} else {
debug("No file found for alternative ref " . $alternative["ref"]);
}
}
}
collection_download_log_resource_ready($dl_data, $tmpfile, $ref);
}
if (0 < $count_data_only_types) {
collection_download_process_data_only_types($dl_data, $zip);
}
collection_download_process_summary_notes($dl_data, $filename, $zip);
if ($include_csv_file == 'yes') {
collection_download_process_csv_metadata_file($dl_data, $zip);
}
if ($collection_download_tar) {
$suffix = '.tar';
} elseif ($archiver) {
$suffix = '.' . $GLOBALS["collection_download_settings"][$settings_id]['extension'];
} else {
$suffix = '.zip';
}
$filename = "";
collection_download_process_collection_download_name($filename, $collection, $size, $suffix, $collectiondata);
$completed = collection_download_process_archive_command($dl_data, $zip, $filename, $zipfile);
collection_download_clean_temp_files($dl_data['deletion_array']);
db_end_transaction("collection_download");
// Reset global
$exiftool_write_option = $saved_exiftool_write_option;
return [
"filename" => $filename,
"path" => $zipfile,
"completed" => $completed,
];
}