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

3475 lines
155 KiB
PHP
Executable File

<?php
# Search functions
# Functions to perform searches (read only)
# - For resource indexing / keyword creation, see resource_functions.php
/**
* Resolves the most commonly used keyword that sounds like the given keyword.
*
* This function attempts to find a keyword that phonetically matches the provided keyword
* using the Soundex algorithm. If no Soundex match is found, it will suggest the most commonly
* used keyword that starts with the same first few letters.
*
* @param string $keyword The keyword to resolve.
* @return string|false Returns the matched keyword if found, or false if no match is found.
*/
function resolve_soundex($keyword)
{
global $soundex_suggest_limit;
$soundex = ps_value("SELECT keyword value FROM keyword WHERE soundex = ? AND keyword NOT LIKE '% %' AND hit_count >= ? ORDER BY hit_count DESC LIMIT 1", ["s",soundex($keyword),"i",$soundex_suggest_limit], false);
if (($soundex === false) && (strlen($keyword) >= 4)) {
# No soundex match, suggest words that start with the same first few letters.
return ps_value("SELECT keyword value FROM keyword WHERE keyword LIKE ? AND keyword NOT LIKE '% %' ORDER BY hit_count DESC LIMIT 1", ["s",substr($keyword, 0, 4) . "%"], false);
}
return $soundex;
}
/**
* Suggests search refinements based on common keywords from a set of resource references.
*
* This function analyzes the provided array of resource references and the original search query.
* It identifies common keywords associated with the specified resources and suggests new search queries
* by appending these keywords to the original search query, provided they are not already included in it.
*
* @param array $refs An array of resource references to analyze.
* @param string $search The original search query.
* @return array An array of suggested search refinements. Returns an empty array if no refinements can be suggested.
*/
function suggest_refinement($refs, $search)
{
if (count($refs) == 0) {
return array();
} // Nothing to do, nothing to return
$in = ps_param_insert(count($refs));
$suggest = array();
# find common keywords
$refine = ps_query("SELECT k.keyword,count(k.ref) c FROM resource_node rn LEFT JOIN node n ON n.ref=rn.node LEFT JOIN node_keyword nk ON nk.node=n.ref LEFT JOIN keyword k on nk.keyword=k.ref WHERE rn.resource IN ($in) AND length(k.keyword)>=3 AND length(k.keyword)<=15 AND k.keyword NOT LIKE '%0%' AND k.keyword NOT LIKE '%1%' AND k.keyword NOT LIKE '%2%' AND k.keyword NOT LIKE '%3%' AND k.keyword NOT LIKE '%4%' AND k.keyword NOT LIKE '%5%' AND k.keyword NOT LIKE '%6%' AND k.keyword NOT LIKE '%7%' AND k.keyword NOT LIKE '%8%' AND k.keyword NOT LIKE '%9%' GROUP BY k.keyword ORDER BY c DESC LIMIT 5", ps_param_fill($refs, "i"));
for ($n = 0; $n < count($refine); $n++) {
if (strpos($search, $refine[$n]["keyword"]) === false) {
$suggest[] = $search . " " . $refine[$n]["keyword"];
}
}
return $suggest;
}
/**
* Retrieves a list of fields suitable for advanced searching.
*
* This function queries the database for resource type fields that are marked for advanced searching.
* It checks for visibility based on user permissions and whether the fields are hidden from the search.
* If a designated date field is specified and not already included in the results, it will be added
* to the beginning of the list if it matches the resource types of the other fields.
*
* @param bool $archive Whether to include fields related to archived resources. Defaults to false.
* @param string $hiddenfields A comma-separated string of field references that should be hidden from the search.
* @return array An array of searchable fields that can be used in an advanced search form.
*/
function get_advanced_search_fields($archive = false, $hiddenfields = "")
{
global $FIXED_LIST_FIELD_TYPES, $date_field, $daterange_search;
# Returns a list of fields suitable for advanced searching.
$return = array();
$date_field_already_present = false; # Date field not present in searchable fields array
$date_field_data = null; # If set then this is the date field to be added to searchable fields array
$hiddenfields = explode(",", $hiddenfields);
$fields = ps_query("SELECT " . columns_in("resource_type_field", "f") . ", GROUP_CONCAT(rtfrt.resource_type) resource_types FROM resource_type_field f LEFT JOIN resource_type_field_resource_type rtfrt ON rtfrt.resource_type_field = f.ref WHERE f.advanced_search=1 AND f.active=1 AND (f.keywords_index=1 AND length(f.name)>0) AND (f.global=1 OR rtfrt.resource_type IS NOT NULL) GROUP BY f.ref ORDER BY f.global DESC, f.order_by ASC", [], "schema"); // Constants do not need to be parameters in the prepared statement
# Apply field permissions and check for fields hidden in advanced search
for ($n = 0; $n < count($fields); $n++) {
if (metadata_field_view_access($fields[$n]["ref"]) && !in_array($fields[$n]["ref"], $hiddenfields)) {
$return[] = $fields[$n];
if ($fields[$n]["ref"] == $date_field) {
$date_field_already_present = true;
}
}
}
# If not already in the list of advanced search metadata fields, insert the field which is the designated searchable date ($date_field)
if (
!$date_field_already_present
&& $daterange_search
&& metadata_field_view_access($date_field)
&& !in_array($date_field, $hiddenfields)
) {
$date_field_data = get_resource_type_field($date_field);
if (!is_array($date_field_data) || is_null($date_field_data['ref'])) {
debug("WARNING: Invalid \$date_field specified in config : " . $date_field);
return $return;
}
# Insert searchable date field so that it appears as the first array entry for a given resource type
$return1 = array();
for ($n = 0; $n < count($return); $n++) {
if (
isset($date_field_data)
&& count(array_intersect(explode(",", (string)$return[$n]["resource_types"]), explode(",", (string)$date_field_data['resource_types']))) > 0
) {
$return1[] = $date_field_data;
$date_field_data = null; # Only insert it once
}
$return1[] = $return[$n];
}
# If not yet added because it's resource type differs from everything in the list then add it to the end of the list
if (is_array($date_field_data)) {
$return1[] = $date_field_data;
$date_field_data = null; # Keep things tidy
}
return $return1;
}
# Designated searchable date_field is already present in the lost of advanced search metadata fields }
return $return;
}
/**
* Retrieves a list of fields suitable for advanced searching within collections.
*
* This function constructs an array of fields specifically related to collections, including
* collection title, keywords, and owner. It checks against a list of hidden fields to determine
* which fields should be included in the return array for advanced searching.
*
* @param bool $archive Whether to include fields related to archived collections. Defaults to false.
* @param string $hiddenfields A comma-separated string of field references that should be hidden from the search.
* @return array An array of fields suitable for advanced searching in the context of collections.
*/
function get_advanced_search_collection_fields($archive = false, $hiddenfields = "")
{
$return = array();
$hiddenfields = explode(",", $hiddenfields);
$fields[] = array("ref" => "collection_title", "name" => "collectiontitle", "display_condition" => "", "tooltip_text" => "", "title" => "Title", "type" => 0, "global" => 0, "resource_types" => 'Collections');
$fields[] = array("ref" => "collection_keywords", "name" => "collectionkeywords", "display_condition" => "", "tooltip_text" => "", "title" => "Keywords", "type" => 0, "global" => 0, "resource_types" => 'Collections');
$fields[] = array("ref" => "collection_owner", "name" => "collectionowner", "display_condition" => "", "tooltip_text" => "", "title" => "Owner", "type" => 0, "global" => 0, "resource_types" => 'Collections');
# Apply field permissions and check for fields hidden in advanced search
for ($n = 0; $n < count($fields); $n++) {
if (!in_array($fields[$n]["ref"], $hiddenfields)) {
$return[] = $fields[$n];
}
}
return $return;
}
/**
* Constructs a search query string from the posted search form data.
*
* This function takes the advanced search form fields and assembles them
* into a search query string that can be used for a standard search. It
* processes various input fields, including dates, keywords, and resource IDs,
* while respecting user permissions and field visibility settings.
*
* @param array $fields An array of fields used in the search form.
* @param bool $fromsearchbar Indicates if the search is initiated from a search bar.
* @return string The constructed search query string based on the input data.
*/
function search_form_to_search_query($fields, $fromsearchbar = false)
{
global $auto_order_checkbox,$checkbox_and,$resource_field_verbatim_keyword_regex;
$search = "";
if (getval("basicyear", "") != "") {
if ($search != "") {
$search .= ", ";
}
$search .= "basicyear:" . getval("basicyear", "");
}
if (getval("basicmonth", "") != "") {
if ($search != "") {
$search .= ", ";
}
$search .= "basicmonth:" . getval("basicmonth", "");
}
if (getval("basicday", "") != "") {
if ($search != "") {
$search .= ", ";
}
$search .= "basicday:" . getval("basicday", "");
}
if (getval("startdate", "") != "") {
if ($search != "") {
$search .= ", ";
}
$search .= "startdate:" . getval("startdate", "");
}
if (getval("enddate", "") != "") {
if ($search != "") {
$search .= ", ";
}
$search .= "enddate:" . getval("enddate", "");
}
if (getval("start-y", "") != "") {
if ($search != "") {
$search .= ", ";
}
$search .= "startdate:" . getval("start-y", "");
if (getval("start-m", "") != "") {
$search .= "-" . getval("start-m", "");
if (getval("start-d", "") != "") {
$search .= "-" . getval("start-d", "");
} else {
$search .= "-01";
}
} else {
$search .= "-01-01";
}
}
if (getval("end-y", "") != "") {
if ($search != "") {
$search .= ", ";
}
$search .= "enddate:" . getval("end-y", "");
if (getval("end-m", "") != "") {
$search .= "-" . getval("end-m", "");
if (getval("end-d", "") != "") {
$search .= "-" . getval("end-d", "");
} else {
$search .= "-31";
}
} else {
$search .= "-12-31";
}
}
if (getval("allfields", "") != "") {
if ($search != "") {
$search .= ", ";
}
$search .= join(", ", explode(" ", getval("allfields", ""))); # prepend 'all fields' option
}
if (getval("resourceids", "") != "") {
$listsql = "!list" . join(":", trim_array(split_keywords(getval("resourceids", ""))));
$search = $listsql . " " . $search;
}
$full_text_search = getval(FULLTEXT_SEARCH_PREFIX, "");
if ($full_text_search != "") {
if ($search != "") {
$search .= " ";
}
$full_text_search = str_replace("\"", FULLTEXT_SEARCH_QUOTES_PLACEHOLDER, $full_text_search);
$search .= '"' . FULLTEXT_SEARCH_PREFIX . ':' . $full_text_search . '"';
}
for ($n = 0; $n < count($fields); $n++) {
switch ($fields[$n]["type"]) {
case FIELD_TYPE_TEXT_BOX_MULTI_LINE:
case FIELD_TYPE_TEXT_BOX_LARGE_MULTI_LINE:
case FIELD_TYPE_TEXT_BOX_FORMATTED_AND_TINYMCE:
$name = "field_" . $fields[$n]["ref"];
$value = getval($name, "");
if ($value != "") {
if (
isset($resource_field_verbatim_keyword_regex[$fields[$n]["ref"]])
&& preg_match(
$resource_field_verbatim_keyword_regex[$fields[$n]["ref"]],
str_replace('*', '', $value)
)
) {
// Keyword matches verbatim regex, do not split
$vs = [$value];
} else {
$vs = split_keywords($value, false, false, false, false, true);
}
for ($m = 0; $m < count($vs); $m++) {
if ($search != "") {
$search .= ", ";
}
$search .= ((strpos($vs[$m], "\"") === false) ? $fields[$n]["name"] . ":" . $vs[$m] : "\"" . $fields[$n]["name"] . ":" . substr($vs[$m], 1, -1) . "\""); // Move any quotes around whole field:value element so that they are kept together
}
}
break;
case FIELD_TYPE_DROP_DOWN_LIST: # -------- Dropdowns / check lists
case FIELD_TYPE_CHECK_BOX_LIST:
if ($fields[$n]["display_as_dropdown"]) {
# Process dropdown box
$name = "field_" . $fields[$n]["ref"];
$value = getval($name, "");
if ($value !== "") {
if ($search != "") {
$search .= ", ";
}
$search .= ((strpos($value, " ") === false) ? $fields[$n]["name"] . ":" . $value : "\"" . $fields[$n]["name"] . ":" . substr($value, 1, -1) . "\"");
}
} else {
# Process checkbox list
$options = array();
node_field_options_override($options, $fields[$n]['ref']);
$p = "";
$c = 0;
for ($m = 0; $m < count($options); $m++) {
$name = $fields[$n]["ref"] . "_" . md5($options[$m]);
$value = getval($name, "");
if ($value == "yes") {
$c++;
if ($p != "") {
$p .= ";";
}
$p .= mb_strtolower(i18n_get_translated($options[$m]), 'UTF-8');
}
}
if (($c == count($options) && !$checkbox_and) && (count($options) > 1)) {
# all options ticked - omit from the search (unless using AND matching, or there is only one option intended as a boolean selection)
$p = "";
}
if ($p != "") {
if ($search != "") {
$search .= ", ";
}
if ($checkbox_and) {
$p = str_replace(";", ", {$fields[$n]["name"]}:", $p); // this will force each and condition into a separate union in do_search (which will AND)
if ($search != "") {
$search .= ", ";
}
}
$search .= $fields[$n]["name"] . ":" . $p;
}
}
break;
case FIELD_TYPE_DATE_AND_OPTIONAL_TIME:
case FIELD_TYPE_EXPIRY_DATE:
case FIELD_TYPE_DATE:
case FIELD_TYPE_DATE_RANGE:
$name = "field_" . $fields[$n]["ref"];
$datepart = "";
$value = "";
if (strpos($search, $name . ":") === false) {
// Get each part of the date
$key_year = $name . "-y";
$value_year = getval($key_year, "");
$key_month = $name . "-m";
$value_month = getval($key_month, "");
$key_day = $name . "-d";
$value_day = getval($key_day, "");
// The following constructs full date yyyy-mm-dd or partial dates yyyy-mm or yyyy
// However yyyy-00-dd is interpreted as yyyy because its not a valid partial date
$value_date_final = "";
// Process the valid combinations, otherwise treat it as an empty date
if ($value_year != "" && $value_month != "" && $value_day != "") {
$value_date_final = $value_year . "-" . $value_month . "-" . $value_day;
} elseif ($value_year != "" && $value_month != "") {
$value_date_final = $value_year . "-" . $value_month;
} elseif ($value_year != "") {
$value_date_final = $value_year;
}
if ($value_date_final != "") {
// If search already has value, then attach this value separated by a comma
if ($search != "") {
$search .= ", ";
}
$search .= $fields[$n]["name"] . ":" . $value_date_final;
}
}
if (($date_edtf = getval("field_" . $fields[$n]["ref"] . "_edtf", "")) !== "") {
// We have been passed the range in EDTF format, check it is in the correct format
$rangeregex = "/^(\d{4})(-\d{2})?(-\d{2})?\/(\d{4})(-\d{2})?(-\d{2})?/";
if (!preg_match($rangeregex, $date_edtf, $matches)) {
//ignore this string as it is not a valid EDTF string
continue 2;
}
$rangedates = explode("/", $date_edtf);
$rangestart = str_pad($rangedates[0], 10, "-00");
$rangeendparts = explode("-", $rangedates[1]);
$rangeend = $rangeendparts[0] . "-" . (isset($rangeendparts[1]) ? $rangeendparts[1] : "12") . "-" . (isset($rangeendparts[2]) ? $rangeendparts[2] : "99");
$datepart = "start" . $rangestart . "end" . $rangeend;
} else {
#Date range search - start date
if (getval($name . "_start-y", "") != "") {
$datepart .= "start" . getval($name . "_start-y", "");
if (getval($name . "_start-m", "") != "") {
$datepart .= "-" . getval($name . "_start-m", "");
if (getval($name . "_start-d", "") != "") {
$datepart .= "-" . getval($name . "_start-d", "");
} else {
$datepart .= "";
}
} else {
$datepart .= "";
}
}
#Date range search - end date
if (getval($name . "_end-y", "") != "") {
$datepart .= "end" . getval($name . "_end-y", "");
if (getval($name . "_end-m", "") != "") {
$datepart .= "-" . getval($name . "_end-m", "");
if (getval($name . "_end-d", "") != "") {
$datepart .= "-" . getval($name . "_end-d", "");
} else {
$datepart .= "-31";
}
} else {
$datepart .= "-12-31";
}
}
}
if ($datepart != "") {
if ($search != "") {
$search .= ", ";
}
$search .= $fields[$n]["name"] . ":range" . $datepart;
}
break;
case FIELD_TYPE_TEXT_BOX_SINGLE_LINE: # -------- Text boxes
default:
$value = getval('field_' . $fields[$n]["ref"], '');
if ($value != "") {
if (
isset($resource_field_verbatim_keyword_regex[$fields[$n]["ref"]])
&& preg_match(
$resource_field_verbatim_keyword_regex[$fields[$n]["ref"]],
str_replace('*', '', $value)
)
) {
// Keyword matches verbatim regex, do not split
$valueparts = [$value];
} else {
$valueparts = split_keywords($value, false, false, false, false, true);
}
foreach ($valueparts as $valuepart) {
if ($search != "") {
$search .= ", ";
}
// Move any quotes around whole field:value element so that they are kept together
$search .= (strpos($valuepart, "\"") === false) ? ($fields[$n]["name"] . ":" . $valuepart) : ("\"" . $fields[$n]["name"] . ":" . substr($valuepart, 1, -1) . "\"");
}
}
break;
}
}
##### NODES #####
// Fixed lists will be handled separately as we don't care about the field
// they belong to
$node_ref = '';
foreach (getval('nodes_searched', [], false, 'is_array') as $searchedfield => $searched_field_nodes) {
// Fields that are displayed as a dropdown will only pass one node ID
if (!is_array($searched_field_nodes) && '' == $searched_field_nodes) {
continue;
} elseif (!is_array($searched_field_nodes)) {
$node_ref .= ', ' . NODE_TOKEN_PREFIX . $searched_field_nodes;
continue;
}
$fieldinfo = get_resource_type_field($searchedfield);
// For fields that are displayed as checkboxes
$node_ref .= ', ';
foreach ($searched_field_nodes as $searched_node_ref) {
if ($fieldinfo["type"] == FIELD_TYPE_CHECK_BOX_LIST && $checkbox_and) {
// Split into an additional search element to force a join since this is a separate condition
$node_ref .= ', ';
}
$node_ref .= NODE_TOKEN_PREFIX . $searched_node_ref;
}
}
$search = ('' == $search ? '' : join(', ', split_keywords($search, false, false, false, false, true))) . $node_ref;
##### END OF NODES #####
$propertysearchcodes = array();
global $advanced_search_properties;
foreach ($advanced_search_properties as $advanced_search_property => $code) {
$propval = getval($advanced_search_property, "");
if ($propval != "") {
$propertysearchcodes[] = $code . ":" . $propval;
}
}
if (count($propertysearchcodes) > 0) {
$search = '!properties' . implode(';', $propertysearchcodes) . ' ,' . $search;
} else {
// Allow a single special search to be prepended to the search string. For example, !contributions<user id>
foreach ($_POST as $key => $value) {
if ($key[0] == '!' && strlen($value) > 0) {
$search = $key . $value . ',' . $search;
}
}
}
return $search;
}
/**
* Refines the search string to eliminate duplicates and ensure proper formatting.
*
* This function addresses several issues related to searching, including:
* - Eliminating duplicate terms from the search query.
* - Preserving string search functionality when quotes are used.
* - Formatting date-related keywords correctly.
* - Adjusting keywords for advanced search fields and ensuring they carry over properly.
* - Fixing bugs related to search separators and ensuring valid search syntax.
*
* @param string $search The original search string to be refined.
* @return string The refined search string, with duplicates removed and properly formatted.
*/
function refine_searchstring($search)
{
global $use_refine_searchstring;
if (!$use_refine_searchstring) {
return $search;
}
if (substr($search, 0, 1) == "\"" && substr($search, -1, 1) == "\"") {
return $search;
} // preserve string search functionality.
global $noadd;
$search = str_replace(",-", ", -", $search);
$search = str_replace("\xe2\x80\x8b", "", $search);// remove any zero width spaces.
$keywords = split_keywords($search, false, false, false, false, true);
if (preg_match('/^[^\\s]+\\*/', $search)) {
// No spaces and a wildcard search - don't separate
$keywords = [$search];
} else {
$keywords = split_keywords($search, false, false, false, false, true);
}
$orfields = get_OR_fields(); // leave checkbox type fields alone
$dynamic_keyword_fields = ps_array("SELECT name value FROM resource_type_field where type=9", array(), "schema");
$fixedkeywords = array();
foreach ($keywords as $keyword) {
if (strpos($keyword, "startdate") !== false || strpos($keyword, "enddate") !== false) {
$keyword = str_replace(" ", "-", $keyword);
}
if (strpos($keyword, "!collection") === 0) {
$collection = intval(substr($search, 11));
$keyword = "!collection" . $collection;
}
if (strpos($keyword, ":") > 0) {
$keywordar = explode(":", $keyword, 2);
$keyname = $keywordar[0];
if (substr($keyname, 0, 1) != "!") {
if (substr($keywordar[1], 0, 5) == "range") {
$keywordar[1] = str_replace(" ", "-", $keywordar[1]);
}
if (!in_array($keyname, $orfields)) {
$keyvalues = explode(" ", str_replace($keywordar[0] . ":", "", $keywordar[1]));
} else {
$keyvalues = array($keywordar[1]);
}
foreach ($keyvalues as $keyvalue) {
if (!in_array($keyvalue, $noadd)) {
$fixedkeywords[] = $keyname . ":" . $keyvalue;
}
}
} elseif (!in_array($keyword, $noadd)) {
$keywords = explode(" ", $keyword);
$fixedkeywords[] = $keywords[0];
} // for searches such as !list
} else {
if (!in_array($keyword, $noadd)) {
$fixedkeywords[] = $keyword;
}
}
}
$keywords = $fixedkeywords;
$keywords = array_unique($keywords);
$search = implode(", ", $keywords);
$search = str_replace(",-", " -", $search); // support the omission search
return $search;
}
/**
* Compiles a list of actions based on the provided top actions and search parameters.
*
* This function generates an array of options for various actions that can be performed
* on search results, such as saving searches to collections, saving to dashboards,
* exporting results, editing resources, and running reports. The available actions depend
* on user permissions and specific conditions.
*
* @param bool $top_actions Indicates whether to include top actions in the options.
* @return array An array of action options, each containing value, label, data attributes,
* category, and order for sorting.
*/
function compile_search_actions($top_actions)
{
$options = array();
$o = 0;
global $baseurl,$baseurl_short, $lang, $k, $search, $restypes, $order_by, $archive, $sort, $daylimit, $home_dash, $url,
$allow_smart_collections, $resources_count, $show_searchitemsdiskusage, $offset,
$collection, $usercollection, $internal_share_access, $system_read_only, $search_access;
if (!isset($internal_share_access)) {
$internal_share_access = false;
}
$urlparams = array(
"search" => $search,
"collection" => $collection,
"restypes" => $restypes,
"order_by" => $order_by,
"archive" => $archive,
"access" => $search_access,
"sort" => $sort,
"daylimit" => $daylimit,
"offset" => $offset,
"k" => $k
);
$omit_edit_all = false;
#This is to stop duplicate "Edit all resources" caused on a collection search
if (isset($search) && substr($search, 0, 11) == '!collection') {
$omit_edit_all = true;
}
if (!checkperm('b') && ($k == '' || $internal_share_access)) {
if ($top_actions && $usercollection != $collection) {
$options[$o]['value'] = 'save_search_to_collection';
$options[$o]['label'] = $lang['savethissearchtocollection'];
$data_attribute['url'] = generateURL($baseurl_short . "pages/collections.php", $urlparams, array("addsearch" => $search));
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 70;
$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 && checkPermission_dashcreate()) {
$option_name = 'save_search_to_dash';
$extraparams = array();
$extraparams["create"] = "true";
$extraparams["tltype"] = "srch";
$extraparams["freetext"] = "true";
$data_attribute = array(
'url' => generateURL($baseurl_short . "pages/dash_tile.php", $urlparams, $extraparams),
'link' => str_replace($baseurl, '', (string) $url)
);
if (substr($search, 0, 11) == '!collection') {
$option_name = 'save_collection_to_dash';
$extraparams["promoted_resource"] = "true";
$extraparams["all_users"] = "1";
$extraparams["link"] = $baseurl_short . "pages/search.php?search=!collection" . $collection;
$data_attribute['url'] = generateURL($baseurl_short . "pages/dash_tile.php", $urlparams, $extraparams);
}
$options[$o]['value'] = $option_name;
$options[$o]['label'] = $lang['savethissearchtodash'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_SHARE;
$options[$o]['order_by'] = 170;
$o++;
}
// Save search as Smart Collections
if ($top_actions && $allow_smart_collections && substr($search, 0, 11) != '!collection') {
$extra_tag_attributes = sprintf(
'
data-url="%spages/collections.php?addsmartcollection=%s&restypes=%s&archive=%s"
',
$baseurl_short,
urlencode((string) $search),
urlencode((string) $restypes),
urlencode((string) $archive)
);
$options[$o]['value'] = 'save_search_smart_collection';
$options[$o]['label'] = $lang['savesearchassmartcollection'];
$options[$o]['data_attr'] = array();
$options[$o]['extra_tag_attributes'] = $extra_tag_attributes;
$options[$o]['category'] = ACTIONGROUP_COLLECTION;
$options[$o]['order_by'] = 170;
$o++;
}
if ($resources_count != 0 && !$system_read_only) {
$extra_tag_attributes = sprintf(
'
data-url="%spages/collections.php?addsearch=%s&restypes=%s&order_by=%s&sort=%s&archive=%s&mode=resources&daylimit=%s"
',
$baseurl_short,
urlencode((string) $search),
urlencode((string) $restypes),
urlencode((string) $order_by),
urlencode((string) $sort),
urlencode((string) $archive),
urlencode((string) $daylimit)
);
$options[$o]['value'] = 'save_search_items_to_collection';
$options[$o]['label'] = $lang['savesearchitemstocollection'];
$options[$o]['data_attr'] = array();
$options[$o]['extra_tag_attributes'] = $extra_tag_attributes;
$options[$o]['category'] = ACTIONGROUP_COLLECTION;
$options[$o]['order_by'] = 170;
$o++;
if (0 != $resources_count && $show_searchitemsdiskusage) {
$extra_tag_attributes = sprintf(
'
data-url="%spages/search_disk_usage.php?search=%s&restypes=%s&offset=%s&order_by=%s&sort=%s&archive=%s&daylimit=%s&k=%s"
',
$baseurl_short,
urlencode((string) $search),
urlencode((string) $restypes),
urlencode((string) $offset),
urlencode((string) $order_by),
urlencode((string) $sort),
urlencode((string) $archive),
urlencode((string) $daylimit),
urlencode((string) $k)
);
$options[$o]['value'] = 'search_items_disk_usage';
$options[$o]['label'] = $lang['searchitemsdiskusage'];
$options[$o]['data_attr'] = array();
$options[$o]['extra_tag_attributes'] = $extra_tag_attributes;
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 300;
$o++;
}
}
}
// If all resources are editable, display an edit all link
if ($top_actions && !$omit_edit_all) {
$data_attribute['url'] = generateURL($baseurl_short . "pages/edit.php", $urlparams, array("editsearchresults" => "true", "search_access" => $search_access));
$options[$o]['value'] = 'editsearchresults';
$options[$o]['label'] = $lang['edit_all_resources'];
$options[$o]['data_attr'] = $data_attribute;
$options[$o]['category'] = ACTIONGROUP_EDIT;
$options[$o]['order_by'] = 130;
$o++;
}
if ($top_actions && ($k == '' || $internal_share_access)) {
$options[$o]['value'] = 'csv_export_results_metadata';
$options[$o]['label'] = $lang['csvExportResultsMetadata'];
$options[$o]['data_attr']['url'] = sprintf(
'%spages/csv_export_results_metadata.php?search=%s&restypes=%s&order_by=%s&archive=%s&sort=%s&access=%s',
$baseurl_short,
urlencode((string) $search),
urlencode((string) $restypes),
urlencode((string) $order_by),
urlencode((string) $archive),
urlencode((string) $sort),
urlencode((string) $search_access)
);
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 290;
$o++;
}
// Run report on search results
if ($top_actions && checkperm('t')) {
$backurl_to_search = generateURL("{$baseurl_short}pages/search.php", get_search_params(), $urlparams);
$options[$o]['value'] = 'run_report_on_search_results';
$options[$o]['label'] = $lang['run_report_on_search_results'];
$options[$o]['data_attr']['url'] = generateURL("{$baseurl_short}pages/team/team_report.php", ['backurl' => $backurl_to_search]);
$options[$o]['category'] = ACTIONGROUP_ADVANCED;
$options[$o]['order_by'] = 280;
$o++;
}
// Add extra search actions or modify existing options through plugins
$modified_options = hook('render_search_actions_add_option', '', array($options, $urlparams));
if ($top_actions && !empty($modified_options)) {
$options = $modified_options;
}
return $options;
}
/**
* Constructs a SQL filter based on the provided search parameters.
*
* This function generates a prepared statement query that can be used to filter search results
* based on various criteria, including archive status, resource types, user permissions, and more.
* The function also takes into account user-specific access rights and other configurations
* to ensure that the returned resources meet the necessary visibility and editability criteria.
*
* @param string $search The search query string.
* @param mixed $archive Archive states to filter by (can be a comma-separated string).
* @param string $restypes Resource types to include in the search.
* @param int $recent_search_daylimit Limit for filtering recent searches by creation date.
* @param mixed $access_override If set, overrides access restrictions.
* @param bool $return_disk_usage Indicates whether to include disk usage information.
* @param bool $editable_only If true, only returns resources that are editable by the user.
* @param int|null $access The specific access level to filter by (if applicable).
* @param bool $smartsearch If true, enables smart search features.
* @return PreparedStatementQuery A prepared statement object containing the SQL query and parameters.
*/
function search_filter($search, $archive, $restypes, $recent_search_daylimit, $access_override, $return_disk_usage, $editable_only = false, $access = null, $smartsearch = false)
{
debug_function_call("search_filter", func_get_args());
global $userref,$userpermissions,$resource_created_by_filter,$uploader_view_override,$edit_access_for_contributor,$additional_archive_states,$heightmin,
$search_all_workflow_states,$collections_omit_archived,$k,$collection_allow_not_approved_share,$archive_standard;
if (hook("modifyuserpermissions")) {
$userpermissions = hook("modifyuserpermissions");
}
$userpermissions = (isset($userpermissions)) ? $userpermissions : array();
# Convert the provided search parameters into appropriate SQL, ready for inclusion in the do_search() search query.
if (!is_array($archive)) {
$archive = explode(",", $archive);
}
$archive = array_filter($archive, function ($state) {
return (string)(int)$state == (string)$state;
}); // remove non-numeric values
$sql_filter = new PreparedStatementQuery();
# Apply resource types
if (($restypes != "") && (substr($restypes, 0, 6) != "Global") && substr($search, 0, 11) != '!collection') {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$restypes_x = explode(",", $restypes);
$sql_filter->sql .= "resource_type IN (" . ps_param_insert(count($restypes_x)) . ")";
$sql_filter->parameters = array_merge($sql_filter->parameters, ps_param_fill($restypes_x, "i"));
}
# Apply day limit
if ('' != $recent_search_daylimit && is_numeric($recent_search_daylimit)) {
if ('' != $sql_filter->sql) {
$sql_filter->sql .= ' AND ';
}
$sql_filter->sql .= "creation_date > (curdate() - interval ? DAY)";
$sql_filter->parameters = array_merge($sql_filter->parameters, ["i",$recent_search_daylimit]);
}
# The ability to restrict access by the user that created the resource.
if (isset($resource_created_by_filter) && count($resource_created_by_filter) > 0) {
$created_filter = "";
$created_filter_params = [];
foreach ($resource_created_by_filter as $filter_user) {
if ($filter_user == -1) {
$filter_user = $userref;
} # '-1' can be used as an alias to the current user. I.e. they can only see their own resources in search results.
if ($created_filter != "") {
$created_filter .= " OR ";
}
$created_filter .= "created_by = ?";
$created_filter_params[] = "i";
$created_filter_params[] = $filter_user;
}
if ($created_filter != "") {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "(" . $created_filter . ")";
$sql_filter->parameters = array_merge($sql_filter->parameters, $created_filter_params);
}
}
# append resource type restrictions based on 'T' permission
# look for all 'T' permissions and append to the SQL filter.
$rtfilter = array();
for ($n = 0; $n < count($userpermissions); $n++) {
if (substr($userpermissions[$n], 0, 1) == "T") {
$rt = substr($userpermissions[$n], 1);
if (is_numeric($rt) && !$access_override) {
$rtfilter[] = $rt;
}
}
}
if (count($rtfilter) > 0) {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "resource_type NOT IN (" . ps_param_insert(count($rtfilter)) . ")";
$sql_filter->parameters = array_merge($sql_filter->parameters, ps_param_fill($rtfilter, "i"));
}
# append "use" access rights, do not show confidential resources unless admin
if (!checkperm("v") && !$access_override) {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
# Check both the resource access, but if confidential is returned, also look at the joined user-specific or group-specific custom access for rows.
$sql_filter->sql .= "(r.access<>'2' OR (r.access=2 AND ((rca.access IS NOT null AND rca.access<>2) OR (rca2.access IS NOT null AND rca2.access<>2))))";
}
# append standard archive searching criteria. Updated Jan 2016 to apply to collections as resources in a pending state that are in a shared collection could bypass approval process
if (!$access_override) {
if (substr($search, 0, 11) == "!collection" || substr($search, 0, 5) == "!list" || substr($search, 0, 15) == "!archivepending" || substr($search, 0, 12) == "!userpending") {
# Resources in a collection or list may be in any archive state
# Other special searches define the archive state in search_special()
if (substr($search, 0, 11) == "!collection" && $collections_omit_archived && !checkperm("e2")) {
$sql_filter->sql .= (($sql_filter->sql != "") ? " AND " : "") . "archive<>2";
}
} elseif ($search_all_workflow_states || substr($search, 0, 8) == "!related" || substr($search, 0, 8) == "!hasdata" || strpos($search, "integrityfail") !== false) {
hook("search_all_workflow_states_filter", "", [$sql_filter]);
} elseif (count($archive) == 0 || $archive_standard && !$smartsearch) {
# If no archive specified add in default archive states (set by config options or as set in rse_workflow plugin)
# Defaults are not used if searching smartsearch collection, actual values will be used instead
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$defaultsearchstates = get_default_search_states();
if (count($defaultsearchstates) == 0) {
// Make sure we have at least one state - system has been misconfigured
$defaultsearchstates[] = 0;
}
$sql_filter->sql .= "archive IN (" . ps_param_insert(count($defaultsearchstates)) . ")";
$sql_filter->parameters = array_merge($sql_filter->parameters, ps_param_fill($defaultsearchstates, "i"));
} else {
# Append normal filtering - extended as advanced search now allows searching by archive state
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "archive IN (" . ps_param_insert(count($archive)) . ")";
$sql_filter->parameters = array_merge($sql_filter->parameters, ps_param_fill($archive, "i"));
}
if (!checkperm("v") && !(substr($search, 0, 11) == "!collection" && $k != '' && $collection_allow_not_approved_share)) {
// Append standard filtering to hide resources in a pending state, whatever the search
// except when the resource is of a type that the user has ert permission for
$rtexclusions = "";
$rtexclusions_params = [];
for ($n = 0; $n < count($userpermissions); $n++) {
if (substr($userpermissions[$n], 0, 3) == "ert") {
$rt = substr($userpermissions[$n], 3);
if (is_int_loose($rt)) {
$rtexclusions .= " OR (resource_type = ?)";
array_push($rtexclusions_params, "i", $rt);
}
}
}
$sql_filter->sql .= " AND (((r.archive<>-2 OR r.created_by = ?) AND (r.archive<>-1 OR r.created_by = ?)) " . $rtexclusions . ")";
$sql_filter->parameters = array_merge($sql_filter->parameters, ["i",$userref,"i",$userref], $rtexclusions_params);
unset($rtexclusions);
}
}
# Add code to filter out resoures in archive states that the user does not have access to due to a 'z' permission
$filterblockstates = [];
for ($n = -2; $n <= 3; $n++) {
if (checkperm("z" . $n) && !$access_override) {
$filterblockstates[] = $n;
}
}
foreach ($additional_archive_states as $additional_archive_state) {
if (checkperm("z" . $additional_archive_state)) {
$filterblockstates[] = $additional_archive_state;
}
}
if (count($filterblockstates) > 0 && !$access_override) {
if ($uploader_view_override) {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "(archive NOT IN (" . ps_param_insert(count($filterblockstates)) . ") OR created_by = ?)";
$sql_filter->parameters = array_merge($sql_filter->parameters, ps_param_fill($filterblockstates, "i"));
$sql_filter->parameters[] = "i";
$sql_filter->parameters[] = $userref;
} else {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "archive NOT IN (" . ps_param_insert(count($filterblockstates)) . ")";
$sql_filter->parameters = array_merge($sql_filter->parameters, ps_param_fill($filterblockstates, "i"));
}
}
# Append media restrictions
if ($heightmin != '') {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "dim.height>= ? ";
$sql_filter->parameters[] = "i";
$sql_filter->parameters[] = $heightmin;
}
# append ref filter - never return the batch upload template (negative refs)
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= "r.ref>0";
// Only users with v perm can search for resources with a specific access
if (checkperm("v") && !is_null($access) && is_numeric($access)) {
$sql_filter->sql .= (trim($sql_filter->sql) != "" ? " AND " : "");
$sql_filter->sql .= "r.access = ?";
$sql_filter->parameters[] = "i";
$sql_filter->parameters[] = $access;
}
// Append filter if only searching for editable resources
if ($editable_only) {
$editable_filter = new PreparedStatementQuery();
if (!checkperm("v") && !$access_override) {
// following condition added 2020-03-02 so that resources without an entry in the resource_custom_access table are included in the search results - "OR (rca.access IS NULL AND rca2.access IS NULL)"
$editable_filter->sql .= "(r.access <> 1 OR (r.access = 1 AND ((rca.access IS NOT null AND rca.access <> 1) OR (rca2.access IS NOT null AND rca2.access <> 1) OR (rca.access IS NULL AND rca2.access IS NULL)))) ";
}
# Construct resource type exclusion based on 'ert' permission
# look for all 'ert' permissions and append to the exclusion array.
$rtexclusions = array();
for ($n = 0; $n < count($userpermissions); $n++) {
if (substr($userpermissions[$n], 0, 3) == "ert") {
$rt = substr($userpermissions[$n], 3);
if (is_numeric($rt)) {
$rtexclusions[] = $rt;
}
}
}
$blockeditstates = array();
for ($n = -2; $n <= 3; $n++) {
if (!checkperm("e" . $n)) {
$blockeditstates[] = $n;
}
}
foreach ($additional_archive_states as $additional_archive_state) {
if (!checkperm("e" . $n)) {
$blockeditstates[] = $n;
}
}
// Add code to hide resources in archive<0 unless has 't' permission, resource has been contributed by user or has ert permission
if (!checkperm("t")) {
if ($editable_filter->sql != "") {
$editable_filter->sql .= " AND ";
}
$editable_filter->sql .= "(archive NOT IN (-2,-1) OR (created_by = ?";
$editable_filter->parameters = ["i",$userref];
if (count($rtexclusions) > 0) {
$editable_filter->sql .= " OR resource_type IN (" . ps_param_insert(count($rtexclusions)) . ")";
$editable_filter->parameters = array_merge($editable_filter->parameters, ps_param_fill($rtexclusions, "i"));
}
$editable_filter->sql .= "))";
}
if (count($blockeditstates) > 0) {
$blockeditoverride = "";
$blockeditoverride_params = [];
global $userref;
if ($edit_access_for_contributor) {
$blockeditoverride .= " created_by = ?";
$blockeditoverride_params[] = "i";
$blockeditoverride_params[] = $userref;
}
if (count($rtexclusions) > 0) {
if ($blockeditoverride != "") {
$blockeditoverride .= " AND ";
}
$blockeditoverride .= "resource_type IN (" . ps_param_insert(count($rtexclusions)) . ")";
$blockeditoverride_params = array_merge($blockeditoverride_params, ps_param_fill($rtexclusions, "i"));
}
if ($editable_filter->sql != "") {
$editable_filter->sql .= " AND ";
}
$editable_filter->sql .= "(archive NOT IN (" . ps_param_insert(count($blockeditstates)) . ")";
$editable_filter->parameters = array_merge($editable_filter->parameters, ps_param_fill($blockeditstates, "i"));
if ($blockeditoverride != "") {
$editable_filter->sql .= " OR " . $blockeditoverride;
$editable_filter->parameters = array_merge($editable_filter->parameters, $blockeditoverride_params);
}
$editable_filter->sql .= ")";
}
// Check for blocked/allowed resource types
$allrestypes = get_resource_types("", false, false, true);
$blockedrestypes = array();
foreach ($allrestypes as $restype) {
if (checkperm("XE" . $restype["ref"])) {
$blockedrestypes[] = $restype["ref"];
}
}
if (checkperm("XE")) {
$okrestypes = array();
$okrestypesor = "";
$okrestypesorparams = [];
foreach ($allrestypes as $restype) {
if (checkperm("XE-" . $restype["ref"])) {
$okrestypes[] = $restype["ref"];
}
}
if (count($okrestypes) > 0) {
if ($editable_filter->sql != "") {
$editable_filter->sql .= " AND ";
}
if ($edit_access_for_contributor) {
$okrestypesor .= " created_by = ?";
$okrestypesorparams = ["i",$userref];
}
$editable_filter->sql .= "(resource_type IN (" . ps_param_insert(count($okrestypes)) . ")" . (($okrestypesor != "") ? " OR " . $okrestypesor : "") . ")";
$editable_filter->parameters = array_merge($editable_filter->parameters, ps_param_fill($okrestypes, "i"), $okrestypesorparams);
} else {
if ($editable_filter->sql != "") {
$editable_filter->sql .= " AND ";
}
$editable_filter->sql .= " 0=1";
}
}
if (count($blockedrestypes) > 0) {
$blockrestypesor = "";
$blockrestypesorparams = [];
if ($edit_access_for_contributor) {
$blockrestypesor .= " created_by = ?";
$blockrestypesorparams = ["i",$userref];
}
if ($editable_filter->sql != "") {
$editable_filter->sql .= " AND ";
}
$editable_filter->sql .= "(resource_type NOT IN (" . ps_param_insert(count($blockedrestypes)) . ")" . (($blockrestypesor != "") ? " OR " . $blockrestypesor : "") . ")";
$editable_filter->parameters = array_merge($editable_filter->parameters, ps_param_fill($blockedrestypes, "i"), $blockrestypesorparams);
}
$updated_editable_filter = hook("modifysearcheditable", "", array($editable_filter,$userref));
if ($updated_editable_filter !== false) {
$editable_filter = $updated_editable_filter;
}
if ($editable_filter->sql != "") {
if ($sql_filter->sql != "") {
$sql_filter->sql .= " AND ";
}
$sql_filter->sql .= $editable_filter->sql;
$sql_filter->parameters = array_merge($sql_filter->parameters, $editable_filter->parameters);
}
}
return $sql_filter;
}
/**
* Processes special searches and constructs a corresponding SQL query.
*
* This function handles various special search commands (like viewing the last resources,
* resources with no downloads, duplicates, collections, etc.) and creates a prepared statement
* for the query that retrieves the desired resources based on the search parameters.
* It also incorporates user permissions and other configurations into the search logic.
*
* @param string $search The search string indicating the type of special search.
* @param PreparedStatementQuery $sql_join The SQL JOIN query to be applied.
* @param int $fetchrows The number of rows to fetch.
* @param string $sql_prefix The prefix for the SQL query.
* @param string $sql_suffix The suffix for the SQL query.
* @param string $order_by The order by clause for sorting the results.
* @param string $orig_order The original order specified by the user.
* @param PreparedStatementQuery $select The fields to select in the query.
* @param PreparedStatementQuery $sql_filter The SQL WHERE filter to apply.
* @param mixed $archive Archive states to filter by.
* @param bool $return_disk_usage Indicates whether to return disk usage information.
* @param bool $return_refs_only If true, returns only resource references.
* @param bool $returnsql If true, returns the constructed SQL query instead of executing it.
* @return mixed The results of the special search or false if no special search was matched.
*/
function search_special($search, $sql_join, $fetchrows, $sql_prefix, $sql_suffix, $order_by, $orig_order, $select, $sql_filter, $archive, $return_disk_usage, $return_refs_only = false, $returnsql = false)
{
# Process special searches. These return early with results.
global $FIXED_LIST_FIELD_TYPES, $lang, $k, $USER_SELECTION_COLLECTION, $date_field;
global $allow_smart_collections, $smart_collections_async;
global $config_search_for_number,$userref;
setup_search_chunks($fetchrows, $chunk_offset, $search_chunk_size);
// Don't cache special searches by default as often used for special purposes
// e.g. collection count to determine edit accesss
$b_cache_count = false;
if (!is_a($sql_join, "PreparedStatementQuery") && trim($sql_join == "")) {
$sql_join = new PreparedStatementQuery();
}
if (!is_a($sql_filter, "PreparedStatementQuery") && trim($sql_filter == "")) {
$sql_filter = new PreparedStatementQuery();
}
$sql = new PreparedStatementQuery();
# View Last
if (substr($search, 0, 5) == "!last") {
# Replace r2.ref with r.ref for the alternative query used here.
$order_by = str_replace("r.ref", "r2.ref", $order_by);
if ($orig_order == "relevance") {
# Special case for ordering by relevance for this query.
$direction = ((strpos($order_by, "DESC") === false) ? "ASC" : "DESC");
$order_by = "r2.ref " . $direction;
}
# add date field, if access allowed, for use in $order_by
if (metadata_field_view_access($date_field) && strpos($select->sql, "field" . $date_field) === false) {
$select->sql .= ", field{$date_field} ";
}
# Extract the number of records to produce
$last = explode(",", $search);
$last = str_replace("!last", "", $last[0]);
# !Last must be followed by an integer. SQL injection filter.
if (is_int_loose($last)) {
$last = (int)$last;
} else {
$last = 1000;
$search = "!last1000";
}
# Fix the ORDER BY for this query (special case due to inner query)
$order_by = str_replace("r.rating", "rating", $order_by);
$sql->sql = $sql_prefix . "SELECT DISTINCT *,r2.total_hit_count score FROM (SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE " . $sql_filter->sql . " ORDER BY ref DESC LIMIT $last ) r2 [ORDER_BY_SQL] " . $sql_suffix;
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (substr($search, 0, 12) == "!nodownloads") {
// View Resources With No Downloads
if ($orig_order == "relevance") {
$order_by = "ref DESC";
}
$select->sql = "r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE " . $sql_filter->sql . " AND r.ref NOT IN (SELECT DISTINCT object_ref FROM daily_stat WHERE activity_type='Resource download') [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (substr($search, 0, 11) == "!duplicates") {
// Duplicate Resources (based on file_checksum)
// Extract the resource ID
$ref = explode(" ", $search);
$ref = str_replace("!duplicates", "", $ref[0]);
$ref = explode(",", $ref); // just get the number
$ref = $ref[0];
if ($ref != "") {
# Find duplicates of a given resource
if (is_int_loose($ref)) {
$sql->sql = sprintf(
"SELECT [SELECT_SQL]
FROM resource r %s
WHERE %s
AND file_checksum <> ''
AND file_checksum IS NOT NULL
AND file_checksum = (
SELECT file_checksum
FROM resource
WHERE ref= ?
AND (file_checksum <> '' AND file_checksum IS NOT NULL)
)
[GROUP_BY_SQL]
[ORDER_BY_SQL]",
$sql_join->sql,
$sql_filter->sql
);
$order_by = "file_checksum, ref";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters, ["i",$ref]);
} else {
// Given resource is not a valid identifier
return [];
}
} else {
// Find all duplicate resources
$order_by = "file_checksum, ref";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE " . $sql_filter->sql . " AND file_checksum IN (SELECT file_checksum FROM (SELECT file_checksum FROM resource WHERE file_checksum <> '' AND file_checksum IS NOT null GROUP BY file_checksum having count(file_checksum)>1)r2) [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
}
$select->sql = "r.hit_count score, {$select->sql}";
} elseif (substr($search, 0, 11) == '!collection') {
# View Collection
global $userref,$ignore_collection_access;
$colcustperm = $sql_join;
# Extract the collection number
$collection = explode(' ', $search);
$collection = str_replace('!collection', '', $collection[0]);
$collection = explode(',', $collection); // just get the number
$collection = (int)$collection[0];
if (!checkperm('a')) {
# Check access
$validcollections = [];
if (upload_share_active() !== false) {
$validcollections = get_session_collections(get_rs_session_id(), $userref);
} else {
$user_collections = array_column(get_user_collections($userref, "", "name", "ASC", -1, false), "ref");
$public_collections = array_column(search_public_collections('', 'name', 'ASC', true, false), 'ref');
# include collections of requested resources
$request_collections = array();
if (checkperm("R")) {
include_once 'request_functions.php';
$request_collections = array_column(get_requests(), 'collection');
$externally_requested_collections = array_column(ps_query('SELECT ref FROM collection WHERE user = -2'), 'ref');
$request_collections = array_merge($request_collections, $externally_requested_collections);
}
# include collections of research resources
$research_collections = array();
if (checkperm("r")) {
include_once 'research_functions.php';
$research_collections = array_column(get_research_requests(), 'collection');
}
$validcollections = array_unique(array_merge($user_collections, array($USER_SELECTION_COLLECTION), $public_collections, $request_collections, $research_collections));
}
// Attach the negated user reference special collection
$validcollections[] = (0 - $userref);
if (in_array($collection, $validcollections) || (in_array($collection, array_column(get_all_featured_collections(), 'ref')) && featured_collection_check_access_control($collection)) || $ignore_collection_access) {
if (!collection_readable($collection)) {
return array();
}
} elseif ($k == "" || upload_share_active() !== false) {
return [];
}
}
if ($allow_smart_collections) {
global $smartsearch_ref_cache;
if (isset($smartsearch_ref_cache[$collection])) {
$smartsearch_ref = $smartsearch_ref_cache[$collection]; // this value is pretty much constant
} else {
$smartsearch_ref = ps_value('SELECT savedsearch value FROM collection WHERE ref = ?', ['i',$collection], '');
$smartsearch_ref_cache[$collection] = $smartsearch_ref;
}
global $php_path;
if ($smartsearch_ref != '' && !$return_disk_usage) {
if ($smart_collections_async && isset($php_path) && file_exists($php_path . '/php')) {
exec($php_path . '/php ' . dirname(__FILE__) . '/../pages/ajax/update_smart_collection.php ' . escapeshellarg($smartsearch_ref) . ' ' . '> /dev/null 2>&1 &');
} else {
update_smart_collection($smartsearch_ref);
}
}
}
$select->sql = "DISTINCT c.date_added,c.comment,r.hit_count score,length(c.comment) commentset, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r JOIN collection_resource c ON r.ref=c.resource " . $colcustperm->sql . " WHERE c.collection = ? AND (" . $sql_filter->sql . ") [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $colcustperm->parameters, ["i",$collection], $sql_filter->parameters);
$collectionsearchsql = hook('modifycollectionsearchsql', '', array($sql));
if ($collectionsearchsql) {
$sql = $collectionsearchsql;
}
} elseif (substr($search, 0, 14) == "!relatedpushed") {
# View Related - Pushed Metadata (for the view page)
# Extract the resource number
$resource = explode(" ", $search);
$resource = str_replace("!relatedpushed", "", $resource[0]);
if (isset($GLOBALS["related_pushed_order_by"])) {
if (is_int_loose($GLOBALS["related_pushed_order_by"])) {
if (metadata_field_view_access($GLOBALS["related_pushed_order_by"])) {
$order_by = set_search_order_by($search, "field" . $GLOBALS["related_pushed_order_by"], "ASC");
}
} else {
$order_by = set_search_order_by($search, $GLOBALS["related_pushed_order_by"], "ASC");
}
}
$order_by = str_replace("r.", "", $order_by); # UNION below doesn't like table aliases in the ORDER BY.
$select->sql = "DISTINCT r.hit_count score, rt.name resource_type_name, {$select->sql}";
$relatedselect = $sql_prefix . "
SELECT [SELECT_SQL]
FROM resource r
JOIN resource_type rt ON r.resource_type=rt.ref AND rt.push_metadata=1
JOIN resource_related t ON (%s) "
. $sql_join->sql
. " WHERE 1=1 AND " . $sql_filter->sql
. " [GROUP_BY_SQL]";
$sql->sql = sprintf($relatedselect, "t.related=r.ref AND t.resource = ?")
. " UNION "
. sprintf($relatedselect, "t.resource=r.ref AND t.related= ?")
. " [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge(
$select->parameters,
["i",$resource],
$sql_join->parameters,
$sql_filter->parameters,
$select->parameters,
["i",$resource],
$sql_join->parameters,
$sql_filter->parameters
);
} elseif (substr($search, 0, 8) == "!related") {
# View Related
# Extract the resource number
$resource = explode(" ", $search);
$resource = str_replace("!related", "", $resource[0]);
$order_by = str_replace("r.", "", $order_by); # UNION below doesn't like table aliases in the ORDER BY.
global $pagename, $related_search_show_self;
$sql_self = new PreparedStatementQuery();
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
if ($related_search_show_self && ($pagename == 'search' || $pagename == 'collections')) {
$sql_self->sql = " SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE r.ref = ? AND " . $sql_filter->sql . " GROUP BY r.ref UNION ";
$sql_self->parameters = array_merge($select->parameters, $sql_join->parameters, ["i",$resource], $sql_filter->parameters);
}
$sql->sql = $sql_prefix . $sql_self->sql . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " JOIN resource_related t ON (t.related = r.ref AND t.resource = ?) WHERE " . $sql_filter->sql . " [GROUP_BY_SQL]
UNION
SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " JOIN resource_related t ON (t.resource = r.ref AND t.related = ?) WHERE " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($sql_self->parameters, $select->parameters, $sql_join->parameters, ["i", $resource], $sql_filter->parameters, $select->parameters, $sql_join->parameters, ["i", $resource], $sql_filter->parameters);
} elseif (substr($search, 0, 4) == "!geo") {
# Geographic search
$geo = explode("t", str_replace(array("m","p"), array("-","."), substr($search, 4))); # Specially encoded string to avoid keyword splitting
if (!isset($geo[0]) || empty($geo[0]) || !isset($geo[1]) || empty($geo[1])) {
exit($lang["geographicsearchmissing"]);
}
$bl = explode("b", $geo[0]);
$tr = explode("b", $geo[1]);
$select->sql = "r.hit_count score, {$select->sql}";
$sql->sql = "SELECT [SELECT_SQL]
FROM resource r " . $sql_join->sql .
"WHERE geo_lat > ? AND geo_lat < ? " .
"AND geo_long > ? AND geo_long < ?
AND " . $sql_filter->sql .
" [GROUP_BY_SQL] [ORDER_BY_SQL]";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, ["d",$bl[0],"d",$tr[0],"d",$bl[1],"d",$tr[1]], $sql_filter->parameters);
$sql->sql = $sql_prefix . $sql->sql . $sql_suffix;
} elseif (substr($search, 0, 10) == "!colourkey") {
# Similar to a colour by key
# Extract the colour key
$colourkey = explode(" ", $search);
$colourkey = str_replace("!colourkey", "", $colourkey[0]);
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql = new PreparedStatementQuery();
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE has_image > 0 AND LEFT(colour_key,4) = ? AND " . $sql_filter->sql . " [GROUP_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, ["s",$colourkey], $sql_filter->parameters);
} elseif (substr($search, 0, 7) == "!colour") {
# Colour search
$colour = explode(" ", $search);
$colour = str_replace("!colour", "", $colour[0]);
$select->sql = "r.hit_count score, {$select->sql}";
$sql = new PreparedStatementQuery();
$sql->sql = "SELECT [SELECT_SQL]
FROM resource r " . $sql_join->sql .
" WHERE colour_key LIKE ? " .
"OR colour_key LIKE ? " .
"AND " . $sql_filter->sql .
" [GROUP_BY_SQL] [ORDER_BY_SQL]";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, ["s",$colour . "%","s","_" . $colour . "%"], $sql_filter->parameters);
$searchsql = $sql_prefix . $sql->sql . $sql_suffix;
$sql->sql = $searchsql;
} elseif (substr($search, 0, 4) == "!rgb") {
// Similar to a colour
$rgb = explode(":", $search);
$rgb = explode(",", $rgb[1]);
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$searchsql = new PreparedStatementQuery();
$searchsql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE has_image > 0 AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL]" . $sql_suffix;
$order_by = "(abs(image_red - ?)+abs(image_green - ?)+abs(image_blue - ?)) ASC";
$order_by_params = ["i",$rgb[0],"i",$rgb[1],"i",$rgb[2]];
$hardlimit = 500;
$searchsql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
$sql = $searchsql;
} elseif (substr($search, 0, 10) == "!nopreview") {
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql = new PreparedStatementQuery();
$sql->sql = $sql_prefix .
"SELECT [SELECT_SQL]
FROM resource r
$sql_join->sql
WHERE has_image=0
AND {$sql_filter->sql}
[GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (($config_search_for_number && is_numeric($search)) || substr($search, 0, 9) == "!resource") {
$searchref = preg_replace("/[^0-9]/", "", $search);
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE r.ref = ? AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL]" . $sql_suffix;
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, ["i",$searchref], $sql_filter->parameters);
} elseif (substr($search, 0, 15) == "!archivepending") {
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE r.archive=1 AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (substr($search, 0, 12) == "!userpending") {
if ($orig_order == "rating") {
$order_by = "request_count DESC," . $order_by;
}
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE r.archive=-1
AND {$sql_filter->sql} [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (substr($search, 0, 14) == "!contributions") {
global $userref;
# Extract the user ref
$cuser = explode(" ", $search);
$cuser = str_replace("!contributions", "", $cuser[0]);
// Don't filter if user is searching for their own resources and $open_access_for_contributor=true;
global $open_access_for_contributor;
if ($open_access_for_contributor && $userref == $cuser) {
$sql_filter->sql = "archive IN (" . ps_param_insert(count($archive)) . ")";
$sql_filter->parameters = ps_param_fill($archive, "i");
$sql_join->sql = " JOIN resource_type AS rty ON r.resource_type = rty.ref ";
$sql_join->parameters = array();
// Remove reference to custom access
$select->sql = str_replace(["rca.access", "rca2.access"], "0", $select->sql);
}
$select->sql = "DISTINCT r.hit_count score, " . str_replace(",rca.access group_access,rca2.access user_access ", ",null group_access, null user_access ", $select->sql);
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE created_by = ? AND r.ref > 0 AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, ["i", $cuser], $sql_filter->parameters);
} elseif ($search == "!images") {
// Search for resources with images
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE has_image>0 AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (substr($search, 0, 7) == "!unused") {
// Search for resources not used in any collections
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix;
$sql->sql .= sprintf(
"SELECT [SELECT_SQL]
FROM resource r %s
WHERE r.ref>0
AND r.ref NOT IN (SELECT c.resource FROM collection_resource c)
AND %s
[GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}",
$sql_join->sql,
$sql_filter->sql
);
$sql->sql .= $sql_suffix;
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (substr($search, 0, 5) == "!list") {
// Search for a list of resources
// !listall = archive state is not applied as a filter to the list of resources.
$resources = explode(" ", $search);
if (substr($search, 0, 8) == "!listall") {
$resources = str_replace("!listall", "", $resources[0]);
} else {
$resources = str_replace("!list", "", $resources[0]);
}
$resources = explode(",", $resources); // Separate out any additional keywords
$resources = array_filter(explode(":", $resources[0]), "is_int_loose");
$listsql = new PreparedStatementQuery();
if (count($resources) == 0) {
$listsql->sql = " WHERE r.ref IS NULL";
$listsql->parameters = [];
} else {
$listsql->sql = " WHERE r.ref IN (" . ps_param_insert(count($resources)) . ")";
$listsql->parameters = ps_param_fill($resources, "i");
}
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix;
$sql->sql .= sprintf(
"SELECT [SELECT_SQL]
FROM resource r %s%s
AND %s
[GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}",
$sql_join->sql,
$listsql->sql,
$sql_filter->sql,
);
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $listsql->parameters, $sql_filter->parameters);
} elseif (substr($search, 0, 8) == "!hasdata") {
// View resources that have data in the specified field reference - useful if deleting unused fields
$fieldref = intval(trim(substr($search, 8)));
$sql_join->sql .= " RIGHT JOIN resource_node rn ON r.ref=rn.resource JOIN node n ON n.ref=rn.node WHERE n.resource_type_field = ?";
array_push($sql_join->parameters, "i", $fieldref);
// Cache this as it is a very slow query
$b_cache_count = true;
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (substr($search, 0, 11) == "!properties") {
// Search for resource properties
// Note: in order to combine special searches with normal searches, these are separated by space (" ")
$searches_array = explode(' ', $search);
$properties = explode(';', substr($searches_array[0], 11));
// Use a new variable to ensure nothing changes $sql_filter unless this is a valid property search
$propertiessql = new PreparedStatementQuery();
foreach ($properties as $property) {
$propertycheck = explode(":", $property);
if (count($propertycheck) == 2) {
$propertyname = $propertycheck[0];
$propertyval = $propertycheck[1];
$sql_filter_properties_and = $propertiessql->sql != "" ? " AND " : "";
switch ($propertyname) {
case "hmin":
$propertiessql->sql .= $sql_filter_properties_and . " rdim.height >= ?";
array_push($propertiessql->parameters, "i", $propertyval);
break;
case "hmax":
$propertiessql->sql .= $sql_filter_properties_and . " rdim.height <= ?";
array_push($propertiessql->parameters, "i", $propertyval);
break;
case "wmin":
$propertiessql->sql .= $sql_filter_properties_and . " rdim.width >= ?";
array_push($propertiessql->parameters, "i", $propertyval);
break;
case "wmax":
$propertiessql->sql .= $sql_filter_properties_and . " rdim.width <= ?";
array_push($propertiessql->parameters, "i", $propertyval);
break;
case "fmin":
// Need to convert MB value to bytes
$propertiessql->sql .= $sql_filter_properties_and . " r.file_size >= ?";
array_push($propertiessql->parameters, "i", floatval($propertyval) * 1024 * 1024);
break;
case "fmax":
// Need to convert MB value to bytes
$propertiessql->sql .= $sql_filter_properties_and . " r.file_size <= ?";
array_push($propertiessql->parameters, "i", floatval($propertyval) * 1024 * 1024);
break;
case "fext":
$propertyval = str_replace("*", "%", $propertyval);
$propertiessql->sql .= $sql_filter_properties_and . " r.file_extension ";
if (substr($propertyval, 0, 1) == "-") {
$propertyval = substr($propertyval, 1);
$propertiessql->sql .= " NOT ";
}
if (substr($propertyval, 0, 1) == ".") {
$propertyval = substr($propertyval, 1);
}
$propertiessql->sql .= " LIKE ?";
array_push($propertiessql->parameters, "s", $propertyval);
break;
case "pi":
$propertiessql->sql .= $sql_filter_properties_and . " r.has_image = ?";
array_push($propertiessql->parameters, "i", $propertyval);
break;
case "cu":
$propertiessql->sql .= $sql_filter_properties_and . " r.created_by = ?";
array_push($propertiessql->parameters, "i", $propertyval);
break;
case "orientation":
$orientation_filters = array(
"portrait" => "COALESCE(rdim.height, 0) > COALESCE(rdim.width, 0)",
"landscape" => "COALESCE(rdim.height, 0) < COALESCE(rdim.width, 0)",
"square" => "COALESCE(rdim.height, 0) = COALESCE(rdim.width, 0)",
);
if (!in_array($propertyval, array_keys($orientation_filters))) {
break;
}
$propertiessql->sql .= $sql_filter_properties_and . $orientation_filters[$propertyval];
break;
}
}
}
if ($propertiessql->sql != "") {
if (strpos($sql_join->sql, "LEFT JOIN resource_dimensions rdim on r.ref=rdim.resource") === false) {
$sql_join->sql .= " LEFT JOIN resource_dimensions rdim on r.ref=rdim.resource";
}
if ($sql_filter->sql == "") {
$sql_filter->sql .= " WHERE " . $propertiessql->sql;
} else {
$sql_filter-> sql .= " AND " . $propertiessql->sql;
}
$sql_filter->parameters = array_merge($sql_filter->parameters, $propertiessql->parameters);
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
}
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE r.ref > 0 AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif ($search == "!integrityfail") {
// Search for resources where the file integrity has been marked as problematic or the file is missing
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE integrity_fail=1 AND no_file=0 AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif ($search == "!locked") {
// Search for locked resources
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE lock_user<>0 AND " . $sql_filter->sql . " [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
} elseif (checkperm('a') && $search == "!noningested") {
// System admins only - search for resources that have not been ingested - unfiltered
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource r " . $sql_join->sql . " WHERE file_path IS NOT NULL AND file_path <> '' [GROUP_BY_SQL] [ORDER_BY_SQL] {$sql_suffix}";
$sql->parameters = array_merge($select->parameters, $sql_join->parameters);
} elseif (preg_match('/^!report(\d+)(p[-1\d]+)?(d\d+)?(fy\d{4})?(fm\d{2})?(fd\d{2})?(ty\d{4})?(tm\d{2})?(td\d{2})?/i', $search, $report_search_data)) {
/*
View report as search results.
Special search "!report" can contain extra info for the reports' query period.
Syntax: !reportID[p?][d??][fy????][fm??][fd??][ty????][tm??][td??]
Where:
- ID is the actual report ref (mandatory)
- p is the selected period (see $reporting_periods_default config option)
- d is the period in specific number of days (p=0 in this case)
- fy,fm,fd (and their counter parts: ty,tm,td) represent a full date range (p=-1 in this case)
Examples for viewing as search results report #18:
- Last 7 days: !report18p7
- Last 23 days: !report18p0d23
- Between 2000-01-06 & 2023-03-16: !report18p-1fy2000fm01fd06ty2023tm03td16
*/
debug('[search_special] Running a "!report" search...');
$select->sql = "DISTINCT r.hit_count score, {$select->sql}";
$no_results_sql = new PreparedStatementQuery(
$sql_prefix . "SELECT [SELECT_SQL] FROM resource AS r "
. $sql_join->sql
. ' WHERE 1 = 2 AND ' . $sql_filter->sql
. ' [GROUP_BY_SQL] [ORDER_BY_SQL] ' . $sql_suffix,
array_merge($sql_join->parameters, $sql_filter->parameters)
);
// Users with no access control to reports get no results back (ie []).
if (!checkperm('t')) {
debug(sprintf('[WARNING][search_special][access control] User #%s attempted to run "%s" search without the right permissions', (int) $userref, $search));
$sql = $no_results_sql;
} else {
include_once 'reporting_functions.php';
$report_id = $report_search_data[1];
$all_reports = get_reports();
$reports_w_thumbnail = array_filter(array_column($all_reports, 'query', 'ref'), 'report_has_thumbnail');
$reports_w_support_non_correlated_sql = array_filter(array_column($all_reports, 'support_non_correlated_sql', 'ref'));
$reports = array_diff_key($reports_w_thumbnail, $reports_w_support_non_correlated_sql);
if (isset($reports[$report_id])) {
$report = $reports[$report_id];
$report_period = [];
$report_period_info_idxs = range(2, 9);
$report_period_info_names = array_combine($report_period_info_idxs, ['period', 'period_days', 'from-y', 'from-m', 'from-d', 'to-y', 'to-m', 'to-d']);
$report_period_info_lookups = array_combine($report_period_info_idxs, ['p', 'd', 'fy', 'fm', 'fd', 'ty', 'tm', 'td']);
foreach ($report_period_info_names as $idx => $info_name) {
if (!isset($report_search_data[$idx])) {
continue;
}
$report_period[$info_name] = str_replace($report_period_info_lookups[$idx], '', $report_search_data[$idx]);
}
$period = report_process_period($report_period);
$report_sql = report_process_query_placeholders($report, [
'[from-y]' => $period['from_year'],
'[from-m]' => $period['from_month'],
'[from-d]' => $period['from_day'],
'[to-y]' => $period['to_year'],
'[to-m]' => $period['to_month'],
'[to-d]' => $period['to_day'],
]);
$report_sql = preg_replace('/;\s?/m', '', $report_sql, 1);
$sql->sql = $sql_prefix . "SELECT [SELECT_SQL] FROM resource AS r"
. " INNER JOIN ($report_sql) AS rsr ON rsr.thumbnail = r.ref "
. $sql_join->sql
. ' WHERE ' . $sql_filter->sql . ' [GROUP_BY_SQL] [ORDER_BY_SQL] ' . $sql_suffix;
$sql->parameters = array_merge($select->parameters, $sql_join->parameters, $sql_filter->parameters);
debug("[search_special] SQL = " . json_encode($sql));
} else {
debug("[search_special] Report #{$report_id} not found");
$sql = $no_results_sql;
}
}
}
# Within this hook implementation, set the value of the global $sql variable:
# Since there will only be one special search executed at a time, only one of the
# hook implementations will set the value. So, you know that the value set
# will always be the correct one (unless two plugins use the same !<type> value).
$hooksql = hook("addspecialsearch", "", array($search, $select, $sql_join , $sql_filter, $sql));
if (is_a($hooksql, 'PreparedStatementQuery')) {
debug("Addspecialsearch hook returned useful results.");
$hooksql->sql = $sql_prefix . $hooksql->sql . $sql_suffix;
$sql = $hooksql;
}
if ($sql->sql != "") {
// Construct a reduced query for refs only searches, disk usage or count only searches
$removecolumns = ["c.date_added",
"c.comment",
"length\\(c\\.comment\\) commentset",
"r.has_image",
"r.is_transcoding",
"r.creation_date",
"r.user_rating_count",
"r.user_rating_total",
"r.user_rating",
"r.rating",
"r.file_extension",
"r.preview_extension",
"r.image_red",
"r.image_green",
"r.image_blue",
"r.thumb_width",
"r.thumb_height",
"r.colour_key",
"r.created_by",
"r.file_modified",
"r.file_checksum",
"r.request_count",
"r.new_hit_count",
"r.expiry_notification_sent",
"r.preview_tweaks",
"r.file_path",
"r.modified",
"r.file_size",
"rty.order_by",
"c.date_added",
"c.comment",
];
$reducedselect = $select->sql;
// Only reduce columns if an sql prefix is not set, this is to ensure compatibility with any encapsulating query that might be present. I.e. disk usage
if (trim((string) $sql_prefix) == "") {
foreach ($removecolumns as $removecolumn) {
$reducedselect = preg_replace("/(,\s?" . $removecolumn . ")/", "", $reducedselect);
}
$reducedselect = preg_replace("/(,\s?r\\.field\\d+)/", "", $reducedselect); // remove any fieldXX columns from select
}
$reduced_sql = clone $sql;
$reduced_sql->sql = str_replace(
["[SELECT_SQL]", "[GROUP_BY_SQL]", "[ORDER_BY_SQL]"],
[$reducedselect, "GROUP BY r.ref", ""],
$reduced_sql->sql
);
if (isset($hardlimit)) {
$reduced_sql->sql .= " LIMIT $hardlimit";
}
if ($return_refs_only) {
$sql = $reduced_sql;
} else {
$sql->sql = str_replace(
["[SELECT_SQL]", "[GROUP_BY_SQL]", "[ORDER_BY_SQL]"],
[$select->sql, "GROUP BY r.ref", "ORDER BY {$order_by}"],
$sql->sql
);
if (isset($order_by_params)) {
// Only used by $rgb search
$sql->parameters = array_merge($sql->parameters, $order_by_params);
if (isset($hardlimit) && $chunk_offset > $hardlimit - $search_chunk_size) {
$chunk_offset = $hardlimit - $search_chunk_size;
}
}
}
if ($returnsql) {
return $sql;
} else {
$result = sql_limit_with_total_count($sql, $search_chunk_size, $chunk_offset, $b_cache_count, $reduced_sql);
if (is_array($fetchrows)) {
return $result;
}
$resultcount = $result["total"] ?? 0;
if ($resultcount > 0 && count($result["data"]) > 0) {
$return = $result['data'];
$resultcount -= count($return);
while ($resultcount > 0) {
$return = array_merge($return, array_pad([], ($resultcount > 1000000 ? 1000000 : $resultcount), 0));
$resultcount -= 1000000;
}
} else {
$return = [];
}
}
hook('beforereturnresults', '', array($result, $archive));
return $return;
}
# Arrived here? There were no special searches. Return false.
return false;
}
/**
* Function used to create a list of nodes found in a search string
*
* IMPORTANT: use resolve_given_nodes() if you need to detect nodes based on
* search string format (ie. @@253@@255 and/ or @@!260)
*
* @param string $string
*
* @return array
*/
function resolve_nodes_from_string($string)
{
if (!is_string($string)) {
return array();
}
$node_bucket = array();
$node_bucket_not = array();
$return = array();
resolve_given_nodes($string, $node_bucket, $node_bucket_not);
foreach ($node_bucket as $nodes) {
foreach ($nodes as $node) {
$return[] = $node;
}
}
foreach ($node_bucket_not as $node_not) {
$return[] = "-" . $node_not;
}
return $return;
}
/**
* Utility function which helps rebuilding a specific field search string
* from a node element
*
* @param array $node A node element as returned by get_node() or get_nodes()
*
* @return string
*/
function rebuild_specific_field_search_from_node(array $node)
{
if (0 == count($node)) {
return '';
}
$field_shortname = ps_value("SELECT name AS `value` FROM resource_type_field WHERE ref = ?", array("i",$node['resource_type_field']), "field{$node['resource_type_field']}", "schema");
// Note: at the moment there is no need to return a specific field search by multiple options
// Example: country:keyword1;keyword2
return (strpos($node['name'], " ") === false) ? $field_shortname . ":" . i18n_get_translated($node['name']) : "\"" . $field_shortname . ":" . i18n_get_translated($node['name']) . "\"";
}
function search_get_previews($search, $restypes = "", $order_by = "relevance", $archive = 0, $fetchrows = -1, $sort = "DESC", $access_override = false, $ignore_filters = false, $return_disk_usage = false, $recent_search_daylimit = "", $go = false, $stats_logging = true, $return_refs_only = false, $editable_only = false, $returnsql = false, $getsizes = array(), $previewextension = "jpg")
{
global $access;
$structured = false;
if (is_array($fetchrows)) {
$structured = true;
} elseif (!is_array($fetchrows) && strpos((string)$fetchrows, ",") !== false) {
$fetchrows = explode(",", $fetchrows);
if (count($fetchrows) == 2) {
$structured = true;
} else {
$fetchrows = 0;
}
}
if ($structured) {
array_map(function ($val) {
return $val > 0 ? $val : 0;
}, $fetchrows);
}
# Note the subset of the available parameters. We definitely don't want to allow override of permissions or filters.
$results = do_search($search, $restypes, $order_by, $archive, $fetchrows, $sort, $access_override, DEPRECATED_STARSEARCH, $ignore_filters, $return_disk_usage, $recent_search_daylimit, $go, $stats_logging, $return_refs_only, $editable_only, $returnsql);
if (is_string($getsizes)) {
$getsizes = explode(",", $getsizes);
}
$getsizes = array_map('trim', $getsizes);
if (!is_array($results)) {
return $results;
}
$total = $results["total"] ?? count($results);
$resultset = $results["data"] ?? $results;
$use_watermark = check_use_watermark();
if (is_array($resultset) && is_array($getsizes) && count($getsizes) > 0) {
$available = get_all_image_sizes(true, ($access == 1));
for ($n = 0; $n < $total; $n++) {
// if using fetchrows some results may just be == 0 - remove from results array
if (!isset($resultset[$n]) || $resultset[$n] == 0) {
continue;
}
$access = $resultset[$n]["resultant_access"] ?? get_resource_access($resultset[$n]);
if ($access == 2) {
// No images for confidential resources
continue;
}
foreach ($getsizes as $getsize) {
if (!(in_array($getsize, array_column($available, "id")))) {
continue;
}
if (
!resource_has_access_denied_by_RT_size($resultset[$n]['resource_type'], $getsize)
&& file_exists(get_resource_path($resultset[$n]["ref"], true, $getsize, false, $previewextension, -1, 1, $use_watermark))
) {
$resultset[$n]["url_" . $getsize] = get_resource_path($resultset[$n]["ref"], false, $getsize, false, $previewextension, -1, 1, $use_watermark);
}
}
}
}
return $structured ? ["total" => $total, "data" => $resultset] : $resultset;
}
function get_upload_here_selected_nodes($search, array $nodes)
{
$upload_here_nodes = resolve_nodes_from_string($search);
if (empty($upload_here_nodes)) {
return $nodes;
}
return array_merge($nodes, $upload_here_nodes);
}
/**
* get the default archive states to search
*
* @return array
*/
function get_default_search_states()
{
global $searchstates;
$defaultsearchstates = isset($searchstates) ? $searchstates : array(0); // May be set by rse_workflow plugin
$modifiedstates = hook("modify_default_search_states", "", array($defaultsearchstates));
if (is_array($modifiedstates)) {
return $modifiedstates;
}
return $defaultsearchstates;
}
/**
* Get the required search filter sql for the given filter for use in do_search()
*
* @return PreparedStatementQuery
*/
function get_filter_sql($filterid)
{
global $userref, $access_override, $custom_access_overrides_search_filter, $open_access_for_contributor;
$filter = get_filter($filterid);
if (!$filter) {
return false;
}
$filterrules = get_filter_rules($filterid);
$modfilterrules = hook("modifysearchfilterrules");
if ($modfilterrules) {
$filterrules = $modfilterrules;
}
$filtercondition = $filter["filter_condition"];
$filters = array();
$filter_ors = array(); // Allow filters to be overridden in certain cases
$filter_ors_params = array();
foreach ($filterrules as $filterrule) {
$filtersql = new PreparedStatementQuery();
if (count($filterrule["nodes_on"]) > 0) {
$filtersql->sql .= "r.ref " . ($filtercondition == RS_FILTER_NONE ? " NOT " : "") . " IN (SELECT rn.resource FROM resource_node rn WHERE rn.node IN (" . ps_param_insert(count($filterrule["nodes_on"])) . ")) ";
$filtersql->parameters = array_merge($filtersql->parameters, ps_param_fill($filterrule["nodes_on"], "i"));
}
if (count($filterrule["nodes_off"]) > 0) {
if ($filtersql->sql != "") {
$filtersql->sql .= " OR ";
}
$filtersql->sql .= "r.ref " . ($filtercondition == RS_FILTER_NONE ? "" : " NOT") . " IN (SELECT rn.resource FROM resource_node rn WHERE rn.node IN (" . ps_param_insert(count($filterrule["nodes_off"])) . ")) ";
$filtersql->parameters = array_merge($filtersql->parameters, ps_param_fill($filterrule["nodes_off"], "i"));
}
$filters[] = $filtersql;
}
if (count($filters) > 0) {
if ($filtercondition == RS_FILTER_ALL || $filtercondition == RS_FILTER_NONE) {
$glue = " AND ";
} else {
// This is an OR filter
$glue = " OR ";
}
$filter_add = new PreparedStatementQuery();
// Bracket the filters to ensure that there is no hanging OR to create an unintentional disjunct
$filter_add->sql = "(" . implode($glue, array_column($filters, "sql")) . ")";
foreach ($filters as $filter) {
$filter_add->parameters = array_merge($filter_add->parameters, $filter->parameters);
}
# If custom access has been granted for the user or group, nullify the search filter, effectively selecting "true".
if (!$access_override && $custom_access_overrides_search_filter) {
$filter_ors[] = "(rca.access IS NOT null AND rca.access<>2) OR (rca2.access IS NOT null AND rca2.access<>2)";
}
if ($open_access_for_contributor) {
$filter_ors[] = "(r.created_by = ?)";
array_push($filter_ors_params, "i", $userref);
}
if (count($filter_ors) > 0) {
$filter_add->sql = "((" . $filter_add->sql . ") OR (" . implode(") OR (", $filter_ors) . "))";
$filter_add->parameters = array_merge($filter_add->parameters, $filter_ors_params);
}
return $filter_add;
}
return false;
}
function split_keywords($search, $index = false, $partial_index = false, $is_date = false, $is_html = false, $keepquotes = false, bool $preserve_separators = false)
{
# Takes $search and returns an array of individual keywords.
global $permitted_html_tags, $permitted_html_attributes;
if ($index && $is_date) {
# Date handling... index a little differently to support various levels of date matching (Year, Year+Month, Year+Month+Day).
$s = explode("-", $search);
if (count($s) >= 3) {
return array($s[0],$s[0] . "-" . $s[1],$search);
} elseif (is_array($search)) {
return $search;
} else {
return array($search);
}
}
# Remove any real / unescaped lf/cr
$search = str_replace("\r", " ", $search);
$search = str_replace("\n", " ", $search);
$search = str_replace("\\r", " ", $search);
$search = str_replace("\\n", " ", $search);
if ($is_html || (substr($search, 0, 1) == "<" && substr($search, -1, 1) == ">")) {
// String can't be in encoded format at this point or string won't be indexed correctly.
$search = html_entity_decode($search);
if ($index) {
// Clean up html for indexing
// Allow indexing of anchor text
$allowed_tags = array_merge(array("a"), $permitted_html_tags);
$allowed_attributes = array_merge(array("href"), $permitted_html_attributes);
$search = strip_tags_and_attributes($search, $allowed_tags, $allowed_attributes);
// Get rid of the actual html tags and attribute ids to prevent indexing these
foreach ($allowed_tags as $allowed_tag) {
$search = str_replace(array("<" . $allowed_tag . ">","<" . $allowed_tag,"</" . $allowed_tag), " ", $search);
}
foreach ($allowed_attributes as $allowed_attribute) {
$search = str_replace($allowed_attribute . "=", " ", $search);
}
// Remove any left over tag parts
$search = str_replace(array(">", "<","="), " ", $search);
}
}
$ns = trim_spaces($search);
if (!$index && strpos($ns, ":") !== false) { # special 'constructed' query type
if ($keepquotes) {
preg_match_all('/("|-")(?:\\\\.|[^\\\\"])*"|\S+/', $ns, $matches);
$return = trim_array($matches[0], ",");
} elseif (strpos($ns, "startdate") !== false || strpos($ns, "enddate") !== false) {
$return = explode(",", $ns);
} else {
$ns = cleanse_string($ns, $preserve_separators, !$index, $is_html);
$return = explode(" ", $ns);
}
// If we are not breaking quotes we may end up a with commas in the array of keywords which need to be removed
if ($keepquotes) {
$return = trim_array($return, ",");
}
return $return;
} else {
# split using spaces and similar chars (according to configured whitespace characters)
if (!$index && $keepquotes && strpos($ns, "\"") !== false) {
preg_match_all('/("|-")(?:\\\\.|[^\\\\"])*"|\S+/', $ns, $matches);
$splits = $matches[0];
$ns = array();
foreach ($splits as $split) {
if (!(substr($split, 0, 1) == "\"" && substr($split, -1, 1) == "\"") && strpos($split, ",") !== false) {
$split = explode(",", $split);
$ns = array_merge($ns, $split);
} else {
$ns[] = $split;
}
}
} else {
# split using spaces and similar chars (according to configured whitespace characters)
$ns = explode(" ", cleanse_string($ns, false, !$index, $is_html));
}
if ($keepquotes) {
$ns = trim_array($ns, ",");
}
if ($index && $partial_index) {
return add_partial_index($ns);
}
return $ns;
}
}
function cleanse_string($string, $preserve_separators, $preserve_hyphen = false, $is_html = false)
{
# Removes characters from a string prior to keyword splitting, for example full stops
# Also makes the string lower case ready for indexing.
global $config_separators;
$separators = $config_separators;
// Replace some HTML entities with empty space
// Most of them should already be in $config_separators
// but others, like &shy; don't have an actual character that we can copy and paste
// to $config_separators
$string = htmlentities($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$string = str_replace('&nbsp;', ' ', $string);
$string = str_replace('&shy;', ' ', $string);
$string = str_replace('&lsquo;', ' ', $string);
$string = str_replace('&rsquo;', ' ', $string);
$string = str_replace('&ldquo;', ' ', $string);
$string = str_replace('&rdquo;', ' ', $string);
$string = str_replace('&ndash;', ' ', $string);
// Revert the htmlentities as otherwise we lose ability to identify certain text e.g. diacritics
$string = html_entity_decode($string, ENT_QUOTES, 'UTF-8');
if (
$preserve_hyphen
&& (substr($string, 0, 1) == "-" || strpos($string, " -") !== false) /*support minus as first character for simple NOT searches */
&& strpos($string, " - ") === false
) {
# Preserve hyphen - used when NOT indexing so we know which keywords to omit from the search.
$separators = array_diff($separators, array("-")); # Remove hyphen from separator array.
}
if (substr($string, 0, 1) == "!" && strpos(substr($string, 1), "!") === false) {
// If we have the exclamation mark configured as a config separator but we are doing a special search we don't want to remove it
$separators = array_diff($separators, array("!"));
}
if ($preserve_separators) {
return mb_strtolower(trim_spaces(str_replace($separators, " ", $string)), 'UTF-8');
} else {
# Also strip out the separators used when specifying multiple field/keyword pairs (comma and colon)
$s = $separators;
$s[] = ",";
$s[] = ":";
return mb_strtolower(trim_spaces(str_replace($s, " ", $string)), 'UTF-8');
}
}
/**
* Resolve keyword
*
* @param string $keyword The keyword to resolve
* @param bool $create If keyword not found, should we create it instead?
* @param bool $normalize Should we normalize the keyword before resolving?
* @param bool $stem Should we use the keywords' stem when resolving?
*
* @return int|bool Returns the keyword reference for $keyword, or false if no such keyword exists.
*/
function resolve_keyword($keyword, $create = false, $normalize = true, $stem = true)
{
debug_function_call("resolve_keyword", func_get_args());
// Create a cache to ensure we find new nodes subsequently if in a transaction
global $resolve_keyword_cache;
global $quoted_string, $stemming;
$kwhash = md5($keyword) . md5("!" . $keyword . ($normalize ? "NORM" : "") . ($stem ? "STEM" : ""));
if (isset($resolve_keyword_cache[$kwhash])) {
return $resolve_keyword_cache[$kwhash];
}
$keyword = mb_strcut($keyword, 0, 100); # Trim keywords to 100 chars for indexing, as this is the length of the keywords column.
if (!$quoted_string && $normalize) {
$keyword = normalize_keyword($keyword);
debug("resolving normalized keyword " . $keyword . ".");
}
# Stemming support. If enabled and a stemmer is available for the current language, index the stem of the keyword not the keyword itself.
# This means plural/singular (and other) forms of a word are treated as equivalents.
if ($stem && $stemming && function_exists("GetStem")) {
$keyword = GetStem($keyword);
}
$return = ps_value("SELECT ref value FROM keyword WHERE keyword = ?", array("s",trim($keyword)), 0, "keyword");
if ($return === 0) {
if ($create) {
# Create a new keyword.
debug("resolve_keyword: Creating new keyword for " . $keyword);
ps_query("insert into keyword (keyword,soundex,hit_count) values (?,left(?,10),0)", array("s",$keyword,"s",soundex($keyword)));
$return = sql_insert_id();
clear_query_cache("keyword");
} else {
return false;
}
}
$resolve_keyword_cache[$kwhash] = $return;
return $return;
}
/**
* Generates a list of keywords for indexing, including all possible infixes
* for each keyword in the provided list.
*
* This function processes each keyword and, for those without spaces,
* adds all possible infixes of a specified minimum length to the return array.
* The resulting array is suitable for indexing in fields that have partial indexing enabled.
*
* @param array $keywords An array of keywords to process for partial indexing.
* @return array An array of keywords, each with its associated position in the original list.
*/
function add_partial_index($keywords)
{
$return = array();
$position = 0;
$x = 0;
for ($n = 0; $n < count($keywords); $n++) {
$keyword = trim($keywords[$n]);
$return[$x]['keyword'] = $keyword;
$return[$x]['position'] = $position;
$x++;
if (strpos($keyword, " ") === false) { # Do not do this for keywords containing spaces as these have already been broken to individual words using the code above.
global $partial_index_min_word_length;
# For each appropriate infix length
for ($m = $partial_index_min_word_length; $m < strlen($keyword); $m++) {
# For each position an infix of this length can exist in the string
for ($o = 0; $o <= strlen($keyword) - $m; $o++) {
$infix = mb_substr($keyword, $o, $m);
$return[$x]['keyword'] = $infix;
$return[$x]['position'] = $position; // infix has same position as root
$x++;
}
}
} # End of no-spaces condition
$position++; // end of root keyword
} # End of partial indexing keywords loop
return $return;
}
/**
* Highlights keywords in the given text based on the search query.
*
* This function scans the provided text for keywords specified in the search string
* and wraps them in HTML markup to highlight them.
*
* @param string $text The text in which to highlight keywords.
* @param string $search The search query containing keywords to highlight.
* @param bool $partial_index Indicates whether partial indexing is used (default: false).
* @param string $field_name Optional. The name of the field being searched.
* @param int $keywords_index Indicates the indexing status of the field (default: 1).
* @param int $str_highlight_options Options for highlighting (default: STR_HIGHLIGHT_SIMPLE).
* @return string The text with highlighted keywords.
*/
function highlightkeywords($text, $search, $partial_index = false, $field_name = "", $keywords_index = 1, $str_highlight_options = STR_HIGHLIGHT_SIMPLE)
{
global $noadd;
# do not highlight if the field is not indexed, so it is clearer where results came from.
if ($keywords_index != 1) {
return $text;
}
# Highlight searched keywords in $text
global $stemming;
# Situations where we do not need to do this.
if ($search == "" || $text == "") {
return $text;
}
# Generate the cache of search keywords (no longer global so it can test against particular fields.
# a search is a small array so I don't think there is much to lose by processing it.
$hlkeycache = array();
$s = split_keywords($search, false, false, false, false, false, true);
for ($n = 0; $n < count($s); $n++) {
if (strpos($s[$n], ":") !== false) {
$c = explode(":", $s[$n]);
// Only add field specific keywords
if ($field_name != "" && $c[0] == $field_name) {
$hlkeycache[] = $c[1];
}
} else {
// Add general keywords
$keyword = $s[$n];
if (in_array($keyword, $noadd)) { # skip common words that are excluded from indexing
continue;
}
if ($stemming && function_exists("GetStem")) { // Stemming enabled. Highlight any words matching the stem.
$keyword = GetStem($keyword);
}
if (strpos($keyword, "*") !== false) {
$keyword = str_replace("*", "", $keyword);
}
$hlkeycache[] = $keyword;
}
}
# Parse and replace.
return str_highlight($text, $hlkeycache, $str_highlight_options);
}
/**
* Highlight the relevant text in a string
*
* @param string $text Text to search
* @param string $needle Text to highlight
* @param int $options String highlight options - See include/definitions.php
* @param string $highlight Optional custom highlight code
* @return string
*/
function str_highlight($text, $needle, $options = null, $highlight = null)
{
$text = (string) $text;
# If search text contains HTML entities, convert the search phrase to allow matching of entities e.g. &amp;
if (htmlspecialchars_decode($text) != $text) {
if (is_array($needle)) {
$needle = array_map('escape', $needle);
} else {
$needle = escape($needle);
}
}
/*
this function requires that needle array does not contain any of the following characters: "(" ")"
*/
$remove_from_needle = array("(", ")");
$needle = str_replace($remove_from_needle, "", $needle);
/*
Sometimes the text can contain HTML entities and can break the highlighting feature
Example: searching for "q&a" in a string like "q&amp;a" will highlight the wrong string
*/
$htmltext = htmlspecialchars_decode($text);
// If text contains HTML tags then ignore them
if ($htmltext != strip_tags($htmltext)) {
$options = $options & STR_HIGHLIGHT_STRIPLINKS;
}
# Thanks to Aidan Lister <aidan@php.net>
# Sourced from http://aidanlister.com/repos/v/function.str_highlight.php on 2007-10-09
# As of 2020-09-07 code is now at https://github.com/aidanlister/code/blob/master/function.str_highlight.php
# The GitHub code repository README states: "The code resides entirely in the public domain."
# https://github.com/aidanlister/code
$text = str_replace("_", "", $text);// underscores are considered part of words, so temporarily replace them for better \b search.
$text = str_replace("#zwspace;", "", $text);
// Default highlighting. This used to use '<' and '>' characters as placeholders but now changed as they were being removed by strip_tags
if ($highlight === null) {
$highlight = '\(\1\)';
}
// Select pattern to use
if ($options & STR_HIGHLIGHT_SIMPLE) {
$pattern = '#(%s)#';
$sl_pattern = '#(%s)#';
} else {
$pattern = '#(?!<.*?)(%s)(?![^<>]*?>)#';
$sl_pattern = '#<a\s(?:.*?)>(%s)</a>#';
}
// Case sensitivity
if (!($options & STR_HIGHLIGHT_CASESENS)) {
$pattern .= 'i';
$sl_pattern .= 'i';
}
$needle = (array) $needle;
usort($needle, "sorthighlights");
foreach ($needle as $needle_s) {
if (strlen($needle_s) > 0) {
$needle_s = preg_quote($needle_s, "#");
// Escape needle with optional whole word check
if ($options & STR_HIGHLIGHT_WHOLEWD) {
$needle_s = '\b' . $needle_s . '\b';
}
// Strip links
if ($options & STR_HIGHLIGHT_STRIPLINKS) {
$sl_regex = sprintf($sl_pattern, $needle_s);
$text = preg_replace($sl_regex, '\1', $text);
}
$regex = sprintf($pattern, $needle_s);
$text = preg_replace($regex, $highlight, $text);
}
}
$text = str_replace("", "_", $text);
$text = str_replace("", "#zwspace;", $text);
# Fix - do the final replace at the end - fixes a glitch whereby the highlight HTML itself gets highlighted if it matches search terms, and you get nested HTML.
$text = str_replace("\(", '<span class="highlight">', $text);
$text = str_replace("\)", '</span>', $text);
return $text;
}
/**
* Comparison function for sorting highlights by their length.
*
* This function is used to sort keyword highlights for the `str_highlight` function.
* It ensures that keywords are sorted based on their length, with shorter keywords
* coming before longer ones. If the lengths are equal, it sorts them alphabetically.
*
* This fixes an odd problem for str_highlight related to the order of keywords.
*
* @param string $a The first string to compare.
* @param string $b The second string to compare.
* @return int Returns 0 if lengths are equal, -1 if $a is shorter than $b, and 1 if $a is longer than $b.
*/
function sorthighlights($a, $b)
{
if (strlen($a) < strlen($b)) {
return 0;
}
return ($a < $b) ? -1 : 1;
}
/**
* Suggests complete existing keywords based on a partial search term.
*
* This function fetches keywords that match the given partial word, returning
* suggestions from the keyword database. It also considers user permissions by
* excluding indexed fields that are hidden from the user. Additionally, it can
* restrict results to a specific resource type field.
*
* @param string $search The partial keyword to search for.
* @param string $ref (optional) The resource type field to restrict suggestions to.
* @return array An array of suggested keywords matching the search criteria.
*/
function get_suggested_keywords($search, $ref = "")
{
global $autocomplete_search_items,$autocomplete_search_min_hitcount;
# Fetch a list of fields that are not available to the user - these must be omitted from the search.
$hidden_indexed_fields = get_hidden_indexed_fields();
$restriction_clause_node = "";
$params = array("s",$search . "%");
if (count($hidden_indexed_fields) > 0) {
$restriction_clause_node .= " AND n.resource_type_field NOT IN (" . ps_param_insert(count($hidden_indexed_fields)) . ")";
$params = array_merge($params, ps_param_fill($hidden_indexed_fields, "i"));
}
if ((string)(int)$ref == $ref) {
$restriction_clause_node .= " AND n.resource_type_field = ?";
$params[] = "i";
$params[] = $ref;
}
$params[] = "i";
$params[] = $autocomplete_search_items;
return ps_array("SELECT ak.keyword value
FROM
(
SELECT k.keyword, k.hit_count
FROM keyword k
JOIN node_keyword nk ON nk.keyword=k.ref
JOIN node n ON n.ref=nk.node
WHERE k.keyword LIKE ? " . $restriction_clause_node . "
) ak
GROUP BY ak.keyword, ak.hit_count
ORDER BY ak.hit_count DESC LIMIT ?", $params);
}
/**
* Retrieves keywords related to a given keyword reference.
*
* This function checks a cache for related keywords associated with the provided
* keyword reference. If not found in the cache, it queries the database for related
* keywords. The relationship can be one-way or bidirectional based on the
* configuration. It returns an array of related keyword references.
*
* @param int $keyref The reference ID of the keyword for which to find related keywords.
* @return array An array of related keyword references.
*/
function get_related_keywords($keyref)
{
debug_function_call("get_related_keywords", func_get_args());
# For a given keyword reference returns the related keywords
# Also reverses the process, returning keywords for matching related words
# and for matching related words, also returns other words related to the same keyword.
global $keyword_relationships_one_way, $related_keywords_cache;
if (isset($related_keywords_cache[$keyref])) {
return $related_keywords_cache[$keyref];
} else {
if ($keyword_relationships_one_way) {
$related_keywords_cache[$keyref] = ps_array("SELECT related value FROM keyword_related WHERE keyword = ?", array("i", $keyref), "keywords_related");
return $related_keywords_cache[$keyref];
} else {
$related_keywords_cache[$keyref] = ps_array("SELECT keyword value FROM keyword_related WHERE related = ? UNION SELECT related value FROM keyword_related WHERE (keyword = ? OR keyword IN (SELECT keyword value FROM keyword_related WHERE related = ?)) AND related <> ?", array("i", $keyref, "i", $keyref, "i", $keyref, "i", $keyref), "keywords_related");
return $related_keywords_cache[$keyref];
}
}
}
/**
* Retrieves keywords and their related keywords, optionally filtered by specific keywords.
*
* This function returns a list of keywords along with their related keywords grouped
* together. It can filter the results based on the provided keyword or specific keyword
* string. The related keywords are returned as a comma-separated string.
*
* @param string $find An optional keyword to find related keywords for. If specified,
* it filters the results to include only the related keywords for
* this keyword.
* @param string $specific An optional specific keyword to find. If specified, it filters
* the results to include only the related keywords for this specific
* keyword.
* @return array An array of keywords and their related keywords grouped together.
*/
function get_grouped_related_keywords($find = "", $specific = "")
{
debug_function_call("get_grouped_related_keywords", func_get_args());
$sql = "";
$params = array();
if ($find != "") {
$sql = "where k1.keyword=? or k2.keyword=?";
$params[] = "s";
$params[] = $find;
$params[] = "s";
$params[] = $find;
}
if ($specific != "") {
$sql = "where k1.keyword=?";
$params[] = "s";
$params[] = $specific;
}
return ps_query("
select k1.keyword,group_concat(k2.keyword order by k2.keyword separator ', ') related from keyword_related kr
join keyword k1 on kr.keyword=k1.ref
join keyword k2 on kr.related=k2.ref
$sql
group by k1.keyword order by k1.keyword
", $params, "keywords_related");
}
/**
* Saves the related keywords for a specified keyword.
*
* This function first resolves the keyword reference for the provided keyword. It then deletes
* any existing relationships for that keyword and inserts the new related keywords into the
* database.
*
* @param string $keyword The keyword for which related keywords are being saved.
* @param string $related A comma-separated string of related keywords to associate with the
* specified keyword.
* @return bool Returns true on success, or false on failure.
*/
function save_related_keywords($keyword, $related)
{
debug_function_call("save_related_keywords", func_get_args());
$keyref = resolve_keyword($keyword, true, false, false);
$s = trim_array(explode(",", $related));
ps_query("DELETE FROM keyword_related WHERE keyword = ?", array("i",$keyref));
if (trim($related) != "") {
for ($n = 0; $n < count($s); $n++) {
ps_query("insert into keyword_related (keyword,related) values (?,?)", array("i",$keyref,"i",resolve_keyword($s[$n], true, false, false)));
}
}
clear_query_cache("keywords_related");
return true;
}
/**
* Retrieves a list of fields suitable for the simple search box.
*
* This function gathers all resource type fields that are marked for simple search usage.
* It includes standard fields and custom fields that have their titles translated. It ensures
* that only fields with appropriate permissions and those that are either indexed or of a
* fixed list type are included in the returned array.
*
* @return array An array of fields suitable for simple search, including their titles and other
* properties, filtered by permissions and search capabilities.
*/
function get_simple_search_fields()
{
global $FIXED_LIST_FIELD_TYPES, $country_search;
# First get all the fields
$allfields = get_resource_type_fields("", "global,order_by");
# Applies field permissions and translates field titles in the newly created array.
$return = array();
for ($n = 0; $n < count($allfields); $n++) {
if (
# Check if for simple_search
# Also include the country field even if not selected
# This is to provide compatibility for older systems on which the simple search box was not configurable
# and had a simpler 'country search' option.
($allfields[$n]["simple_search"] == 1 || (isset($country_search) && $country_search && $allfields[$n]["ref"] == 3))
&&
# Must be either indexed or a fixed list type
($allfields[$n]["keywords_index"] == 1 || in_array($allfields[$n]["type"], $FIXED_LIST_FIELD_TYPES))
&&
metadata_field_view_access($allfields[$n]["ref"])
) {
$allfields[$n]["title"] = lang_or_i18n_get_translated($allfields[$n]["title"], "fieldtitle-");
$return[] = $allfields[$n];
}
}
return $return;
}
/**
* Retrieves a list of fields/properties suitable for search display based on the provided field references.
*
* @param array $field_refs An array of field references to filter the search display fields.
* @return array An array of fields with their properties, including translated titles, that are
* visible to the user based on permission checks.
* @throws Exception if the input parameter is not an array.
*/
function get_fields_for_search_display($field_refs)
{
if (!is_array($field_refs)) {
exit(" passed to getfields() is not an array. ");
}
# Executes query.
$fields = ps_query("select " . columns_in("resource_type_field") . " from resource_type_field where ref in (" . ps_param_insert(count($field_refs)) . ")", ps_param_fill($field_refs, "i"), "schema");
# Applies field permissions and translates field titles in the newly created array.
$return = array();
for ($n = 0; $n < count($fields); $n++) {
if (metadata_field_view_access($fields[$n]["ref"])) {
$fields[$n]["title"] = lang_or_i18n_get_translated($fields[$n]["title"], "fieldtitle-");
$return[] = $fields[$n];
}
}
return $return;
}
/**
* Get all defined filters (currently only used for search)
*
* @param string $order column to order by
* @param string $sort sort order ("ASC" or "DESC")
* @param string $find text to search for in filter
*
* @return array
*/
function get_filters($order = "ref", $sort = "ASC", $find = "")
{
$validorder = array("ref","name");
if (!in_array($order, $validorder)) {
$order = "ref";
}
if ($sort != "ASC") {
$sort = "DESC";
}
$condition = "";
$join = "";
$params = array();
if (trim($find) != "") {
$join = " LEFT JOIN filter_rule_node fn ON fn.filter=f.ref LEFT JOIN node n ON n.ref = fn.node LEFT JOIN resource_type_field rtf ON rtf.ref=n.resource_type_field";
$condition = " WHERE f.name LIKE ? OR n.name LIKE ? OR rtf.name LIKE ? OR rtf.title LIKE ?";
$params[] = "s";
$params[] = "%" . $find . "%";
$params[] = "s";
$params[] = "%" . $find . "%";
$params[] = "s";
$params[] = "" . $find . "";
$params[] = "s";
$params[] = "" . $find . "";
}
$sql = "SELECT f.ref, f.name FROM filter f {$join}{$condition} GROUP BY f.ref ORDER BY f.{$order} {$sort}"; // $order and $sort are already confirmed to be valid.
return ps_query($sql, $params);
}
/**
* Get filter summary details
*
* @param int $filterid ID of filter (from usergroup search_filter_id or user search_filter_oid)
*
* @return array
*/
function get_filter($filterid)
{
// Codes for filter 'condition' column
// 1 = ALL must apply
// 2 = NONE must apply
// 3 = ANY can apply
if (!is_numeric($filterid) || $filterid < 1) {
return false;
}
$filter = ps_query("SELECT ref, name, filter_condition FROM filter f WHERE ref=?", array("i",$filterid));
if (count($filter) > 0) {
return $filter[0];
}
return false;
}
/**
* Get filter rules for use in search
*
* @param int $filterid ID of filter (from usergroup search_filter_id or user search_filter_oid)
*
* @return array
*/
function get_filter_rules($filterid)
{
$filter_rule_nodes = ps_query("SELECT fr.ref as rule, frn.node_condition, frn.node FROM filter_rule fr LEFT JOIN filter_rule_node frn ON frn.filter_rule=fr.ref WHERE fr.filter=?", array("i",$filterid));
// Convert results into useful array
$rules = array();
foreach ($filter_rule_nodes as $filter_rule_node) {
$rule = $filter_rule_node["rule"];
if (!isset($rules[$filter_rule_node["rule"]])) {
$rules[$rule] = array();
$rules[$rule]["nodes_on"] = array();
$rules[$rule]["nodes_off"] = array();
}
if ($filter_rule_node["node_condition"] == 1) {
$rules[$rule]["nodes_on"][] = $filter_rule_node["node"];
} else {
$rules[$rule]["nodes_off"][] = $filter_rule_node["node"];
}
}
return $rules;
}
/**
* Get filter rule
*
* @param int $ruleid - ID of filter rule
*
* @return array
*/
function get_filter_rule($ruleid)
{
$rule_data = ps_query("SELECT fr.ref, frn.node_condition, group_concat(frn.node) AS nodes, n.resource_type_field FROM filter_rule fr JOIN filter_rule_node frn ON frn.filter_rule=fr.ref join node n on frn.node=n.ref WHERE fr.ref=? GROUP BY n.resource_type_field,frn.node_condition", array("i",$ruleid));
if (count($rule_data) > 0) {
return $rule_data;
}
return false;
}
/**
* Save filter, will return existing filter ID if text matches already migrated
*
* @param int $filter - ID of filter. Set to 0 for new filter
* @param int $filter_name - Name of filter
* @param int $filter_condition - One of RS_FILTER_ALL,RS_FILTER_NONE,RS_FILTER_ANY
*
* @return boolean | integer - false, or ID of filter
*/
function save_filter(int $filter, string $filter_name, string $filter_condition)
{
if (!in_array($filter_condition, array(RS_FILTER_ALL,RS_FILTER_NONE,RS_FILTER_ANY))) {
return false;
}
if ($filter != 0) {
if (!is_int_loose($filter)) {
return false;
}
ps_query("UPDATE filter SET name=?, filter_condition=? WHERE ref = ?", array("s",$filter_name,"s",$filter_condition,"i",$filter));
} else {
ps_query("INSERT INTO filter (name, filter_condition) VALUES (?,?)", array("s",$filter_name,"s",$filter_condition));
return sql_insert_id();
}
return $filter;
}
/**
* Save filter rule, will return existing rule ID if text matches already migrated
*
* @param int $filter_rule - ID of filter_rule
* @param int $filterid - ID of associated filter
* @param array|string $ruledata - Details of associated rule nodes (as JSON if submitted from rule edit page)
*
* @return boolean | integer - false, or ID of filter_rule
*/
function save_filter_rule($filter_rule, $filterid, $rule_data)
{
if (!is_array($rule_data)) {
$rule_data = json_decode($rule_data);
}
if ($filter_rule != "new" && is_int_loose($filter_rule) && $filter_rule > 0) {
ps_query("DELETE FROM filter_rule_node WHERE filter_rule = ?", array("i",$filter_rule));
} else {
ps_query("INSERT INTO filter_rule (filter) VALUES (?)", array("i",$filterid));
$filter_rule = sql_insert_id();
}
if (count($rule_data) > 0) {
$nodeinsert = array();
$params = array();
for ($n = 0; $n < count($rule_data); $n++) {
$condition = $rule_data[$n][0];
for ($rd = 0; $rd < count($rule_data[$n][1]); $rd++) {
$nodeid = $rule_data[$n][1][$rd];
$nodeinsert[] = "(?,?,?)";
$params[] = "i";
$params[] = $filter_rule;
$params[] = "i";
$params[] = $nodeid;
$params[] = "i";
$params[] = $condition;
}
}
$sql = "INSERT INTO filter_rule_node (filter_rule,node,node_condition) VALUES " . implode(',', $nodeinsert);
ps_query($sql, $params);
}
return $filter_rule;
}
/**
* Delete specified filter
*
* @param int $filter - ID of filter
*
* @return boolean | array of users/groups using filter
*/
function delete_filter($filter)
{
if (!is_numeric($filter)) {
return false;
}
// Check for existing use of filter
$checkgroups = ps_array("SELECT ref value FROM usergroup WHERE search_filter_id=? OR edit_filter_id=? OR derestrict_filter_id=?", ['i', $filter,'i', $filter,'i', $filter], "");
$checkusers = ps_array("SELECT ref value FROM user WHERE search_filter_o_id=? ", array("i",$filter), "");
if (count($checkgroups) > 0 || count($checkusers) > 0) {
return array("groups" => $checkgroups, "users" => $checkusers);
}
// Delete and cleanup any unused
ps_query("DELETE FROM filter WHERE ref=?", array("i",$filter));
ps_query("DELETE FROM filter_rule WHERE filter NOT IN (SELECT ref FROM filter)");
ps_query("DELETE FROM filter_rule_node WHERE filter_rule NOT IN (SELECT ref FROM filter_rule)");
ps_query("DELETE FROM filter_rule WHERE ref NOT IN (SELECT DISTINCT filter_rule FROM filter_rule_node)");
return true;
}
/**
* Delete specified filter_rule
*
* @param int $filter - ID of filter_rule
*
* @return boolean | integer - false, or ID of filter_rule
*/
function delete_filter_rule($filter_rule)
{
if (!is_numeric($filter_rule)) {
return false;
}
// Delete and cleanup any unused nodes
ps_query("DELETE FROM filter_rule WHERE ref=?", array("i",$filter_rule));
ps_query("DELETE FROM filter_rule_node WHERE filter_rule NOT IN (SELECT ref FROM filter_rule)");
ps_query("DELETE FROM filter_rule WHERE ref NOT IN (SELECT DISTINCT filter_rule FROM filter_rule_node)");
return true;
}
/**
* Copy specified filter_rule
*
* @param int $filter - ID of filter_rule to copy
*
* @return boolean | integer - false, or ID of new filter
*/
function copy_filter($filter)
{
if (!is_numeric($filter)) {
return false;
}
ps_query("INSERT INTO filter (name, filter_condition) SELECT name, filter_condition FROM filter WHERE ref=?", array("i",$filter));
$newfilter = sql_insert_id();
$rules = ps_array("SELECT ref value from filter_rule WHERE filter=?", array("i",$filter));
foreach ($rules as $rule) {
ps_query("INSERT INTO filter_rule (filter) VALUES (?)", array("i",$newfilter));
$newrule = sql_insert_id();
ps_query("INSERT INTO filter_rule_node (filter_rule, node_condition, node) SELECT ? , node_condition, node FROM filter_rule_node WHERE filter_rule=?", array("i",$newrule,"i",$rule));
}
return $newfilter;
}
/**
* Add POST/GET parameters into search string. Moved from pages/search.php
*
* @param string $search Existing search string without params added
*
* @return string Updated string with params added
*/
function update_search_from_request($search)
{
global $config_separators,$resource_field_verbatim_keyword_regex;
reset($_POST);
reset($_GET);
foreach (array_merge($_GET, $_POST) as $key => $value) {
if (is_string($value)) {
$value = trim($value);
}
if ($value != "") {
if (substr($key, 0, 6) == "field_") {
if ((string_ends_with($key, "-y") !== false) || (string_ends_with($key, "-m") !== false) || (string_ends_with($key, "_day") !== false)) {
# Date field
# Construct the date from the supplied dropdown values
$key_part = substr($key, 0, strrpos($key, "-"));
$field = substr($key_part, 6);
$value = "";
if (strpos($search, $field . ":") === false) {
$key_year = $key_part . "-y";
$value_year = getval($key_year, "");
if ($value_year != "") {
$value = $value_year;
} else {
$value = "nnnn";
}
$key_month = $key_part . "-m";
$value_month = getval($key_month, "");
if ($value_month == "") {
$value_month .= "nn";
}
$key_day = $key_part . "-d";
$value_day = getval($key_day, "");
if ($value_day != "") {
$value .= "|" . $value_month . "|" . $value_day;
} elseif ($value_month != "nn") {
$value .= "|" . $value_month;
}
$search = (($search == "") ? "" : join(", ", split_keywords($search)) . ", ") . $field . ":" . $value;
}
} elseif (strpos($key, "_drop_") !== false) {
# Dropdown field
# Add keyword exactly as it is as the full value is indexed as a single keyword for dropdown boxes.
$search = (($search == "") ? "" : join(", ", split_keywords($search, false, false, false, false, true)) . ", ") . substr($key, 11) . ":" . $value;
} elseif (strpos($key, "_cat_") !== false) {
# Category tree field
# Add keyword exactly as it is as the full value is indexed as a single keyword for dropdown boxes.
$value = str_replace(",", ";", $value);
if (substr($value, 0, 1) == ";") {
$value = substr($value, 1);
}
$search = (($search == "") ? "" : join(", ", split_keywords($search, false, false, false, false, true)) . ", ") . substr($key, 10) . ":" . $value;
} else {
# Standard field
if (
isset($resource_field_verbatim_keyword_regex[substr($key, 6)])
&& preg_match($resource_field_verbatim_keyword_regex[substr($key, 6)], str_replace('*', '', $value))
) {
$values = explode(' ', mb_strtolower(trim_spaces(str_replace($config_separators, ' ', $value)), 'UTF-8'));
} else {
$values = [$value];
}
foreach ($values as $value) {
# Standard field
$search = (($search == "") ? "" : join(", ", split_keywords($search, false, false, false, false, true)) . ", ") . substr($key, 6) . ":" . $value;
}
}
} elseif ('' != $value && is_iterable($value) && substr($key, 0, 14) == 'nodes_searched') {
// Nodes can be searched directly when displayed on simple search bar
// Note: intially they come grouped by field as we need to know whether if
// there is a OR case involved (ie. @@101@@102)
$node_ref = '';
foreach ($value as $searched_field_nodes) {
// Fields that are displayed as a dropdown will only pass one node ID
if (!is_array($searched_field_nodes) && '' == $searched_field_nodes) {
continue;
} elseif (!is_array($searched_field_nodes)) {
$node_ref .= ', ' . NODE_TOKEN_PREFIX . $searched_field_nodes;
continue;
}
// For fields that can pass multiple node IDs at a time
$node_ref .= ', ';
foreach ($searched_field_nodes as $searched_node_ref) {
$node_ref .= NODE_TOKEN_PREFIX . $searched_node_ref;
}
}
if ($node_ref !== '') {
$search .= ", " . $node_ref;
}
}
}
}
$year = getval("basicyear", "");
if ($year != "") {
$search = (($search == "") ? "" : join(", ", split_keywords($search, false, false, false, false, true)) . ", ") . "basicyear:" . $year;
}
$month = getval("basicmonth", "");
if ($month != "") {
$search = (($search == "") ? "" : join(", ", split_keywords($search, false, false, false, false, true)) . ", ") . "basicmonth:" . $month;
}
$day = getval("basicday", "");
if ($day != "") {
$search = (($search == "") ? "" : join(", ", split_keywords($search, false, false, false, false, true)) . ", ") . "basicday:" . $day;
}
return $search;
}
/**
* Retrieves the default resource types for search functionality.
*
* This function determines which resource types to include in the search based on the global
* settings for resource and theme inclusion. If resources are to be included, it checks the
* default resource types and returns them as an array. If no specific default resource types
* are defined, it defaults to including "Global." If resources are not to be included, it
* defaults to "Collections," and if themes are included, "FeaturedCollections" is also added.
*
* @return array An array of default resource types to be used in the search.
*/
function get_search_default_restypes()
{
global $search_includes_resources, $search_includes_themes,$default_res_types;
$defaultrestypes = array();
if ($search_includes_resources) {
if ($default_res_types == "") {
$defaultrestypes[] = "Global";
} else {
$defaultrestypes = (is_array($default_res_types) ? $default_res_types : explode(",", (string) $default_res_types));
}
} else {
$defaultrestypes[] = "Collections";
if ($search_includes_themes) {
$defaultrestypes[] = "FeaturedCollections";
}
}
return $defaultrestypes;
}
/**
* Retrieves the selected resource types for the search functionality.
*
*
* @return array An array of selected resource types for the search.
*/
function get_selectedtypes()
{
global $search_includes_resources, $default_advanced_search_mode;
$restypes = getval("restypes", "");
$advanced_search_section = getval("advanced_search_section", "");
# If advanced_search_section is absent then load it from restypes
if (
getval("submitted", "") == ""
&& !isset($advanced_search_section)
) {
$advanced_search_section = $restypes;
}
# If clearbutton pressed then the selected types are reset based on configuration settings
if (getval('resetform', '') != '') {
if (isset($default_advanced_search_mode)) {
$selectedtypes = explode(',', trim($default_advanced_search_mode, ' ,'));
} else {
if ($search_includes_resources) {
$selectedtypes = array('Global', 'Media');
} else {
$selectedtypes = array('Collections');
}
}
} else # Not clearing, so get the currently selected types
{
$selectedtypes = explode(',', $advanced_search_section);
}
return $selectedtypes;
}
/**
* Renders the buttons for the advanced search form.
*
* This function generates HTML for two buttons:
* one to reset the search form and clear the submitted search criteria,
* and another to execute the search.
*
* @return void This function outputs HTML directly and does not return a value.
*/
function render_advanced_search_buttons()
{
global $lang, $baseurl_short;
?>
<div class="QuestionSubmit QuestionSticky">
<input name="resetform" class="resetform" type="submit" onClick="unsetCookie('search_form_submit','<?php echo $baseurl_short; ?>')" value="<?php echo escape($lang["clearbutton"]); ?>" />
&nbsp;
<input name="dosearch" class="dosearch" type="submit" value="<?php echo escape($lang["action-viewmatchingresults"]); ?>" />
</div>
<?php
}
/**
* If a "fieldX" order_by is used, check it's a valid value
*
* @param string string of order by
*/
function check_order_by_in_table_joins($order_by)
{
global $lang;
if (substr($order_by, 0, 5) == "field" && !in_array(substr($order_by, 5), get_resource_table_joins())) {
exit($lang['error_invalid_input'] . ":- <pre>order_by : " . escape($order_by) . "</pre>");
}
}
/**
* Get collection total resource count for a list of collections
*
* @param array $refs List of collection IDs
*
* @return array Returns table of collections and their total resource count (taking into account access controls). Please
* note that the returned array might NOT contain keys for all the input IDs (e.g validation failed).
*/
function get_collections_resource_count(array $refs)
{
$return = [];
foreach ($refs as $ref) {
if (!(is_int_loose($ref) && $ref > 0)) {
continue;
}
$colresults = do_search("!collection{$ref}", '', 'relevance', '0', [0,0]);
if (!isset($colresults["total"])) {
continue;
}
$return[$ref] = $colresults["total"];
}
return $return;
}
/**
* Get all search request parameters. Note that this does not escape the
* parameters which must be sanitised using e.g. htmlspecialchars() or urlencode() before rendering on page
*
* @return array()
*/
function get_search_params()
{
$searchparams = array(
"search" => "",
"restypes" => "",
"archive" => "",
"order_by" => "",
"sort" => "",
"offset" => "",
"k" => "",
"access" => "",
"foredit" => "",
"recentdaylimit" => "",
"go" => "",
);
$requestparams = array();
foreach ($searchparams as $searchparam => $default) {
$requestparams[$searchparam] = getval($searchparam, $default);
}
return $requestparams;
}
/**
* Helper function to check a string is not just the asterisk.
*
* @param string $str The string to be checked.
*
* @return boolean
*/
function is_not_wildcard_only(string $str)
{
return trim($str) !== '*';
}
/**
* Convert node searches into a friendly syntax. Used by search_title_processing.php
*
* @param string $string Search string
* @return string
*/
function search_title_node_processing($string)
{
if (substr(ltrim($string), 0, 2) == NODE_TOKEN_PREFIX) {
# convert to shortname:value
$node_id = substr(ltrim($string), 2);
$node_data = array();
get_node($node_id, $node_data);
$field_title = ps_value("select name value from resource_type_field where ref=?", array("i",$node_data['resource_type_field']), '', 'schema');
return $field_title . ":" . $node_data['name'];
}
return $string;
}
/**
* Allow $fetchrows as supplied to do_search() to support an integer or array. If integer then search will recieve the number of rows with no offset.
* If array then search will receive the number of rows to return and an offset allowing for chunking of results.
* $chunk_offset[0] is the offset of the first row to return. $chunk_offset[1] is the number of rows to return in the batch. $chunk_offset[0] will normally be 0 in the first search,
* increasing by $chunk_offset[1] for each search, generated by an external looping structure. This allows for batches of $chunk_offset[1] search results up to the total size of the search.
* For an example {@see pages/csv_export_results_metadata.php}. This approach can be used to avoid particularly large searches exceeding the PHP memory_limit when processing the data in ps_query().
*
* @param int|array $fetchrows $fetchrows value passed from do_search() / search_special(). See details above.
* @param int $chunk_offset Starting position for offset. Default is 0 if none supplied i.e. $fetchrows is int.
* @param int $search_chunk_size Number of rows to return.
*
* @return void
*/
function setup_search_chunks($fetchrows, ?int &$chunk_offset, ?int &$search_chunk_size): void
{
if (is_array($fetchrows) && isset($fetchrows[0]) && isset($fetchrows[1])) {
$chunk_offset = max((int)$fetchrows[0], 0);
$search_chunk_size = (int)$fetchrows[1];
} else {
$chunk_offset = 0;
$search_chunk_size = (int)$fetchrows;
}
}
/**
* Log which keywords are used in a search
*
* @param array $keywords refs of keywords used in a search
* @param array $search_results result of the search
*/
function log_keyword_usage($keywords, $search_result)
{
if (is_array($keywords) && count($keywords) > 0) {
if (array_key_exists('total', $search_result)) {
$count = $search_result['total'];
} elseif (is_array($search_result)) {
$count = count($search_result);
}
$log_code = (!isset($count) || $count > 0 ? 'Keyword usage' : 'Keyword usage - no results found');
foreach ($keywords as $keyword) {
daily_stat($log_code, $keyword);
}
}
}
/**
* Validate and set the order_by for the current search from the requested values passed to do_search()
*
* @param string $search
* @param string $order_by
* @param string $sort
*
* @return string
*
*/
function set_search_order_by(string $search, string $order_by, string $sort): string
{
if (!validate_sort_value($sort)) {
$sort = 'asc';
}
$order_by_date_sql_comma = ",";
$order_by_date = "r.ref $sort";
if (metadata_field_view_access($GLOBALS["date_field"])) {
$order_by_date_sql = "field" . (int) $GLOBALS["date_field"] . " " . $sort;
$order_by_date_sql_comma = ", {$order_by_date_sql}, ";
$order_by_date = "{$order_by_date_sql}, r.ref {$sort}";
}
# Check if order_by is empty string as this avoids 'relevance' default
if ($order_by === "") {
$order_by = "relevance";
}
$order = [
"relevance" => "score $sort, user_rating $sort, total_hit_count $sort {$order_by_date_sql_comma} r.ref $sort",
"popularity" => "user_rating $sort, total_hit_count $sort {$order_by_date_sql_comma} r.ref $sort",
"rating" => "r.rating $sort, user_rating $sort, score $sort, r.ref $sort",
"date" => "$order_by_date, r.ref $sort",
"colour" => "has_image $sort, image_blue $sort, image_green $sort, image_red $sort {$order_by_date_sql_comma} r.ref $sort",
"title" => "field" . (int) $GLOBALS["view_title_field"] . " " . $sort . ", r.ref $sort",
"file_path" => "file_path $sort, r.ref $sort",
"resourceid" => "r.ref $sort",
"resourcetype" => "order_by $sort, resource_type $sort, r.ref $sort",
"extension" => "file_extension $sort, r.ref $sort",
"status" => "archive $sort, r.ref $sort",
"modified" => "modified $sort, r.ref $sort"
];
// Used for collection sort order as sortorder is ASC, date is DESC
$revsort = (strtoupper($sort) == 'DESC') ? "ASC" : " DESC";
// These options are only supported if the default field 3 is still present
if (in_array(3, get_resource_table_joins())) {
$order["country"] = "field3 $sort, r.ref $sort";
$order["titleandcountry"] = "field" . $GLOBALS["view_title_field"] . " $sort, field3 $sort, r.ref $sort";
}
// Add collection sort option only if searching a collection
if (substr($search, 0, 11) == '!collection') {
$order["collection"] = "c.sortorder $sort,c.date_added $revsort,r.ref $sort";
}
# Check if date_field is being used as this will be needed in the inner select to be used in ordering
$GLOBALS["include_fieldx"] = false;
if (isset($order_by_date_sql) && array_key_exists($order_by, $order) && strpos($order[$order_by], $order_by_date_sql) !== false) {
$GLOBALS["include_fieldx"] = true;
}
# Append order by field to the above array if absent and if named "fieldn" (where n is one or more digits)
if (!in_array($order_by, $order) && (substr($order_by, 0, 5) == "field")) {
if (!is_numeric(str_replace("field", "", $order_by))) {
exit("Order field incorrect.");
}
# If fieldx is being used this will be needed in the inner select to be used in ordering
$GLOBALS["include_fieldx"] = true;
# Check for field type
$field_order_check = ps_query(
"SELECT field_constraint, sort_method
FROM resource_type_field
WHERE ref = ?
LIMIT 1",
["i",str_replace("field", "", $order_by)],
"",
"schema"
)[0];
# Establish sort order (numeric or otherwise)
# Attach ref as a final key to foster stable result sets which should eliminate resequencing when moving <- and -> through resources (in view.php)
if ($field_order_check["sort_method"] == FIELD_SORT_METHODS['dot-notation']) {
$order[$order_by] =
"CASE WHEN TRIM($order_by) = '' THEN 0 ELSE 1 END $sort,
ISNULL(REGEXP_SUBSTR(SUBSTRING_INDEX($order_by, '.', 1), '[^0-9]+')) $sort,
IFNULL(REGEXP_SUBSTR(SUBSTRING_INDEX($order_by, '.', 1), '[^0-9]+'), '') $sort,
CAST(REGEXP_SUBSTR(SUBSTRING_INDEX($order_by, '.', 1), '[0-9]+') AS UNSIGNED) $sort,";
for ($n = 2; $n <= 10; $n++) {
$order[$order_by] .= "\nCAST(SUBSTRING_INDEX(SUBSTRING_INDEX($order_by, '.', $n), '.', -1) AS UNSIGNED) $sort,";
}
$order[$order_by] .= "\nref $sort";
} elseif ($field_order_check["field_constraint"] == 1) {
$order[$order_by] = "$order_by +0 $sort,r.ref $sort";
} else {
$order[$order_by] = "$order_by $sort,r.ref $sort";
}
}
hook("modifyorderarray");
$order_by = (isset($order[$order_by]) ? $order[$order_by] : (substr($search, 0, 11) == '!collection' ? $order['collection'] : $order['relevance'])); // fail safe by falling back to default if not found
return $order_by;
}