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

geographika / mapserver / 22953070987

11 Mar 2026 12:41PM UTC coverage: 42.422% (+0.4%) from 42.009%
22953070987

push

github

geographika
Switch result to ASCII

64590 of 152256 relevant lines covered (42.42%)

27315.35 hits per line

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

82.83
/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) {
148✔
82
  const char *compliance_mode =
83
      msOWSLookupMetadata(&(map->web.metadata), "A", "compliance_mode");
148✔
84
  return compliance_mode == NULL || strcasecmp(compliance_mode, "true") == 0;
148✔
85
}
86

87
/*
88
** Returns a JSON object using and a description.
89
*/
90
void msOGCAPIOutputError(OGCAPIErrorType errorType,
33✔
91
                         const std::string &description) {
92
  const char *code = "";
33✔
93
  const char *status = "";
94
  switch (errorType) {
33✔
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: {
27✔
106
    code = "InvalidParameterValue";
27✔
107
    status = "400";
108
    break;
27✔
109
  }
110
  case OGCAPI_NOT_FOUND_ERROR: {
4✔
111
    code = "NotFound";
4✔
112
    status = "404";
113
    break;
4✔
114
  }
115
  }
116

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

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

125
static int includeLayer(mapObj *map, layerObj *layer) {
147✔
126
  if (!msOWSRequestIsEnabled(map, layer, "AO", "OGCAPI", MS_FALSE) ||
294✔
127
      !msIsLayerSupportedForWFSOrOAPIF(layer) || !msIsLayerQueryable(layer)) {
294✔
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,147✔
143
                                       const char *item) {
144
  for (int i = 0; i < request->NumParams; i++) {
3,367✔
145
    if (strcmp(item, request->ParamNames[i]) == 0)
2,545✔
146
      return request->ParamValues[i];
325✔
147
  }
148

149
  return nullptr;
150
}
151

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

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

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

167
  return max_limit;
112✔
168
}
169

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

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

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

185
  return default_limit;
185✔
186
}
187

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

191
  std::string extra_params;
192

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

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

208
  return extra_params;
133✔
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,
153✔
223
                                                const layerObj *layer) {
224

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

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

240
  return {};
141✔
241
}
242

243
static bool
244
msOOGCAPICheckQueryParameters(const mapObj *map, const cgiRequestObj *request,
148✔
245
                              const std::set<std::string> &allowedParameters) {
246
  if (msOGCAPIComplianceMode(map)) {
148✔
247
    for (int j = 0; j < request->NumParams; j++) {
468✔
248
      const char *paramName = request->ParamNames[j];
332✔
249
      if (allowedParameters.find(paramName) == allowedParameters.end()) {
664✔
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,
485✔
261
                                      const std::string &item) {
262
  std::string key = item;
263
  key += "_alias";
264
  if (const char *value =
485✔
265
          msOWSLookupMetadata(&(layer->metadata), "OGA", key.c_str())) {
485✔
266
    return value;
240✔
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");
72✔
274
  if (!geometryName)
78✔
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
/** Return the list of queryable items */
294
static std::vector<std::string> msOOGCAPIGetLayerQueryables(
119✔
295
    layerObj *layer, const std::set<std::string> &reservedParams, bool &error) {
296
  error = false;
119✔
297
  std::vector<std::string> queryableItems;
298
  if (const char *value =
119✔
299
          msOWSLookupMetadata(&(layer->metadata), "OGA", "queryable_items")) {
119✔
300
    queryableItems = msStringSplit(value, ',');
104✔
301
    if (!queryableItems.empty()) {
104✔
302
      if (msLayerOpen(layer) != MS_SUCCESS ||
208✔
303
          msLayerGetItems(layer) != MS_SUCCESS) {
104✔
304
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot get layer fields");
×
305
        return {};
×
306
      }
307
      if (queryableItems[0] == "all") {
104✔
308
        queryableItems.clear();
309
        for (int i = 0; i < layer->numitems; ++i) {
20✔
310
          if (reservedParams.find(layer->items[i]) == reservedParams.end()) {
36✔
311
            queryableItems.push_back(layer->items[i]);
36✔
312
          }
313
        }
314
      } else {
315
        std::set<std::string> validItems;
316
        for (int i = 0; i < layer->numitems; ++i) {
1,020✔
317
          validItems.insert(layer->items[i]);
1,836✔
318
        }
319
        for (const auto &item : queryableItems) {
510✔
320
          if (validItems.find(item) == validItems.end()) {
408✔
321
            // This is not a known field
322
            msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
323
                                "Invalid item '" + item +
×
324
                                    "' in queryable_items");
325
            error = true;
×
326
            return {};
×
327
          } else if (reservedParams.find(item) != reservedParams.end()) {
408✔
328
            // Check clashes with OGC API Features reserved keywords (bbox,
329
            // etc.)
330
            msOGCAPIOutputError(
×
331
                OGCAPI_CONFIG_ERROR,
332
                "Item '" + item +
×
333
                    "' in queryable_items is a reserved parameter name");
334
            error = true;
×
335
            return {};
×
336
          }
337
        }
338
      }
339
    }
340
  }
341
  return queryableItems;
342
}
119✔
343

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

394
/*
395
** Returns the limit as an int - between 1 and getMaxLimit(). We always return a
396
*valid value...
397
*/
398
static int getLimit(mapObj *map, cgiRequestObj *request, layerObj *layer,
112✔
399
                    int *limit) {
400
  int status;
401
  const char *p;
402

403
  int max_limit;
404
  max_limit = getMaxLimit(map, layer);
112✔
405

406
  p = getRequestParameter(request, "limit");
112✔
407
  if (!p || (p && strlen(p) == 0)) { // missing or empty
112✔
408
    *limit = MS_MIN(getDefaultLimit(map, layer),
90✔
409
                    max_limit); // max could be smaller than the default
410
  } else {
411
    status = msStringToInt(p, limit, 10);
22✔
412
    if (status != MS_SUCCESS)
22✔
413
      return MS_FAILURE;
414

415
    if (*limit <= 0) {
22✔
416
      *limit = MS_MIN(getDefaultLimit(map, layer),
×
417
                      max_limit); // max could be smaller than the default
418
    } else {
419
      *limit = MS_MIN(*limit, max_limit);
22✔
420
    }
421
  }
422

423
  return MS_SUCCESS;
424
}
425

426
// Return the content of the "crs" member of the /collections/{name} response
427
static json getCrsList(mapObj *map, layerObj *layer) {
27✔
428
  char *pszSRSList = NULL;
27✔
429
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_FALSE,
27✔
430
                   &pszSRSList);
431
  if (!pszSRSList)
27✔
432
    msOWSGetEPSGProj(&(map->projection), &(map->web.metadata), "AOF", MS_FALSE,
1✔
433
                     &pszSRSList);
434
  json jCrsList;
435
  if (pszSRSList) {
27✔
436
    const auto tokens = msStringSplit(pszSRSList, ' ');
27✔
437
    for (const auto &crs : tokens) {
70✔
438
      if (crs.find("EPSG:") == 0) {
43✔
439
        if (jCrsList.empty()) {
16✔
440
          jCrsList.push_back(CRS84_URL);
54✔
441
        }
442
        const std::string url =
443
            std::string(EPSG_PREFIX_URL) + crs.substr(strlen("EPSG:"));
86✔
444
        jCrsList.push_back(url);
86✔
445
      }
446
    }
447
    msFree(pszSRSList);
27✔
448
  }
27✔
449
  return jCrsList;
27✔
450
}
451

452
// Return the content of the "storageCrs" member of the /collections/{name}
453
// response
454
static std::string getStorageCrs(layerObj *layer) {
16✔
455
  std::string storageCrs;
456
  char *pszFirstSRS = nullptr;
16✔
457
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_TRUE,
16✔
458
                   &pszFirstSRS);
459
  if (pszFirstSRS) {
16✔
460
    if (std::string(pszFirstSRS).find("EPSG:") == 0) {
30✔
461
      storageCrs =
462
          std::string(EPSG_PREFIX_URL) + (pszFirstSRS + strlen("EPSG:"));
45✔
463
    }
464
    msFree(pszFirstSRS);
15✔
465
  }
466
  return storageCrs;
16✔
467
}
468

469
/*
470
** Returns the bbox in output CRS (CRS84 by default, or "bbox-crs" request
471
*parameter when specified)
472
*/
473
static bool getBbox(mapObj *map, layerObj *layer, cgiRequestObj *request,
97✔
474
                    rectObj *bbox, projectionObj *outputProj) {
475
  int status;
476

477
  const char *bboxParam = getRequestParameter(request, "bbox");
97✔
478
  if (!bboxParam || strlen(bboxParam) == 0) { // missing or empty extent
97✔
479
    rectObj rect;
480
    if (FLTLayerSetInvalidRectIfSupported(layer, &rect, "AO")) {
90✔
481
      bbox->minx = rect.minx;
×
482
      bbox->miny = rect.miny;
×
483
      bbox->maxx = rect.maxx;
×
484
      bbox->maxy = rect.maxy;
×
485
    } else {
486
      // assign map->extent (no projection necessary)
487
      bbox->minx = map->extent.minx;
90✔
488
      bbox->miny = map->extent.miny;
90✔
489
      bbox->maxx = map->extent.maxx;
90✔
490
      bbox->maxy = map->extent.maxy;
90✔
491
    }
492
  } else {
90✔
493
    const auto tokens = msStringSplit(bboxParam, ',');
7✔
494
    if (tokens.size() != 4) {
7✔
495
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
496
      return false;
×
497
    }
498

499
    double values[4];
500
    for (int i = 0; i < 4; i++) {
35✔
501
      status = msStringToDouble(tokens[i].c_str(), &values[i]);
28✔
502
      if (status != MS_SUCCESS) {
28✔
503
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
504
        return false;
×
505
      }
506
    }
507

508
    bbox->minx = values[0]; // assign
7✔
509
    bbox->miny = values[1];
7✔
510
    bbox->maxx = values[2];
7✔
511
    bbox->maxy = values[3];
7✔
512

513
    // validate bbox is well-formed (degenerate is ok)
514
    if (MS_VALID_SEARCH_EXTENT(*bbox) != MS_TRUE) {
7✔
515
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
516
      return false;
×
517
    }
518

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

548
    projectionObj bboxProj;
549
    msInitProjection(&bboxProj);
5✔
550
    msProjectionInheritContextFrom(&bboxProj, &(map->projection));
5✔
551
    if (msLoadProjectionString(&bboxProj, bboxCrs.c_str()) != 0) {
5✔
552
      msFreeProjection(&bboxProj);
×
553
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot process bbox-crs.");
×
554
      return false;
×
555
    }
556

557
    status = msProjectRect(&bboxProj, outputProj, bbox);
5✔
558
    msFreeProjection(&bboxProj);
5✔
559
    if (status != MS_SUCCESS) {
5✔
560
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
561
                          "Cannot reproject bbox from bbox-crs to output CRS.");
562
      return false;
×
563
    }
564
  }
7✔
565

566
  return true;
567
}
568

569
/*
570
** Returns the template directory location or NULL if it isn't set.
571
*/
572
std::string msOGCAPIGetTemplateDirectory(const mapObj *map, const char *key,
15✔
573
                                         const char *envvar) {
574
  const char *directory = NULL;
575

576
  if (map != NULL) {
15✔
577
    directory = msOWSLookupMetadata(&(map->web.metadata), "A", key);
13✔
578
  }
579

580
  if (directory == NULL) {
13✔
581
    directory = CPLGetConfigOption(envvar, NULL);
2✔
582
  }
583

584
  std::string s;
585
  if (directory != NULL) {
15✔
586
    s = directory;
587
    if (!s.empty() && (s.back() != '/' && s.back() != '\\')) {
15✔
588
      // add a trailing slash if missing
589
      std::string slash = "/";
3✔
590
#ifdef _WIN32
591
      slash = "\\";
592
#endif
593
      s += slash;
594
    }
595
  }
596

597
  return s;
15✔
598
}
599

600
/*
601
** Returns the service title from oga_{key} and/or ows_{key} or a default value
602
*if not set.
603
*/
604
static const char *getWebMetadata(const mapObj *map, const char *domain,
605
                                  const char *key, const char *defaultVal) {
606
  const char *value;
607

608
  if ((value = msOWSLookupMetadata(&(map->web.metadata), domain, key)) != NULL)
22✔
609
    return value;
610
  else
611
    return defaultVal;
1✔
612
}
613

614
/*
615
** Returns the service title from oga|ows_title or a default value if not set.
616
*/
617
static const char *getTitle(const mapObj *map) {
618
  return getWebMetadata(map, "OA", "title", OGCAPI_DEFAULT_TITLE);
619
}
620

621
/*
622
** Returns the API root URL from oga_onlineresource or builds a value if not
623
*set.
624
*/
625
std::string msOGCAPIGetApiRootUrl(const mapObj *map,
156✔
626
                                  const cgiRequestObj *request,
627
                                  const char *namespaces) {
628
  const char *root;
629
  if ((root = msOWSLookupMetadata(&(map->web.metadata), namespaces,
156✔
630
                                  "onlineresource")) != NULL) {
631
    return std::string(root);
150✔
632
  }
633

634
  std::string api_root;
635
  if (char *res = msBuildOnlineResource(NULL, request)) {
6✔
636
    api_root = res;
637
    free(res);
4✔
638

639
    // find last ogcapi in the string and strip the rest to get the root API
640
    std::size_t pos = api_root.rfind("ogcapi");
641
    if (pos != std::string::npos) {
4✔
642
      api_root = api_root.substr(0, pos + std::string("ogcapi").size());
4✔
643
    } else {
644
      // strip trailing '?' or '/' and append "/ogcapi"
645
      while (!api_root.empty() &&
6✔
646
             (api_root.back() == '?' || api_root.back() == '/')) {
6✔
647
        api_root.pop_back();
648
      }
649
      api_root += "/ogcapi";
650
    }
651
  }
652

653
  if (api_root.empty()) {
6✔
654
    api_root = "/ogcapi";
655
  }
656

657
  return api_root;
6✔
658
}
659

660
static json getFeatureConstant(const gmlConstantObj *constant) {
×
661
  json j; // empty (null)
662

663
  if (!constant)
×
664
    throw std::runtime_error("Null constant metadata.");
×
665
  if (!constant->value)
×
666
    return j;
667

668
  // initialize
669
  j = {{constant->name, constant->value}};
×
670

671
  return j;
×
672
}
×
673

674
static json getFeatureItem(const gmlItemObj *item, const char *value) {
1,561✔
675
  json j; // empty (null)
676
  const char *key;
677

678
  if (!item)
1,561✔
679
    throw std::runtime_error("Null item metadata.");
×
680
  if (!item->visible)
1,561✔
681
    return j;
682

683
  if (item->alias)
1,006✔
684
    key = item->alias;
608✔
685
  else
686
    key = item->name;
398✔
687

688
  // initialize
689
  j = {{key, value}};
4,024✔
690

691
  if (item->type &&
1,610✔
692
      (EQUAL(item->type, "Date") || EQUAL(item->type, "DateTime") ||
604✔
693
       EQUAL(item->type, "Time"))) {
453✔
694
    struct tm tm;
695
    if (msParseTime(value, &tm) == MS_TRUE) {
151✔
696
      char tmpValue[64];
697
      if (EQUAL(item->type, "Date"))
151✔
698
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02d",
×
699
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
×
700
      else if (EQUAL(item->type, "Time"))
151✔
701
        snprintf(tmpValue, sizeof(tmpValue), "%02d:%02d:%02dZ", tm.tm_hour,
×
702
                 tm.tm_min, tm.tm_sec);
703
      else
704
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02dT%02d:%02d:%02dZ",
151✔
705
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
151✔
706
                 tm.tm_min, tm.tm_sec);
707

708
      j = {{key, tmpValue}};
604✔
709
    }
710
  } else if (item->type &&
1,006✔
711
             (EQUAL(item->type, "Integer") || EQUAL(item->type, "Long"))) {
453✔
712
    try {
713
      j = {{key, std::stoll(value)}};
755✔
714
    } catch (const std::exception &) {
×
715
    }
×
716
  } else if (item->type && EQUAL(item->type, "Real")) {
704✔
717
    try {
718
      j = {{key, std::stod(value)}};
1,510✔
719
    } catch (const std::exception &) {
×
720
    }
×
721
  } else if (item->type && EQUAL(item->type, "Boolean")) {
402✔
722
    if (EQUAL(value, "0") || EQUAL(value, "false")) {
×
723
      j = {{key, false}};
×
724
    } else {
725
      j = {{key, true}};
×
726
    }
727
  }
728

729
  return j;
730
}
6,893✔
731

732
static double round_down(double value, int decimal_places) {
32✔
733
  const double multiplier = std::pow(10.0, decimal_places);
734
  return std::floor(value * multiplier) / multiplier;
32✔
735
}
736
// https://stackoverflow.com/questions/25925290/c-round-a-double-up-to-2-decimal-places
737
static double round_up(double value, int decimal_places) {
221,374✔
738
  const double multiplier = std::pow(10.0, decimal_places);
739
  return std::ceil(value * multiplier) / multiplier;
221,374✔
740
}
741

742
static json getFeatureGeometry(shapeObj *shape, int precision,
180✔
743
                               bool outputCrsAxisInverted) {
744
  json geometry; // empty (null)
745
  int *outerList = NULL, numOuterRings = 0;
746

747
  if (!shape)
180✔
748
    throw std::runtime_error("Null shape.");
×
749

750
  switch (shape->type) {
180✔
751
  case (MS_SHAPE_POINT):
23✔
752
    if (shape->numlines == 0 ||
23✔
753
        shape->line[0].numpoints == 0) // not enough info for a point
23✔
754
      return geometry;
755

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

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

815
    outerList = msGetOuterList(shape);
157✔
816
    if (outerList == NULL)
157✔
817
      throw std::runtime_error("Unable to allocate list of outer rings.");
×
818
    for (int k = 0; k < shape->numlines; k++) {
320✔
819
      if (outerList[k] == MS_TRUE)
163✔
820
        numOuterRings++;
159✔
821
    }
822

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

841
      for (int k = 0; k < shape->numlines; k++) {
10✔
842
        if (outerList[k] ==
8✔
843
            MS_TRUE) { // outer ring: generate polygon and add to coordinates
844
          int *innerList = msGetInnerList(shape, k, outerList);
4✔
845
          if (innerList == NULL) {
4✔
846
            msFree(outerList);
×
847
            throw std::runtime_error("Unable to allocate list of inner rings.");
×
848
          }
849

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

868
          msFree(innerList);
4✔
869
          geometry["coordinates"].push_back(polygon);
4✔
870
        }
871
      }
872
    }
873
    msFree(outerList);
157✔
874
    break;
157✔
875
  default:
×
876
    throw std::runtime_error("Invalid shape type.");
×
877
    break;
878
  }
879

880
  return geometry;
881
}
882

883
/*
884
** Return a GeoJSON representation of a shape.
885
*/
886
static json getFeature(layerObj *layer, shapeObj *shape, gmlItemListObj *items,
180✔
887
                       gmlConstantListObj *constants, int geometry_precision,
888
                       bool outputCrsAxisInverted) {
889
  int i;
890
  json feature; // empty (null)
891

892
  if (!layer || !shape)
180✔
893
    throw std::runtime_error("Null arguments.");
×
894

895
  // initialize
896
  feature = {{"type", "Feature"}, {"properties", json::object()}};
1,440✔
897

898
  // id
899
  const char *featureIdItem =
900
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
180✔
901
  if (featureIdItem == NULL)
180✔
902
    throw std::runtime_error(
903
        "Missing required featureid metadata."); // should have been trapped
×
904
                                                 // earlier
905
  for (i = 0; i < items->numitems; i++) {
880✔
906
    if (strcasecmp(featureIdItem, items->items[i].name) == 0) {
880✔
907
      feature["id"] = shape->values[i];
360✔
908
      break;
180✔
909
    }
910
  }
911

912
  if (i == items->numitems)
180✔
913
    throw std::runtime_error("Feature id not found.");
×
914

915
  // properties - build from items and constants, no group support for now
916

917
  for (int i = 0; i < items->numitems; i++) {
1,741✔
918
    try {
919
      json item = getFeatureItem(&(items->items[i]), shape->values[i]);
1,561✔
920
      if (!item.is_null())
1,561✔
921
        feature["properties"].insert(item.begin(), item.end());
1,006✔
922
    } catch (const std::runtime_error &) {
×
923
      throw std::runtime_error("Error fetching item.");
×
924
    }
×
925
  }
926

927
  for (int i = 0; i < constants->numconstants; i++) {
180✔
928
    try {
929
      json constant = getFeatureConstant(&(constants->constants[i]));
×
930
      if (!constant.is_null())
×
931
        feature["properties"].insert(constant.begin(), constant.end());
×
932
    } catch (const std::runtime_error &) {
×
933
      throw std::runtime_error("Error fetching constant.");
×
934
    }
×
935
  }
936

937
  // geometry
938
  try {
939
    json geometry =
940
        getFeatureGeometry(shape, geometry_precision, outputCrsAxisInverted);
180✔
941
    if (!geometry.is_null())
180✔
942
      feature["geometry"] = std::move(geometry);
360✔
943
  } catch (const std::runtime_error &) {
×
944
    throw std::runtime_error("Error fetching geometry.");
×
945
  }
×
946

947
  return feature;
180✔
948
}
1,440✔
949

950
static json getLink(hashTableObj *metadata, const std::string &name) {
19✔
951
  json link;
952

953
  const char *href =
954
      msOWSLookupMetadata(metadata, "A", (name + "_href").c_str());
19✔
955
  if (!href)
19✔
956
    throw std::runtime_error("Missing required link href property.");
×
957

958
  const char *title =
959
      msOWSLookupMetadata(metadata, "A", (name + "_title").c_str());
19✔
960
  const char *type =
961
      msOWSLookupMetadata(metadata, "A", (name + "_type").c_str());
38✔
962

963
  link = {{"href", href},
964
          {"title", title ? title : href},
965
          {"type", type ? type : "text/html"}};
228✔
966

967
  return link;
19✔
968
}
190✔
969

970
static const char *getCollectionDescription(layerObj *layer) {
41✔
971
  const char *description =
972
      msOWSLookupMetadata(&(layer->metadata), "A", "description");
41✔
973
  if (!description)
41✔
974
    description = msOWSLookupMetadata(&(layer->metadata), "OF",
1✔
975
                                      "abstract"); // fallback on abstract
976
  if (!description)
1✔
977
    description =
978
        "<!-- Warning: unable to set the collection description. -->"; // finally
979
                                                                       // a
980
                                                                       // warning...
981
  return description;
41✔
982
}
983

984
static const char *getCollectionTitle(layerObj *layer) {
985
  const char *title = msOWSLookupMetadata(&(layer->metadata), "AOF", "title");
59✔
986
  if (!title)
75✔
987
    title = layer->name; // revert to layer name if no title found
×
988
  return title;
989
}
990

991
static int getGeometryPrecision(mapObj *map, layerObj *layer) {
110✔
992
  int geometry_precision = OGCAPI_DEFAULT_GEOMETRY_PRECISION;
993
  if (msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision")) {
110✔
994
    geometry_precision = atoi(
×
995
        msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision"));
996
  } else if (msOWSLookupMetadata(&map->web.metadata, "AF",
110✔
997
                                 "geometry_precision")) {
998
    geometry_precision = atoi(
98✔
999
        msOWSLookupMetadata(&map->web.metadata, "AF", "geometry_precision"));
1000
  }
1001
  return geometry_precision;
110✔
1002
}
1003

1004
static json getCollection(mapObj *map, layerObj *layer, OGCAPIFormat format,
16✔
1005
                          const std::string &api_root) {
1006
  json collection; // empty (null)
1007
  rectObj bbox;
1008

1009
  if (!map || !layer)
16✔
1010
    return collection;
1011

1012
  if (!includeLayer(map, layer))
16✔
1013
    return collection;
1014

1015
  // initialize some things
1016
  if (msOWSGetLayerExtent(map, layer, "AOF", &bbox) == MS_SUCCESS) {
16✔
1017
    if (layer->projection.numargs > 0)
16✔
1018
      msOWSProjectToWGS84(&layer->projection, &bbox);
15✔
1019
    else if (map->projection.numargs > 0)
1✔
1020
      msOWSProjectToWGS84(&map->projection, &bbox);
1✔
1021
    else
1022
      throw std::runtime_error(
1023
          "Unable to transform bounding box, no projection defined.");
×
1024
  } else {
1025
    throw std::runtime_error(
1026
        "Unable to get collection bounding box."); // might be too harsh since
×
1027
                                                   // extent is optional
1028
  }
1029

1030
  const char *description = getCollectionDescription(layer);
16✔
1031
  const char *title = getCollectionTitle(layer);
16✔
1032

1033
  const char *id = layer->name;
16✔
1034
  char *id_encoded = msEncodeUrl(id); // free after use
16✔
1035

1036
  const int geometry_precision = getGeometryPrecision(map, layer);
16✔
1037

1038
  const std::string extra_params = getExtraParameterString(map, layer);
16✔
1039

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

1108
  msFree(id_encoded); // done
16✔
1109

1110
  // handle optional configuration (keywords and links)
1111
  const char *value = msOWSLookupMetadata(&(layer->metadata), "A", "keywords");
16✔
1112
  if (!value)
16✔
1113
    value = msOWSLookupMetadata(&(layer->metadata), "OF",
9✔
1114
                                "keywordlist"); // fallback on keywordlist
1115
  if (value) {
9✔
1116
    std::vector<std::string> keywords = msStringSplit(value, ',');
7✔
1117
    for (const std::string &keyword : keywords) {
32✔
1118
      collection["keywords"].push_back(keyword);
75✔
1119
    }
1120
  }
7✔
1121

1122
  value = msOWSLookupMetadata(&(layer->metadata), "A", "links");
16✔
1123
  if (value) {
16✔
1124
    std::vector<std::string> names = msStringSplit(value, ',');
11✔
1125
    for (const std::string &name : names) {
22✔
1126
      try {
1127
        json link = getLink(&(layer->metadata), name);
11✔
1128
        collection["links"].push_back(link);
11✔
1129
      } catch (const std::runtime_error &e) {
×
1130
        throw e;
×
1131
      }
×
1132
    }
1133
  }
11✔
1134

1135
  // Part 2 - CRS support
1136
  // Inspect metadata to set the "crs": [] member and "storageCrs" member
1137

1138
  json jCrsList = getCrsList(map, layer);
16✔
1139
  if (!jCrsList.empty()) {
16✔
1140
    collection["crs"] = std::move(jCrsList);
16✔
1141

1142
    std::string storageCrs = getStorageCrs(layer);
16✔
1143
    if (!storageCrs.empty()) {
16✔
1144
      collection["storageCrs"] = std::move(storageCrs);
30✔
1145
    }
1146
  }
1147

1148
  return collection;
1149
}
3,408✔
1150

1151
/*
1152
** Output stuff...
1153
*/
1154

1155
void msOGCAPIOutputJson(
123✔
1156
    const json &j, const char *mimetype,
1157
    const std::map<std::string, std::vector<std::string>> &extraHeaders) {
1158
  std::string js;
1159

1160
  try {
1161
    js = j.dump();
123✔
1162
  } catch (...) {
1✔
1163
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
1164
                        "Invalid UTF-8 data, check encoding.");
1165
    return;
1166
  }
1✔
1167

1168
  msIO_setHeader("Content-Type", "%s", mimetype);
122✔
1169
  for (const auto &kvp : extraHeaders) {
472✔
1170
    for (const auto &value : kvp.second) {
806✔
1171
      msIO_setHeader(kvp.first.c_str(), "%s", value.c_str());
456✔
1172
    }
1173
  }
1174
  msIO_sendHeaders();
122✔
1175
  msIO_printf("%s\n", js.c_str());
122✔
1176
}
1177

1178
void msOGCAPIOutputTemplate(const char *directory, const char *filename,
15✔
1179
                            const json &j, const char *mimetype) {
1180
  std::string _directory(directory);
15✔
1181
  std::string _filename(filename);
15✔
1182
  Environment env{_directory}; // catch
15✔
1183

1184
  // ERB-style instead of Mustache (we'll see)
1185
  // env.set_expression("<%=", "%>");
1186
  // env.set_statement("<%", "%>");
1187

1188
  // callbacks, need:
1189
  //   - match (regex)
1190
  //   - contains (substring)
1191
  //   - URL encode
1192

1193
  try {
1194
    std::string js = j.dump();
15✔
1195
  } catch (...) {
1✔
1196
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
1✔
1197
                        "Invalid UTF-8 data, check encoding.");
1198
    return;
1199
  }
1✔
1200

1201
  try {
1202
    Template t = env.parse_template(_filename); // catch
14✔
1203
    std::string result = env.render(t, j);
14✔
1204

1205
    msIO_setHeader("Content-Type", "%s", mimetype);
14✔
1206
    msIO_sendHeaders();
14✔
1207
    msIO_printf("%s\n", result.c_str());
14✔
1208
  } catch (const inja::RenderError &e) {
14✔
1209
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template rendering error. " +
×
1210
                                                 std::string(e.what()) + " (" +
×
1211
                                                 std::string(filename) + ").");
×
1212
    return;
1213
  } catch (const inja::InjaError &e) {
×
1214
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1215
                        "InjaError error. " + std::string(e.what()) + " (" +
×
1216
                            std::string(filename) + ")." + " (" +
×
1217
                            std::string(directory) + ").");
×
1218
    return;
1219
  } catch (...) {
×
1220
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1221
                        "General template handling error.");
1222
    return;
1223
  }
×
1224
}
15✔
1225

1226
/*
1227
** Generic response output.
1228
*/
1229
static void outputResponse(
123✔
1230
    const mapObj *map, const cgiRequestObj *request, OGCAPIFormat format,
1231
    const char *filename, const json &response,
1232
    const std::map<std::string, std::vector<std::string>> &extraHeaders =
1233
        std::map<std::string, std::vector<std::string>>()) {
1234
  std::string path;
1235
  char fullpath[MS_MAXPATHLEN];
1236

1237
  if (format == OGCAPIFormat::JSON) {
1238
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_JSON, extraHeaders);
12✔
1239
  } else if (format == OGCAPIFormat::GeoJSON) {
1240
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_GEOJSON, extraHeaders);
90✔
1241
  } else if (format == OGCAPIFormat::OpenAPI_V3) {
1242
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_OPENAPI_V3, extraHeaders);
1✔
1243
  } else if (format == OGCAPIFormat::JSONSchema) {
1244
    msOGCAPIOutputJson(response, OGCAPI_MIMETYPE_JSON_SCHEMA, extraHeaders);
7✔
1245
  } else if (format == OGCAPIFormat::HTML) {
1246
    path = msOGCAPIGetTemplateDirectory(map, "html_template_directory",
26✔
1247
                                        "OGCAPI_HTML_TEMPLATE_DIRECTORY");
13✔
1248
    if (path.empty()) {
13✔
1249
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, "Template directory not set.");
×
1250
      return; // bail
×
1251
    }
1252
    msBuildPath(fullpath, map->mappath, path.c_str());
13✔
1253

1254
    json j;
1255

1256
    j["response"] = response; // nest the response so we could write the whole
13✔
1257
                              // object in the template
1258

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

1262
    j["template"] = {{"path", json::array()},
26✔
1263
                     {"params", json::object()},
13✔
1264
                     {"api_root", msOGCAPIGetApiRootUrl(map, request)},
13✔
1265
                     {"extra_params", extra_params},
1266
                     {"title", getTitle(map)},
13✔
1267
                     {"tags", json::object()}};
260✔
1268

1269
    // api path
1270
    for (int i = 0; i < request->api_path_length; i++)
68✔
1271
      j["template"]["path"].push_back(request->api_path[i]);
165✔
1272

1273
    // parameters (optional)
1274
    for (int i = 0; i < request->NumParams; i++) {
33✔
1275
      if (request->ParamValues[i] &&
20✔
1276
          strlen(request->ParamValues[i]) > 0) { // skip empty params
20✔
1277
        j["template"]["params"].update(
108✔
1278
            {{request->ParamNames[i], request->ParamValues[i]}});
36✔
1279
      }
1280
    }
1281

1282
    // add custom tags (optional)
1283
    const char *tags =
1284
        msOWSLookupMetadata(&(map->web.metadata), "A", "html_tags");
13✔
1285
    if (tags) {
13✔
1286
      std::vector<std::string> names = msStringSplit(tags, ',');
6✔
1287
      for (std::string name : names) {
18✔
1288
        const char *value = msOWSLookupMetadata(&(map->web.metadata), "A",
12✔
1289
                                                ("tag_" + name).c_str());
24✔
1290
        if (value) {
12✔
1291
          j["template"]["tags"].update({{name, value}}); // add object
72✔
1292
        }
1293
      }
1294
    }
6✔
1295

1296
    msOGCAPIOutputTemplate(fullpath, filename, j, OGCAPI_MIMETYPE_HTML);
13✔
1297
  } else {
1298
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
1299
  }
1300
}
437✔
1301

1302
/*
1303
** Process stuff...
1304
*/
1305
static int processLandingRequest(mapObj *map, cgiRequestObj *request,
7✔
1306
                                 OGCAPIFormat format) {
1307
  json response;
1308

1309
  auto allowedParameters = getExtraParameters(map, nullptr);
7✔
1310
  allowedParameters.insert("f");
7✔
1311
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
7✔
1312
    return MS_SUCCESS;
1313
  }
1314

1315
  // define ambiguous elements
1316
  const char *description =
1317
      msOWSLookupMetadata(&(map->web.metadata), "A", "description");
6✔
1318
  if (!description)
6✔
1319
    description =
1320
        msOWSLookupMetadata(&(map->web.metadata), "OF",
3✔
1321
                            "abstract"); // fallback on abstract if necessary
1322

1323
  const std::string extra_params = getExtraParameterString(map, nullptr);
6✔
1324

1325
  // define api root url
1326
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
6✔
1327

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

1370
  // handle custom links (optional)
1371
  const char *links = msOWSLookupMetadata(&(map->web.metadata), "A", "links");
6✔
1372
  if (links) {
6✔
1373
    std::vector<std::string> names = msStringSplit(links, ',');
5✔
1374
    for (std::string name : names) {
13✔
1375
      try {
1376
        json link = getLink(&(map->web.metadata), name);
8✔
1377
        response["links"].push_back(link);
8✔
1378
      } catch (const std::runtime_error &e) {
×
1379
        msOGCAPIOutputError(OGCAPI_CONFIG_ERROR, std::string(e.what()));
×
1380
        return MS_SUCCESS;
1381
      }
×
1382
    }
1383
  }
5✔
1384

1385
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_LANDING, response);
6✔
1386
  return MS_SUCCESS;
6✔
1387
}
918✔
1388

1389
static int processConformanceRequest(mapObj *map, cgiRequestObj *request,
3✔
1390
                                     OGCAPIFormat format) {
1391
  json response;
1392

1393
  auto allowedParameters = getExtraParameters(map, nullptr);
3✔
1394
  allowedParameters.insert("f");
3✔
1395
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
3✔
1396
    return MS_SUCCESS;
1397
  }
1398

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

1434
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_CONFORMANCE,
2✔
1435
                 response);
1436
  return MS_SUCCESS;
2✔
1437
}
56✔
1438

1439
static int findLayerIndex(const mapObj *map, const char *collectionId) {
129✔
1440
  for (int i = 0; i < map->numlayers; i++) {
160✔
1441
    if (strcmp(map->layers[i]->name, collectionId) == 0) {
157✔
1442
      return i;
126✔
1443
    }
1444
  }
1445
  return -1;
1446
}
1447

1448
static int processCollectionItemsRequest(mapObj *map, cgiRequestObj *request,
112✔
1449
                                         const char *collectionId,
1450
                                         const char *featureId,
1451
                                         OGCAPIFormat format) {
1452
  json response;
1453

1454
  // find the right layer
1455
  const int iLayer = findLayerIndex(map, collectionId);
112✔
1456

1457
  if (iLayer < 0) { // invalid collectionId
112✔
1458
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1459
    return MS_SUCCESS;
×
1460
  }
1461

1462
  layerObj *layer = map->layers[iLayer];
112✔
1463
  layer->status = MS_ON; // force on (do we need to save and reset?)
112✔
1464

1465
  if (!includeLayer(map, layer)) {
112✔
1466
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1467
    return MS_SUCCESS;
×
1468
  }
1469

1470
  //
1471
  // handle parameters specific to this endpoint
1472
  //
1473
  int limit = -1;
112✔
1474
  if (getLimit(map, request, layer, &limit) != MS_SUCCESS) {
112✔
1475
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for limit.");
×
1476
    return MS_SUCCESS;
×
1477
  }
1478

1479
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
112✔
1480
  const char *crs = getRequestParameter(request, "crs");
112✔
1481

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

1510
  auto reservedParameters = getExtraParameters(map, layer);
110✔
1511
  reservedParameters.insert("f");
110✔
1512
  reservedParameters.insert("bbox");
110✔
1513
  reservedParameters.insert("bbox-crs");
110✔
1514
  reservedParameters.insert("datetime");
110✔
1515
  reservedParameters.insert("limit");
110✔
1516
  reservedParameters.insert("offset");
110✔
1517
  reservedParameters.insert("crs");
110✔
1518
  reservedParameters.insert("filter");
110✔
1519
  reservedParameters.insert("filter-lang");
110✔
1520
  reservedParameters.insert("filter-crs");
110✔
1521
  reservedParameters.insert("sortby");
110✔
1522

1523
  bool error = false;
1524
  std::vector<std::string> queryableItems =
1525
      msOOGCAPIGetLayerQueryables(layer, reservedParameters, error);
110✔
1526
  if (error) {
110✔
1527
    return MS_SUCCESS;
1528
  }
1529

1530
  for (std::string &item : queryableItems) {
506✔
1531
    item = getItemAliasOrName(layer, item);
792✔
1532
  }
1533

1534
  auto allowedParameters = reservedParameters;
1535
  for (const auto &item : queryableItems)
506✔
1536
    allowedParameters.insert(item);
1537

1538
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
110✔
1539
    return MS_SUCCESS;
1540
  }
1541

1542
  // Simple filtering like "field_name=value"
1543
  std::string filter;
1544
  std::string query_kvp;
1545
  if (!queryableItems.empty()) {
107✔
1546
    for (int i = 0; i < request->NumParams; i++) {
349✔
1547
      if (std::find(queryableItems.begin(), queryableItems.end(),
252✔
1548
                    request->ParamNames[i]) != queryableItems.end()) {
252✔
1549

1550
        // Find actual item name from alias
1551
        const char *pszItem = nullptr;
1552
        for (int j = 0; j < layer->numitems; ++j) {
48✔
1553
          if (request->ParamNames[i] ==
48✔
1554
              getItemAliasOrName(layer, layer->items[j])) {
96✔
1555
            pszItem = layer->items[j];
8✔
1556
            break;
8✔
1557
          }
1558
        }
1559
        assert(pszItem);
1560

1561
        const std::string expr = FLTGetBinaryComparisonCommonExpression(
1562
            layer, pszItem, false, "=", request->ParamValues[i]);
8✔
1563
        if (!filter.empty())
8✔
1564
          filter += " AND ";
1565
        filter += expr;
1566

1567
        query_kvp += '&';
1568
        char *encoded = msEncodeUrl(request->ParamNames[i]);
8✔
1569
        query_kvp += encoded;
1570
        msFree(encoded);
8✔
1571
        query_kvp += '=';
1572
        encoded = msEncodeUrl(request->ParamValues[i]);
8✔
1573
        query_kvp += encoded;
1574
        msFree(encoded);
8✔
1575
      }
1576
    }
1577
  }
1578

1579
  const char *filterParam = getRequestParameter(request, "filter");
107✔
1580
  const char *filterLang = getRequestParameter(request, "filter-lang");
107✔
1581
  if (filterParam) {
107✔
1582
    if (filterLang && strcmp(filterLang, "cql2-text") != 0 &&
77✔
1583
        strcmp(filterLang, "cql2-json") != 0) {
30✔
1584
      msOGCAPIOutputError(
1✔
1585
          OGCAPI_PARAM_ERROR,
1586
          "Only filter-lang=cql2-text or filter-lang=cql2-json is handled");
1587
      return MS_SUCCESS;
9✔
1588
    }
1589

1590
    std::string osErrorMsg;
1591
    auto cql2 = (filterLang && strcmp(filterLang, "cql2-json") == 0)
29✔
1592
                    ? CQL2JSONParse(filterParam, osErrorMsg)
76✔
1593
                    : CQL2TextParse(filterParam, osErrorMsg);
76✔
1594
    if (!cql2) {
76✔
1595
      msOGCAPIOutputError(OGCAPI_PARAM_ERROR,
3✔
1596
                          "Cannot parse filter: " + osErrorMsg);
3✔
1597
      return MS_SUCCESS;
3✔
1598
    }
1599

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

1625
    const char *geometryName = getGeometryName(layer);
1626
    const std::string filterFromCQL =
1627
        cql2->ToMapServerFilter(layer, queryableItems, geometryName, filterCrs,
1628
                                axisInverted, osErrorMsg);
144✔
1629
    if (!osErrorMsg.empty()) {
72✔
1630
      msOGCAPIOutputError(
4✔
1631
          OGCAPI_PARAM_ERROR,
1632
          "Cannot translate CQL2 filter to MapServer expression: " +
8✔
1633
              osErrorMsg);
1634
      return MS_SUCCESS;
1635
    }
1636
    if (filter.empty())
68✔
1637
      filter = filterFromCQL;
1638
    else {
1639
      filter = "(" + filter + ") AND (" + filterFromCQL + ")";
12✔
1640
    }
1641

1642
    query_kvp += "&filter=";
1643
    char *encoded = msEncodeUrl(filterParam);
68✔
1644
    query_kvp += encoded;
1645
    msFree(encoded);
68✔
1646
    if (filterLang) {
68✔
1647
      query_kvp += "&filter-lang=";
1648
      query_kvp += filterLang;
1649
    }
1650
    if (filterCrsParam) {
68✔
1651
      query_kvp += "&filter-crs=";
1652
      encoded = msEncodeUrl(filterCrsParam);
2✔
1653
      query_kvp += encoded;
1654
      msFree(encoded);
2✔
1655
    }
1656
  }
76✔
1657

1658
  if (!filter.empty()) {
98✔
1659
    msDebug("filter = %s\n", filter.c_str());
72✔
1660
  }
1661

1662
  const char *sortby = getRequestParameter(request, "sortby");
98✔
1663
  if (sortby) {
98✔
1664
    query_kvp += "&sortby=";
1665
    {
1666
      char *encoded = msEncodeUrl(sortby);
4✔
1667
      query_kvp += encoded;
1668
      msFree(encoded);
4✔
1669
    }
1670

1671
    const auto sortables =
1672
        msOOGCAPIGetLayerSortables(layer, reservedParameters, error);
4✔
1673
    if (error) {
4✔
1674
      return MS_SUCCESS;
1675
    }
1676

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

1707
    sortByClause sortByClause;
1708
    sortByClause.nProperties = static_cast<int>(props.size());
3✔
1709
    sortByClause.properties = props.data();
3✔
1710
    msLayerSetSort(layer, &sortByClause);
3✔
1711
  }
4✔
1712

1713
  struct ReprojectionObjects {
1714
    reprojectionObj *reprojector = NULL;
1715
    projectionObj proj;
1716

1717
    ReprojectionObjects() { msInitProjection(&proj); }
97✔
1718

1719
    ~ReprojectionObjects() {
1720
      msProjectDestroyReprojector(reprojector);
97✔
1721
      msFreeProjection(&proj);
97✔
1722
    }
97✔
1723

1724
    int executeQuery(mapObj *map) {
176✔
1725
      projectionObj backupMapProjection = map->projection;
176✔
1726
      map->projection = proj;
176✔
1727
      int ret = msExecuteQuery(map);
176✔
1728
      map->projection = backupMapProjection;
176✔
1729
      return ret;
176✔
1730
    }
1731
  };
1732
  ReprojectionObjects reprObjs;
1733

1734
  msProjectionInheritContextFrom(&reprObjs.proj, &(map->projection));
97✔
1735
  if (msLoadProjectionString(&reprObjs.proj, outputCrs.c_str()) != 0) {
97✔
1736
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot instantiate output CRS.");
×
1737
    return MS_SUCCESS;
×
1738
  }
1739

1740
  if (layer->projection.numargs > 0) {
97✔
1741
    if (msProjectionsDiffer(&(layer->projection), &reprObjs.proj)) {
97✔
1742
      reprObjs.reprojector =
90✔
1743
          msProjectCreateReprojector(&(layer->projection), &reprObjs.proj);
90✔
1744
      if (reprObjs.reprojector == NULL) {
90✔
1745
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1746
                            "Error creating re-projector.");
1747
        return MS_SUCCESS;
×
1748
      }
1749
    }
1750
  } else if (map->projection.numargs > 0) {
×
1751
    if (msProjectionsDiffer(&(map->projection), &reprObjs.proj)) {
×
1752
      reprObjs.reprojector =
×
1753
          msProjectCreateReprojector(&(map->projection), &reprObjs.proj);
×
1754
      if (reprObjs.reprojector == NULL) {
×
1755
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1756
                            "Error creating re-projector.");
1757
        return MS_SUCCESS;
×
1758
      }
1759
    }
1760
  } else {
1761
    msOGCAPIOutputError(
×
1762
        OGCAPI_CONFIG_ERROR,
1763
        "Unable to transform geometries, no projection defined.");
1764
    return MS_SUCCESS;
×
1765
  }
1766

1767
  if (map->projection.numargs > 0) {
97✔
1768
    msProjectRect(&(map->projection), &reprObjs.proj, &map->extent);
97✔
1769
  }
1770

1771
  rectObj bbox;
1772
  if (!getBbox(map, layer, request, &bbox, &reprObjs.proj)) {
97✔
1773
    return MS_SUCCESS;
1774
  }
1775

1776
  int offset = 0;
95✔
1777
  int numberMatched = 0;
1778
  if (featureId) {
95✔
1779
    const char *featureIdItem =
1780
        msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
3✔
1781
    if (featureIdItem == NULL) {
3✔
1782
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
1783
                          "Missing required featureid metadata.");
1784
      return MS_SUCCESS;
×
1785
    }
1786

1787
    // TODO: does featureIdItem exist in the data?
1788

1789
    // optional validation
1790
    const char *featureIdValidation =
1791
        msLookupHashTable(&(layer->validation), featureIdItem);
3✔
1792
    if (featureIdValidation &&
6✔
1793
        msValidateParameter(featureId, featureIdValidation, NULL, NULL, NULL) !=
3✔
1794
            MS_SUCCESS) {
1795
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid feature id.");
1✔
1796
      return MS_SUCCESS;
1✔
1797
    }
1798

1799
    map->query.type = MS_QUERY_BY_FILTER;
2✔
1800
    map->query.mode = MS_QUERY_SINGLE;
2✔
1801
    map->query.layer = iLayer;
2✔
1802
    map->query.rect = bbox;
2✔
1803
    map->query.filteritem = strdup(featureIdItem);
2✔
1804

1805
    msInitExpression(&map->query.filter);
2✔
1806
    map->query.filter.type = MS_STRING;
2✔
1807
    map->query.filter.string = strdup(featureId);
2✔
1808

1809
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
2✔
1810
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1811
                          "Collection items id query failed.");
1812
      return MS_SUCCESS;
×
1813
    }
1814

1815
    if (!layer->resultcache || layer->resultcache->numresults != 1) {
2✔
1816
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1817
                          "Collection items id query failed.");
1818
      return MS_SUCCESS;
×
1819
    }
1820
  } else { // bbox query
1821
    map->query.type = MS_QUERY_BY_RECT;
92✔
1822
    map->query.mode = MS_QUERY_MULTIPLE;
92✔
1823
    map->query.layer = iLayer;
92✔
1824
    map->query.rect = bbox;
92✔
1825
    map->query.only_cache_result_count = MS_TRUE;
92✔
1826

1827
    if (!filter.empty()) {
92✔
1828
      map->query.type = MS_QUERY_BY_FILTER;
72✔
1829
      msInitExpression(&map->query.filter);
72✔
1830
      map->query.filter.string = msStrdup(filter.c_str());
72✔
1831
      map->query.filter.type = MS_EXPRESSION;
72✔
1832
    }
1833

1834
    // get number matched
1835
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
92✔
1836
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1837
                          "Collection items query failed.");
1838
      return MS_SUCCESS;
×
1839
    }
1840

1841
    if (!layer->resultcache) {
92✔
1842
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1843
                          "Collection items query failed.");
1844
      return MS_SUCCESS;
×
1845
    }
1846

1847
    numberMatched = layer->resultcache->numresults;
92✔
1848

1849
    if (numberMatched > 0) {
92✔
1850
      map->query.only_cache_result_count = MS_FALSE;
82✔
1851
      map->query.maxfeatures = limit;
82✔
1852

1853
      msOWSSetShapeCache(map, "AO");
82✔
1854

1855
      const char *offsetStr = getRequestParameter(request, "offset");
82✔
1856
      if (offsetStr) {
82✔
1857
        if (msStringToInt(offsetStr, &offset, 10) != MS_SUCCESS) {
1✔
1858
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Bad value for offset.");
×
1859
          return MS_SUCCESS;
×
1860
        }
1861

1862
        if (offset < 0 || offset >= numberMatched) {
1✔
1863
          msOGCAPIOutputError(OGCAPI_PARAM_ERROR, "Offset out of range.");
×
1864
          return MS_SUCCESS;
×
1865
        }
1866

1867
        // msExecuteQuery() use a 1-based offset convention, whereas the API
1868
        // uses a 0-based offset convention.
1869
        map->query.startindex = 1 + offset;
1✔
1870
        layer->startindex = 1 + offset;
1✔
1871
      }
1872

1873
      if (reprObjs.executeQuery(map) != MS_SUCCESS || !layer->resultcache) {
82✔
1874
        msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR,
×
1875
                            "Collection items query failed.");
1876
        return MS_SUCCESS;
×
1877
      }
1878
    }
1879
  }
1880

1881
  const std::string extra_params = getExtraParameterString(map, layer);
94✔
1882

1883
  // build response object
1884
  if (!featureId) {
94✔
1885
    const char *id = layer->name;
92✔
1886
    char *id_encoded = msEncodeUrl(id); // free after use
92✔
1887

1888
    std::string extra_kvp = "&limit=" + std::to_string(limit);
92✔
1889
    extra_kvp += "&offset=" + std::to_string(offset);
184✔
1890

1891
    std::string other_extra_kvp;
1892
    if (crs)
92✔
1893
      other_extra_kvp += "&crs=" + std::string(crs);
4✔
1894
    const char *bbox = getRequestParameter(request, "bbox");
92✔
1895
    if (bbox)
92✔
1896
      other_extra_kvp += "&bbox=" + std::string(bbox);
10✔
1897
    const char *bboxCrs = getRequestParameter(request, "bbox-crs");
92✔
1898
    if (bboxCrs)
92✔
1899
      other_extra_kvp += "&bbox-crs=" + std::string(bboxCrs);
4✔
1900

1901
    other_extra_kvp += query_kvp;
1902

1903
    response = {{"type", "FeatureCollection"},
1904
                {"numberMatched", numberMatched},
1905
                {"numberReturned", layer->resultcache->numresults},
92✔
1906
                {"features", json::array()},
92✔
1907
                {"links",
1908
                 {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
96✔
1909
                   {"type", OGCAPI_MIMETYPE_GEOJSON},
1910
                   {"title", "Items for this collection as GeoJSON"},
1911
                   {"href", api_root + "/collections/" +
184✔
1912
                                std::string(id_encoded) + "/items?f=json" +
92✔
1913
                                extra_kvp + other_extra_kvp + extra_params}},
92✔
1914
                  {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
180✔
1915
                   {"type", OGCAPI_MIMETYPE_HTML},
1916
                   {"title", "Items for this collection as HTML"},
1917
                   {"href", api_root + "/collections/" +
184✔
1918
                                std::string(id_encoded) + "/items?f=html" +
92✔
1919
                                extra_kvp + other_extra_kvp + extra_params}}}}};
3,864✔
1920

1921
    if (offset + layer->resultcache->numresults < numberMatched) {
92✔
1922
      response["links"].push_back(
266✔
1923
          {{"rel", "next"},
1924
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
20✔
1925
                                                 : OGCAPI_MIMETYPE_HTML},
1926
           {"title", "next page"},
1927
           {"href",
1928
            api_root + "/collections/" + std::string(id_encoded) +
38✔
1929
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
38✔
1930
                "&limit=" + std::to_string(limit) +
57✔
1931
                "&offset=" + std::to_string(offset + limit) + other_extra_kvp +
38✔
1932
                extra_params}});
1933
    }
1934

1935
    if (offset > 0) {
92✔
1936
      response["links"].push_back(
14✔
1937
          {{"rel", "prev"},
1938
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
1✔
1939
                                                 : OGCAPI_MIMETYPE_HTML},
1940
           {"title", "previous page"},
1941
           {"href",
1942
            api_root + "/collections/" + std::string(id_encoded) +
2✔
1943
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
2✔
1944
                "&limit=" + std::to_string(limit) +
3✔
1945
                "&offset=" + std::to_string(MS_MAX(0, (offset - limit))) +
2✔
1946
                other_extra_kvp + extra_params}});
1✔
1947
    }
1948

1949
    extraHeaders["OGC-NumberReturned"].push_back(
92✔
1950
        std::to_string(layer->resultcache->numresults));
184✔
1951
    extraHeaders["OGC-NumberMatched"].push_back(std::to_string(numberMatched));
184✔
1952
    std::vector<std::string> linksHeaders;
1953
    for (auto &link : response["links"]) {
388✔
1954
      linksHeaders.push_back("<" + link["href"].get<std::string>() +
408✔
1955
                             ">; rel=\"" + link["rel"].get<std::string>() +
612✔
1956
                             "\"; title=\"" + link["title"].get<std::string>() +
612✔
1957
                             "\"; type=\"" + link["type"].get<std::string>() +
612✔
1958
                             "\"");
1959
    }
1960
    extraHeaders["Link"] = std::move(linksHeaders);
92✔
1961

1962
    msFree(id_encoded); // done
92✔
1963
  }
92✔
1964

1965
  // features (items)
1966
  {
1967
    shapeObj shape;
94✔
1968
    msInitShape(&shape);
94✔
1969

1970
    // we piggyback on GML configuration
1971
    gmlItemListObj *items = msGMLGetItems(layer, "AG");
94✔
1972
    gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
94✔
1973

1974
    if (!items || !constants) {
94✔
1975
      msGMLFreeItems(items);
×
1976
      msGMLFreeConstants(constants);
×
1977
      msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
1978
                          "Error fetching layer attribute metadata.");
1979
      return MS_SUCCESS;
×
1980
    }
1981

1982
    const int geometry_precision = getGeometryPrecision(map, layer);
94✔
1983

1984
    for (int i = 0; i < layer->resultcache->numresults; i++) {
274✔
1985
      if (layer->resultcache->results[i].shape) {
180✔
1986
        msCopyShape(layer->resultcache->results[i].shape, &shape);
×
1987
      } else {
1988
        int status =
1989
            msLayerGetShape(layer, &shape, &(layer->resultcache->results[i]));
180✔
1990
        if (status != MS_SUCCESS) {
180✔
1991
          msGMLFreeItems(items);
×
1992
          msGMLFreeConstants(constants);
×
1993
          msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Error fetching feature.");
×
1994
          return MS_SUCCESS;
×
1995
        }
1996

1997
        if (reprObjs.reprojector) {
180✔
1998
          status = msProjectShapeEx(reprObjs.reprojector, &shape);
173✔
1999
          if (status != MS_SUCCESS) {
173✔
2000
            msGMLFreeItems(items);
×
2001
            msGMLFreeConstants(constants);
×
2002
            msFreeShape(&shape);
×
2003
            msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
2004
                                "Error reprojecting feature.");
2005
            return MS_SUCCESS;
×
2006
          }
2007
        }
2008
      }
2009

2010
      try {
2011
        json feature = getFeature(layer, &shape, items, constants,
2012
                                  geometry_precision, outputCrsAxisInverted);
180✔
2013
        if (featureId) {
180✔
2014
          response = std::move(feature);
2✔
2015
        } else {
2016
          response["features"].emplace_back(std::move(feature));
178✔
2017
        }
2018
      } catch (const std::runtime_error &e) {
×
2019
        msGMLFreeItems(items);
×
2020
        msGMLFreeConstants(constants);
×
2021
        msFreeShape(&shape);
×
2022
        msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
2023
                            "Error getting feature. " + std::string(e.what()));
×
2024
        return MS_SUCCESS;
2025
      }
×
2026

2027
      msFreeShape(&shape); // next
180✔
2028
    }
2029

2030
    msGMLFreeItems(items); // clean up
94✔
2031
    msGMLFreeConstants(constants);
94✔
2032
  }
94✔
2033

2034
  // extend the response a bit for templating (HERE)
2035
  if (format == OGCAPIFormat::HTML) {
94✔
2036
    const char *title = getCollectionTitle(layer);
2037
    const char *id = layer->name;
4✔
2038
    response["collection"] = {{"id", id}, {"title", title ? title : ""}};
36✔
2039
  }
2040

2041
  if (featureId) {
94✔
2042
    const char *id = layer->name;
2✔
2043
    char *id_encoded = msEncodeUrl(id); // free after use
2✔
2044

2045
    response["links"] = {
2✔
2046
        {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
2✔
2047
         {"type", OGCAPI_MIMETYPE_GEOJSON},
2048
         {"title", "This document as GeoJSON"},
2049
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
2050
                      "/items/" + featureId + "?f=json" + extra_params}},
2✔
2051
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
4✔
2052
         {"type", OGCAPI_MIMETYPE_HTML},
2053
         {"title", "This document as HTML"},
2054
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
2055
                      "/items/" + featureId + "?f=html" + extra_params}},
2✔
2056
        {{"rel", "collection"},
2057
         {"type", OGCAPI_MIMETYPE_JSON},
2058
         {"title", "This collection as JSON"},
2059
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
2060
                      "?f=json" + extra_params}},
2✔
2061
        {{"rel", "collection"},
2062
         {"type", OGCAPI_MIMETYPE_HTML},
2063
         {"title", "This collection as HTML"},
2064
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
2065
                      "?f=html" + extra_params}}};
106✔
2066

2067
    msFree(id_encoded);
2✔
2068

2069
    outputResponse(
2✔
2070
        map, request,
2071
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
2072
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM, response, extraHeaders);
2073
  } else {
2074
    outputResponse(
180✔
2075
        map, request,
2076
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
2077
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS, response, extraHeaders);
2078
  }
2079
  return MS_SUCCESS;
2080
}
5,340✔
2081

2082
static std::pair<const char *, const char *>
2083
convertGmlTypeToJsonSchema(const char *gmlType) {
59✔
2084
  const char *type = "string";
2085
  const char *format = nullptr;
2086
  if (gmlType) {
59✔
2087
    if (strcasecmp(gmlType, "Character") == 0)
31✔
2088
      type = "string";
2089
    else if (strcasecmp(gmlType, "Date") == 0) {
31✔
2090
      type = "string";
2091
      format = "date";
2092
    } else if (strcasecmp(gmlType, "Time") == 0) {
31✔
2093
      type = "string";
2094
      format = "time";
2095
    } else if (strcasecmp(gmlType, "DateTime") == 0) {
31✔
2096
      type = "string";
2097
      format = "date-time";
2098
    } else if (strcasecmp(gmlType, "Integer") == 0 ||
21✔
2099
               strcasecmp(gmlType, "Long") == 0)
13✔
2100
      type = "integer";
2101
    else if (strcasecmp(gmlType, "Real") == 0)
13✔
2102
      type = "number";
2103
    else if (strcasecmp(gmlType, "Boolean") == 0)
×
2104
      type = "boolean";
2105
  }
2106
  return {type, format};
59✔
2107
}
2108

2109
std::pair<const char *, const char *>
2110
getItemTypeAndFormat(const layerObj *layer, const std::string &item) {
59✔
2111
  const char *pszType =
2112
      msOWSLookupMetadata(&(layer->metadata), "OFG", (item + "_type").c_str());
59✔
2113
  return convertGmlTypeToJsonSchema(pszType);
59✔
2114
}
2115

2116
static int processCollectionQueryablesRequest(mapObj *map,
7✔
2117
                                              const cgiRequestObj *request,
2118
                                              const char *collectionId,
2119
                                              OGCAPIFormat format) {
2120

2121
  // find the right layer
2122
  const int iLayer = findLayerIndex(map, collectionId);
7✔
2123

2124
  if (iLayer < 0) { // invalid collectionId
7✔
2125
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
1✔
2126
    return MS_SUCCESS;
1✔
2127
  }
2128

2129
  layerObj *layer = map->layers[iLayer];
6✔
2130
  if (!includeLayer(map, layer)) {
6✔
2131
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
2132
    return MS_SUCCESS;
×
2133
  }
2134

2135
  auto allowedParameters = getExtraParameters(map, layer);
6✔
2136
  allowedParameters.insert("f");
6✔
2137

2138
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
6✔
2139
    return MS_SUCCESS;
2140
  }
2141

2142
  bool error = false;
2143
  const std::vector<std::string> queryableItems =
2144
      msOOGCAPIGetLayerQueryables(layer, allowedParameters, error);
4✔
2145
  if (error) {
4✔
2146
    return MS_SUCCESS;
2147
  }
2148

2149
  std::unique_ptr<char, decltype(&msFree)> id_encoded(msEncodeUrl(collectionId),
2150
                                                      msFree);
4✔
2151

2152
  json response = {
2153
      {"$schema", "https://json-schema.org/draft/2020-12/schema"},
2154
      {"$id", msOGCAPIGetApiRootUrl(map, request) + "/collections/" +
12✔
2155
                  std::string(id_encoded.get()) + "/queryables"},
4✔
2156
      {"type", "object"},
2157
      {"title", getCollectionTitle(layer)},
4✔
2158
      {"description", getCollectionDescription(layer)},
4✔
2159
      {"properties", json::object()},
4✔
2160
      {"additionalProperties", false},
2161
  };
88✔
2162

2163
  const char *geometryName = getGeometryName(layer);
4✔
2164
  if (geometryName[0]) {
4✔
2165
    const char *geometryFormat = getGeometryFormat(layer);
4✔
2166

2167
    json j = {
2168
        {"title", geometryName},
2169
        {"description", "The geometry of the collection."},
2170
        {"x-ogc-role", "primary-geometry"},
2171
        {"format", geometryFormat},
2172
    };
52✔
2173
    response["properties"][geometryName] = j;
8✔
2174
  }
2175

2176
  const char *featureIdItem =
2177
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
4✔
2178
  for (const std::string &item : queryableItems) {
21✔
2179
    json j;
2180
    const auto name = getItemAliasOrName(layer, item);
17✔
2181
    const auto [type, format] = getItemTypeAndFormat(layer, item);
17✔
2182
    j["description"] = "Queryable item '" + name + "'";
51✔
2183
    j["type"] = type;
17✔
2184
    if (format)
17✔
2185
      j["format"] = format;
6✔
2186
    if (featureIdItem && featureIdItem == item)
17✔
2187
      j["x-ogc-role"] = "id";
×
2188
    response["properties"][name] = j;
34✔
2189
  }
2190

2191
  std::map<std::string, std::vector<std::string>> extraHeaders;
2192
  outputResponse(
7✔
2193
      map, request,
2194
      format == OGCAPIFormat::JSON ? OGCAPIFormat::JSONSchema : format,
2195
      OGCAPI_TEMPLATE_HTML_COLLECTION_QUERYABLES, response, extraHeaders);
2196

2197
  return MS_SUCCESS;
2198
}
152✔
2199

2200
namespace {
2201
struct Returnable {
2202
  std::string item;
2203
  std::string user_name;
2204
  std::string type;
2205
  std::string format;
2206
};
2207
} // namespace
2208

2209
/** Return the list of returnable items as (alias, type, format) tuples */
2210
static std::vector<Returnable> msOOGCAPIGetLayerReturnables(layerObj *layer,
2✔
2211
                                                            bool &error) {
2212
  error = false;
2✔
2213
  std::vector<Returnable> returnableItems;
2214

2215
  // we piggyback on GML configuration
2216
  gmlItemListObj *items = msGMLGetItems(layer, "AG");
2✔
2217
  gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
2✔
2218

2219
  if (!items || !constants) {
2✔
2220
    msGMLFreeItems(items);
×
2221
    msGMLFreeConstants(constants);
×
2222
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR,
×
2223
                        "Error fetching layer attribute metadata.");
2224
    error = true;
×
2225
    return returnableItems;
×
2226
  }
2227

2228
  for (int i = 0; i < items->numitems; ++i) {
20✔
2229
    Returnable returnable;
2230
    returnable.item = items->items[i].name;
18✔
2231
    returnable.user_name =
2232
        items->items[i].alias ? items->items[i].alias : items->items[i].name;
18✔
2233
    const auto [type, format] =
2234
        getItemTypeAndFormat(layer, items->items[i].name);
18✔
2235
    returnable.type = type;
18✔
2236
    if (format)
18✔
2237
      returnable.format = format;
2238
    returnableItems.push_back(std::move(returnable));
2239
  }
18✔
2240

2241
  for (int i = 0; i < constants->numconstants; ++i) {
2✔
2242
    Returnable returnable;
2243
    returnable.user_name = constants->constants[i].name;
×
2244
    const auto [type, format] =
2245
        convertGmlTypeToJsonSchema(constants->constants[i].type);
×
2246
    returnable.type = type;
×
2247
    if (format)
×
2248
      returnable.format = format;
2249
    returnableItems.push_back(std::move(returnable));
2250
  }
×
2251

2252
  msGMLFreeItems(items);
2✔
2253
  msGMLFreeConstants(constants);
2✔
2254
  return returnableItems;
2255
}
×
2256

2257
static int processCollectionSchemaRequest(mapObj *map,
4✔
2258
                                          const cgiRequestObj *request,
2259
                                          const char *collectionId,
2260
                                          OGCAPIFormat format) {
2261

2262
  // find the right layer
2263
  const int iLayer = findLayerIndex(map, collectionId);
4✔
2264

2265
  if (iLayer < 0) { // invalid collectionId
4✔
2266
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
1✔
2267
    return MS_SUCCESS;
1✔
2268
  }
2269

2270
  layerObj *layer = map->layers[iLayer];
3✔
2271
  if (!includeLayer(map, layer)) {
3✔
2272
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
2273
    return MS_SUCCESS;
×
2274
  }
2275

2276
  if (msLayerOpen(layer) != MS_SUCCESS ||
6✔
2277
      msLayerGetItems(layer) != MS_SUCCESS) {
3✔
2278
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot get layer fields");
×
2279
    return MS_SUCCESS;
×
2280
  }
2281

2282
  auto allowedParameters = getExtraParameters(map, layer);
3✔
2283
  allowedParameters.insert("f");
3✔
2284

2285
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
3✔
2286
    return MS_SUCCESS;
2287
  }
2288

2289
  bool error = false;
2290
  const auto returnableItems = msOOGCAPIGetLayerReturnables(layer, error);
2✔
2291
  if (error) {
2✔
2292
    return MS_SUCCESS;
2293
  }
2294

2295
  std::unique_ptr<char, decltype(&msFree)> id_encoded(msEncodeUrl(collectionId),
2296
                                                      msFree);
2✔
2297

2298
  json response = {
2299
      {"$schema", "https://json-schema.org/draft/2020-12/schema"},
2300
      {"$id", msOGCAPIGetApiRootUrl(map, request) + "/collections/" +
6✔
2301
                  std::string(id_encoded.get()) + "/schema"},
2✔
2302
      {"type", "object"},
2303
      {"title", getCollectionTitle(layer)},
2✔
2304
      {"description", getCollectionDescription(layer)},
2✔
2305
      {"properties", json::object()},
2✔
2306
      {"additionalProperties", false},
2307
  };
44✔
2308

2309
  const char *geometryName = getGeometryName(layer);
2✔
2310
  if (geometryName[0]) {
2✔
2311
    const char *geometryFormat = getGeometryFormat(layer);
2✔
2312

2313
    json j = {
2314
        {"title", geometryName},
2315
        {"description", "The geometry of the collection."},
2316
        {"x-ogc-role", "primary-geometry"},
2317
        {"format", geometryFormat},
2318
    };
26✔
2319
    response["properties"][geometryName] = j;
4✔
2320
  }
2321

2322
  const char *featureIdItem =
2323
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
2✔
2324
  for (const auto &item : returnableItems) {
20✔
2325
    json j;
2326
    j["description"] = "Returnable item '" + item.user_name + "'";
36✔
2327
    j["type"] = item.type;
54✔
2328
    if (!item.format.empty())
18✔
2329
      j["format"] = item.format;
6✔
2330
    if (featureIdItem && featureIdItem == item.item)
18✔
2331
      j["x-ogc-role"] = "id";
×
2332
    response["properties"][item.user_name] = j;
36✔
2333
  }
2334

2335
  std::map<std::string, std::vector<std::string>> extraHeaders;
2336
  outputResponse(
3✔
2337
      map, request,
2338
      format == OGCAPIFormat::JSON ? OGCAPIFormat::JSONSchema : format,
2339
      OGCAPI_TEMPLATE_HTML_COLLECTION_SCHEMA, response, extraHeaders);
2340

2341
  return MS_SUCCESS;
2342
}
76✔
2343

2344
static int processCollectionSortablesRequest(mapObj *map,
6✔
2345
                                             const cgiRequestObj *request,
2346
                                             const char *collectionId,
2347
                                             OGCAPIFormat format) {
2348

2349
  // find the right layer
2350
  const int iLayer = findLayerIndex(map, collectionId);
6✔
2351

2352
  if (iLayer < 0) { // invalid collectionId
6✔
2353
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
1✔
2354
    return MS_SUCCESS;
1✔
2355
  }
2356

2357
  layerObj *layer = map->layers[iLayer];
5✔
2358
  if (!includeLayer(map, layer)) {
5✔
2359
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
2360
    return MS_SUCCESS;
×
2361
  }
2362

2363
  if (msLayerOpen(layer) != MS_SUCCESS ||
10✔
2364
      msLayerGetItems(layer) != MS_SUCCESS) {
5✔
2365
    msOGCAPIOutputError(OGCAPI_SERVER_ERROR, "Cannot get layer fields");
×
2366
    return MS_SUCCESS;
×
2367
  }
2368

2369
  auto allowedParameters = getExtraParameters(map, layer);
5✔
2370
  allowedParameters.insert("f");
5✔
2371

2372
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
5✔
2373
    return MS_SUCCESS;
2374
  }
2375

2376
  bool error = false;
2377
  const auto sortableItems =
2378
      msOOGCAPIGetLayerSortables(layer, allowedParameters, error);
4✔
2379
  if (error) {
4✔
2380
    return MS_SUCCESS;
2381
  }
2382

2383
  std::unique_ptr<char, decltype(&msFree)> id_encoded(msEncodeUrl(collectionId),
2384
                                                      msFree);
4✔
2385

2386
  json response = {
2387
      {"$schema", "https://json-schema.org/draft/2020-12/schema"},
2388
      {"$id", msOGCAPIGetApiRootUrl(map, request) + "/collections/" +
12✔
2389
                  std::string(id_encoded.get()) + "/sortables"},
4✔
2390
      {"type", "object"},
2391
      {"title", getCollectionTitle(layer)},
4✔
2392
      {"description", getCollectionDescription(layer)},
4✔
2393
      {"properties", json::object()},
4✔
2394
      {"additionalProperties", false},
2395
  };
88✔
2396

2397
  const char *featureIdItem =
2398
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
4✔
2399
  for (const std::string &item : sortableItems) {
15✔
2400
    json j;
2401
    const auto name = getItemAliasOrName(layer, item);
11✔
2402
    j["description"] = "Sortable item '" + name + "'";
22✔
2403
    const auto [type, format] = getItemTypeAndFormat(layer, item);
11✔
2404
    j["type"] = type;
11✔
2405
    if (format)
11✔
2406
      j["format"] = format;
6✔
2407
    if (featureIdItem && featureIdItem == item)
11✔
2408
      j["x-ogc-role"] = "id";
×
2409
    response["properties"][name] = j;
22✔
2410
  }
2411

2412
  std::map<std::string, std::vector<std::string>> extraHeaders;
2413
  outputResponse(
7✔
2414
      map, request,
2415
      format == OGCAPIFormat::JSON ? OGCAPIFormat::JSONSchema : format,
2416
      OGCAPI_TEMPLATE_HTML_COLLECTION_SORTABLES, response, extraHeaders);
2417

2418
  return MS_SUCCESS;
2419
}
100✔
2420

2421
static int processCollectionRequest(mapObj *map, cgiRequestObj *request,
7✔
2422
                                    const char *collectionId,
2423
                                    OGCAPIFormat format) {
2424
  json response;
2425
  int l;
2426

2427
  for (l = 0; l < map->numlayers; l++) {
9✔
2428
    if (strcmp(map->layers[l]->name, collectionId) == 0)
9✔
2429
      break; // match
2430
  }
2431

2432
  if (l == map->numlayers) { // invalid collectionId
7✔
2433
    msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
2434
    return MS_SUCCESS;
×
2435
  }
2436

2437
  layerObj *layer = map->layers[l];
7✔
2438
  auto allowedParameters = getExtraParameters(map, layer);
7✔
2439
  allowedParameters.insert("f");
7✔
2440
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
7✔
2441
    return MS_SUCCESS;
2442
  }
2443

2444
  try {
2445
    response =
2446
        getCollection(map, layer, format, msOGCAPIGetApiRootUrl(map, request));
12✔
2447
    if (response.is_null()) { // same as not found
6✔
2448
      msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
2449
      return MS_SUCCESS;
×
2450
    }
2451
  } catch (const std::runtime_error &e) {
×
2452
    msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
2453
                        "Error getting collection. " + std::string(e.what()));
×
2454
    return MS_SUCCESS;
2455
  }
×
2456

2457
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTION,
6✔
2458
                 response);
2459
  return MS_SUCCESS;
6✔
2460
}
2461

2462
static int processCollectionsRequest(mapObj *map, cgiRequestObj *request,
5✔
2463
                                     OGCAPIFormat format) {
2464
  json response;
2465
  int i;
2466

2467
  auto allowedParameters = getExtraParameters(map, nullptr);
5✔
2468
  allowedParameters.insert("f");
5✔
2469
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
5✔
2470
    return MS_SUCCESS;
2471
  }
2472

2473
  // define api root url
2474
  std::string api_root = msOGCAPIGetApiRootUrl(map, request);
4✔
2475
  const std::string extra_params = getExtraParameterString(map, nullptr);
4✔
2476

2477
  // build response object
2478
  response = {{"links",
2479
               {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
5✔
2480
                 {"type", OGCAPI_MIMETYPE_JSON},
2481
                 {"title", "This document as JSON"},
2482
                 {"href", api_root + "/collections?f=json" + extra_params}},
4✔
2483
                {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
7✔
2484
                 {"type", OGCAPI_MIMETYPE_HTML},
2485
                 {"title", "This document as HTML"},
2486
                 {"href", api_root + "/collections?f=html" + extra_params}}}},
4✔
2487
              {"collections", json::array()}};
136✔
2488

2489
  for (i = 0; i < map->numlayers; i++) {
14✔
2490
    try {
2491
      json collection = getCollection(map, map->layers[i], format, api_root);
10✔
2492
      if (!collection.is_null())
10✔
2493
        response["collections"].push_back(collection);
10✔
2494
    } catch (const std::runtime_error &e) {
×
2495
      msOGCAPIOutputError(OGCAPI_CONFIG_ERROR,
×
2496
                          "Error getting collection." + std::string(e.what()));
×
2497
      return MS_SUCCESS;
2498
    }
×
2499
  }
2500

2501
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTIONS,
4✔
2502
                 response);
2503
  return MS_SUCCESS;
4✔
2504
}
172✔
2505

2506
static int processApiRequest(mapObj *map, cgiRequestObj *request,
2✔
2507
                             OGCAPIFormat format) {
2508
  // Strongly inspired from
2509
  // https://github.com/geopython/pygeoapi/blob/master/pygeoapi/openapi.py
2510

2511
  auto allowedParameters = getExtraParameters(map, nullptr);
2✔
2512
  allowedParameters.insert("f");
2✔
2513
  if (!msOOGCAPICheckQueryParameters(map, request, allowedParameters)) {
2✔
2514
    return MS_SUCCESS;
2515
  }
2516

2517
  json response;
2518

2519
  response = {
2520
      {"openapi", "3.0.2"},
2521
      {"tags", json::array()},
1✔
2522
  };
7✔
2523

2524
  response["info"] = {
1✔
2525
      {"title", getTitle(map)},
1✔
2526
      {"version", getWebMetadata(map, "A", "version", "1.0.0")},
1✔
2527
  };
7✔
2528

2529
  for (const char *item : {"description", "termsOfService"}) {
3✔
2530
    const char *value = getWebMetadata(map, "AO", item, nullptr);
2✔
2531
    if (value) {
2✔
2532
      response["info"][item] = value;
4✔
2533
    }
2534
  }
2535

2536
  for (const auto &pair : {
3✔
2537
           std::make_pair("name", "contactperson"),
2538
           std::make_pair("url", "contacturl"),
2539
           std::make_pair("email", "contactelectronicmailaddress"),
2540
       }) {
4✔
2541
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
3✔
2542
    if (value) {
3✔
2543
      response["info"]["contact"][pair.first] = value;
6✔
2544
    }
2545
  }
2546

2547
  for (const auto &pair : {
2✔
2548
           std::make_pair("name", "licensename"),
2549
           std::make_pair("url", "licenseurl"),
2550
       }) {
3✔
2551
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
2✔
2552
    if (value) {
2✔
2553
      response["info"]["license"][pair.first] = value;
×
2554
    }
2555
  }
2556

2557
  {
2558
    const char *value = getWebMetadata(map, "AO", "keywords", nullptr);
1✔
2559
    if (value) {
1✔
2560
      response["info"]["x-keywords"] = value;
2✔
2561
    }
2562
  }
2563

2564
  json server;
2565
  server["url"] = msOGCAPIGetApiRootUrl(map, request);
3✔
2566

2567
  {
2568
    const char *value =
2569
        getWebMetadata(map, "AO", "server_description", nullptr);
1✔
2570
    if (value) {
1✔
2571
      server["description"] = value;
2✔
2572
    }
2573
  }
2574
  response["servers"].push_back(server);
1✔
2575

2576
  const std::string oapif_schema_base_url = msOWSGetSchemasLocation(map);
1✔
2577
  const std::string oapif_yaml_url = oapif_schema_base_url +
2578
                                     "/ogcapi/features/part1/1.0/openapi/"
2579
                                     "ogcapi-features-1.yaml";
1✔
2580
  const std::string oapif_part2_yaml_url = oapif_schema_base_url +
2581
                                           "/ogcapi/features/part2/1.0/openapi/"
2582
                                           "ogcapi-features-2.yaml";
1✔
2583
  const std::string oapif_part3_yaml_url = oapif_schema_base_url +
2584
                                           "/ogcapi/features/part3/1.0/openapi/"
2585
                                           "ogcapi-features-3.yaml";
1✔
2586

2587
  json paths;
2588

2589
  paths["/"]["get"] = {
1✔
2590
      {"summary", "Landing page"},
2591
      {"description", "Landing page"},
2592
      {"tags", {"server"}},
2593
      {"operationId", "getLandingPage"},
2594
      {"parameters",
2595
       {
2596
           {{"$ref", "#/components/parameters/f"}},
2597
       }},
2598
      {"responses",
2599
       {{"200",
2600
         {{"$ref", oapif_yaml_url + "#/components/responses/LandingPage"}}},
1✔
2601
        {"400",
2602
         {{"$ref",
2603
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2604
        {"500",
2605
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2606

2607
  paths["/api"]["get"] = {
1✔
2608
      {"summary", "API documentation"},
2609
      {"description", "API documentation"},
2610
      {"tags", {"server"}},
2611
      {"operationId", "getOpenapi"},
2612
      {"parameters",
2613
       {
2614
           {{"$ref", "#/components/parameters/f"}},
2615
       }},
2616
      {"responses",
2617
       {{"200", {{"$ref", "#/components/responses/200"}}},
2618
        {"400",
2619
         {{"$ref",
2620
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2621
        {"default", {{"$ref", "#/components/responses/default"}}}}}};
42✔
2622

2623
  paths["/conformance"]["get"] = {
1✔
2624
      {"summary", "API conformance definition"},
2625
      {"description", "API conformance definition"},
2626
      {"tags", {"server"}},
2627
      {"operationId", "getConformanceDeclaration"},
2628
      {"parameters",
2629
       {
2630
           {{"$ref", "#/components/parameters/f"}},
2631
       }},
2632
      {"responses",
2633
       {{"200",
2634
         {{"$ref",
2635
           oapif_yaml_url + "#/components/responses/ConformanceDeclaration"}}},
1✔
2636
        {"400",
2637
         {{"$ref",
2638
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2639
        {"500",
2640
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2641

2642
  paths["/collections"]["get"] = {
1✔
2643
      {"summary", "Collections"},
2644
      {"description", "Collections"},
2645
      {"tags", {"server"}},
2646
      {"operationId", "getCollections"},
2647
      {"parameters",
2648
       {
2649
           {{"$ref", "#/components/parameters/f"}},
2650
       }},
2651
      {"responses",
2652
       {{"200",
2653
         {{"$ref", oapif_yaml_url + "#/components/responses/Collections"}}},
1✔
2654
        {"400",
2655
         {{"$ref",
2656
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
2657
        {"500",
2658
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
2659

2660
  for (int i = 0; i < map->numlayers; i++) {
6✔
2661
    layerObj *layer = map->layers[i];
5✔
2662
    if (!includeLayer(map, layer)) {
5✔
2663
      continue;
×
2664
    }
2665

2666
    json collection_get = {
2667
        {"summary",
2668
         std::string("Get ") + getCollectionTitle(layer) + " metadata"},
5✔
2669
        {"description", getCollectionDescription(layer)},
5✔
2670
        {"tags", {layer->name}},
5✔
2671
        {"operationId", "describe" + std::string(layer->name) + "Collection"},
5✔
2672
        {"parameters",
2673
         {
2674
             {{"$ref", "#/components/parameters/f"}},
2675
         }},
2676
        {"responses",
2677
         {{"200",
2678
           {{"$ref", oapif_yaml_url + "#/components/responses/Collection"}}},
5✔
2679
          {"400",
2680
           {{"$ref",
2681
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2682
          {"500",
2683
           {{"$ref",
2684
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
215✔
2685

2686
    std::string collectionNamePath("/collections/");
×
2687
    collectionNamePath += layer->name;
5✔
2688
    paths[collectionNamePath]["get"] = std::move(collection_get);
5✔
2689

2690
    // check metadata, layer then map
2691
    const char *max_limit_str =
2692
        msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
5✔
2693
    if (max_limit_str == nullptr)
5✔
2694
      max_limit_str =
2695
          msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
5✔
2696
    const int max_limit =
2697
        max_limit_str ? atoi(max_limit_str) : OGCAPI_MAX_LIMIT;
5✔
2698
    const int default_limit = getDefaultLimit(map, layer);
5✔
2699

2700
    json items_get = {
2701
        {"summary", std::string("Get ") + getCollectionTitle(layer) + " items"},
5✔
2702
        {"description", getCollectionDescription(layer)},
5✔
2703
        {"tags", {layer->name}},
2704
        {"operationId", "get" + std::string(layer->name) + "Features"},
5✔
2705
        {"parameters",
2706
         {
2707
             {{"$ref", "#/components/parameters/f"}},
2708
             {{"$ref", oapif_yaml_url + "#/components/parameters/bbox"}},
5✔
2709
             {{"$ref", oapif_yaml_url + "#/components/parameters/datetime"}},
5✔
2710
             {{"$ref",
2711
               oapif_part2_yaml_url + "#/components/parameters/bbox-crs"}},
5✔
2712
             {{"$ref", oapif_part2_yaml_url + "#/components/parameters/crs"}},
5✔
2713
             {{"$ref", "#/components/parameters/offset"}},
2714
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2715
             {{"$ref",
2716
               oapif_part3_yaml_url + "#/components/parameters/filter"}},
5✔
2717
             {{"$ref",
2718
               oapif_part3_yaml_url + "#/components/parameters/filter-lang"}},
5✔
2719
             {{"$ref",
2720
               oapif_part3_yaml_url + "#/components/parameters/filter-crs"}},
5✔
2721
         }},
2722
        {"responses",
2723
         {{"200",
2724
           {{"$ref", oapif_yaml_url + "#/components/responses/Features"}}},
5✔
2725
          {"400",
2726
           {{"$ref",
2727
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2728
          {"500",
2729
           {{"$ref",
2730
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
395✔
2731

2732
    json param_limit = {
2733
        {"name", "limit"},
2734
        {"in", "query"},
2735
        {"description", "The optional limit parameter limits the number of "
2736
                        "items that are presented in the response document."},
2737
        {"required", false},
2738
        {"schema",
2739
         {
2740
             {"type", "integer"},
2741
             {"minimum", 1},
2742
             {"maximum", max_limit},
2743
             {"default", default_limit},
2744
         }},
2745
        {"style", "form"},
2746
        {"explode", false},
2747
    };
170✔
2748
    items_get["parameters"].emplace_back(param_limit);
5✔
2749

2750
    json param_sortby = {
2751
        {"name", "sortby"},
2752
        {"in", "query"},
2753
        {"description",
2754
         "Optional list of properties by which to sort the items."},
2755
        {"required", false},
2756
        {"schema",
2757
         {
2758
             {"type", "array"},
2759
             {"minItems", 1},
2760
             {"items",
2761
              {
2762
                  {"type", "string"},
2763
                  {"pattern", "[+|-]?[A-Za-z_].*"},
2764
              }},
2765
         }},
2766
        {"style", "form"},
2767
        {"explode", false},
2768
    };
185✔
2769
    items_get["parameters"].emplace_back(param_sortby);
5✔
2770

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

2807
    std::string itemsPath(collectionNamePath + "/items");
5✔
2808
    paths[itemsPath]["get"] = std::move(items_get);
10✔
2809

2810
    json schema_get = {
2811
        {"summary",
2812
         std::string("Get ") + getCollectionTitle(layer) + " schema"},
5✔
2813
        {"description",
2814
         std::string("Get ") + getCollectionTitle(layer) + " schema"},
5✔
2815
        {"tags", {layer->name}},
2816
        {"operationId", "get" + std::string(layer->name) + "Schema"},
5✔
2817
        {"parameters",
2818
         {
2819
             {{"$ref", "#/components/parameters/f"}},
2820
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2821
         }},
2822
        {"responses",
2823
         {{"200",
2824
           {{"description", "The returnable properties of the collection."},
2825
            {"content",
2826
             {{"application/schema+json",
2827
               {{"schema", {{"type", "object"}}}}}}}}},
2828
          {"400",
2829
           {{"$ref",
2830
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2831
          {"500",
2832
           {{"$ref",
2833
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
295✔
2834

2835
    std::string schemaPath(collectionNamePath + "/schema");
5✔
2836
    paths[schemaPath]["get"] = std::move(schema_get);
10✔
2837

2838
    json queryables_get = {
2839
        {"summary",
2840
         std::string("Get ") + getCollectionTitle(layer) + " queryables"},
5✔
2841
        {"description",
2842
         std::string("Get ") + getCollectionTitle(layer) + " queryables"},
5✔
2843
        {"tags", {layer->name}},
2844
        {"operationId", "get" + std::string(layer->name) + "Queryables"},
5✔
2845
        {"parameters",
2846
         {
2847
             {{"$ref", "#/components/parameters/f"}},
2848
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2849
         }},
2850
        {"responses",
2851
         {{"200",
2852
           {{"description", "The queryable properties of the collection."},
2853
            {"content",
2854
             {{"application/schema+json",
2855
               {{"schema", {{"type", "object"}}}}}}}}},
2856
          {"400",
2857
           {{"$ref",
2858
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2859
          {"500",
2860
           {{"$ref",
2861
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
295✔
2862

2863
    std::string queryablesPath(collectionNamePath + "/queryables");
5✔
2864
    paths[queryablesPath]["get"] = std::move(queryables_get);
10✔
2865

2866
    json sortables_get = {
2867
        {"summary",
2868
         std::string("Get ") + getCollectionTitle(layer) + " sortables"},
5✔
2869
        {"description",
2870
         std::string("Get ") + getCollectionTitle(layer) + " sortables"},
5✔
2871
        {"tags", {layer->name}},
2872
        {"operationId", "get" + std::string(layer->name) + "Sortables"},
5✔
2873
        {"parameters",
2874
         {
2875
             {{"$ref", "#/components/parameters/f"}},
2876
             {{"$ref", "#/components/parameters/vendorSpecificParameters"}},
2877
         }},
2878
        {"responses",
2879
         {{"200",
2880
           {{"description", "The sortable properties of the collection."},
2881
            {"content",
2882
             {{"application/schema+json",
2883
               {{"schema", {{"type", "object"}}}}}}}}},
2884
          {"400",
2885
           {{"$ref",
2886
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2887
          {"500",
2888
           {{"$ref",
2889
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
295✔
2890

2891
    std::string sortablesPath(collectionNamePath + "/sortables");
5✔
2892
    paths[sortablesPath]["get"] = std::move(sortables_get);
10✔
2893

2894
    json feature_id_get = {
2895
        {"summary",
2896
         std::string("Get ") + getCollectionTitle(layer) + " item by id"},
5✔
2897
        {"description", getCollectionDescription(layer)},
5✔
2898
        {"tags", {layer->name}},
2899
        {"operationId", "get" + std::string(layer->name) + "Feature"},
5✔
2900
        {"parameters",
2901
         {
2902
             {{"$ref", "#/components/parameters/f"}},
2903
             {{"$ref", oapif_yaml_url + "#/components/parameters/featureId"}},
5✔
2904
         }},
2905
        {"responses",
2906
         {{"200",
2907
           {{"$ref", oapif_yaml_url + "#/components/responses/Feature"}}},
5✔
2908
          {"400",
2909
           {{"$ref",
2910
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
5✔
2911
          {"404",
2912
           {{"$ref", oapif_yaml_url + "#/components/responses/NotFound"}}},
5✔
2913
          {"500",
2914
           {{"$ref",
2915
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
265✔
2916
    std::string itemsFeatureIdPath(collectionNamePath + "/items/{featureId}");
5✔
2917
    paths[itemsFeatureIdPath]["get"] = std::move(feature_id_get);
10✔
2918
  }
5✔
2919

2920
  response["paths"] = std::move(paths);
2✔
2921

2922
  json components;
2923
  components["responses"]["200"] = {{"description", "successful operation"}};
5✔
2924
  components["responses"]["default"] = {
1✔
2925
      {"description", "unexpected error"},
2926
      {"content",
2927
       {{"application/json",
2928
         {{"schema",
2929
           {{"$ref", "https://schemas.opengis.net/ogcapi/common/part1/1.0/"
2930
                     "openapi/schemas/exception.yaml"}}}}}}}};
16✔
2931

2932
  json parameters;
2933
  parameters["f"] = {
1✔
2934
      {"name", "f"},
2935
      {"in", "query"},
2936
      {"description", "The optional f parameter indicates the output format "
2937
                      "which the server shall provide as part of the response "
2938
                      "document.  The default format is GeoJSON."},
2939
      {"required", false},
2940
      {"schema",
2941
       {{"type", "string"}, {"enum", {"json", "html"}}, {"default", "json"}}},
2942
      {"style", "form"},
2943
      {"explode", false},
2944
  };
33✔
2945

2946
  parameters["offset"] = {
1✔
2947
      {"name", "offset"},
2948
      {"in", "query"},
2949
      {"description",
2950
       "The optional offset parameter indicates the index within the result "
2951
       "set from which the server shall begin presenting results in the "
2952
       "response document.  The first element has an index of 0 (default)."},
2953
      {"required", false},
2954
      {"schema",
2955
       {
2956
           {"type", "integer"},
2957
           {"minimum", 0},
2958
           {"default", 0},
2959
       }},
2960
      {"style", "form"},
2961
      {"explode", false},
2962
  };
31✔
2963

2964
  parameters["vendorSpecificParameters"] = {
1✔
2965
      {"name", "vendorSpecificParameters"},
2966
      {"in", "query"},
2967
      {"description",
2968
       "Additional \"free-form\" parameters that are not explicitly defined"},
2969
      {"schema",
2970
       {
2971
           {"type", "object"},
2972
           {"additionalProperties", true},
2973
       }},
2974
      {"style", "form"},
2975
  };
22✔
2976

2977
  components["parameters"] = std::move(parameters);
2✔
2978

2979
  response["components"] = std::move(components);
2✔
2980

2981
  // TODO: "tags" array ?
2982

2983
  outputResponse(map, request,
3✔
2984
                 format == OGCAPIFormat::JSON ? OGCAPIFormat::OpenAPI_V3
2985
                                              : format,
2986
                 OGCAPI_TEMPLATE_HTML_OPENAPI, response);
2987
  return MS_SUCCESS;
2988
}
3,105✔
2989

2990
#endif
2991

2992
OGCAPIFormat msOGCAPIGetOutputFormat(const cgiRequestObj *request) {
168✔
2993
  OGCAPIFormat format; // all endpoints need a format
2994
  const char *p = getRequestParameter(request, "f");
168✔
2995

2996
  // if f= query parameter is not specified, use HTTP Accept header if available
2997
  if (p == nullptr) {
168✔
2998
    const char *accept = getenv("HTTP_ACCEPT");
2✔
2999
    if (accept) {
2✔
3000
      if (strcmp(accept, "*/*") == 0)
1✔
3001
        p = OGCAPI_MIMETYPE_JSON;
3002
      else
3003
        p = accept;
3004
    }
3005
  }
3006

3007
  if (p &&
167✔
3008
      (strcmp(p, "json") == 0 || strstr(p, OGCAPI_MIMETYPE_JSON) != nullptr ||
167✔
3009
       strstr(p, OGCAPI_MIMETYPE_GEOJSON) != nullptr ||
14✔
3010
       strstr(p, OGCAPI_MIMETYPE_OPENAPI_V3) != nullptr)) {
3011
    format = OGCAPIFormat::JSON;
3012
  } else if (p && (strcmp(p, "html") == 0 ||
14✔
3013
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
3014
    format = OGCAPIFormat::HTML;
3015
  } else if (p) {
3016
    std::string errorMsg("Unsupported format requested: ");
×
3017
    errorMsg += p;
3018
    msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
×
3019
    format = OGCAPIFormat::Invalid;
3020
  } else {
3021
    format = OGCAPIFormat::HTML; // default for now
3022
  }
3023

3024
  return format;
168✔
3025
}
3026

3027
int msOGCAPIDispatchRequest(mapObj *map, cgiRequestObj *request) {
154✔
3028
#ifdef USE_OGCAPI_SVR
3029

3030
  // make sure ogcapi requests are enabled for this map
3031
  int status = msOWSRequestIsEnabled(map, NULL, "AO", "OGCAPI", MS_FALSE);
154✔
3032
  if (status != MS_TRUE) {
154✔
3033
    msSetError(MS_OGCAPIERR, "OGC API requests are not enabled.",
×
3034
               "msCGIDispatchAPIRequest()");
3035
    return MS_FAILURE; // let normal error handling take over
×
3036
  }
3037

3038
  for (int i = 0; i < request->NumParams; i++) {
495✔
3039
    for (int j = i + 1; j < request->NumParams; j++) {
619✔
3040
      if (strcmp(request->ParamNames[i], request->ParamNames[j]) == 0) {
278✔
3041
        std::string errorMsg("Query parameter ");
1✔
3042
        errorMsg += request->ParamNames[i];
1✔
3043
        errorMsg += " is repeated";
3044
        msOGCAPIOutputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
2✔
3045
        return MS_SUCCESS;
3046
      }
3047
    }
3048
  }
3049

3050
  const OGCAPIFormat format = msOGCAPIGetOutputFormat(request);
153✔
3051

3052
  if (format == OGCAPIFormat::Invalid) {
153✔
3053
    return MS_SUCCESS; // avoid any downstream MapServer processing
3054
  }
3055

3056
  if (request->api_path_length == 2) {
153✔
3057

3058
    return processLandingRequest(map, request, format);
7✔
3059

3060
  } else if (request->api_path_length == 3) {
3061

3062
    if (strcmp(request->api_path[2], "conformance") == 0) {
10✔
3063
      return processConformanceRequest(map, request, format);
3✔
3064
    } else if (strcmp(request->api_path[2], "conformance.html") == 0) {
7✔
3065
      return processConformanceRequest(map, request, OGCAPIFormat::HTML);
×
3066
    } else if (strcmp(request->api_path[2], "collections") == 0) {
7✔
3067
      return processCollectionsRequest(map, request, format);
5✔
3068
    } else if (strcmp(request->api_path[2], "collections.html") == 0) {
2✔
3069
      return processCollectionsRequest(map, request, OGCAPIFormat::HTML);
×
3070
    } else if (strcmp(request->api_path[2], "api") == 0) {
2✔
3071
      return processApiRequest(map, request, format);
2✔
3072
    }
3073

3074
  } else if (request->api_path_length == 4) {
3075

3076
    if (strcmp(request->api_path[2], "collections") ==
7✔
3077
        0) { // next argument (3) is collectionId
3078
      return processCollectionRequest(map, request, request->api_path[3],
7✔
3079
                                      format);
7✔
3080
    }
3081

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

3084
    if (strcmp(request->api_path[2], "collections") == 0 &&
126✔
3085
        strcmp(request->api_path[4], "items") ==
126✔
3086
            0) { // middle argument (3) is the collectionId
3087
      return processCollectionItemsRequest(map, request, request->api_path[3],
109✔
3088
                                           NULL, format);
109✔
3089
    } else if (strcmp(request->api_path[2], "collections") == 0 &&
17✔
3090
               strcmp(request->api_path[4], "queryables") == 0) {
17✔
3091
      return processCollectionQueryablesRequest(map, request,
7✔
3092
                                                request->api_path[3], format);
7✔
3093
    } else if (strcmp(request->api_path[2], "collections") == 0 &&
10✔
3094
               strcmp(request->api_path[4], "schema") == 0) {
10✔
3095
      return processCollectionSchemaRequest(map, request, request->api_path[3],
4✔
3096
                                            format);
4✔
3097
    } else if (strcmp(request->api_path[2], "collections") == 0 &&
6✔
3098
               strcmp(request->api_path[4], "sortables") == 0) {
6✔
3099
      return processCollectionSortablesRequest(map, request,
6✔
3100
                                               request->api_path[3], format);
6✔
3101
    }
3102
  } else if (request->api_path_length == 6) {
3103

3104
    if (strcmp(request->api_path[2], "collections") == 0 &&
3✔
3105
        strcmp(request->api_path[4], "items") ==
3✔
3106
            0) { // middle argument (3) is the collectionId, last argument (5)
3107
                 // is featureId
3108
      return processCollectionItemsRequest(map, request, request->api_path[3],
3✔
3109
                                           request->api_path[5], format);
3✔
3110
    }
3111
  }
3112

3113
  msOGCAPIOutputError(OGCAPI_NOT_FOUND_ERROR, "Invalid API path.");
×
3114
  return MS_SUCCESS; // avoid any downstream MapServer processing
×
3115
#else
3116
  msSetError(MS_OGCAPIERR, "OGC API server support is not enabled.",
3117
             "msOGCAPIDispatchRequest()");
3118
  return MS_FAILURE;
3119
#endif
3120
}
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