? 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 ""; } else { $headerinsert .= ""; } } /** * 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 don’t change type or parent | 3 | null | not set | null | no | no Save a child FC but don’t 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 don’t 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 = "

"; $htmlbreaksingle = "
"; } 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 = '' . $themename . ''; $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 = '' . escape($collection_name) . ''; $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'] . " "); $notifymessage->append_text($internalmessage); $notifymessage->append_text("

" . $templatevars['message'] . "

"); $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"] . "
" . $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 ""; } /** * 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 ""; } /** * Generates a HTML link for adding a changing the current collection * * @param integer $collection * @return string */ function change_collection_link($collection) { global $lang; return '' . LINK_CARET . $lang["selectcollection"] . ''; } /** * 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"; ?>

">
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 = "" . $collection_name . ""; $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 .= "

\n" . ($templatevars['message'] != "" ? $templatevars['message'] : ""); $body .= "

\n" . $templatevars['link']; if ($passwordtext != "") { $body .= "

\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, ]; }