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

geographika / mapserver / 28017742761

23 Jun 2026 09:51AM UTC coverage: 42.467% (+0.01%) from 42.453%
28017742761

push

github

geographika
Add FID tests

1 of 1 new or added line in 1 file covered. (100.0%)

535 existing lines in 4 files now uncovered.

64718 of 152397 relevant lines covered (42.47%)

27423.8 hits per line

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

83.07
/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
#include "cql2json.h"
35
#include "cql2text.h"
36

37
#include "cpl_conv.h"
38

39
#include "third-party/include_nlohmann_json.hpp"
40
#include "third-party/include_pantor_inja.hpp"
41

42
#include <algorithm>
43
#include <map>
44
#include <set>
45
#include <string>
46
#include <iostream>
47
#include <utility>
48

49
using namespace inja;
50
using json = nlohmann::json;
51

52
#define OGCAPI_DEFAULT_TITLE "MapServer OGC API"
53

54
/*
55
** HTML Templates
56
*/
57
#define OGCAPI_TEMPLATE_HTML_LANDING "landing.html"
58
#define OGCAPI_TEMPLATE_HTML_CONFORMANCE "conformance.html"
59
#define OGCAPI_TEMPLATE_HTML_COLLECTION "collection.html"
60
#define OGCAPI_TEMPLATE_HTML_COLLECTIONS "collections.html"
61
#define OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS "collection-items.html"
62
#define OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM "collection-item.html"
63
#define OGCAPI_TEMPLATE_HTML_COLLECTION_QUERYABLES "collection-queryables.html"
64
#define OGCAPI_TEMPLATE_HTML_COLLECTION_SORTABLES "collection-sortables.html"
65
#define OGCAPI_TEMPLATE_HTML_COLLECTION_SCHEMA "collection-schema.html"
66
#define OGCAPI_TEMPLATE_HTML_OPENAPI "openapi.html"
67

68
#define OGCAPI_DEFAULT_LIMIT 10 // by specification
69
#define OGCAPI_MAX_LIMIT 10000
70

71
#define OGCAPI_DEFAULT_GEOMETRY_PRECISION 6
72

73
constexpr const char *EPSG_PREFIX_URL =
74
    "http://www.opengis.net/def/crs/EPSG/0/";
75
constexpr const char *CRS84_URL =
76
    "http://www.opengis.net/def/crs/OGC/1.3/CRS84";
77

78
#ifdef USE_OGCAPI_SVR
79

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

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

117
  json j = {{"code", code}, {"description", description}};
245✔
118

119
  msIO_setHeader("Content-Type", "%s", OGCAPI_MIMETYPE_JSON);
35✔
120
  msIO_setHeader("Status", "%s", status);
35✔
121
  msIO_sendHeaders();
35✔
122
  msIO_printf("%s\n", j.dump().c_str());
70✔
123
}
280✔
124

125
static int includeLayer(mapObj *map, layerObj *layer) {
152✔
126
  if (!msOWSRequestIsEnabled(map, layer, "AO", "OGCAPI", MS_FALSE) ||
304✔
127
      !msIsLayerSupportedForWFSOrOAPIF(layer) || !msIsLayerQueryable(layer)) {
304✔
128
    return MS_FALSE;
×
129
  } else {
130
    return MS_TRUE;
131
  }
132
}
133

134
/*
135
** Get stuff...
136
*/
137

138
/*
139
** Returns the value associated with an item from the request's query string and
140
*NULL if the item was not found.
141
*/
142
static const char *getRequestParameter(const cgiRequestObj *request,
1,162✔
143
                                       const char *item) {
144
  for (int i = 0; i < request->NumParams; i++) {
3,404✔
145
    if (strcmp(item, request->ParamNames[i]) == 0)
2,573✔
146
      return request->ParamValues[i];
331✔
147
  }
148

149
  return nullptr;
150
}
151

152
static int getMaxLimit(mapObj *map, layerObj *layer) {
113✔
153
  int max_limit = OGCAPI_MAX_LIMIT;
113✔
154
  const char *value;
155

156
  // check metadata, layer then map
157
  value = msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
113✔
158
  if (value == NULL)
113✔
159
    value = msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
113✔
160

161
  if (value != NULL) {
113✔
162
    int status = msStringToInt(value, &max_limit, 10);
103✔
163
    if (status != MS_SUCCESS)
103✔
164
      max_limit = OGCAPI_MAX_LIMIT; // conversion failed
×
165
  }
166

167
  return max_limit;
113✔
168
}
169

170
static int getDefaultLimit(const mapObj *map, const layerObj *layer) {
187✔
171
  int default_limit = OGCAPI_DEFAULT_LIMIT;
187✔
172

173
  // check metadata, layer then map
174
  const char *value =
175
      msOWSLookupMetadata(&(layer->metadata), "A", "default_limit");
187✔
176
  if (value == NULL)
187✔
177
    value = msOWSLookupMetadata(&(map->web.metadata), "A", "default_limit");
187✔
178

179
  if (value != NULL) {
187✔
180
    int status = msStringToInt(value, &default_limit, 10);
169✔
181
    if (status != MS_SUCCESS)
169✔
182
      default_limit = OGCAPI_DEFAULT_LIMIT; // conversion failed
×
183
  }
184

185
  return default_limit;
187✔
186
}
187

188
static std::string getExtraParameterString(const mapObj *map,
139✔
189
                                           const layerObj *layer) {
190

191
  std::string extra_params;
192

193
  // first check layer metadata if layer is not null
194
  if (layer) {
139✔
195
    const char *layerVal =
196
        msOWSLookupMetadata(&(layer->metadata), "AO", "extra_params");
115✔
197
    if (layerVal)
115✔
198
      extra_params = std::string("&") + layerVal;
12✔
199
  }
200

201
  if (extra_params.empty() && map) {
139✔
202
    const char *mapVal =
203
        msOWSLookupMetadata(&(map->web.metadata), "AO", "extra_params");
135✔
204
    if (mapVal)
135✔
205
      extra_params = std::string("&") + mapVal;
45✔
206
  }
207

208
  return extra_params;
139✔
209
}
210

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

222
static std::set<std::string> getExtraParameters(const mapObj *map,
155✔
223
                                                const layerObj *layer) {
224

225
  // first check layer metadata if layer is not null
226
  if (layer) {
155✔
227
    const char *layerVal =
228
        msOWSLookupMetadata(&(layer->metadata), "AO", "extra_params");
137✔
229
    if (layerVal)
137✔
230
      return getExtraParameters(layerVal);
2✔
231
  }
232

233
  if (map) {
153✔
234
    const char *mapVal =
235
        msOWSLookupMetadata(&(map->web.metadata), "AO", "extra_params");
153✔
236
    if (mapVal)
153✔
237
      return getExtraParameters(mapVal);
10✔
238
  }
239

240
  return {};
143✔
241
}
242

243
static bool
244
msOOGCAPICheckQueryParameters(const mapObj *map, const cgiRequestObj *request,
150✔
245
                              const std::set<std::string> &allowedParameters) {
246
  if (msOGCAPIComplianceMode(map)) {
150✔
247
    for (int j = 0; j < request->NumParams; j++) {
473✔
248
      const char *paramName = request->ParamNames[j];
335✔
249
      if (allowedParameters.find(paramName) == allowedParameters.end()) {
670✔
250
        msOGCAPIOutputError(
12✔
251
            OGCAPI_PARAM_ERROR,
252
            (std::string("Unknown query parameter: ") + paramName).c_str());
12✔
253
        return false;
12✔
254
      }
255
    }
256
  }
257
  return true;
258
}
259

260
static std::string getItemAliasOrName(const layerObj *layer,
489✔
261
                                      const std::string &item) {
262
  std::string key = item;
263
  key += "_alias";
264
  if (const char *value =
489✔
265
          msOWSLookupMetadata(&(layer->metadata), "OGA", key.c_str())) {
489✔
266
    return value;
242✔
267
  }
268
  return item;
269
}
270

271
static const char *getGeometryName(const layerObj *layer) {
272
  const char *geometryName =
273
      msOWSLookupMetadata(&(layer->metadata), "A", "geometry_name");
73✔
274
  if (!geometryName)
79✔
275
    geometryName = "geom";
276
  return geometryName;
277
}
278

279
static const char *getGeometryFormat(const layerObj *layer) {
6✔
280
  const char *geometryFormat =
281
      msOWSLookupMetadata(&(layer->metadata), "A", "geometry_format");
6✔
282
  if (layer->type == MS_LAYER_POINT)
6✔
283
    geometryFormat = "geometry-point-or-multipoint";
284
  else if (layer->type == MS_LAYER_LINE)
285
    geometryFormat = "geometry-linestring-or-multilinestring";
286
  else if (layer->type == MS_LAYER_POLYGON)
287
    geometryFormat = "geometry-polygon-or-multipolygon";
288
  else if (!geometryFormat)
×
289
    geometryFormat = "geometry-any";
290
  return geometryFormat;
6✔
291
}
292

293
namespace {
294
struct CaseInsensitiveComparator {
295
  bool operator()(const std::string &a, const std::string &b) const {
296
    return strcasecmp(a.c_str(), b.c_str()) < 0;
5,802✔
297
  }
298
};
299
} // namespace
300

301
/** Return the list of queryable items */
302
static std::vector<std::string> msOOGCAPIGetLayerQueryables(
120✔
303
    layerObj *layer, const std::set<std::string> &reservedParams, bool &error) {
304
  error = false;
120✔
305
  std::vector<std::string> queryableItems;
306
  if (const char *value =
120✔
307
          msOWSLookupMetadata(&(layer->metadata), "OGA", "queryable_items")) {
120✔
308
    queryableItems = msStringSplit(value, ',');
105✔
309
    if (!queryableItems.empty()) {
105✔
310
      if (msLayerOpen(layer) != MS_SUCCESS ||
210✔
311
          msLayerGetItems(layer) != MS_SUCCESS) {
105✔
UNCOV
312
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot get layer fields");
×
UNCOV
313
        return {};
×
314
      }
315
      if (queryableItems[0] == "all") {
105✔
316
        queryableItems.clear();
317
        for (int i = 0; i < layer->numitems; ++i) {
20✔
318
          if (reservedParams.find(layer->items[i]) == reservedParams.end()) {
36✔
319
            queryableItems.push_back(layer->items[i]);
36✔
320
          }
321
        }
322
      } else {
323
        std::set<std::string, CaseInsensitiveComparator> validItems;
324
        for (int i = 0; i < layer->numitems; ++i) {
1,030✔
325
          validItems.insert(layer->items[i]);
1,854✔
326
        }
327
        for (const auto &item : queryableItems) {
515✔
328
          if (validItems.find(item) == validItems.end()) {
412✔
329
            // This is not a known field
330
            msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
UNCOV
331
                                "Invalid item '" + item +
×
332
                                    "' in queryable_items");
UNCOV
333
            error = true;
×
334
            return {};
×
335
          } else if (reservedParams.find(item) != reservedParams.end()) {
412✔
336
            // Check clashes with OGC API Features reserved keywords (bbox,
337
            // etc.)
UNCOV
338
            msOGCAPIOutputError(
×
339
                OGCAPI_CONFIG_ERROR,
UNCOV
340
                "Item '" + item +
×
341
                    "' in queryable_items is a reserved parameter name");
UNCOV
342
            error = true;
×
UNCOV
343
            return {};
×
344
          }
345
        }
346
      }
347
    }
348
  }
349
  return queryableItems;
350
}
120✔
351

352
/** Return the list of sortable items */
353
static std::vector<std::string> msOOGCAPIGetLayerSortables(
8✔
354
    layerObj *layer, const std::set<std::string> &reservedParams, bool &error) {
355
  error = false;
8✔
356
  std::vector<std::string> sortableItems;
357
  if (const char *value =
8✔
358
          msOWSLookupMetadata(&(layer->metadata), "OGA", "sortable_items")) {
8✔
359
    sortableItems = msStringSplit(value, ',');
7✔
360
    if (!sortableItems.empty()) {
7✔
361
      if (msLayerOpen(layer) != MS_SUCCESS ||
14✔
362
          msLayerGetItems(layer) != MS_SUCCESS) {
7✔
UNCOV
363
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot get layer fields");
×
UNCOV
364
        return {};
×
365
      }
366
      if (sortableItems[0] == "all") {
7✔
367
        sortableItems.clear();
368
        for (int i = 0; i < layer->numitems; ++i) {
10✔
369
          if (reservedParams.find(layer->items[i]) == reservedParams.end()) {
18✔
370
            sortableItems.push_back(layer->items[i]);
18✔
371
          }
372
        }
373
      } else {
374
        std::set<std::string, CaseInsensitiveComparator> validItems;
375
        for (int i = 0; i < layer->numitems; ++i) {
60✔
376
          validItems.insert(layer->items[i]);
108✔
377
        }
378
        for (const auto &item : sortableItems) {
12✔
379
          if (validItems.find(item) == validItems.end()) {
6✔
380
            // This is not a known field
UNCOV
381
            msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Invalid item '" + item +
×
382
                                                         "' in sortable_items");
UNCOV
383
            error = true;
×
384
            return {};
×
385
          } else if (reservedParams.find(item) != reservedParams.end()) {
6✔
386
            // Check clashes with OGC API Features reserved keywords (bbox,
387
            // etc.)
UNCOV
388
            msOGCAPIOutputError(
×
389
                OGCAPI_CONFIG_ERROR,
UNCOV
390
                "Item '" + item +
×
391
                    "' in sortable_items is a reserved parameter name");
UNCOV
392
            error = true;
×
UNCOV
393
            return {};
×
394
          }
395
        }
396
      }
397
    }
398
  }
399
  return sortableItems;
400
}
8✔
401

402
/*
403
** Returns the limit as an int - between 1 and getMaxLimit(). We always return a
404
*valid value...
405
*/
406
static int getLimit(mapObj *map, cgiRequestObj *request, layerObj *layer,
113✔
407
                    int *limit) {
408
  int status;
409
  const char *p;
410

411
  int max_limit;
412
  max_limit = getMaxLimit(map, layer);
113✔
413

414
  p = getRequestParameter(request, "limit");
113✔
415
  if (!p || (p && strlen(p) == 0)) { // missing or empty
113✔
416
    *limit = MS_MIN(getDefaultLimit(map, layer),
91✔
417
                    max_limit); // max could be smaller than the default
418
  } else {
419
    status = msStringToInt(p, limit, 10);
22✔
420
    if (status != MS_SUCCESS)
22✔
421
      return MS_FAILURE;
422

423
    if (*limit <= 0) {
22✔
UNCOV
424
      *limit = MS_MIN(getDefaultLimit(map, layer),
×
425
                      max_limit); // max could be smaller than the default
426
    } else {
427
      *limit = MS_MIN(*limit, max_limit);
22✔
428
    }
429
  }
430

431
  return MS_SUCCESS;
432
}
433

434
// Return the content of the "crs" member of the /collections/{name} response
435
static json getCrsList(mapObj *map, layerObj *layer) {
31✔
436
  char *pszSRSList = NULL;
31✔
437
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_FALSE,
31✔
438
                   &pszSRSList);
439
  if (!pszSRSList)
31✔
440
    msOWSGetEPSGProj(&(map->projection), &(map->web.metadata), "AOF", MS_FALSE,
3✔
441
                     &pszSRSList);
442
  json jCrsList;
443
  if (pszSRSList) {
31✔
444
    const auto tokens = msStringSplit(pszSRSList, ' ');
31✔
445
    for (const auto &crs : tokens) {
78✔
446
      if (crs.find("EPSG:") == 0) {
47✔
447
        if (jCrsList.empty()) {
16✔
448
          jCrsList.push_back(CRS84_URL);
62✔
449
        }
450
        const std::string url =
451
            std::string(EPSG_PREFIX_URL) + crs.substr(strlen("EPSG:"));
94✔
452
        jCrsList.push_back(url);
94✔
453
      }
454
    }
455
    msFree(pszSRSList);
31✔
456
  }
31✔
457
  return jCrsList;
31✔
458
}
459

460
// Return the content of the "storageCrs" member of the /collections/{name}
461
// response
462
static std::string getStorageCrs(layerObj *layer) {
20✔
463
  std::string storageCrs;
464
  char *pszFirstSRS = nullptr;
20✔
465
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_TRUE,
20✔
466
                   &pszFirstSRS);
467
  if (pszFirstSRS) {
20✔
468
    if (std::string(pszFirstSRS).find("EPSG:") == 0) {
34✔
469
      storageCrs =
470
          std::string(EPSG_PREFIX_URL) + (pszFirstSRS + strlen("EPSG:"));
51✔
471
    }
472
    msFree(pszFirstSRS);
17✔
473
  }
474
  return storageCrs;
20✔
475
}
476

477
/*
478
** Returns the bbox in output CRS (CRS84 by default, or "bbox-crs" request
479
*parameter when specified)
480
*/
481
static bool getBbox(mapObj *map, layerObj *layer, cgiRequestObj *request,
98✔
482
                    rectObj *bbox, projectionObj *outputProj) {
483
  int status;
484

485
  const char *bboxParam = getRequestParameter(request, "bbox");
98✔
486
  if (!bboxParam || strlen(bboxParam) == 0) { // missing or empty extent
98✔
487
    rectObj rect;
488
    if (FLTLayerSetInvalidRectIfSupported(map, layer, &rect, "AO")) {
91✔
UNCOV
489
      bbox->minx = rect.minx;
×
UNCOV
490
      bbox->miny = rect.miny;
×
UNCOV
491
      bbox->maxx = rect.maxx;
×
UNCOV
492
      bbox->maxy = rect.maxy;
×
493
    } else {
494
      // assign map->extent (no projection necessary)
495
      bbox->minx = map->extent.minx;
91✔
496
      bbox->miny = map->extent.miny;
91✔
497
      bbox->maxx = map->extent.maxx;
91✔
498
      bbox->maxy = map->extent.maxy;
91✔
499
    }
500
  } else {
91✔
501
    const auto tokens = msStringSplit(bboxParam, ',');
7✔
502
    if (tokens.size() != 4) {
7✔
503
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
504
      return false;
×
505
    }
506

507
    double values[4];
508
    for (int i = 0; i < 4; i++) {
35✔
509
      status = msStringToDouble(tokens[i].c_str(), &values[i]);
28✔
510
      if (status != MS_SUCCESS) {
28✔
UNCOV
511
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
UNCOV
512
        return false;
×
513
      }
514
    }
515

516
    bbox->minx = values[0]; // assign
7✔
517
    bbox->miny = values[1];
7✔
518
    bbox->maxx = values[2];
7✔
519
    bbox->maxy = values[3];
7✔
520

521
    // validate bbox is well-formed (degenerate is ok)
522
    if (MS_VALID_SEARCH_EXTENT(*bbox) != MS_TRUE) {
7✔
UNCOV
523
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
UNCOV
524
      return false;
×
525
    }
526

527
    std::string bboxCrs = "EPSG:4326";
7✔
528
    bool axisInverted =
529
        false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
530
    const char *bboxCrsParam = getRequestParameter(request, "bbox-crs");
7✔
531
    if (bboxCrsParam) {
7✔
532
      bool isExpectedCrs = false;
533
      for (const auto &crsItem : getCrsList(map, layer)) {
26✔
534
        if (bboxCrsParam == crsItem.get<std::string>()) {
22✔
535
          isExpectedCrs = true;
536
          break;
537
        }
538
      }
539
      if (!isExpectedCrs) {
4✔
540
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox-crs.");
2✔
541
        return false;
2✔
542
      }
543
      if (std::string(bboxCrsParam) != CRS84_URL) {
4✔
544
        if (std::string(bboxCrsParam).find(EPSG_PREFIX_URL) == 0) {
4✔
545
          const char *code = bboxCrsParam + strlen(EPSG_PREFIX_URL);
2✔
546
          bboxCrs = std::string("EPSG:") + code;
6✔
547
          axisInverted = msIsAxisInverted(atoi(code));
2✔
548
        }
549
      }
550
    }
551
    if (axisInverted) {
2✔
552
      std::swap(bbox->minx, bbox->miny);
553
      std::swap(bbox->maxx, bbox->maxy);
554
    }
555

556
    projectionObj bboxProj;
557
    msInitProjection(&bboxProj);
5✔
558
    msProjectionInheritContextFrom(&bboxProj, &(map->projection));
5✔
559
    if (msLoadProjectionString(&bboxProj, bboxCrs.c_str()) != 0) {
5✔
560
      msFreeProjection(&bboxProj);
×
UNCOV
561
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot process bbox-crs.");
×
562
      return false;
×
563
    }
564

565
    status = msProjectRect(&bboxProj, outputProj, bbox);
5✔
566
    msFreeProjection(&bboxProj);
5✔
567
    if (status != MS_SUCCESS) {
5✔
UNCOV
568
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
569
                          "Cannot reproject bbox from bbox-crs to output CRS.");
UNCOV
570
      return false;
×
571
    }
572
  }
7✔
573

574
  return true;
575
}
576

577
/*
578
** Returns the template directory location or NULL if it isn't set.
579
*/
580
std::string msOGCAPIGetTemplateDirectory(const mapObj *map, const char *key,
16✔
581
                                         const char *envvar) {
582
  const char *directory = NULL;
583

584
  if (map != NULL) {
16✔
585
    directory = msOWSLookupMetadata(&(map->web.metadata), "A", key);
13✔
586
  }
587

588
  if (directory == NULL) {
13✔
589
    directory = CPLGetConfigOption(envvar, NULL);
3✔
590
  }
591

592
  std::string s;
593
  if (directory != NULL) {
16✔
594
    s = directory;
595
    if (!s.empty() && (s.back() != '/' && s.back() != '\\')) {
16✔
596
      // add a trailing slash if missing
597
      std::string slash = "/";
3✔
598
#ifdef _WIN32
599
      slash = "\\";
600
#endif
601
      s += slash;
602
    }
603
  }
604

605
  return s;
16✔
606
}
607

608
/*
609
** Returns the service title from oga_{key} and/or ows_{key} or a default value
610
*if not set.
611
*/
612
static const char *getWebMetadata(const mapObj *map, const char *domain,
613
                                  const char *key, const char *defaultVal) {
614
  const char *value;
615

616
  if ((value = msOWSLookupMetadata(&(map->web.metadata), domain, key)) != NULL)
22✔
617
    return value;
618
  else
619
    return defaultVal;
1✔
620
}
621

622
/*
623
** Returns the service title from oga|ows_title or a default value if not set.
624
*/
625
static const char *getTitle(const mapObj *map) {
626
  return getWebMetadata(map, "OA", "title", OGCAPI_DEFAULT_TITLE);
627
}
628

629
/*
630
** Returns the API root URL from oga_onlineresource or builds a value if not
631
*set.
632
*/
633
std::string msOGCAPIGetApiRootUrl(const mapObj *map,
158✔
634
                                  const cgiRequestObj *request,
635
                                  const char *namespaces) {
636
  const char *root;
637
  if ((root = msOWSLookupMetadata(&(map->web.metadata), namespaces,
158✔
638
                                  "onlineresource")) != NULL) {
639
    return std::string(root);
152✔
640
  }
641

642
  std::string api_root;
643
  if (char *res = msBuildOnlineResource(NULL, request)) {
6✔
644
    api_root = res;
645
    free(res);
4✔
646

647
    // find last ogcapi in the string and strip the rest to get the root API
648
    std::size_t pos = api_root.rfind("ogcapi");
649
    if (pos != std::string::npos) {
4✔
650
      api_root = api_root.substr(0, pos + std::string("ogcapi").size());
4✔
651
    } else {
652
      // strip trailing '?' or '/' and append "/ogcapi"
653
      while (!api_root.empty() &&
6✔
654
             (api_root.back() == '?' || api_root.back() == '/')) {
6✔
655
        api_root.pop_back();
656
      }
657
      api_root += "/ogcapi";
658
    }
659
  }
660

661
  if (api_root.empty()) {
6✔
662
    api_root = "/ogcapi";
663
  }
664

665
  return api_root;
6✔
666
}
667

UNCOV
668
static json getFeatureConstant(const gmlConstantObj *constant) {
×
669
  json j; // empty (null)
670

671
  if (!constant)
×
672
    throw std::runtime_error("Null constant metadata.");
×
UNCOV
673
  if (!constant->value)
×
674
    return j;
675

676
  // initialize
UNCOV
677
  j = {{constant->name, constant->value}};
×
678

679
  return j;
×
UNCOV
680
}
×
681

682
static json getFeatureItem(const gmlItemObj *item, const char *value) {
1,561✔
683
  json j; // empty (null)
684
  const char *key;
685

686
  if (!item)
1,561✔
UNCOV
687
    throw std::runtime_error("Null item metadata.");
×
688
  if (!item->visible)
1,561✔
689
    return j;
690

691
  if (item->alias)
1,006✔
692
    key = item->alias;
608✔
693
  else
694
    key = item->name;
398✔
695

696
  // initialize
697
  j = {{key, value}};
4,024✔
698

699
  if (item->type &&
1,610✔
700
      (EQUAL(item->type, "Date") || EQUAL(item->type, "DateTime") ||
604✔
701
       EQUAL(item->type, "Time"))) {
453✔
702
    struct tm tm;
703
    if (msParseTime(value, &tm) == MS_TRUE) {
151✔
704
      char tmpValue[64];
705
      if (EQUAL(item->type, "Date"))
151✔
UNCOV
706
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02d",
×
UNCOV
707
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
×
708
      else if (EQUAL(item->type, "Time"))
151✔
UNCOV
709
        snprintf(tmpValue, sizeof(tmpValue), "%02d:%02d:%02dZ", tm.tm_hour,
×
710
                 tm.tm_min, tm.tm_sec);
711
      else
712
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02dT%02d:%02d:%02dZ",
151✔
713
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
151✔
714
                 tm.tm_min, tm.tm_sec);
715

716
      j = {{key, tmpValue}};
604✔
717
    }
718
  } else if (item->type &&
1,006✔
719
             (EQUAL(item->type, "Integer") || EQUAL(item->type, "Long"))) {
453✔
720
    try {
721
      j = {{key, std::stoll(value)}};
755✔
722
    } catch (const std::exception &) {
×
723
    }
×
724
  } else if (item->type && EQUAL(item->type, "Real")) {
704✔
725
    try {
726
      j = {{key, std::stod(value)}};
1,510✔
UNCOV
727
    } catch (const std::exception &) {
×
UNCOV
728
    }
×
729
  } else if (item->type && EQUAL(item->type, "Boolean")) {
402✔
UNCOV
730
    if (EQUAL(value, "0") || EQUAL(value, "false")) {
×
UNCOV
731
      j = {{key, false}};
×
732
    } else {
UNCOV
733
      j = {{key, true}};
×
734
    }
735
  }
736

737
  return j;
738
}
6,893✔
739

740
static double round_down(double value, int decimal_places) {
40✔
741
  const double multiplier = std::pow(10.0, decimal_places);
742
  return std::floor(value * multiplier) / multiplier;
40✔
743
}
744
// https://stackoverflow.com/questions/25925290/c-round-a-double-up-to-2-decimal-places
745
static double round_up(double value, int decimal_places) {
221,382✔
746
  const double multiplier = std::pow(10.0, decimal_places);
747
  return std::ceil(value * multiplier) / multiplier;
221,382✔
748
}
749

750
static json getFeatureGeometry(shapeObj *shape, int precision,
180✔
751
                               bool outputCrsAxisInverted) {
752
  json geometry; // empty (null)
753
  int *outerList = NULL, numOuterRings = 0;
754

755
  if (!shape)
180✔
UNCOV
756
    throw std::runtime_error("Null shape.");
×
757

758
  switch (shape->type) {
180✔
759
  case (MS_SHAPE_POINT):
23✔
760
    if (shape->numlines == 0 ||
23✔
761
        shape->line[0].numpoints == 0) // not enough info for a point
23✔
762
      return geometry;
763

764
    if (shape->line[0].numpoints == 1) {
23✔
765
      geometry["type"] = "Point";
23✔
766
      double x = shape->line[0].point[0].x;
23✔
767
      double y = shape->line[0].point[0].y;
23✔
768
      if (outputCrsAxisInverted)
23✔
769
        std::swap(x, y);
770
      geometry["coordinates"] = {round_up(x, precision),
46✔
771
                                 round_up(y, precision)};
115✔
772
    } else {
773
      geometry["type"] = "MultiPoint";
×
UNCOV
774
      geometry["coordinates"] = json::array();
×
UNCOV
775
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
UNCOV
776
        double x = shape->line[0].point[j].x;
×
777
        double y = shape->line[0].point[j].y;
×
778
        if (outputCrsAxisInverted)
×
779
          std::swap(x, y);
UNCOV
780
        geometry["coordinates"].push_back(
×
UNCOV
781
            {round_up(x, precision), round_up(y, precision)});
×
782
      }
783
    }
784
    break;
785
  case (MS_SHAPE_LINE):
×
786
    if (shape->numlines == 0 ||
×
787
        shape->line[0].numpoints < 2) // not enough info for a line
×
788
      return geometry;
789

790
    if (shape->numlines == 1) {
×
791
      geometry["type"] = "LineString";
×
UNCOV
792
      geometry["coordinates"] = json::array();
×
UNCOV
793
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
794
        double x = shape->line[0].point[j].x;
×
795
        double y = shape->line[0].point[j].y;
×
796
        if (outputCrsAxisInverted)
×
797
          std::swap(x, y);
798
        geometry["coordinates"].push_back(
×
799
            {round_up(x, precision), round_up(y, precision)});
×
800
      }
801
    } else {
UNCOV
802
      geometry["type"] = "MultiLineString";
×
803
      geometry["coordinates"] = json::array();
×
UNCOV
804
      for (int i = 0; i < shape->numlines; i++) {
×
805
        json part = json::array();
×
UNCOV
806
        for (int j = 0; j < shape->line[i].numpoints; j++) {
×
UNCOV
807
          double x = shape->line[i].point[j].x;
×
UNCOV
808
          double y = shape->line[i].point[j].y;
×
UNCOV
809
          if (outputCrsAxisInverted)
×
810
            std::swap(x, y);
UNCOV
811
          part.push_back({round_up(x, precision), round_up(y, precision)});
×
812
        }
UNCOV
813
        geometry["coordinates"].push_back(part);
×
814
      }
815
    }
816
    break;
817
  case (MS_SHAPE_POLYGON):
157✔
818
    if (shape->numlines == 0 ||
157✔
819
        shape->line[0].numpoints <
157✔
820
            4) // not enough info for a polygon (first=last)
821
      return geometry;
822

823
    outerList = msGetOuterList(shape);
157✔
824
    if (outerList == NULL)
157✔
UNCOV
825
      throw std::runtime_error("Unable to allocate list of outer rings.");
×
826
    for (int k = 0; k < shape->numlines; k++) {
320✔
827
      if (outerList[k] == MS_TRUE)
163✔
828
        numOuterRings++;
159✔
829
    }
830

831
    if (numOuterRings == 1) {
157✔
832
      geometry["type"] = "Polygon";
310✔
833
      geometry["coordinates"] = json::array();
155✔
834
      for (int i = 0; i < shape->numlines; i++) {
310✔
835
        json part = json::array();
155✔
836
        for (int j = 0; j < shape->line[i].numpoints; j++) {
110,757✔
837
          double x = shape->line[i].point[j].x;
110,602✔
838
          double y = shape->line[i].point[j].y;
110,602✔
839
          if (outputCrsAxisInverted)
110,602✔
840
            std::swap(x, y);
841
          part.push_back({round_up(x, precision), round_up(y, precision)});
663,612✔
842
        }
843
        geometry["coordinates"].push_back(part);
155✔
844
      }
845
    } else {
846
      geometry["type"] = "MultiPolygon";
4✔
847
      geometry["coordinates"] = json::array();
2✔
848

849
      for (int k = 0; k < shape->numlines; k++) {
10✔
850
        if (outerList[k] ==
8✔
851
            MS_TRUE) { // outer ring: generate polygon and add to coordinates
852
          int *innerList = msGetInnerList(shape, k, outerList);
4✔
853
          if (innerList == NULL) {
4✔
UNCOV
854
            msFree(outerList);
×
UNCOV
855
            throw std::runtime_error("Unable to allocate list of inner rings.");
×
856
          }
857

858
          json polygon = json::array();
4✔
859
          for (int i = 0; i < shape->numlines; i++) {
20✔
860
            if (i == k ||
16✔
861
                innerList[i] ==
12✔
862
                    MS_TRUE) { // add outer ring (k) and any inner rings
863
              json part = json::array();
8✔
864
              for (int j = 0; j < shape->line[i].numpoints; j++) {
54✔
865
                double x = shape->line[i].point[j].x;
46✔
866
                double y = shape->line[i].point[j].y;
46✔
867
                if (outputCrsAxisInverted)
46✔
868
                  std::swap(x, y);
869
                part.push_back(
184✔
870
                    {round_up(x, precision), round_up(y, precision)});
92✔
871
              }
872
              polygon.push_back(part);
8✔
873
            }
874
          }
875

876
          msFree(innerList);
4✔
877
          geometry["coordinates"].push_back(polygon);
4✔
878
        }
879
      }
880
    }
881
    msFree(outerList);
157✔
882
    break;
157✔
UNCOV
883
  default:
×
UNCOV
884
    throw std::runtime_error("Invalid shape type.");
×
885
    break;
886
  }
887

888
  return geometry;
889
}
890

891
/*
892
** Return a GeoJSON representation of a shape.
893
*/
894
static json getFeature(layerObj *layer, shapeObj *shape, gmlItemListObj *items,
180✔
895
                       gmlConstantListObj *constants, int geometry_precision,
896
                       bool outputCrsAxisInverted) {
897
  int i;
898
  json feature; // empty (null)
899

900
  if (!layer || !shape)
180✔
UNCOV
901
    throw std::runtime_error("Null arguments.");
×
902

903
  // initialize
904
  feature = {{"type", "Feature"}, {"properties", json::object()}};
1,440✔
905

906
  // id
907
  const char *featureIdItem =
908
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
180✔
909
  if (featureIdItem == NULL)
180✔
910
    throw std::runtime_error(
UNCOV
911
        "Missing required featureid metadata."); // should have been trapped
×
912
                                                 // earlier
913
  for (i = 0; i < items->numitems; i++) {
880✔
914
    if (strcasecmp(featureIdItem, items->items[i].name) == 0) {
880✔
915
      feature["id"] = shape->values[i];
360✔
916
      break;
180✔
917
    }
918
  }
919

920
  if (i == items->numitems)
180✔
UNCOV
921
    throw std::runtime_error("Feature id not found.");
×
922

923
  // properties - build from items and constants, no group support for now
924

925
  for (int i = 0; i < items->numitems; i++) {
1,741✔
926
    try {
927
      json item = getFeatureItem(&(items->items[i]), shape->values[i]);
1,561✔
928
      if (!item.is_null())
1,561✔
929
        feature["properties"].insert(item.begin(), item.end());
1,006✔
930
    } catch (const std::runtime_error &) {
×
931
      throw std::runtime_error("Error fetching item.");
×
932
    }
×
933
  }
934

935
  for (int i = 0; i < constants->numconstants; i++) {
180✔
936
    try {
UNCOV
937
      json constant = getFeatureConstant(&(constants->constants[i]));
×
UNCOV
938
      if (!constant.is_null())
×
UNCOV
939
        feature["properties"].insert(constant.begin(), constant.end());
×
UNCOV
940
    } catch (const std::runtime_error &) {
×
UNCOV
941
      throw std::runtime_error("Error fetching constant.");
×
UNCOV
942
    }
×
943
  }
944

945
  // geometry
946
  try {
947
    json geometry =
948
        getFeatureGeometry(shape, geometry_precision, outputCrsAxisInverted);
180✔
949
    if (!geometry.is_null())
180✔
950
      feature["geometry"] = std::move(geometry);
360✔
UNCOV
951
  } catch (const std::runtime_error &) {
×
UNCOV
952
    throw std::runtime_error("Error fetching geometry.");
×
UNCOV
953
  }
×
954

955
  return feature;
180✔
956
}
1,440✔
957

958
static json getLink(hashTableObj *metadata, const std::string &name) {
19✔
959
  json link;
960

961
  const char *href =
962
      msOWSLookupMetadata(metadata, "A", (name + "_href").c_str());
19✔
963
  if (!href)
19✔
UNCOV
964
    throw std::runtime_error("Missing required link href property.");
×
965

966
  const char *title =
967
      msOWSLookupMetadata(metadata, "A", (name + "_title").c_str());
19✔
968
  const char *type =
969
      msOWSLookupMetadata(metadata, "A", (name + "_type").c_str());
38✔
970

971
  link = {{"href", href},
972
          {"title", title ? title : href},
973
          {"type", type ? type : "text/html"}};
228✔
974

975
  return link;
19✔
976
}
190✔
977

978
static const char *getCollectionDescription(layerObj *layer) {
45✔
979
  const char *description =
980
      msOWSLookupMetadata(&(layer->metadata), "A", "description");
45✔
981
  if (!description)
45✔
982
    description = msOWSLookupMetadata(&(layer->metadata), "OF",
1✔
983
                                      "abstract"); // fallback on abstract
984
  if (!description)
1✔
985
    description =
986
        "<!-- Warning: unable to set the collection description. -->"; // finally
987
                                                                       // a
988
                                                                       // warning...
989
  return description;
45✔
990
}
991

992
static const char *getCollectionTitle(layerObj *layer) {
993
  const char *title = msOWSLookupMetadata(&(layer->metadata), "AOF", "title");
59✔
994
  if (!title)
79✔
995
    title = layer->name; // revert to layer name if no title found
4✔
996
  return title;
997
}
998

999
static int getGeometryPrecision(mapObj *map, layerObj *layer) {
115✔
1000
  int geometry_precision = OGCAPI_DEFAULT_GEOMETRY_PRECISION;
1001
  if (msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision")) {
115✔
UNCOV
1002
    geometry_precision = atoi(
×
1003
        msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision"));
1004
  } else if (msOWSLookupMetadata(&map->web.metadata, "AF",
115✔
1005
                                 "geometry_precision")) {
1006
    geometry_precision = atoi(
99✔
1007
        msOWSLookupMetadata(&map->web.metadata, "AF", "geometry_precision"));
1008
  }
1009
  return geometry_precision;
115✔
1010
}
1011

1012
static json getCollection(mapObj *map, layerObj *layer, OGCAPIFormat format,
20✔
1013
                          const std::string &api_root) {
1014
  json collection; // empty (null)
1015
  rectObj bbox;
1016

1017
  if (!map || !layer)
20✔
1018
    return collection;
1019

1020
  if (!includeLayer(map, layer))
20✔
1021
    return collection;
1022

1023
  // initialize some things
1024
  if (msOWSGetLayerExtent(map, layer, "AOF", &bbox) == MS_SUCCESS) {
20✔
1025
    if (layer->projection.numargs > 0)
20✔
1026
      msOWSProjectToWGS84(&layer->projection, &bbox);
17✔
1027
    else if (map->projection.numargs > 0)
3✔
1028
      msOWSProjectToWGS84(&map->projection, &bbox);
3✔
1029
    else
1030
      throw std::runtime_error(
UNCOV
1031
          "Unable to transform bounding box, no projection defined.");
×
1032
  } else {
1033
    throw std::runtime_error(
UNCOV
1034
        "Unable to get collection bounding box."); // might be too harsh since
×
1035
                                                   // extent is optional
1036
  }
1037

1038
  const char *description = getCollectionDescription(layer);
20✔
1039
  const char *title = getCollectionTitle(layer);
20✔
1040

1041
  const char *id = layer->name;
20✔
1042
  char *id_encoded = msEncodeUrl(id); // free after use
20✔
1043

1044
  const int geometry_precision = getGeometryPrecision(map, layer);
20✔
1045

1046
  const std::string extra_params = getExtraParameterString(map, layer);
20✔
1047

1048
  // build collection object
1049
  collection = {
1050
      {"id", id},
1051
      {"description", description},
1052
      {"title", title},
1053
      {"extent",
1054
       {{"spatial",
1055
         {{"bbox",
1056
           {{round_down(bbox.minx, geometry_precision),
20✔
1057
             round_down(bbox.miny, geometry_precision),
20✔
1058
             round_up(bbox.maxx, geometry_precision),
20✔
1059
             round_up(bbox.maxy, geometry_precision)}}},
20✔
1060
          {"crs", CRS84_URL}}}}},
1061
      {"links",
1062
       {
1063
           {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
25✔
1064
            {"type", OGCAPI_MIMETYPE_JSON},
1065
            {"title", "This collection as JSON"},
1066
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1067
                         "?f=json" + extra_params}},
20✔
1068
           {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
35✔
1069
            {"type", OGCAPI_MIMETYPE_HTML},
1070
            {"title", "This collection as HTML"},
1071
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1072
                         "?f=html" + extra_params}},
20✔
1073
           {{"rel", "items"},
1074
            {"type", OGCAPI_MIMETYPE_GEOJSON},
1075
            {"title", "Items for this collection as GeoJSON"},
1076
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1077
                         "/items?f=json" + extra_params}},
20✔
1078
           {{"rel", "items"},
1079
            {"type", OGCAPI_MIMETYPE_HTML},
1080
            {"title", "Items for this collection as HTML"},
1081
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1082
                         "/items?f=html" + extra_params}},
20✔
1083
           {{"rel", "http://www.opengis.net/def/rel/ogc/1.0/schema"},
1084
            {"type", OGCAPI_MIMETYPE_JSON_SCHEMA},
1085
            {"title", "Schema for this collection as JSON schema"},
1086
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1087
                         "/schema?f=json" + extra_params}},
20✔
1088
           {{"rel", "http://www.opengis.net/def/rel/ogc/1.0/schema"},
1089
            {"type", OGCAPI_MIMETYPE_HTML},
1090
            {"title", "Schema for this collection as HTML"},
1091
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1092
                         "/schema?f=html" + extra_params}},
20✔
1093
           {{"rel", "http://www.opengis.net/def/rel/ogc/1.0/queryables"},
1094
            {"type", OGCAPI_MIMETYPE_JSON_SCHEMA},
1095
            {"title", "Queryables for this collection as JSON schema"},
1096
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1097
                         "/queryables?f=json" + extra_params}},
20✔
1098
           {{"rel", "http://www.opengis.net/def/rel/ogc/1.0/queryables"},
1099
            {"type", OGCAPI_MIMETYPE_HTML},
1100
            {"title", "Queryables for this collection as HTML"},
1101
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1102
                         "/queryables?f=html" + extra_params}},
20✔
1103
           {{"rel", "http://www.opengis.net/def/rel/ogc/1.0/sortables"},
1104
            {"type", OGCAPI_MIMETYPE_JSON_SCHEMA},
1105
            {"title", "Sortables for this collection as JSON schema"},
1106
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1107
                         "/sortables?f=json" + extra_params}},
20✔
1108
           {{"rel", "http://www.opengis.net/def/rel/ogc/1.0/sortables"},
1109
            {"type", OGCAPI_MIMETYPE_HTML},
1110
            {"title", "Sortables for this collection as HTML"},
1111
            {"href", api_root + "/collections/" + std::string(id_encoded) +
40✔
1112
                         "/sortables?f=html" + extra_params}},
20✔
1113
       }},
1114
      {"itemType", "feature"}};
3,260✔
1115

1116
  msFree(id_encoded); // done
20✔
1117

1118
  // handle optional configuration (keywords and links)
1119
  const char *value = msOWSLookupMetadata(&(layer->metadata), "A", "keywords");
20✔
1120
  if (!value)
20✔
1121
    value = msOWSLookupMetadata(&(layer->metadata), "OF",
13✔
1122
                                "keywordlist"); // fallback on keywordlist
1123
  if (value) {
13✔
1124
    std::vector<std::string> keywords = msStringSplit(value, ',');
7✔
1125
    for (const std::string &keyword : keywords) {
32✔
1126
      collection["keywords"].push_back(keyword);
75✔
1127
    }
1128
  }
7✔
1129

1130
  value = msOWSLookupMetadata(&(layer->metadata), "A", "links");
20✔
1131
  if (value) {
20✔
1132
    std::vector<std::string> names = msStringSplit(value, ',');
11✔
1133
    for (const std::string &name : names) {
22✔
1134
      try {
1135
        json link = getLink(&(layer->metadata), name);
11✔
1136
        collection["links"].push_back(link);
11✔
UNCOV
1137
      } catch (const std::runtime_error &e) {
×
UNCOV
1138
        throw e;
×
UNCOV
1139
      }
×
1140
    }
1141
  }
11✔
1142

1143
  // Part 2 - CRS support
1144
  // Inspect metadata to set the "crs": [] member and "storageCrs" member
1145

1146
  json jCrsList = getCrsList(map, layer);
20✔
1147
  if (!jCrsList.empty()) {
20✔
1148
    collection["crs"] = std::move(jCrsList);
20✔
1149

1150
    std::string storageCrs = getStorageCrs(layer);
20✔
1151
    if (!storageCrs.empty()) {
20✔
1152
      collection["storageCrs"] = std::move(storageCrs);
34✔
1153
    }
1154
  }
1155

1156
  return collection;
1157
}
4,260✔
1158

1159
/*
1160
** Output stuff...
1161
*/
1162

1163
void msOGCAPIOutputJson(
126✔
1164
    const json &j, const char *mimetype,
1165
    const std::map<std::string, std::vector<std::string>> &extraHeaders) {
1166
  std::string js;
1167

1168
  try {
1169
    js = j.dump();
126✔
1170
  } catch (...) {
1✔
1171
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
1172
                        "Invalid UTF-8 data, check encoding.");
1173
    return;
1174
  }
1✔
1175

1176
  msIO_setHeader("Content-Type", "%s", mimetype);
125✔
1177
  for (const auto &kvp : extraHeaders) {
479✔
1178
    for (const auto &value : kvp.second) {
815✔
1179
      msIO_setHeader(kvp.first.c_str(), "%s", value.c_str());
461✔
1180
    }
1181
  }
1182
  msIO_sendHeaders();
125✔
1183
  msIO_printf("%s\n", js.c_str());
125✔
1184
}
1185

1186
void msOGCAPIOutputTemplate(const char *directory, const char *filename,
16✔
1187
                            const json &j, const char *mimetype) {
1188
  std::string _directory(directory);
16✔
1189
  std::string _filename(filename);
16✔
1190
  Environment env{_directory}; // catch
16✔
1191

1192
  // ERB-style instead of Mustache (we'll see)
1193
  // env.set_expression("<%=", "%>");
1194
  // env.set_statement("<%", "%>");
1195

1196
  // callbacks, need:
1197
  //   - match (regex)
1198
  //   - contains (substring)
1199
  //   - URL encode
1200

1201
  try {
1202
    std::string js = j.dump();
16✔
1203
  } catch (...) {
1✔
1204
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
1205
                        "Invalid UTF-8 data, check encoding.");
1206
    return;
1207
  }
1✔
1208

1209
  try {
1210
    Template t = env.parse_template(_filename); // catch
15✔
1211
    std::string result = env.render(t, j);
15✔
1212

1213
    msIO_setHeader("Content-Type", "%s", mimetype);
15✔
1214
    msIO_sendHeaders();
15✔
1215
    msIO_printf("%s\n", result.c_str());
15✔
1216
  } catch (const inja::RenderError &e) {
15✔
1217
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template rendering error. " +
×
UNCOV
1218
                                                 std::string(e.what()) + " (" +
×
1219
                                                 std::string(filename) + ").");
×
1220
    return;
UNCOV
1221
  } catch (const inja::InjaError &e) {
×
UNCOV
1222
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1223
                        "InjaError error. " + std::string(e.what()) + " (" +
×
UNCOV
1224
                            std::string(filename) + ")." + " (" +
×
UNCOV
1225
                            std::string(directory) + ").");
×
1226
    return;
UNCOV
1227
  } catch (...) {
×
UNCOV
1228
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1229
                        "General template handling error.");
1230
    return;
UNCOV
1231
  }
×
1232
}
16✔
1233

1234
/*
1235
** Generic response output.
1236
*/
1237
static void outputResponse(
125✔
1238
    const mapObj *map, const cgiRequestObj *request, OGCAPIFormat format,
1239
    const char *filename, const json &response,
1240
    const std::map<std::string, std::vector<std::string>> &extraHeaders =
1241
        std::map<std::string, std::vector<std::string>>()) {
1242
  std::string path;
1243
  char fullpath[MS_MAXPATHLEN];
1244

1245
  if (format == OGCAPIFormat::JSON) {
1246
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_JSON, extraHeaders);
13✔
1247
  } else if (format == OGCAPIFormat::GeoJSON) {
1248
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_GEOJSON, extraHeaders);
91✔
1249
  } else if (format == OGCAPIFormat::OpenAPI_V3) {
1250
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_OPENAPI_V3, extraHeaders);
1✔
1251
  } else if (format == OGCAPIFormat::JSONSchema) {
1252
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_JSON_SCHEMA, extraHeaders);
7✔
1253
  } else if (format == OGCAPIFormat::HTML) {
1254
    path = msOGCAPIGetTemplateDirectory(map, "html_template_directory",
26✔
1255
                                        "OGCAPI_HTML_TEMPLATE_DIRECTORY");
13✔
1256
    if (path.empty()) {
13✔
UNCOV
1257
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template directory not set.");
×
UNCOV
1258
      return; // bail
×
1259
    }
1260
    msBuildPath(fullpath, map->mappath, path.c_str());
13✔
1261

1262
    json j;
1263

1264
    j["response"] = response; // nest the response so we could write the whole
13✔
1265
                              // object in the template
1266

1267
    // extend the JSON with a few things that we need for templating
1268
    const std::string extra_params = getExtraParameterString(map, nullptr);
13✔
1269

1270
    j["template"] = {{"path", json::array()},
26✔
1271
                     {"params", json::object()},
13✔
1272
                     {"api_root", msOGCAPIGetApiRootUrl(map, request)},
13✔
1273
                     {"extra_params", extra_params},
1274
                     {"title", getTitle(map)},
13✔
1275
                     {"tags", json::object()}};
260✔
1276

1277
    // api path
1278
    for (int i = 0; i < request->api_path_length; i++)
68✔
1279
      j["template"]["path"].push_back(request->api_path[i]);
165✔
1280

1281
    // parameters (optional)
1282
    for (int i = 0; i < request->NumParams; i++) {
33✔
1283
      if (request->ParamValues[i] &&
20✔
1284
          strlen(request->ParamValues[i]) > 0) { // skip empty params
20✔
1285
        j["template"]["params"].update(
108✔
1286
            {{request->ParamNames[i], request->ParamValues[i]}});
36✔
1287
      }
1288
    }
1289

1290
    // add custom tags (optional)
1291
    const char *tags =
1292
        msOWSLookupMetadata(&(map->web.metadata), "A", "html_tags");
13✔
1293
    if (tags) {
13✔
1294
      std::vector<std::string> names = msStringSplit(tags, ',');
6✔
1295
      for (std::string name : names) {
18✔
1296
        const char *value = msOWSLookupMetadata(&(map->web.metadata), "A",
12✔
1297
                                                ("tag_" + name).c_str());
24✔
1298
        if (value) {
12✔
1299
          j["template"]["tags"].update({{name, value}}); // add object
72✔
1300
        }
1301
      }
1302
    }
6✔
1303

1304
    msOGCAPIOutputTemplate(fullpath, filename, j, OGCAPI_MIMETYPE_HTML);
13✔
1305
  } else {
UNCOV
1306
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
1307
  }
1308
}
437✔
1309

1310
/*
1311
** Process stuff...
1312
*/
1313
static int processLandingRequest(mapObj *map, cgiRequestObj *request,
7✔
1314
                                 OGCAPIFormat format) {
1315
  json response;
1316

1317
  auto allowedParameters = getExtraParameters(map, nullptr);
7✔
1318
  allowedParameters.insert("f");
7✔
1319
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
7✔
1320
    return MS_SUCCESS;
1321
  }
1322

1323
  // define ambiguous elements
1324
  const char *description =
1325
      msOWSLookupMetadata(&(map->web.metadata), "A", "description");
6✔
1326
  if (!description)
6✔
1327
    description =
1328
        msOWSLookupMetadata(&(map->web.metadata), "OF",
3✔
1329
                            "abstract"); // fallback on abstract if necessary
1330

1331
  const std::string extra_params = getExtraParameterString(map, nullptr);
6✔
1332

1333
  // define api root url
1334
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
6✔
1335

1336
  // build response object
1337
  //   - consider conditionally excluding links for HTML format
1338
  response = {
1339
      {"title", getTitle(map)},
6✔
1340
      {"description", description ? description : ""},
9✔
1341
      {"links",
1342
       {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
7✔
1343
         {"type", OGCAPI_MIMETYPE_JSON},
1344
         {"title", "This document as JSON"},
1345
         {"href", api_root + "?f=json" + extra_params}},
6✔
1346
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
11✔
1347
         {"type", OGCAPI_MIMETYPE_HTML},
1348
         {"title", "This document as HTML"},
1349
         {"href", api_root + "?f=html" + extra_params}},
6✔
1350
        {{"rel", "conformance"},
1351
         {"type", OGCAPI_MIMETYPE_JSON},
1352
         {"title",
1353
          "OCG API conformance classes implemented by this server (JSON)"},
1354
         {"href", api_root + "/conformance?f=json" + extra_params}},
6✔
1355
        {{"rel", "conformance"},
1356
         {"type", OGCAPI_MIMETYPE_HTML},
1357
         {"title", "OCG API conformance classes implemented by this server"},
1358
         {"href", api_root + "/conformance?f=html" + extra_params}},
6✔
1359
        {{"rel", "data"},
1360
         {"type", OGCAPI_MIMETYPE_JSON},
1361
         {"title", "Information about feature collections available from this "
1362
                   "server (JSON)"},
1363
         {"href", api_root + "/collections?f=json" + extra_params}},
6✔
1364
        {{"rel", "data"},
1365
         {"type", OGCAPI_MIMETYPE_HTML},
1366
         {"title",
1367
          "Information about feature collections available from this server"},
1368
         {"href", api_root + "/collections?f=html" + extra_params}},
6✔
1369
        {{"rel", "service-desc"},
1370
         {"type", OGCAPI_MIMETYPE_OPENAPI_V3},
1371
         {"title", "OpenAPI document"},
1372
         {"href", api_root + "/api?f=json" + extra_params}},
6✔
1373
        {{"rel", "service-doc"},
1374
         {"type", OGCAPI_MIMETYPE_HTML},
1375
         {"title", "API documentation"},
1376
         {"href", api_root + "/api?f=html" + extra_params}}}}};
690✔
1377

1378
  // handle custom links (optional)
1379
  const char *links = msOWSLookupMetadata(&(map->web.metadata), "A", "links");
6✔
1380
  if (links) {
6✔
1381
    std::vector<std::string> names = msStringSplit(links, ',');
5✔
1382
    for (std::string name : names) {
13✔
1383
      try {
1384
        json link = getLink(&(map->web.metadata), name);
8✔
1385
        response["links"].push_back(link);
8✔
UNCOV
1386
      } catch (const std::runtime_error &e) {
×
UNCOV
1387
        msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, std::string(e.what()));
×
1388
        return MS_SUCCESS;
UNCOV
1389
      }
×
1390
    }
1391
  }
5✔
1392

1393
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_LANDING, response);
6✔
1394
  return MS_SUCCESS;
6✔
1395
}
918✔
1396

1397
static int processConformanceRequest(mapObj *map, cgiRequestObj *request,
3✔
1398
                                     OGCAPIFormat format) {
1399
  json response;
1400

1401
  auto allowedParameters = getExtraParameters(map, nullptr);
3✔
1402
  allowedParameters.insert("f");
3✔
1403
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
3✔
1404
    return MS_SUCCESS;
1405
  }
1406

1407
  // build response object
1408
  response = {
1409
      {"conformsTo",
1410
       {
1411
           "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core",
1412
           "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections",
1413
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
1414
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30",
1415
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html",
1416
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
1417
           "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs",
1418
           "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables",
1419
           "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/"
1420
           "queryables-query-parameters",
1421
           "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter",
1422
           "http://www.opengis.net/spec/cql2/1.0/conf/cql2-text",
1423
           "http://www.opengis.net/spec/cql2/1.0/conf/cql2-json",
1424
           "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2",
1425
           "http://www.opengis.net/spec/cql2/1.0/conf/"
1426
           "advanced-comparison-operators",
1427
           "http://www.opengis.net/spec/cql2/1.0/conf/"
1428
           "case-insensitive-comparison",
1429
           "http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-functions",
1430
           "http://www.opengis.net/spec/cql2/1.0/conf/"
1431
           "basic-spatial-functions-plus",
1432
           "http://www.opengis.net/spec/cql2/1.0/conf/spatial-functions",
1433
           "http://www.opengis.net/spec/cql2/1.0/conf/property-property",
1434
           "http://www.opengis.net/spec/cql2/1.0/conf/arithmetic",
1435
           "http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/queryables",
1436
           "http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/schemas",
1437
           "http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/"
1438
           "returnables-and-receivables",
1439
           "http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/sortables",
1440
       }}};
56✔
1441

1442
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_CONFORMANCE,
2✔
1443
                 response);
1444
  return MS_SUCCESS;
2✔
1445
}
56✔
1446

1447
static int findLayerIndex(const mapObj *map, const char *collectionId) {
130✔
1448
  for (int i = 0; i < map->numlayers; i++) {
161✔
1449
    if (strcmp(map->layers[i]->name, collectionId) == 0) {
158✔
1450
      return i;
127✔
1451
    }
1452
  }
1453
  return -1;
1454
}
1455

1456
static int processCollectionItemsRequest(mapObj *map, cgiRequestObj *request,
113✔
1457
                                         const char *collectionId,
1458
                                         const char *featureId,
1459
                                         OGCAPIFormat format) {
1460
  json response;
1461

1462
  // find the right layer
1463
  const int iLayer = findLayerIndex(map, collectionId);
113✔
1464

1465
  if (iLayer < 0) { // invalid collectionId
113✔
1466
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1467
    return MS_SUCCESS;
×
1468
  }
1469

1470
  layerObj *layer = map->layers[iLayer];
113✔
1471
  layer->status = MS_ON; // force on (do we need to save and reset?)
113✔
1472

1473
  if (!includeLayer(map, layer)) {
113✔
UNCOV
1474
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1475
    return MS_SUCCESS;
×
1476
  }
1477

1478
  //
1479
  // handle parameters specific to this endpoint
1480
  //
1481
  int limit = -1;
113✔
1482
  if (getLimit(map, request, layer, &limit) != MS_SUCCESS) {
113✔
UNCOV
1483
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for limit.");
×
UNCOV
1484
    return MS_SUCCESS;
×
1485
  }
1486

1487
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
113✔
1488
  const char *crs = getRequestParameter(request, "crs");
113✔
1489

1490
  std::string outputCrs = "EPSG:4326";
113✔
1491
  bool outputCrsAxisInverted =
1492
      false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
1493
  std::map<std::string, std::vector<std::string>> extraHeaders;
1494
  if (crs) {
113✔
1495
    bool isExpectedCrs = false;
1496
    for (const auto &crsItem : getCrsList(map, layer)) {
26✔
1497
      if (crs == crsItem.get<std::string>()) {
22✔
1498
        isExpectedCrs = true;
1499
        break;
1500
      }
1501
    }
1502
    if (!isExpectedCrs) {
4✔
1503
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for crs.");
2✔
1504
      return MS_SUCCESS;
2✔
1505
    }
1506
    extraHeaders["Content-Crs"].push_back('<' + std::string(crs) + '>');
6✔
1507
    if (std::string(crs) != CRS84_URL) {
4✔
1508
      if (std::string(crs).find(EPSG_PREFIX_URL) == 0) {
4✔
1509
        const char *code = crs + strlen(EPSG_PREFIX_URL);
2✔
1510
        outputCrs = std::string("EPSG:") + code;
6✔
1511
        outputCrsAxisInverted = msIsAxisInverted(atoi(code));
2✔
1512
      }
1513
    }
1514
  } else {
1515
    extraHeaders["Content-Crs"].push_back('<' + std::string(CRS84_URL) + '>');
327✔
1516
  }
1517

1518
  auto reservedParameters = getExtraParameters(map, layer);
111✔
1519
  reservedParameters.insert("f");
111✔
1520
  reservedParameters.insert("bbox");
111✔
1521
  reservedParameters.insert("bbox-crs");
111✔
1522
  reservedParameters.insert("datetime");
111✔
1523
  reservedParameters.insert("limit");
111✔
1524
  reservedParameters.insert("offset");
111✔
1525
  reservedParameters.insert("crs");
111✔
1526
  reservedParameters.insert("filter");
111✔
1527
  reservedParameters.insert("filter-lang");
111✔
1528
  reservedParameters.insert("filter-crs");
111✔
1529
  reservedParameters.insert("sortby");
111✔
1530

1531
  bool error = false;
1532
  std::vector<std::string> queryableItems =
1533
      msOOGCAPIGetLayerQueryables(layer, reservedParameters, error);
111✔
1534
  if (error) {
111✔
1535
    return MS_SUCCESS;
1536
  }
1537

1538
  for (std::string &item : queryableItems) {
511✔
1539
    item = getItemAliasOrName(layer, item);
800✔
1540
  }
1541

1542
  auto allowedParameters = reservedParameters;
1543
  for (const auto &item : queryableItems)
511✔
1544
    allowedParameters.insert(item);
1545

1546
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
111✔
1547
    return MS_SUCCESS;
1548
  }
1549

1550
  // Simple filtering like "field_name=value"
1551
  std::string filter;
1552
  std::string query_kvp;
1553
  if (!queryableItems.empty()) {
108✔
1554
    for (int i = 0; i < request->NumParams; i++) {
352✔
1555
      if (std::find(queryableItems.begin(), queryableItems.end(),
254✔
1556
                    request->ParamNames[i]) != queryableItems.end()) {
254✔
1557

1558
        // Find actual item name from alias
1559
        const char *pszItem = nullptr;
1560
        for (int j = 0; j < layer->numitems; ++j) {
48✔
1561
          if (request->ParamNames[i] ==
48✔
1562
              getItemAliasOrName(layer, layer->items[j])) {
96✔
1563
            pszItem = layer->items[j];
8✔
1564
            break;
8✔
1565
          }
1566
        }
1567
        assert(pszItem);
1568

1569
        const std::string expr = FLTGetBinaryComparisonCommonExpression(
1570
            layer, pszItem, false, "=", request->ParamValues[i]);
8✔
1571
        if (!filter.empty())
8✔
1572
          filter += " AND ";
1573
        filter += expr;
1574

1575
        query_kvp += '&';
1576
        char *encoded = msEncodeUrl(request->ParamNames[i]);
8✔
1577
        query_kvp += encoded;
1578
        msFree(encoded);
8✔
1579
        query_kvp += '=';
1580
        encoded = msEncodeUrl(request->ParamValues[i]);
8✔
1581
        query_kvp += encoded;
1582
        msFree(encoded);
8✔
1583
      }
1584
    }
1585
  }
1586

1587
  const char *filterParam = getRequestParameter(request, "filter");
108✔
1588
  const char *filterLang = getRequestParameter(request, "filter-lang");
108✔
1589
  if (filterParam) {
108✔
1590
    if (filterLang && strcmp(filterLang, "cql2-text") != 0 &&
78✔
1591
        strcmp(filterLang, "cql2-json") != 0) {
30✔
1592
      msOGCAPIOutputError(
1✔
1593
          OGCAPI_PARAM_ERROR,
1594
          "Only filter-lang=cql2-text or filter-lang=cql2-json is handled");
1595
      return MS_SUCCESS;
9✔
1596
    }
1597

1598
    std::string osErrorMsg;
1599
    auto cql2 = (filterLang && strcmp(filterLang, "cql2-json") == 0)
29✔
1600
                    ? CQL2JSONParse(filterParam, osErrorMsg)
77✔
1601
                    : CQL2TextParse(filterParam, osErrorMsg);
77✔
1602
    if (!cql2) {
77✔
1603
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR,
3✔
1604
                          "Cannot parse filter: " + osErrorMsg);
3✔
1605
      return MS_SUCCESS;
3✔
1606
    }
1607

1608
    std::string filterCrs = "EPSG:4326";
74✔
1609
    bool axisInverted =
1610
        false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
1611
    const char *filterCrsParam = getRequestParameter(request, "filter-crs");
74✔
1612
    if (filterCrsParam) {
74✔
1613
      bool isExpectedCrs = false;
1614
      for (const auto &crsItem : getCrsList(map, layer)) {
20✔
1615
        if (filterCrsParam == crsItem.get<std::string>()) {
18✔
1616
          isExpectedCrs = true;
1617
          break;
1618
        }
1619
      }
1620
      if (!isExpectedCrs) {
3✔
1621
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for filter-crs.");
1✔
1622
        return MS_SUCCESS;
1✔
1623
      }
1624
      if (std::string(filterCrsParam) != CRS84_URL) {
4✔
1625
        if (std::string(filterCrsParam).find(EPSG_PREFIX_URL) == 0) {
4✔
1626
          const char *code = filterCrsParam + strlen(EPSG_PREFIX_URL);
2✔
1627
          filterCrs = std::string("EPSG:") + code;
6✔
1628
          axisInverted = msIsAxisInverted(atoi(code));
2✔
1629
        }
1630
      }
1631
    }
1632

1633
    const char *geometryName = getGeometryName(layer);
1634
    const std::string filterFromCQL =
1635
        cql2->ToMapServerFilter(layer, queryableItems, geometryName, filterCrs,
1636
                                axisInverted, osErrorMsg);
146✔
1637
    if (!osErrorMsg.empty()) {
73✔
1638
      msOGCAPIOutputError(
4✔
1639
          OGCAPI_PARAM_ERROR,
1640
          "Cannot translate CQL2 filter to MapServer expression: " +
8✔
1641
              osErrorMsg);
1642
      return MS_SUCCESS;
1643
    }
1644
    if (filter.empty())
69✔
1645
      filter = filterFromCQL;
1646
    else {
1647
      filter = "(" + filter + ") AND (" + filterFromCQL + ")";
12✔
1648
    }
1649

1650
    query_kvp += "&filter=";
1651
    char *encoded = msEncodeUrl(filterParam);
69✔
1652
    query_kvp += encoded;
1653
    msFree(encoded);
69✔
1654
    if (filterLang) {
69✔
1655
      query_kvp += "&filter-lang=";
1656
      query_kvp += filterLang;
1657
    }
1658
    if (filterCrsParam) {
69✔
1659
      query_kvp += "&filter-crs=";
1660
      encoded = msEncodeUrl(filterCrsParam);
2✔
1661
      query_kvp += encoded;
1662
      msFree(encoded);
2✔
1663
    }
1664
  }
77✔
1665

1666
  if (!filter.empty()) {
99✔
1667
    msDebug("filter = %s\n", filter.c_str());
73✔
1668
  }
1669

1670
  const char *sortby = getRequestParameter(request, "sortby");
99✔
1671
  if (sortby) {
99✔
1672
    query_kvp += "&sortby=";
1673
    {
1674
      char *encoded = msEncodeUrl(sortby);
4✔
1675
      query_kvp += encoded;
1676
      msFree(encoded);
4✔
1677
    }
1678

1679
    const auto sortables =
1680
        msOOGCAPIGetLayerSortables(layer, reservedParameters, error);
4✔
1681
    if (error) {
4✔
1682
      return MS_SUCCESS;
1683
    }
1684

1685
    std::vector<sortByProperties> props;
1686
    struct msFreeReleaser {
1687
      void operator()(char *s) { msFree(s); }
4✔
1688
    };
1689
    std::vector<std::unique_ptr<char, msFreeReleaser>> items;
1690
    for (const auto &item : msStringSplit(sortby, ',')) {
7✔
1691
      if (item.empty())
4✔
UNCOV
1692
        continue;
×
1693
      sortByProperties prop;
1694
      if (item[0] == '-') {
4✔
1695
        prop.sortOrder = SORT_DESC;
1✔
1696
        items.emplace_back(msStrdup(item.c_str() + 1));
1✔
1697
      } else if (item[0] == '+') {
3✔
1698
        prop.sortOrder = SORT_ASC;
1✔
1699
        items.emplace_back(msStrdup(item.c_str() + 1));
1✔
1700
      } else {
1701
        prop.sortOrder = SORT_ASC;
2✔
1702
        items.emplace_back(msStrdup(item.c_str()));
2✔
1703
      }
1704
      prop.item = items.back().get();
4✔
1705
      if (std::find_if(sortables.begin(), sortables.end(),
4✔
1706
                       [&prop](const std::string &s) {
1707
                         return strcasecmp(prop.item, s.c_str()) == 0;
4✔
1708
                       }) == sortables.end()) {
1709
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, (std::string("'") + prop.item +
2✔
1710
                                                 "' is not a sortable item")
1711
                                                    .c_str());
1712
        return MS_SUCCESS;
1✔
1713
      }
1714
      props.push_back(prop);
3✔
1715
    }
4✔
1716

1717
    sortByClause sortByClause;
1718
    sortByClause.nProperties = static_cast<int>(props.size());
3✔
1719
    sortByClause.properties = props.data();
3✔
1720
    msLayerSetSort(layer, &sortByClause);
3✔
1721
  }
4✔
1722

1723
  struct ReprojectionObjects {
1724
    reprojectionObj *reprojector = NULL;
1725
    projectionObj proj;
1726

1727
    ReprojectionObjects() { msInitProjection(&proj); }
98✔
1728

1729
    ~ReprojectionObjects() {
1730
      msProjectDestroyReprojector(reprojector);
98✔
1731
      msFreeProjection(&proj);
98✔
1732
    }
98✔
1733

1734
    int executeQuery(mapObj *map) {
177✔
1735
      projectionObj backupMapProjection = map->projection;
177✔
1736
      map->projection = proj;
177✔
1737
      int ret = msExecuteQuery(map);
177✔
1738
      map->projection = backupMapProjection;
177✔
1739
      return ret;
177✔
1740
    }
1741
  };
1742
  ReprojectionObjects reprObjs;
1743

1744
  msProjectionInheritContextFrom(&reprObjs.proj, &(map->projection));
98✔
1745
  if (msLoadProjectionString(&reprObjs.proj, outputCrs.c_str()) != 0) {
98✔
UNCOV
1746
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot instantiate output CRS.");
×
1747
    return MS_SUCCESS;
×
1748
  }
1749

1750
  if (layer->projection.numargs > 0) {
98✔
1751
    if (msProjectionsDiffer(&(layer->projection), &reprObjs.proj)) {
98✔
1752
      reprObjs.reprojector =
91✔
1753
          msProjectCreateReprojector(&(layer->projection), &reprObjs.proj);
91✔
1754
      if (reprObjs.reprojector == NULL) {
91✔
1755
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1756
                            "Error creating re-projector.");
1757
        return MS_SUCCESS;
×
1758
      }
1759
    }
UNCOV
1760
  } else if (map->projection.numargs > 0) {
×
1761
    if (msProjectionsDiffer(&(map->projection), &reprObjs.proj)) {
×
UNCOV
1762
      reprObjs.reprojector =
×
UNCOV
1763
          msProjectCreateReprojector(&(map->projection), &reprObjs.proj);
×
1764
      if (reprObjs.reprojector == NULL) {
×
UNCOV
1765
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1766
                            "Error creating re-projector.");
UNCOV
1767
        return MS_SUCCESS;
×
1768
      }
1769
    }
1770
  } else {
UNCOV
1771
    msOGCAPIOutputError(
×
1772
        OGCAPI_CONFIG_ERROR,
1773
        "Unable to transform geometries, no projection defined.");
UNCOV
1774
    return MS_SUCCESS;
×
1775
  }
1776

1777
  if (map->projection.numargs > 0) {
98✔
1778
    msProjectRect(&(map->projection), &reprObjs.proj, &map->extent);
98✔
1779
  }
1780

1781
  rectObj bbox;
1782
  if (!getBbox(map, layer, request, &bbox, &reprObjs.proj)) {
98✔
1783
    return MS_SUCCESS;
1784
  }
1785

1786
  int offset = 0;
96✔
1787
  int numberMatched = 0;
1788
  if (featureId) {
96✔
1789
    const char *featureIdItem =
1790
        msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
3✔
1791
    if (featureIdItem == NULL) {
3✔
UNCOV
1792
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1793
                          "Missing required featureid metadata.");
UNCOV
1794
      return MS_SUCCESS;
×
1795
    }
1796

1797
    // TODO: does featureIdItem exist in the data?
1798

1799
    // optional validation
1800
    const char *featureIdValidation =
1801
        msLookupHashTable(&(layer->validation), featureIdItem);
3✔
1802
    if (featureIdValidation &&
6✔
1803
        msValidateParameter(featureId, featureIdValidation, NULL, NULL, NULL) !=
3✔
1804
            MS_SUCCESS) {
1805
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid feature id.");
1✔
1806
      return MS_SUCCESS;
1✔
1807
    }
1808

1809
    map->query.type = MS_QUERY_BY_FILTER;
2✔
1810
    map->query.mode = MS_QUERY_SINGLE;
2✔
1811
    map->query.layer = iLayer;
2✔
1812
    map->query.rect = bbox;
2✔
1813
    map->query.filteritem = strdup(featureIdItem);
2✔
1814

1815
    msInitExpression(&map->query.filter);
2✔
1816
    map->query.filter.type = MS_STRING;
2✔
1817
    map->query.filter.string = strdup(featureId);
2✔
1818

1819
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
2✔
UNCOV
1820
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1821
                          "Collection items id query failed.");
UNCOV
1822
      return MS_SUCCESS;
×
1823
    }
1824

1825
    if (!layer->resultcache || layer->resultcache->numresults != 1) {
2✔
UNCOV
1826
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1827
                          "Collection items id query failed.");
UNCOV
1828
      return MS_SUCCESS;
×
1829
    }
1830
  } else { // bbox query
1831
    map->query.type = MS_QUERY_BY_RECT;
93✔
1832
    map->query.mode = MS_QUERY_MULTIPLE;
93✔
1833
    map->query.layer = iLayer;
93✔
1834
    map->query.rect = bbox;
93✔
1835
    map->query.only_cache_result_count = MS_TRUE;
93✔
1836

1837
    if (!filter.empty()) {
93✔
1838
      map->query.type = MS_QUERY_BY_FILTER;
73✔
1839
      msInitExpression(&map->query.filter);
73✔
1840
      map->query.filter.string = msStrdup(filter.c_str());
73✔
1841
      map->query.filter.type = MS_EXPRESSION;
73✔
1842
    }
1843

1844
    // get number matched
1845
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
93✔
UNCOV
1846
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1847
                          "Collection items query failed.");
UNCOV
1848
      return MS_SUCCESS;
×
1849
    }
1850

1851
    if (!layer->resultcache) {
93✔
UNCOV
1852
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1853
                          "Collection items query failed.");
UNCOV
1854
      return MS_SUCCESS;
×
1855
    }
1856

1857
    numberMatched = layer->resultcache->numresults;
93✔
1858

1859
    if (numberMatched > 0) {
93✔
1860
      map->query.only_cache_result_count = MS_FALSE;
82✔
1861
      map->query.maxfeatures = limit;
82✔
1862

1863
      msOWSSetShapeCache(map, "AO");
82✔
1864

1865
      const char *offsetStr = getRequestParameter(request, "offset");
82✔
1866
      if (offsetStr) {
82✔
1867
        if (msStringToInt(offsetStr, &offset, 10) != MS_SUCCESS) {
1✔
UNCOV
1868
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for offset.");
×
UNCOV
1869
          return MS_SUCCESS;
×
1870
        }
1871

1872
        if (offset < 0 || offset >= numberMatched) {
1✔
UNCOV
1873
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Offset out of range.");
×
1874
          return MS_SUCCESS;
×
1875
        }
1876

1877
        // msExecuteQuery() use a 1-based offset convention, whereas the API
1878
        // uses a 0-based offset convention.
1879
        map->query.startindex = 1 + offset;
1✔
1880
        layer->startindex = 1 + offset;
1✔
1881
      }
1882

1883
      if (reprObjs.executeQuery(map) != MS_SUCCESS || !layer->resultcache) {
82✔
UNCOV
1884
        msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1885
                            "Collection items query failed.");
UNCOV
1886
        return MS_SUCCESS;
×
1887
      }
1888
    }
1889
  }
1890

1891
  const std::string extra_params = getExtraParameterString(map, layer);
95✔
1892

1893
  // build response object
1894
  if (!featureId) {
95✔
1895
    const char *id = layer->name;
93✔
1896
    char *id_encoded = msEncodeUrl(id); // free after use
93✔
1897

1898
    std::string extra_kvp = "&limit=" + std::to_string(limit);
93✔
1899
    extra_kvp += "&offset=" + std::to_string(offset);
186✔
1900

1901
    std::string other_extra_kvp;
1902
    if (crs)
93✔
1903
      other_extra_kvp += "&crs=" + std::string(crs);
4✔
1904
    const char *bbox = getRequestParameter(request, "bbox");
93✔
1905
    if (bbox)
93✔
1906
      other_extra_kvp += "&bbox=" + std::string(bbox);
10✔
1907
    const char *bboxCrs = getRequestParameter(request, "bbox-crs");
93✔
1908
    if (bboxCrs)
93✔
1909
      other_extra_kvp += "&bbox-crs=" + std::string(bboxCrs);
4✔
1910

1911
    other_extra_kvp += query_kvp;
1912

1913
    response = {{"type", "FeatureCollection"},
1914
                {"numberMatched", numberMatched},
1915
                {"numberReturned", layer->resultcache->numresults},
93✔
1916
                {"features", json::array()},
93✔
1917
                {"links",
1918
                 {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
97✔
1919
                   {"type", OGCAPI_MIMETYPE_GEOJSON},
1920
                   {"title", "Items for this collection as GeoJSON"},
1921
                   {"href", api_root + "/collections/" +
186✔
1922
                                std::string(id_encoded) + "/items?f=json" +
93✔
1923
                                extra_kvp + other_extra_kvp + extra_params}},
93✔
1924
                  {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
182✔
1925
                   {"type", OGCAPI_MIMETYPE_HTML},
1926
                   {"title", "Items for this collection as HTML"},
1927
                   {"href", api_root + "/collections/" +
186✔
1928
                                std::string(id_encoded) + "/items?f=html" +
93✔
1929
                                extra_kvp + other_extra_kvp + extra_params}}}}};
3,906✔
1930

1931
    if (offset + layer->resultcache->numresults < numberMatched) {
93✔
1932
      response["links"].push_back(
266✔
1933
          {{"rel", "next"},
1934
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
20✔
1935
                                                 : OGCAPI_MIMETYPE_HTML},
1936
           {"title", "next page"},
1937
           {"href",
1938
            api_root + "/collections/" + std::string(id_encoded) +
38✔
1939
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
38✔
1940
                "&limit=" + std::to_string(limit) +
57✔
1941
                "&offset=" + std::to_string(offset + limit) + other_extra_kvp +
38✔
1942
                extra_params}});
1943
    }
1944

1945
    if (offset > 0) {
93✔
1946
      response["links"].push_back(
14✔
1947
          {{"rel", "prev"},
1948
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
1✔
1949
                                                 : OGCAPI_MIMETYPE_HTML},
1950
           {"title", "previous page"},
1951
           {"href",
1952
            api_root + "/collections/" + std::string(id_encoded) +
2✔
1953
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
2✔
1954
                "&limit=" + std::to_string(limit) +
3✔
1955
                "&offset=" + std::to_string(MS_MAX(0, (offset - limit))) +
2✔
1956
                other_extra_kvp + extra_params}});
1✔
1957
    }
1958

1959
    extraHeaders["OGC-NumberReturned"].push_back(
93✔
1960
        std::to_string(layer->resultcache->numresults));
186✔
1961
    extraHeaders["OGC-NumberMatched"].push_back(std::to_string(numberMatched));
186✔
1962
    std::vector<std::string> linksHeaders;
1963
    for (auto &link : response["links"]) {
392✔
1964
      linksHeaders.push_back("<" + link["href"].get<std::string>() +
412✔
1965
                             ">; rel=\"" + link["rel"].get<std::string>() +
618✔
1966
                             "\"; title=\"" + link["title"].get<std::string>() +
618✔
1967
                             "\"; type=\"" + link["type"].get<std::string>() +
618✔
1968
                             "\"");
1969
    }
1970
    extraHeaders["Link"] = std::move(linksHeaders);
93✔
1971

1972
    msFree(id_encoded); // done
93✔
1973
  }
93✔
1974

1975
  // features (items)
1976
  {
1977
    shapeObj shape;
95✔
1978
    msInitShape(&shape);
95✔
1979

1980
    // we piggyback on GML configuration
1981
    gmlItemListObj *items = msGMLGetItems(layer, "AG");
95✔
1982
    gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
95✔
1983

1984
    if (!items || !constants) {
95✔
UNCOV
1985
      msGMLFreeItems(items);
×
1986
      msGMLFreeConstants(constants);
×
UNCOV
1987
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1988
                          "Error fetching layer attribute metadata.");
UNCOV
1989
      return MS_SUCCESS;
×
1990
    }
1991

1992
    const int geometry_precision = getGeometryPrecision(map, layer);
95✔
1993

1994
    for (int i = 0; i < layer->resultcache->numresults; i++) {
275✔
1995
      if (layer->resultcache->results[i].shape) {
180✔
UNCOV
1996
        msCopyShape(layer->resultcache->results[i].shape, &shape);
×
1997
      } else {
1998
        int status =
1999
            msLayerGetShape(layer, &shape, &(layer->resultcache->results[i]));
180✔
2000
        if (status != MS_SUCCESS) {
180✔
2001
          msGMLFreeItems(items);
×
2002
          msGMLFreeConstants(constants);
×
2003
          msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Error fetching feature.");
×
UNCOV
2004
          return MS_SUCCESS;
×
2005
        }
2006

2007
        if (reprObjs.reprojector) {
180✔
2008
          status = msProjectShapeEx(reprObjs.reprojector, &shape);
173✔
2009
          if (status != MS_SUCCESS) {
173✔
UNCOV
2010
            msGMLFreeItems(items);
×
UNCOV
2011
            msGMLFreeConstants(constants);
×
UNCOV
2012
            msFreeShape(&shape);
×
UNCOV
2013
            msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
2014
                                "Error reprojecting feature.");
UNCOV
2015
            return MS_SUCCESS;
×
2016
          }
2017
        }
2018
      }
2019

2020
      try {
2021
        json feature = getFeature(layer, &shape, items, constants,
2022
                                  geometry_precision, outputCrsAxisInverted);
180✔
2023
        if (featureId) {
180✔
2024
          response = std::move(feature);
2✔
2025
        } else {
2026
          response["features"].emplace_back(std::move(feature));
178✔
2027
        }
UNCOV
2028
      } catch (const std::runtime_error &e) {
×
UNCOV
2029
        msGMLFreeItems(items);
×
UNCOV
2030
        msGMLFreeConstants(constants);
×
UNCOV
2031
        msFreeShape(&shape);
×
UNCOV
2032
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
UNCOV
2033
                            "Error getting feature. " + std::string(e.what()));
×
2034
        return MS_SUCCESS;
UNCOV
2035
      }
×
2036

2037
      msFreeShape(&shape); // next
180✔
2038
    }
2039

2040
    msGMLFreeItems(items); // clean up
95✔
2041
    msGMLFreeConstants(constants);
95✔
2042
  }
95✔
2043

2044
  // extend the response a bit for templating (HERE)
2045
  if (format == OGCAPIFormat::HTML) {
95✔
2046
    const char *title = getCollectionTitle(layer);
2047
    const char *id = layer->name;
4✔
2048
    response["collection"] = {{"id", id}, {"title", title ? title : ""}};
36✔
2049
  }
2050

2051
  if (featureId) {
95✔
2052
    const char *id = layer->name;
2✔
2053
    char *id_encoded = msEncodeUrl(id); // free after use
2✔
2054

2055
    response["links"] = {
2✔
2056
        {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
2✔
2057
         {"type", OGCAPI_MIMETYPE_GEOJSON},
2058
         {"title", "This document as GeoJSON"},
2059
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
2060
                      "/items/" + featureId + "?f=json" + extra_params}},
2✔
2061
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
4✔
2062
         {"type", OGCAPI_MIMETYPE_HTML},
2063
         {"title", "This document as HTML"},
2064
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
2065
                      "/items/" + featureId + "?f=html" + extra_params}},
2✔
2066
        {{"rel", "collection"},
2067
         {"type", OGCAPI_MIMETYPE_JSON},
2068
         {"title", "This collection as JSON"},
2069
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
2070
                      "?f=json" + extra_params}},
2✔
2071
        {{"rel", "collection"},
2072
         {"type", OGCAPI_MIMETYPE_HTML},
2073
         {"title", "This collection as HTML"},
2074
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
2075
                      "?f=html" + extra_params}}};
106✔
2076

2077
    msFree(id_encoded);
2✔
2078

2079
    outputResponse(
2✔
2080
        map, request,
2081
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
2082
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM, response, extraHeaders);
2083
  } else {
2084
    outputResponse(
182✔
2085
        map, request,
2086
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
2087
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS, response, extraHeaders);
2088
  }
2089
  return MS_SUCCESS;
2090
}
5,393✔
2091

2092
static std::pair<const char *, const char *>
2093
convertGmlTypeToJsonSchema(const char *gmlType) {
59✔
2094
  const char *type = "string";
2095
  const char *format = nullptr;
2096
  if (gmlType) {
59✔
2097
    if (strcasecmp(gmlType, "Character") == 0)
31✔
2098
      type = "string";
2099
    else if (strcasecmp(gmlType, "Date") == 0) {
31✔
2100
      type = "string";
2101
      format = "date";
2102
    } else if (strcasecmp(gmlType, "Time") == 0) {
31✔
2103
      type = "string";
2104
      format = "time";
2105
    } else if (strcasecmp(gmlType, "DateTime") == 0) {
31✔
2106
      type = "string";
2107
      format = "date-time";
2108
    } else if (strcasecmp(gmlType, "Integer") == 0 ||
21✔
2109
               strcasecmp(gmlType, "Long") == 0)
13✔
2110
      type = "integer";
2111
    else if (strcasecmp(gmlType, "Real") == 0)
13✔
2112
      type = "number";
UNCOV
2113
    else if (strcasecmp(gmlType, "Boolean") == 0)
×
2114
      type = "boolean";
2115
  }
2116
  return {type, format};
59✔
2117
}
2118

2119
std::pair<const char *, const char *>
2120
getItemTypeAndFormat(const layerObj *layer, const std::string &item) {
59✔
2121
  const char *pszType =
2122
      msOWSLookupMetadata(&(layer->metadata), "OFG", (item + "_type").c_str());
59✔
2123
  return convertGmlTypeToJsonSchema(pszType);
59✔
2124
}
2125

2126
static int processCollectionQueryablesRequest(mapObj *map,
7✔
2127
                                              const cgiRequestObj *request,
2128
                                              const char *collectionId,
2129
                                              OGCAPIFormat format) {
2130

2131
  // find the right layer
2132
  const int iLayer = findLayerIndex(map, collectionId);
7✔
2133

2134
  if (iLayer < 0) { // invalid collectionId
7✔
2135
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
1✔
2136
    return MS_SUCCESS;
1✔
2137
  }
2138

2139
  layerObj *layer = map->layers[iLayer];
6✔
2140
  if (!includeLayer(map, layer)) {
6✔
UNCOV
2141
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
2142
    return MS_SUCCESS;
×
2143
  }
2144

2145
  auto allowedParameters = getExtraParameters(map, layer);
6✔
2146
  allowedParameters.insert("f");
6✔
2147

2148
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
6✔
2149
    return MS_SUCCESS;
2150
  }
2151

2152
  bool error = false;
2153
  const std::vector<std::string> queryableItems =
2154
      msOOGCAPIGetLayerQueryables(layer, allowedParameters, error);
4✔
2155
  if (error) {
4✔
2156
    return MS_SUCCESS;
2157
  }
2158

2159
  std::unique_ptr<char, decltype(&msFree)> id_encoded(msEncodeUrl(collectionId),
2160
                                                      msFree);
4✔
2161

2162
  json response = {
2163
      {"$schema", "https://json-schema.org/draft/2020-12/schema"},
2164
      {"$id", msOGCAPIGetApiRootUrl(map, request) + "/collections/" +
12✔
2165
                  std::string(id_encoded.get()) + "/queryables"},
4✔
2166
      {"type", "object"},
2167
      {"title", getCollectionTitle(layer)},
4✔
2168
      {"description", getCollectionDescription(layer)},
4✔
2169
      {"properties", json::object()},
4✔
2170
      {"additionalProperties", false},
2171
  };
88✔
2172

2173
  const char *geometryName = getGeometryName(layer);
4✔
2174
  if (geometryName[0]) {
4✔
2175
    const char *geometryFormat = getGeometryFormat(layer);
4✔
2176

2177
    json j = {
2178
        {"title", geometryName},
2179
        {"description", "The geometry of the collection."},
2180
        {"x-ogc-role", "primary-geometry"},
2181
        {"format", geometryFormat},
2182
    };
52✔
2183
    response["properties"][geometryName] = j;
8✔
2184
  }
2185

2186
  const char *featureIdItem =
2187
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
4✔
2188
  for (const std::string &item : queryableItems) {
21✔
2189
    json j;
2190
    const auto name = getItemAliasOrName(layer, item);
17✔
2191
    const auto [type, format] = getItemTypeAndFormat(layer, item);
17✔
2192
    j["description"] = "Queryable item '" + name + "'";
51✔
2193
    j["type"] = type;
17✔
2194
    if (format)
17✔
2195
      j["format"] = format;
6✔
2196
    if (featureIdItem && featureIdItem == item)
17✔
UNCOV
2197
      j["x-ogc-role"] = "id";
×
2198
    response["properties"][name] = j;
34✔
2199
  }
2200

2201
  std::map<std::string, std::vector<std::string>> extraHeaders;
2202
  outputResponse(
7✔
2203
      map, request,
2204
      format == OGCAPIFormat::JSON ? OGCAPIFormat::JSONSchema : format,
2205
      OGCAPI_TEMPLATE_HTML_COLLECTION_QUERYABLES, response, extraHeaders);
2206

2207
  return MS_SUCCESS;
2208
}
152✔
2209

2210
namespace {
2211
struct Returnable {
2212
  std::string item;
2213
  std::string user_name;
2214
  std::string type;
2215
  std::string format;
2216
};
2217
} // namespace
2218

2219
/** Return the list of returnable items as (alias, type, format) tuples */
2220
static std::vector<Returnable> msOOGCAPIGetLayerReturnables(layerObj *layer,
2✔
2221
                                                            bool &error) {
2222
  error = false;
2✔
2223
  std::vector<Returnable> returnableItems;
2224

2225
  // we piggyback on GML configuration
2226
  gmlItemListObj *items = msGMLGetItems(layer, "AG");
2✔
2227
  gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
2✔
2228

2229
  if (!items || !constants) {
2✔
UNCOV
2230
    msGMLFreeItems(items);
×
UNCOV
2231
    msGMLFreeConstants(constants);
×
UNCOV
2232
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
2233
                        "Error fetching layer attribute metadata.");
UNCOV
2234
    error = true;
×
UNCOV
2235
    return returnableItems;
×
2236
  }
2237

2238
  for (int i = 0; i < items->numitems; ++i) {
20✔
2239
    Returnable returnable;
2240
    returnable.item = items->items[i].name;
18✔
2241
    returnable.user_name =
2242
        items->items[i].alias ? items->items[i].alias : items->items[i].name;
18✔
2243
    const auto [type, format] =
2244
        getItemTypeAndFormat(layer, items->items[i].name);
18✔
2245
    returnable.type = type;
18✔
2246
    if (format)
18✔
2247
      returnable.format = format;
2248
    returnableItems.push_back(std::move(returnable));
2249
  }
18✔
2250

2251
  for (int i = 0; i < constants->numconstants; ++i) {
2✔
2252
    Returnable returnable;
UNCOV
2253
    returnable.user_name = constants->constants[i].name;
×
2254
    const auto [type, format] =
2255
        convertGmlTypeToJsonSchema(constants->constants[i].type);
×
UNCOV
2256
    returnable.type = type;
×
UNCOV
2257
    if (format)
×
2258
      returnable.format = format;
2259
    returnableItems.push_back(std::move(returnable));
UNCOV
2260
  }
×
2261

2262
  msGMLFreeItems(items);
2✔
2263
  msGMLFreeConstants(constants);
2✔
2264
  return returnableItems;
UNCOV
2265
}
×
2266

2267
static int processCollectionSchemaRequest(mapObj *map,
4✔
2268
                                          const cgiRequestObj *request,
2269
                                          const char *collectionId,
2270
                                          OGCAPIFormat format) {
2271

2272
  // find the right layer
2273
  const int iLayer = findLayerIndex(map, collectionId);
4✔
2274

2275
  if (iLayer < 0) { // invalid collectionId
4✔
2276
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
1✔
2277
    return MS_SUCCESS;
1✔
2278
  }
2279

2280
  layerObj *layer = map->layers[iLayer];
3✔
2281
  if (!includeLayer(map, layer)) {
3✔
UNCOV
2282
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
2283
    return MS_SUCCESS;
×
2284
  }
2285

2286
  if (msLayerOpen(layer) != MS_SUCCESS ||
6✔
2287
      msLayerGetItems(layer) != MS_SUCCESS) {
3✔
UNCOV
2288
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot get layer fields");
×
UNCOV
2289
    return MS_SUCCESS;
×
2290
  }
2291

2292
  auto allowedParameters = getExtraParameters(map, layer);
3✔
2293
  allowedParameters.insert("f");
3✔
2294

2295
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
3✔
2296
    return MS_SUCCESS;
2297
  }
2298

2299
  bool error = false;
2300
  const auto returnableItems = msOOGCAPIGetLayerReturnables(layer, error);
2✔
2301
  if (error) {
2✔
2302
    return MS_SUCCESS;
2303
  }
2304

2305
  std::unique_ptr<char, decltype(&msFree)> id_encoded(msEncodeUrl(collectionId),
2306
                                                      msFree);
2✔
2307

2308
  json response = {
2309
      {"$schema", "https://json-schema.org/draft/2020-12/schema"},
2310
      {"$id", msOGCAPIGetApiRootUrl(map, request) + "/collections/" +
6✔
2311
                  std::string(id_encoded.get()) + "/schema"},
2✔
2312
      {"type", "object"},
2313
      {"title", getCollectionTitle(layer)},
2✔
2314
      {"description", getCollectionDescription(layer)},
2✔
2315
      {"properties", json::object()},
2✔
2316
      {"additionalProperties", false},
2317
  };
44✔
2318

2319
  const char *geometryName = getGeometryName(layer);
2✔
2320
  if (geometryName[0]) {
2✔
2321
    const char *geometryFormat = getGeometryFormat(layer);
2✔
2322

2323
    json j = {
2324
        {"title", geometryName},
2325
        {"description", "The geometry of the collection."},
2326
        {"x-ogc-role", "primary-geometry"},
2327
        {"format", geometryFormat},
2328
    };
26✔
2329
    response["properties"][geometryName] = j;
4✔
2330
  }
2331

2332
  const char *featureIdItem =
2333
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
2✔
2334
  for (const auto &item : returnableItems) {
20✔
2335
    json j;
2336
    j["description"] = "Returnable item '" + item.user_name + "'";
36✔
2337
    j["type"] = item.type;
54✔
2338
    if (!item.format.empty())
18✔
2339
      j["format"] = item.format;
6✔
2340
    if (featureIdItem && featureIdItem == item.item)
18✔
UNCOV
2341
      j["x-ogc-role"] = "id";
×
2342
    response["properties"][item.user_name] = j;
36✔
2343
  }
2344

2345
  std::map<std::string, std::vector<std::string>> extraHeaders;
2346
  outputResponse(
3✔
2347
      map, request,
2348
      format == OGCAPIFormat::JSON ? OGCAPIFormat::JSONSchema : format,
2349
      OGCAPI_TEMPLATE_HTML_COLLECTION_SCHEMA, response, extraHeaders);
2350

2351
  return MS_SUCCESS;
2352
}
76✔
2353

2354
static int processCollectionSortablesRequest(mapObj *map,
6✔
2355
                                             const cgiRequestObj *request,
2356
                                             const char *collectionId,
2357
                                             OGCAPIFormat format) {
2358

2359
  // find the right layer
2360
  const int iLayer = findLayerIndex(map, collectionId);
6✔
2361

2362
  if (iLayer < 0) { // invalid collectionId
6✔
2363
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
1✔
2364
    return MS_SUCCESS;
1✔
2365
  }
2366

2367
  layerObj *layer = map->layers[iLayer];
5✔
2368
  if (!includeLayer(map, layer)) {
5✔
UNCOV
2369
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
2370
    return MS_SUCCESS;
×
2371
  }
2372

2373
  if (msLayerOpen(layer) != MS_SUCCESS ||
10✔
2374
      msLayerGetItems(layer) != MS_SUCCESS) {
5✔
UNCOV
2375
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot get layer fields");
×
UNCOV
2376
    return MS_SUCCESS;
×
2377
  }
2378

2379
  auto allowedParameters = getExtraParameters(map, layer);
5✔
2380
  allowedParameters.insert("f");
5✔
2381

2382
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
5✔
2383
    return MS_SUCCESS;
2384
  }
2385

2386
  bool error = false;
2387
  const auto sortableItems =
2388
      msOOGCAPIGetLayerSortables(layer, allowedParameters, error);
4✔
2389
  if (error) {
4✔
2390
    return MS_SUCCESS;
2391
  }
2392

2393
  std::unique_ptr<char, decltype(&msFree)> id_encoded(msEncodeUrl(collectionId),
2394
                                                      msFree);
4✔
2395

2396
  json response = {
2397
      {"$schema", "https://json-schema.org/draft/2020-12/schema"},
2398
      {"$id", msOGCAPIGetApiRootUrl(map, request) + "/collections/" +
12✔
2399
                  std::string(id_encoded.get()) + "/sortables"},
4✔
2400
      {"type", "object"},
2401
      {"title", getCollectionTitle(layer)},
4✔
2402
      {"description", getCollectionDescription(layer)},
4✔
2403
      {"properties", json::object()},
4✔
2404
      {"additionalProperties", false},
2405
  };
88✔
2406

2407
  const char *featureIdItem =
2408
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
4✔
2409
  for (const std::string &item : sortableItems) {
15✔
2410
    json j;
2411
    const auto name = getItemAliasOrName(layer, item);
11✔
2412
    j["description"] = "Sortable item '" + name + "'";
22✔
2413
    const auto [type, format] = getItemTypeAndFormat(layer, item);
11✔
2414
    j["type"] = type;
11✔
2415
    if (format)
11✔
2416
      j["format"] = format;
6✔
2417
    if (featureIdItem && featureIdItem == item)
11✔
UNCOV
2418
      j["x-ogc-role"] = "id";
×
2419
    response["properties"][name] = j;
22✔
2420
  }
2421

2422
  std::map<std::string, std::vector<std::string>> extraHeaders;
2423
  outputResponse(
7✔
2424
      map, request,
2425
      format == OGCAPIFormat::JSON ? OGCAPIFormat::JSONSchema : format,
2426
      OGCAPI_TEMPLATE_HTML_COLLECTION_SORTABLES, response, extraHeaders);
2427

2428
  return MS_SUCCESS;
2429
}
100✔
2430

2431
static int processCollectionRequest(mapObj *map, cgiRequestObj *request,
7✔
2432
                                    const char *collectionId,
2433
                                    OGCAPIFormat format) {
2434
  json response;
2435
  int l;
2436

2437
  for (l = 0; l < map->numlayers; l++) {
9✔
2438
    if (strcmp(map->layers[l]->name, collectionId) == 0)
9✔
2439
      break; // match
2440
  }
2441

2442
  if (l == map->numlayers) { // invalid collectionId
7✔
UNCOV
2443
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
2444
    return MS_SUCCESS;
×
2445
  }
2446

2447
  layerObj *layer = map->layers[l];
7✔
2448
  auto allowedParameters = getExtraParameters(map, layer);
7✔
2449
  allowedParameters.insert("f");
7✔
2450
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
7✔
2451
    return MS_SUCCESS;
2452
  }
2453

2454
  try {
2455
    response =
2456
        getCollection(map, layer, format, msOGCAPIGetApiRootUrl(map, request));
12✔
2457
    if (response.is_null()) { // same as not found
6✔
UNCOV
2458
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
UNCOV
2459
      return MS_SUCCESS;
×
2460
    }
UNCOV
2461
  } catch (const std::runtime_error &e) {
×
UNCOV
2462
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
UNCOV
2463
                        "Error getting collection. " + std::string(e.what()));
×
2464
    return MS_SUCCESS;
UNCOV
2465
  }
×
2466

2467
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTION,
6✔
2468
                 response);
2469
  return MS_SUCCESS;
6✔
2470
}
2471

2472
static int processCollectionsRequest(mapObj *map, cgiRequestObj *request,
6✔
2473
                                     OGCAPIFormat format) {
2474
  json response;
2475
  int i;
2476

2477
  auto allowedParameters = getExtraParameters(map, nullptr);
6✔
2478
  allowedParameters.insert("f");
6✔
2479
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
6✔
2480
    return MS_SUCCESS;
2481
  }
2482

2483
  // define api root url
2484
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
5✔
2485
  const std::string extra_params = getExtraParameterString(map, nullptr);
5✔
2486

2487
  // build response object
2488
  response = {{"links",
2489
               {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
6✔
2490
                 {"type", OGCAPI_MIMETYPE_JSON},
2491
                 {"title", "This document as JSON"},
2492
                 {"href", api_root + "/collections?f=json" + extra_params}},
5✔
2493
                {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
9✔
2494
                 {"type", OGCAPI_MIMETYPE_HTML},
2495
                 {"title", "This document as HTML"},
2496
                 {"href", api_root + "/collections?f=html" + extra_params}}}},
5✔
2497
              {"collections", json::array()}};
170✔
2498

2499
  for (i = 0; i < map->numlayers; i++) {
19✔
2500
    try {
2501
      json collection = getCollection(map, map->layers[i], format, api_root);
14✔
2502
      if (!collection.is_null())
14✔
2503
        response["collections"].push_back(collection);
14✔
UNCOV
2504
    } catch (const std::runtime_error &e) {
×
UNCOV
2505
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
UNCOV
2506
                          "Error getting collection." + std::string(e.what()));
×
2507
      return MS_SUCCESS;
UNCOV
2508
    }
×
2509
  }
2510

2511
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTIONS,
5✔
2512
                 response);
2513
  return MS_SUCCESS;
5✔
2514
}
215✔
2515

2516
static int processApiRequest(mapObj *map, cgiRequestObj *request,
2✔
2517
                             OGCAPIFormat format) {
2518
  // Strongly inspired from
2519
  // https://github.com/geopython/pygeoapi/blob/master/pygeoapi/openapi.py
2520

2521
  auto allowedParameters = getExtraParameters(map, nullptr);
2✔
2522
  allowedParameters.insert("f");
2✔
2523
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
2✔
2524
    return MS_SUCCESS;
2525
  }
2526

2527
  json response;
2528

2529
  response = {
2530
      {"openapi", "3.0.2"},
2531
      {"tags", json::array()},
1✔
2532
  };
7✔
2533

2534
  response["info"] = {
1✔
2535
      {"title", getTitle(map)},
1✔
2536
      {"version", getWebMetadata(map, "A", "version", "1.0.0")},
1✔
2537
  };
7✔
2538

2539
  for (const char *item : {"description", "termsOfService"}) {
3✔
2540
    const char *value = getWebMetadata(map, "AO", item, nullptr);
2✔
2541
    if (value) {
2✔
2542
      response["info"][item] = value;
4✔
2543
    }
2544
  }
2545

2546
  for (const auto &pair : {
3✔
2547
           std::make_pair("name", "contactperson"),
2548
           std::make_pair("url", "contacturl"),
2549
           std::make_pair("email", "contactelectronicmailaddress"),
2550
       }) {
4✔
2551
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
3✔
2552
    if (value) {
3✔
2553
      response["info"]["contact"][pair.first] = value;
6✔
2554
    }
2555
  }
2556

2557
  for (const auto &pair : {
2✔
2558
           std::make_pair("name", "licensename"),
2559
           std::make_pair("url", "licenseurl"),
2560
       }) {
3✔
2561
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
2✔
2562
    if (value) {
2✔
UNCOV
2563
      response["info"]["license"][pair.first] = value;
×
2564
    }
2565
  }
2566

2567
  {
2568
    const char *value = getWebMetadata(map, "AO", "keywords", nullptr);
1✔
2569
    if (value) {
1✔
2570
      response["info"]["x-keywords"] = value;
2✔
2571
    }
2572
  }
2573

2574
  json server;
2575
  server["url"] = msOGCAPIGetApiRootUrl(map, request);
3✔
2576

2577
  {
2578
    const char *value =
2579
        getWebMetadata(map, "AO", "server_description", nullptr);
1✔
2580
    if (value) {
1✔
2581
      server["description"] = value;
2✔
2582
    }
2583
  }
2584
  response["servers"].push_back(server);
1✔
2585

2586
  const std::string oapif_schema_base_url = msOWSGetSchemasLocation(map);
1✔
2587
  const std::string oapif_yaml_url = oapif_schema_base_url +
2588
                                     "/ogcapi/features/part1/1.0/openapi/"
2589
                                     "ogcapi-features-1.yaml";
1✔
2590
  const std::string oapif_part2_yaml_url = oapif_schema_base_url +
2591
                                           "/ogcapi/features/part2/1.0/openapi/"
2592
                                           "ogcapi-features-2.yaml";
1✔
2593
  const std::string oapif_part3_yaml_url = oapif_schema_base_url +
2594
                                           "/ogcapi/features/part3/1.0/openapi/"
2595
                                           "ogcapi-features-3.yaml";
1✔
2596

2597
  json paths;
2598

2599
  paths["/"]["get"] = {
1✔
2600
      {"summary", "Landing page"},
2601
      {"description", "Landing page"},
2602
      {"tags", {"server"}},
2603
      {"operationId", "getLandingPage"},
2604
      {"parameters",
2605
       {
2606
           {{"$ref", "#/components/parameters/f"}},
2607
       }},
2608
      {"responses",
2609
       {{"200",
2610
         {{"$ref", oapif_yaml_url + "#/components/responses/LandingPage"}}},
1✔
2611
        {"400",
2612
         {{"$ref",
2613
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2614
        {"500",
2615
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2616

2617
  paths["/api"]["get"] = {
1✔
2618
      {"summary", "API documentation"},
2619
      {"description", "API documentation"},
2620
      {"tags", {"server"}},
2621
      {"operationId", "getOpenapi"},
2622
      {"parameters",
2623
       {
2624
           {{"$ref", "#/components/parameters/f"}},
2625
       }},
2626
      {"responses",
2627
       {{"200", {{"$ref", "#/components/responses/200"}}},
2628
        {"400",
2629
         {{"$ref",
2630
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2631
        {"default", {{"$ref", "#/components/responses/default"}}}}}};
42✔
2632

2633
  paths["/conformance"]["get"] = {
1✔
2634
      {"summary", "API conformance definition"},
2635
      {"description", "API conformance definition"},
2636
      {"tags", {"server"}},
2637
      {"operationId", "getConformanceDeclaration"},
2638
      {"parameters",
2639
       {
2640
           {{"$ref", "#/components/parameters/f"}},
2641
       }},
2642
      {"responses",
2643
       {{"200",
2644
         {{"$ref",
2645
           oapif_yaml_url + "#/components/responses/ConformanceDeclaration"}}},
1✔
2646
        {"400",
2647
         {{"$ref",
2648
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2649
        {"500",
2650
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2651

2652
  paths["/collections"]["get"] = {
1✔
2653
      {"summary", "Collections"},
2654
      {"description", "Collections"},
2655
      {"tags", {"server"}},
2656
      {"operationId", "getCollections"},
2657
      {"parameters",
2658
       {
2659
           {{"$ref", "#/components/parameters/f"}},
2660
       }},
2661
      {"responses",
2662
       {{"200",
2663
         {{"$ref", oapif_yaml_url + "#/components/responses/Collections"}}},
1✔
2664
        {"400",
2665
         {{"$ref",
2666
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2667
        {"500",
2668
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2669

2670
  for (int i = 0; i < map->numlayers; i++) {
6✔
2671
    layerObj *layer = map->layers[i];
5✔
2672
    if (!includeLayer(map, layer)) {
5✔
UNCOV
2673
      continue;
×
2674
    }
2675

2676
    json collection_get = {
2677
        {"summary",
2678
         std::string("Get ") + getCollectionTitle(layer) + " metadata"},
5✔
2679
        {"description", getCollectionDescription(layer)},
5✔
2680
        {"tags", {layer->name}},
5✔
2681
        {"operationId", "describe" + std::string(layer->name) + "Collection"},
5✔
2682
        {"parameters",
2683
         {
2684
             {{"$ref", "#/components/parameters/f"}},
2685
         }},
2686
        {"responses",
2687
         {{"200",
2688
           {{"$ref", oapif_yaml_url + "#/components/responses/Collection"}}},
5✔
2689
          {"400",
2690
           {{"$ref",
2691
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2692
          {"500",
2693
           {{"$ref",
2694
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
215✔
2695

UNCOV
2696
    std::string collectionNamePath("/collections/");
×
2697
    collectionNamePath += layer->name;
5✔
2698
    paths[collectionNamePath]["get"] = std::move(collection_get);
5✔
2699

2700
    // check metadata, layer then map
2701
    const char *max_limit_str =
2702
        msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
5✔
2703
    if (max_limit_str == nullptr)
5✔
2704
      max_limit_str =
2705
          msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
5✔
2706
    const int max_limit =
2707
        max_limit_str ? atoi(max_limit_str) : OGCAPI_MAX_LIMIT;
5✔
2708
    const int default_limit = getDefaultLimit(map, layer);
5✔
2709

2710
    json items_get = {
2711
        {"summary", std::string("Get ") + getCollectionTitle(layer) + " items"},
5✔
2712
        {"description", getCollectionDescription(layer)},
5✔
2713
        {"tags", {layer->name}},
2714
        {"operationId", "get" + std::string(layer->name) + "Features"},
5✔
2715
        {"parameters",
2716
         {
2717
             {{"$ref", "#/components/parameters/f"}},
2718
             {{"$ref", oapif_yaml_url + "#/components/parameters/bbox"}},
5✔
2719
             {{"$ref", oapif_yaml_url + "#/components/parameters/datetime"}},
5✔
2720
             {{"$ref",
2721
               oapif_part2_yaml_url + "#/components/parameters/bbox-crs"}},
5✔
2722
             {{"$ref", oapif_part2_yaml_url + "#/components/parameters/crs"}},
5✔
2723
             {{"$ref", "#/components/parameters/offset"}},
2724
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2725
             {{"$ref",
2726
               oapif_part3_yaml_url + "#/components/parameters/filter"}},
5✔
2727
             {{"$ref",
2728
               oapif_part3_yaml_url + "#/components/parameters/filter-lang"}},
5✔
2729
             {{"$ref",
2730
               oapif_part3_yaml_url + "#/components/parameters/filter-crs"}},
5✔
2731
         }},
2732
        {"responses",
2733
         {{"200",
2734
           {{"$ref", oapif_yaml_url + "#/components/responses/Features"}}},
5✔
2735
          {"400",
2736
           {{"$ref",
2737
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2738
          {"500",
2739
           {{"$ref",
2740
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
395✔
2741

2742
    json param_limit = {
2743
        {"name", "limit"},
2744
        {"in", "query"},
2745
        {"description", "The optional limit parameter limits the number of "
2746
                        "items that are presented in the response document."},
2747
        {"required", false},
2748
        {"schema",
2749
         {
2750
             {"type", "integer"},
2751
             {"minimum", 1},
2752
             {"maximum", max_limit},
2753
             {"default", default_limit},
2754
         }},
2755
        {"style", "form"},
2756
        {"explode", false},
2757
    };
170✔
2758
    items_get["parameters"].emplace_back(param_limit);
5✔
2759

2760
    json param_sortby = {
2761
        {"name", "sortby"},
2762
        {"in", "query"},
2763
        {"description",
2764
         "Optional list of properties by which to sort the items."},
2765
        {"required", false},
2766
        {"schema",
2767
         {
2768
             {"type", "array"},
2769
             {"minItems", 1},
2770
             {"items",
2771
              {
2772
                  {"type", "string"},
2773
                  {"pattern", "[+|-]?[A-Za-z_].*"},
2774
              }},
2775
         }},
2776
        {"style", "form"},
2777
        {"explode", false},
2778
    };
185✔
2779
    items_get["parameters"].emplace_back(param_sortby);
5✔
2780

2781
    bool error = false;
5✔
2782
    auto reservedParams = getExtraParameters(map, layer);
5✔
2783
    reservedParams.insert("f");
5✔
2784
    reservedParams.insert("bbox");
5✔
2785
    reservedParams.insert("bbox-crs");
5✔
2786
    reservedParams.insert("datetime");
5✔
2787
    reservedParams.insert("limit");
5✔
2788
    reservedParams.insert("offset");
5✔
2789
    reservedParams.insert("crs");
5✔
2790
    reservedParams.insert("filter");
5✔
2791
    reservedParams.insert("filter-lang");
5✔
2792
    reservedParams.insert("filter-crs");
5✔
2793
    reservedParams.insert("sortby");
5✔
2794
    const std::vector<std::string> queryableItems =
2795
        msOOGCAPIGetLayerQueryables(layer, reservedParams, error);
5✔
2796
    for (const auto &item : queryableItems) {
18✔
2797
      const auto name = getItemAliasOrName(layer, item);
13✔
2798
      const auto [type, format] = getItemTypeAndFormat(layer, item);
13✔
2799
      json queryable_param = {
2800
          {"name", name},
2801
          {"in", "query"},
2802
          {"description", "Queryable item '" + name + "'"},
13✔
2803
          {"required", false},
2804
          {"schema",
2805
           {
2806
               {"type", type},
2807
           }},
2808
          {"style", "form"},
2809
          {"explode", false},
2810
      };
325✔
2811
      if (format) {
13✔
2812
        queryable_param["schema"]["format"] = format;
4✔
2813
      }
2814
      items_get["parameters"].emplace_back(queryable_param);
13✔
2815
    }
2816

2817
    std::string itemsPath(collectionNamePath + "/items");
5✔
2818
    paths[itemsPath]["get"] = std::move(items_get);
10✔
2819

2820
    json schema_get = {
2821
        {"summary",
2822
         std::string("Get ") + getCollectionTitle(layer) + " schema"},
5✔
2823
        {"description",
2824
         std::string("Get ") + getCollectionTitle(layer) + " schema"},
5✔
2825
        {"tags", {layer->name}},
2826
        {"operationId", "get" + std::string(layer->name) + "Schema"},
5✔
2827
        {"parameters",
2828
         {
2829
             {{"$ref", "#/components/parameters/f"}},
2830
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2831
         }},
2832
        {"responses",
2833
         {{"200",
2834
           {{"description", "The returnable properties of the collection."},
2835
            {"content",
2836
             {{"application/schema+json",
2837
               {{"schema", {{"type", "object"}}}}}}}}},
2838
          {"400",
2839
           {{"$ref",
2840
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2841
          {"500",
2842
           {{"$ref",
2843
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
295✔
2844

2845
    std::string schemaPath(collectionNamePath + "/schema");
5✔
2846
    paths[schemaPath]["get"] = std::move(schema_get);
10✔
2847

2848
    json queryables_get = {
2849
        {"summary",
2850
         std::string("Get ") + getCollectionTitle(layer) + " queryables"},
5✔
2851
        {"description",
2852
         std::string("Get ") + getCollectionTitle(layer) + " queryables"},
5✔
2853
        {"tags", {layer->name}},
2854
        {"operationId", "get" + std::string(layer->name) + "Queryables"},
5✔
2855
        {"parameters",
2856
         {
2857
             {{"$ref", "#/components/parameters/f"}},
2858
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2859
         }},
2860
        {"responses",
2861
         {{"200",
2862
           {{"description", "The queryable properties of the collection."},
2863
            {"content",
2864
             {{"application/schema+json",
2865
               {{"schema", {{"type", "object"}}}}}}}}},
2866
          {"400",
2867
           {{"$ref",
2868
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2869
          {"500",
2870
           {{"$ref",
2871
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
295✔
2872

2873
    std::string queryablesPath(collectionNamePath + "/queryables");
5✔
2874
    paths[queryablesPath]["get"] = std::move(queryables_get);
10✔
2875

2876
    json sortables_get = {
2877
        {"summary",
2878
         std::string("Get ") + getCollectionTitle(layer) + " sortables"},
5✔
2879
        {"description",
2880
         std::string("Get ") + getCollectionTitle(layer) + " sortables"},
5✔
2881
        {"tags", {layer->name}},
2882
        {"operationId", "get" + std::string(layer->name) + "Sortables"},
5✔
2883
        {"parameters",
2884
         {
2885
             {{"$ref", "#/components/parameters/f"}},
2886
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2887
         }},
2888
        {"responses",
2889
         {{"200",
2890
           {{"description", "The sortable properties of the collection."},
2891
            {"content",
2892
             {{"application/schema+json",
2893
               {{"schema", {{"type", "object"}}}}}}}}},
2894
          {"400",
2895
           {{"$ref",
2896
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2897
          {"500",
2898
           {{"$ref",
2899
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
295✔
2900

2901
    std::string sortablesPath(collectionNamePath + "/sortables");
5✔
2902
    paths[sortablesPath]["get"] = std::move(sortables_get);
10✔
2903

2904
    json feature_id_get = {
2905
        {"summary",
2906
         std::string("Get ") + getCollectionTitle(layer) + " item by id"},
5✔
2907
        {"description", getCollectionDescription(layer)},
5✔
2908
        {"tags", {layer->name}},
2909
        {"operationId", "get" + std::string(layer->name) + "Feature"},
5✔
2910
        {"parameters",
2911
         {
2912
             {{"$ref", "#/components/parameters/f"}},
2913
             {{"$ref", oapif_yaml_url + "#/components/parameters/featureId"}},
5✔
2914
         }},
2915
        {"responses",
2916
         {{"200",
2917
           {{"$ref", oapif_yaml_url + "#/components/responses/Feature"}}},
5✔
2918
          {"400",
2919
           {{"$ref",
2920
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2921
          {"404",
2922
           {{"$ref", oapif_yaml_url + "#/components/responses/NotFound"}}},
5✔
2923
          {"500",
2924
           {{"$ref",
2925
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
265✔
2926
    std::string itemsFeatureIdPath(collectionNamePath + "/items/{featureId}");
5✔
2927
    paths[itemsFeatureIdPath]["get"] = std::move(feature_id_get);
10✔
2928
  }
5✔
2929

2930
  response["paths"] = std::move(paths);
2✔
2931

2932
  json components;
2933
  components["responses"]["200"] = {{"description", "successful operation"}};
5✔
2934
  components["responses"]["default"] = {
1✔
2935
      {"description", "unexpected error"},
2936
      {"content",
2937
       {{"application/json",
2938
         {{"schema",
2939
           {{"$ref", "https://schemas.opengis.net/ogcapi/common/part1/1.0/"
2940
                     "openapi/schemas/exception.yaml"}}}}}}}};
16✔
2941

2942
  json parameters;
2943
  parameters["f"] = {
1✔
2944
      {"name", "f"},
2945
      {"in", "query"},
2946
      {"description", "The optional f parameter indicates the output format "
2947
                      "which the server shall provide as part of the response "
2948
                      "document.  The default format is GeoJSON."},
2949
      {"required", false},
2950
      {"schema",
2951
       {{"type", "string"}, {"enum", {"json", "html"}}, {"default", "json"}}},
2952
      {"style", "form"},
2953
      {"explode", false},
2954
  };
33✔
2955

2956
  parameters["offset"] = {
1✔
2957
      {"name", "offset"},
2958
      {"in", "query"},
2959
      {"description",
2960
       "The optional offset parameter indicates the index within the result "
2961
       "set from which the server shall begin presenting results in the "
2962
       "response document.  The first element has an index of 0 (default)."},
2963
      {"required", false},
2964
      {"schema",
2965
       {
2966
           {"type", "integer"},
2967
           {"minimum", 0},
2968
           {"default", 0},
2969
       }},
2970
      {"style", "form"},
2971
      {"explode", false},
2972
  };
31✔
2973

2974
  parameters["vendorSpecificParameters"] = {
1✔
2975
      {"name", "vendorSpecificParameters"},
2976
      {"in", "query"},
2977
      {"description",
2978
       "Additional \"free-form\" parameters that are not explicitly defined"},
2979
      {"schema",
2980
       {
2981
           {"type", "object"},
2982
           {"additionalProperties", true},
2983
       }},
2984
      {"style", "form"},
2985
  };
22✔
2986

2987
  components["parameters"] = std::move(parameters);
2✔
2988

2989
  response["components"] = std::move(components);
2✔
2990

2991
  // TODO: "tags" array ?
2992

2993
  outputResponse(map, request,
3✔
2994
                 format == OGCAPIFormat::JSON ? OGCAPIFormat::OpenAPI_V3
2995
                                              : format,
2996
                 OGCAPI_TEMPLATE_HTML_OPENAPI, response);
2997
  return MS_SUCCESS;
2998
}
3,105✔
2999

3000
#endif
3001

3002
OGCAPIFormat msOGCAPIGetOutputFormat(const cgiRequestObj *request) {
174✔
3003
  OGCAPIFormat format; // all endpoints need a format
3004
  const char *p = getRequestParameter(request, "f");
174✔
3005

3006
  // if f= query parameter is not specified, use HTTP Accept header if available
3007
  if (p == nullptr) {
174✔
3008
    const char *accept = getenv("HTTP_ACCEPT");
3✔
3009
    if (accept) {
3✔
3010
      if (strcmp(accept, "*/*") == 0)
1✔
3011
        p = OGCAPI_MIMETYPE_JSON;
3012
      else
3013
        p = accept;
3014
    }
3015
  }
3016

3017
  if (p &&
172✔
3018
      (strcmp(p, "json") == 0 || strstr(p, OGCAPI_MIMETYPE_JSON) != nullptr ||
172✔
3019
       strstr(p, OGCAPI_MIMETYPE_GEOJSON) != nullptr ||
15✔
3020
       strstr(p, OGCAPI_MIMETYPE_OPENAPI_V3) != nullptr)) {
3021
    format = OGCAPIFormat::JSON;
3022
  } else if (p && (strcmp(p, "html") == 0 ||
15✔
3023
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
3024
    format = OGCAPIFormat::HTML;
3025
  } else if (p) {
3026
    format = OGCAPIFormat::Invalid;
3027
  } else {
3028
    format = OGCAPIFormat::HTML; // default for now
3029
  }
3030

3031
  return format;
174✔
3032
}
3033

3034
int msOGCAPIDispatchRequest(mapObj *map, cgiRequestObj *request) {
157✔
3035
#ifdef USE_OGCAPI_SVR
3036

3037
  // make sure ogcapi requests are enabled for this map
3038
  int status = msOWSRequestIsEnabled(map, NULL, "AO", "OGCAPI", MS_FALSE);
157✔
3039
  if (status != MS_TRUE) {
157✔
UNCOV
3040
    msSetError(MS_OGCAPIERR, "OGC API requests are not enabled.",
×
3041
               "msCGIDispatchAPIRequest()");
UNCOV
3042
    return MS_FAILURE; // let normal error handling take over
×
3043
  }
3044

3045
  for (int i = 0; i < request->NumParams; i++) {
502✔
3046
    for (int j = i + 1; j < request->NumParams; j++) {
624✔
3047
      if (strcmp(request->ParamNames[i], request->ParamNames[j]) == 0) {
279✔
3048
        std::string errorMsg("Query parameter ");
1✔
3049
        errorMsg += request->ParamNames[i];
1✔
3050
        errorMsg += " is repeated";
3051
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
2✔
3052
        return MS_SUCCESS;
3053
      }
3054
    }
3055
  }
3056

3057
  const OGCAPIFormat format = msOGCAPIGetOutputFormat(request);
156✔
3058

3059
  if (format == OGCAPIFormat::Invalid) {
156✔
UNCOV
3060
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
UNCOV
3061
    return MS_SUCCESS; // avoid any downstream MapServer processing
×
3062
  }
3063

3064
  if (request->api_path_length == 2) {
156✔
3065

3066
    return processLandingRequest(map, request, format);
7✔
3067

3068
  } else if (request->api_path_length == 3) {
3069

3070
    if (strcmp(request->api_path[2], "conformance") == 0) {
11✔
3071
      return processConformanceRequest(map, request, format);
3✔
3072
    } else if (strcmp(request->api_path[2], "conformance.html") == 0) {
8✔
UNCOV
3073
      return processConformanceRequest(map, request, OGCAPIFormat::HTML);
×
3074
    } else if (strcmp(request->api_path[2], "collections") == 0) {
8✔
3075
      return processCollectionsRequest(map, request, format);
6✔
3076
    } else if (strcmp(request->api_path[2], "collections.html") == 0) {
2✔
UNCOV
3077
      return processCollectionsRequest(map, request, OGCAPIFormat::HTML);
×
3078
    } else if (strcmp(request->api_path[2], "api") == 0) {
2✔
3079
      return processApiRequest(map, request, format);
2✔
3080
    }
3081

3082
  } else if (request->api_path_length == 4) {
3083

3084
    if (strcmp(request->api_path[2], "collections") ==
7✔
3085
        0) { // next argument (3) is collectionId
3086
      return processCollectionRequest(map, request, request->api_path[3],
7✔
3087
                                      format);
7✔
3088
    }
3089

3090
  } else if (request->api_path_length == 5) {
3091

3092
    if (strcmp(request->api_path[2], "collections") == 0 &&
128✔
3093
        strcmp(request->api_path[4], "items") ==
128✔
3094
            0) { // middle argument (3) is the collectionId
3095
      return processCollectionItemsRequest(map, request, request->api_path[3],
110✔
3096
                                           NULL, format);
110✔
3097
    } else if (strcmp(request->api_path[2], "collections") == 0 &&
18✔
3098
               strcmp(request->api_path[4], "queryables") == 0) {
18✔
3099
      return processCollectionQueryablesRequest(map, request,
7✔
3100
                                                request->api_path[3], format);
7✔
3101
    } else if (strcmp(request->api_path[2], "collections") == 0 &&
11✔
3102
               strcmp(request->api_path[4], "schema") == 0) {
11✔
3103
      return processCollectionSchemaRequest(map, request, request->api_path[3],
4✔
3104
                                            format);
4✔
3105
    } else if (strcmp(request->api_path[2], "collections") == 0 &&
7✔
3106
               strcmp(request->api_path[4], "sortables") == 0) {
7✔
3107
      return processCollectionSortablesRequest(map, request,
6✔
3108
                                               request->api_path[3], format);
6✔
3109
    }
3110
  } else if (request->api_path_length == 6) {
3111

3112
    if (strcmp(request->api_path[2], "collections") == 0 &&
3✔
3113
        strcmp(request->api_path[4], "items") ==
3✔
3114
            0) { // middle argument (3) is the collectionId, last argument (5)
3115
                 // is featureId
3116
      return processCollectionItemsRequest(map, request, request->api_path[3],
3✔
3117
                                           request->api_path[5], format);
3✔
3118
    }
3119
  }
3120

3121
  msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid API path.");
1✔
3122
  return MS_SUCCESS; // avoid any downstream MapServer processing
1✔
3123
#else
3124
  msSetError(MS_OGCAPIERR, "OGC API server support is not enabled.",
3125
             "msOGCAPIDispatchRequest()");
3126
  return MS_FAILURE;
3127
#endif
3128
}
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