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