to OR) $node_bucket = array(); // add to this normal array to exclude nodes from entire search $node_bucket_not = array(); // Take the current search URL and extract any nodes (putting into buckets) removing terms from $search resolve_given_nodes($search, $node_bucket, $node_bucket_not); $searchidmatch = false; // Used to check if ok to skip keyword due to a match with resource type/resource ID if (is_int_loose($search)) { // Resource ID is no longer indexed, if search is just for a single integer then include this $searchidmatch = ps_value("SELECT COUNT(*) AS value FROM resource WHERE ref = ?", ["i",$search], 0) != 0; } // Resource type is no longer indexed $restypenames = get_resource_types(); # Extract search parameters and split to keywords. $search_params = $search; if (substr($search, 0, 1) == "!" && substr($search, 0, 6) != "!empty") { # Special search, discard the special search identifier when splitting keywords and extract the search parameters $s = strpos($search, " "); if ($s === false) { $search_params = ""; # No search specified } else { $search_params = substr($search, $s + 1); # Extract search params } } if ($search_params != "") { if (preg_match('/^[^\\s]+$/',$search) && ($wildcard_always_applied || strpos($search,"*") !== false)) { $keywords = [$search_params]; } else { $keywords = split_keywords($search_params, false, false, false, false, true); } } else { $keywords = array(); } $search = trim($search); $keywords = array_values(array_filter(array_unique($keywords), 'is_not_wildcard_only')); $modified_keywords = hook('dosearchmodifykeywords', '', array($keywords, $search)); if ($modified_keywords) { $keywords = $modified_keywords; } # -- Build up filter SQL that will be used for all queries $sql_filter = new PreparedStatementQuery(); $sql_filter = search_filter($search, $archive, $restypes, $recent_search_daylimit, $access_override, $return_disk_usage, $editable_only, $access, $smartsearch); debug("do_search(): \$sql_filter = '" . $sql_filter->sql . "', parameters = ['" . implode("','", $sql_filter->parameters) . "']"); # Initialise variables. $sql = ""; $sql_keyword_union = array(); // An array of all the unions - at least one for each keyword //$sql_keyword_union_params = array(); $sql_keyword_union_aggregation = array(); // This is added to the SELECT statement. Normally 'BIT_OR(`keyword_[union_index]_found`) AS `keyword_[union_index]_found`', where '[union_index]' will be replaced $sql_keyword_union_criteria = array(); // Criteria for the union to be true - normally '`h`.`keyword_[union_index]_found`', where '[union_index]' will be replaced // For each union sql_keyword_union_or must be set // This will normally will be false to ensure that all keywords are found // Needs to be set to false when keywords are expanded and an extra $sql_keyword_union element is added (e.g. for wildcards) so that a match on any is ok $sql_keyword_union_or = array(); $sql_join = new PreparedStatementQuery(); $sql_join->sql = ""; $sql_join->parameters = []; # If returning disk used by the resources in the search results ($return_disk_usage=true) then wrap the returned SQL in an outer query that sums disk usage. $sql_prefix = ""; $sql_suffix = ""; if ($return_disk_usage) { $sql_prefix = "SELECT sum(disk_usage) total_disk_usage, count(*) total_resources, resourcelist.ref, resourcelist.score, resourcelist.user_rating, resourcelist.total_hit_count FROM ("; $sql_suffix = ") resourcelist"; } # ------ Advanced 'custom' permissions, need to join to access table. if ((!checkperm("v")) && !$access_override) { # one extra join (rca2) is required for user specific permissions (enabling more intelligent watermarks in search view) # the original join is used to gather group access into the search query as well. $sql_join->sql = " LEFT OUTER JOIN resource_custom_access rca2 ON r.ref=rca2.resource AND rca2.user = ? AND (rca2.user_expires IS null or rca2.user_expires>now()) AND rca2.access<>2 "; array_push($sql_join->parameters, "i", $userref); $sql_join->sql .= " LEFT OUTER JOIN resource_custom_access rca ON r.ref=rca.resource AND rca.usergroup = ? AND rca.access<>2 "; array_push($sql_join->parameters, "i", $usergroup); if ($sql_filter->sql != "") { $sql_filter->sql .= " AND "; } # If rca.resource is null, then no matching custom access record was found # If r.access is also 3 (custom) then the user is not allowed access to this resource. # Note that it's normal for null to be returned if this is a resource with non custom permissions (r.access<>3). $sql_filter->sql .= " NOT (rca.resource IS null AND r.access=3)"; } # Join thumbs_display_fields to resource table $select->sql = "r.ref, r.resource_type, r.has_image, r.is_transcoding, r.creation_date, r.rating, r.user_rating, r.user_rating_count, r.user_rating_total, r.file_extension, r.preview_extension, r.image_red, r.image_green, r.image_blue, r.thumb_width, r.thumb_height, r.archive, r.access, 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 "; $select->parameters = array(); $sql_hitcount_select = "r.hit_count"; $modified_select = hook('modifyselect'); $select->sql .= $modified_select ? $modified_select : ''; // modify select hook 1 $modified_select2 = hook('modifyselect2'); $select->sql .= $modified_select2 ? $modified_select2 : ''; // modify select hook 2 $select->sql .= $return_disk_usage ? ',r.disk_usage' : ''; // disk usage // Get custom group and user access rights if available to generate resultant_access, // otherwise select null values so columns can still be used regardless // This can then be passed through to access checking functions in order to eliminate many single queries. if (!checkperm("v") && !$access_override) { // Get custom access $select->sql .= ",rca.access group_access,rca2.access user_access "; if (!checkperm("g") && !$internal_share_access) { // Restrict all resources by default if (is_int_loose($userderestrictfilter) && $userderestrictfilter > 0) { // Add exemption for custom access $derestrict_filter_sql = get_filter_sql($userderestrictfilter); if (is_a($derestrict_filter_sql, "PreparedStatementQuery")) { $access_sql = "CASE WHEN " . $derestrict_filter_sql->sql . " THEN 0 ELSE 1 END"; $select->sql .= ", LEAST(IFNULL(rca.access, $access_sql), IFNULL(rca2.access, $access_sql)) resultant_access "; // Add node parameters to the start $select->parameters = array_merge( $select->parameters, $derestrict_filter_sql->parameters, $derestrict_filter_sql->parameters, ); } } else { // Set access to restricted unless custom access is set $select->sql .= ", LEAST(IFNULL(rca.access, 1), IFNULL(rca2.access, 1)) resultant_access "; } } else { // Consider X permission else apply standard resultant_access for this user based on resource access and custom access $sql_restricted_types = array(); foreach ($restypenames as $res_check_type) { if (checkperm('X' . $res_check_type['ref'])) { $sql_restricted_types[] = $res_check_type['ref']; } } // Custom access can override X permission. Note: rows not returned for custom access "Confidential" so X can't increase access here. if (count($sql_restricted_types) > 0) { $select->sql .= ", CASE "; foreach ($sql_restricted_types as $sql_restricted_type) { $select->sql .= "WHEN resource_type = ? THEN LEAST(IFNULL(rca.access, 1), IFNULL(rca2.access, 1)) "; $select->parameters[] = 'i'; $select->parameters[] = $sql_restricted_type; } $select->sql .= "ELSE LEAST(IFNULL(rca.access, r.access), IFNULL(rca2.access, r.access)) END resultant_access "; } else { $select->sql .= ", LEAST(IFNULL(rca.access, r.access), IFNULL(rca2.access, r.access)) resultant_access "; } } } else { $select->sql .= ", null group_access, null user_access "; $select->sql .= ", r.access resultant_access "; } # add 'joins' to select (only add fields if not returning the refs only) $joins = $return_refs_only === false || $GLOBALS["include_fieldx"] === true ? get_resource_table_joins() : array(); foreach ($joins as $datajoin) { if (metadata_field_view_access($datajoin) || $datajoin == $GLOBALS["view_title_field"]) { $select->sql .= ", r.field{$datajoin} "; } } # Prepare SQL to add join table for all provided keywords $suggested = $keywords; # a suggested search $fullmatch = true; $c = 0; $t = new PreparedStatementQuery(); $t2 = new PreparedStatementQuery(); $score = ""; # Do not process if a numeric search is provided (resource ID) global $keysearch; $keysearch = !($config_search_for_number && is_numeric($search)); # 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(); // ******************************************************************************* // order by RESOURCE TYPE // ******************************************************************************* $sql_join->sql .= " JOIN resource_type AS rty ON r.resource_type = rty.ref "; $select->sql .= ", rty.order_by "; hook("search_pipeline_setup", "", array($search, $select, $sql_join , $sql_filter, $sql)); // Search pipeline - each step handles an aspect of search and adds to the assembled SQL. $return = include "do_search_keywords.php"; if ($return !== 1) { return $return; } // Forward any return from this include. $return = include "do_search_nodes.php"; if ($return !== 1) { return $return; } // Handle returns from this include. $return = include "do_search_suggest.php"; if ($return !== 1) { return $return; } // Handle returns from this include. $return = include "do_search_filtering.php"; if ($return !== 1) { return $return; } // Handle returns from this include. $return = include "do_search_union_assembly.php"; if ($return !== 1) { return $return; } // Handle returns from this include. # Handle numeric searches when $config_search_for_number=false, i.e. perform a normal search but include matches for resource ID first global $config_search_for_number; if (!$config_search_for_number && is_int_loose($search)) { # Always show exact resource matches first. $order_by = "(r.ref='" . $search . "') desc," . $order_by; } # Can only search for resources that belong to featured collections. Doesn't apply to user's special upload collection to allow for upload then edit mode. $upload_collection = '!collection' . (0 - $userref); if (checkperm("J") && $search != $upload_collection) { $collection_join = " JOIN collection_resource AS jcr ON jcr.resource = r.ref JOIN collection AS jc ON jcr.collection = jc.ref"; $collection_join .= featured_collections_permissions_filter_sql("AND", "jc.ref", true); $sql_join->sql = $collection_join . $sql_join->sql; } # -------------------------------------------------------------------------------- # Special Searches (start with an exclamation mark) # -------------------------------------------------------------------------------- $sql_select = clone $select; $special_results = search_special($search, $sql_join, $fetchrows, $sql_prefix, $sql_suffix, $order_by, $orig_order, $sql_select, $sql_filter, $archive, $return_disk_usage, $return_refs_only, $returnsql); if ($special_results !== false) { log_keyword_usage($keywords_used, $special_results); return $special_results; } # ------------------------------------------------------------------------------------- # Standard Searches # ------------------------------------------------------------------------------------- # We've reached this far without returning. # This must be a standard (non-special) search. # Construct and perform the standard search query. $sql = new PreparedStatementQuery(); if ($sql_filter->sql != "") { if ($sql->sql != "") { $sql->sql .= " AND "; } $sql->sql .= $sql_filter->sql; $sql->parameters = array_merge($sql->parameters, $sql_filter->parameters); } # Append custom permissions $t->sql .= $sql_join->sql; $t->parameters = array_merge($t->parameters, $sql_join->parameters); if ($score == "") { $score = $sql_hitcount_select; } # In case score hasn't been set (i.e. empty search) if (($t2->sql != "") && ($sql->sql != "")) { $sql->sql = " AND " . $sql->sql; } # Compile final SQL $results_sql = new PreparedStatementQuery(); $results_sql->sql = $sql_prefix . "SELECT DISTINCT $score score, $select->sql FROM resource r" . $t->sql . " WHERE " . $t2->sql . $sql->sql . " GROUP BY r.ref, user_access, group_access ORDER BY " . $order_by . $sql_suffix; $results_sql->parameters = array_merge($select->parameters, $t->parameters, $t2->parameters, $sql->parameters); # Debug debug('$results_sql=' . $results_sql->sql . ", parameters: " . implode(",", $results_sql->parameters)); setup_search_chunks($fetchrows, $chunk_offset, $search_chunk_size); if ($return_refs_only) { # Execute query but only ask for ref columns back from ps_query(); # We force verbatim query mode on (and restore it afterwards) as there is no point trying to strip slashes etc. just for a ref column if ($returnsql) { return $results_sql; } $count_sql = clone $results_sql; $count_sql->sql = str_replace("ORDER BY " . $order_by, "", $count_sql->sql); $result = sql_limit_with_total_count($results_sql, $search_chunk_size, $chunk_offset, true, $count_sql); if (is_array($fetchrows)) { // Return without converting into the legacy padded array log_keyword_usage($keywords_used, $result); return $result; } $resultcount = $result["total"] ?? 0; if ($resultcount > 0 && count($result["data"]) > 0) { $result = array_map(function ($val) { return ["ref" => $val["ref"]]; }, $result["data"]); } elseif (!is_array($fetchrows)) { $result = []; } log_keyword_usage($keywords_used, $result); return $result; } else { # Execute query as normal if ($returnsql) { return $results_sql; } $count_sql = clone $results_sql; $count_sql->sql = str_replace("ORDER BY " . $order_by, "", $count_sql->sql); $result = sql_limit_with_total_count($results_sql, $search_chunk_size, $chunk_offset, true, $count_sql); } if (is_array($fetchrows)) { // Return without converting into the legacy padded array log_keyword_usage($keywords_used, $result); return $result; } $resultcount = $result["total"] ?? 0; if ($resultcount > 0 & count($result["data"]) > 0) { $result = $result['data']; if ($search_chunk_size !== -1) { // Only perform legacy padding of results if not all rows have been requested or total may be incorrect $diff = $resultcount - count($result); while ($diff > 0) { $result = array_merge($result, array_fill(0, ($diff < 1000000 ? $diff : 1000000), 0)); $diff -= 1000000; } } hook("beforereturnresults", "", array($result, $archive)); log_keyword_usage($keywords_used, $result); return $result; } else { $result = []; } hook('zero_search_results'); // No suggestions for field-specific searching or if just one keyword if (strpos($search, ":") !== false || count($keywords) === 1) { return ""; } # All keywords resolved OK, but there were no matches # Remove keywords, least used first, until we get results. $lsql = new PreparedStatementQuery(); $omitmatch = false; for ($n = 0; $n < count($keywords); $n++) { if (substr($keywords[$n], 0, 1) == "-") { $omitmatch = true; $omit = $keywords[$n]; } if ($lsql->sql != "") { $lsql->sql .= " OR "; } $lsql->sql .= "keyword = ?"; $lsql->parameters = array_merge($lsql->parameters, ["i", $keywords[$n]]); } if ($omitmatch) { return trim_spaces(str_replace(" " . $omit . " ", " ", " " . join(" ", $keywords) . " ")); } if ($lsql->sql != "") { $least = ps_value("SELECT keyword value FROM keyword WHERE " . $lsql->sql . " ORDER BY hit_count ASC LIMIT 1", $lsql->parameters, ""); return trim_spaces(str_replace(" " . $least . " ", " ", " " . join(" ", $keywords) . " ")); } else { return array(); } } // Take the current search URL and extract any nodes (putting into buckets) removing terms from $search // // Currently supports: // @@! (NOT) // @@@@ (OR) function resolve_given_nodes(&$search, &$node_bucket, &$node_bucket_not) { // extract all of the words, a word being a bunch of tokens with optional NOTs if (preg_match_all('/(' . NODE_TOKEN_PREFIX . NODE_TOKEN_NOT . '*\d+)+/', $search, $words) === false || count($words[0]) == 0) { return; } // spin through each of the words and process tokens foreach ($words[0] as $word) { $search = str_replace($word, '', $search); // remove the entire word from the search string $search = trim(trim($search), ','); preg_match_all('/' . NODE_TOKEN_PREFIX . '(' . NODE_TOKEN_NOT . '*)(\d+)/', $word, $tokens); if (count($tokens[1]) == 1 && $tokens[1][0] == NODE_TOKEN_NOT) { // you are currently only allowed NOT condition for a single token within a single word $node_bucket_not[] = $tokens[2][0]; // add the node number to the node_bucket_not continue; } $node_bucket[] = $tokens[2]; } }