xref: /openbmc/bmcweb/features/redfish/include/utils/query_param.hpp (revision c1a75ebc267a78853fb26a3da8c6b3388e6ee07c)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 #include "bmcweb_config.h"
5 
6 #include "app.hpp"
7 #include "async_resp.hpp"
8 #include "error_code.hpp"
9 #include "error_messages.hpp"
10 #include "filter_expr_executor.hpp"
11 #include "filter_expr_parser_ast.hpp"
12 #include "filter_expr_printer.hpp"
13 #include "http_request.hpp"
14 #include "http_response.hpp"
15 #include "json_formatters.hpp"
16 #include "logging.hpp"
17 #include "redfishoemrule.hpp"
18 #include "str_utility.hpp"
19 #include "utils/json_utils.hpp"
20 
21 #include <unistd.h>
22 
23 #include <boost/beast/http/status.hpp>
24 #include <boost/beast/http/verb.hpp>
25 #include <boost/url/params_view.hpp>
26 #include <nlohmann/json.hpp>
27 
28 #include <algorithm>
29 #include <array>
30 #include <cctype>
31 #include <charconv>
32 #include <cstddef>
33 #include <cstdint>
34 #include <functional>
35 #include <iterator>
36 #include <limits>
37 #include <map>
38 #include <memory>
39 #include <optional>
40 #include <ranges>
41 #include <string>
42 #include <string_view>
43 #include <system_error>
44 #include <utility>
45 #include <vector>
46 
47 namespace redfish
48 {
49 namespace query_param
50 {
51 
52 enum class ExpandType : uint8_t
53 {
54     None,
55     Links,
56     NotLinks,
57     Both,
58 };
59 
60 // A simple implementation of Trie to help |recursiveSelect|.
61 class SelectTrieNode
62 {
63   public:
64     SelectTrieNode() = default;
65 
66     const SelectTrieNode* find(const std::string& jsonKey) const
67     {
68         auto it = children.find(jsonKey);
69         if (it == children.end())
70         {
71             return nullptr;
72         }
73         return &it->second;
74     }
75 
76     // Creates a new node if the key doesn't exist, returns the reference to the
77     // newly created node; otherwise, return the reference to the existing node
78     SelectTrieNode* emplace(std::string_view jsonKey)
79     {
80         auto [it, _] = children.emplace(jsonKey, SelectTrieNode{});
81         return &it->second;
82     }
83 
84     bool empty() const
85     {
86         return children.empty();
87     }
88 
89     void clear()
90     {
91         children.clear();
92     }
93 
94     void setToSelected()
95     {
96         selected = true;
97     }
98 
99     bool isSelected() const
100     {
101         return selected;
102     }
103 
104   private:
105     std::map<std::string, SelectTrieNode, std::less<>> children;
106     bool selected = false;
107 };
108 
109 // Validates the property in the $select parameter. Every character is among
110 // [a-zA-Z0-9#@_.] (taken from Redfish spec, section 9.6 Properties)
111 inline bool isSelectedPropertyAllowed(std::string_view property)
112 {
113     // These a magic number, but with it it's less likely that this code
114     // introduces CVE; e.g., too large properties crash the service.
115     constexpr int maxPropertyLength = 60;
116     if (property.empty() || property.size() > maxPropertyLength)
117     {
118         return false;
119     }
120     for (char ch : property)
121     {
122         if (std::isalnum(static_cast<unsigned char>(ch)) == 0 && ch != '#' &&
123             ch != '@' && ch != '.')
124         {
125             return false;
126         }
127     }
128     return true;
129 }
130 
131 struct SelectTrie
132 {
133     SelectTrie() = default;
134 
135     // Inserts a $select value; returns false if the nestedProperty is illegal.
136     bool insertNode(std::string_view nestedProperty)
137     {
138         if (nestedProperty.empty())
139         {
140             return false;
141         }
142         SelectTrieNode* currNode = &root;
143         size_t index = nestedProperty.find_first_of('/');
144         while (!nestedProperty.empty())
145         {
146             std::string_view property = nestedProperty.substr(0, index);
147             if (!isSelectedPropertyAllowed(property))
148             {
149                 return false;
150             }
151             currNode = currNode->emplace(property);
152             if (index == std::string::npos)
153             {
154                 break;
155             }
156             nestedProperty.remove_prefix(index + 1);
157             index = nestedProperty.find_first_of('/');
158         }
159         currNode->setToSelected();
160         return true;
161     }
162 
163     SelectTrieNode root;
164 };
165 
166 // The struct stores the parsed query parameters of the default Redfish route.
167 struct Query
168 {
169     // Only
170     bool isOnly = false;
171 
172     // Expand
173     uint8_t expandLevel = 0;
174     ExpandType expandType = ExpandType::None;
175 
176     // Skip
177     std::optional<size_t> skip = std::nullopt;
178 
179     // Top
180     static constexpr size_t maxTop = 1000; // Max entries a response contain
181     std::optional<size_t> top = std::nullopt;
182 
183     // Filter
184     std::optional<filter_ast::LogicalAnd> filter = std::nullopt;
185 
186     // Select
187     // Unclear how to make this use structured initialization without this.
188     // Might be a tidy bug?  Ignore for now
189     // NOLINTNEXTLINE(readability-redundant-member-init)
190     SelectTrie selectTrie{};
191 };
192 
193 // The struct defines how resource handlers in redfish-core/lib/ can handle
194 // query parameters themselves, so that the default Redfish route will delegate
195 // the processing.
196 struct QueryCapabilities
197 {
198     bool canDelegateOnly = false;
199     bool canDelegateTop = false;
200     bool canDelegateSkip = false;
201     uint8_t canDelegateExpandLevel = 0;
202     bool canDelegateSelect = false;
203 };
204 
205 // Delegates query parameters according to the given |queryCapabilities|
206 // This function doesn't check query parameter conflicts since the parse
207 // function will take care of it.
208 // Returns a delegated query object which can be used by individual resource
209 // handlers so that handlers don't need to query again.
210 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
211 {
212     Query delegated{};
213     // delegate only
214     if (query.isOnly && queryCapabilities.canDelegateOnly)
215     {
216         delegated.isOnly = true;
217         query.isOnly = false;
218     }
219     // delegate expand as much as we can
220     if (query.expandType != ExpandType::None)
221     {
222         delegated.expandType = query.expandType;
223         if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
224         {
225             query.expandType = ExpandType::None;
226             delegated.expandLevel = query.expandLevel;
227             query.expandLevel = 0;
228         }
229         else
230         {
231             delegated.expandLevel = queryCapabilities.canDelegateExpandLevel;
232         }
233     }
234 
235     // delegate top
236     if (query.top && queryCapabilities.canDelegateTop)
237     {
238         delegated.top = query.top;
239         query.top = std::nullopt;
240     }
241 
242     // delegate skip
243     if (query.skip && queryCapabilities.canDelegateSkip)
244     {
245         delegated.skip = query.skip;
246         query.skip = 0;
247     }
248 
249     // delegate select
250     if (!query.selectTrie.root.empty() && queryCapabilities.canDelegateSelect)
251     {
252         delegated.selectTrie = std::move(query.selectTrie);
253         query.selectTrie.root.clear();
254     }
255     return delegated;
256 }
257 
258 inline bool getExpandType(std::string_view value, Query& query)
259 {
260     if (value.empty())
261     {
262         return false;
263     }
264     switch (value[0])
265     {
266         case '*':
267             query.expandType = ExpandType::Both;
268             break;
269         case '.':
270             query.expandType = ExpandType::NotLinks;
271             break;
272         case '~':
273             query.expandType = ExpandType::Links;
274             break;
275         default:
276             return false;
277     }
278     value.remove_prefix(1);
279     if (value.empty())
280     {
281         query.expandLevel = 1;
282         return true;
283     }
284     constexpr std::string_view levels = "($levels=";
285     if (!value.starts_with(levels))
286     {
287         return false;
288     }
289     value.remove_prefix(levels.size());
290 
291     auto it = std::from_chars(value.begin(), value.end(), query.expandLevel);
292     if (it.ec != std::errc())
293     {
294         return false;
295     }
296     value.remove_prefix(
297         static_cast<size_t>(std::distance(value.begin(), it.ptr)));
298     return value == ")";
299 }
300 
301 enum class QueryError
302 {
303     Ok,
304     OutOfRange,
305     ValueFormat,
306 };
307 
308 inline QueryError getNumericParam(std::string_view value, size_t& param)
309 {
310     std::from_chars_result r =
311         std::from_chars(value.begin(), value.end(), param);
312 
313     // If the number wasn't representable in the type, it's out of range
314     if (r.ec == std::errc::result_out_of_range)
315     {
316         return QueryError::OutOfRange;
317     }
318     // All other errors are value format
319     if (r.ec != std::errc())
320     {
321         return QueryError::ValueFormat;
322     }
323     return QueryError::Ok;
324 }
325 
326 inline QueryError getSkipParam(std::string_view value, Query& query)
327 {
328     return getNumericParam(value, query.skip.emplace());
329 }
330 
331 inline QueryError getTopParam(std::string_view value, Query& query)
332 {
333     QueryError ret = getNumericParam(value, query.top.emplace());
334     if (ret != QueryError::Ok)
335     {
336         return ret;
337     }
338 
339     // Range check for sanity.
340     if (query.top > Query::maxTop)
341     {
342         return QueryError::OutOfRange;
343     }
344 
345     return QueryError::Ok;
346 }
347 
348 // Parses and validates the $select parameter.
349 // As per OData URL Conventions and Redfish Spec, the $select values shall be
350 // comma separated Resource Path
351 // Ref:
352 // 1. https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
353 // 2.
354 // https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt
355 inline bool getSelectParam(std::string_view value, Query& query)
356 {
357     std::vector<std::string> properties;
358     bmcweb::split(properties, value, ',');
359     if (properties.empty())
360     {
361         return false;
362     }
363     // These a magic number, but with it it's less likely that this code
364     // introduces CVE; e.g., too large properties crash the service.
365     constexpr int maxNumProperties = 10;
366     if (properties.size() > maxNumProperties)
367     {
368         return false;
369     }
370     for (const auto& property : properties)
371     {
372         if (!query.selectTrie.insertNode(property))
373         {
374             return false;
375         }
376     }
377     return true;
378 }
379 
380 // Parses and validates the $filter parameter.
381 inline bool getFilterParam(std::string_view value, Query& query)
382 {
383     query.filter = parseFilter(value);
384     return query.filter.has_value();
385 }
386 
387 inline std::optional<Query> parseParameters(boost::urls::params_view urlParams,
388                                             crow::Response& res)
389 {
390     Query ret{};
391     for (const boost::urls::params_view::value_type& it : urlParams)
392     {
393         if (it.key == "only")
394         {
395             if (!it.value.empty())
396             {
397                 messages::queryParameterValueFormatError(res, it.value, it.key);
398                 return std::nullopt;
399             }
400             ret.isOnly = true;
401         }
402         else if (it.key == "$expand" && BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
403         {
404             if (!getExpandType(it.value, ret))
405             {
406                 messages::queryParameterValueFormatError(res, it.value, it.key);
407                 return std::nullopt;
408             }
409         }
410         else if (it.key == "$top")
411         {
412             QueryError topRet = getTopParam(it.value, ret);
413             if (topRet == QueryError::ValueFormat)
414             {
415                 messages::queryParameterValueFormatError(res, it.value, it.key);
416                 return std::nullopt;
417             }
418             if (topRet == QueryError::OutOfRange)
419             {
420                 messages::queryParameterOutOfRange(
421                     res, it.value, "$top",
422                     "0-" + std::to_string(Query::maxTop));
423                 return std::nullopt;
424             }
425         }
426         else if (it.key == "$skip")
427         {
428             QueryError topRet = getSkipParam(it.value, ret);
429             if (topRet == QueryError::ValueFormat)
430             {
431                 messages::queryParameterValueFormatError(res, it.value, it.key);
432                 return std::nullopt;
433             }
434             if (topRet == QueryError::OutOfRange)
435             {
436                 messages::queryParameterOutOfRange(
437                     res, it.value, it.key,
438                     "0-" + std::to_string(std::numeric_limits<size_t>::max()));
439                 return std::nullopt;
440             }
441         }
442         else if (it.key == "$select")
443         {
444             if (!getSelectParam(it.value, ret))
445             {
446                 messages::queryParameterValueFormatError(res, it.value, it.key);
447                 return std::nullopt;
448             }
449         }
450         else if (it.key == "$filter" && BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
451         {
452             if (!getFilterParam(it.value, ret))
453             {
454                 messages::queryParameterValueFormatError(res, it.value, it.key);
455                 return std::nullopt;
456             }
457         }
458         else
459         {
460             // Intentionally ignore other errors Redfish spec, 7.3.1
461             if (it.key.starts_with("$"))
462             {
463                 // Services shall return... The HTTP 501 Not Implemented
464                 // status code for any unsupported query parameters that
465                 // start with $ .
466                 messages::queryParameterValueFormatError(res, it.value, it.key);
467                 res.result(boost::beast::http::status::not_implemented);
468                 return std::nullopt;
469             }
470             // "Shall ignore unknown or unsupported query parameters that do
471             // not begin with $ ."
472         }
473     }
474 
475     if (ret.expandType != ExpandType::None && !ret.selectTrie.root.empty())
476     {
477         messages::queryCombinationInvalid(res);
478         return std::nullopt;
479     }
480 
481     return ret;
482 }
483 
484 inline bool processOnly(crow::App& app, crow::Response& res,
485                         std::function<void(crow::Response&)>& completionHandler)
486 {
487     BMCWEB_LOG_DEBUG("Processing only query param");
488     auto itMembers = res.jsonValue.find("Members");
489     if (itMembers == res.jsonValue.end())
490     {
491         messages::queryNotSupportedOnResource(res);
492         completionHandler(res);
493         return false;
494     }
495     auto itMemBegin = itMembers->begin();
496     if (itMemBegin == itMembers->end() || itMembers->size() != 1)
497     {
498         BMCWEB_LOG_DEBUG(
499             "Members contains {} element, returning full collection.",
500             itMembers->size());
501         completionHandler(res);
502         return false;
503     }
504 
505     auto itUrl = itMemBegin->find("@odata.id");
506     if (itUrl == itMemBegin->end())
507     {
508         BMCWEB_LOG_DEBUG("No found odata.id");
509         messages::internalError(res);
510         completionHandler(res);
511         return false;
512     }
513     const std::string* url = itUrl->get_ptr<const std::string*>();
514     if (url == nullptr)
515     {
516         BMCWEB_LOG_DEBUG("@odata.id wasn't a string????");
517         messages::internalError(res);
518         completionHandler(res);
519         return false;
520     }
521     // TODO(Ed) copy request headers?
522     // newReq.session = req.session;
523     std::error_code ec;
524     auto newReq = std::make_shared<crow::Request>(
525         crow::Request::Body{boost::beast::http::verb::get, *url, 11}, ec);
526     if (ec)
527     {
528         messages::internalError(res);
529         completionHandler(res);
530         return false;
531     }
532 
533     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
534     BMCWEB_LOG_DEBUG("setting completion handler on {}",
535                      logPtr(&asyncResp->res));
536     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
537     app.handle(newReq, asyncResp);
538     return true;
539 }
540 
541 struct ExpandNode
542 {
543     nlohmann::json::json_pointer location;
544     std::string uri;
545 
546     bool operator==(const ExpandNode& other) const
547     {
548         return location == other.location && uri == other.uri;
549     }
550 };
551 
552 inline void findNavigationReferencesInArrayRecursive(
553     ExpandType eType, nlohmann::json::array_t& array,
554     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
555     bool inLinks, std::vector<ExpandNode>& out);
556 
557 inline void findNavigationReferencesInObjectRecursive(
558     ExpandType eType, nlohmann::json::object_t& obj,
559     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
560     bool inLinks, std::vector<ExpandNode>& out);
561 
562 // Walks a json object looking for Redfish NavigationReference entries that
563 // might need resolved.  It recursively walks the jsonResponse object, looking
564 // for links at every level, and returns a list (out) of locations within the
565 // tree that need to be expanded.  The current json pointer location p is passed
566 // in to reference the current node that's being expanded, so it can be combined
567 // with the keys from the jsonResponse object
568 inline void findNavigationReferencesRecursive(
569     ExpandType eType, nlohmann::json& jsonResponse,
570     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
571     bool inLinks, std::vector<ExpandNode>& out)
572 {
573     // If no expand is needed, return early
574     if (eType == ExpandType::None)
575     {
576         return;
577     }
578 
579     nlohmann::json::array_t* array =
580         jsonResponse.get_ptr<nlohmann::json::array_t*>();
581     if (array != nullptr)
582     {
583         findNavigationReferencesInArrayRecursive(eType, *array, jsonPtr, depth,
584                                                  skipDepth, inLinks, out);
585     }
586     nlohmann::json::object_t* obj =
587         jsonResponse.get_ptr<nlohmann::json::object_t*>();
588     if (obj == nullptr)
589     {
590         return;
591     }
592     findNavigationReferencesInObjectRecursive(eType, *obj, jsonPtr, depth,
593                                               skipDepth, inLinks, out);
594 }
595 
596 inline void findNavigationReferencesInArrayRecursive(
597     ExpandType eType, nlohmann::json::array_t& array,
598     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
599     bool inLinks, std::vector<ExpandNode>& out)
600 {
601     size_t index = 0;
602     // For arrays, walk every element in the array
603     for (auto& element : array)
604     {
605         nlohmann::json::json_pointer newPtr = jsonPtr / index;
606         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr.to_string());
607         findNavigationReferencesRecursive(eType, element, newPtr, depth,
608                                           skipDepth, inLinks, out);
609         index++;
610     }
611 }
612 
613 inline void findNavigationReferencesInObjectRecursive(
614     ExpandType eType, nlohmann::json::object_t& obj,
615     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
616     bool inLinks, std::vector<ExpandNode>& out)
617 {
618     // Navigation References only ever have a single element
619     if (obj.size() == 1)
620     {
621         if (obj.begin()->first == "@odata.id")
622         {
623             const std::string* uri =
624                 obj.begin()->second.get_ptr<const std::string*>();
625             if (uri != nullptr)
626             {
627                 BMCWEB_LOG_DEBUG("Found {} at {}", *uri, jsonPtr.to_string());
628                 if (skipDepth == 0)
629                 {
630                     out.push_back({jsonPtr, *uri});
631                 }
632                 return;
633             }
634         }
635     }
636 
637     int newDepth = depth;
638     auto odataId = obj.find("@odata.id");
639     if (odataId != obj.end())
640     {
641         // The Redfish spec requires all resources to include the resource
642         // identifier.  If the object has multiple elements and one of them is
643         // "@odata.id" then that means we have entered a new level / expanded
644         // resource.  We need to stop traversing if we're already at the desired
645         // depth
646         if (obj.size() > 1)
647         {
648             if (depth == 0)
649             {
650                 return;
651             }
652             if (skipDepth > 0)
653             {
654                 skipDepth--;
655             }
656         }
657 
658         if (skipDepth == 0)
659         {
660             newDepth--;
661         }
662     }
663 
664     // Loop the object and look for links
665     for (auto& element : obj)
666     {
667         bool localInLinks = inLinks;
668         if (!localInLinks)
669         {
670             // Check if this is a links node
671             localInLinks = element.first == "Links";
672         }
673         // Only traverse the parts of the tree the user asked for
674         // Per section 7.3 of the redfish specification
675         if (localInLinks && eType == ExpandType::NotLinks)
676         {
677             continue;
678         }
679         if (!localInLinks && eType == ExpandType::Links)
680         {
681             continue;
682         }
683         nlohmann::json::json_pointer newPtr = jsonPtr / element.first;
684         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr);
685 
686         findNavigationReferencesRecursive(eType, element.second, newPtr,
687                                           newDepth, skipDepth, localInLinks,
688                                           out);
689     }
690 }
691 
692 // TODO: When aggregation is enabled and we receive a partially expanded
693 // response we may need need additional handling when the original URI was
694 // up tree from a top level collection.
695 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556
696 // lands.  May want to avoid forwarding query params when request is uptree from
697 // a top level collection.
698 inline std::vector<ExpandNode> findNavigationReferences(
699     ExpandType eType, int depth, int skipDepth, nlohmann::json& jsonResponse)
700 {
701     std::vector<ExpandNode> ret;
702     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
703     // SkipDepth +1 since we are skipping the root by default.
704     findNavigationReferencesRecursive(eType, jsonResponse, root, depth,
705                                       skipDepth + 1, false, ret);
706     return ret;
707 }
708 
709 // Formats a query parameter string for the sub-query.
710 // Returns std::nullopt on failures.
711 // This function shall handle $select when it is added.
712 // There is no need to handle parameters that's not compatible with $expand,
713 // e.g., $only, since this function will only be called in side $expand handlers
714 inline std::optional<std::string> formatQueryForExpand(const Query& query)
715 {
716     // query.expandLevel<=1: no need to do subqueries
717     if (query.expandLevel <= 1)
718     {
719         return "";
720     }
721     std::string str = "?$expand=";
722     switch (query.expandType)
723     {
724         case ExpandType::Links:
725             str += '~';
726             break;
727         case ExpandType::NotLinks:
728             str += '.';
729             break;
730         case ExpandType::Both:
731             str += '*';
732             break;
733         case ExpandType::None:
734             return "";
735         default:
736             return std::nullopt;
737     }
738     str += "($levels=";
739     str += std::to_string(query.expandLevel - 1);
740     str += ')';
741     return str;
742 }
743 
744 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
745 {
746   public:
747     // This object takes a single asyncResp object as the "final" one, then
748     // allows callers to attach sub-responses within the json tree that need
749     // to be executed and filled into their appropriate locations.  This
750     // class manages the final "merge" of the json resources.
751     MultiAsyncResp(crow::App& appIn,
752                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
753         app(&appIn), finalRes(std::move(finalResIn))
754     {}
755 
756     explicit MultiAsyncResp(std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
757         app(nullptr), finalRes(std::move(finalResIn))
758     {}
759 
760     void addAwaitingResponse(
761         const std::shared_ptr<bmcweb::AsyncResp>& res,
762         const nlohmann::json::json_pointer& finalExpandLocation)
763     {
764         res->res.setCompleteRequestHandler(std::bind_front(
765             placeResultStatic, shared_from_this(), finalExpandLocation));
766     }
767 
768     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
769                      crow::Response& res)
770     {
771         BMCWEB_LOG_DEBUG("placeResult for {}", locationToPlace);
772         propogateError(finalRes->res, res);
773         if (!res.jsonValue.is_object() || res.jsonValue.empty())
774         {
775             return;
776         }
777         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
778         finalObj = std::move(res.jsonValue);
779     }
780 
781     // Handles the very first level of Expand, and starts a chain of sub-queries
782     // for deeper levels.
783     void startQuery(const Query& query, const Query& delegated)
784     {
785         std::vector<ExpandNode> nodes = findNavigationReferences(
786             query.expandType, query.expandLevel, delegated.expandLevel,
787             finalRes->res.jsonValue);
788         BMCWEB_LOG_DEBUG("{} nodes to traverse", nodes.size());
789         const std::optional<std::string> queryStr = formatQueryForExpand(query);
790         if (!queryStr)
791         {
792             messages::internalError(finalRes->res);
793             return;
794         }
795         for (const ExpandNode& node : nodes)
796         {
797             const std::string subQuery = node.uri + *queryStr;
798             BMCWEB_LOG_DEBUG("URL of subquery:  {}", subQuery);
799             std::error_code ec;
800             auto newReq = std::make_shared<crow::Request>(
801                 crow::Request::Body{boost::beast::http::verb::get, subQuery,
802                                     11},
803                 ec);
804             if (ec)
805             {
806                 messages::internalError(finalRes->res);
807                 return;
808             }
809 
810             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
811             BMCWEB_LOG_DEBUG("setting completion handler on {}",
812                              logPtr(&asyncResp->res));
813 
814             addAwaitingResponse(asyncResp, node.location);
815             if (app != nullptr)
816             {
817                 app->handle(newReq, asyncResp);
818             }
819         }
820     }
821 
822     static void startMultiFragmentHandle(
823         const std::shared_ptr<crow::Request>& req,
824         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
825         const std::shared_ptr<std::vector<OemBaseRule*>>& fragments,
826         const std::shared_ptr<std::vector<std::string>>& params,
827         const crow::Response& resIn)
828     {
829         asyncResp->res.jsonValue = resIn.jsonValue;
830         auto multi = std::make_shared<MultiAsyncResp>(asyncResp);
831         for (OemBaseRule* fragment : *fragments)
832         {
833             if (fragment != nullptr)
834             {
835                 OemBaseRule& fragmentRule = *fragment;
836                 auto rsp = std::make_shared<bmcweb::AsyncResp>();
837                 BMCWEB_LOG_DEBUG("Matched fragment GET rule '{}' {}",
838                                  fragmentRule.rule, req->methodString());
839                 BMCWEB_LOG_DEBUG(
840                     "Handling fragment rules: setting completion handler on {}",
841                     logPtr(&rsp->res));
842                 std::optional<nlohmann::json::json_pointer> jsonFragmentPtr =
843                     json_util::createJsonPointerFromFragment(fragmentRule.rule);
844                 if (jsonFragmentPtr)
845                 {
846                     multi->addAwaitingResponse(rsp, *jsonFragmentPtr);
847                     fragmentRule.handle(*req, rsp, *params);
848                 }
849             }
850         }
851     }
852 
853   private:
854     static void placeResultStatic(
855         const std::shared_ptr<MultiAsyncResp>& multi,
856         const nlohmann::json::json_pointer& locationToPlace,
857         crow::Response& res)
858     {
859         multi->placeResult(locationToPlace, res);
860     }
861 
862     crow::App* app;
863     std::shared_ptr<bmcweb::AsyncResp> finalRes;
864 };
865 
866 inline void processTopAndSkip(const Query& query, crow::Response& res)
867 {
868     if (!query.skip && !query.top)
869     {
870         // No work to do.
871         return;
872     }
873     nlohmann::json::object_t* obj =
874         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
875     if (obj == nullptr)
876     {
877         // Shouldn't be possible.  All responses should be objects.
878         messages::internalError(res);
879         return;
880     }
881 
882     BMCWEB_LOG_DEBUG("Handling top/skip");
883     nlohmann::json::object_t::iterator members = obj->find("Members");
884     if (members == obj->end())
885     {
886         // From the Redfish specification 7.3.1
887         // ... the HTTP 400 Bad Request status code with the
888         // QueryNotSupportedOnResource message from the Base Message Registry
889         // for any supported query parameters that apply only to resource
890         // collections but are used on singular resources.
891         messages::queryNotSupportedOnResource(res);
892         return;
893     }
894 
895     nlohmann::json::array_t* arr =
896         members->second.get_ptr<nlohmann::json::array_t*>();
897     if (arr == nullptr)
898     {
899         messages::internalError(res);
900         return;
901     }
902 
903     if (query.skip)
904     {
905         // Per section 7.3.1 of the Redfish specification, $skip is run before
906         // $top Can only skip as many values as we have
907         size_t skip = std::min(arr->size(), *query.skip);
908         arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
909     }
910     if (query.top)
911     {
912         size_t top = std::min(arr->size(), *query.top);
913         arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
914     }
915 }
916 
917 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are
918 // not in the |currNode| Trie node.
919 inline void recursiveSelect(nlohmann::json& currRoot,
920                             const SelectTrieNode& currNode)
921 {
922     nlohmann::json::object_t* object =
923         currRoot.get_ptr<nlohmann::json::object_t*>();
924     if (object != nullptr)
925     {
926         BMCWEB_LOG_DEBUG("Current JSON is an object");
927         auto it = currRoot.begin();
928         while (it != currRoot.end())
929         {
930             auto nextIt = std::next(it);
931             BMCWEB_LOG_DEBUG("key={}", it.key());
932             const SelectTrieNode* nextNode = currNode.find(it.key());
933             // Per the Redfish spec section 7.3.3, the service shall select
934             // certain properties as if $select was omitted. This applies to
935             // every TrieNode that contains leaves and the root.
936             constexpr std::array<std::string_view, 5> reservedProperties = {
937                 "@odata.id", "@odata.type", "@odata.context", "@odata.etag",
938                 "error"};
939             bool reserved = std::ranges::find(reservedProperties, it.key()) !=
940                             reservedProperties.end();
941             if (reserved || (nextNode != nullptr && nextNode->isSelected()))
942             {
943                 it = nextIt;
944                 continue;
945             }
946             if (nextNode != nullptr)
947             {
948                 BMCWEB_LOG_DEBUG("Recursively select: {}", it.key());
949                 recursiveSelect(*it, *nextNode);
950                 it = nextIt;
951                 continue;
952             }
953             BMCWEB_LOG_DEBUG("{} is getting removed!", it.key());
954             it = currRoot.erase(it);
955         }
956     }
957     nlohmann::json::array_t* array =
958         currRoot.get_ptr<nlohmann::json::array_t*>();
959     if (array != nullptr)
960     {
961         BMCWEB_LOG_DEBUG("Current JSON is an array");
962         // Array index is omitted, so reuse the same Trie node
963         for (nlohmann::json& nextRoot : *array)
964         {
965             recursiveSelect(nextRoot, currNode);
966         }
967     }
968 }
969 
970 // The current implementation of $select still has the following TODOs due to
971 //  ambiguity and/or complexity.
972 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was
973 // created for clarification.
974 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*),
975 // etc.
976 inline void processSelect(crow::Response& intermediateResponse,
977                           const SelectTrieNode& trieRoot)
978 {
979     BMCWEB_LOG_DEBUG("Process $select quary parameter");
980     recursiveSelect(intermediateResponse.jsonValue, trieRoot);
981 }
982 
983 inline void processAllParams(
984     crow::App& app, const Query& query, const Query& delegated,
985     std::function<void(crow::Response&)>& completionHandler,
986     crow::Response& intermediateResponse)
987 {
988     if (!completionHandler)
989     {
990         BMCWEB_LOG_DEBUG("Function was invalid?");
991         return;
992     }
993 
994     BMCWEB_LOG_DEBUG("Processing query params");
995     // If the request failed, there's no reason to even try to run query
996     // params.
997     if (intermediateResponse.resultInt() < 200 ||
998         intermediateResponse.resultInt() >= 400)
999     {
1000         completionHandler(intermediateResponse);
1001         return;
1002     }
1003     if (query.isOnly)
1004     {
1005         processOnly(app, intermediateResponse, completionHandler);
1006         return;
1007     }
1008 
1009     if (query.top || query.skip)
1010     {
1011         processTopAndSkip(query, intermediateResponse);
1012     }
1013 
1014     if (query.expandType != ExpandType::None)
1015     {
1016         BMCWEB_LOG_DEBUG("Executing expand query");
1017         auto asyncResp = std::make_shared<bmcweb::AsyncResp>(
1018             std::move(intermediateResponse));
1019 
1020         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
1021         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
1022         multi->startQuery(query, delegated);
1023         return;
1024     }
1025 
1026     if (query.filter)
1027     {
1028         applyFilterToCollection(intermediateResponse.jsonValue, *query.filter);
1029     }
1030 
1031     // According to Redfish Spec Section 7.3.1, $select is the last parameter to
1032     // to process
1033     if (!query.selectTrie.root.empty())
1034     {
1035         processSelect(intermediateResponse, query.selectTrie.root);
1036     }
1037 
1038     completionHandler(intermediateResponse);
1039 }
1040 
1041 } // namespace query_param
1042 } // namespace redfish
1043