• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MapServer / MapServer / 22113288002

17 Feb 2026 07:49PM UTC coverage: 41.892% (+0.09%) from 41.804%
22113288002

push

github

web-flow
Merge pull request #7433 from rouault/ogcapi_part3_queryables

OGCAPI Features Part 3 Filtering: add support for 'Queryables' and 'Queryables as query parameter'

224 of 245 new or added lines in 4 files covered. (91.43%)

328 existing lines in 4 files now uncovered.

63165 of 150782 relevant lines covered (41.89%)

25378.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.25
/src/mapogcapi.cpp
1
/**********************************************************************
2
 * $id$
3
 *
4
 * Project:  MapServer
5
 * Purpose:  OGCAPI Implementation
6
 * Author:   Steve Lime and the MapServer team.
7
 *
8
 **********************************************************************
9
 * Copyright (c) 1996-2005 Regents of the University of Minnesota.
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a
12
 * copy of this software and associated documentation files (the "Software"),
13
 * to deal in the Software without restriction, including without limitation
14
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15
 * and/or sell copies of the Software, and to permit persons to whom the
16
 * Software is furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies of this Software or works derived from this Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
24
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27
 ****************************************************************************/
28
#include "mapserver.h"
29
#include "mapogcapi.h"
30
#include "mapows.h"
31
#include "mapgml.h"
32
#include "maptime.h"
33
#include "mapogcfilter.h"
34

35
#include "cpl_conv.h"
36

37
#include "third-party/include_nlohmann_json.hpp"
38
#include "third-party/include_pantor_inja.hpp"
39

40
#include <algorithm>
41
#include <map>
42
#include <set>
43
#include <string>
44
#include <iostream>
45
#include <utility>
46

47
using namespace inja;
48
using json = nlohmann::json;
49

50
#define OGCAPI_DEFAULT_TITLE "MapServer OGC API"
51

52
/*
53
** HTML Templates
54
*/
55
#define OGCAPI_TEMPLATE_HTML_LANDING "landing.html"
56
#define OGCAPI_TEMPLATE_HTML_CONFORMANCE "conformance.html"
57
#define OGCAPI_TEMPLATE_HTML_COLLECTION "collection.html"
58
#define OGCAPI_TEMPLATE_HTML_COLLECTIONS "collections.html"
59
#define OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS "collection-items.html"
60
#define OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM "collection-item.html"
61
#define OGCAPI_TEMPLATE_HTML_COLLECTION_QUERYABLES "collection-queryables.html"
62
#define OGCAPI_TEMPLATE_HTML_OPENAPI "openapi.html"
63

64
#define OGCAPI_DEFAULT_LIMIT 10 // by specification
65
#define OGCAPI_MAX_LIMIT 10000
66

67
#define OGCAPI_DEFAULT_GEOMETRY_PRECISION 6
68

69
constexpr const char *EPSG_PREFIX_URL =
70
    "http://www.opengis.net/def/crs/EPSG/0/";
71
constexpr const char *CRS84_URL =
72
    "http://www.opengis.net/def/crs/OGC/1.3/CRS84";
73

74
#ifdef USE_OGCAPI_SVR
75

76
/** Returns whether we enforce compliance mode. Defaults to true */
77
static bool msOGCAPIComplianceMode(const mapObj *map) {
53✔
78
  const char *compliance_mode =
79
      msOWSLookupMetadata(&(map->web.metadata), "A", "compliance_mode");
53✔
80
  return compliance_mode == NULL || strcasecmp(compliance_mode, "true") == 0;
53✔
81
}
82

83
/*
84
** Returns a JSON object using and a description.
85
*/
86
void msOGCAPIOutputError(OGCAPIErrorType errorType,
17✔
87
                         const std::string &description) {
88
  const char *code = "";
17✔
89
  const char *status = "";
90
  switch (errorType) {
17✔
91
  case OGCAPI_SERVER_ERROR: {
×
92
    code = "ServerError";
×
93
    status = "500";
94
    break;
×
95
  }
96
  case OGCAPI_CONFIG_ERROR: {
2✔
97
    code = "ConfigError";
2✔
98
    status = "500";
99
    break;
2✔
100
  }
101
  case OGCAPI_PARAM_ERROR: {
14✔
102
    code = "InvalidParameterValue";
14✔
103
    status = "400";
104
    break;
14✔
105
  }
106
  case OGCAPI_NOT_FOUND_ERROR: {
1✔
107
    code = "NotFound";
1✔
108
    status = "404";
109
    break;
1✔
110
  }
111
  }
112

113
  json j = {{"code", code}, {"description", description}};
119✔
114

115
  msIO_setHeader("Content-Type", "%s", OGCAPI_MIMETYPE_JSON);
17✔
116
  msIO_setHeader("Status", "%s", status);
17✔
117
  msIO_sendHeaders();
17✔
118
  msIO_printf("%s\n", j.dump().c_str());
34✔
119
}
136✔
120

121
static int includeLayer(mapObj *map, layerObj *layer) {
50✔
122
  if (!msOWSRequestIsEnabled(map, layer, "AO", "OGCAPI", MS_FALSE) ||
100✔
123
      !msIsLayerSupportedForWFSOrOAPIF(layer) || !msIsLayerQueryable(layer)) {
100✔
124
    return MS_FALSE;
×
125
  } else {
126
    return MS_TRUE;
127
  }
128
}
129

130
/*
131
** Get stuff...
132
*/
133

134
/*
135
** Returns the value associated with an item from the request's query string and
136
*NULL if the item was not found.
137
*/
138
static const char *getRequestParameter(const cgiRequestObj *request,
222✔
139
                                       const char *item) {
140
  for (int i = 0; i < request->NumParams; i++) {
515✔
141
    if (strcmp(item, request->ParamNames[i]) == 0)
392✔
142
      return request->ParamValues[i];
99✔
143
  }
144

145
  return nullptr;
146
}
147

148
static int getMaxLimit(mapObj *map, layerObj *layer) {
31✔
149
  int max_limit = OGCAPI_MAX_LIMIT;
31✔
150
  const char *value;
151

152
  // check metadata, layer then map
153
  value = msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
31✔
154
  if (value == NULL)
31✔
155
    value = msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
31✔
156

157
  if (value != NULL) {
31✔
158
    int status = msStringToInt(value, &max_limit, 10);
21✔
159
    if (status != MS_SUCCESS)
21✔
160
      max_limit = OGCAPI_MAX_LIMIT; // conversion failed
×
161
  }
162

163
  return max_limit;
31✔
164
}
165

166
static int getDefaultLimit(const mapObj *map, const layerObj *layer) {
43✔
167
  int default_limit = OGCAPI_DEFAULT_LIMIT;
43✔
168

169
  // check metadata, layer then map
170
  const char *value =
171
      msOWSLookupMetadata(&(layer->metadata), "A", "default_limit");
43✔
172
  if (value == NULL)
43✔
173
    value = msOWSLookupMetadata(&(map->web.metadata), "A", "default_limit");
43✔
174

175
  if (value != NULL) {
43✔
176
    int status = msStringToInt(value, &default_limit, 10);
25✔
177
    if (status != MS_SUCCESS)
25✔
178
      default_limit = OGCAPI_DEFAULT_LIMIT; // conversion failed
×
179
  }
180

181
  return default_limit;
43✔
182
}
183

184
static std::string getExtraParameterString(const mapObj *map,
54✔
185
                                           const layerObj *layer) {
186

187
  std::string extra_params;
188

189
  // first check layer metadata if layer is not null
190
  if (layer) {
54✔
191
    const char *layerVal =
192
        msOWSLookupMetadata(&(layer->metadata), "AO", "extra_params");
36✔
193
    if (layerVal)
36✔
194
      extra_params = std::string("&") + layerVal;
12✔
195
  }
196

197
  if (extra_params.empty() && map) {
54✔
198
    const char *mapVal =
199
        msOWSLookupMetadata(&(map->web.metadata), "AO", "extra_params");
50✔
200
    if (mapVal)
50✔
201
      extra_params = std::string("&") + mapVal;
45✔
202
  }
203

204
  return extra_params;
54✔
205
}
206

207
static std::set<std::string>
208
getExtraParameters(const char *pszExtraParameters) {
12✔
209
  std::set<std::string> ret;
210
  for (const auto &param : msStringSplit(pszExtraParameters, '&')) {
38✔
211
    const auto keyValue = msStringSplit(param.c_str(), '=');
26✔
212
    if (!keyValue.empty())
26✔
213
      ret.insert(keyValue[0]);
214
  }
38✔
215
  return ret;
12✔
216
}
217

218
static std::set<std::string> getExtraParameters(const mapObj *map,
56✔
219
                                                const layerObj *layer) {
220

221
  // first check layer metadata if layer is not null
222
  if (layer) {
56✔
223
    const char *layerVal =
224
        msOWSLookupMetadata(&(layer->metadata), "AO", "extra_params");
42✔
225
    if (layerVal)
42✔
226
      return getExtraParameters(layerVal);
2✔
227
  }
228

229
  if (map) {
54✔
230
    const char *mapVal =
231
        msOWSLookupMetadata(&(map->web.metadata), "AO", "extra_params");
54✔
232
    if (mapVal)
54✔
233
      return getExtraParameters(mapVal);
10✔
234
  }
235

236
  return {};
44✔
237
}
238

239
static bool
240
msOOGCAPICheckQueryParameters(const mapObj *map, const cgiRequestObj *request,
53✔
241
                              const std::set<std::string> &allowedParameters) {
242
  if (msOGCAPIComplianceMode(map)) {
53✔
243
    for (int j = 0; j < request->NumParams; j++) {
149✔
244
      const char *paramName = request->ParamNames[j];
105✔
245
      if (allowedParameters.find(paramName) == allowedParameters.end()) {
210✔
246
        msOGCAPIOutputError(
9✔
247
            OGCAPI_PARAM_ERROR,
248
            (std::string("Unknown query parameter: ") + paramName).c_str());
9✔
249
        return false;
9✔
250
      }
251
    }
252
  }
253
  return true;
254
}
255

256
static const char *getItemAliasOrName(const layerObj *layer, const char *item) {
87✔
257
  std::string key = std::string(item) + "_alias";
174✔
258
  if (const char *value =
87✔
259
          msOWSLookupMetadata(&(layer->metadata), "OGA", key.c_str())) {
87✔
260
    return value;
33✔
261
  }
262
  return item;
263
}
264

265
/** Return the list of queryable items */
266
static std::vector<std::string> msOOGCAPIGetLayerQueryables(
34✔
267
    layerObj *layer, const std::set<std::string> &reservedParams, bool &error) {
268
  error = false;
34✔
269
  std::vector<std::string> queryableItems;
270
  if (const char *value =
34✔
271
          msOWSLookupMetadata(&(layer->metadata), "OGA", "queryable_items")) {
34✔
272
    queryableItems = msStringSplit(value, ',');
21✔
273
    if (!queryableItems.empty()) {
21✔
274
      if (msLayerOpen(layer) != MS_SUCCESS ||
42✔
275
          msLayerGetItems(layer) != MS_SUCCESS) {
21✔
NEW
276
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot get layer fields");
×
NEW
277
        return {};
×
278
      }
279
      if (queryableItems[0] == "all") {
21✔
280
        queryableItems.clear();
NEW
281
        for (int i = 0; i < layer->numitems; ++i) {
×
NEW
282
          if (reservedParams.find(layer->items[i]) == reservedParams.end()) {
×
NEW
283
            queryableItems.push_back(layer->items[i]);
×
284
          }
285
        }
286
      } else {
287
        std::set<std::string> validItems;
288
        for (int i = 0; i < layer->numitems; ++i) {
210✔
289
          validItems.insert(layer->items[i]);
378✔
290
        }
291
        for (auto &item : queryableItems) {
84✔
292
          if (validItems.find(item) == validItems.end()) {
63✔
293
            // This is not a known field
NEW
294
            msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
NEW
295
                                "Invalid item '" + item +
×
296
                                    "' in queryable_items");
NEW
297
            error = true;
×
NEW
298
            return {};
×
299
          } else if (reservedParams.find(item) != reservedParams.end()) {
63✔
300
            // Check clashes with OGC API Features reserved keywords (bbox,
301
            // etc.)
NEW
302
            msOGCAPIOutputError(
×
303
                OGCAPI_CONFIG_ERROR,
NEW
304
                "Item '" + item +
×
305
                    "' in queryable_items is a reserved parameter name");
NEW
306
            error = true;
×
NEW
307
            return {};
×
308
          } else {
309
            item = getItemAliasOrName(layer, item.c_str());
63✔
310
          }
311
        }
312
      }
313
    }
314
  }
315
  return queryableItems;
316
}
34✔
317

318
/*
319
** Returns the limit as an int - between 1 and getMaxLimit(). We always return a
320
*valid value...
321
*/
322
static int getLimit(mapObj *map, cgiRequestObj *request, layerObj *layer,
31✔
323
                    int *limit) {
324
  int status;
325
  const char *p;
326

327
  int max_limit;
328
  max_limit = getMaxLimit(map, layer);
31✔
329

330
  p = getRequestParameter(request, "limit");
31✔
331
  if (!p || (p && strlen(p) == 0)) { // missing or empty
31✔
332
    *limit = MS_MIN(getDefaultLimit(map, layer),
20✔
333
                    max_limit); // max could be smaller than the default
334
  } else {
335
    status = msStringToInt(p, limit, 10);
11✔
336
    if (status != MS_SUCCESS)
11✔
337
      return MS_FAILURE;
338

339
    if (*limit <= 0) {
11✔
340
      *limit = MS_MIN(getDefaultLimit(map, layer),
×
341
                      max_limit); // max could be smaller than the default
342
    } else {
343
      *limit = MS_MIN(*limit, max_limit);
11✔
344
    }
345
  }
346

347
  return MS_SUCCESS;
348
}
349

350
// Return the content of the "crs" member of the /collections/{name} response
351
static json getCrsList(mapObj *map, layerObj *layer) {
21✔
352
  char *pszSRSList = NULL;
21✔
353
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_FALSE,
21✔
354
                   &pszSRSList);
355
  if (!pszSRSList)
21✔
356
    msOWSGetEPSGProj(&(map->projection), &(map->web.metadata), "AOF", MS_FALSE,
×
357
                     &pszSRSList);
358
  json jCrsList;
359
  if (pszSRSList) {
21✔
360
    const auto tokens = msStringSplit(pszSRSList, ' ');
21✔
361
    for (const auto &crs : tokens) {
53✔
362
      if (crs.find("EPSG:") == 0) {
32✔
363
        if (jCrsList.empty()) {
11✔
364
          jCrsList.push_back(CRS84_URL);
42✔
365
        }
366
        const std::string url =
367
            std::string(EPSG_PREFIX_URL) + crs.substr(strlen("EPSG:"));
64✔
368
        jCrsList.push_back(url);
64✔
369
      }
370
    }
371
    msFree(pszSRSList);
21✔
372
  }
21✔
373
  return jCrsList;
21✔
374
}
375

376
// Return the content of the "storageCrs" member of the /collections/{name}
377
// response
378
static std::string getStorageCrs(layerObj *layer) {
13✔
379
  std::string storageCrs;
380
  char *pszFirstSRS = nullptr;
13✔
381
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_TRUE,
13✔
382
                   &pszFirstSRS);
383
  if (pszFirstSRS) {
13✔
384
    if (std::string(pszFirstSRS).find("EPSG:") == 0) {
26✔
385
      storageCrs =
386
          std::string(EPSG_PREFIX_URL) + (pszFirstSRS + strlen("EPSG:"));
39✔
387
    }
388
    msFree(pszFirstSRS);
13✔
389
  }
390
  return storageCrs;
13✔
391
}
392

393
/*
394
** Returns the bbox in output CRS (CRS84 by default, or "crs" request parameter
395
*when specified)
396
*/
397
static bool getBbox(mapObj *map, layerObj *layer, cgiRequestObj *request,
26✔
398
                    rectObj *bbox, projectionObj *outputProj) {
399
  int status;
400

401
  const char *bboxParam = getRequestParameter(request, "bbox");
26✔
402
  if (!bboxParam || strlen(bboxParam) == 0) { // missing or empty extent
26✔
403
    rectObj rect;
404
    if (FLTLayerSetInvalidRectIfSupported(layer, &rect, "AO")) {
19✔
405
      bbox->minx = rect.minx;
×
406
      bbox->miny = rect.miny;
×
407
      bbox->maxx = rect.maxx;
×
408
      bbox->maxy = rect.maxy;
×
409
    } else {
410
      // assign map->extent (no projection necessary)
411
      bbox->minx = map->extent.minx;
19✔
412
      bbox->miny = map->extent.miny;
19✔
413
      bbox->maxx = map->extent.maxx;
19✔
414
      bbox->maxy = map->extent.maxy;
19✔
415
    }
416
  } else {
19✔
417
    const auto tokens = msStringSplit(bboxParam, ',');
7✔
418
    if (tokens.size() != 4) {
7✔
419
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
420
      return false;
×
421
    }
422

423
    double values[4];
424
    for (int i = 0; i < 4; i++) {
35✔
425
      status = msStringToDouble(tokens[i].c_str(), &values[i]);
28✔
426
      if (status != MS_SUCCESS) {
28✔
427
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
428
        return false;
×
429
      }
430
    }
431

432
    bbox->minx = values[0]; // assign
7✔
433
    bbox->miny = values[1];
7✔
434
    bbox->maxx = values[2];
7✔
435
    bbox->maxy = values[3];
7✔
436

437
    // validate bbox is well-formed (degenerate is ok)
438
    if (MS_VALID_SEARCH_EXTENT(*bbox) != MS_TRUE) {
7✔
439
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
440
      return false;
×
441
    }
442

443
    std::string bboxCrs = "EPSG:4326";
7✔
444
    bool axisInverted =
445
        false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
446
    const char *bboxCrsParam = getRequestParameter(request, "bbox-crs");
7✔
447
    if (bboxCrsParam) {
7✔
448
      bool isExpectedCrs = false;
449
      for (const auto &crsItem : getCrsList(map, layer)) {
26✔
450
        if (bboxCrsParam == crsItem.get<std::string>()) {
22✔
451
          isExpectedCrs = true;
452
          break;
453
        }
454
      }
455
      if (!isExpectedCrs) {
4✔
456
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox-crs.");
2✔
457
        return false;
2✔
458
      }
459
      if (std::string(bboxCrsParam) != CRS84_URL) {
4✔
460
        if (std::string(bboxCrsParam).find(EPSG_PREFIX_URL) == 0) {
4✔
461
          const char *code = bboxCrsParam + strlen(EPSG_PREFIX_URL);
2✔
462
          bboxCrs = std::string("EPSG:") + code;
6✔
463
          axisInverted = msIsAxisInverted(atoi(code));
2✔
464
        }
465
      }
466
    }
467
    if (axisInverted) {
2✔
468
      std::swap(bbox->minx, bbox->miny);
469
      std::swap(bbox->maxx, bbox->maxy);
470
    }
471

472
    projectionObj bboxProj;
473
    msInitProjection(&bboxProj);
5✔
474
    msProjectionInheritContextFrom(&bboxProj, &(map->projection));
5✔
475
    if (msLoadProjectionString(&bboxProj, bboxCrs.c_str()) != 0) {
5✔
476
      msFreeProjection(&bboxProj);
×
477
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot process bbox-crs.");
×
478
      return false;
×
479
    }
480

481
    status = msProjectRect(&bboxProj, outputProj, bbox);
5✔
482
    msFreeProjection(&bboxProj);
5✔
483
    if (status != MS_SUCCESS) {
5✔
484
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
485
                          "Cannot reproject bbox from bbox-crs to output CRS.");
486
      return false;
×
487
    }
488
  }
7✔
489

490
  return true;
491
}
492

493
/*
494
** Returns the template directory location or NULL if it isn't set.
495
*/
496
std::string msOGCAPIGetTemplateDirectory(const mapObj *map, const char *key,
12✔
497
                                         const char *envvar) {
498
  const char *directory = NULL;
499

500
  if (map != NULL) {
12✔
501
    directory = msOWSLookupMetadata(&(map->web.metadata), "A", key);
10✔
502
  }
503

504
  if (directory == NULL) {
10✔
505
    directory = CPLGetConfigOption(envvar, NULL);
2✔
506
  }
507

508
  std::string s;
509
  if (directory != NULL) {
12✔
510
    s = directory;
511
    if (!s.empty() && (s.back() != '/' && s.back() != '\\')) {
12✔
512
      // add a trailing slash if missing
513
      std::string slash = "/";
3✔
514
#ifdef _WIN32
515
      slash = "\\";
516
#endif
517
      s += slash;
518
    }
519
  }
520

521
  return s;
12✔
522
}
523

524
/*
525
** Returns the service title from oga_{key} and/or ows_{key} or a default value
526
*if not set.
527
*/
528
static const char *getWebMetadata(const mapObj *map, const char *domain,
529
                                  const char *key, const char *defaultVal) {
530
  const char *value;
531

532
  if ((value = msOWSLookupMetadata(&(map->web.metadata), domain, key)) != NULL)
18✔
533
    return value;
534
  else
535
    return defaultVal;
1✔
536
}
537

538
/*
539
** Returns the service title from oga|ows_title or a default value if not set.
540
*/
541
static const char *getTitle(const mapObj *map) {
542
  return getWebMetadata(map, "OA", "title", OGCAPI_DEFAULT_TITLE);
543
}
544

545
/*
546
** Returns the API root URL from oga_onlineresource or builds a value if not
547
*set.
548
*/
549
std::string msOGCAPIGetApiRootUrl(const mapObj *map,
60✔
550
                                  const cgiRequestObj *request,
551
                                  const char *namespaces) {
552
  const char *root;
553
  if ((root = msOWSLookupMetadata(&(map->web.metadata), namespaces,
60✔
554
                                  "onlineresource")) != NULL) {
555
    return std::string(root);
58✔
556
  }
557

558
  std::string api_root;
559
  if (char *res = msBuildOnlineResource(NULL, request)) {
2✔
560
    api_root = res;
561
    free(res);
×
562

563
    // find last ogcapi in the string and strip the rest to get the root API
564
    std::size_t pos = api_root.rfind("ogcapi");
565
    if (pos != std::string::npos) {
×
566
      api_root = api_root.substr(0, pos + std::string("ogcapi").size());
×
567
    } else {
568
      // strip trailing '?' or '/' and append "/ogcapi"
569
      while (!api_root.empty() &&
×
570
             (api_root.back() == '?' || api_root.back() == '/')) {
×
571
        api_root.pop_back();
572
      }
573
      api_root += "/ogcapi";
574
    }
575
  }
576

577
  if (api_root.empty()) {
2✔
578
    api_root = "/ogcapi";
579
  }
580

581
  return api_root;
2✔
582
}
583

584
static json getFeatureConstant(const gmlConstantObj *constant) {
×
585
  json j; // empty (null)
586

587
  if (!constant)
×
588
    throw std::runtime_error("Null constant metadata.");
×
589
  if (!constant->value)
×
590
    return j;
591

592
  // initialize
593
  j = {{constant->name, constant->value}};
×
594

595
  return j;
×
596
}
×
597

598
static json getFeatureItem(const gmlItemObj *item, const char *value) {
364✔
599
  json j; // empty (null)
600
  const char *key;
601

602
  if (!item)
364✔
603
    throw std::runtime_error("Null item metadata.");
×
604
  if (!item->visible)
364✔
605
    return j;
606

607
  if (item->alias)
208✔
608
    key = item->alias;
76✔
609
  else
610
    key = item->name;
132✔
611

612
  // initialize
613
  j = {{key, value}};
832✔
614

615
  if (item->type &&
280✔
616
      (EQUAL(item->type, "Date") || EQUAL(item->type, "DateTime") ||
72✔
617
       EQUAL(item->type, "Time"))) {
54✔
618
    struct tm tm;
619
    if (msParseTime(value, &tm) == MS_TRUE) {
18✔
620
      char tmpValue[64];
621
      if (EQUAL(item->type, "Date"))
18✔
622
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02d",
×
623
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
×
624
      else if (EQUAL(item->type, "Time"))
18✔
625
        snprintf(tmpValue, sizeof(tmpValue), "%02d:%02d:%02dZ", tm.tm_hour,
×
626
                 tm.tm_min, tm.tm_sec);
627
      else
628
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02dT%02d:%02d:%02dZ",
18✔
629
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
18✔
630
                 tm.tm_min, tm.tm_sec);
631

632
      j = {{key, tmpValue}};
72✔
633
    }
634
  } else if (item->type &&
208✔
635
             (EQUAL(item->type, "Integer") || EQUAL(item->type, "Long"))) {
54✔
636
    try {
637
      j = {{key, std::stoll(value)}};
90✔
638
    } catch (const std::exception &) {
×
639
    }
×
640
  } else if (item->type && EQUAL(item->type, "Real")) {
172✔
641
    try {
642
      j = {{key, std::stod(value)}};
180✔
643
    } catch (const std::exception &) {
×
644
    }
×
645
  } else if (item->type && EQUAL(item->type, "Boolean")) {
136✔
646
    if (EQUAL(value, "0") || EQUAL(value, "false")) {
×
647
      j = {{key, false}};
×
648
    } else {
649
      j = {{key, true}};
×
650
    }
651
  }
652

653
  return j;
654
}
1,174✔
655

656
static double round_down(double value, int decimal_places) {
26✔
657
  const double multiplier = std::pow(10.0, decimal_places);
658
  return std::floor(value * multiplier) / multiplier;
26✔
659
}
660
// https://stackoverflow.com/questions/25925290/c-round-a-double-up-to-2-decimal-places
661
static double round_up(double value, int decimal_places) {
31,274✔
662
  const double multiplier = std::pow(10.0, decimal_places);
663
  return std::ceil(value * multiplier) / multiplier;
31,274✔
664
}
665

666
static json getFeatureGeometry(shapeObj *shape, int precision,
47✔
667
                               bool outputCrsAxisInverted) {
668
  json geometry; // empty (null)
669
  int *outerList = NULL, numOuterRings = 0;
670

671
  if (!shape)
47✔
672
    throw std::runtime_error("Null shape.");
×
673

674
  switch (shape->type) {
47✔
675
  case (MS_SHAPE_POINT):
23✔
676
    if (shape->numlines == 0 ||
23✔
677
        shape->line[0].numpoints == 0) // not enough info for a point
23✔
678
      return geometry;
679

680
    if (shape->line[0].numpoints == 1) {
23✔
681
      geometry["type"] = "Point";
23✔
682
      double x = shape->line[0].point[0].x;
23✔
683
      double y = shape->line[0].point[0].y;
23✔
684
      if (outputCrsAxisInverted)
23✔
685
        std::swap(x, y);
686
      geometry["coordinates"] = {round_up(x, precision),
46✔
687
                                 round_up(y, precision)};
115✔
688
    } else {
689
      geometry["type"] = "MultiPoint";
×
690
      geometry["coordinates"] = json::array();
×
691
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
692
        double x = shape->line[0].point[j].x;
×
693
        double y = shape->line[0].point[j].y;
×
694
        if (outputCrsAxisInverted)
×
695
          std::swap(x, y);
696
        geometry["coordinates"].push_back(
×
697
            {round_up(x, precision), round_up(y, precision)});
×
698
      }
699
    }
700
    break;
701
  case (MS_SHAPE_LINE):
×
702
    if (shape->numlines == 0 ||
×
703
        shape->line[0].numpoints < 2) // not enough info for a line
×
704
      return geometry;
705

706
    if (shape->numlines == 1) {
×
707
      geometry["type"] = "LineString";
×
708
      geometry["coordinates"] = json::array();
×
709
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
710
        double x = shape->line[0].point[j].x;
×
711
        double y = shape->line[0].point[j].y;
×
712
        if (outputCrsAxisInverted)
×
713
          std::swap(x, y);
714
        geometry["coordinates"].push_back(
×
715
            {round_up(x, precision), round_up(y, precision)});
×
716
      }
717
    } else {
718
      geometry["type"] = "MultiLineString";
×
719
      geometry["coordinates"] = json::array();
×
720
      for (int i = 0; i < shape->numlines; i++) {
×
721
        json part = json::array();
×
722
        for (int j = 0; j < shape->line[i].numpoints; j++) {
×
723
          double x = shape->line[i].point[j].x;
×
724
          double y = shape->line[i].point[j].y;
×
725
          if (outputCrsAxisInverted)
×
726
            std::swap(x, y);
727
          part.push_back({round_up(x, precision), round_up(y, precision)});
×
728
        }
729
        geometry["coordinates"].push_back(part);
×
730
      }
731
    }
732
    break;
733
  case (MS_SHAPE_POLYGON):
24✔
734
    if (shape->numlines == 0 ||
24✔
735
        shape->line[0].numpoints <
24✔
736
            4) // not enough info for a polygon (first=last)
737
      return geometry;
738

739
    outerList = msGetOuterList(shape);
24✔
740
    if (outerList == NULL)
24✔
741
      throw std::runtime_error("Unable to allocate list of outer rings.");
×
742
    for (int k = 0; k < shape->numlines; k++) {
54✔
743
      if (outerList[k] == MS_TRUE)
30✔
744
        numOuterRings++;
26✔
745
    }
746

747
    if (numOuterRings == 1) {
24✔
748
      geometry["type"] = "Polygon";
44✔
749
      geometry["coordinates"] = json::array();
22✔
750
      for (int i = 0; i < shape->numlines; i++) {
44✔
751
        json part = json::array();
22✔
752
        for (int j = 0; j < shape->line[i].numpoints; j++) {
15,577✔
753
          double x = shape->line[i].point[j].x;
15,555✔
754
          double y = shape->line[i].point[j].y;
15,555✔
755
          if (outputCrsAxisInverted)
15,555✔
756
            std::swap(x, y);
757
          part.push_back({round_up(x, precision), round_up(y, precision)});
93,330✔
758
        }
759
        geometry["coordinates"].push_back(part);
22✔
760
      }
761
    } else {
762
      geometry["type"] = "MultiPolygon";
4✔
763
      geometry["coordinates"] = json::array();
2✔
764

765
      for (int k = 0; k < shape->numlines; k++) {
10✔
766
        if (outerList[k] ==
8✔
767
            MS_TRUE) { // outer ring: generate polygon and add to coordinates
768
          int *innerList = msGetInnerList(shape, k, outerList);
4✔
769
          if (innerList == NULL) {
4✔
770
            msFree(outerList);
×
771
            throw std::runtime_error("Unable to allocate list of inner rings.");
×
772
          }
773

774
          json polygon = json::array();
4✔
775
          for (int i = 0; i < shape->numlines; i++) {
20✔
776
            if (i == k ||
16✔
777
                innerList[i] ==
12✔
778
                    MS_TRUE) { // add outer ring (k) and any inner rings
779
              json part = json::array();
8✔
780
              for (int j = 0; j < shape->line[i].numpoints; j++) {
54✔
781
                double x = shape->line[i].point[j].x;
46✔
782
                double y = shape->line[i].point[j].y;
46✔
783
                if (outputCrsAxisInverted)
46✔
784
                  std::swap(x, y);
785
                part.push_back(
184✔
786
                    {round_up(x, precision), round_up(y, precision)});
92✔
787
              }
788
              polygon.push_back(part);
8✔
789
            }
790
          }
791

792
          msFree(innerList);
4✔
793
          geometry["coordinates"].push_back(polygon);
4✔
794
        }
795
      }
796
    }
797
    msFree(outerList);
24✔
798
    break;
24✔
799
  default:
×
800
    throw std::runtime_error("Invalid shape type.");
×
801
    break;
802
  }
803

804
  return geometry;
805
}
806

807
/*
808
** Return a GeoJSON representation of a shape.
809
*/
810
static json getFeature(layerObj *layer, shapeObj *shape, gmlItemListObj *items,
47✔
811
                       gmlConstantListObj *constants, int geometry_precision,
812
                       bool outputCrsAxisInverted) {
813
  int i;
814
  json feature; // empty (null)
815

816
  if (!layer || !shape)
47✔
817
    throw std::runtime_error("Null arguments.");
×
818

819
  // initialize
820
  feature = {{"type", "Feature"}, {"properties", json::object()}};
376✔
821

822
  // id
823
  const char *featureIdItem =
824
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
47✔
825
  if (featureIdItem == NULL)
47✔
826
    throw std::runtime_error(
827
        "Missing required featureid metadata."); // should have been trapped
×
828
                                                 // earlier
829
  for (i = 0; i < items->numitems; i++) {
215✔
830
    if (strcasecmp(featureIdItem, items->items[i].name) == 0) {
215✔
831
      feature["id"] = shape->values[i];
94✔
832
      break;
47✔
833
    }
834
  }
835

836
  if (i == items->numitems)
47✔
837
    throw std::runtime_error("Feature id not found.");
×
838

839
  // properties - build from items and constants, no group support for now
840

841
  for (int i = 0; i < items->numitems; i++) {
411✔
842
    try {
843
      json item = getFeatureItem(&(items->items[i]), shape->values[i]);
364✔
844
      if (!item.is_null())
364✔
845
        feature["properties"].insert(item.begin(), item.end());
208✔
846
    } catch (const std::runtime_error &) {
×
847
      throw std::runtime_error("Error fetching item.");
×
848
    }
×
849
  }
850

851
  for (int i = 0; i < constants->numconstants; i++) {
47✔
852
    try {
853
      json constant = getFeatureConstant(&(constants->constants[i]));
×
854
      if (!constant.is_null())
×
855
        feature["properties"].insert(constant.begin(), constant.end());
×
856
    } catch (const std::runtime_error &) {
×
857
      throw std::runtime_error("Error fetching constant.");
×
858
    }
×
859
  }
860

861
  // geometry
862
  try {
863
    json geometry =
864
        getFeatureGeometry(shape, geometry_precision, outputCrsAxisInverted);
47✔
865
    if (!geometry.is_null())
47✔
866
      feature["geometry"] = std::move(geometry);
94✔
867
  } catch (const std::runtime_error &) {
×
868
    throw std::runtime_error("Error fetching geometry.");
×
869
  }
×
870

871
  return feature;
47✔
872
}
376✔
873

874
static json getLink(hashTableObj *metadata, const std::string &name) {
17✔
875
  json link;
876

877
  const char *href =
878
      msOWSLookupMetadata(metadata, "A", (name + "_href").c_str());
17✔
879
  if (!href)
17✔
880
    throw std::runtime_error("Missing required link href property.");
×
881

882
  const char *title =
883
      msOWSLookupMetadata(metadata, "A", (name + "_title").c_str());
17✔
884
  const char *type =
885
      msOWSLookupMetadata(metadata, "A", (name + "_type").c_str());
34✔
886

887
  link = {{"href", href},
888
          {"title", title ? title : href},
889
          {"type", type ? type : "text/html"}};
204✔
890

891
  return link;
17✔
892
}
170✔
893

894
static const char *getCollectionDescription(layerObj *layer) {
24✔
895
  const char *description =
896
      msOWSLookupMetadata(&(layer->metadata), "A", "description");
24✔
897
  if (!description)
24✔
898
    description = msOWSLookupMetadata(&(layer->metadata), "OF",
×
899
                                      "abstract"); // fallback on abstract
900
  if (!description)
×
901
    description =
902
        "<!-- Warning: unable to set the collection description. -->"; // finally
903
                                                                       // a
904
                                                                       // warning...
905
  return description;
24✔
906
}
907

908
static const char *getCollectionTitle(layerObj *layer) {
909
  const char *title = msOWSLookupMetadata(&(layer->metadata), "AOF", "title");
21✔
910
  if (!title)
34✔
911
    title = layer->name; // revert to layer name if no title found
×
912
  return title;
913
}
914

915
static int getGeometryPrecision(mapObj *map, layerObj *layer) {
36✔
916
  int geometry_precision = OGCAPI_DEFAULT_GEOMETRY_PRECISION;
917
  if (msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision")) {
36✔
918
    geometry_precision = atoi(
×
919
        msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision"));
920
  } else if (msOWSLookupMetadata(&map->web.metadata, "AF",
36✔
921
                                 "geometry_precision")) {
922
    geometry_precision = atoi(
25✔
923
        msOWSLookupMetadata(&map->web.metadata, "AF", "geometry_precision"));
924
  }
925
  return geometry_precision;
36✔
926
}
927

928
static json getCollection(mapObj *map, layerObj *layer, OGCAPIFormat format,
13✔
929
                          const std::string &api_root) {
930
  json collection; // empty (null)
931
  rectObj bbox;
932

933
  if (!map || !layer)
13✔
934
    return collection;
935

936
  if (!includeLayer(map, layer))
13✔
937
    return collection;
938

939
  // initialize some things
940
  if (msOWSGetLayerExtent(map, layer, "AOF", &bbox) == MS_SUCCESS) {
13✔
941
    if (layer->projection.numargs > 0)
13✔
942
      msOWSProjectToWGS84(&layer->projection, &bbox);
13✔
943
    else if (map->projection.numargs > 0)
×
944
      msOWSProjectToWGS84(&map->projection, &bbox);
×
945
    else
946
      throw std::runtime_error(
947
          "Unable to transform bounding box, no projection defined.");
×
948
  } else {
949
    throw std::runtime_error(
950
        "Unable to get collection bounding box."); // might be too harsh since
×
951
                                                   // extent is optional
952
  }
953

954
  const char *description = getCollectionDescription(layer);
13✔
955
  const char *title = getCollectionTitle(layer);
13✔
956

957
  const char *id = layer->name;
13✔
958
  char *id_encoded = msEncodeUrl(id); // free after use
13✔
959

960
  const int geometry_precision = getGeometryPrecision(map, layer);
13✔
961

962
  const std::string extra_params = getExtraParameterString(map, layer);
13✔
963

964
  // build collection object
965
  collection = {
966
      {"id", id},
967
      {"description", description},
968
      {"title", title},
969
      {"extent",
970
       {{"spatial",
971
         {{"bbox",
972
           {{round_down(bbox.minx, geometry_precision),
13✔
973
             round_down(bbox.miny, geometry_precision),
13✔
974
             round_up(bbox.maxx, geometry_precision),
13✔
975
             round_up(bbox.maxy, geometry_precision)}}},
13✔
976
          {"crs", CRS84_URL}}}}},
977
      {"links",
978
       {
979
           {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
18✔
980
            {"type", OGCAPI_MIMETYPE_JSON},
981
            {"title", "This collection as JSON"},
982
            {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
983
                         "?f=json" + extra_params}},
13✔
984
           {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
21✔
985
            {"type", OGCAPI_MIMETYPE_HTML},
986
            {"title", "This collection as HTML"},
987
            {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
988
                         "?f=html" + extra_params}},
13✔
989
           {{"rel", "items"},
990
            {"type", OGCAPI_MIMETYPE_GEOJSON},
991
            {"title", "Items for this collection as GeoJSON"},
992
            {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
993
                         "/items?f=json" + extra_params}},
13✔
994
           {{"rel", "items"},
995
            {"type", OGCAPI_MIMETYPE_HTML},
996
            {"title", "Items for this collection as HTML"},
997
            {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
998
                         "/items?f=html" + extra_params}},
13✔
999
           {{"rel", "http://www.opengis.net/def/rel/ogc/1.0/queryables"},
1000
            {"type", OGCAPI_MIMETYPE_JSON_SCHEMA},
1001
            {"title", "Queryables for this collection as JSON schema"},
1002
            {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
1003
                         "/queryables?f=json" + extra_params}},
13✔
1004
           {{"rel", "http://www.opengis.net/def/rel/ogc/1.0/queryables"},
1005
            {"type", OGCAPI_MIMETYPE_HTML},
1006
            {"title", "Queryables for this collection as HTML"},
1007
            {"href", api_root + "/collections/" + std::string(id_encoded) +
26✔
1008
                         "/queryables?f=html" + extra_params}},
13✔
1009

1010
       }},
1011
      {"itemType", "feature"}};
1,443✔
1012

1013
  msFree(id_encoded); // done
13✔
1014

1015
  // handle optional configuration (keywords and links)
1016
  const char *value = msOWSLookupMetadata(&(layer->metadata), "A", "keywords");
13✔
1017
  if (!value)
13✔
1018
    value = msOWSLookupMetadata(&(layer->metadata), "OF",
8✔
1019
                                "keywordlist"); // fallback on keywordlist
1020
  if (value) {
8✔
1021
    std::vector<std::string> keywords = msStringSplit(value, ',');
5✔
1022
    for (const std::string &keyword : keywords) {
24✔
1023
      collection["keywords"].push_back(keyword);
57✔
1024
    }
1025
  }
5✔
1026

1027
  value = msOWSLookupMetadata(&(layer->metadata), "A", "links");
13✔
1028
  if (value) {
13✔
1029
    std::vector<std::string> names = msStringSplit(value, ',');
9✔
1030
    for (const std::string &name : names) {
18✔
1031
      try {
1032
        json link = getLink(&(layer->metadata), name);
9✔
1033
        collection["links"].push_back(link);
9✔
1034
      } catch (const std::runtime_error &e) {
×
1035
        throw e;
×
1036
      }
×
1037
    }
1038
  }
9✔
1039

1040
  // Part 2 - CRS support
1041
  // Inspect metadata to set the "crs": [] member and "storageCrs" member
1042

1043
  json jCrsList = getCrsList(map, layer);
13✔
1044
  if (!jCrsList.empty()) {
13✔
1045
    collection["crs"] = std::move(jCrsList);
13✔
1046

1047
    std::string storageCrs = getStorageCrs(layer);
13✔
1048
    if (!storageCrs.empty()) {
13✔
1049
      collection["storageCrs"] = std::move(storageCrs);
26✔
1050
    }
1051
  }
1052

1053
  return collection;
1054
}
1,833✔
1055

1056
/*
1057
** Output stuff...
1058
*/
1059

1060
void msOGCAPIOutputJson(
41✔
1061
    const json &j, const char *mimetype,
1062
    const std::map<std::string, std::vector<std::string>> &extraHeaders) {
1063
  std::string js;
1064

1065
  try {
1066
    js = j.dump();
41✔
1067
  } catch (...) {
1✔
1068
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
1069
                        "Invalid UTF-8 data, check encoding.");
1070
    return;
1071
  }
1✔
1072

1073
  msIO_setHeader("Content-Type", "%s", mimetype);
40✔
1074
  for (const auto &kvp : extraHeaders) {
106✔
1075
    for (const auto &value : kvp.second) {
158✔
1076
      msIO_setHeader(kvp.first.c_str(), "%s", value.c_str());
92✔
1077
    }
1078
  }
1079
  msIO_sendHeaders();
40✔
1080
  msIO_printf("%s\n", js.c_str());
40✔
1081
}
1082

1083
void msOGCAPIOutputTemplate(const char *directory, const char *filename,
12✔
1084
                            const json &j, const char *mimetype) {
1085
  std::string _directory(directory);
12✔
1086
  std::string _filename(filename);
12✔
1087
  Environment env{_directory}; // catch
12✔
1088

1089
  // ERB-style instead of Mustache (we'll see)
1090
  // env.set_expression("<%=", "%>");
1091
  // env.set_statement("<%", "%>");
1092

1093
  // callbacks, need:
1094
  //   - match (regex)
1095
  //   - contains (substring)
1096
  //   - URL encode
1097

1098
  try {
1099
    std::string js = j.dump();
12✔
1100
  } catch (...) {
1✔
1101
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
1102
                        "Invalid UTF-8 data, check encoding.");
1103
    return;
1104
  }
1✔
1105

1106
  try {
1107
    Template t = env.parse_template(_filename); // catch
11✔
1108
    std::string result = env.render(t, j);
11✔
1109

1110
    msIO_setHeader("Content-Type", "%s", mimetype);
11✔
1111
    msIO_sendHeaders();
11✔
1112
    msIO_printf("%s\n", result.c_str());
11✔
1113
  } catch (const inja::RenderError &e) {
11✔
1114
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template rendering error. " +
×
1115
                                                 std::string(e.what()) + " (" +
×
1116
                                                 std::string(filename) + ").");
×
1117
    return;
1118
  } catch (const inja::InjaError &e) {
×
1119
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1120
                        "InjaError error. " + std::string(e.what()) + " (" +
×
1121
                            std::string(filename) + ")." + " (" +
×
1122
                            std::string(directory) + ").");
×
1123
    return;
1124
  } catch (...) {
×
1125
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1126
                        "General template handling error.");
1127
    return;
1128
  }
×
1129
}
12✔
1130

1131
/*
1132
** Generic response output.
1133
*/
1134
static void outputResponse(
41✔
1135
    const mapObj *map, const cgiRequestObj *request, OGCAPIFormat format,
1136
    const char *filename, const json &response,
1137
    const std::map<std::string, std::vector<std::string>> &extraHeaders =
1138
        std::map<std::string, std::vector<std::string>>()) {
1139
  std::string path;
1140
  char fullpath[MS_MAXPATHLEN];
1141

1142
  if (format == OGCAPIFormat::JSON) {
1143
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_JSON, extraHeaders);
10✔
1144
  } else if (format == OGCAPIFormat::GeoJSON) {
1145
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_GEOJSON, extraHeaders);
19✔
1146
  } else if (format == OGCAPIFormat::OpenAPI_V3) {
1147
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_OPENAPI_V3, extraHeaders);
1✔
1148
  } else if (format == OGCAPIFormat::JSONSchema) {
1149
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_JSON_SCHEMA, extraHeaders);
1✔
1150
  } else if (format == OGCAPIFormat::HTML) {
1151
    path = msOGCAPIGetTemplateDirectory(map, "html_template_directory",
20✔
1152
                                        "OGCAPI_HTML_TEMPLATE_DIRECTORY");
10✔
1153
    if (path.empty()) {
10✔
1154
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template directory not set.");
×
1155
      return; // bail
×
1156
    }
1157
    msBuildPath(fullpath, map->mappath, path.c_str());
10✔
1158

1159
    json j;
1160

1161
    j["response"] = response; // nest the response so we could write the whole
10✔
1162
                              // object in the template
1163

1164
    // extend the JSON with a few things that we need for templating
1165
    const std::string extra_params = getExtraParameterString(map, nullptr);
10✔
1166

1167
    j["template"] = {{"path", json::array()},
20✔
1168
                     {"params", json::object()},
10✔
1169
                     {"api_root", msOGCAPIGetApiRootUrl(map, request)},
10✔
1170
                     {"extra_params", extra_params},
1171
                     {"title", getTitle(map)},
10✔
1172
                     {"tags", json::object()}};
200✔
1173

1174
    // api path
1175
    for (int i = 0; i < request->api_path_length; i++)
52✔
1176
      j["template"]["path"].push_back(request->api_path[i]);
126✔
1177

1178
    // parameters (optional)
1179
    for (int i = 0; i < request->NumParams; i++) {
27✔
1180
      if (request->ParamValues[i] &&
17✔
1181
          strlen(request->ParamValues[i]) > 0) { // skip empty params
17✔
1182
        j["template"]["params"].update(
90✔
1183
            {{request->ParamNames[i], request->ParamValues[i]}});
30✔
1184
      }
1185
    }
1186

1187
    // add custom tags (optional)
1188
    const char *tags =
1189
        msOWSLookupMetadata(&(map->web.metadata), "A", "html_tags");
10✔
1190
    if (tags) {
10✔
1191
      std::vector<std::string> names = msStringSplit(tags, ',');
3✔
1192
      for (std::string name : names) {
9✔
1193
        const char *value = msOWSLookupMetadata(&(map->web.metadata), "A",
6✔
1194
                                                ("tag_" + name).c_str());
12✔
1195
        if (value) {
6✔
1196
          j["template"]["tags"].update({{name, value}}); // add object
36✔
1197
        }
1198
      }
1199
    }
3✔
1200

1201
    msOGCAPIOutputTemplate(fullpath, filename, j, OGCAPI_MIMETYPE_HTML);
10✔
1202
  } else {
1203
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
1204
  }
1205
}
329✔
1206

1207
/*
1208
** Process stuff...
1209
*/
1210
static int processLandingRequest(mapObj *map, cgiRequestObj *request,
6✔
1211
                                 OGCAPIFormat format) {
1212
  json response;
1213

1214
  auto allowedParameters = getExtraParameters(map, nullptr);
6✔
1215
  allowedParameters.insert("f");
6✔
1216
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
6✔
1217
    return MS_SUCCESS;
1218
  }
1219

1220
  // define ambiguous elements
1221
  const char *description =
1222
      msOWSLookupMetadata(&(map->web.metadata), "A", "description");
5✔
1223
  if (!description)
5✔
1224
    description =
1225
        msOWSLookupMetadata(&(map->web.metadata), "OF",
2✔
1226
                            "abstract"); // fallback on abstract if necessary
1227

1228
  const std::string extra_params = getExtraParameterString(map, nullptr);
5✔
1229

1230
  // define api root url
1231
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
5✔
1232

1233
  // build response object
1234
  //   - consider conditionally excluding links for HTML format
1235
  response = {
1236
      {"title", getTitle(map)},
5✔
1237
      {"description", description ? description : ""},
7✔
1238
      {"links",
1239
       {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
6✔
1240
         {"type", OGCAPI_MIMETYPE_JSON},
1241
         {"title", "This document as JSON"},
1242
         {"href", api_root + "?f=json" + extra_params}},
5✔
1243
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
9✔
1244
         {"type", OGCAPI_MIMETYPE_HTML},
1245
         {"title", "This document as HTML"},
1246
         {"href", api_root + "?f=html" + extra_params}},
5✔
1247
        {{"rel", "conformance"},
1248
         {"type", OGCAPI_MIMETYPE_JSON},
1249
         {"title",
1250
          "OCG API conformance classes implemented by this server (JSON)"},
1251
         {"href", api_root + "/conformance?f=json" + extra_params}},
5✔
1252
        {{"rel", "conformance"},
1253
         {"type", OGCAPI_MIMETYPE_HTML},
1254
         {"title", "OCG API conformance classes implemented by this server"},
1255
         {"href", api_root + "/conformance?f=html" + extra_params}},
5✔
1256
        {{"rel", "data"},
1257
         {"type", OGCAPI_MIMETYPE_JSON},
1258
         {"title", "Information about feature collections available from this "
1259
                   "server (JSON)"},
1260
         {"href", api_root + "/collections?f=json" + extra_params}},
5✔
1261
        {{"rel", "data"},
1262
         {"type", OGCAPI_MIMETYPE_HTML},
1263
         {"title",
1264
          "Information about feature collections available from this server"},
1265
         {"href", api_root + "/collections?f=html" + extra_params}},
5✔
1266
        {{"rel", "service-desc"},
1267
         {"type", OGCAPI_MIMETYPE_OPENAPI_V3},
1268
         {"title", "OpenAPI document"},
1269
         {"href", api_root + "/api?f=json" + extra_params}},
5✔
1270
        {{"rel", "service-doc"},
1271
         {"type", OGCAPI_MIMETYPE_HTML},
1272
         {"title", "API documentation"},
1273
         {"href", api_root + "/api?f=html" + extra_params}}}}};
575✔
1274

1275
  // handle custom links (optional)
1276
  const char *links = msOWSLookupMetadata(&(map->web.metadata), "A", "links");
5✔
1277
  if (links) {
5✔
1278
    std::vector<std::string> names = msStringSplit(links, ',');
5✔
1279
    for (std::string name : names) {
13✔
1280
      try {
1281
        json link = getLink(&(map->web.metadata), name);
8✔
1282
        response["links"].push_back(link);
8✔
1283
      } catch (const std::runtime_error &e) {
×
1284
        msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, std::string(e.what()));
×
1285
        return MS_SUCCESS;
1286
      }
×
1287
    }
1288
  }
5✔
1289

1290
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_LANDING, response);
5✔
1291
  return MS_SUCCESS;
5✔
1292
}
765✔
1293

1294
static int processConformanceRequest(mapObj *map, cgiRequestObj *request,
2✔
1295
                                     OGCAPIFormat format) {
1296
  json response;
1297

1298
  auto allowedParameters = getExtraParameters(map, nullptr);
2✔
1299
  allowedParameters.insert("f");
2✔
1300
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
2✔
1301
    return MS_SUCCESS;
1302
  }
1303

1304
  // build response object
1305
  response = {
1306
      {"conformsTo",
1307
       {
1308
           "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core",
1309
           "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections",
1310
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
1311
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30",
1312
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html",
1313
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
1314
           "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs",
1315
           "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables",
1316
           "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/"
1317
           "queryables-query-parameters",
1318
           "http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/queryables",
1319
       }}};
14✔
1320

1321
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_CONFORMANCE,
1✔
1322
                 response);
1323
  return MS_SUCCESS;
1✔
1324
}
14✔
1325

1326
static int findLayerIndex(const mapObj *map, const char *collectionId) {
34✔
1327
  for (int i = 0; i < map->numlayers; i++) {
42✔
1328
    if (strcmp(map->layers[i]->name, collectionId) == 0) {
42✔
1329
      return i;
34✔
1330
    }
1331
  }
1332
  return -1;
1333
}
1334

1335
static int processCollectionItemsRequest(mapObj *map, cgiRequestObj *request,
31✔
1336
                                         const char *collectionId,
1337
                                         const char *featureId,
1338
                                         OGCAPIFormat format) {
1339
  json response;
1340

1341
  // find the right layer
1342
  const int iLayer = findLayerIndex(map, collectionId);
31✔
1343

1344
  if (iLayer < 0) { // invalid collectionId
31✔
1345
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1346
    return MS_SUCCESS;
×
1347
  }
1348

1349
  layerObj *layer = map->layers[iLayer];
31✔
1350
  layer->status = MS_ON; // force on (do we need to save and reset?)
31✔
1351

1352
  if (!includeLayer(map, layer)) {
31✔
1353
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1354
    return MS_SUCCESS;
×
1355
  }
1356

1357
  //
1358
  // handle parameters specific to this endpoint
1359
  //
1360
  int limit = -1;
31✔
1361
  if (getLimit(map, request, layer, &limit) != MS_SUCCESS) {
31✔
1362
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for limit.");
×
1363
    return MS_SUCCESS;
×
1364
  }
1365

1366
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
31✔
1367
  const char *crs = getRequestParameter(request, "crs");
31✔
1368

1369
  std::string outputCrs = "EPSG:4326";
31✔
1370
  bool outputCrsAxisInverted =
1371
      false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
1372
  std::map<std::string, std::vector<std::string>> extraHeaders;
1373
  if (crs) {
31✔
1374
    bool isExpectedCrs = false;
1375
    for (const auto &crsItem : getCrsList(map, layer)) {
26✔
1376
      if (crs == crsItem.get<std::string>()) {
22✔
1377
        isExpectedCrs = true;
1378
        break;
1379
      }
1380
    }
1381
    if (!isExpectedCrs) {
4✔
1382
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for crs.");
2✔
1383
      return MS_SUCCESS;
2✔
1384
    }
1385
    extraHeaders["Content-Crs"].push_back('<' + std::string(crs) + '>');
6✔
1386
    if (std::string(crs) != CRS84_URL) {
4✔
1387
      if (std::string(crs).find(EPSG_PREFIX_URL) == 0) {
4✔
1388
        const char *code = crs + strlen(EPSG_PREFIX_URL);
2✔
1389
        outputCrs = std::string("EPSG:") + code;
6✔
1390
        outputCrsAxisInverted = msIsAxisInverted(atoi(code));
2✔
1391
      }
1392
    }
1393
  } else {
1394
    extraHeaders["Content-Crs"].push_back('<' + std::string(CRS84_URL) + '>');
81✔
1395
  }
1396

1397
  auto allowedParameters = getExtraParameters(map, layer);
29✔
1398
  allowedParameters.insert("f");
29✔
1399
  allowedParameters.insert("bbox");
29✔
1400
  allowedParameters.insert("bbox-crs");
29✔
1401
  allowedParameters.insert("datetime");
29✔
1402
  allowedParameters.insert("limit");
29✔
1403
  allowedParameters.insert("offset");
29✔
1404
  allowedParameters.insert("crs");
29✔
1405

1406
  bool error = false;
1407
  const std::vector<std::string> queryableItems =
1408
      msOOGCAPIGetLayerQueryables(layer, allowedParameters, error);
29✔
1409
  if (error) {
29✔
1410
    return MS_SUCCESS;
1411
  }
1412

1413
  for (const auto &item : queryableItems)
83✔
1414
    allowedParameters.insert(item);
1415

1416
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
29✔
1417
    return MS_SUCCESS;
1418
  }
1419

1420
  std::string filter;
1421
  std::string query_kvp;
1422
  if (!queryableItems.empty()) {
26✔
1423
    for (int i = 0; i < request->NumParams; i++) {
58✔
1424
      if (std::find(queryableItems.begin(), queryableItems.end(),
42✔
1425
                    request->ParamNames[i]) != queryableItems.end()) {
42✔
1426

1427
        // Find actual item name from alias
1428
        const char *pszItem = nullptr;
1429
        for (int j = 0; j < layer->numitems; ++j) {
24✔
1430
          if (strcmp(request->ParamNames[i],
24✔
1431
                     getItemAliasOrName(layer, layer->items[j])) == 0) {
24✔
1432
            pszItem = layer->items[j];
4✔
1433
            break;
4✔
1434
          }
1435
        }
1436
        assert(pszItem);
1437

1438
        const std::string expr = FLTGetBinaryComparisonCommonExpression(
1439
            layer, pszItem, false, "=", request->ParamValues[i]);
4✔
1440
        if (!filter.empty())
4✔
1441
          filter += " AND ";
1442
        filter += expr;
1443

1444
        query_kvp += '&';
1445
        char *encoded = msEncodeUrl(request->ParamNames[i]);
4✔
1446
        query_kvp += encoded;
1447
        msFree(encoded);
4✔
1448
        query_kvp += '=';
1449
        encoded = msEncodeUrl(request->ParamValues[i]);
4✔
1450
        query_kvp += encoded;
1451
        msFree(encoded);
4✔
1452
      }
1453
    }
1454
  }
1455

1456
  struct ReprojectionObjects {
1457
    reprojectionObj *reprojector = NULL;
1458
    projectionObj proj;
1459

1460
    ReprojectionObjects() { msInitProjection(&proj); }
26✔
1461

1462
    ~ReprojectionObjects() {
1463
      msProjectDestroyReprojector(reprojector);
26✔
1464
      msFreeProjection(&proj);
26✔
1465
    }
26✔
1466

1467
    int executeQuery(mapObj *map) {
41✔
1468
      projectionObj backupMapProjection = map->projection;
41✔
1469
      map->projection = proj;
41✔
1470
      int ret = msExecuteQuery(map);
41✔
1471
      map->projection = backupMapProjection;
41✔
1472
      return ret;
41✔
1473
    }
1474
  };
1475
  ReprojectionObjects reprObjs;
1476

1477
  msProjectionInheritContextFrom(&reprObjs.proj, &(map->projection));
26✔
1478
  if (msLoadProjectionString(&reprObjs.proj, outputCrs.c_str()) != 0) {
26✔
1479
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot instantiate output CRS.");
×
1480
    return MS_SUCCESS;
×
1481
  }
1482

1483
  if (layer->projection.numargs > 0) {
26✔
1484
    if (msProjectionsDiffer(&(layer->projection), &reprObjs.proj)) {
26✔
1485
      reprObjs.reprojector =
19✔
1486
          msProjectCreateReprojector(&(layer->projection), &reprObjs.proj);
19✔
1487
      if (reprObjs.reprojector == NULL) {
19✔
1488
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1489
                            "Error creating re-projector.");
1490
        return MS_SUCCESS;
×
1491
      }
1492
    }
1493
  } else if (map->projection.numargs > 0) {
×
1494
    if (msProjectionsDiffer(&(map->projection), &reprObjs.proj)) {
×
1495
      reprObjs.reprojector =
×
1496
          msProjectCreateReprojector(&(map->projection), &reprObjs.proj);
×
1497
      if (reprObjs.reprojector == NULL) {
×
1498
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1499
                            "Error creating re-projector.");
1500
        return MS_SUCCESS;
×
1501
      }
1502
    }
1503
  } else {
1504
    msOGCAPIOutputError(
×
1505
        OGCAPI_CONFIG_ERROR,
1506
        "Unable to transform geometries, no projection defined.");
1507
    return MS_SUCCESS;
×
1508
  }
1509

1510
  if (map->projection.numargs > 0) {
26✔
1511
    msProjectRect(&(map->projection), &reprObjs.proj, &map->extent);
26✔
1512
  }
1513

1514
  rectObj bbox;
1515
  if (!getBbox(map, layer, request, &bbox, &reprObjs.proj)) {
26✔
1516
    return MS_SUCCESS;
1517
  }
1518

1519
  int offset = 0;
24✔
1520
  int numberMatched = 0;
1521
  if (featureId) {
24✔
1522
    const char *featureIdItem =
1523
        msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
3✔
1524
    if (featureIdItem == NULL) {
3✔
1525
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1526
                          "Missing required featureid metadata.");
1527
      return MS_SUCCESS;
×
1528
    }
1529

1530
    // TODO: does featureIdItem exist in the data?
1531

1532
    // optional validation
1533
    const char *featureIdValidation =
1534
        msLookupHashTable(&(layer->validation), featureIdItem);
3✔
1535
    if (featureIdValidation &&
6✔
1536
        msValidateParameter(featureId, featureIdValidation, NULL, NULL, NULL) !=
3✔
1537
            MS_SUCCESS) {
1538
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid feature id.");
1✔
1539
      return MS_SUCCESS;
1✔
1540
    }
1541

1542
    map->query.type = MS_QUERY_BY_FILTER;
2✔
1543
    map->query.mode = MS_QUERY_SINGLE;
2✔
1544
    map->query.layer = iLayer;
2✔
1545
    map->query.rect = bbox;
2✔
1546
    map->query.filteritem = strdup(featureIdItem);
2✔
1547

1548
    msInitExpression(&map->query.filter);
2✔
1549
    map->query.filter.type = MS_STRING;
2✔
1550
    map->query.filter.string = strdup(featureId);
2✔
1551

1552
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
2✔
1553
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1554
                          "Collection items id query failed.");
1555
      return MS_SUCCESS;
×
1556
    }
1557

1558
    if (!layer->resultcache || layer->resultcache->numresults != 1) {
2✔
1559
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1560
                          "Collection items id query failed.");
1561
      return MS_SUCCESS;
×
1562
    }
1563
  } else { // bbox query
1564
    map->query.type = MS_QUERY_BY_RECT;
21✔
1565
    map->query.mode = MS_QUERY_MULTIPLE;
21✔
1566
    map->query.layer = iLayer;
21✔
1567
    map->query.rect = bbox;
21✔
1568
    map->query.only_cache_result_count = MS_TRUE;
21✔
1569

1570
    if (!filter.empty()) {
21✔
1571
      map->query.type = MS_QUERY_BY_FILTER;
4✔
1572
      msInitExpression(&map->query.filter);
4✔
1573
      map->query.filter.string = msStrdup(filter.c_str());
4✔
1574
      map->query.filter.type = MS_EXPRESSION;
4✔
1575
    }
1576

1577
    // get number matched
1578
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
21✔
1579
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1580
                          "Collection items query failed.");
1581
      return MS_SUCCESS;
×
1582
    }
1583

1584
    if (!layer->resultcache) {
21✔
1585
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1586
                          "Collection items query failed.");
1587
      return MS_SUCCESS;
×
1588
    }
1589

1590
    numberMatched = layer->resultcache->numresults;
21✔
1591

1592
    if (numberMatched > 0) {
21✔
1593
      map->query.only_cache_result_count = MS_FALSE;
18✔
1594
      map->query.maxfeatures = limit;
18✔
1595

1596
      const char *offsetStr = getRequestParameter(request, "offset");
18✔
1597
      if (offsetStr) {
18✔
1598
        if (msStringToInt(offsetStr, &offset, 10) != MS_SUCCESS) {
1✔
1599
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for offset.");
×
1600
          return MS_SUCCESS;
×
1601
        }
1602

1603
        if (offset < 0 || offset >= numberMatched) {
1✔
1604
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Offset out of range.");
×
1605
          return MS_SUCCESS;
×
1606
        }
1607

1608
        // msExecuteQuery() use a 1-based offset convention, whereas the API
1609
        // uses a 0-based offset convention.
1610
        map->query.startindex = 1 + offset;
1✔
1611
        layer->startindex = 1 + offset;
1✔
1612
      }
1613

1614
      if (reprObjs.executeQuery(map) != MS_SUCCESS || !layer->resultcache) {
18✔
1615
        msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1616
                            "Collection items query failed.");
1617
        return MS_SUCCESS;
×
1618
      }
1619
    }
1620
  }
1621

1622
  const std::string extra_params = getExtraParameterString(map, layer);
23✔
1623

1624
  // build response object
1625
  if (!featureId) {
23✔
1626
    const char *id = layer->name;
21✔
1627
    char *id_encoded = msEncodeUrl(id); // free after use
21✔
1628

1629
    std::string extra_kvp = "&limit=" + std::to_string(limit);
21✔
1630
    extra_kvp += "&offset=" + std::to_string(offset);
42✔
1631

1632
    std::string other_extra_kvp;
1633
    if (crs)
21✔
1634
      other_extra_kvp += "&crs=" + std::string(crs);
4✔
1635
    const char *bbox = getRequestParameter(request, "bbox");
21✔
1636
    if (bbox)
21✔
1637
      other_extra_kvp += "&bbox=" + std::string(bbox);
10✔
1638
    const char *bboxCrs = getRequestParameter(request, "bbox-crs");
21✔
1639
    if (bboxCrs)
21✔
1640
      other_extra_kvp += "&bbox-crs=" + std::string(bboxCrs);
4✔
1641

1642
    other_extra_kvp += query_kvp;
1643

1644
    response = {{"type", "FeatureCollection"},
1645
                {"numberMatched", numberMatched},
1646
                {"numberReturned", layer->resultcache->numresults},
21✔
1647
                {"features", json::array()},
21✔
1648
                {"links",
1649
                 {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
25✔
1650
                   {"type", OGCAPI_MIMETYPE_GEOJSON},
1651
                   {"title", "Items for this collection as GeoJSON"},
1652
                   {"href", api_root + "/collections/" +
42✔
1653
                                std::string(id_encoded) + "/items?f=json" +
21✔
1654
                                extra_kvp + other_extra_kvp + extra_params}},
21✔
1655
                  {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
38✔
1656
                   {"type", OGCAPI_MIMETYPE_HTML},
1657
                   {"title", "Items for this collection as HTML"},
1658
                   {"href", api_root + "/collections/" +
42✔
1659
                                std::string(id_encoded) + "/items?f=html" +
21✔
1660
                                extra_kvp + other_extra_kvp + extra_params}}}}};
882✔
1661

1662
    if (offset + layer->resultcache->numresults < numberMatched) {
21✔
1663
      response["links"].push_back(
140✔
1664
          {{"rel", "next"},
1665
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
11✔
1666
                                                 : OGCAPI_MIMETYPE_HTML},
1667
           {"title", "next page"},
1668
           {"href",
1669
            api_root + "/collections/" + std::string(id_encoded) +
20✔
1670
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
20✔
1671
                "&limit=" + std::to_string(limit) +
30✔
1672
                "&offset=" + std::to_string(offset + limit) + other_extra_kvp +
20✔
1673
                extra_params}});
1674
    }
1675

1676
    if (offset > 0) {
21✔
1677
      response["links"].push_back(
14✔
1678
          {{"rel", "prev"},
1679
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
1✔
1680
                                                 : OGCAPI_MIMETYPE_HTML},
1681
           {"title", "previous page"},
1682
           {"href",
1683
            api_root + "/collections/" + std::string(id_encoded) +
2✔
1684
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
2✔
1685
                "&limit=" + std::to_string(limit) +
3✔
1686
                "&offset=" + std::to_string(MS_MAX(0, (offset - limit))) +
2✔
1687
                other_extra_kvp + extra_params}});
1✔
1688
    }
1689

1690
    extraHeaders["OGC-NumberReturned"].push_back(
21✔
1691
        std::to_string(layer->resultcache->numresults));
42✔
1692
    extraHeaders["OGC-NumberMatched"].push_back(std::to_string(numberMatched));
42✔
1693
    std::vector<std::string> linksHeaders;
1694
    for (auto &link : response["links"]) {
95✔
1695
      linksHeaders.push_back("<" + link["href"].get<std::string>() +
106✔
1696
                             ">; rel=\"" + link["rel"].get<std::string>() +
159✔
1697
                             "\"; title=\"" + link["title"].get<std::string>() +
159✔
1698
                             "\"; type=\"" + link["type"].get<std::string>() +
159✔
1699
                             "\"");
1700
    }
1701
    extraHeaders["Link"] = std::move(linksHeaders);
21✔
1702

1703
    msFree(id_encoded); // done
21✔
1704
  }
21✔
1705

1706
  // features (items)
1707
  {
1708
    shapeObj shape;
23✔
1709
    msInitShape(&shape);
23✔
1710

1711
    // we piggyback on GML configuration
1712
    gmlItemListObj *items = msGMLGetItems(layer, "AG");
23✔
1713
    gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
23✔
1714

1715
    if (!items || !constants) {
23✔
1716
      msGMLFreeItems(items);
×
1717
      msGMLFreeConstants(constants);
×
1718
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1719
                          "Error fetching layer attribute metadata.");
1720
      return MS_SUCCESS;
×
1721
    }
1722

1723
    const int geometry_precision = getGeometryPrecision(map, layer);
23✔
1724

1725
    for (int i = 0; i < layer->resultcache->numresults; i++) {
70✔
1726
      int status =
1727
          msLayerGetShape(layer, &shape, &(layer->resultcache->results[i]));
47✔
1728
      if (status != MS_SUCCESS) {
47✔
1729
        msGMLFreeItems(items);
×
1730
        msGMLFreeConstants(constants);
×
1731
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Error fetching feature.");
×
1732
        return MS_SUCCESS;
×
1733
      }
1734

1735
      if (reprObjs.reprojector) {
47✔
1736
        status = msProjectShapeEx(reprObjs.reprojector, &shape);
40✔
1737
        if (status != MS_SUCCESS) {
40✔
1738
          msGMLFreeItems(items);
×
1739
          msGMLFreeConstants(constants);
×
1740
          msFreeShape(&shape);
×
1741
          msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1742
                              "Error reprojecting feature.");
1743
          return MS_SUCCESS;
×
1744
        }
1745
      }
1746

1747
      try {
1748
        json feature = getFeature(layer, &shape, items, constants,
1749
                                  geometry_precision, outputCrsAxisInverted);
47✔
1750
        if (featureId) {
47✔
1751
          response = std::move(feature);
2✔
1752
        } else {
1753
          response["features"].emplace_back(std::move(feature));
45✔
1754
        }
1755
      } catch (const std::runtime_error &e) {
×
1756
        msGMLFreeItems(items);
×
1757
        msGMLFreeConstants(constants);
×
1758
        msFreeShape(&shape);
×
1759
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1760
                            "Error getting feature. " + std::string(e.what()));
×
1761
        return MS_SUCCESS;
1762
      }
×
1763

1764
      msFreeShape(&shape); // next
47✔
1765
    }
1766

1767
    msGMLFreeItems(items); // clean up
23✔
1768
    msGMLFreeConstants(constants);
23✔
1769
  }
23✔
1770

1771
  // extend the response a bit for templating (HERE)
1772
  if (format == OGCAPIFormat::HTML) {
23✔
1773
    const char *title = getCollectionTitle(layer);
1774
    const char *id = layer->name;
4✔
1775
    response["collection"] = {{"id", id}, {"title", title ? title : ""}};
36✔
1776
  }
1777

1778
  if (featureId) {
23✔
1779
    const char *id = layer->name;
2✔
1780
    char *id_encoded = msEncodeUrl(id); // free after use
2✔
1781

1782
    response["links"] = {
2✔
1783
        {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
2✔
1784
         {"type", OGCAPI_MIMETYPE_GEOJSON},
1785
         {"title", "This document as GeoJSON"},
1786
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1787
                      "/items/" + featureId + "?f=json" + extra_params}},
2✔
1788
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
4✔
1789
         {"type", OGCAPI_MIMETYPE_HTML},
1790
         {"title", "This document as HTML"},
1791
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1792
                      "/items/" + featureId + "?f=html" + extra_params}},
2✔
1793
        {{"rel", "collection"},
1794
         {"type", OGCAPI_MIMETYPE_JSON},
1795
         {"title", "This collection as JSON"},
1796
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1797
                      "?f=json" + extra_params}},
2✔
1798
        {{"rel", "collection"},
1799
         {"type", OGCAPI_MIMETYPE_HTML},
1800
         {"title", "This collection as HTML"},
1801
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1802
                      "?f=html" + extra_params}}};
106✔
1803

1804
    msFree(id_encoded);
2✔
1805

1806
    outputResponse(
2✔
1807
        map, request,
1808
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1809
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM, response, extraHeaders);
1810
  } else {
1811
    outputResponse(
38✔
1812
        map, request,
1813
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1814
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS, response, extraHeaders);
1815
  }
1816
  return MS_SUCCESS;
1817
}
1,441✔
1818

1819
std::pair<const char *, const char *>
1820
getQueryableTypeAndFormat(const layerObj *layer, const std::string &item) {
9✔
1821
  const char *format = nullptr;
1822
  const char *type = "string";
1823
  const char *pszType =
1824
      msOWSLookupMetadata(&(layer->metadata), "OFG", (item + "_type").c_str());
9✔
1825
  if (pszType != NULL && (strcasecmp(pszType, "Character") == 0))
9✔
1826
    type = "string";
1827
  else if (pszType != NULL && (strcasecmp(pszType, "Date") == 0)) {
6✔
1828
    type = "string";
1829
    format = "date";
1830
  } else if (pszType != NULL && (strcasecmp(pszType, "Time") == 0)) {
6✔
1831
    type = "string";
1832
    format = "time";
1833
  } else if (pszType != NULL && (strcasecmp(pszType, "DateTime") == 0)) {
6✔
1834
    type = "string";
1835
    format = "date-time";
1836
  } else if (pszType != NULL && (strcasecmp(pszType, "Integer") == 0 ||
3✔
NEW
1837
                                 strcasecmp(pszType, "Long") == 0))
×
1838
    type = "integer";
NEW
1839
  else if (pszType != NULL && (strcasecmp(pszType, "Real") == 0))
×
1840
    type = "numeric";
NEW
1841
  else if (pszType != NULL && (strcasecmp(pszType, "Boolean") == 0))
×
1842
    type = "boolean";
1843

1844
  return {type, format};
9✔
1845
}
1846

1847
static int processCollectionQueryablesRequest(mapObj *map,
3✔
1848
                                              const cgiRequestObj *request,
1849
                                              const char *collectionId,
1850
                                              OGCAPIFormat format) {
1851

1852
  // find the right layer
1853
  const int iLayer = findLayerIndex(map, collectionId);
3✔
1854

1855
  if (iLayer < 0) { // invalid collectionId
3✔
NEW
1856
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
NEW
1857
    return MS_SUCCESS;
×
1858
  }
1859

1860
  layerObj *layer = map->layers[iLayer];
3✔
1861
  if (!includeLayer(map, layer)) {
3✔
NEW
1862
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
NEW
1863
    return MS_SUCCESS;
×
1864
  }
1865

1866
  auto allowedParameters = getExtraParameters(map, layer);
3✔
1867
  allowedParameters.insert("f");
3✔
1868

1869
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
3✔
1870
    return MS_SUCCESS;
1871
  }
1872

1873
  bool error = false;
1874
  const std::vector<std::string> queryableItems =
1875
      msOOGCAPIGetLayerQueryables(layer, allowedParameters, error);
2✔
1876
  if (error) {
2✔
1877
    return MS_SUCCESS;
1878
  }
1879

1880
  std::unique_ptr<char, decltype(&msFree)> id_encoded(msEncodeUrl(collectionId),
1881
                                                      msFree);
2✔
1882

1883
  json response = {
1884
      {"$schema", "https://json-schema.org/draft/2020-12/schema"},
1885
      {"$id", msOGCAPIGetApiRootUrl(map, request) + "/collections/" +
6✔
1886
                  std::string(id_encoded.get()) + "/queryables"},
2✔
1887
      {"type", "object"},
1888
      {"title", getCollectionTitle(layer)},
2✔
1889
      {"description", getCollectionDescription(layer)},
2✔
1890
      {"properties",
1891
       {
1892
#ifdef to_enable_once_we_support_geometry_requests
1893
           {"shape",
1894
            {
1895
                {"title", "Geometry"},
1896
                {"description", "The geometry of the collection."},
1897
                {"x-ogc-role", "primary-geometry"},
1898
                {"format", "geometry-any"},
1899
            }},
1900
#endif
1901
       }},
1902
      {"additionalProperties", false},
1903
  };
44✔
1904

1905
  for (const std::string &item : queryableItems) {
8✔
1906
    json j;
1907
    const auto [type, format] = getQueryableTypeAndFormat(layer, item);
6✔
1908
    j["description"] = "Queryable item '" + item + "'";
18✔
1909
    j["type"] = type;
6✔
1910
    if (format)
6✔
1911
      j["format"] = format;
4✔
1912
    response["properties"][item] = j;
12✔
1913
  }
1914

1915
  std::map<std::string, std::vector<std::string>> extraHeaders;
1916
  outputResponse(
3✔
1917
      map, request,
1918
      format == OGCAPIFormat::JSON ? OGCAPIFormat::JSONSchema : format,
1919
      OGCAPI_TEMPLATE_HTML_COLLECTION_QUERYABLES, response, extraHeaders);
1920

1921
  return MS_SUCCESS;
1922
}
48✔
1923

1924
static int processCollectionRequest(mapObj *map, cgiRequestObj *request,
7✔
1925
                                    const char *collectionId,
1926
                                    OGCAPIFormat format) {
1927
  json response;
1928
  int l;
1929

1930
  for (l = 0; l < map->numlayers; l++) {
9✔
1931
    if (strcmp(map->layers[l]->name, collectionId) == 0)
9✔
1932
      break; // match
1933
  }
1934

1935
  if (l == map->numlayers) { // invalid collectionId
7✔
1936
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1937
    return MS_SUCCESS;
×
1938
  }
1939

1940
  layerObj *layer = map->layers[l];
7✔
1941
  auto allowedParameters = getExtraParameters(map, layer);
7✔
1942
  allowedParameters.insert("f");
7✔
1943
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
7✔
1944
    return MS_SUCCESS;
1945
  }
1946

1947
  try {
1948
    response =
1949
        getCollection(map, layer, format, msOGCAPIGetApiRootUrl(map, request));
12✔
1950
    if (response.is_null()) { // same as not found
6✔
1951
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1952
      return MS_SUCCESS;
×
1953
    }
1954
  } catch (const std::runtime_error &e) {
×
1955
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1956
                        "Error getting collection. " + std::string(e.what()));
×
1957
    return MS_SUCCESS;
1958
  }
×
1959

1960
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTION,
6✔
1961
                 response);
1962
  return MS_SUCCESS;
6✔
1963
}
1964

1965
static int processCollectionsRequest(mapObj *map, cgiRequestObj *request,
4✔
1966
                                     OGCAPIFormat format) {
1967
  json response;
1968
  int i;
1969

1970
  auto allowedParameters = getExtraParameters(map, nullptr);
4✔
1971
  allowedParameters.insert("f");
4✔
1972
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
4✔
1973
    return MS_SUCCESS;
1974
  }
1975

1976
  // define api root url
1977
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
3✔
1978
  const std::string extra_params = getExtraParameterString(map, nullptr);
3✔
1979

1980
  // build response object
1981
  response = {{"links",
1982
               {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
4✔
1983
                 {"type", OGCAPI_MIMETYPE_JSON},
1984
                 {"title", "This document as JSON"},
1985
                 {"href", api_root + "/collections?f=json" + extra_params}},
3✔
1986
                {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
5✔
1987
                 {"type", OGCAPI_MIMETYPE_HTML},
1988
                 {"title", "This document as HTML"},
1989
                 {"href", api_root + "/collections?f=html" + extra_params}}}},
3✔
1990
              {"collections", json::array()}};
102✔
1991

1992
  for (i = 0; i < map->numlayers; i++) {
10✔
1993
    try {
1994
      json collection = getCollection(map, map->layers[i], format, api_root);
7✔
1995
      if (!collection.is_null())
7✔
1996
        response["collections"].push_back(collection);
7✔
1997
    } catch (const std::runtime_error &e) {
×
1998
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1999
                          "Error getting collection." + std::string(e.what()));
×
2000
      return MS_SUCCESS;
2001
    }
×
2002
  }
2003

2004
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTIONS,
3✔
2005
                 response);
2006
  return MS_SUCCESS;
3✔
2007
}
129✔
2008

2009
static int processApiRequest(mapObj *map, cgiRequestObj *request,
2✔
2010
                             OGCAPIFormat format) {
2011
  // Strongly inspired from
2012
  // https://github.com/geopython/pygeoapi/blob/master/pygeoapi/openapi.py
2013

2014
  auto allowedParameters = getExtraParameters(map, nullptr);
2✔
2015
  allowedParameters.insert("f");
2✔
2016
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
2✔
2017
    return MS_SUCCESS;
2018
  }
2019

2020
  json response;
2021

2022
  response = {
2023
      {"openapi", "3.0.2"},
2024
      {"tags", json::array()},
1✔
2025
  };
7✔
2026

2027
  response["info"] = {
1✔
2028
      {"title", getTitle(map)},
1✔
2029
      {"version", getWebMetadata(map, "A", "version", "1.0.0")},
1✔
2030
  };
7✔
2031

2032
  for (const char *item : {"description", "termsOfService"}) {
3✔
2033
    const char *value = getWebMetadata(map, "AO", item, nullptr);
2✔
2034
    if (value) {
2✔
2035
      response["info"][item] = value;
4✔
2036
    }
2037
  }
2038

2039
  for (const auto &pair : {
3✔
2040
           std::make_pair("name", "contactperson"),
2041
           std::make_pair("url", "contacturl"),
2042
           std::make_pair("email", "contactelectronicmailaddress"),
2043
       }) {
4✔
2044
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
3✔
2045
    if (value) {
3✔
2046
      response["info"]["contact"][pair.first] = value;
6✔
2047
    }
2048
  }
2049

2050
  for (const auto &pair : {
2✔
2051
           std::make_pair("name", "licensename"),
2052
           std::make_pair("url", "licenseurl"),
2053
       }) {
3✔
2054
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
2✔
2055
    if (value) {
2✔
2056
      response["info"]["license"][pair.first] = value;
×
2057
    }
2058
  }
2059

2060
  {
2061
    const char *value = getWebMetadata(map, "AO", "keywords", nullptr);
1✔
2062
    if (value) {
1✔
2063
      response["info"]["x-keywords"] = value;
2✔
2064
    }
2065
  }
2066

2067
  json server;
2068
  server["url"] = msOGCAPIGetApiRootUrl(map, request);
3✔
2069

2070
  {
2071
    const char *value =
2072
        getWebMetadata(map, "AO", "server_description", nullptr);
1✔
2073
    if (value) {
1✔
2074
      server["description"] = value;
2✔
2075
    }
2076
  }
2077
  response["servers"].push_back(server);
1✔
2078

2079
  const std::string oapif_schema_base_url = msOWSGetSchemasLocation(map);
1✔
2080
  const std::string oapif_yaml_url = oapif_schema_base_url +
2081
                                     "/ogcapi/features/part1/1.0/openapi/"
2082
                                     "ogcapi-features-1.yaml";
1✔
2083
  const std::string oapif_part2_yaml_url = oapif_schema_base_url +
2084
                                           "/ogcapi/features/part2/1.0/openapi/"
2085
                                           "ogcapi-features-2.yaml";
1✔
2086

2087
  json paths;
2088

2089
  paths["/"]["get"] = {
1✔
2090
      {"summary", "Landing page"},
2091
      {"description", "Landing page"},
2092
      {"tags", {"server"}},
2093
      {"operationId", "getLandingPage"},
2094
      {"parameters",
2095
       {
2096
           {{"$ref", "#/components/parameters/f"}},
2097
       }},
2098
      {"responses",
2099
       {{"200",
2100
         {{"$ref", oapif_yaml_url + "#/components/responses/LandingPage"}}},
1✔
2101
        {"400",
2102
         {{"$ref",
2103
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2104
        {"500",
2105
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2106

2107
  paths["/api"]["get"] = {
1✔
2108
      {"summary", "API documentation"},
2109
      {"description", "API documentation"},
2110
      {"tags", {"server"}},
2111
      {"operationId", "getOpenapi"},
2112
      {"parameters",
2113
       {
2114
           {{"$ref", "#/components/parameters/f"}},
2115
       }},
2116
      {"responses",
2117
       {{"200", {{"$ref", "#/components/responses/200"}}},
2118
        {"400",
2119
         {{"$ref",
2120
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2121
        {"default", {{"$ref", "#/components/responses/default"}}}}}};
42✔
2122

2123
  paths["/conformance"]["get"] = {
1✔
2124
      {"summary", "API conformance definition"},
2125
      {"description", "API conformance definition"},
2126
      {"tags", {"server"}},
2127
      {"operationId", "getConformanceDeclaration"},
2128
      {"parameters",
2129
       {
2130
           {{"$ref", "#/components/parameters/f"}},
2131
       }},
2132
      {"responses",
2133
       {{"200",
2134
         {{"$ref",
2135
           oapif_yaml_url + "#/components/responses/ConformanceDeclaration"}}},
1✔
2136
        {"400",
2137
         {{"$ref",
2138
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2139
        {"500",
2140
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2141

2142
  paths["/collections"]["get"] = {
1✔
2143
      {"summary", "Collections"},
2144
      {"description", "Collections"},
2145
      {"tags", {"server"}},
2146
      {"operationId", "getCollections"},
2147
      {"parameters",
2148
       {
2149
           {{"$ref", "#/components/parameters/f"}},
2150
       }},
2151
      {"responses",
2152
       {{"200",
2153
         {{"$ref", oapif_yaml_url + "#/components/responses/Collections"}}},
1✔
2154
        {"400",
2155
         {{"$ref",
2156
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2157
        {"500",
2158
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2159

2160
  for (int i = 0; i < map->numlayers; i++) {
4✔
2161
    layerObj *layer = map->layers[i];
3✔
2162
    if (!includeLayer(map, layer)) {
3✔
2163
      continue;
×
2164
    }
2165

2166
    json collection_get = {
2167
        {"summary",
2168
         std::string("Get ") + getCollectionTitle(layer) + " metadata"},
3✔
2169
        {"description", getCollectionDescription(layer)},
3✔
2170
        {"tags", {layer->name}},
3✔
2171
        {"operationId", "describe" + std::string(layer->name) + "Collection"},
3✔
2172
        {"parameters",
2173
         {
2174
             {{"$ref", "#/components/parameters/f"}},
2175
         }},
2176
        {"responses",
2177
         {{"200",
2178
           {{"$ref", oapif_yaml_url + "#/components/responses/Collection"}}},
3✔
2179
          {"400",
2180
           {{"$ref",
2181
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
2182
          {"500",
2183
           {{"$ref",
2184
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
129✔
2185

2186
    std::string collectionNamePath("/collections/");
×
2187
    collectionNamePath += layer->name;
3✔
2188
    paths[collectionNamePath]["get"] = std::move(collection_get);
3✔
2189

2190
    // check metadata, layer then map
2191
    const char *max_limit_str =
2192
        msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
3✔
2193
    if (max_limit_str == nullptr)
3✔
2194
      max_limit_str =
2195
          msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
3✔
2196
    const int max_limit =
2197
        max_limit_str ? atoi(max_limit_str) : OGCAPI_MAX_LIMIT;
3✔
2198
    const int default_limit = getDefaultLimit(map, layer);
3✔
2199

2200
    json items_get = {
2201
        {"summary", std::string("Get ") + getCollectionTitle(layer) + " items"},
3✔
2202
        {"description", getCollectionDescription(layer)},
3✔
2203
        {"tags", {layer->name}},
2204
        {"operationId", "get" + std::string(layer->name) + "Features"},
3✔
2205
        {"parameters",
2206
         {
2207
             {{"$ref", "#/components/parameters/f"}},
2208
             {{"$ref", oapif_yaml_url + "#/components/parameters/bbox"}},
3✔
2209
             {{"$ref", oapif_yaml_url + "#/components/parameters/datetime"}},
3✔
2210
             {{"$ref",
2211
               oapif_part2_yaml_url + "#/components/parameters/bbox-crs"}},
3✔
2212
             {{"$ref", oapif_part2_yaml_url + "#/components/parameters/crs"}},
3✔
2213
             {{"$ref", "#/components/parameters/offset"}},
2214
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2215
         }},
2216
        {"responses",
2217
         {{"200",
2218
           {{"$ref", oapif_yaml_url + "#/components/responses/Features"}}},
3✔
2219
          {"400",
2220
           {{"$ref",
2221
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
2222
          {"500",
2223
           {{"$ref",
2224
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
201✔
2225

2226
    json param_limit = {
2227
        {"name", "limit"},
2228
        {"in", "query"},
2229
        {"description", "The optional limit parameter limits the number of "
2230
                        "items that are presented in the response document."},
2231
        {"required", false},
2232
        {"schema",
2233
         {
2234
             {"type", "integer"},
2235
             {"minimum", 1},
2236
             {"maximum", max_limit},
2237
             {"default", default_limit},
2238
         }},
2239
        {"style", "form"},
2240
        {"explode", false},
2241
    };
102✔
2242
    items_get["parameters"].emplace_back(param_limit);
3✔
2243

2244
    bool error = false;
3✔
2245
    auto reservedParams = getExtraParameters(map, layer);
3✔
2246
    reservedParams.insert("f");
3✔
2247
    reservedParams.insert("bbox");
3✔
2248
    reservedParams.insert("bbox-crs");
3✔
2249
    reservedParams.insert("datetime");
3✔
2250
    reservedParams.insert("limit");
3✔
2251
    reservedParams.insert("offset");
3✔
2252
    reservedParams.insert("crs");
3✔
2253
    const std::vector<std::string> queryableItems =
2254
        msOOGCAPIGetLayerQueryables(layer, reservedParams, error);
3✔
2255
    for (const auto &item : queryableItems) {
6✔
2256
      const auto [type, format] = getQueryableTypeAndFormat(layer, item);
3✔
2257
      json queryable_param = {
2258
          {"name", item},
2259
          {"in", "query"},
2260
          {"description", "Queryable item '" + item + "'"},
3✔
2261
          {"required", false},
2262
          {"schema",
2263
           {
2264
               {"type", type},
2265
           }},
2266
          {"style", "form"},
2267
          {"explode", false},
2268
      };
75✔
2269
      if (format) {
3✔
2270
        queryable_param["schema"]["format"] = format;
2✔
2271
      }
2272
      items_get["parameters"].emplace_back(queryable_param);
3✔
2273
    }
2274

2275
    std::string itemsPath(collectionNamePath + "/items");
3✔
2276
    paths[itemsPath]["get"] = std::move(items_get);
6✔
2277

2278
    json queryables_get = {
2279
        {"summary",
2280
         std::string("Get ") + getCollectionTitle(layer) + " queryables"},
3✔
2281
        {"description",
2282
         std::string("Get ") + getCollectionTitle(layer) + " queryables"},
3✔
2283
        {"tags", {layer->name}},
2284
        {"operationId", "get" + std::string(layer->name) + "Queryables"},
3✔
2285
        {"parameters",
2286
         {
2287
             {{"$ref", "#/components/parameters/f"}},
2288
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2289
         }},
2290
        {"responses",
2291
         {{"200",
2292
           {{"description", "The queryable properties of the collection."},
2293
            {"content",
2294
             {{"application/schema+json",
2295
               {{"schema", {{"type", "object"}}}}}}}}},
2296
          {"400",
2297
           {{"$ref",
2298
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
2299
          {"500",
2300
           {{"$ref",
2301
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
177✔
2302

2303
    std::string queryablesPath(collectionNamePath + "/queryables");
3✔
2304
    paths[queryablesPath]["get"] = std::move(queryables_get);
6✔
2305

2306
    json feature_id_get = {
2307
        {"summary",
2308
         std::string("Get ") + getCollectionTitle(layer) + " item by id"},
3✔
2309
        {"description", getCollectionDescription(layer)},
3✔
2310
        {"tags", {layer->name}},
2311
        {"operationId", "get" + std::string(layer->name) + "Feature"},
3✔
2312
        {"parameters",
2313
         {
2314
             {{"$ref", "#/components/parameters/f"}},
2315
             {{"$ref", oapif_yaml_url + "#/components/parameters/featureId"}},
3✔
2316
         }},
2317
        {"responses",
2318
         {{"200",
2319
           {{"$ref", oapif_yaml_url + "#/components/responses/Feature"}}},
3✔
2320
          {"400",
2321
           {{"$ref",
2322
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
2323
          {"404",
2324
           {{"$ref", oapif_yaml_url + "#/components/responses/NotFound"}}},
3✔
2325
          {"500",
2326
           {{"$ref",
2327
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
159✔
2328
    std::string itemsFeatureIdPath(collectionNamePath + "/items/{featureId}");
3✔
2329
    paths[itemsFeatureIdPath]["get"] = std::move(feature_id_get);
6✔
2330
  }
3✔
2331

2332
  response["paths"] = std::move(paths);
2✔
2333

2334
  json components;
2335
  components["responses"]["200"] = {{"description", "successful operation"}};
5✔
2336
  components["responses"]["default"] = {
1✔
2337
      {"description", "unexpected error"},
2338
      {"content",
2339
       {{"application/json",
2340
         {{"schema",
2341
           {{"$ref", "https://schemas.opengis.net/ogcapi/common/part1/1.0/"
2342
                     "openapi/schemas/exception.yaml"}}}}}}}};
16✔
2343

2344
  json parameters;
2345
  parameters["f"] = {
1✔
2346
      {"name", "f"},
2347
      {"in", "query"},
2348
      {"description", "The optional f parameter indicates the output format "
2349
                      "which the server shall provide as part of the response "
2350
                      "document.  The default format is GeoJSON."},
2351
      {"required", false},
2352
      {"schema",
2353
       {{"type", "string"}, {"enum", {"json", "html"}}, {"default", "json"}}},
2354
      {"style", "form"},
2355
      {"explode", false},
2356
  };
33✔
2357

2358
  parameters["offset"] = {
1✔
2359
      {"name", "offset"},
2360
      {"in", "query"},
2361
      {"description",
2362
       "The optional offset parameter indicates the index within the result "
2363
       "set from which the server shall begin presenting results in the "
2364
       "response document.  The first element has an index of 0 (default)."},
2365
      {"required", false},
2366
      {"schema",
2367
       {
2368
           {"type", "integer"},
2369
           {"minimum", 0},
2370
           {"default", 0},
2371
       }},
2372
      {"style", "form"},
2373
      {"explode", false},
2374
  };
31✔
2375

2376
  parameters["vendorSpecificParameters"] = {
1✔
2377
      {"name", "vendorSpecificParameters"},
2378
      {"in", "query"},
2379
      {"description",
2380
       "Additional \"free-form\" parameters that are not explicitly defined"},
2381
      {"schema",
2382
       {
2383
           {"type", "object"},
2384
           {"additionalProperties", true},
2385
       }},
2386
      {"style", "form"},
2387
  };
22✔
2388

2389
  components["parameters"] = std::move(parameters);
2✔
2390

2391
  response["components"] = std::move(components);
2✔
2392

2393
  // TODO: "tags" array ?
2394

2395
  outputResponse(map, request,
3✔
2396
                 format == OGCAPIFormat::JSON ? OGCAPIFormat::OpenAPI_V3
2397
                                              : format,
2398
                 OGCAPI_TEMPLATE_HTML_OPENAPI, response);
2399
  return MS_SUCCESS;
2400
}
1,287✔
2401

2402
#endif
2403

2404
OGCAPIFormat msOGCAPIGetOutputFormat(const cgiRequestObj *request) {
67✔
2405
  OGCAPIFormat format; // all endpoints need a format
2406
  const char *p = getRequestParameter(request, "f");
67✔
2407

2408
  // if f= query parameter is not specified, use HTTP Accept header if available
2409
  if (p == nullptr) {
67✔
2410
    const char *accept = getenv("HTTP_ACCEPT");
2✔
2411
    if (accept) {
2✔
2412
      if (strcmp(accept, "*/*") == 0)
1✔
2413
        p = OGCAPI_MIMETYPE_JSON;
2414
      else
2415
        p = accept;
2416
    }
2417
  }
2418

2419
  if (p &&
66✔
2420
      (strcmp(p, "json") == 0 || strstr(p, OGCAPI_MIMETYPE_JSON) != nullptr ||
66✔
2421
       strstr(p, OGCAPI_MIMETYPE_GEOJSON) != nullptr ||
11✔
2422
       strstr(p, OGCAPI_MIMETYPE_OPENAPI_V3) != nullptr)) {
2423
    format = OGCAPIFormat::JSON;
2424
  } else if (p && (strcmp(p, "html") == 0 ||
11✔
2425
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
2426
    format = OGCAPIFormat::HTML;
2427
  } else if (p) {
2428
    std::string errorMsg("Unsupported format requested: ");
×
2429
    errorMsg += p;
2430
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
×
2431
    format = OGCAPIFormat::Invalid;
2432
  } else {
2433
    format = OGCAPIFormat::HTML; // default for now
2434
  }
2435

2436
  return format;
67✔
2437
}
2438

2439
int msOGCAPIDispatchRequest(mapObj *map, cgiRequestObj *request) {
56✔
2440
#ifdef USE_OGCAPI_SVR
2441

2442
  // make sure ogcapi requests are enabled for this map
2443
  int status = msOWSRequestIsEnabled(map, NULL, "AO", "OGCAPI", MS_FALSE);
56✔
2444
  if (status != MS_TRUE) {
56✔
2445
    msSetError(MS_OGCAPIERR, "OGC API requests are not enabled.",
×
2446
               "msCGIDispatchAPIRequest()");
2447
    return MS_FAILURE; // let normal error handling take over
×
2448
  }
2449

2450
  for (int i = 0; i < request->NumParams; i++) {
167✔
2451
    for (int j = i + 1; j < request->NumParams; j++) {
198✔
2452
      if (strcmp(request->ParamNames[i], request->ParamNames[j]) == 0) {
87✔
2453
        std::string errorMsg("Query parameter ");
1✔
2454
        errorMsg += request->ParamNames[i];
1✔
2455
        errorMsg += " is repeated";
2456
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
2✔
2457
        return MS_SUCCESS;
2458
      }
2459
    }
2460
  }
2461

2462
  const OGCAPIFormat format = msOGCAPIGetOutputFormat(request);
55✔
2463

2464
  if (format == OGCAPIFormat::Invalid) {
55✔
2465
    return MS_SUCCESS; // avoid any downstream MapServer processing
2466
  }
2467

2468
  if (request->api_path_length == 2) {
55✔
2469

2470
    return processLandingRequest(map, request, format);
6✔
2471

2472
  } else if (request->api_path_length == 3) {
2473

2474
    if (strcmp(request->api_path[2], "conformance") == 0) {
8✔
2475
      return processConformanceRequest(map, request, format);
2✔
2476
    } else if (strcmp(request->api_path[2], "conformance.html") == 0) {
6✔
2477
      return processConformanceRequest(map, request, OGCAPIFormat::HTML);
×
2478
    } else if (strcmp(request->api_path[2], "collections") == 0) {
6✔
2479
      return processCollectionsRequest(map, request, format);
4✔
2480
    } else if (strcmp(request->api_path[2], "collections.html") == 0) {
2✔
2481
      return processCollectionsRequest(map, request, OGCAPIFormat::HTML);
×
2482
    } else if (strcmp(request->api_path[2], "api") == 0) {
2✔
2483
      return processApiRequest(map, request, format);
2✔
2484
    }
2485

2486
  } else if (request->api_path_length == 4) {
2487

2488
    if (strcmp(request->api_path[2], "collections") ==
7✔
2489
        0) { // next argument (3) is collectionId
2490
      return processCollectionRequest(map, request, request->api_path[3],
7✔
2491
                                      format);
7✔
2492
    }
2493

2494
  } else if (request->api_path_length == 5) {
2495

2496
    if (strcmp(request->api_path[2], "collections") == 0 &&
31✔
2497
        strcmp(request->api_path[4], "items") ==
31✔
2498
            0) { // middle argument (3) is the collectionId
2499
      return processCollectionItemsRequest(map, request, request->api_path[3],
28✔
2500
                                           NULL, format);
28✔
2501
    } else if (strcmp(request->api_path[2], "collections") == 0 &&
3✔
2502
               strcmp(request->api_path[4], "queryables") == 0) {
3✔
2503
      return processCollectionQueryablesRequest(map, request,
3✔
2504
                                                request->api_path[3], format);
3✔
2505
    }
2506

2507
  } else if (request->api_path_length == 6) {
2508

2509
    if (strcmp(request->api_path[2], "collections") == 0 &&
3✔
2510
        strcmp(request->api_path[4], "items") ==
3✔
2511
            0) { // middle argument (3) is the collectionId, last argument (5)
2512
                 // is featureId
2513
      return processCollectionItemsRequest(map, request, request->api_path[3],
3✔
2514
                                           request->api_path[5], format);
3✔
2515
    }
2516
  }
2517

2518
  msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid API path.");
×
2519
  return MS_SUCCESS; // avoid any downstream MapServer processing
×
2520
#else
2521
  msSetError(MS_OGCAPIERR, "OGC API server support is not enabled.",
2522
             "msOGCAPIDispatchRequest()");
2523
  return MS_FAILURE;
2524
#endif
2525
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc