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 
11 #include <sys/types.h>
12 
13 #include <boost/algorithm/string/classification.hpp>
14 #include <boost/algorithm/string/split.hpp>
15 #include <boost/beast/http/message.hpp> // IWYU pragma: keep
16 #include <boost/beast/http/status.hpp>
17 #include <boost/beast/http/verb.hpp>
18 #include <boost/url/params_view.hpp>
19 #include <nlohmann/json.hpp>
20 
21 #include <algorithm>
22 #include <array>
23 #include <cctype>
24 #include <charconv>
25 #include <compare>
26 #include <cstdint>
27 #include <functional>
28 #include <iterator>
29 #include <limits>
30 #include <map>
31 #include <memory>
32 #include <optional>
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     SelectTrie selectTrie = {};
184 };
185 
186 // The struct defines how resource handlers in redfish-core/lib/ can handle
187 // query parameters themselves, so that the default Redfish route will delegate
188 // the processing.
189 struct QueryCapabilities
190 {
191     bool canDelegateOnly = false;
192     bool canDelegateTop = false;
193     bool canDelegateSkip = false;
194     uint8_t canDelegateExpandLevel = 0;
195     bool canDelegateSelect = false;
196 };
197 
198 // Delegates query parameters according to the given |queryCapabilities|
199 // This function doesn't check query parameter conflicts since the parse
200 // function will take care of it.
201 // Returns a delegated query object which can be used by individual resource
202 // handlers so that handlers don't need to query again.
203 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
204 {
205     Query delegated;
206     // delegate only
207     if (query.isOnly && queryCapabilities.canDelegateOnly)
208     {
209         delegated.isOnly = true;
210         query.isOnly = false;
211     }
212     // delegate expand as much as we can
213     if (query.expandType != ExpandType::None)
214     {
215         delegated.expandType = query.expandType;
216         if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
217         {
218             query.expandType = ExpandType::None;
219             delegated.expandLevel = query.expandLevel;
220             query.expandLevel = 0;
221         }
222         else
223         {
224             query.expandLevel -= queryCapabilities.canDelegateExpandLevel;
225             delegated.expandLevel = queryCapabilities.canDelegateExpandLevel;
226         }
227     }
228 
229     // delegate top
230     if (query.top && queryCapabilities.canDelegateTop)
231     {
232         delegated.top = query.top;
233         query.top = std::nullopt;
234     }
235 
236     // delegate skip
237     if (query.skip && queryCapabilities.canDelegateSkip)
238     {
239         delegated.skip = query.skip;
240         query.skip = 0;
241     }
242 
243     // delegate select
244     if (!query.selectTrie.root.empty() && queryCapabilities.canDelegateSelect)
245     {
246         delegated.selectTrie = std::move(query.selectTrie);
247         query.selectTrie.root.clear();
248     }
249     return delegated;
250 }
251 
252 inline bool getExpandType(std::string_view value, Query& query)
253 {
254     if (value.empty())
255     {
256         return false;
257     }
258     switch (value[0])
259     {
260         case '*':
261             query.expandType = ExpandType::Both;
262             break;
263         case '.':
264             query.expandType = ExpandType::NotLinks;
265             break;
266         case '~':
267             query.expandType = ExpandType::Links;
268             break;
269         default:
270             return false;
271 
272             break;
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.data(), value.data() + value.size(),
288                               query.expandLevel);
289     if (it.ec != std::errc())
290     {
291         return false;
292     }
293     value.remove_prefix(static_cast<size_t>(it.ptr - value.data()));
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 =
307         std::from_chars(value.data(), value.data() + value.size(), 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     boost::split(properties, value, boost::is_any_of(","));
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 << "Members contains " << itMembers->size()
480                          << " element, returning full collection.";
481         completionHandler(res);
482         return false;
483     }
484 
485     auto itUrl = itMemBegin->find("@odata.id");
486     if (itUrl == itMemBegin->end())
487     {
488         BMCWEB_LOG_DEBUG << "No found odata.id";
489         messages::internalError(res);
490         completionHandler(res);
491         return false;
492     }
493     const std::string* url = itUrl->get_ptr<const std::string*>();
494     if (url == nullptr)
495     {
496         BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????";
497         messages::internalError(res);
498         completionHandler(res);
499         return false;
500     }
501     // TODO(Ed) copy request headers?
502     // newReq.session = req.session;
503     std::error_code ec;
504     crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec);
505     if (ec)
506     {
507         messages::internalError(res);
508         completionHandler(res);
509         return false;
510     }
511 
512     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
513     BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res;
514     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
515     asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper());
516     app.handle(newReq, asyncResp);
517     return true;
518 }
519 
520 struct ExpandNode
521 {
522     nlohmann::json::json_pointer location;
523     std::string uri;
524 
525     inline bool operator==(const ExpandNode& other) const
526     {
527         return location == other.location && uri == other.uri;
528     }
529 };
530 
531 // Walks a json object looking for Redfish NavigationReference entries that
532 // might need resolved.  It recursively walks the jsonResponse object, looking
533 // for links at every level, and returns a list (out) of locations within the
534 // tree that need to be expanded.  The current json pointer location p is passed
535 // in to reference the current node that's being expanded, so it can be combined
536 // with the keys from the jsonResponse object
537 inline void findNavigationReferencesRecursive(
538     ExpandType eType, nlohmann::json& jsonResponse,
539     const nlohmann::json::json_pointer& p, bool inLinks,
540     std::vector<ExpandNode>& out)
541 {
542     // If no expand is needed, return early
543     if (eType == ExpandType::None)
544     {
545         return;
546     }
547     nlohmann::json::array_t* array =
548         jsonResponse.get_ptr<nlohmann::json::array_t*>();
549     if (array != nullptr)
550     {
551         size_t index = 0;
552         // For arrays, walk every element in the array
553         for (auto& element : *array)
554         {
555             nlohmann::json::json_pointer newPtr = p / index;
556             BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string();
557             findNavigationReferencesRecursive(eType, element, newPtr, inLinks,
558                                               out);
559             index++;
560         }
561     }
562     nlohmann::json::object_t* obj =
563         jsonResponse.get_ptr<nlohmann::json::object_t*>();
564     if (obj == nullptr)
565     {
566         return;
567     }
568     // Navigation References only ever have a single element
569     if (obj->size() == 1)
570     {
571         if (obj->begin()->first == "@odata.id")
572         {
573             const std::string* uri =
574                 obj->begin()->second.get_ptr<const std::string*>();
575             if (uri != nullptr)
576             {
577                 BMCWEB_LOG_DEBUG << "Found element at " << p.to_string();
578                 out.push_back({p, *uri});
579             }
580         }
581     }
582     // Loop the object and look for links
583     for (auto& element : *obj)
584     {
585         bool localInLinks = inLinks;
586         if (!localInLinks)
587         {
588             // Check if this is a links node
589             localInLinks = element.first == "Links";
590         }
591         // Only traverse the parts of the tree the user asked for
592         // Per section 7.3 of the redfish specification
593         if (localInLinks && eType == ExpandType::NotLinks)
594         {
595             continue;
596         }
597         if (!localInLinks && eType == ExpandType::Links)
598         {
599             continue;
600         }
601         nlohmann::json::json_pointer newPtr = p / element.first;
602         BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr;
603 
604         findNavigationReferencesRecursive(eType, element.second, newPtr,
605                                           localInLinks, out);
606     }
607 }
608 
609 inline std::vector<ExpandNode>
610     findNavigationReferences(ExpandType eType, nlohmann::json& jsonResponse)
611 {
612     std::vector<ExpandNode> ret;
613     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
614     findNavigationReferencesRecursive(eType, jsonResponse, root, false, ret);
615     return ret;
616 }
617 
618 // Formats a query parameter string for the sub-query.
619 // Returns std::nullopt on failures.
620 // This function shall handle $select when it is added.
621 // There is no need to handle parameters that's not campatible with $expand,
622 // e.g., $only, since this function will only be called in side $expand handlers
623 inline std::optional<std::string> formatQueryForExpand(const Query& query)
624 {
625     // query.expandLevel<=1: no need to do subqueries
626     if (query.expandLevel <= 1)
627     {
628         return "";
629     }
630     std::string str = "?$expand=";
631     bool queryTypeExpected = false;
632     switch (query.expandType)
633     {
634         case ExpandType::None:
635             return "";
636         case ExpandType::Links:
637             queryTypeExpected = true;
638             str += '~';
639             break;
640         case ExpandType::NotLinks:
641             queryTypeExpected = true;
642             str += '.';
643             break;
644         case ExpandType::Both:
645             queryTypeExpected = true;
646             str += '*';
647             break;
648     }
649     if (!queryTypeExpected)
650     {
651         return std::nullopt;
652     }
653     str += "($levels=";
654     str += std::to_string(query.expandLevel - 1);
655     str += ')';
656     return str;
657 }
658 
659 // Propogates the worst error code to the final response.
660 // The order of error code is (from high to low)
661 // 500 Internal Server Error
662 // 511 Network Authentication Required
663 // 510 Not Extended
664 // 508 Loop Detected
665 // 507 Insufficient Storage
666 // 506 Variant Also Negotiates
667 // 505 HTTP Version Not Supported
668 // 504 Gateway Timeout
669 // 503 Service Unavailable
670 // 502 Bad Gateway
671 // 501 Not Implemented
672 // 401 Unauthorized
673 // 451 - 409 Error codes (not listed explictly)
674 // 408 Request Timeout
675 // 407 Proxy Authentication Required
676 // 406 Not Acceptable
677 // 405 Method Not Allowed
678 // 404 Not Found
679 // 403 Forbidden
680 // 402 Payment Required
681 // 400 Bad Request
682 inline unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode)
683 {
684     // We keep a explicit list for error codes that this project often uses
685     // Higer priority codes are in lower indexes
686     constexpr std::array<unsigned, 13> orderedCodes = {
687         500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400};
688     size_t finalCodeIndex = std::numeric_limits<size_t>::max();
689     size_t subResponseCodeIndex = std::numeric_limits<size_t>::max();
690     for (size_t i = 0; i < orderedCodes.size(); ++i)
691     {
692         if (orderedCodes[i] == finalCode)
693         {
694             finalCodeIndex = i;
695         }
696         if (orderedCodes[i] == subResponseCode)
697         {
698             subResponseCodeIndex = i;
699         }
700     }
701     if (finalCodeIndex != std::numeric_limits<size_t>::max() &&
702         subResponseCodeIndex != std::numeric_limits<size_t>::max())
703     {
704         return finalCodeIndex <= subResponseCodeIndex ? finalCode
705                                                       : subResponseCode;
706     }
707     if (subResponseCode == 500 || finalCode == 500)
708     {
709         return 500;
710     }
711     if (subResponseCode > 500 || finalCode > 500)
712     {
713         return std::max(finalCode, subResponseCode);
714     }
715     if (subResponseCode == 401)
716     {
717         return subResponseCode;
718     }
719     return std::max(finalCode, subResponseCode);
720 }
721 
722 // Propogates all error messages into |finalResponse|
723 inline void propogateError(crow::Response& finalResponse,
724                            crow::Response& subResponse)
725 {
726     // no errors
727     if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400)
728     {
729         return;
730     }
731     messages::moveErrorsToErrorJson(finalResponse.jsonValue,
732                                     subResponse.jsonValue);
733     finalResponse.result(
734         propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt()));
735 }
736 
737 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
738 {
739   public:
740     // This object takes a single asyncResp object as the "final" one, then
741     // allows callers to attach sub-responses within the json tree that need
742     // to be executed and filled into their appropriate locations.  This
743     // class manages the final "merge" of the json resources.
744     MultiAsyncResp(crow::App& appIn,
745                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
746         app(appIn),
747         finalRes(std::move(finalResIn))
748     {}
749 
750     void addAwaitingResponse(
751         const std::shared_ptr<bmcweb::AsyncResp>& res,
752         const nlohmann::json::json_pointer& finalExpandLocation)
753     {
754         res->res.setCompleteRequestHandler(std::bind_front(
755             placeResultStatic, shared_from_this(), finalExpandLocation));
756     }
757 
758     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
759                      crow::Response& res)
760     {
761         BMCWEB_LOG_DEBUG << "placeResult for " << locationToPlace;
762         propogateError(finalRes->res, res);
763         if (!res.jsonValue.is_object() || res.jsonValue.empty())
764         {
765             return;
766         }
767         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
768         finalObj = std::move(res.jsonValue);
769     }
770 
771     // Handles the very first level of Expand, and starts a chain of sub-queries
772     // for deeper levels.
773     void startQuery(const Query& query)
774     {
775         std::vector<ExpandNode> nodes =
776             findNavigationReferences(query.expandType, finalRes->res.jsonValue);
777         BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse";
778         const std::optional<std::string> queryStr = formatQueryForExpand(query);
779         if (!queryStr)
780         {
781             messages::internalError(finalRes->res);
782             return;
783         }
784         for (const ExpandNode& node : nodes)
785         {
786             const std::string subQuery = node.uri + *queryStr;
787             BMCWEB_LOG_DEBUG << "URL of subquery:  " << subQuery;
788             std::error_code ec;
789             crow::Request newReq({boost::beast::http::verb::get, subQuery, 11},
790                                  ec);
791             if (ec)
792             {
793                 messages::internalError(finalRes->res);
794                 return;
795             }
796 
797             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
798             BMCWEB_LOG_DEBUG << "setting completion handler on "
799                              << &asyncResp->res;
800 
801             addAwaitingResponse(asyncResp, node.location);
802             app.handle(newReq, asyncResp);
803         }
804     }
805 
806   private:
807     static void
808         placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
809                           const nlohmann::json::json_pointer& locationToPlace,
810                           crow::Response& res)
811     {
812         multi->placeResult(locationToPlace, res);
813     }
814 
815     crow::App& app;
816     std::shared_ptr<bmcweb::AsyncResp> finalRes;
817 };
818 
819 inline void processTopAndSkip(const Query& query, crow::Response& res)
820 {
821     if (!query.skip && !query.top)
822     {
823         // No work to do.
824         return;
825     }
826     nlohmann::json::object_t* obj =
827         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
828     if (obj == nullptr)
829     {
830         // Shouldn't be possible.  All responses should be objects.
831         messages::internalError(res);
832         return;
833     }
834 
835     BMCWEB_LOG_DEBUG << "Handling top/skip";
836     nlohmann::json::object_t::iterator members = obj->find("Members");
837     if (members == obj->end())
838     {
839         // From the Redfish specification 7.3.1
840         // ... the HTTP 400 Bad Request status code with the
841         // QueryNotSupportedOnResource message from the Base Message Registry
842         // for any supported query parameters that apply only to resource
843         // collections but are used on singular resources.
844         messages::queryNotSupportedOnResource(res);
845         return;
846     }
847 
848     nlohmann::json::array_t* arr =
849         members->second.get_ptr<nlohmann::json::array_t*>();
850     if (arr == nullptr)
851     {
852         messages::internalError(res);
853         return;
854     }
855 
856     if (query.skip)
857     {
858         // Per section 7.3.1 of the Redfish specification, $skip is run before
859         // $top Can only skip as many values as we have
860         size_t skip = std::min(arr->size(), *query.skip);
861         arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
862     }
863     if (query.top)
864     {
865         size_t top = std::min(arr->size(), *query.top);
866         arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
867     }
868 }
869 
870 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are
871 // not in the |currNode| Trie node.
872 inline void recursiveSelect(nlohmann::json& currRoot,
873                             const SelectTrieNode& currNode)
874 {
875     nlohmann::json::object_t* object =
876         currRoot.get_ptr<nlohmann::json::object_t*>();
877     if (object != nullptr)
878     {
879         BMCWEB_LOG_DEBUG << "Current JSON is an object";
880         auto it = currRoot.begin();
881         while (it != currRoot.end())
882         {
883             auto nextIt = std::next(it);
884             BMCWEB_LOG_DEBUG << "key=" << it.key();
885             const SelectTrieNode* nextNode = currNode.find(it.key());
886             // Per the Redfish spec section 7.3.3, the service shall select
887             // certain properties as if $select was omitted. This applies to
888             // every TrieNode that contains leaves and the root.
889             constexpr std::array<std::string_view, 5> reservedProperties = {
890                 "@odata.id", "@odata.type", "@odata.context", "@odata.etag",
891                 "error"};
892             bool reserved =
893                 std::find(reservedProperties.begin(), reservedProperties.end(),
894                           it.key()) != reservedProperties.end();
895             if (reserved || (nextNode != nullptr && nextNode->isSelected()))
896             {
897                 it = nextIt;
898                 continue;
899             }
900             if (nextNode != nullptr)
901             {
902                 BMCWEB_LOG_DEBUG << "Recursively select: " << it.key();
903                 recursiveSelect(*it, *nextNode);
904                 it = nextIt;
905                 continue;
906             }
907             BMCWEB_LOG_DEBUG << it.key() << " is getting removed!";
908             it = currRoot.erase(it);
909         }
910     }
911     nlohmann::json::array_t* array =
912         currRoot.get_ptr<nlohmann::json::array_t*>();
913     if (array != nullptr)
914     {
915         BMCWEB_LOG_DEBUG << "Current JSON is an array";
916         // Array index is omitted, so reuse the same Trie node
917         for (nlohmann::json& nextRoot : *array)
918         {
919             recursiveSelect(nextRoot, currNode);
920         }
921     }
922 }
923 
924 // The current implementation of $select still has the following TODOs due to
925 //  ambiguity and/or complexity.
926 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was
927 // created for clarification.
928 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*),
929 // etc.
930 inline void processSelect(crow::Response& intermediateResponse,
931                           const SelectTrieNode& trieRoot)
932 {
933     BMCWEB_LOG_DEBUG << "Process $select quary parameter";
934     recursiveSelect(intermediateResponse.jsonValue, trieRoot);
935 }
936 
937 inline void
938     processAllParams(crow::App& app, const Query& query,
939                      std::function<void(crow::Response&)>& completionHandler,
940                      crow::Response& intermediateResponse)
941 {
942     if (!completionHandler)
943     {
944         BMCWEB_LOG_DEBUG << "Function was invalid?";
945         return;
946     }
947 
948     BMCWEB_LOG_DEBUG << "Processing query params";
949     // If the request failed, there's no reason to even try to run query
950     // params.
951     if (intermediateResponse.resultInt() < 200 ||
952         intermediateResponse.resultInt() >= 400)
953     {
954         completionHandler(intermediateResponse);
955         return;
956     }
957     if (query.isOnly)
958     {
959         processOnly(app, intermediateResponse, completionHandler);
960         return;
961     }
962 
963     if (query.top || query.skip)
964     {
965         processTopAndSkip(query, intermediateResponse);
966     }
967 
968     if (query.expandType != ExpandType::None)
969     {
970         BMCWEB_LOG_DEBUG << "Executing expand query";
971         auto asyncResp = std::make_shared<bmcweb::AsyncResp>(
972             std::move(intermediateResponse));
973 
974         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
975         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
976         multi->startQuery(query);
977         return;
978     }
979 
980     // According to Redfish Spec Section 7.3.1, $select is the last parameter to
981     // to process
982     if (!query.selectTrie.root.empty())
983     {
984         processSelect(intermediateResponse, query.selectTrie.root);
985     }
986 
987     completionHandler(intermediateResponse);
988 }
989 
990 } // namespace query_param
991 } // namespace redfish
992