= ? 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 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 ! 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,"", "<","="), " ", $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 ­ 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(' ', ' ', $string); $string = str_replace('­', ' ', $string); $string = str_replace('‘', ' ', $string); $string = str_replace('’', ' ', $string); $string = str_replace('“', ' ', $string); $string = str_replace('”', ' ', $string); $string = str_replace('–', ' ', $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. & 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&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 # 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 = '#(%s)#'; } // 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("\(", '', $text); $text = str_replace("\)", '', $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; ?>
" />   " />
order_by : " . escape($order_by) . ""); } } /** * 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; }