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